From 2871c438890d6989a18491406d74a873eee255bc Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 5 Mar 2025 17:59:47 -0800 Subject: [PATCH 01/31] WIP: Start of AI builder extension --- .../microsoft.azd.ai.builder/.gitignore | 25 ++ .../microsoft.azd.ai.builder/README.md | 3 + .../microsoft.azd.ai.builder/build.sh | 89 +++++++ .../microsoft.azd.ai.builder/extension.yaml | 13 + .../internal/cmd/root.go | 28 +++ .../internal/cmd/start.go | 215 +++++++++++++++++ .../internal/cmd/version.go | 27 +++ .../internal/pkg/qna/decision_tree.go | 227 ++++++++++++++++++ .../microsoft.azd.ai.builder/main.go | 30 +++ 9 files changed, 657 insertions(+) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/.gitignore create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/README.md create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/build.sh create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/version.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/main.go diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/.gitignore b/cli/azd/extensions/microsoft.azd.ai.builder/.gitignore new file mode 100644 index 00000000000..6f72f892618 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/README.md b/cli/azd/extensions/microsoft.azd.ai.builder/README.md new file mode 100644 index 00000000000..09d2eb6b8e8 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/README.md @@ -0,0 +1,3 @@ +# `azd` Demo Extension + +An AZD Demo extension diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/build.sh b/cli/azd/extensions/microsoft.azd.ai.builder/build.sh new file mode 100644 index 00000000000..4a8688cf91c --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/build.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Get the directory of the script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Change to the script directory +cd "$SCRIPT_DIR" || exit + +# Parse named input parameters: --app-name and --version +while [[ "$#" -gt 0 ]]; do + case $1 in + --app-name) + APP_NAME="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + *) + echo "Unknown parameter passed: $1" + exit 1 + ;; + esac +done + +if [ -z "$APP_NAME" ]; then + echo "Error: --app-name parameter is required" + exit 1 +fi + +if [ -z "$VERSION" ]; then + echo "Error: --version parameter is required" + exit 1 +fi + +# Create a safe version of APP_NAME replacing dots with dashes +APP_NAME_SAFE="${APP_NAME//./-}" + +# Define output directory +OUTPUT_DIR="$SCRIPT_DIR/bin" + +# Create output and target directories if they don't exist +mkdir -p "$OUTPUT_DIR" +mkdir -p "$HOME/.azd/extensions/$APP_NAME" # new: create destination directory + +# Get Git commit hash and build date +COMMIT=$(git rev-parse HEAD) +BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +# List of OS and architecture combinations +PLATFORMS=( + "windows/amd64" + "windows/arm64" + "darwin/amd64" + "darwin/arm64" + "linux/amd64" + "linux/arm64" +) + +APP_PATH="github.com/azure/azure-dev/cli/azd/extensions/$APP_NAME/internal/cmd" + +# Loop through platforms and build +for PLATFORM in "${PLATFORMS[@]}"; do + OS=$(echo "$PLATFORM" | cut -d'/' -f1) + ARCH=$(echo "$PLATFORM" | cut -d'/' -f2) + + OUTPUT_NAME="$OUTPUT_DIR/$APP_NAME_SAFE-$OS-$ARCH" + + if [ "$OS" = "windows" ]; then + OUTPUT_NAME+='.exe' + fi + + echo "Building for $OS/$ARCH..." + GOOS=$OS GOARCH=$ARCH go build \ + -ldflags="-X '$APP_PATH.Version=$VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" \ + -o "$OUTPUT_NAME" + + if [ $? -ne 0 ]; then + echo "An error occurred while building for $OS/$ARCH" + exit 1 + fi + + # new: copy built binary to extensions folder + cp "$OUTPUT_NAME" "$HOME/.azd/extensions/$APP_NAME" +done + +echo "Build completed successfully!" +echo "Binaries are located in the $OUTPUT_DIR directory." diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml b/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml new file mode 100644 index 00000000000..56226bfb6b1 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/extension.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=../extension.schema.json +id: microsoft.azd.ai.builder +namespace: ai +displayName: AZD AI Builder +description: This extension provides custom commands for building AI applications using Azure Developer CLI. +usage: azd ai builder [options] +version: 0.1.0 +capabilities: + - custom-commands +examples: + - name: start + description: Provides a guided experience for building AI applications using Azure Developer CLI. + usage: azd ai builder start \ No newline at end of file diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go new file mode 100644 index 00000000000..16bdb921086 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func NewRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "azd ai builder [options]", + Short: "Runs the Azure AI Builder.", + SilenceUsage: true, + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + } + + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") + + rootCmd.AddCommand(newStartCommand()) + rootCmd.AddCommand(newVersionCommand()) + + return rootCmd +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go new file mode 100644 index 00000000000..33b16968a4f --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/spf13/cobra" +) + +type scenarioInput struct { + SelectedScenario string `json:"selectedScenario,omitempty"` + Rag ragScenarioInput `json:"rag,omitempty"` + Agent AgentScenarioInput `json:"agent,omitempty"` + ModelExploration modelExplorationInput `json:"modelExploration,omitempty"` +} + +type ragScenarioInput struct { + UseCustomData bool `json:"useCustomData,omitempty"` + DataTypes []string `json:"dataTypes,omitempty"` + DataLocations []string `json:"dataLocations,omitempty"` + InteractionTypes []string `json:"interactionTypes,omitempty"` + ModelSelection string `json:"modelSelection,omitempty"` +} + +type AgentScenarioInput struct { + Tasks []string `json:"tasks,omitempty"` + DataTypes []string `json:"dataTypes,omitempty"` + InteractionTypes []string `json:"interactionTypes,omitempty"` +} + +type modelExplorationInput struct { + Tasks []string `json:"tasks,omitempty"` +} + +func newStartCommand() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Get the context of the AZD project & environment.", + RunE: func(cmd *cobra.Command, args []string) error { + // Create a new context that includes the AZD access token + ctx := azdext.WithAccessToken(cmd.Context()) + + // Create a new AZD client + azdClient, err := azdext.NewAzdClient() + if err != nil { + return fmt.Errorf("failed to create azd client: %w", err) + } + + defer azdClient.Close() + + scenarioData := scenarioInput{} + + // Build up list of questions + config := qna.DecisionTreeConfig{ + Questions: map[string]qna.Question{ + "root": { + Text: "What type of AI scenario are you building?", + Type: qna.SingleSelect, + Binding: &scenarioData.SelectedScenario, + Choices: []qna.Choice{ + {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, + {Label: "AI Agent", Value: "agent"}, + {Label: "Explore AI Models", Value: "ai-model"}, + }, + Branches: map[any]string{ + "rag": "rag-data-type", + "agent": "agent-tasks", + "ai-model": "model-exploration", + }, + }, + "rag-custom-data": { + Text: "Does your application require custom data?", + Type: qna.BooleanInput, + Binding: &scenarioData.Rag.UseCustomData, + Branches: map[any]string{ + true: "rag-data-type", + false: "rag-user-interaction", + }, + }, + "rag-data-type": { + Text: "What type of data are you using?", + Type: qna.MultiSelect, + Binding: &scenarioData.Rag.DataTypes, + Choices: []qna.Choice{ + {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, + {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, + {Label: "Videos", Value: "videos"}, + {Label: "Images", Value: "images"}, + {Label: "Audio", Value: "audio"}, + }, + Branches: map[any]string{ + "*": "rag-data-location", + }, + }, + "rag-data-location": { + Text: "Where is your data located?", + Type: qna.MultiSelect, + Binding: &scenarioData.Rag.DataLocations, + Choices: []qna.Choice{ + {Label: "Azure Blob Storage", Value: "rag-blob-storage"}, + {Label: "Azure SQL Database", Value: "rag-databases"}, + {Label: "Local file system", Value: "rag-local-file-system"}, + }, + Branches: map[any]string{ + "*": "rag-user-interaction", + }, + }, + "rag-user-interaction": { + Text: "How do you want users to interact with the data?", + Type: qna.MultiSelect, + Binding: &scenarioData.Rag.InteractionTypes, + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "rag-chatbot"}, + {Label: "Web Application", Value: "rag-web"}, + {Label: "Mobile Application", Value: "rag-mobile-app"}, + }, + Branches: map[any]string{ + "*": "rag-model-selection", + }, + }, + "rag-model-selection": { + Text: "Do you know which models you want to use?", + Type: qna.SingleSelect, + Binding: &scenarioData.Rag.ModelSelection, + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "rag-choose-model"}, + {Label: "Help me choose", Value: "rag-guide-model"}, + {Label: "Yes, I have some models in mind", Value: "rag-user-models"}, + }, + }, + "agent-tasks": { + Text: "What tasks do you want the AI agent to perform?", + Type: qna.MultiSelect, + Binding: &scenarioData.Agent.Tasks, + Choices: []qna.Choice{ + {Label: "Custom Function Calling", Value: "custom-function-calling"}, + {Label: "Integrate with Open API based services", Value: "openapi"}, + {Label: "Run Azure Functions", Value: "azure-functions"}, + }, + Branches: map[any]string{ + "*": "agent-data-types", + }, + }, + "agent-data-types": { + Text: "Where will the agent retrieve it's data from?", + Type: qna.MultiSelect, + Binding: &scenarioData.Agent.DataTypes, + Choices: []qna.Choice{ + {Label: "Azure Blob Storage", Value: "agent-blob-storage"}, + {Label: "Azure SQL Database", Value: "agent-databases"}, + {Label: "Local file system", Value: "agent-local-file-system"}, + }, + Branches: map[any]string{ + "*": "agent-interaction", + }, + }, + "agent-interaction": { + Text: "How do you want users to interact with the agent?", + Type: qna.MultiSelect, + Binding: &scenarioData.Agent.InteractionTypes, + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "agent-chatbot"}, + {Label: "Web Application", Value: "agent-web"}, + {Label: "Mobile Application", Value: "agent-mobile-app"}, + {Label: "Message Queue", Value: "agent-message-queue"}, + }, + }, + "model-exploration": { + Text: "What types of tasks should the AI models perform?", + Type: qna.MultiSelect, + Binding: &scenarioData.ModelExploration.Tasks, + Choices: []qna.Choice{ + {Label: "Text Generation", Value: "text-generation"}, + {Label: "Image Generation", Value: "image-generation"}, + {Label: "Audio Generation", Value: "audio-generation"}, + {Label: "Video Generation", Value: "video-generation"}, + {Label: "Text Classification", Value: "text-classification"}, + {Label: "Image Classification", Value: "image-classification"}, + {Label: "Audio Classification", Value: "audio-classification"}, + {Label: "Video Classification", Value: "video-classification"}, + {Label: "Text Summarization", Value: "text-summarization"}, + }, + }, + }, + } + + fmt.Println("Welcome to the AI Builder CLI!") + fmt.Println("This tool will help you build an AI scenario using Azure services.") + fmt.Println("Please answer the following questions to get started.") + fmt.Println() + + decisionTree := qna.NewDecisionTree(azdClient, config) + if err := decisionTree.Run(ctx); err != nil { + return fmt.Errorf("failed to run decision tree: %w", err) + } + + jsonBytes, err := json.MarshalIndent(scenarioData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal scenario data: %w", err) + } + + fmt.Println() + fmt.Println("Captured scenario data:") + fmt.Println() + fmt.Println(string(jsonBytes)) + + return nil + }, + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/version.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/version.go new file mode 100644 index 00000000000..715323a6c5c --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/version.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + // Populated at build time + Version = "dev" // Default value for development builds + Commit = "none" + BuildDate = "unknown" +) + +func newVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints the version of the application", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s\nCommit: %s\nBuild Date: %s\n", Version, Commit, BuildDate) + }, + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go new file mode 100644 index 00000000000..48c92208fb2 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -0,0 +1,227 @@ +package qna + +import ( + "context" + "errors" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) + +// DecisionTree represents the entire decision tree structure. +type DecisionTree struct { + azdClient *azdext.AzdClient + config DecisionTreeConfig +} + +type DecisionTreeConfig struct { + Questions map[string]Question `json:"questions"` + End EndNode `json:"end"` +} + +func NewDecisionTree(azdClient *azdext.AzdClient, config DecisionTreeConfig) *DecisionTree { + return &DecisionTree{ + azdClient: azdClient, + config: config, + } +} + +// Question represents a single prompt in the decision tree. +type Question struct { + ID string `json:"id"` + Text string `json:"text"` + Type QuestionType `json:"type"` + Choices []Choice `json:"choices,omitempty"` + Branches map[any]string `json:"branches"` + Binding any `json:"-"` +} + +type Choice struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// EndNode represents the terminal state of the decision tree. +type EndNode struct { + Message string `json:"message"` +} + +// QuestionType defines the allowed input types. +type QuestionType string + +const ( + TextInput QuestionType = "text" + BooleanInput QuestionType = "boolean" + SingleSelect QuestionType = "single_select" + MultiSelect QuestionType = "multi_select" +) + +func (t *DecisionTree) Run(ctx context.Context) error { + rootQuestion, has := t.config.Questions["root"] + if !has { + return errors.New("root question not found") + } + + return t.askQuestion(ctx, rootQuestion) +} + +func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error { + var err error + var value any + + switch question.Type { + case TextInput: + value, err = t.askTextQuestion(ctx, question) + case BooleanInput: + value, err = t.askBooleanQuestion(ctx, question) + case SingleSelect: + value, err = t.askSingleSelectQuestion(ctx, question) + case MultiSelect: + value, err = t.askMultiSelectQuestion(ctx, question) + default: + return errors.New("unsupported question type") + } + + if err != nil { + return fmt.Errorf("failed to ask question: %w", err) + } + + var nextQuestionKey string + + // No branches means no further questions + if len(question.Branches) == 0 { + return nil + } + + // Handle the case where the branch is a wildcard + if branchValue, has := question.Branches["*"]; has { + nextQuestionKey = branchValue + } else { + // Handle the case where the branch is based on the user's response + switch v := value.(type) { + case string: + if branch, has := question.Branches[v]; has { + nextQuestionKey = branch + } + case bool: + if branch, has := question.Branches[v]; has { + nextQuestionKey = branch + } + case []string: + for _, selectedValue := range v { + if branch, has := question.Branches[selectedValue]; has { + nextQuestionKey = branch + break + } + } + default: + return errors.New("unsupported value type") + } + } + + if nextQuestion, has := t.config.Questions[nextQuestionKey]; has { + return t.askQuestion(ctx, nextQuestion) + } + + if nextQuestionKey == "end" { + return nil + } + + return fmt.Errorf("next question not found: %s", nextQuestionKey) +} + +func (t *DecisionTree) askTextQuestion(ctx context.Context, question Question) (any, error) { + promptResponse, err := t.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{ + Options: &azdext.PromptOptions{ + Message: question.Text, + Required: true, + }, + }) + if err != nil { + return nil, err + } + + if stringPtr, ok := question.Binding.(*string); ok { + *stringPtr = promptResponse.Value + } + + return promptResponse.Value, nil +} + +func (t *DecisionTree) askBooleanQuestion(ctx context.Context, question Question) (any, error) { + defaultValue := true + confirmResponse, err := t.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: question.Text, + DefaultValue: &defaultValue, + }, + }) + if err != nil { + return nil, err + } + + if boolPtr, ok := question.Binding.(*bool); ok { + *boolPtr = *confirmResponse.Value + } + + return confirmResponse.Value, nil +} + +func (t *DecisionTree) askSingleSelectQuestion(ctx context.Context, question Question) (any, error) { + choices := make([]*azdext.SelectChoice, len(question.Choices)) + for i, choice := range question.Choices { + choices[i] = &azdext.SelectChoice{ + Label: choice.Label, + Value: choice.Value, + } + } + + selectResponse, err := t.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: question.Text, + Choices: choices, + }, + }) + if err != nil { + return nil, err + } + + selectedChoice := question.Choices[*selectResponse.Value] + + if stringPtr, ok := question.Binding.(*string); ok { + *stringPtr = selectedChoice.Value + } + + return selectedChoice.Value, nil +} + +func (t *DecisionTree) askMultiSelectQuestion(ctx context.Context, question Question) (any, error) { + choices := make([]*azdext.MultiSelectChoice, len(question.Choices)) + for i, choice := range question.Choices { + choices[i] = &azdext.MultiSelectChoice{ + Label: choice.Label, + Value: choice.Value, + } + } + + multiSelectResponse, err := t.azdClient.Prompt().MultiSelect(ctx, &azdext.MultiSelectRequest{ + Options: &azdext.MultiSelectOptions{ + Message: question.Text, + Choices: choices, + }, + }) + if err != nil { + return nil, err + } + + selectedChoices := make([]string, len(multiSelectResponse.Values)) + for i, value := range multiSelectResponse.Values { + selectedChoices[i] = value.Value + } + + if stringSlicePtr, ok := question.Binding.(*[]string); ok { + *stringSlicePtr = selectedChoices + } + + return selectedChoices, nil +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/main.go b/cli/azd/extensions/microsoft.azd.ai.builder/main.go new file mode 100644 index 00000000000..a2b2b81eaeb --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/main.go @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "context" + "os" + + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd" + "github.com/fatih/color" +) + +func init() { + forceColorVal, has := os.LookupEnv("FORCE_COLOR") + if has && forceColorVal == "1" { + color.NoColor = false + } +} + +func main() { + // Execute the root command + ctx := context.Background() + rootCmd := cmd.NewRootCommand() + + if err := rootCmd.ExecuteContext(ctx); err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } +} From 2442f56ad76154ba13932429ed6e0867fd8af545 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 6 Mar 2025 17:29:35 -0800 Subject: [PATCH 02/31] Adds questions & resource selection prompts --- .../internal/cmd/start.go | 463 ++++++++++++++---- .../internal/pkg/qna/decision_tree.go | 175 ++----- .../internal/pkg/qna/prompt.go | 167 +++++++ 3 files changed, 569 insertions(+), 236 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 33b16968a4f..c4779125023 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -4,38 +4,99 @@ package cmd import ( + "context" "encoding/json" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) type scenarioInput struct { - SelectedScenario string `json:"selectedScenario,omitempty"` - Rag ragScenarioInput `json:"rag,omitempty"` - Agent AgentScenarioInput `json:"agent,omitempty"` - ModelExploration modelExplorationInput `json:"modelExploration,omitempty"` -} + SelectedScenario string `json:"selectedScenario,omitempty"` -type ragScenarioInput struct { UseCustomData bool `json:"useCustomData,omitempty"` DataTypes []string `json:"dataTypes,omitempty"` DataLocations []string `json:"dataLocations,omitempty"` InteractionTypes []string `json:"interactionTypes,omitempty"` ModelSelection string `json:"modelSelection,omitempty"` + LocalFilePath string `json:"localFilePath,omitempty"` + DatabaseType string `json:"databaseType,omitempty"` + StorageAccountId string `json:"storageAccountId,omitempty"` + DatabaseId string `json:"databaseId,omitempty"` + MessagingType string `json:"messageType,omitempty"` + MessagingId string `json:"messagingId,omitempty"` + ModelTasks []string `json:"modelTasks,omitempty"` + AppType string `json:"appType,omitempty"` + AppId string `json:"appId,omitempty"` } -type AgentScenarioInput struct { - Tasks []string `json:"tasks,omitempty"` - DataTypes []string `json:"dataTypes,omitempty"` - InteractionTypes []string `json:"interactionTypes,omitempty"` +type resourceTypeConfig struct { + ResourceType string + ResourceTypeDisplayName string + Kinds []string } -type modelExplorationInput struct { - Tasks []string `json:"tasks,omitempty"` -} +var ( + dbResourceMap = map[string]resourceTypeConfig{ + "db.cosmos": { + ResourceType: "Microsoft.DocumentDB/databaseAccounts", + ResourceTypeDisplayName: "Cosmos Database Account", + Kinds: []string{"GlobalDocumentDB"}, + }, + "db.mongo": { + ResourceType: "Microsoft.DocumentDB/databaseAccounts", + ResourceTypeDisplayName: "MongoDB Database Account", + Kinds: []string{"MongoDB"}, + }, + "db.postgres": { + ResourceType: "Microsoft.DBforPostgreSQL/flexibleServers", + ResourceTypeDisplayName: "PostgreSQL Server", + }, + "db.mysql": { + ResourceType: "Microsoft.DBforMySQL/flexibleServers", + ResourceTypeDisplayName: "MySQL Server", + }, + "db.redis": { + ResourceType: "Microsoft.Cache/Redis", + ResourceTypeDisplayName: "Redis Cache", + }, + } + + messagingResourceMap = map[string]resourceTypeConfig{ + "messaging.eventhubs": { + ResourceType: "Microsoft.EventHub/namespaces", + ResourceTypeDisplayName: "Event Hub Namespace", + }, + "messaging.servicebus": { + ResourceType: "Microsoft.ServiceBus/namespaces", + ResourceTypeDisplayName: "Service Bus Namespace", + }, + } + + appResourceMap = map[string]resourceTypeConfig{ + "webapp": { + ResourceType: "Microsoft.Web/sites", + ResourceTypeDisplayName: "Web App", + Kinds: []string{"app"}, + }, + "containerapp": { + ResourceType: "Microsoft.App/containerApps", + ResourceTypeDisplayName: "Container App", + }, + "functionapp": { + ResourceType: "Microsoft.Web/sites", + ResourceTypeDisplayName: "Function App", + Kinds: []string{"functionapp"}, + }, + "staticwebapp": { + ResourceType: "Microsoft.Web/staticSites", + ResourceTypeDisplayName: "Static Web App", + }, + } +) func newStartCommand() *cobra.Command { return &cobra.Command{ @@ -53,137 +114,329 @@ func newStartCommand() *cobra.Command { defer azdClient.Close() + _, err = azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) + } + + envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) + } + + envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ + Name: envResponse.Environment.Name, + }) + if err != nil { + return fmt.Errorf("failed to get environment values: %w", err) + } + + envValueMap := make(map[string]string) + for _, value := range envValues.KeyValues { + envValueMap[value.Key] = value.Value + } + + azureContext := &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], + }, + Resources: []string{}, + } + scenarioData := scenarioInput{} // Build up list of questions config := qna.DecisionTreeConfig{ Questions: map[string]qna.Question{ "root": { - Text: "What type of AI scenario are you building?", - Type: qna.SingleSelect, Binding: &scenarioData.SelectedScenario, - Choices: []qna.Choice{ - {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, - {Label: "AI Agent", Value: "agent"}, - {Label: "Explore AI Models", Value: "ai-model"}, + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "What type of AI scenario are you building?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, + {Label: "AI Agent", Value: "agent"}, + {Label: "Explore AI Models", Value: "ai-model"}, + }, }, Branches: map[any]string{ - "rag": "rag-data-type", + "rag": "use-custom-data", "agent": "agent-tasks", "ai-model": "model-exploration", }, }, - "rag-custom-data": { - Text: "Does your application require custom data?", - Type: qna.BooleanInput, - Binding: &scenarioData.Rag.UseCustomData, - Branches: map[any]string{ - true: "rag-data-type", - false: "rag-user-interaction", + "use-custom-data": { + Binding: &scenarioData.UseCustomData, + BeforeAsk: func(ctx context.Context, q *qna.Question) error { + switch scenarioData.SelectedScenario { + case "rag": + q.Branches = map[any]string{ + true: "choose-data-types", + false: "rag-user-interaction", + } + case "agent": + q.Branches = map[any]string{ + true: "choose-data-types", + false: "agent-tasks", + } + } + + return nil + }, + Prompt: &qna.ConfirmPrompt{ + Client: azdClient, + Message: "Does your application require custom data?", + DefaultValue: to.Ptr(true), + }, + }, + "choose-data-types": { + Binding: &scenarioData.DataTypes, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "What type of data are you using?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, + {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, + {Label: "Videos", Value: "videos"}, + {Label: "Images", Value: "images"}, + {Label: "Audio", Value: "audio"}, + }, }, + Next: "data-location", }, - "rag-data-type": { - Text: "What type of data are you using?", - Type: qna.MultiSelect, - Binding: &scenarioData.Rag.DataTypes, - Choices: []qna.Choice{ - {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, - {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, - {Label: "Videos", Value: "videos"}, - {Label: "Images", Value: "images"}, - {Label: "Audio", Value: "audio"}, + "data-location": { + Binding: &scenarioData.DataLocations, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "Where is your data located?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Azure Blob Storage", Value: "blob-storage"}, + {Label: "Azure Database", Value: "databases"}, + {Label: "Local file system", Value: "local-file-system"}, + }, }, Branches: map[any]string{ - "*": "rag-data-location", + "blob-storage": "choose-storage", + "databases": "choose-database-type", + "local-file-system": "local-file-system", + }, + BeforeAsk: func(ctx context.Context, q *qna.Question) error { + switch scenarioData.SelectedScenario { + case "rag": + q.Next = "rag-user-interaction" + case "agent": + q.Next = "agent-interaction" + } + return nil }, }, - "rag-data-location": { - Text: "Where is your data located?", - Type: qna.MultiSelect, - Binding: &scenarioData.Rag.DataLocations, - Choices: []qna.Choice{ - {Label: "Azure Blob Storage", Value: "rag-blob-storage"}, - {Label: "Azure SQL Database", Value: "rag-databases"}, - {Label: "Local file system", Value: "rag-local-file-system"}, + "choose-storage": { + Binding: &scenarioData.StorageAccountId, + Prompt: &qna.SubscriptionResourcePrompt{ + Client: azdClient, + ResourceType: "Microsoft.Storage/storageAccounts", + ResourceTypeDisplayName: "Storage Account", + AzureContext: azureContext, }, - Branches: map[any]string{ - "*": "rag-user-interaction", + }, + "choose-database-type": { + Binding: &scenarioData.DatabaseType, + Prompt: &qna.SingleSelectPrompt{ + Message: "Which type of database?", + Client: azdClient, + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "CosmosDB", Value: "db.cosmos"}, + {Label: "PostgreSQL", Value: "db.postgres"}, + {Label: "MySQL", Value: "db.mysql"}, + {Label: "Redis", Value: "db.redis"}, + {Label: "MongoDB", Value: "db.mongo"}, + }, + }, + Next: "choose-database-resource", + }, + "choose-database-resource": { + Binding: &scenarioData.DatabaseId, + Prompt: &qna.SubscriptionResourcePrompt{ + Client: azdClient, + AzureContext: azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := dbResourceMap[scenarioData.DatabaseType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + scenarioData.DatabaseType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, + }, + }, + "local-file-system": { + Binding: &scenarioData.LocalFilePath, + Prompt: &qna.TextPrompt{ + Client: azdClient, + Message: "Path to the local files", }, }, "rag-user-interaction": { - Text: "How do you want users to interact with the data?", - Type: qna.MultiSelect, - Binding: &scenarioData.Rag.InteractionTypes, - Choices: []qna.Choice{ - {Label: "Chatbot", Value: "rag-chatbot"}, - {Label: "Web Application", Value: "rag-web"}, - {Label: "Mobile Application", Value: "rag-mobile-app"}, + Binding: &scenarioData.InteractionTypes, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "How do you want users to interact with the data?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "chatbot"}, + {Label: "Web Application", Value: "webapp"}, + }, + }, + Branches: map[any]string{ + "chatbot": "choose-app-type", + "webapp": "choose-app-type", + }, + Next: "model-selection", + }, + "agent-interaction": { + Binding: &scenarioData.InteractionTypes, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "How do you want users to interact with the agent?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "chatbot"}, + {Label: "Web Application", Value: "webapp"}, + {Label: "Message Queue", Value: "messaging"}, + }, }, Branches: map[any]string{ - "*": "rag-model-selection", + "chatbot": "choose-app-type", + "webapp": "choose-app-type", + "messaging": "choose-app-type", }, + Next: "model-selection", }, - "rag-model-selection": { - Text: "Do you know which models you want to use?", - Type: qna.SingleSelect, - Binding: &scenarioData.Rag.ModelSelection, - Choices: []qna.Choice{ - {Label: "Choose for me", Value: "rag-choose-model"}, - {Label: "Help me choose", Value: "rag-guide-model"}, - {Label: "Yes, I have some models in mind", Value: "rag-user-models"}, + "model-selection": { + Binding: &scenarioData.ModelSelection, + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "Do you want to know what type(s) of model(s) you would like to use?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-model"}, + {Label: "Help me choose", Value: "guide-model"}, + {Label: "Yes, I have some models in mind", Value: "user-models"}, + }, }, }, "agent-tasks": { - Text: "What tasks do you want the AI agent to perform?", - Type: qna.MultiSelect, - Binding: &scenarioData.Agent.Tasks, - Choices: []qna.Choice{ - {Label: "Custom Function Calling", Value: "custom-function-calling"}, - {Label: "Integrate with Open API based services", Value: "openapi"}, - {Label: "Run Azure Functions", Value: "azure-functions"}, + Binding: &scenarioData.ModelTasks, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "What tasks do you want the AI agent to perform?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Custom Function Calling", Value: "custom-function-calling"}, + {Label: "Integrate with Open API based services", Value: "openapi"}, + {Label: "Run Azure Functions", Value: "azure-functions"}, + }, }, - Branches: map[any]string{ - "*": "agent-data-types", + Next: "use-custom-data", + }, + "choose-app-type": { + Binding: &scenarioData.AppType, + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "What type of application do you want to build?", + Choices: []qna.Choice{ + {Label: "App Service", Value: "webapp"}, + {Label: "Container App", Value: "containerapp"}, + {Label: "Function App", Value: "functionapp"}, + {Label: "Static Web App", Value: "staticwebapp"}, + }, }, + Next: "choose-app-resource", }, - "agent-data-types": { - Text: "Where will the agent retrieve it's data from?", - Type: qna.MultiSelect, - Binding: &scenarioData.Agent.DataTypes, - Choices: []qna.Choice{ - {Label: "Azure Blob Storage", Value: "agent-blob-storage"}, - {Label: "Azure SQL Database", Value: "agent-databases"}, - {Label: "Local file system", Value: "agent-local-file-system"}, + "choose-app-resource": { + Binding: &scenarioData.AppId, + Prompt: &qna.SubscriptionResourcePrompt{ + Client: azdClient, + AzureContext: azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := appResourceMap[scenarioData.AppType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + scenarioData.AppType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, }, - Branches: map[any]string{ - "*": "agent-interaction", + }, + "choose-messaging-type": { + Binding: &scenarioData.MessagingType, + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "Which messaging service do you want to use?", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Azure Service Bus", Value: "messaging.eventhubs"}, + {Label: "Azure Event Hubs", Value: "messaging.servicebus"}, + }, }, + Next: "choose-messaging-resource", }, - "agent-interaction": { - Text: "How do you want users to interact with the agent?", - Type: qna.MultiSelect, - Binding: &scenarioData.Agent.InteractionTypes, - Choices: []qna.Choice{ - {Label: "Chatbot", Value: "agent-chatbot"}, - {Label: "Web Application", Value: "agent-web"}, - {Label: "Mobile Application", Value: "agent-mobile-app"}, - {Label: "Message Queue", Value: "agent-message-queue"}, + "choose-messaging-resource": { + Binding: &scenarioData.MessagingId, + Prompt: &qna.SubscriptionResourcePrompt{ + Client: azdClient, + AzureContext: azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := messagingResourceMap[scenarioData.MessagingType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + scenarioData.MessagingType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, }, }, "model-exploration": { - Text: "What types of tasks should the AI models perform?", - Type: qna.MultiSelect, - Binding: &scenarioData.ModelExploration.Tasks, - Choices: []qna.Choice{ - {Label: "Text Generation", Value: "text-generation"}, - {Label: "Image Generation", Value: "image-generation"}, - {Label: "Audio Generation", Value: "audio-generation"}, - {Label: "Video Generation", Value: "video-generation"}, - {Label: "Text Classification", Value: "text-classification"}, - {Label: "Image Classification", Value: "image-classification"}, - {Label: "Audio Classification", Value: "audio-classification"}, - {Label: "Video Classification", Value: "video-classification"}, - {Label: "Text Summarization", Value: "text-summarization"}, + Binding: &scenarioData.ModelTasks, + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "What type of tasks should the AI models perform?", + Choices: []qna.Choice{ + {Label: "Text Generation", Value: "text-generation"}, + {Label: "Image Generation", Value: "image-generation"}, + {Label: "Audio Generation", Value: "audio-generation"}, + {Label: "Video Generation", Value: "video-generation"}, + {Label: "Text Classification", Value: "text-classification"}, + {Label: "Image Classification", Value: "image-classification"}, + {Label: "Audio Classification", Value: "audio-classification"}, + {Label: "Video Classification", Value: "video-classification"}, + {Label: "Text Summarization", Value: "text-summarization"}, + }, }, }, }, diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 48c92208fb2..11de13b47b2 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package qna import ( @@ -26,14 +29,18 @@ func NewDecisionTree(azdClient *azdext.AzdClient, config DecisionTreeConfig) *De } } +type Prompt interface { + Ask(ctx context.Context, question Question) (any, error) +} + // Question represents a single prompt in the decision tree. type Question struct { - ID string `json:"id"` - Text string `json:"text"` - Type QuestionType `json:"type"` - Choices []Choice `json:"choices,omitempty"` - Branches map[any]string `json:"branches"` - Binding any `json:"-"` + ID string `json:"id"` + Branches map[any]string `json:"branches"` + Next string `json:"next"` + Binding any `json:"-"` + Prompt Prompt `json:"prompt,omitempty"` + BeforeAsk func(ctx context.Context, question *Question) error } type Choice struct { @@ -66,38 +73,25 @@ func (t *DecisionTree) Run(ctx context.Context) error { } func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error { - var err error - var value any - - switch question.Type { - case TextInput: - value, err = t.askTextQuestion(ctx, question) - case BooleanInput: - value, err = t.askBooleanQuestion(ctx, question) - case SingleSelect: - value, err = t.askSingleSelectQuestion(ctx, question) - case MultiSelect: - value, err = t.askMultiSelectQuestion(ctx, question) - default: - return errors.New("unsupported question type") + if question.Prompt == nil { + return errors.New("question prompt is nil") } + if question.BeforeAsk != nil { + if err := question.BeforeAsk(ctx, &question); err != nil { + return fmt.Errorf("before ask function failed: %w", err) + } + } + + value, err := question.Prompt.Ask(ctx, question) if err != nil { return fmt.Errorf("failed to ask question: %w", err) } var nextQuestionKey string - // No branches means no further questions - if len(question.Branches) == 0 { - return nil - } - - // Handle the case where the branch is a wildcard - if branchValue, has := question.Branches["*"]; has { - nextQuestionKey = branchValue - } else { - // Handle the case where the branch is based on the user's response + // Handle the case where the branch is based on the user's response + if len(question.Branches) > 0 { switch v := value.(type) { case string: if branch, has := question.Branches[v]; has { @@ -108,10 +102,20 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error nextQuestionKey = branch } case []string: + // Handle multi-select case for _, selectedValue := range v { - if branch, has := question.Branches[selectedValue]; has { - nextQuestionKey = branch - break + branch, has := question.Branches[selectedValue] + if !has { + return fmt.Errorf("branch not found for selected value: %s", selectedValue) + } + + question, has := t.config.Questions[branch] + if !has { + return fmt.Errorf("question not found for branch: %s", branch) + } + + if err = t.askQuestion(ctx, question); err != nil { + return fmt.Errorf("failed to ask question: %w", err) } } default: @@ -119,109 +123,18 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error } } - if nextQuestion, has := t.config.Questions[nextQuestionKey]; has { - return t.askQuestion(ctx, nextQuestion) + if nextQuestionKey == "" { + nextQuestionKey = question.Next } - if nextQuestionKey == "end" { + if nextQuestionKey == "" || nextQuestionKey == "end" { return nil } - return fmt.Errorf("next question not found: %s", nextQuestionKey) -} - -func (t *DecisionTree) askTextQuestion(ctx context.Context, question Question) (any, error) { - promptResponse, err := t.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{ - Options: &azdext.PromptOptions{ - Message: question.Text, - Required: true, - }, - }) - if err != nil { - return nil, err - } - - if stringPtr, ok := question.Binding.(*string); ok { - *stringPtr = promptResponse.Value - } - - return promptResponse.Value, nil -} - -func (t *DecisionTree) askBooleanQuestion(ctx context.Context, question Question) (any, error) { - defaultValue := true - confirmResponse, err := t.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ - Options: &azdext.ConfirmOptions{ - Message: question.Text, - DefaultValue: &defaultValue, - }, - }) - if err != nil { - return nil, err - } - - if boolPtr, ok := question.Binding.(*bool); ok { - *boolPtr = *confirmResponse.Value - } - - return confirmResponse.Value, nil -} - -func (t *DecisionTree) askSingleSelectQuestion(ctx context.Context, question Question) (any, error) { - choices := make([]*azdext.SelectChoice, len(question.Choices)) - for i, choice := range question.Choices { - choices[i] = &azdext.SelectChoice{ - Label: choice.Label, - Value: choice.Value, - } - } - - selectResponse, err := t.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ - Options: &azdext.SelectOptions{ - Message: question.Text, - Choices: choices, - }, - }) - if err != nil { - return nil, err - } - - selectedChoice := question.Choices[*selectResponse.Value] - - if stringPtr, ok := question.Binding.(*string); ok { - *stringPtr = selectedChoice.Value - } - - return selectedChoice.Value, nil -} - -func (t *DecisionTree) askMultiSelectQuestion(ctx context.Context, question Question) (any, error) { - choices := make([]*azdext.MultiSelectChoice, len(question.Choices)) - for i, choice := range question.Choices { - choices[i] = &azdext.MultiSelectChoice{ - Label: choice.Label, - Value: choice.Value, - } - } - - multiSelectResponse, err := t.azdClient.Prompt().MultiSelect(ctx, &azdext.MultiSelectRequest{ - Options: &azdext.MultiSelectOptions{ - Message: question.Text, - Choices: choices, - }, - }) - if err != nil { - return nil, err - } - - selectedChoices := make([]string, len(multiSelectResponse.Values)) - for i, value := range multiSelectResponse.Values { - selectedChoices[i] = value.Value - } - - if stringSlicePtr, ok := question.Binding.(*[]string); ok { - *stringSlicePtr = selectedChoices + nextQuestion, has := t.config.Questions[nextQuestionKey] + if !has { + return fmt.Errorf("next question not found: %s", nextQuestionKey) } - return selectedChoices, nil + return t.askQuestion(ctx, nextQuestion) } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go new file mode 100644 index 00000000000..284ae9adaa2 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package qna + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) + +type TextPrompt struct { + Message string + Client *azdext.AzdClient +} + +func (p *TextPrompt) Ask(ctx context.Context, question Question) (any, error) { + promptResponse, err := p.Client.Prompt().Prompt(ctx, &azdext.PromptRequest{ + Options: &azdext.PromptOptions{ + Message: p.Message, + Required: true, + }, + }) + if err != nil { + return nil, err + } + + if stringPtr, ok := question.Binding.(*string); ok { + *stringPtr = promptResponse.Value + } + + return promptResponse.Value, nil +} + +type SingleSelectPrompt struct { + Message string + Choices []Choice + EnableFiltering *bool + Client *azdext.AzdClient +} + +func (p *SingleSelectPrompt) Ask(ctx context.Context, question Question) (any, error) { + choices := make([]*azdext.SelectChoice, len(p.Choices)) + for i, choice := range p.Choices { + choices[i] = &azdext.SelectChoice{ + Label: choice.Label, + Value: choice.Value, + } + } + + selectResponse, err := p.Client.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: p.Message, + Choices: choices, + EnableFiltering: p.EnableFiltering, + }, + }) + if err != nil { + return nil, err + } + + selectedChoice := p.Choices[*selectResponse.Value] + + if stringPtr, ok := question.Binding.(*string); ok { + *stringPtr = selectedChoice.Value + } + + return selectedChoice.Value, nil +} + +type MultiSelectPrompt struct { + Message string + Choices []Choice + EnableFiltering *bool + Client *azdext.AzdClient +} + +func (p *MultiSelectPrompt) Ask(ctx context.Context, question Question) (any, error) { + choices := make([]*azdext.MultiSelectChoice, len(p.Choices)) + for i, choice := range p.Choices { + choices[i] = &azdext.MultiSelectChoice{ + Label: choice.Label, + Value: choice.Value, + } + } + + selectResponse, err := p.Client.Prompt().MultiSelect(ctx, &azdext.MultiSelectRequest{ + Options: &azdext.MultiSelectOptions{ + Message: p.Message, + Choices: choices, + EnableFiltering: p.EnableFiltering, + }, + }) + if err != nil { + return nil, err + } + + selectedChoices := make([]string, len(selectResponse.Values)) + for i, value := range selectResponse.Values { + selectedChoices[i] = value.Value + } + + if stringPtr, ok := question.Binding.(*[]string); ok { + *stringPtr = selectedChoices + } + + return selectedChoices, nil +} + +type ConfirmPrompt struct { + Message string + DefaultValue *bool + Client *azdext.AzdClient +} + +func (p *ConfirmPrompt) Ask(ctx context.Context, question Question) (any, error) { + confirmResponse, err := p.Client.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: p.Message, + DefaultValue: p.DefaultValue, + }, + }) + if err != nil { + return nil, err + } + + if boolPtr, ok := question.Binding.(*bool); ok { + *boolPtr = *confirmResponse.Value + } + + return *confirmResponse.Value, nil +} + +type SubscriptionResourcePrompt struct { + ResourceType string + ResourceTypeDisplayName string + Kinds []string + AzureContext *azdext.AzureContext + Client *azdext.AzdClient + BeforeAsk func(ctx context.Context, q *Question, p *SubscriptionResourcePrompt) error +} + +func (p *SubscriptionResourcePrompt) Ask(ctx context.Context, question Question) (any, error) { + if p.BeforeAsk != nil { + if err := p.BeforeAsk(ctx, &question, p); err != nil { + return nil, err + } + } + + resourceResponse, err := p.Client.Prompt().PromptSubscriptionResource(ctx, &azdext.PromptSubscriptionResourceRequest{ + Options: &azdext.PromptResourceOptions{ + ResourceType: p.ResourceType, + Kinds: p.Kinds, + ResourceTypeDisplayName: p.ResourceTypeDisplayName, + }, + AzureContext: p.AzureContext, + }) + if err != nil { + return nil, err + } + + if stringPtr, ok := question.Binding.(*string); ok { + *stringPtr = resourceResponse.Resource.Id + } + + return resourceResponse.Resource.Id, nil +} From 40fbc9b91a603e45fa705530a048b340b5c8c2a5 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 6 Mar 2025 17:45:34 -0800 Subject: [PATCH 03/31] Adds support for state between questions --- .../internal/cmd/start.go | 19 ++++++++++-- .../internal/pkg/qna/decision_tree.go | 30 +++++++++++++++++-- .../internal/pkg/qna/prompt.go | 7 +++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index c4779125023..eba4b5397c9 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -302,6 +302,10 @@ func newStartCommand() *cobra.Command { "chatbot": "choose-app-type", "webapp": "choose-app-type", }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["interactionType"] = value + return nil + }, Next: "model-selection", }, "agent-interaction": { @@ -321,6 +325,10 @@ func newStartCommand() *cobra.Command { "webapp": "choose-app-type", "messaging": "choose-app-type", }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["interactionType"] = value + return nil + }, Next: "model-selection", }, "model-selection": { @@ -353,8 +361,15 @@ func newStartCommand() *cobra.Command { "choose-app-type": { Binding: &scenarioData.AppType, Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "What type of application do you want to build?", + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { + appName := q.State["interactionType"].(string) + p.Message = fmt.Sprintf( + "Which type of application do you want to build for %s?", + appName, + ) + return nil + }, + Client: azdClient, Choices: []qna.Choice{ {Label: "App Service", Value: "webapp"}, {Label: "Container App", Value: "containerapp"}, diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 11de13b47b2..8df37c7eda9 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -40,7 +40,9 @@ type Question struct { Next string `json:"next"` Binding any `json:"-"` Prompt Prompt `json:"prompt,omitempty"` + State map[string]any BeforeAsk func(ctx context.Context, question *Question) error + AfterAsk func(ctx context.Context, question *Question, value any) error } type Choice struct { @@ -77,6 +79,10 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error return errors.New("question prompt is nil") } + if question.State == nil { + question.State = map[string]any{} + } + if question.BeforeAsk != nil { if err := question.BeforeAsk(ctx, &question); err != nil { return fmt.Errorf("before ask function failed: %w", err) @@ -97,24 +103,43 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error if branch, has := question.Branches[v]; has { nextQuestionKey = branch } + + if question.AfterAsk != nil { + if err := question.AfterAsk(ctx, &question, v); err != nil { + return fmt.Errorf("after ask function failed: %w", err) + } + } case bool: if branch, has := question.Branches[v]; has { nextQuestionKey = branch } + + if question.AfterAsk != nil { + if err := question.AfterAsk(ctx, &question, v); err != nil { + return fmt.Errorf("after ask function failed: %w", err) + } + } case []string: // Handle multi-select case for _, selectedValue := range v { + if question.AfterAsk != nil { + if err := question.AfterAsk(ctx, &question, selectedValue); err != nil { + return fmt.Errorf("after ask function failed: %w", err) + } + } + branch, has := question.Branches[selectedValue] if !has { return fmt.Errorf("branch not found for selected value: %s", selectedValue) } - question, has := t.config.Questions[branch] + nextQuestion, has := t.config.Questions[branch] if !has { return fmt.Errorf("question not found for branch: %s", branch) } - if err = t.askQuestion(ctx, question); err != nil { + nextQuestion.State = question.State + if err = t.askQuestion(ctx, nextQuestion); err != nil { return fmt.Errorf("failed to ask question: %w", err) } } @@ -136,5 +161,6 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error return fmt.Errorf("next question not found: %s", nextQuestionKey) } + nextQuestion.State = question.State return t.askQuestion(ctx, nextQuestion) } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go index 284ae9adaa2..511b1c22337 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go @@ -37,9 +37,16 @@ type SingleSelectPrompt struct { Choices []Choice EnableFiltering *bool Client *azdext.AzdClient + BeforeAsk func(ctx context.Context, q *Question, p *SingleSelectPrompt) error } func (p *SingleSelectPrompt) Ask(ctx context.Context, question Question) (any, error) { + if p.BeforeAsk != nil { + if err := p.BeforeAsk(ctx, &question, p); err != nil { + return nil, err + } + } + choices := make([]*azdext.SelectChoice, len(p.Choices)) for i, choice := range p.Choices { choices[i] = &azdext.SelectChoice{ From a9bd011c930aa835ba50d5f65198867dc7d64f6b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 7 Mar 2025 10:30:36 -0800 Subject: [PATCH 04/31] Adds help and fixes --- .../microsoft.azd.ai.builder/build.sh | 10 +- .../internal/cmd/start.go | 114 ++++++++++++++---- .../internal/pkg/qna/decision_tree.go | 16 ++- .../internal/pkg/qna/prompt.go | 32 ++++- 4 files changed, 140 insertions(+), 32 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/build.sh b/cli/azd/extensions/microsoft.azd.ai.builder/build.sh index 4a8688cf91c..16e6e246357 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/build.sh +++ b/cli/azd/extensions/microsoft.azd.ai.builder/build.sh @@ -51,11 +51,11 @@ BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) # List of OS and architecture combinations PLATFORMS=( "windows/amd64" - "windows/arm64" - "darwin/amd64" - "darwin/arm64" - "linux/amd64" - "linux/arm64" + # "windows/arm64" + # "darwin/amd64" + # "darwin/arm64" + # "linux/amd64" + # "linux/arm64" ) APP_PATH="github.com/azure/azure-dev/cli/azd/extensions/$APP_NAME/internal/cmd" diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index eba4b5397c9..c5e3ef984fb 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" @@ -17,20 +18,22 @@ import ( type scenarioInput struct { SelectedScenario string `json:"selectedScenario,omitempty"` - UseCustomData bool `json:"useCustomData,omitempty"` - DataTypes []string `json:"dataTypes,omitempty"` - DataLocations []string `json:"dataLocations,omitempty"` - InteractionTypes []string `json:"interactionTypes,omitempty"` - ModelSelection string `json:"modelSelection,omitempty"` - LocalFilePath string `json:"localFilePath,omitempty"` - DatabaseType string `json:"databaseType,omitempty"` - StorageAccountId string `json:"storageAccountId,omitempty"` - DatabaseId string `json:"databaseId,omitempty"` - MessagingType string `json:"messageType,omitempty"` - MessagingId string `json:"messagingId,omitempty"` - ModelTasks []string `json:"modelTasks,omitempty"` - AppType string `json:"appType,omitempty"` - AppId string `json:"appId,omitempty"` + UseCustomData bool `json:"useCustomData,omitempty"` + DataTypes []string `json:"dataTypes,omitempty"` + DataLocations []string `json:"dataLocations,omitempty"` + InteractionTypes []string `json:"interactionTypes,omitempty"` + ModelSelection string `json:"modelSelection,omitempty"` + LocalFilePath string `json:"localFilePath,omitempty"` + LocalFileSelection string `json:"localFileSelection,omitempty"` + LocalFileGlobFilter string `json:"localFileGlobFilter,omitempty"` + DatabaseType string `json:"databaseType,omitempty"` + StorageAccountId string `json:"storageAccountId,omitempty"` + DatabaseId string `json:"databaseId,omitempty"` + MessagingType string `json:"messageType,omitempty"` + MessagingId string `json:"messagingId,omitempty"` + ModelTasks []string `json:"modelTasks,omitempty"` + AppType string `json:"appType,omitempty"` + AppId string `json:"appId,omitempty"` } type resourceTypeConfig struct { @@ -145,14 +148,22 @@ func newStartCommand() *cobra.Command { scenarioData := scenarioInput{} + welcomeMessage := []string{ + "This tool will help you build an AI scenario using Azure services.", + "Please answer the following questions to get started.", + } + // Build up list of questions config := qna.DecisionTreeConfig{ Questions: map[string]qna.Question{ "root": { Binding: &scenarioData.SelectedScenario, + Heading: "Welcome to the AI Builder Extension!", + Message: strings.Join(welcomeMessage, "\n"), Prompt: &qna.SingleSelectPrompt{ Client: azdClient, Message: "What type of AI scenario are you building?", + HelpMessage: "Choose the scenario that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, @@ -187,14 +198,18 @@ func newStartCommand() *cobra.Command { Prompt: &qna.ConfirmPrompt{ Client: azdClient, Message: "Does your application require custom data?", + HelpMessage: "Custom data is data that is not publicly available and is specific to your application.", DefaultValue: to.Ptr(true), }, }, "choose-data-types": { Binding: &scenarioData.DataTypes, + Heading: "Data Sources", + Message: "Lets identify all the data source that will be used in your application.", Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "What type of data are you using?", + HelpMessage: "Select all the data types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, @@ -211,11 +226,13 @@ func newStartCommand() *cobra.Command { Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "Where is your data located?", + HelpMessage: "Select all the data locations that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Azure Blob Storage", Value: "blob-storage"}, {Label: "Azure Database", Value: "databases"}, {Label: "Local file system", Value: "local-file-system"}, + {Label: "Other", Value: "other-datasource"}, }, }, Branches: map[any]string{ @@ -239,6 +256,7 @@ func newStartCommand() *cobra.Command { Client: azdClient, ResourceType: "Microsoft.Storage/storageAccounts", ResourceTypeDisplayName: "Storage Account", + HelpMessage: "You can select an existing storage account or create a new one.", AzureContext: azureContext, }, }, @@ -246,6 +264,7 @@ func newStartCommand() *cobra.Command { Binding: &scenarioData.DatabaseType, Prompt: &qna.SingleSelectPrompt{ Message: "Which type of database?", + HelpMessage: "Select the type of database that best fits your needs.", Client: azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ @@ -261,6 +280,7 @@ func newStartCommand() *cobra.Command { "choose-database-resource": { Binding: &scenarioData.DatabaseId, Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing database or create a new one.", Client: azdClient, AzureContext: azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -285,13 +305,46 @@ func newStartCommand() *cobra.Command { Prompt: &qna.TextPrompt{ Client: azdClient, Message: "Path to the local files", + HelpMessage: "This path can be absolute or relative to the current working directory. " + + "Please make sure the path is accessible from the machine running this command.", + Placeholder: "./data", + }, + Next: "local-file-choose-files", + }, + "local-file-choose-files": { + Binding: &scenarioData.LocalFileSelection, + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "Which files?", + HelpMessage: "You can select all files or use a glob expression to filter the files.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "All Files", Value: "all-files"}, + {Label: "Glob Expression", Value: "glob-expression"}, + }, + }, + Branches: map[any]string{ + "glob-expression": "local-file-glob", + }, + }, + "local-file-glob": { + Binding: &scenarioData.LocalFileGlobFilter, + Prompt: &qna.TextPrompt{ + Client: azdClient, + Message: "Enter a glob expression to filter files", + HelpMessage: "A glob expression is a string that uses wildcard characters to match file names. " + + " For example, *.txt will match all text files in the current directory.", + Placeholder: "*.json", }, }, "rag-user-interaction": { Binding: &scenarioData.InteractionTypes, + Heading: "Application Hosting", + Message: "Now we will figure out all the different ways users will interact with your application.", Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "How do you want users to interact with the data?", + HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Chatbot", Value: "chatbot"}, @@ -310,9 +363,12 @@ func newStartCommand() *cobra.Command { }, "agent-interaction": { Binding: &scenarioData.InteractionTypes, + Heading: "Agent Hosting", + Message: "Now we will figure out all the different ways users and systems will interact with your agent.", Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "How do you want users to interact with the agent?", + HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Chatbot", Value: "chatbot"}, @@ -349,11 +405,13 @@ func newStartCommand() *cobra.Command { Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "What tasks do you want the AI agent to perform?", + HelpMessage: "Select all the tasks that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Custom Function Calling", Value: "custom-function-calling"}, {Label: "Integrate with Open API based services", Value: "openapi"}, {Label: "Run Azure Functions", Value: "azure-functions"}, + {Label: "Other", Value: "other-model-tasks"}, }, }, Next: "use-custom-data", @@ -369,19 +427,28 @@ func newStartCommand() *cobra.Command { ) return nil }, - Client: azdClient, + Client: azdClient, + EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-app"}, {Label: "App Service", Value: "webapp"}, {Label: "Container App", Value: "containerapp"}, {Label: "Function App", Value: "functionapp"}, {Label: "Static Web App", Value: "staticwebapp"}, + {Label: "Other", Value: "otherapp"}, }, }, - Next: "choose-app-resource", + Branches: map[any]string{ + "webapp": "choose-app-resource", + "containerapp": "choose-app-resource", + "functionapp": "choose-app-resource", + "staticwebapp": "choose-app-resource", + }, }, "choose-app-resource": { Binding: &scenarioData.AppId, Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing application or create a new one.", Client: azdClient, AzureContext: azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -406,17 +473,23 @@ func newStartCommand() *cobra.Command { Prompt: &qna.SingleSelectPrompt{ Client: azdClient, Message: "Which messaging service do you want to use?", + HelpMessage: "Select the messaging service that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-messaging"}, {Label: "Azure Service Bus", Value: "messaging.eventhubs"}, {Label: "Azure Event Hubs", Value: "messaging.servicebus"}, }, }, - Next: "choose-messaging-resource", + Branches: map[any]string{ + "messaging.eventhubs": "choose-messaging-resource", + "messaging.servicebus": "choose-messaging-resource", + }, }, "choose-messaging-resource": { Binding: &scenarioData.MessagingId, Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing messaging service or create a new one.", Client: azdClient, AzureContext: azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -441,6 +514,8 @@ func newStartCommand() *cobra.Command { Prompt: &qna.MultiSelectPrompt{ Client: azdClient, Message: "What type of tasks should the AI models perform?", + HelpMessage: "Select all the tasks that apply to your application. " + + "These tasks will help you narrow down the type of models you need.", Choices: []qna.Choice{ {Label: "Text Generation", Value: "text-generation"}, {Label: "Image Generation", Value: "image-generation"}, @@ -457,11 +532,6 @@ func newStartCommand() *cobra.Command { }, } - fmt.Println("Welcome to the AI Builder CLI!") - fmt.Println("This tool will help you build an AI scenario using Azure services.") - fmt.Println("Please answer the following questions to get started.") - fmt.Println() - decisionTree := qna.NewDecisionTree(azdClient, config) if err := decisionTree.Run(ctx); err != nil { return fmt.Errorf("failed to run decision tree: %w", err) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 8df37c7eda9..a5b66d4ed30 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/fatih/color" ) // DecisionTree represents the entire decision tree structure. @@ -39,7 +40,10 @@ type Question struct { Branches map[any]string `json:"branches"` Next string `json:"next"` Binding any `json:"-"` - Prompt Prompt `json:"prompt,omitempty"` + Heading string `json:"heading,omitempty"` + Help string `json:"help,omitempty"` + Message string + Prompt Prompt `json:"prompt,omitempty"` State map[string]any BeforeAsk func(ctx context.Context, question *Question) error AfterAsk func(ctx context.Context, question *Question, value any) error @@ -83,6 +87,16 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error question.State = map[string]any{} } + if question.Heading != "" { + fmt.Println() + color.New(color.FgHiWhite, color.Bold).Printf("%s\n", question.Heading) + } + + if question.Message != "" { + fmt.Println(question.Message) + fmt.Println() + } + if question.BeforeAsk != nil { if err := question.BeforeAsk(ctx, &question); err != nil { return fmt.Errorf("before ask function failed: %w", err) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go index 511b1c22337..87928bb865c 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go @@ -10,15 +10,25 @@ import ( ) type TextPrompt struct { - Message string - Client *azdext.AzdClient + Message string + HelpMessage string + Placeholder string + DefaultValue string + ValidationMessage string + RequiredMessage string + Client *azdext.AzdClient } func (p *TextPrompt) Ask(ctx context.Context, question Question) (any, error) { promptResponse, err := p.Client.Prompt().Prompt(ctx, &azdext.PromptRequest{ Options: &azdext.PromptOptions{ - Message: p.Message, - Required: true, + Message: p.Message, + HelpMessage: p.HelpMessage, + Placeholder: p.Placeholder, + ValidationMessage: p.ValidationMessage, + RequiredMessage: p.RequiredMessage, + DefaultValue: p.DefaultValue, + Required: true, }, }) if err != nil { @@ -34,6 +44,7 @@ func (p *TextPrompt) Ask(ctx context.Context, question Question) (any, error) { type SingleSelectPrompt struct { Message string + HelpMessage string Choices []Choice EnableFiltering *bool Client *azdext.AzdClient @@ -58,6 +69,7 @@ func (p *SingleSelectPrompt) Ask(ctx context.Context, question Question) (any, e selectResponse, err := p.Client.Prompt().Select(ctx, &azdext.SelectRequest{ Options: &azdext.SelectOptions{ Message: p.Message, + HelpMessage: p.HelpMessage, Choices: choices, EnableFiltering: p.EnableFiltering, }, @@ -77,6 +89,7 @@ func (p *SingleSelectPrompt) Ask(ctx context.Context, question Question) (any, e type MultiSelectPrompt struct { Message string + HelpMessage string Choices []Choice EnableFiltering *bool Client *azdext.AzdClient @@ -95,6 +108,7 @@ func (p *MultiSelectPrompt) Ask(ctx context.Context, question Question) (any, er Options: &azdext.MultiSelectOptions{ Message: p.Message, Choices: choices, + HelpMessage: p.HelpMessage, EnableFiltering: p.EnableFiltering, }, }) @@ -117,6 +131,8 @@ func (p *MultiSelectPrompt) Ask(ctx context.Context, question Question) (any, er type ConfirmPrompt struct { Message string DefaultValue *bool + HelpMessage string + Placeholder string Client *azdext.AzdClient } @@ -125,6 +141,8 @@ func (p *ConfirmPrompt) Ask(ctx context.Context, question Question) (any, error) Options: &azdext.ConfirmOptions{ Message: p.Message, DefaultValue: p.DefaultValue, + HelpMessage: p.HelpMessage, + Placeholder: p.Placeholder, }, }) if err != nil { @@ -139,6 +157,8 @@ func (p *ConfirmPrompt) Ask(ctx context.Context, question Question) (any, error) } type SubscriptionResourcePrompt struct { + Message string + HelpMessage string ResourceType string ResourceTypeDisplayName string Kinds []string @@ -159,6 +179,10 @@ func (p *SubscriptionResourcePrompt) Ask(ctx context.Context, question Question) ResourceType: p.ResourceType, Kinds: p.Kinds, ResourceTypeDisplayName: p.ResourceTypeDisplayName, + SelectOptions: &azdext.PromptResourceSelectOptions{ + Message: p.HelpMessage, + HelpMessage: p.HelpMessage, + }, }, AzureContext: p.AzureContext, }) From eb9344eb2504c4cfe2ac988cbc428cd7b5735fb0 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 7 Mar 2025 16:40:22 -0800 Subject: [PATCH 05/31] Adds support for model selection --- .../internal/cmd/start.go | 183 ++++++++++-- .../internal/pkg/azure/ai/model_catalog.go | 261 ++++++++++++++++++ .../internal/pkg/azure/azure_client.go | 47 ++++ .../internal/pkg/qna/decision_tree.go | 4 +- .../internal/pkg/qna/prompt.go | 7 + .../microsoft.azd.ai.builder/main.go | 62 +++++ 6 files changed, 543 insertions(+), 21 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index c5e3ef984fb..f548742c132 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -10,8 +10,11 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/ux" "github.com/spf13/cobra" ) @@ -148,6 +151,36 @@ func newStartCommand() *cobra.Command { scenarioData := scenarioInput{} + credential, err := azidentity.NewAzureDeveloperCLICredential(nil) + if err != nil { + return fmt.Errorf("Run `azd auth login` to login", err) + } + + modelCatalog := ai.NewModelCatalog(credential) + var aiModelCatalog []*ai.AiModel + + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: "Loading AI Model Catalog", + }) + err = spinner.Run(ctx, func(ctx context.Context) error { + credential, err := azidentity.NewAzureDeveloperCLICredential(nil) + if err != nil { + return err + } + + var loadErr error + modelCatalog = ai.NewModelCatalog(credential) + aiModelCatalog, loadErr = modelCatalog.ListAllModels(ctx, azureContext.Scope.SubscriptionId) + if loadErr != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("failed to load AI model catalog: %w", err) + } + welcomeMessage := []string{ "This tool will help you build an AI scenario using Azure services.", "Please answer the following questions to get started.", @@ -174,7 +207,7 @@ func newStartCommand() *cobra.Command { Branches: map[any]string{ "rag": "use-custom-data", "agent": "agent-tasks", - "ai-model": "model-exploration", + "ai-model": "model-capabilities", }, }, "use-custom-data": { @@ -359,7 +392,7 @@ func newStartCommand() *cobra.Command { q.State["interactionType"] = value return nil }, - Next: "model-selection", + Next: "start-choose-model", }, "agent-interaction": { Binding: &scenarioData.InteractionTypes, @@ -379,26 +412,13 @@ func newStartCommand() *cobra.Command { Branches: map[any]string{ "chatbot": "choose-app-type", "webapp": "choose-app-type", - "messaging": "choose-app-type", + "messaging": "choose-messaging-type", }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["interactionType"] = value return nil }, - Next: "model-selection", - }, - "model-selection": { - Binding: &scenarioData.ModelSelection, - Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "Do you want to know what type(s) of model(s) you would like to use?", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Choose for me", Value: "choose-model"}, - {Label: "Help me choose", Value: "guide-model"}, - {Label: "Yes, I have some models in mind", Value: "user-models"}, - }, - }, + Next: "start-choose-model", }, "agent-tasks": { Binding: &scenarioData.ModelTasks, @@ -431,10 +451,10 @@ func newStartCommand() *cobra.Command { EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "choose-app"}, - {Label: "App Service", Value: "webapp"}, + {Label: "App Service (Coming Soon)", Value: "webapp"}, {Label: "Container App", Value: "containerapp"}, - {Label: "Function App", Value: "functionapp"}, - {Label: "Static Web App", Value: "staticwebapp"}, + {Label: "Function App (Coming Soon)", Value: "functionapp"}, + {Label: "Static Web App (Coming Soon)", Value: "staticwebapp"}, {Label: "Other", Value: "otherapp"}, }, }, @@ -529,6 +549,129 @@ func newStartCommand() *cobra.Command { }, }, }, + "start-choose-model": { + Prompt: &qna.SingleSelectPrompt{ + Client: azdClient, + Message: "How do you want to find the right model?", + HelpMessage: "Select the option that best fits your needs.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-model"}, + {Label: "Help me choose", Value: "guide-model"}, + {Label: "I will choose model", Value: "user-model"}, + }, + }, + Branches: map[any]string{ + "guide-model": "guide-model-select", + "user-model": "user-model-select", + }, + }, + "user-model-select": { + Prompt: &qna.SingleSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { + filteredModels := modelCatalog.ListFilteredModels(ctx, aiModelCatalog, nil) + + choices := make([]qna.Choice, len(filteredModels)) + for i, model := range aiModelCatalog { + choices[i] = qna.Choice{ + Label: model.Name, + Value: model.Name, + } + } + p.Choices = choices + + return nil + }, + Client: azdClient, + Message: "Which model do you want to use?", + HelpMessage: "Select the model that best fits your needs.", + }, + }, + "guide-model-select": { + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "Filter AI Models", + Choices: []qna.Choice{ + {Label: "Filter by capabilities", Value: "filter-model-capability"}, + {Label: "Filter by creator", Value: "filter-model-format"}, + {Label: "Filter by status", Value: "filter-model-status"}, + }, + }, + Branches: map[any]string{ + "filter-model-capability": "filter-model-capability", + "filter-model-format": "filter-model-format", + "filter-model-status": "filter-model-status", + }, + Next: "user-model-select", + }, + "filter-model-capability": { + Prompt: &qna.MultiSelectPrompt{ + Client: azdClient, + Message: "What capabilities do you want the model to have?", + HelpMessage: "Select all the capabilities that apply to your application.", + Choices: []qna.Choice{ + {Label: "Audio", Value: "audio"}, + {Label: "Chat Completion", Value: "chatCompletion"}, + {Label: "Text Completion", Value: "completion"}, + {Label: "Generate Vector Embeddings", Value: "embeddings"}, + {Label: "Image Generation", Value: "imageGenerations"}, + }, + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + capabilities := value.([]string) + q.State["capabilities"] = capabilities + return nil + }, + }, + "filter-model-format": { + Prompt: &qna.MultiSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + formats := modelCatalog.ListAllFormats(ctx, aiModelCatalog) + choices := make([]qna.Choice, len(formats)) + for i, format := range formats { + choices[i] = qna.Choice{ + Label: format, + Value: format, + } + } + p.Choices = choices + return nil + }, + Client: azdClient, + Message: "Filter my by company or creator", + HelpMessage: "Select all the companies or creators that apply to your application.", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + formats := value.([]string) + q.State["formats"] = formats + return nil + }, + }, + "filter-model-status": { + Prompt: &qna.MultiSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + statuses := modelCatalog.ListAllStatuses(ctx, aiModelCatalog) + choices := make([]qna.Choice, len(statuses)) + for i, status := range statuses { + choices[i] = qna.Choice{ + Label: status, + Value: status, + } + } + p.Choices = choices + return nil + }, + Client: azdClient, + Message: "Filter by model release status?", + HelpMessage: "Select all the model release status that apply to your application.", + EnableFiltering: to.Ptr(false), + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + statuses := value.([]string) + q.State["status"] = statuses + return nil + }, + }, }, } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go new file mode 100644 index 00000000000..5063e35ee19 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go @@ -0,0 +1,261 @@ +package ai + +import ( + "context" + "slices" + "strings" + "sync" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure" +) + +type AiModel struct { + Name string + Locations []*AiModelLocation +} + +type AiModelLocation struct { + Model *armcognitiveservices.Model + Location *armsubscriptions.Location +} + +type ModelCatalog struct { + credential azcore.TokenCredential + azureClient *azure.AzureClient +} + +func NewModelCatalog(credential azcore.TokenCredential) *ModelCatalog { + return &ModelCatalog{ + credential: credential, + azureClient: azure.NewAzureClient(credential), + } +} + +func (c *ModelCatalog) ListAllCapabilities(ctx context.Context, models []*AiModel) []string { + return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { + capabilities := []string{} + for key := range m.Model.Capabilities { + capabilities = append(capabilities, key) + } + + return capabilities + }) +} + +func (c *ModelCatalog) ListAllStatuses(ctx context.Context, models []*AiModel) []string { + return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { + return []string{string(*m.Model.LifecycleStatus)} + }) +} + +func (c *ModelCatalog) ListAllFormats(ctx context.Context, models []*AiModel) []string { + return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { + return []string{*m.Model.Format} + }) +} + +func (c *ModelCatalog) ListAllKinds(ctx context.Context, models []*AiModel) []string { + return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { + return []string{*m.Kind} + }) +} + +func (c *ModelCatalog) ListModelVersions(ctx context.Context, model *AiModel) ([]string, error) { + versions := make(map[string]struct{}) + for _, location := range model.Locations { + versions[*location.Model.Model.Version] = struct{}{} + } + + versionList := make([]string, len(versions)) + for version := range versions { + versionList = append(versionList, version) + } + + slices.Sort(versionList) + + return versionList, nil +} + +func (c *ModelCatalog) ListModelSkus(ctx context.Context, model *AiModel) ([]string, error) { + skus := make(map[string]struct{}) + for _, location := range model.Locations { + for _, sku := range location.Model.Model.SKUs { + skus[*sku.Name] = struct{}{} + } + } + + skuList := make([]string, len(skus)) + for sku := range skus { + skuList = append(skuList, sku) + } + + slices.Sort(skuList) + + return skuList, nil +} + +type FilterOptions struct { + Capabilities []string + Statuses []string + Formats []string + Kinds []string + Locations []string +} + +func (c *ModelCatalog) ListFilteredModels(ctx context.Context, allModels []*AiModel, options *FilterOptions) []*AiModel { + if options == nil { + return allModels + } + + filteredModels := []*AiModel{} + + for _, model := range allModels { + for _, location := range model.Locations { + if len(options.Capabilities) > 0 { + for _, capability := range options.Capabilities { + if _, exists := location.Model.Model.Capabilities[capability]; !exists { + continue + } + } + } + + if len(options.Locations) > 0 && !slices.Contains(options.Locations, *location.Location.Name) { + continue + } + + if len(options.Statuses) > 0 && slices.Contains(options.Statuses, string(*location.Model.Model.LifecycleStatus)) { + continue + } + + if len(options.Formats) > 0 && slices.Contains(options.Formats, *location.Model.Model.Format) { + continue + } + + if len(options.Kinds) > 0 && slices.Contains(options.Kinds, *location.Model.Kind) { + continue + } + } + + filteredModels = append(filteredModels, model) + } + + return filteredModels +} + +func (c *ModelCatalog) ListAllModels(ctx context.Context, subscriptionId string) ([]*AiModel, error) { + locations, err := c.azureClient.ListLocation(ctx, subscriptionId) + if err != nil { + return nil, err + } + + modelsClient, err := createModelsClient(subscriptionId, c.credential) + if err != nil { + return nil, err + } + + var locationResults sync.Map + var wg sync.WaitGroup + + for _, location := range locations { + wg.Add(1) + go func(location *armsubscriptions.Location) { + defer wg.Done() + pager := modelsClient.NewListPager(*location.Name, nil) + + results := []*armcognitiveservices.Model{} + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return + } + + results = append(results, page.Value...) + } + + locationResults.Store(location, results) + }(location) + } + + wg.Wait() + + modelMap := map[string]*AiModel{} + + locationResults.Range(func(key, value any) bool { + location := key.(*armsubscriptions.Location) + modelsList := value.([]*armcognitiveservices.Model) + + for _, model := range modelsList { + modelName := *model.Model.Name + existingModel, exists := modelMap[modelName] + if !exists { + existingModel = &AiModel{ + Name: modelName, + Locations: []*AiModelLocation{}, + } + } + + existingModel.Locations = append(existingModel.Locations, &AiModelLocation{ + Model: model, + Location: location, + }) + + modelMap[modelName] = existingModel + } + + return true + }) + + allModels := []*AiModel{} + for _, model := range modelMap { + allModels = append(allModels, model) + } + + slices.SortFunc(allModels, func(a, b *AiModel) int { + return strings.Compare(a.Name, b.Name) + }) + + return allModels, nil +} + +func filterDistinctModelData( + models []*AiModel, + filterFunc func(*armcognitiveservices.Model) []string, +) []string { + filtered := make(map[string]struct{}) + for _, model := range models { + for _, location := range model.Locations { + value := filterFunc(location.Model) + for _, v := range value { + if v != "" { + filtered[v] = struct{}{} + } + } + } + } + + list := make([]string, len(filtered)) + i := 0 + for value := range filtered { + list[i] = value + i++ + } + + slices.Sort(list) + return list +} + +func createModelsClient( + subscriptionId string, + credential azcore.TokenCredential, +) (*armcognitiveservices.ModelsClient, error) { + client, err := armcognitiveservices.NewModelsClient(subscriptionId, credential, nil) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go new file mode 100644 index 00000000000..f022f7ac3eb --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go @@ -0,0 +1,47 @@ +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" +) + +type AzureClient struct { + credential azcore.TokenCredential +} + +func NewAzureClient(credential azcore.TokenCredential) *AzureClient { + return &AzureClient{ + credential: credential, + } +} + +func (c *AzureClient) ListLocation(ctx context.Context, subscriptionId string) ([]*armsubscriptions.Location, error) { + client, err := createSubscriptionsClient(subscriptionId, c.credential) + if err != nil { + return nil, err + } + + pager := client.NewListLocationsPager(subscriptionId, nil) + + var locations []*armsubscriptions.Location + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + locations = append(locations, page.Value...) + } + + return locations, nil +} + +func createSubscriptionsClient(subscriptionId string, credentail azcore.TokenCredential) (*armsubscriptions.Client, error) { + client, err := armsubscriptions.NewClient(credentail, nil) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index a5b66d4ed30..2d1f3b4e8c1 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "log" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/fatih/color" @@ -144,7 +145,8 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error branch, has := question.Branches[selectedValue] if !has { - return fmt.Errorf("branch not found for selected value: %s", selectedValue) + log.Printf("branch not found for selected value: %s\n", selectedValue) + continue } nextQuestion, has := t.config.Questions[branch] diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go index 87928bb865c..4dfb812b2e8 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go @@ -93,9 +93,16 @@ type MultiSelectPrompt struct { Choices []Choice EnableFiltering *bool Client *azdext.AzdClient + BeforeAsk func(ctx context.Context, q *Question, p *MultiSelectPrompt) error } func (p *MultiSelectPrompt) Ask(ctx context.Context, question Question) (any, error) { + if p.BeforeAsk != nil { + if err := p.BeforeAsk(ctx, &question, p); err != nil { + return nil, err + } + } + choices := make([]*azdext.MultiSelectChoice, len(p.Choices)) for i, choice := range p.Choices { choices[i] = &azdext.MultiSelectChoice{ diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/main.go b/cli/azd/extensions/microsoft.azd.ai.builder/main.go index a2b2b81eaeb..649d4e8ee38 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/main.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/main.go @@ -5,10 +5,16 @@ package main import ( "context" + "fmt" + "io" + "log" "os" + "path/filepath" + "time" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd" "github.com/fatih/color" + "github.com/spf13/pflag" ) func init() { @@ -23,8 +29,64 @@ func main() { ctx := context.Background() rootCmd := cmd.NewRootCommand() + debugEnabled := isDebugEnabled() + configureLogger(debugEnabled) + if err := rootCmd.ExecuteContext(ctx); err != nil { color.Red("Error: %v", err) os.Exit(1) } } + +func isDebugEnabled() bool { + debugEnabled := false + pflag.BoolVar(&debugEnabled, "debug", false, "Enable debug logging") + pflag.Parse() + + return debugEnabled +} + +// ConfigureLogger sets up logging based on the debug flag. +// - If debug is true, logs go to a daily file in "logs/" next to the executable. +// - If debug is false, logs are discarded. +// - Standard log functions (log.Print, log.Println, etc.) will use this setup. +func configureLogger(debug bool) { + var output io.Writer = io.Discard // Default: discard logs + + if debug { + // Determine the directory of the running executable + exePath, err := os.Executable() + if err != nil { + fmt.Println("Failed to get executable path:", err) + return + } + exeDir := filepath.Dir(exePath) + + // Ensure the logs directory exists + logDir := filepath.Join(exeDir, "logs") + if err := os.MkdirAll(logDir, 0755); err != nil { + fmt.Println("Failed to create log directory:", err) + return + } + + // Generate log file name per day + logFile := filepath.Join(logDir, fmt.Sprintf("app-%s.log", time.Now().Format("2006-01-02"))) + fmt.Println("Logging to:", logFile) + + // Open the log file for appending + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Println("Failed to open log file:", err) + return + } + + // Send logs to both file and console + output = io.MultiWriter(file, os.Stdout) + } + + // Configure the global logger + log.SetOutput(output) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.SetPrefix("[APP] ") + log.Println("Logger initialized") +} From 807fbc877212ca977be334fe9f4ef468020ca8c7 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 11 Mar 2025 18:45:54 -0700 Subject: [PATCH 06/31] wip: model selection --- .../internal/cmd/root.go | 3 +- .../internal/cmd/start.go | 696 ------------- .../internal/cmd/start/start.go | 937 ++++++++++++++++++ .../internal/pkg/azure/ai/model_catalog.go | 71 +- .../internal/pkg/azure/azure_client.go | 8 +- .../internal/pkg/qna/decision_tree.go | 209 ++-- .../internal/pkg/qna/prompt.go | 20 - 7 files changed, 1121 insertions(+), 823 deletions(-) delete mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go index 16bdb921086..9ad869a771f 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go @@ -4,6 +4,7 @@ package cmd import ( + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start" "github.com/spf13/cobra" ) @@ -21,7 +22,7 @@ func NewRootCommand() *cobra.Command { rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") - rootCmd.AddCommand(newStartCommand()) + rootCmd.AddCommand(start.NewStartCommand()) rootCmd.AddCommand(newVersionCommand()) return rootCmd diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go deleted file mode 100644 index f548742c132..00000000000 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ /dev/null @@ -1,696 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" - "github.com/azure/azure-dev/cli/azd/pkg/ux" - "github.com/spf13/cobra" -) - -type scenarioInput struct { - SelectedScenario string `json:"selectedScenario,omitempty"` - - UseCustomData bool `json:"useCustomData,omitempty"` - DataTypes []string `json:"dataTypes,omitempty"` - DataLocations []string `json:"dataLocations,omitempty"` - InteractionTypes []string `json:"interactionTypes,omitempty"` - ModelSelection string `json:"modelSelection,omitempty"` - LocalFilePath string `json:"localFilePath,omitempty"` - LocalFileSelection string `json:"localFileSelection,omitempty"` - LocalFileGlobFilter string `json:"localFileGlobFilter,omitempty"` - DatabaseType string `json:"databaseType,omitempty"` - StorageAccountId string `json:"storageAccountId,omitempty"` - DatabaseId string `json:"databaseId,omitempty"` - MessagingType string `json:"messageType,omitempty"` - MessagingId string `json:"messagingId,omitempty"` - ModelTasks []string `json:"modelTasks,omitempty"` - AppType string `json:"appType,omitempty"` - AppId string `json:"appId,omitempty"` -} - -type resourceTypeConfig struct { - ResourceType string - ResourceTypeDisplayName string - Kinds []string -} - -var ( - dbResourceMap = map[string]resourceTypeConfig{ - "db.cosmos": { - ResourceType: "Microsoft.DocumentDB/databaseAccounts", - ResourceTypeDisplayName: "Cosmos Database Account", - Kinds: []string{"GlobalDocumentDB"}, - }, - "db.mongo": { - ResourceType: "Microsoft.DocumentDB/databaseAccounts", - ResourceTypeDisplayName: "MongoDB Database Account", - Kinds: []string{"MongoDB"}, - }, - "db.postgres": { - ResourceType: "Microsoft.DBforPostgreSQL/flexibleServers", - ResourceTypeDisplayName: "PostgreSQL Server", - }, - "db.mysql": { - ResourceType: "Microsoft.DBforMySQL/flexibleServers", - ResourceTypeDisplayName: "MySQL Server", - }, - "db.redis": { - ResourceType: "Microsoft.Cache/Redis", - ResourceTypeDisplayName: "Redis Cache", - }, - } - - messagingResourceMap = map[string]resourceTypeConfig{ - "messaging.eventhubs": { - ResourceType: "Microsoft.EventHub/namespaces", - ResourceTypeDisplayName: "Event Hub Namespace", - }, - "messaging.servicebus": { - ResourceType: "Microsoft.ServiceBus/namespaces", - ResourceTypeDisplayName: "Service Bus Namespace", - }, - } - - appResourceMap = map[string]resourceTypeConfig{ - "webapp": { - ResourceType: "Microsoft.Web/sites", - ResourceTypeDisplayName: "Web App", - Kinds: []string{"app"}, - }, - "containerapp": { - ResourceType: "Microsoft.App/containerApps", - ResourceTypeDisplayName: "Container App", - }, - "functionapp": { - ResourceType: "Microsoft.Web/sites", - ResourceTypeDisplayName: "Function App", - Kinds: []string{"functionapp"}, - }, - "staticwebapp": { - ResourceType: "Microsoft.Web/staticSites", - ResourceTypeDisplayName: "Static Web App", - }, - } -) - -func newStartCommand() *cobra.Command { - return &cobra.Command{ - Use: "start", - Short: "Get the context of the AZD project & environment.", - RunE: func(cmd *cobra.Command, args []string) error { - // Create a new context that includes the AZD access token - ctx := azdext.WithAccessToken(cmd.Context()) - - // Create a new AZD client - azdClient, err := azdext.NewAzdClient() - if err != nil { - return fmt.Errorf("failed to create azd client: %w", err) - } - - defer azdClient.Close() - - _, err = azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) - } - - envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) - } - - envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ - Name: envResponse.Environment.Name, - }) - if err != nil { - return fmt.Errorf("failed to get environment values: %w", err) - } - - envValueMap := make(map[string]string) - for _, value := range envValues.KeyValues { - envValueMap[value.Key] = value.Value - } - - azureContext := &azdext.AzureContext{ - Scope: &azdext.AzureScope{ - SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], - }, - Resources: []string{}, - } - - scenarioData := scenarioInput{} - - credential, err := azidentity.NewAzureDeveloperCLICredential(nil) - if err != nil { - return fmt.Errorf("Run `azd auth login` to login", err) - } - - modelCatalog := ai.NewModelCatalog(credential) - var aiModelCatalog []*ai.AiModel - - spinner := ux.NewSpinner(&ux.SpinnerOptions{ - Text: "Loading AI Model Catalog", - }) - err = spinner.Run(ctx, func(ctx context.Context) error { - credential, err := azidentity.NewAzureDeveloperCLICredential(nil) - if err != nil { - return err - } - - var loadErr error - modelCatalog = ai.NewModelCatalog(credential) - aiModelCatalog, loadErr = modelCatalog.ListAllModels(ctx, azureContext.Scope.SubscriptionId) - if loadErr != nil { - return err - } - - return nil - }) - if err != nil { - return fmt.Errorf("failed to load AI model catalog: %w", err) - } - - welcomeMessage := []string{ - "This tool will help you build an AI scenario using Azure services.", - "Please answer the following questions to get started.", - } - - // Build up list of questions - config := qna.DecisionTreeConfig{ - Questions: map[string]qna.Question{ - "root": { - Binding: &scenarioData.SelectedScenario, - Heading: "Welcome to the AI Builder Extension!", - Message: strings.Join(welcomeMessage, "\n"), - Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "What type of AI scenario are you building?", - HelpMessage: "Choose the scenario that best fits your needs.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, - {Label: "AI Agent", Value: "agent"}, - {Label: "Explore AI Models", Value: "ai-model"}, - }, - }, - Branches: map[any]string{ - "rag": "use-custom-data", - "agent": "agent-tasks", - "ai-model": "model-capabilities", - }, - }, - "use-custom-data": { - Binding: &scenarioData.UseCustomData, - BeforeAsk: func(ctx context.Context, q *qna.Question) error { - switch scenarioData.SelectedScenario { - case "rag": - q.Branches = map[any]string{ - true: "choose-data-types", - false: "rag-user-interaction", - } - case "agent": - q.Branches = map[any]string{ - true: "choose-data-types", - false: "agent-tasks", - } - } - - return nil - }, - Prompt: &qna.ConfirmPrompt{ - Client: azdClient, - Message: "Does your application require custom data?", - HelpMessage: "Custom data is data that is not publicly available and is specific to your application.", - DefaultValue: to.Ptr(true), - }, - }, - "choose-data-types": { - Binding: &scenarioData.DataTypes, - Heading: "Data Sources", - Message: "Lets identify all the data source that will be used in your application.", - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "What type of data are you using?", - HelpMessage: "Select all the data types that apply to your application.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, - {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, - {Label: "Videos", Value: "videos"}, - {Label: "Images", Value: "images"}, - {Label: "Audio", Value: "audio"}, - }, - }, - Next: "data-location", - }, - "data-location": { - Binding: &scenarioData.DataLocations, - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "Where is your data located?", - HelpMessage: "Select all the data locations that apply to your application.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Azure Blob Storage", Value: "blob-storage"}, - {Label: "Azure Database", Value: "databases"}, - {Label: "Local file system", Value: "local-file-system"}, - {Label: "Other", Value: "other-datasource"}, - }, - }, - Branches: map[any]string{ - "blob-storage": "choose-storage", - "databases": "choose-database-type", - "local-file-system": "local-file-system", - }, - BeforeAsk: func(ctx context.Context, q *qna.Question) error { - switch scenarioData.SelectedScenario { - case "rag": - q.Next = "rag-user-interaction" - case "agent": - q.Next = "agent-interaction" - } - return nil - }, - }, - "choose-storage": { - Binding: &scenarioData.StorageAccountId, - Prompt: &qna.SubscriptionResourcePrompt{ - Client: azdClient, - ResourceType: "Microsoft.Storage/storageAccounts", - ResourceTypeDisplayName: "Storage Account", - HelpMessage: "You can select an existing storage account or create a new one.", - AzureContext: azureContext, - }, - }, - "choose-database-type": { - Binding: &scenarioData.DatabaseType, - Prompt: &qna.SingleSelectPrompt{ - Message: "Which type of database?", - HelpMessage: "Select the type of database that best fits your needs.", - Client: azdClient, - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "CosmosDB", Value: "db.cosmos"}, - {Label: "PostgreSQL", Value: "db.postgres"}, - {Label: "MySQL", Value: "db.mysql"}, - {Label: "Redis", Value: "db.redis"}, - {Label: "MongoDB", Value: "db.mongo"}, - }, - }, - Next: "choose-database-resource", - }, - "choose-database-resource": { - Binding: &scenarioData.DatabaseId, - Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing database or create a new one.", - Client: azdClient, - AzureContext: azureContext, - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { - resourceType, has := dbResourceMap[scenarioData.DatabaseType] - if !has { - return fmt.Errorf( - "unknown resource type for database: %s", - scenarioData.DatabaseType, - ) - } - - p.ResourceType = resourceType.ResourceType - p.Kinds = resourceType.Kinds - p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName - - return nil - }, - }, - }, - "local-file-system": { - Binding: &scenarioData.LocalFilePath, - Prompt: &qna.TextPrompt{ - Client: azdClient, - Message: "Path to the local files", - HelpMessage: "This path can be absolute or relative to the current working directory. " + - "Please make sure the path is accessible from the machine running this command.", - Placeholder: "./data", - }, - Next: "local-file-choose-files", - }, - "local-file-choose-files": { - Binding: &scenarioData.LocalFileSelection, - Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "Which files?", - HelpMessage: "You can select all files or use a glob expression to filter the files.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "All Files", Value: "all-files"}, - {Label: "Glob Expression", Value: "glob-expression"}, - }, - }, - Branches: map[any]string{ - "glob-expression": "local-file-glob", - }, - }, - "local-file-glob": { - Binding: &scenarioData.LocalFileGlobFilter, - Prompt: &qna.TextPrompt{ - Client: azdClient, - Message: "Enter a glob expression to filter files", - HelpMessage: "A glob expression is a string that uses wildcard characters to match file names. " + - " For example, *.txt will match all text files in the current directory.", - Placeholder: "*.json", - }, - }, - "rag-user-interaction": { - Binding: &scenarioData.InteractionTypes, - Heading: "Application Hosting", - Message: "Now we will figure out all the different ways users will interact with your application.", - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "How do you want users to interact with the data?", - HelpMessage: "Select all the data interaction types that apply to your application.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Chatbot", Value: "chatbot"}, - {Label: "Web Application", Value: "webapp"}, - }, - }, - Branches: map[any]string{ - "chatbot": "choose-app-type", - "webapp": "choose-app-type", - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - q.State["interactionType"] = value - return nil - }, - Next: "start-choose-model", - }, - "agent-interaction": { - Binding: &scenarioData.InteractionTypes, - Heading: "Agent Hosting", - Message: "Now we will figure out all the different ways users and systems will interact with your agent.", - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "How do you want users to interact with the agent?", - HelpMessage: "Select all the data interaction types that apply to your application.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Chatbot", Value: "chatbot"}, - {Label: "Web Application", Value: "webapp"}, - {Label: "Message Queue", Value: "messaging"}, - }, - }, - Branches: map[any]string{ - "chatbot": "choose-app-type", - "webapp": "choose-app-type", - "messaging": "choose-messaging-type", - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - q.State["interactionType"] = value - return nil - }, - Next: "start-choose-model", - }, - "agent-tasks": { - Binding: &scenarioData.ModelTasks, - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "What tasks do you want the AI agent to perform?", - HelpMessage: "Select all the tasks that apply to your application.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Custom Function Calling", Value: "custom-function-calling"}, - {Label: "Integrate with Open API based services", Value: "openapi"}, - {Label: "Run Azure Functions", Value: "azure-functions"}, - {Label: "Other", Value: "other-model-tasks"}, - }, - }, - Next: "use-custom-data", - }, - "choose-app-type": { - Binding: &scenarioData.AppType, - Prompt: &qna.SingleSelectPrompt{ - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { - appName := q.State["interactionType"].(string) - p.Message = fmt.Sprintf( - "Which type of application do you want to build for %s?", - appName, - ) - return nil - }, - Client: azdClient, - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Choose for me", Value: "choose-app"}, - {Label: "App Service (Coming Soon)", Value: "webapp"}, - {Label: "Container App", Value: "containerapp"}, - {Label: "Function App (Coming Soon)", Value: "functionapp"}, - {Label: "Static Web App (Coming Soon)", Value: "staticwebapp"}, - {Label: "Other", Value: "otherapp"}, - }, - }, - Branches: map[any]string{ - "webapp": "choose-app-resource", - "containerapp": "choose-app-resource", - "functionapp": "choose-app-resource", - "staticwebapp": "choose-app-resource", - }, - }, - "choose-app-resource": { - Binding: &scenarioData.AppId, - Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing application or create a new one.", - Client: azdClient, - AzureContext: azureContext, - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { - resourceType, has := appResourceMap[scenarioData.AppType] - if !has { - return fmt.Errorf( - "unknown resource type for database: %s", - scenarioData.AppType, - ) - } - - p.ResourceType = resourceType.ResourceType - p.Kinds = resourceType.Kinds - p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName - - return nil - }, - }, - }, - "choose-messaging-type": { - Binding: &scenarioData.MessagingType, - Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "Which messaging service do you want to use?", - HelpMessage: "Select the messaging service that best fits your needs.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Choose for me", Value: "choose-messaging"}, - {Label: "Azure Service Bus", Value: "messaging.eventhubs"}, - {Label: "Azure Event Hubs", Value: "messaging.servicebus"}, - }, - }, - Branches: map[any]string{ - "messaging.eventhubs": "choose-messaging-resource", - "messaging.servicebus": "choose-messaging-resource", - }, - }, - "choose-messaging-resource": { - Binding: &scenarioData.MessagingId, - Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing messaging service or create a new one.", - Client: azdClient, - AzureContext: azureContext, - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { - resourceType, has := messagingResourceMap[scenarioData.MessagingType] - if !has { - return fmt.Errorf( - "unknown resource type for database: %s", - scenarioData.MessagingType, - ) - } - - p.ResourceType = resourceType.ResourceType - p.Kinds = resourceType.Kinds - p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName - - return nil - }, - }, - }, - "model-exploration": { - Binding: &scenarioData.ModelTasks, - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "What type of tasks should the AI models perform?", - HelpMessage: "Select all the tasks that apply to your application. " + - "These tasks will help you narrow down the type of models you need.", - Choices: []qna.Choice{ - {Label: "Text Generation", Value: "text-generation"}, - {Label: "Image Generation", Value: "image-generation"}, - {Label: "Audio Generation", Value: "audio-generation"}, - {Label: "Video Generation", Value: "video-generation"}, - {Label: "Text Classification", Value: "text-classification"}, - {Label: "Image Classification", Value: "image-classification"}, - {Label: "Audio Classification", Value: "audio-classification"}, - {Label: "Video Classification", Value: "video-classification"}, - {Label: "Text Summarization", Value: "text-summarization"}, - }, - }, - }, - "start-choose-model": { - Prompt: &qna.SingleSelectPrompt{ - Client: azdClient, - Message: "How do you want to find the right model?", - HelpMessage: "Select the option that best fits your needs.", - EnableFiltering: to.Ptr(false), - Choices: []qna.Choice{ - {Label: "Choose for me", Value: "choose-model"}, - {Label: "Help me choose", Value: "guide-model"}, - {Label: "I will choose model", Value: "user-model"}, - }, - }, - Branches: map[any]string{ - "guide-model": "guide-model-select", - "user-model": "user-model-select", - }, - }, - "user-model-select": { - Prompt: &qna.SingleSelectPrompt{ - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { - filteredModels := modelCatalog.ListFilteredModels(ctx, aiModelCatalog, nil) - - choices := make([]qna.Choice, len(filteredModels)) - for i, model := range aiModelCatalog { - choices[i] = qna.Choice{ - Label: model.Name, - Value: model.Name, - } - } - p.Choices = choices - - return nil - }, - Client: azdClient, - Message: "Which model do you want to use?", - HelpMessage: "Select the model that best fits your needs.", - }, - }, - "guide-model-select": { - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "Filter AI Models", - Choices: []qna.Choice{ - {Label: "Filter by capabilities", Value: "filter-model-capability"}, - {Label: "Filter by creator", Value: "filter-model-format"}, - {Label: "Filter by status", Value: "filter-model-status"}, - }, - }, - Branches: map[any]string{ - "filter-model-capability": "filter-model-capability", - "filter-model-format": "filter-model-format", - "filter-model-status": "filter-model-status", - }, - Next: "user-model-select", - }, - "filter-model-capability": { - Prompt: &qna.MultiSelectPrompt{ - Client: azdClient, - Message: "What capabilities do you want the model to have?", - HelpMessage: "Select all the capabilities that apply to your application.", - Choices: []qna.Choice{ - {Label: "Audio", Value: "audio"}, - {Label: "Chat Completion", Value: "chatCompletion"}, - {Label: "Text Completion", Value: "completion"}, - {Label: "Generate Vector Embeddings", Value: "embeddings"}, - {Label: "Image Generation", Value: "imageGenerations"}, - }, - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - capabilities := value.([]string) - q.State["capabilities"] = capabilities - return nil - }, - }, - "filter-model-format": { - Prompt: &qna.MultiSelectPrompt{ - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { - formats := modelCatalog.ListAllFormats(ctx, aiModelCatalog) - choices := make([]qna.Choice, len(formats)) - for i, format := range formats { - choices[i] = qna.Choice{ - Label: format, - Value: format, - } - } - p.Choices = choices - return nil - }, - Client: azdClient, - Message: "Filter my by company or creator", - HelpMessage: "Select all the companies or creators that apply to your application.", - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - formats := value.([]string) - q.State["formats"] = formats - return nil - }, - }, - "filter-model-status": { - Prompt: &qna.MultiSelectPrompt{ - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { - statuses := modelCatalog.ListAllStatuses(ctx, aiModelCatalog) - choices := make([]qna.Choice, len(statuses)) - for i, status := range statuses { - choices[i] = qna.Choice{ - Label: status, - Value: status, - } - } - p.Choices = choices - return nil - }, - Client: azdClient, - Message: "Filter by model release status?", - HelpMessage: "Select all the model release status that apply to your application.", - EnableFiltering: to.Ptr(false), - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - statuses := value.([]string) - q.State["status"] = statuses - return nil - }, - }, - }, - } - - decisionTree := qna.NewDecisionTree(azdClient, config) - if err := decisionTree.Run(ctx); err != nil { - return fmt.Errorf("failed to run decision tree: %w", err) - } - - jsonBytes, err := json.MarshalIndent(scenarioData, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal scenario data: %w", err) - } - - fmt.Println() - fmt.Println("Captured scenario data:") - fmt.Println() - fmt.Println(string(jsonBytes)) - - return nil - }, - } -} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go new file mode 100644 index 00000000000..3b1aafa4770 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go @@ -0,0 +1,937 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package start + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/azure/azure-dev/cli/azd/pkg/ux" + "github.com/spf13/cobra" +) + +type scenarioInput struct { + SelectedScenario string `json:"selectedScenario,omitempty"` + + UseCustomData bool `json:"useCustomData,omitempty"` + DataTypes []string `json:"dataTypes,omitempty"` + DataLocations []string `json:"dataLocations,omitempty"` + InteractionTypes []string `json:"interactionTypes,omitempty"` + LocalFilePath string `json:"localFilePath,omitempty"` + LocalFileSelection string `json:"localFileSelection,omitempty"` + LocalFileGlobFilter string `json:"localFileGlobFilter,omitempty"` + DatabaseType string `json:"databaseType,omitempty"` + StorageAccountId string `json:"storageAccountId,omitempty"` + DatabaseId string `json:"databaseId,omitempty"` + MessagingType string `json:"messageType,omitempty"` + MessagingId string `json:"messagingId,omitempty"` + ModelTasks []string `json:"modelTasks,omitempty"` + ModelSelections []string `json:"modelSelections,omitempty"` + AppTypes []string `json:"appTypes,omitempty"` + AppResourceIds []string `json:"appResourceIds,omitempty"` + VectorStoreType string `json:"vectorStoreType,omitempty"` + VectorStoreId string `json:"vectorStoreId,omitempty"` +} + +type resourceTypeConfig struct { + ResourceType string + ResourceTypeDisplayName string + Kinds []string +} + +var ( + dbResourceMap = map[string]resourceTypeConfig{ + "db.cosmos": { + ResourceType: "Microsoft.DocumentDB/databaseAccounts", + ResourceTypeDisplayName: "Cosmos Database Account", + Kinds: []string{"GlobalDocumentDB"}, + }, + "db.mongo": { + ResourceType: "Microsoft.DocumentDB/databaseAccounts", + ResourceTypeDisplayName: "MongoDB Database Account", + Kinds: []string{"MongoDB"}, + }, + "db.postgres": { + ResourceType: "Microsoft.DBforPostgreSQL/flexibleServers", + ResourceTypeDisplayName: "PostgreSQL Server", + }, + "db.mysql": { + ResourceType: "Microsoft.DBforMySQL/flexibleServers", + ResourceTypeDisplayName: "MySQL Server", + }, + "db.redis": { + ResourceType: "Microsoft.Cache/Redis", + ResourceTypeDisplayName: "Redis Cache", + }, + } + + vectorStoreMap = map[string]resourceTypeConfig{ + "ai.search": { + ResourceType: "Microsoft.Search/searchServices", + ResourceTypeDisplayName: "AI Search", + }, + "db.cosmos": { + ResourceType: "Microsoft.DocumentDB/databaseAccounts", + ResourceTypeDisplayName: "Cosmos Database Account", + Kinds: []string{"GlobalDocumentDB"}, + }, + } + + messagingResourceMap = map[string]resourceTypeConfig{ + "messaging.eventhubs": { + ResourceType: "Microsoft.EventHub/namespaces", + ResourceTypeDisplayName: "Event Hub Namespace", + }, + "messaging.servicebus": { + ResourceType: "Microsoft.ServiceBus/namespaces", + ResourceTypeDisplayName: "Service Bus Namespace", + }, + } + + appResourceMap = map[string]resourceTypeConfig{ + "webapp": { + ResourceType: "Microsoft.Web/sites", + ResourceTypeDisplayName: "Web App", + Kinds: []string{"app"}, + }, + "containerapp": { + ResourceType: "Microsoft.App/containerApps", + ResourceTypeDisplayName: "Container App", + }, + "functionapp": { + ResourceType: "Microsoft.Web/sites", + ResourceTypeDisplayName: "Function App", + Kinds: []string{"functionapp"}, + }, + "staticwebapp": { + ResourceType: "Microsoft.Web/staticSites", + ResourceTypeDisplayName: "Static Web App", + }, + } +) + +func NewStartCommand() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Get the context of the AZD project & environment.", + RunE: func(cmd *cobra.Command, args []string) error { + // Create a new context that includes the AZD access token + ctx := azdext.WithAccessToken(cmd.Context()) + + // Create a new AZD client + azdClient, err := azdext.NewAzdClient() + if err != nil { + return fmt.Errorf("failed to create azd client: %w", err) + } + + defer azdClient.Close() + + credential, err := azidentity.NewAzureDeveloperCLICredential(nil) + if err != nil { + return fmt.Errorf("failed to create azure credential: %w", err) + } + + action := &startAction{ + azdClient: azdClient, + credential: credential, + azureClient: azure.NewAzureClient(credential), + modelCatalogService: ai.NewModelCatalogService(credential), + scenarioData: &scenarioInput{}, + } + + if err := action.Run(ctx, args); err != nil { + return fmt.Errorf("failed to run start action: %w", err) + } + + jsonBytes, err := json.MarshalIndent(action.scenarioData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal scenario data: %w", err) + } + + fmt.Println() + fmt.Println("Captured scenario data:") + fmt.Println() + fmt.Println(string(jsonBytes)) + + return nil + }, + } +} + +type startAction struct { + credential azcore.TokenCredential + azdClient *azdext.AzdClient + azureContext *azdext.AzureContext + modelCatalogService *ai.ModelCatalogService + azureClient *azure.AzureClient + scenarioData *scenarioInput + modelCatalog []*ai.AiModel +} + +func (a *startAction) Run(ctx context.Context, args []string) error { + _, err := a.azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) + } + + envResponse, err := a.azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) + } + + envValues, err := a.azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ + Name: envResponse.Environment.Name, + }) + if err != nil { + return fmt.Errorf("failed to get environment values: %w", err) + } + + envValueMap := make(map[string]string) + for _, value := range envValues.KeyValues { + envValueMap[value.Key] = value.Value + } + + azureContext := &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], + }, + Resources: []string{}, + } + + a.azureContext = azureContext + + // Build up list of questions + decisionTree := qna.NewDecisionTree(a.createQuestions()) + if err := decisionTree.Run(ctx); err != nil { + return fmt.Errorf("failed to run decision tree: %w", err) + } + + return nil +} + +func (a *startAction) loadAiCatalog(ctx context.Context) error { + if a.modelCatalog != nil { + return nil + } + + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: "Loading AI Model Catalog", + ClearOnStop: true, + }) + + spinner.Start(ctx) + + aiModelCatalog, err := a.modelCatalogService.ListAllModels(ctx, a.azureContext.Scope.SubscriptionId) + if err != nil { + return fmt.Errorf("failed to load AI model catalog: %w", err) + } + + if err := spinner.Stop(ctx); err != nil { + return err + } + + a.modelCatalog = aiModelCatalog + return nil +} + +func (a *startAction) createQuestions() map[string]qna.Question { + welcomeMessage := []string{ + "This tool will help you build an AI scenario using Azure services.", + "Please answer the following questions to get started.", + } + + return map[string]qna.Question{ + "root": { + Binding: &a.scenarioData.SelectedScenario, + Heading: "Welcome to the AI Builder Extension!", + Message: strings.Join(welcomeMessage, "\n"), + Prompt: &qna.SingleSelectPrompt{ + Client: a.azdClient, + Message: "What type of AI scenario are you building?", + HelpMessage: "Choose the scenario that best fits your needs.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, + {Label: "AI Agent", Value: "agent"}, + {Label: "Explore AI Models", Value: "ai-model"}, + }, + }, + Branches: map[any]string{ + "rag": "use-custom-data", + "agent": "agent-tasks", + "ai-model": "model-capabilities", + }, + }, + "use-custom-data": { + Binding: &a.scenarioData.UseCustomData, + Prompt: &qna.ConfirmPrompt{ + Client: a.azdClient, + Message: "Does your application require custom data?", + HelpMessage: "Custom data is data that is not publicly available and is specific to your application.", + DefaultValue: to.Ptr(true), + }, + AfterAsk: func(ctx context.Context, q *qna.Question, _ any) error { + switch a.scenarioData.SelectedScenario { + case "rag": + q.Branches = map[any]string{ + true: "choose-data-types", + false: "rag-user-interaction", + } + case "agent": + q.Branches = map[any]string{ + true: "choose-data-types", + false: "agent-tasks", + } + } + + return nil + }, + }, + "choose-data-types": { + Binding: &a.scenarioData.DataTypes, + Heading: "Data Sources", + Message: "Lets identify all the data source that will be used in your application.", + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "What type of data are you using?", + HelpMessage: "Select all the data types that apply to your application.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, + {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, + {Label: "Videos", Value: "videos"}, + {Label: "Images", Value: "images"}, + {Label: "Audio", Value: "audio"}, + }, + }, + Next: []qna.QuestionReference{{Key: "data-location"}}, + }, + "data-location": { + Binding: &a.scenarioData.DataLocations, + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "Where is your data located?", + HelpMessage: "Select all the data locations that apply to your application.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Azure Blob Storage", Value: "blob-storage"}, + {Label: "Azure Database", Value: "databases"}, + {Label: "Local file system", Value: "local-file-system"}, + {Label: "Other", Value: "other-datasource"}, + }, + }, + Branches: map[any]string{ + "blob-storage": "choose-storage", + "databases": "choose-database-type", + "local-file-system": "local-file-system", + }, + Next: []qna.QuestionReference{{Key: "choose-vector-store-type"}}, + }, + "choose-storage": { + Binding: &a.scenarioData.StorageAccountId, + Heading: "Azure Blob Storage", + Message: "We'll need to setup a storage account to store the data for your application.", + Prompt: &qna.SubscriptionResourcePrompt{ + Client: a.azdClient, + ResourceType: "Microsoft.Storage/storageAccounts", + ResourceTypeDisplayName: "Storage Account", + HelpMessage: "You can select an existing storage account or create a new one.", + AzureContext: a.azureContext, + }, + }, + "choose-database-type": { + Binding: &a.scenarioData.DatabaseType, + Heading: "Azure Database", + Message: "We'll need to setup a database that will be used by your application to power AI model(s).", + Prompt: &qna.SingleSelectPrompt{ + Message: "Which type of database?", + HelpMessage: "Select the type of database that best fits your needs.", + Client: a.azdClient, + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "CosmosDB", Value: "db.cosmos"}, + {Label: "PostgreSQL", Value: "db.postgres"}, + {Label: "MySQL", Value: "db.mysql"}, + {Label: "Redis", Value: "db.redis"}, + {Label: "MongoDB", Value: "db.mongo"}, + }, + }, + Next: []qna.QuestionReference{{Key: "choose-database-resource"}}, + }, + "choose-database-resource": { + Binding: &a.scenarioData.DatabaseId, + Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing database or create a new one.", + Client: a.azdClient, + AzureContext: a.azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := dbResourceMap[a.scenarioData.DatabaseType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + a.scenarioData.DatabaseType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, + }, + }, + "local-file-system": { + Binding: &a.scenarioData.LocalFilePath, + Heading: "Local File System", + Message: "Lets identify the files that will be used in your application. Later on we will upload these files to Azure so they can be used by your application.", + Prompt: &qna.TextPrompt{ + Client: a.azdClient, + Message: "Path to the local files", + HelpMessage: "This path can be absolute or relative to the current working directory. " + + "Please make sure the path is accessible from the machine running this command.", + Placeholder: "./data", + }, + Next: []qna.QuestionReference{{Key: "local-file-choose-files"}}, + }, + "local-file-choose-files": { + Binding: &a.scenarioData.LocalFileSelection, + Prompt: &qna.SingleSelectPrompt{ + Client: a.azdClient, + Message: "Which files?", + HelpMessage: "You can select all files or use a glob expression to filter the files.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "All Files", Value: "all-files"}, + {Label: "Glob Expression", Value: "glob-expression"}, + }, + }, + Branches: map[any]string{ + "glob-expression": "local-file-glob", + }, + }, + "local-file-glob": { + Binding: &a.scenarioData.LocalFileGlobFilter, + Prompt: &qna.TextPrompt{ + Client: a.azdClient, + Message: "Enter a glob expression to filter files", + HelpMessage: "A glob expression is a string that uses wildcard characters to match file names. " + + " For example, *.txt will match all text files in the current directory.", + Placeholder: "*.json", + }, + }, + "choose-vector-store-type": { + Binding: &a.scenarioData.VectorStoreType, + Heading: "Vector Store", + Message: "Based on your choices we're going to need a vector store to store the text embeddings for your data.", + Prompt: &qna.SingleSelectPrompt{ + Message: "What type of vector store do you want to use?", + HelpMessage: "Select the type of vector store that best fits your needs.", + Client: a.azdClient, + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-vector-store"}, + {Label: "AI Search", Value: "ai.search"}, + {Label: "CosmosDB", Value: "db.cosmos"}, + }, + }, + Branches: map[any]string{ + "ai.search": "choose-vector-store-resource", + "db.cosmos": "choose-vector-store-resource", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, _ any) error { + switch a.scenarioData.SelectedScenario { + case "rag": + q.Next = []qna.QuestionReference{{Key: "rag-user-interaction"}} + case "agent": + q.Next = []qna.QuestionReference{{Key: "agent-interaction"}} + } + return nil + }, + }, + "choose-vector-store-resource": { + Binding: &a.scenarioData.VectorStoreId, + Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing vector store or create a new one.", + Client: a.azdClient, + AzureContext: a.azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := vectorStoreMap[a.scenarioData.VectorStoreType] + if !has { + return fmt.Errorf( + "unknown resource type for vector store: %s", + a.scenarioData.VectorStoreType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, + }, + }, + "rag-user-interaction": { + Binding: &a.scenarioData.InteractionTypes, + Heading: "Application Hosting", + Message: "Now we will figure out all the different ways users will interact with your application.", + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "How do you want users to interact with the data?", + HelpMessage: "Select all the data interaction types that apply to your application.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "chatbot"}, + {Label: "Web Application", Value: "webapp"}, + }, + }, + Branches: map[any]string{ + "chatbot": "choose-app-type", + "webapp": "choose-app-type", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["interactionTypes"] = value + return nil + }, + Next: []qna.QuestionReference{{Key: "start-choose-models"}}, + }, + "agent-interaction": { + Binding: &a.scenarioData.InteractionTypes, + Heading: "Agent Hosting", + Message: "Now we will figure out all the different ways users and systems will interact with your agent.", + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "How do you want users to interact with the agent?", + HelpMessage: "Select all the data interaction types that apply to your application.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Chatbot", Value: "chatbot"}, + {Label: "Web Application", Value: "webapp"}, + {Label: "Message Queue", Value: "messaging"}, + }, + }, + Branches: map[any]string{ + "chatbot": "choose-app-type", + "webapp": "choose-app-type", + "messaging": "choose-messaging-type", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["interactionTypes"] = value + return nil + }, + Next: []qna.QuestionReference{{Key: "start-choose-model"}}, + }, + "agent-tasks": { + Binding: &a.scenarioData.ModelTasks, + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "What tasks do you want the AI agent to perform?", + HelpMessage: "Select all the tasks that apply to your application.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Custom Function Calling", Value: "custom-function-calling"}, + {Label: "Integrate with Open API based services", Value: "openapi"}, + {Label: "Run Azure Functions", Value: "azure-functions"}, + {Label: "Other", Value: "other-model-tasks"}, + }, + }, + Next: []qna.QuestionReference{{Key: "use-custom-data"}}, + }, + "choose-app-type": { + Binding: &a.scenarioData.AppTypes, + BeforeAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["interactionType"] = value + return nil + }, + Prompt: &qna.SingleSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { + appName := q.State["interactionType"].(string) + p.Message = fmt.Sprintf( + "Which type of application do you want to build for %s?", + appName, + ) + return nil + }, + Client: a.azdClient, + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-app"}, + {Label: "App Service (Coming Soon)", Value: "webapp"}, + {Label: "Container App", Value: "containerapp"}, + {Label: "Function App (Coming Soon)", Value: "functionapp"}, + {Label: "Static Web App (Coming Soon)", Value: "staticwebapp"}, + {Label: "Other", Value: "otherapp"}, + }, + }, + Branches: map[any]string{ + "containerapp": "choose-app-resource", + }, + }, + "choose-app-resource": { + Binding: &a.scenarioData.AppResourceIds, + BeforeAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["appType"] = value + return nil + }, + Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing application or create a new one.", + Client: a.azdClient, + AzureContext: a.azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + appType := q.State["appType"].(string) + + resourceType, has := appResourceMap[appType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + appType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, + }, + }, + "choose-messaging-type": { + Binding: &a.scenarioData.MessagingType, + Prompt: &qna.SingleSelectPrompt{ + Client: a.azdClient, + Message: "Which messaging service do you want to use?", + HelpMessage: "Select the messaging service that best fits your needs.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-messaging"}, + {Label: "Azure Service Bus", Value: "messaging.eventhubs"}, + {Label: "Azure Event Hubs", Value: "messaging.servicebus"}, + }, + }, + Branches: map[any]string{ + "messaging.eventhubs": "choose-messaging-resource", + "messaging.servicebus": "choose-messaging-resource", + }, + }, + "choose-messaging-resource": { + Binding: &a.scenarioData.MessagingId, + Prompt: &qna.SubscriptionResourcePrompt{ + HelpMessage: "You can select an existing messaging service or create a new one.", + Client: a.azdClient, + AzureContext: a.azureContext, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { + resourceType, has := messagingResourceMap[a.scenarioData.MessagingType] + if !has { + return fmt.Errorf( + "unknown resource type for database: %s", + a.scenarioData.MessagingType, + ) + } + + p.ResourceType = resourceType.ResourceType + p.Kinds = resourceType.Kinds + p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName + + return nil + }, + }, + }, + "model-exploration": { + Binding: &a.scenarioData.ModelTasks, + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "What type of tasks should the AI models perform?", + HelpMessage: "Select all the tasks that apply to your application. " + + "These tasks will help you narrow down the type of models you need.", + Choices: []qna.Choice{ + {Label: "Text Generation", Value: "text-generation"}, + {Label: "Image Generation", Value: "image-generation"}, + {Label: "Audio Generation", Value: "audio-generation"}, + {Label: "Video Generation", Value: "video-generation"}, + {Label: "Text Classification", Value: "text-classification"}, + {Label: "Image Classification", Value: "image-classification"}, + {Label: "Audio Classification", Value: "audio-classification"}, + {Label: "Video Classification", Value: "video-classification"}, + {Label: "Text Summarization", Value: "text-summarization"}, + }, + }, + }, + "start-choose-models": { + Heading: "AI Model Selection", + Message: "Now we will figure out the best AI model(s) for your application.", + BeforeAsk: func(ctx context.Context, question *qna.Question, value any) error { + if err := a.loadAiCatalog(ctx); err != nil { + return fmt.Errorf("failed to load AI model catalog: %w", err) + } + + return nil + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.Next = []qna.QuestionReference{ + { + Key: "start-choose-model", + State: map[string]any{ + "modelSelectMessage": "Lets choose a chat completion model", + "capabilities": []string{"chatCompletion"}, + }, + }, + { + Key: "start-choose-model", + State: map[string]any{ + "modelSelectMessage": "Lets choose a text embedding model", + "capabilities": []string{"embeddings"}, + }, + }, + } + + return nil + }, + }, + "start-choose-model": { + BeforeAsk: func(ctx context.Context, question *qna.Question, value any) error { + if err := a.loadAiCatalog(ctx); err != nil { + return fmt.Errorf("failed to load AI model catalog: %w", err) + } + + return nil + }, + Prompt: &qna.SingleSelectPrompt{ + Client: a.azdClient, + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { + // Override the message + if message, ok := q.State["modelSelectMessage"].(string); ok { + p.Message = message + } + + return nil + }, + Message: "How do you want to find the right model?", + HelpMessage: "Select the option that best fits your needs.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "choose-model"}, + {Label: "Help me choose", Value: "guide-model"}, + {Label: "I will choose model", Value: "user-model"}, + }, + }, + Branches: map[any]string{ + "guide-model": "guide-model-select", + "user-model": "user-model-select", + }, + }, + "user-model-select": { + Binding: &a.scenarioData.ModelSelections, + Prompt: &qna.SingleSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { + var capabilities []string + var formats []string + var statuses []string + var locations []string + + if val, ok := q.State["capabilities"]; ok { + capabilities = val.([]string) + } + if val, ok := q.State["formats"]; ok { + formats = val.([]string) + } + if val, ok := q.State["status"]; ok { + statuses = val.([]string) + } + if val, ok := q.State["locations"]; ok { + locations = val.([]string) + } + + filterOptions := &ai.FilterOptions{ + Capabilities: capabilities, + Formats: formats, + Statuses: statuses, + Locations: locations, + } + filteredModels := a.modelCatalogService.ListFilteredModels(ctx, a.modelCatalog, filterOptions) + + choices := make([]qna.Choice, len(filteredModels)) + for i, model := range filteredModels { + choices[i] = qna.Choice{ + Label: fmt.Sprintf("%s %s", + model.Name, + output.WithGrayFormat("(%s)", *model.Locations[0].Model.Model.Format), + ), + Value: model.Name, + } + } + p.Choices = choices + + return nil + }, + Client: a.azdClient, + Message: "Which model do you want to use?", + HelpMessage: "Select the model that best fits your needs.", + }, + }, + "guide-model-select": { + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "Filter AI Models", + HelpMessage: "Select all the filters that apply to your application. " + + "These filters will help you narrow down the type of models you need.", + EnableFiltering: to.Ptr(false), + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + choices := []qna.Choice{} + + if _, has := q.State["capabilities"]; !has { + choices = append(choices, qna.Choice{ + Label: "Filter by capabilities", + Value: "filter-model-capability", + }) + } + + if _, has := q.State["formats"]; !has { + choices = append(choices, qna.Choice{ + Label: "Filter by author", + Value: "filter-model-format", + }) + } + if _, has := q.State["status"]; !has { + choices = append(choices, qna.Choice{ + Label: "Filter by status", + Value: "filter-model-status", + }) + } + if _, has := q.State["locations"]; !has { + choices = append(choices, qna.Choice{ + Label: "Filter by location", + Value: "filter-model-location", + }) + } + + p.Choices = choices + return nil + }, + }, + Branches: map[any]string{ + "filter-model-capability": "filter-model-capability", + "filter-model-format": "filter-model-format", + "filter-model-status": "filter-model-status", + "filter-model-location": "filter-model-location", + }, + Next: []qna.QuestionReference{{Key: "user-model-select"}}, + }, + "filter-model-capability": { + Prompt: &qna.MultiSelectPrompt{ + Client: a.azdClient, + Message: "What capabilities do you want the model to have?", + HelpMessage: "Select all the capabilities that apply to your application.", + Choices: []qna.Choice{ + {Label: "Audio", Value: "audio"}, + {Label: "Chat Completion", Value: "chatCompletion"}, + {Label: "Text Completion", Value: "completion"}, + {Label: "Generate Vector Embeddings", Value: "embeddings"}, + {Label: "Image Generation", Value: "imageGenerations"}, + }, + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["capabilities"] = value + return nil + }, + }, + "filter-model-format": { + Prompt: &qna.MultiSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + formats := a.modelCatalogService.ListAllFormats(ctx, a.modelCatalog) + choices := make([]qna.Choice, len(formats)) + for i, format := range formats { + choices[i] = qna.Choice{ + Label: format, + Value: format, + } + } + p.Choices = choices + return nil + }, + Client: a.azdClient, + Message: "Filter my by company or creator", + HelpMessage: "Select all the companies or creators that apply to your application.", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["formats"] = value + return nil + }, + }, + "filter-model-status": { + Prompt: &qna.MultiSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + statuses := a.modelCatalogService.ListAllStatuses(ctx, a.modelCatalog) + choices := make([]qna.Choice, len(statuses)) + for i, status := range statuses { + choices[i] = qna.Choice{ + Label: status, + Value: status, + } + } + p.Choices = choices + return nil + }, + Client: a.azdClient, + Message: "Filter by model release status?", + HelpMessage: "Select all the model release status that apply to your application.", + EnableFiltering: to.Ptr(false), + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["status"] = value + return nil + }, + }, + "filter-model-location": { + Prompt: &qna.MultiSelectPrompt{ + BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: "Loading Locations", + }) + + err := spinner.Run(ctx, func(ctx context.Context) error { + locations, err := a.azureClient.ListLocations(ctx, a.azureContext.Scope.SubscriptionId) + if err != nil { + return fmt.Errorf("failed to list locations: %w", err) + } + + choices := make([]qna.Choice, len(locations)) + for i, location := range locations { + choices[i] = qna.Choice{ + Label: fmt.Sprintf("%s (%s)", *location.DisplayName, *location.Name), + Value: *location.Name, + } + } + + p.Choices = choices + return nil + }) + if err != nil { + return fmt.Errorf("failed to load locations: %w", err) + } + + return nil + }, + Client: a.azdClient, + Message: "Filter by model location?", + HelpMessage: "Select all the model locations that apply to your application.", + }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.State["locations"] = value + return nil + }, + }, + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go index 5063e35ee19..bab38facfcc 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go @@ -17,24 +17,34 @@ type AiModel struct { Locations []*AiModelLocation } +type AiModelDescription struct { + Name string + Format string + Kind string + Capabilities []string + Status string + Locations []string + SKUs []string +} + type AiModelLocation struct { Model *armcognitiveservices.Model Location *armsubscriptions.Location } -type ModelCatalog struct { +type ModelCatalogService struct { credential azcore.TokenCredential azureClient *azure.AzureClient } -func NewModelCatalog(credential azcore.TokenCredential) *ModelCatalog { - return &ModelCatalog{ +func NewModelCatalogService(credential azcore.TokenCredential) *ModelCatalogService { + return &ModelCatalogService{ credential: credential, azureClient: azure.NewAzureClient(credential), } } -func (c *ModelCatalog) ListAllCapabilities(ctx context.Context, models []*AiModel) []string { +func (c *ModelCatalogService) ListAllCapabilities(ctx context.Context, models []*AiModel) []string { return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { capabilities := []string{} for key := range m.Model.Capabilities { @@ -45,25 +55,25 @@ func (c *ModelCatalog) ListAllCapabilities(ctx context.Context, models []*AiMode }) } -func (c *ModelCatalog) ListAllStatuses(ctx context.Context, models []*AiModel) []string { +func (c *ModelCatalogService) ListAllStatuses(ctx context.Context, models []*AiModel) []string { return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { return []string{string(*m.Model.LifecycleStatus)} }) } -func (c *ModelCatalog) ListAllFormats(ctx context.Context, models []*AiModel) []string { +func (c *ModelCatalogService) ListAllFormats(ctx context.Context, models []*AiModel) []string { return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { return []string{*m.Model.Format} }) } -func (c *ModelCatalog) ListAllKinds(ctx context.Context, models []*AiModel) []string { +func (c *ModelCatalogService) ListAllKinds(ctx context.Context, models []*AiModel) []string { return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { return []string{*m.Kind} }) } -func (c *ModelCatalog) ListModelVersions(ctx context.Context, model *AiModel) ([]string, error) { +func (c *ModelCatalogService) ListModelVersions(ctx context.Context, model *AiModel) ([]string, error) { versions := make(map[string]struct{}) for _, location := range model.Locations { versions[*location.Model.Model.Version] = struct{}{} @@ -79,7 +89,7 @@ func (c *ModelCatalog) ListModelVersions(ctx context.Context, model *AiModel) ([ return versionList, nil } -func (c *ModelCatalog) ListModelSkus(ctx context.Context, model *AiModel) ([]string, error) { +func (c *ModelCatalogService) ListModelSkus(ctx context.Context, model *AiModel) ([]string, error) { skus := make(map[string]struct{}) for _, location := range model.Locations { for _, sku := range location.Model.Model.SKUs { @@ -105,7 +115,7 @@ type FilterOptions struct { Locations []string } -func (c *ModelCatalog) ListFilteredModels(ctx context.Context, allModels []*AiModel, options *FilterOptions) []*AiModel { +func (c *ModelCatalogService) ListFilteredModels(ctx context.Context, allModels []*AiModel, options *FilterOptions) []*AiModel { if options == nil { return allModels } @@ -113,40 +123,51 @@ func (c *ModelCatalog) ListFilteredModels(ctx context.Context, allModels []*AiMo filteredModels := []*AiModel{} for _, model := range allModels { + // Initialize flags to true if the corresponding filter is not provided. + isCapabilityMatch := len(options.Capabilities) == 0 + isLocationMatch := len(options.Locations) == 0 + isStatusMatch := len(options.Statuses) == 0 + isFormatMatch := len(options.Formats) == 0 + isKindMatch := len(options.Kinds) == 0 + for _, location := range model.Locations { - if len(options.Capabilities) > 0 { - for _, capability := range options.Capabilities { - if _, exists := location.Model.Model.Capabilities[capability]; !exists { - continue + if !isCapabilityMatch && len(options.Capabilities) > 0 { + for modelCapability := range location.Model.Model.Capabilities { + if slices.Contains(options.Capabilities, modelCapability) { + isCapabilityMatch = true + break } } } - if len(options.Locations) > 0 && !slices.Contains(options.Locations, *location.Location.Name) { - continue + if !isLocationMatch && len(options.Locations) > 0 && slices.Contains(options.Locations, *location.Location.Name) { + isLocationMatch = true } - if len(options.Statuses) > 0 && slices.Contains(options.Statuses, string(*location.Model.Model.LifecycleStatus)) { - continue + if !isStatusMatch && len(options.Statuses) > 0 && + slices.Contains(options.Statuses, string(*location.Model.Model.LifecycleStatus)) { + isStatusMatch = true } - if len(options.Formats) > 0 && slices.Contains(options.Formats, *location.Model.Model.Format) { - continue + if !isFormatMatch && len(options.Formats) > 0 && slices.Contains(options.Formats, *location.Model.Model.Format) { + isFormatMatch = true } - if len(options.Kinds) > 0 && slices.Contains(options.Kinds, *location.Model.Kind) { - continue + if !isKindMatch && len(options.Kinds) > 0 && slices.Contains(options.Kinds, *location.Model.Kind) { + isKindMatch = true } } - filteredModels = append(filteredModels, model) + if isLocationMatch && isCapabilityMatch && isFormatMatch && isStatusMatch && isKindMatch { + filteredModels = append(filteredModels, model) + } } return filteredModels } -func (c *ModelCatalog) ListAllModels(ctx context.Context, subscriptionId string) ([]*AiModel, error) { - locations, err := c.azureClient.ListLocation(ctx, subscriptionId) +func (c *ModelCatalogService) ListAllModels(ctx context.Context, subscriptionId string) ([]*AiModel, error) { + locations, err := c.azureClient.ListLocations(ctx, subscriptionId) if err != nil { return nil, err } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go index f022f7ac3eb..89f0d319592 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go @@ -2,6 +2,8 @@ package azure import ( "context" + "slices" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" @@ -17,7 +19,7 @@ func NewAzureClient(credential azcore.TokenCredential) *AzureClient { } } -func (c *AzureClient) ListLocation(ctx context.Context, subscriptionId string) ([]*armsubscriptions.Location, error) { +func (c *AzureClient) ListLocations(ctx context.Context, subscriptionId string) ([]*armsubscriptions.Location, error) { client, err := createSubscriptionsClient(subscriptionId, c.credential) if err != nil { return nil, err @@ -34,6 +36,10 @@ func (c *AzureClient) ListLocation(ctx context.Context, subscriptionId string) ( locations = append(locations, page.Value...) } + slices.SortFunc(locations, func(a, b *armsubscriptions.Location) int { + return strings.Compare(*a.Name, *b.Name) + }) + return locations, nil } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 2d1f3b4e8c1..90f8e1eda6c 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -9,25 +9,18 @@ import ( "fmt" "log" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "dario.cat/mergo" "github.com/fatih/color" ) // DecisionTree represents the entire decision tree structure. type DecisionTree struct { - azdClient *azdext.AzdClient - config DecisionTreeConfig + questions map[string]Question } -type DecisionTreeConfig struct { - Questions map[string]Question `json:"questions"` - End EndNode `json:"end"` -} - -func NewDecisionTree(azdClient *azdext.AzdClient, config DecisionTreeConfig) *DecisionTree { +func NewDecisionTree(questions map[string]Question) *DecisionTree { return &DecisionTree{ - azdClient: azdClient, - config: config, + questions: questions, } } @@ -37,19 +30,22 @@ type Prompt interface { // Question represents a single prompt in the decision tree. type Question struct { - ID string `json:"id"` - Branches map[any]string `json:"branches"` - Next string `json:"next"` - Binding any `json:"-"` - Heading string `json:"heading,omitempty"` - Help string `json:"help,omitempty"` + Branches map[any]string `json:"branches"` + Next []QuestionReference `json:"next"` + Binding any `json:"-"` + Heading string `json:"heading,omitempty"` + Help string `json:"help,omitempty"` Message string Prompt Prompt `json:"prompt,omitempty"` State map[string]any - BeforeAsk func(ctx context.Context, question *Question) error + BeforeAsk func(ctx context.Context, question *Question, value any) error AfterAsk func(ctx context.Context, question *Question, value any) error } +type QuestionReference struct { + Key string `json:"id"` + State map[string]any +} type Choice struct { Label string `json:"label"` Value string `json:"value"` @@ -70,20 +66,19 @@ const ( MultiSelect QuestionType = "multi_select" ) +// Run iterates through the decision tree and asks questions based on the user's responses. +// It starts from the root question and continues until it reaches an end node. func (t *DecisionTree) Run(ctx context.Context) error { - rootQuestion, has := t.config.Questions["root"] + rootQuestion, has := t.questions["root"] if !has { return errors.New("root question not found") } - return t.askQuestion(ctx, rootQuestion) + return t.askQuestion(ctx, rootQuestion, nil) } -func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error { - if question.Prompt == nil { - return errors.New("question prompt is nil") - } - +// askQuestion recursively asks questions based on the decision tree structure. +func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value any) error { if question.State == nil { question.State = map[string]any{} } @@ -99,84 +94,138 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question) error } if question.BeforeAsk != nil { - if err := question.BeforeAsk(ctx, &question); err != nil { + if err := question.BeforeAsk(ctx, &question, value); err != nil { return fmt.Errorf("before ask function failed: %w", err) } } - value, err := question.Prompt.Ask(ctx, question) - if err != nil { - return fmt.Errorf("failed to ask question: %w", err) + var response any + var err error + + if question.Prompt != nil { + response, err = question.Prompt.Ask(ctx, question) + if err != nil { + return fmt.Errorf("failed to ask question: %w", err) + } } - var nextQuestionKey string + if question.AfterAsk != nil { + if err := question.AfterAsk(ctx, &question, response); err != nil { + return fmt.Errorf("after ask function failed: %w", err) + } + } + + t.applyBinding(question, response) // Handle the case where the branch is based on the user's response if len(question.Branches) > 0 { - switch v := value.(type) { + selectionValues := []any{} + + switch result := response.(type) { case string: - if branch, has := question.Branches[v]; has { - nextQuestionKey = branch + selectionValues = append(selectionValues, result) + case bool: + selectionValues = append(selectionValues, result) + case []string: + for _, selectedValue := range result { + selectionValues = append(selectionValues, selectedValue) } + default: + return errors.New("unsupported value type") + } - if question.AfterAsk != nil { - if err := question.AfterAsk(ctx, &question, v); err != nil { - return fmt.Errorf("after ask function failed: %w", err) - } - } - case bool: - if branch, has := question.Branches[v]; has { - nextQuestionKey = branch + // We need to process all the question branches from the selected values + // Iterate through the selected values and find the corresponding branches + for _, selectedValue := range selectionValues { + branch, has := question.Branches[selectedValue] + if !has { + log.Printf("branch not found for selected value: %s\n", selectedValue) + continue } - if question.AfterAsk != nil { - if err := question.AfterAsk(ctx, &question, v); err != nil { - return fmt.Errorf("after ask function failed: %w", err) - } + nextQuestion, has := t.questions[branch] + if !has { + return fmt.Errorf("question not found for branch: %s", branch) } - case []string: - // Handle multi-select case - for _, selectedValue := range v { - if question.AfterAsk != nil { - if err := question.AfterAsk(ctx, &question, selectedValue); err != nil { - return fmt.Errorf("after ask function failed: %w", err) - } - } - - branch, has := question.Branches[selectedValue] - if !has { - log.Printf("branch not found for selected value: %s\n", selectedValue) - continue - } - - nextQuestion, has := t.config.Questions[branch] - if !has { - return fmt.Errorf("question not found for branch: %s", branch) - } - - nextQuestion.State = question.State - if err = t.askQuestion(ctx, nextQuestion); err != nil { - return fmt.Errorf("failed to ask question: %w", err) - } + + nextQuestion.State = question.State + if err = t.askQuestion(ctx, nextQuestion, selectedValue); err != nil { + return fmt.Errorf("failed to ask question: %w", err) } - default: - return errors.New("unsupported value type") + + mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride) } } - if nextQuestionKey == "" { - nextQuestionKey = question.Next + // After processing branches, we need to check if there is a next question + if len(question.Next) == 0 { + return nil } - if nextQuestionKey == "" || nextQuestionKey == "end" { - return nil + for _, nextQuestionRef := range question.Next { + nextQuestion, has := t.questions[nextQuestionRef.Key] + if !has { + return fmt.Errorf("next question not found: %s", nextQuestionRef.Key) + } + + nextQuestion.State = question.State + if nextQuestionRef.State != nil { + mergo.Merge(&nextQuestion.State, nextQuestionRef.State, mergo.WithOverride) + } + + if err = t.askQuestion(ctx, nextQuestion, response); err != nil { + return fmt.Errorf("failed to ask next question: %w", err) + } + + mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride) } - nextQuestion, has := t.config.Questions[nextQuestionKey] - if !has { - return fmt.Errorf("next question not found: %s", nextQuestionKey) + return nil +} + +// applyBinding applies the value to the binding if it exists. +func (t *DecisionTree) applyBinding(question Question, value any) { + if question.Binding == nil { + return } - nextQuestion.State = question.State - return t.askQuestion(ctx, nextQuestion) + switch binding := question.Binding.(type) { + case *bool: + if boolValue, ok := value.(bool); ok { + *binding = boolValue + } + case *int: + if intValue, ok := value.(int); ok { + *binding = intValue + } + case *float64: + if floatValue, ok := value.(float64); ok { + *binding = floatValue + } + case *int64: + if int64Value, ok := value.(int64); ok { + *binding = int64Value + } + case *float32: + if float32Value, ok := value.(float32); ok { + *binding = float32Value + } + case *int32: + if int32Value, ok := value.(int32); ok { + *binding = int32Value + } + case *string: + if strValue, ok := value.(string); ok { + *binding = strValue + } + case *[]string: + if strSliceValue, ok := value.([]string); ok { + *binding = append(*binding, strSliceValue...) + } + if strValue, ok := value.(string); ok { + *binding = append(*binding, strValue) + } + default: + log.Printf("unsupported binding type: %T\n", binding) + } } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go index 4dfb812b2e8..8347169ad5c 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/prompt.go @@ -35,10 +35,6 @@ func (p *TextPrompt) Ask(ctx context.Context, question Question) (any, error) { return nil, err } - if stringPtr, ok := question.Binding.(*string); ok { - *stringPtr = promptResponse.Value - } - return promptResponse.Value, nil } @@ -80,10 +76,6 @@ func (p *SingleSelectPrompt) Ask(ctx context.Context, question Question) (any, e selectedChoice := p.Choices[*selectResponse.Value] - if stringPtr, ok := question.Binding.(*string); ok { - *stringPtr = selectedChoice.Value - } - return selectedChoice.Value, nil } @@ -128,10 +120,6 @@ func (p *MultiSelectPrompt) Ask(ctx context.Context, question Question) (any, er selectedChoices[i] = value.Value } - if stringPtr, ok := question.Binding.(*[]string); ok { - *stringPtr = selectedChoices - } - return selectedChoices, nil } @@ -156,10 +144,6 @@ func (p *ConfirmPrompt) Ask(ctx context.Context, question Question) (any, error) return nil, err } - if boolPtr, ok := question.Binding.(*bool); ok { - *boolPtr = *confirmResponse.Value - } - return *confirmResponse.Value, nil } @@ -197,9 +181,5 @@ func (p *SubscriptionResourcePrompt) Ask(ctx context.Context, question Question) return nil, err } - if stringPtr, ok := question.Binding.(*string); ok { - *stringPtr = resourceResponse.Resource.Id - } - return resourceResponse.Resource.Id, nil } From 2f248c93284a1d3d4963a344e2edf6d916551632 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 12 Mar 2025 16:39:43 -0700 Subject: [PATCH 07/31] minor updates --- .../internal/cmd/start/start.go | 108 ++++++++++-------- .../internal/pkg/qna/decision_tree.go | 4 +- cli/azd/pkg/output/colors.go | 2 +- 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go index 3b1aafa4770..6ec1c00dbb5 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go @@ -264,13 +264,12 @@ func (a *startAction) createQuestions() map[string]qna.Question { Choices: []qna.Choice{ {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, {Label: "AI Agent", Value: "agent"}, - {Label: "Explore AI Models", Value: "ai-model"}, + {Label: "Other Scenarios (Coming Soon)", Value: "other-scenarios"}, }, }, Branches: map[any]string{ - "rag": "use-custom-data", - "agent": "agent-tasks", - "ai-model": "model-capabilities", + "rag": "use-custom-data", + "agent": "agent-tasks", }, }, "use-custom-data": { @@ -340,19 +339,19 @@ func (a *startAction) createQuestions() map[string]qna.Question { }, "choose-storage": { Binding: &a.scenarioData.StorageAccountId, - Heading: "Azure Blob Storage", + Heading: "Storage Account", Message: "We'll need to setup a storage account to store the data for your application.", Prompt: &qna.SubscriptionResourcePrompt{ Client: a.azdClient, ResourceType: "Microsoft.Storage/storageAccounts", ResourceTypeDisplayName: "Storage Account", - HelpMessage: "You can select an existing storage account or create a new one.", + HelpMessage: "Select an existing storage account or create a new one.", AzureContext: a.azureContext, }, }, "choose-database-type": { Binding: &a.scenarioData.DatabaseType, - Heading: "Azure Database", + Heading: "Database", Message: "We'll need to setup a database that will be used by your application to power AI model(s).", Prompt: &qna.SingleSelectPrompt{ Message: "Which type of database?", @@ -372,7 +371,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { "choose-database-resource": { Binding: &a.scenarioData.DatabaseId, Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing database or create a new one.", + HelpMessage: "Select an existing database or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -410,7 +409,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "Which files?", - HelpMessage: "You can select all files or use a glob expression to filter the files.", + HelpMessage: "Select all files or use a glob expression to filter the files.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "All Files", Value: "all-files"}, @@ -463,7 +462,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { "choose-vector-store-resource": { Binding: &a.scenarioData.VectorStoreId, Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing vector store or create a new one.", + HelpMessage: "Select an existing vector store or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -568,8 +567,8 @@ func (a *startAction) createQuestions() map[string]qna.Question { EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "choose-app"}, - {Label: "App Service (Coming Soon)", Value: "webapp"}, {Label: "Container App", Value: "containerapp"}, + {Label: "App Service (Coming Soon)", Value: "webapp"}, {Label: "Function App (Coming Soon)", Value: "functionapp"}, {Label: "Static Web App (Coming Soon)", Value: "staticwebapp"}, {Label: "Other", Value: "otherapp"}, @@ -586,7 +585,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { return nil }, Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing application or create a new one.", + HelpMessage: "Select an existing application or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -629,7 +628,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { "choose-messaging-resource": { Binding: &a.scenarioData.MessagingId, Prompt: &qna.SubscriptionResourcePrompt{ - HelpMessage: "You can select an existing messaging service or create a new one.", + HelpMessage: "Select an existing messaging service or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { @@ -649,26 +648,6 @@ func (a *startAction) createQuestions() map[string]qna.Question { }, }, }, - "model-exploration": { - Binding: &a.scenarioData.ModelTasks, - Prompt: &qna.MultiSelectPrompt{ - Client: a.azdClient, - Message: "What type of tasks should the AI models perform?", - HelpMessage: "Select all the tasks that apply to your application. " + - "These tasks will help you narrow down the type of models you need.", - Choices: []qna.Choice{ - {Label: "Text Generation", Value: "text-generation"}, - {Label: "Image Generation", Value: "image-generation"}, - {Label: "Audio Generation", Value: "audio-generation"}, - {Label: "Video Generation", Value: "video-generation"}, - {Label: "Text Classification", Value: "text-classification"}, - {Label: "Image Classification", Value: "image-classification"}, - {Label: "Audio Classification", Value: "audio-classification"}, - {Label: "Video Classification", Value: "video-classification"}, - {Label: "Text Summarization", Value: "text-summarization"}, - }, - }, - }, "start-choose-models": { Heading: "AI Model Selection", Message: "Now we will figure out the best AI model(s) for your application.", @@ -677,26 +656,54 @@ func (a *startAction) createQuestions() map[string]qna.Question { return fmt.Errorf("failed to load AI model catalog: %w", err) } - return nil - }, - AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { - q.Next = []qna.QuestionReference{ - { - Key: "start-choose-model", - State: map[string]any{ - "modelSelectMessage": "Lets choose a chat completion model", - "capabilities": []string{"chatCompletion"}, + allModelTypes := map[string]struct { + Heading string + Description string + QuestionReference qna.QuestionReference + }{ + "llm": { + Heading: "Large Language Model (LLM) (For generating responses)", + Description: "Processes user queries and retrieved documents to generate intelligent responses.", + QuestionReference: qna.QuestionReference{ + Key: "start-choose-model", + State: map[string]any{ + "modelSelectMessage": "Lets choose a chat completion model", + "capabilities": []string{"chatCompletion"}, + }, }, }, - { - Key: "start-choose-model", - State: map[string]any{ - "modelSelectMessage": "Lets choose a text embedding model", - "capabilities": []string{"embeddings"}, + "embedding": { + Heading: "Embedding Model (For vectorizing text)", + Description: "Used to convert documents and queries into vector representations for efficient similarity searches.", + QuestionReference: qna.QuestionReference{ + Key: "start-choose-model", + State: map[string]any{ + "modelSelectMessage": "Lets choose a text embedding model", + "capabilities": []string{"embeddings"}, + }, }, }, } + requiredModels := []string{"llm"} + if a.scenarioData.UseCustomData { + requiredModels = append(requiredModels, "embedding") + } + + nextQuestions := []qna.QuestionReference{} + + fmt.Printf(" Based on your choices, you will need the following AI models:\n\n") + for _, model := range requiredModels { + if modelType, ok := allModelTypes[model]; ok { + fmt.Printf(" - %s\n", output.WithBold(modelType.Heading)) + fmt.Printf(" %s\n", output.WithGrayFormat(modelType.Description)) + fmt.Println() + nextQuestions = append(nextQuestions, modelType.QuestionReference) + } + } + + question.Next = nextQuestions + return nil }, }, @@ -780,6 +787,13 @@ func (a *startAction) createQuestions() map[string]qna.Question { Message: "Which model do you want to use?", HelpMessage: "Select the model that best fits your needs.", }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + delete(q.State, "capabilities") + delete(q.State, "formats") + delete(q.State, "status") + delete(q.State, "locations") + return nil + }, }, "guide-model-select": { Prompt: &qna.MultiSelectPrompt{ diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 90f8e1eda6c..b738a8dff27 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -10,7 +10,7 @@ import ( "log" "dario.cat/mergo" - "github.com/fatih/color" + "github.com/azure/azure-dev/cli/azd/pkg/output" ) // DecisionTree represents the entire decision tree structure. @@ -85,7 +85,7 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value if question.Heading != "" { fmt.Println() - color.New(color.FgHiWhite, color.Bold).Printf("%s\n", question.Heading) + fmt.Println(output.WithHintFormat(question.Heading)) } if question.Message != "" { diff --git a/cli/azd/pkg/output/colors.go b/cli/azd/pkg/output/colors.go index 1aac3eded8d..6daa42beccb 100644 --- a/cli/azd/pkg/output/colors.go +++ b/cli/azd/pkg/output/colors.go @@ -40,7 +40,7 @@ func WithHintFormat(text string, a ...interface{}) string { } func WithBold(text string, a ...interface{}) string { - format := color.New(color.Bold) + format := color.New(color.FgHiWhite, color.Bold) return format.Sprintf(text, a...) } From 2abca9e7ad868203149f56806b57e4abf5b9ec01 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 12 Mar 2025 18:21:11 -0700 Subject: [PATCH 08/31] WIP: Adding resources to composability --- .../internal/cmd/root.go | 3 +- .../internal/cmd/{start => }/start.go | 89 ++++++++++++++++--- .../internal/pkg/azure/ai/model_catalog.go | 3 + .../internal/pkg/azure/azure_client.go | 3 + 4 files changed, 85 insertions(+), 13 deletions(-) rename cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/{start => }/start.go (93%) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go index 9ad869a771f..16bdb921086 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/root.go @@ -4,7 +4,6 @@ package cmd import ( - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start" "github.com/spf13/cobra" ) @@ -22,7 +21,7 @@ func NewRootCommand() *cobra.Command { rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") - rootCmd.AddCommand(start.NewStartCommand()) + rootCmd.AddCommand(newStartCommand()) rootCmd.AddCommand(newVersionCommand()) return rootCmd diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go similarity index 93% rename from cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go rename to cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 6ec1c00dbb5..828b52fc0ea 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package start +package cmd import ( "context" @@ -100,28 +100,28 @@ var ( } appResourceMap = map[string]resourceTypeConfig{ - "webapp": { + "host.webapp": { ResourceType: "Microsoft.Web/sites", ResourceTypeDisplayName: "Web App", Kinds: []string{"app"}, }, - "containerapp": { + "host.containerapp": { ResourceType: "Microsoft.App/containerApps", ResourceTypeDisplayName: "Container App", }, - "functionapp": { + "host.functionapp": { ResourceType: "Microsoft.Web/sites", ResourceTypeDisplayName: "Function App", Kinds: []string{"functionapp"}, }, - "staticwebapp": { + "host.staticwebapp": { ResourceType: "Microsoft.Web/staticSites", ResourceTypeDisplayName: "Static Web App", }, } ) -func NewStartCommand() *cobra.Command { +func newStartCommand() *cobra.Command { return &cobra.Command{ Use: "start", Short: "Get the context of the AZD project & environment.", @@ -217,6 +217,73 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to run decision tree: %w", err) } + resourcesToAdd := []*azdext.ComposedResource{} + for i, appKey := range a.scenarioData.InteractionTypes { + appResource := &azdext.ComposedResource{ + Name: appKey, + Type: a.scenarioData.AppTypes[i], + } + + resourcesToAdd = append(resourcesToAdd, appResource) + } + + if a.scenarioData.DatabaseType != "" { + dbResource := &azdext.ComposedResource{ + Name: "database", + Type: a.scenarioData.DatabaseType, + } + resourcesToAdd = append(resourcesToAdd, dbResource) + } + + // if a.scenarioData.VectorStoreType != "" { + // vectorStoreResource := &azdext.ComposedResource{ + // Name: "vectorStore", + // Type: a.scenarioData.VectorStoreType, + // } + // resourcesToAdd = append(resourcesToAdd, vectorStoreResource) + // } + + if a.scenarioData.UseCustomData { + storageConfig := map[string]any{ + "containers": []string{"blobs"}, + } + + storageConfigJson, err := json.Marshal(storageConfig) + if err != nil { + return fmt.Errorf("failed to marshal storage config: %w", err) + } + + storageResource := &azdext.ComposedResource{ + Name: "storage", + Type: "storage", + Config: storageConfigJson, + } + + resourcesToAdd = append(resourcesToAdd, storageResource) + } + + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: "Adding infrastructure resources to project", + ClearOnStop: true, + }) + + if err := spinner.Start(ctx); err != nil { + return fmt.Errorf("failed to start spinner: %w", err) + } + + for _, resource := range resourcesToAdd { + _, err := a.azdClient.Compose().AddResource(ctx, &azdext.AddResourceRequest{ + Resource: resource, + }) + if err != nil { + return fmt.Errorf("failed to add resource %s: %w", resource.Name, err) + } + } + + if err := spinner.Stop(ctx); err != nil { + return fmt.Errorf("failed to stop spinner: %w", err) + } + return nil } @@ -567,15 +634,15 @@ func (a *startAction) createQuestions() map[string]qna.Question { EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "choose-app"}, - {Label: "Container App", Value: "containerapp"}, - {Label: "App Service (Coming Soon)", Value: "webapp"}, - {Label: "Function App (Coming Soon)", Value: "functionapp"}, - {Label: "Static Web App (Coming Soon)", Value: "staticwebapp"}, + {Label: "Container App", Value: "host.containerapp"}, + {Label: "App Service (Coming Soon)", Value: "host.webapp"}, + {Label: "Function App (Coming Soon)", Value: "host.functionapp"}, + {Label: "Static Web App (Coming Soon)", Value: "host.staticwebapp"}, {Label: "Other", Value: "otherapp"}, }, }, Branches: map[any]string{ - "containerapp": "choose-app-resource", + "host.containerapp": "choose-app-resource", }, }, "choose-app-resource": { diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go index bab38facfcc..df282f3b864 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package ai import ( diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go index 89f0d319592..4828176aa1b 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azure import ( From 9d985300ac4053797dab1c3ecaf8c3d31f28b386 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 13 Mar 2025 15:40:32 -0700 Subject: [PATCH 09/31] Adds workflow service and updates compose service --- cli/azd/.vscode/cspell.yaml | 4 + .../internal/cmd/start.go | 298 ++++++++++++++---- .../internal/pkg/azure/ai/model_catalog.go | 157 +++++++-- .../internal/pkg/azure/azure_client.go | 4 +- .../internal/pkg/qna/decision_tree.go | 12 +- 5 files changed, 381 insertions(+), 94 deletions(-) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index b2b83caf625..ff86f7a6209 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -154,6 +154,10 @@ overrides: - getenv - errorf - println + - filename: extensions/microsoft.azd.ai.builder/internal/cmd/start.go + words: + - datasource + - vectorizing ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 828b52fc0ea..f153bccaa2f 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -21,6 +20,11 @@ import ( "github.com/spf13/cobra" ) +const ( + defaultChatCompletionModel = "gpt-4o" + defaultEmbeddingModel = "text-embedding-3-small" +) + type scenarioInput struct { SelectedScenario string `json:"selectedScenario,omitempty"` @@ -137,7 +141,20 @@ func newStartCommand() *cobra.Command { defer azdClient.Close() - credential, err := azidentity.NewAzureDeveloperCLICredential(nil) + fmt.Println() + fmt.Println(output.WithHintFormat("Welcome to the AI Builder!")) + fmt.Println("This tool will help you build an AI scenario using Azure services.") + fmt.Println() + + azureContext, err := ensureAzureContext(ctx, azdClient) + if err != nil { + return fmt.Errorf("failed to ensure azure context: %w", err) + } + + credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ + TenantID: azureContext.Scope.TenantId, + AdditionallyAllowedTenants: []string{"*"}, + }) if err != nil { return fmt.Errorf("failed to create azure credential: %w", err) } @@ -145,6 +162,7 @@ func newStartCommand() *cobra.Command { action := &startAction{ azdClient: azdClient, credential: credential, + azureContext: azureContext, azureClient: azure.NewAzureClient(credential), modelCatalogService: ai.NewModelCatalogService(credential), scenarioData: &scenarioInput{}, @@ -154,16 +172,6 @@ func newStartCommand() *cobra.Command { return fmt.Errorf("failed to run start action: %w", err) } - jsonBytes, err := json.MarshalIndent(action.scenarioData, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal scenario data: %w", err) - } - - fmt.Println() - fmt.Println("Captured scenario data:") - fmt.Println() - fmt.Println(string(jsonBytes)) - return nil }, } @@ -176,41 +184,10 @@ type startAction struct { modelCatalogService *ai.ModelCatalogService azureClient *azure.AzureClient scenarioData *scenarioInput - modelCatalog []*ai.AiModel + modelCatalog map[string]*ai.AiModel } func (a *startAction) Run(ctx context.Context, args []string) error { - _, err := a.azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) - } - - envResponse, err := a.azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) - } - - envValues, err := a.azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ - Name: envResponse.Environment.Name, - }) - if err != nil { - return fmt.Errorf("failed to get environment values: %w", err) - } - - envValueMap := make(map[string]string) - for _, value := range envValues.KeyValues { - envValueMap[value.Key] = value.Value - } - - azureContext := &azdext.AzureContext{ - Scope: &azdext.AzureScope{ - SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], - }, - Resources: []string{}, - } - - a.azureContext = azureContext - // Build up list of questions decisionTree := qna.NewDecisionTree(a.createQuestions()) if err := decisionTree.Run(ctx); err != nil { @@ -218,15 +195,33 @@ func (a *startAction) Run(ctx context.Context, args []string) error { } resourcesToAdd := []*azdext.ComposedResource{} + + // Add host resources such as container apps. for i, appKey := range a.scenarioData.InteractionTypes { + appType := a.scenarioData.AppTypes[i] + if appType == "" || appType == "choose-app" { + appType = "host.containerapp" + } + + appConfig := map[string]any{ + "port": 8080, + } + + appConfigJson, err := json.Marshal(appConfig) + if err != nil { + return fmt.Errorf("failed to marshal app config: %w", err) + } + appResource := &azdext.ComposedResource{ - Name: appKey, - Type: a.scenarioData.AppTypes[i], + Name: appKey, + Type: appType, + Config: appConfigJson, } resourcesToAdd = append(resourcesToAdd, appResource) } + // Add database resources if a.scenarioData.DatabaseType != "" { dbResource := &azdext.ComposedResource{ Name: "database", @@ -243,6 +238,7 @@ func (a *startAction) Run(ctx context.Context, args []string) error { // resourcesToAdd = append(resourcesToAdd, vectorStoreResource) // } + // Add storage resources if a.scenarioData.UseCustomData { storageConfig := map[string]any{ "containers": []string{"blobs"}, @@ -262,6 +258,51 @@ func (a *startAction) Run(ctx context.Context, args []string) error { resourcesToAdd = append(resourcesToAdd, storageResource) } + if len(a.scenarioData.ModelSelections) == 0 { + a.scenarioData.ModelSelections = []string{} + + if a.scenarioData.SelectedScenario == "rag" { + a.scenarioData.ModelSelections = append(a.scenarioData.ModelSelections, defaultChatCompletionModel) + if a.scenarioData.UseCustomData { + a.scenarioData.ModelSelections = append(a.scenarioData.ModelSelections, defaultEmbeddingModel) + } + } + } + + models := []*ai.AiModelDeployment{} + + // Add AI model resources + if len(a.scenarioData.ModelSelections) > 0 { + aiProject := &azdext.ComposedResource{ + Name: "ai-project", + Type: "ai.project", + } + + for _, modelName := range a.scenarioData.ModelSelections { + aiModel, exists := a.modelCatalog[modelName] + if exists { + modelDeployment, err := a.modelCatalogService.GetModelDeployment(ctx, aiModel, nil) + if err != nil { + return fmt.Errorf("failed to get model deployment: %w", err) + } + + models = append(models, modelDeployment) + } + } + + resourceConfig := map[string]any{ + "models": models, + } + + configJson, err := json.Marshal(resourceConfig) + if err != nil { + return fmt.Errorf("failed to marshal AI project config: %w", err) + } + + aiProject.Config = configJson + resourcesToAdd = append(resourcesToAdd, aiProject) + } + spinner := ux.NewSpinner(&ux.SpinnerOptions{ Text: "Adding infrastructure resources to project", ClearOnStop: true, @@ -284,6 +325,55 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to stop spinner: %w", err) } + fmt.Println() + fmt.Println(output.WithSuccessFormat("SUCCESS! The following resources have been staged for provisioning:")) + for _, resource := range resourcesToAdd { + fmt.Printf(" - %s (%s)\n", resource.Name, output.WithGrayFormat(resource.Type)) + } + + for _, modelDeployment := range models { + fmt.Printf(" - %s %s\n", + modelDeployment.Name, + output.WithGrayFormat("(Format: %s, Version: %s)", modelDeployment.Format, modelDeployment.Version), + ) + } + + fmt.Println() + confirmResponse, err := a.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: "Do you want to provision resources to your project now?", + DefaultValue: to.Ptr(true), + HelpMessage: "Provisioning resources will create the necessary Azure infrastructure for your application.", + }, + }) + if err != nil { + return fmt.Errorf("failed to confirm provisioning: %w", err) + } + + if *confirmResponse.Value { + workflow := &azdext.Workflow{ + Name: "provision", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"provision"}, + }, + }, + }, + } + + _, err = a.azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ + Workflow: workflow, + }) + + if err != nil { + return fmt.Errorf("failed to run provision workflow: %w", err) + } + } else { + fmt.Println() + fmt.Printf("To provision resources, run %s\n", output.WithHighLightFormat("azd provision")) + } + return nil } @@ -297,7 +387,9 @@ func (a *startAction) loadAiCatalog(ctx context.Context) error { ClearOnStop: true, }) - spinner.Start(ctx) + if err := spinner.Start(ctx); err != nil { + return fmt.Errorf("failed to start spinner: %w", err) + } aiModelCatalog, err := a.modelCatalogService.ListAllModels(ctx, a.azureContext.Scope.SubscriptionId) if err != nil { @@ -312,17 +404,106 @@ func (a *startAction) loadAiCatalog(ctx context.Context) error { return nil } -func (a *startAction) createQuestions() map[string]qna.Question { - welcomeMessage := []string{ - "This tool will help you build an AI scenario using Azure services.", - "Please answer the following questions to get started.", +func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azdext.AzureContext, error) { + _, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return nil, fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) + } + + envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) + if err != nil { + return nil, fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) + } + + envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ + Name: envResponse.Environment.Name, + }) + if err != nil { + return nil, fmt.Errorf("failed to get environment values: %w", err) + } + + envValueMap := make(map[string]string) + for _, value := range envValues.KeyValues { + envValueMap[value.Key] = value.Value } + azureContext := &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + TenantId: envValueMap["AZURE_TENANT_ID"], + SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], + Location: envValueMap["AZURE_LOCATION"], + }, + Resources: []string{}, + } + + if azureContext.Scope.SubscriptionId == "" { + fmt.Print() + fmt.Println("It looks like we first need to connect to your Azure subscription.") + + subscriptionResponse, err := azdClient.Prompt().PromptSubscription(ctx, &azdext.PromptSubscriptionRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to prompt for subscription: %w", err) + } + + azureContext.Scope.SubscriptionId = subscriptionResponse.Subscription.Id + azureContext.Scope.TenantId = subscriptionResponse.Subscription.TenantId + + // Set the subscription ID in the environment + _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ + EnvName: envResponse.Environment.Name, + Key: "AZURE_TENANT_ID", + Value: azureContext.Scope.TenantId, + }) + if err != nil { + return nil, fmt.Errorf("failed to set tenant ID in environment: %w", err) + } + + // Set the tenant ID in the environment + _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ + EnvName: envResponse.Environment.Name, + Key: "AZURE_SUBSCRIPTION_ID", + Value: azureContext.Scope.SubscriptionId, + }) + if err != nil { + return nil, fmt.Errorf("failed to set subscription ID in environment: %w", err) + } + } + + if azureContext.Scope.Location == "" { + fmt.Println() + fmt.Println( + "Next, we need to select a default Azure location that will be used as the target for your infrastructure.", + ) + + locationResponse, err := azdClient.Prompt().PromptLocation(ctx, &azdext.PromptLocationRequest{ + AzureContext: azureContext, + }) + if err != nil { + return nil, fmt.Errorf("failed to prompt for location: %w", err) + } + + azureContext.Scope.Location = locationResponse.Location.Name + + // Set the location in the environment + _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ + EnvName: envResponse.Environment.Name, + Key: "AZURE_LOCATION", + Value: azureContext.Scope.Location, + }) + if err != nil { + return nil, fmt.Errorf("failed to set location in environment: %w", err) + } + } + + return azureContext, nil +} + +func (a *startAction) createQuestions() map[string]qna.Question { return map[string]qna.Question{ "root": { Binding: &a.scenarioData.SelectedScenario, - Heading: "Welcome to the AI Builder Extension!", - Message: strings.Join(welcomeMessage, "\n"), + Heading: "Identify AI Scenario", + Message: "Let's start drilling into your AI scenario to identify all the required infrastructure we will need.", Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "What type of AI scenario are you building?", @@ -461,7 +642,8 @@ func (a *startAction) createQuestions() map[string]qna.Question { "local-file-system": { Binding: &a.scenarioData.LocalFilePath, Heading: "Local File System", - Message: "Lets identify the files that will be used in your application. Later on we will upload these files to Azure so they can be used by your application.", + Message: "Lets identify the files that will be used in your application. " + + "Later on we will upload these files to Azure so they can be used by your application.", Prompt: &qna.TextPrompt{ Client: a.azdClient, Message: "Path to the local files", @@ -638,7 +820,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { {Label: "App Service (Coming Soon)", Value: "host.webapp"}, {Label: "Function App (Coming Soon)", Value: "host.functionapp"}, {Label: "Static Web App (Coming Soon)", Value: "host.staticwebapp"}, - {Label: "Other", Value: "otherapp"}, + {Label: "Other", Value: "other-app"}, }, }, Branches: map[any]string{ @@ -740,8 +922,9 @@ func (a *startAction) createQuestions() map[string]qna.Question { }, }, "embedding": { - Heading: "Embedding Model (For vectorizing text)", - Description: "Used to convert documents and queries into vector representations for efficient similarity searches.", + Heading: "Embedding Model (For vectorizing text)", + Description: "Used to convert documents and queries into vector representations " + + "for efficient similarity searches.", QuestionReference: qna.QuestionReference{ Key: "start-choose-model", State: map[string]any{ @@ -762,6 +945,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { fmt.Printf(" Based on your choices, you will need the following AI models:\n\n") for _, model := range requiredModels { if modelType, ok := allModelTypes[model]; ok { + //nolint printf fmt.Printf(" - %s\n", output.WithBold(modelType.Heading)) fmt.Printf(" %s\n", output.WithGrayFormat(modelType.Description)) fmt.Println() diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go index df282f3b864..5ecfda3597e 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go @@ -5,6 +5,7 @@ package ai import ( "context" + "errors" "slices" "strings" "sync" @@ -20,16 +21,6 @@ type AiModel struct { Locations []*AiModelLocation } -type AiModelDescription struct { - Name string - Format string - Kind string - Capabilities []string - Status string - Locations []string - SKUs []string -} - type AiModelLocation struct { Model *armcognitiveservices.Model Location *armsubscriptions.Location @@ -47,8 +38,8 @@ func NewModelCatalogService(credential azcore.TokenCredential) *ModelCatalogServ } } -func (c *ModelCatalogService) ListAllCapabilities(ctx context.Context, models []*AiModel) []string { - return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { +func (c *ModelCatalogService) ListAllCapabilities(ctx context.Context, allModels map[string]*AiModel) []string { + return filterDistinctModelData(allModels, func(m *armcognitiveservices.Model) []string { capabilities := []string{} for key := range m.Model.Capabilities { capabilities = append(capabilities, key) @@ -58,20 +49,20 @@ func (c *ModelCatalogService) ListAllCapabilities(ctx context.Context, models [] }) } -func (c *ModelCatalogService) ListAllStatuses(ctx context.Context, models []*AiModel) []string { - return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { +func (c *ModelCatalogService) ListAllStatuses(ctx context.Context, allModels map[string]*AiModel) []string { + return filterDistinctModelData(allModels, func(m *armcognitiveservices.Model) []string { return []string{string(*m.Model.LifecycleStatus)} }) } -func (c *ModelCatalogService) ListAllFormats(ctx context.Context, models []*AiModel) []string { - return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { +func (c *ModelCatalogService) ListAllFormats(ctx context.Context, allModels map[string]*AiModel) []string { + return filterDistinctModelData(allModels, func(m *armcognitiveservices.Model) []string { return []string{*m.Model.Format} }) } -func (c *ModelCatalogService) ListAllKinds(ctx context.Context, models []*AiModel) []string { - return filterDistinctModelData(models, func(m *armcognitiveservices.Model) []string { +func (c *ModelCatalogService) ListAllKinds(ctx context.Context, allModels map[string]*AiModel) []string { + return filterDistinctModelData(allModels, func(m *armcognitiveservices.Model) []string { return []string{*m.Kind} }) } @@ -118,9 +109,13 @@ type FilterOptions struct { Locations []string } -func (c *ModelCatalogService) ListFilteredModels(ctx context.Context, allModels []*AiModel, options *FilterOptions) []*AiModel { +func (c *ModelCatalogService) ListFilteredModels( + ctx context.Context, + allModels map[string]*AiModel, + options *FilterOptions, +) []*AiModel { if options == nil { - return allModels + options = &FilterOptions{} } filteredModels := []*AiModel{} @@ -143,7 +138,8 @@ func (c *ModelCatalogService) ListFilteredModels(ctx context.Context, allModels } } - if !isLocationMatch && len(options.Locations) > 0 && slices.Contains(options.Locations, *location.Location.Name) { + if !isLocationMatch && len(options.Locations) > 0 && + slices.Contains(options.Locations, *location.Location.Name) { isLocationMatch = true } @@ -152,11 +148,13 @@ func (c *ModelCatalogService) ListFilteredModels(ctx context.Context, allModels isStatusMatch = true } - if !isFormatMatch && len(options.Formats) > 0 && slices.Contains(options.Formats, *location.Model.Model.Format) { + if !isFormatMatch && len(options.Formats) > 0 && + slices.Contains(options.Formats, *location.Model.Model.Format) { isFormatMatch = true } - if !isKindMatch && len(options.Kinds) > 0 && slices.Contains(options.Kinds, *location.Model.Kind) { + if !isKindMatch && len(options.Kinds) > 0 && + slices.Contains(options.Kinds, *location.Model.Kind) { isKindMatch = true } } @@ -166,10 +164,15 @@ func (c *ModelCatalogService) ListFilteredModels(ctx context.Context, allModels } } + // Sort the filtered models by name + slices.SortFunc(filteredModels, func(a, b *AiModel) int { + return strings.Compare(a.Name, b.Name) + }) + return filteredModels } -func (c *ModelCatalogService) ListAllModels(ctx context.Context, subscriptionId string) ([]*AiModel, error) { +func (c *ModelCatalogService) ListAllModels(ctx context.Context, subscriptionId string) (map[string]*AiModel, error) { locations, err := c.azureClient.ListLocations(ctx, subscriptionId) if err != nil { return nil, err @@ -233,20 +236,110 @@ func (c *ModelCatalogService) ListAllModels(ctx context.Context, subscriptionId return true }) - allModels := []*AiModel{} - for _, model := range modelMap { - allModels = append(allModels, model) + return modelMap, nil + // allModels := []*AiModel{} + // for _, model := range modelMap { + // allModels = append(allModels, model) + // } + + // slices.SortFunc(allModels, func(a, b *AiModel) int { + // return strings.Compare(a.Name, b.Name) + // }) + + // return allModels, nil +} + +type AiModelDeployment struct { + Name string + Format string + Version string + Sku AiModelDeploymentSku +} + +type AiModelDeploymentSku struct { + Name string + UsageName string + Capacity int32 +} + +type AiModelDeploymentOptions struct { + Locations []string + Versions []string + Skus []string +} + +func (c *ModelCatalogService) GetModelDeployment( + ctx context.Context, + model *AiModel, + options *AiModelDeploymentOptions, +) (*AiModelDeployment, error) { + if options == nil { + options = &AiModelDeploymentOptions{ + Skus: []string{ + "GlobalStandard", + "Standard", + }, + } } - slices.SortFunc(allModels, func(a, b *AiModel) int { - return strings.Compare(a.Name, b.Name) - }) + var modelDeployment *AiModelDeployment + + for _, location := range model.Locations { + if modelDeployment != nil { + break + } + + // Check for location match if specified + if len(options.Locations) > 0 && !slices.Contains(options.Locations, *location.Location.Name) { + continue + } + + // Check for version match if specified + if len(options.Versions) > 0 && !slices.Contains(options.Versions, *location.Model.Model.Version) { + continue + } + + // Check for default version if no version is specified + if len(options.Versions) > 0 { + if !slices.Contains(options.Versions, *location.Model.Model.Version) { + continue + } + } else { + if location.Model.Model.IsDefaultVersion != nil && !*location.Model.Model.IsDefaultVersion { + continue + } + } + + // Check for SKU match if specified + for _, sku := range location.Model.Model.SKUs { + if !slices.Contains(options.Skus, *sku.Name) { + continue + } + + modelDeployment = &AiModelDeployment{ + Name: *location.Model.Model.Name, + Format: *location.Model.Model.Format, + Version: *location.Model.Model.Version, + Sku: AiModelDeploymentSku{ + Name: *sku.Name, + UsageName: *sku.UsageName, + Capacity: *sku.Capacity.Default, + }, + } + + break + } + } + + if modelDeployment == nil { + return nil, errors.New("No model deployment found for the specified options") + } - return allModels, nil + return modelDeployment, nil } func filterDistinctModelData( - models []*AiModel, + models map[string]*AiModel, filterFunc func(*armcognitiveservices.Model) []string, ) []string { filtered := make(map[string]struct{}) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go index 4828176aa1b..2cea9b3fb45 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/azure_client.go @@ -46,8 +46,8 @@ func (c *AzureClient) ListLocations(ctx context.Context, subscriptionId string) return locations, nil } -func createSubscriptionsClient(subscriptionId string, credentail azcore.TokenCredential) (*armsubscriptions.Client, error) { - client, err := armsubscriptions.NewClient(credentail, nil) +func createSubscriptionsClient(subscriptionId string, credential azcore.TokenCredential) (*armsubscriptions.Client, error) { + client, err := armsubscriptions.NewClient(credential, nil) if err != nil { return nil, err } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index b738a8dff27..5260ad1c255 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -153,7 +153,9 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value return fmt.Errorf("failed to ask question: %w", err) } - mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride) + if err := mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride); err != nil { + return fmt.Errorf("failed to merge question states: %w", err) + } } } @@ -170,14 +172,18 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value nextQuestion.State = question.State if nextQuestionRef.State != nil { - mergo.Merge(&nextQuestion.State, nextQuestionRef.State, mergo.WithOverride) + if err := mergo.Merge(&nextQuestion.State, nextQuestionRef.State, mergo.WithOverride); err != nil { + return fmt.Errorf("failed to merge question states: %w", err) + } } if err = t.askQuestion(ctx, nextQuestion, response); err != nil { return fmt.Errorf("failed to ask next question: %w", err) } - mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride) + if err := mergo.Merge(&question.State, nextQuestion.State, mergo.WithOverride); err != nil { + return fmt.Errorf("failed to merge question states: %w", err) + } } return nil From 402d1ef7e12b7530964756ba50dfd6c0f7f32650 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Mar 2025 10:14:37 -0700 Subject: [PATCH 10/31] Fixes issues selected models that do not have default versions specified --- .../internal/cmd/start.go | 107 +++++++++++------- .../internal/pkg/azure/ai/model_catalog.go | 13 ++- 2 files changed, 79 insertions(+), 41 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index f153bccaa2f..1b1f45303eb 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -194,6 +195,15 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to run decision tree: %w", err) } + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: "Adding infrastructure resources to project", + ClearOnStop: true, + }) + + if err := spinner.Start(ctx); err != nil { + return fmt.Errorf("failed to start spinner: %w", err) + } + resourcesToAdd := []*azdext.ComposedResource{} // Add host resources such as container apps. @@ -258,17 +268,6 @@ func (a *startAction) Run(ctx context.Context, args []string) error { resourcesToAdd = append(resourcesToAdd, storageResource) } - if len(a.scenarioData.ModelSelections) == 0 { - a.scenarioData.ModelSelections = []string{} - - if a.scenarioData.SelectedScenario == "rag" { - a.scenarioData.ModelSelections = append(a.scenarioData.ModelSelections, defaultChatCompletionModel) - if a.scenarioData.UseCustomData { - a.scenarioData.ModelSelections = append(a.scenarioData.ModelSelections, defaultEmbeddingModel) - } - } - } - models := []*ai.AiModelDeployment{} // Add AI model resources @@ -303,15 +302,6 @@ func (a *startAction) Run(ctx context.Context, args []string) error { resourcesToAdd = append(resourcesToAdd, aiProject) } - spinner := ux.NewSpinner(&ux.SpinnerOptions{ - Text: "Adding infrastructure resources to project", - ClearOnStop: true, - }) - - if err := spinner.Start(ctx); err != nil { - return fmt.Errorf("failed to start spinner: %w", err) - } - for _, resource := range resourcesToAdd { _, err := a.azdClient.Compose().AddResource(ctx, &azdext.AddResourceRequest{ Resource: resource, @@ -328,13 +318,18 @@ func (a *startAction) Run(ctx context.Context, args []string) error { fmt.Println() fmt.Println(output.WithSuccessFormat("SUCCESS! The following resources have been staged for provisioning:")) for _, resource := range resourcesToAdd { - fmt.Printf(" - %s (%s)\n", resource.Name, output.WithGrayFormat(resource.Type)) + fmt.Printf(" - %s %s\n", resource.Name, output.WithGrayFormat("(%s)", resource.Type)) } for _, modelDeployment := range models { fmt.Printf(" - %s %s\n", modelDeployment.Name, - output.WithGrayFormat("(Format: %s, Version: %s)", modelDeployment.Format, modelDeployment.Version), + output.WithGrayFormat( + "(Format: %s, Version: %s, SKU: %s)", + modelDeployment.Format, + modelDeployment.Version, + modelDeployment.Sku.Name, + ), ) } @@ -350,30 +345,38 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to confirm provisioning: %w", err) } - if *confirmResponse.Value { - workflow := &azdext.Workflow{ - Name: "provision", - Steps: []*azdext.WorkflowStep{ - { - Command: &azdext.WorkflowCommand{ - Args: []string{"provision"}, - }, + if !*confirmResponse.Value { + fmt.Println() + fmt.Printf("To provision resources later, run %s\n", output.WithHighLightFormat("azd provision")) + return nil + } + + workflow := &azdext.Workflow{ + Name: "provision", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"provision"}, }, }, - } + }, + } - _, err = a.azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ - Workflow: workflow, - }) + _, err = a.azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ + Workflow: workflow, + }) - if err != nil { - return fmt.Errorf("failed to run provision workflow: %w", err) - } - } else { - fmt.Println() - fmt.Printf("To provision resources, run %s\n", output.WithHighLightFormat("azd provision")) + if err != nil { + return fmt.Errorf("failed to run provision workflow: %w", err) } + fmt.Println() + fmt.Println(output.WithSuccessFormat("SUCCESS! Your Azure resources have been provisioned.")) + fmt.Printf( + "You can add additional resources to your project by running %s\n", + output.WithHighLightFormat("azd compose add"), + ) + return nil } @@ -985,6 +988,30 @@ func (a *startAction) createQuestions() map[string]qna.Question { {Label: "I will choose model", Value: "user-model"}, }, }, + AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { + selectedValue := value.(string) + if selectedValue == "choose-model" { + capabilities, ok := q.State["capabilities"].([]string) + if !ok { + return nil + } + + // If the user selected "choose-model", we need to set the model selection + if slices.Contains(capabilities, "chatCompletion") { + a.scenarioData.ModelSelections = append( + a.scenarioData.ModelSelections, + defaultChatCompletionModel, + ) + } else if slices.Contains(capabilities, "embeddings") { + a.scenarioData.ModelSelections = append( + a.scenarioData.ModelSelections, + defaultEmbeddingModel, + ) + } + } + + return nil + }, Branches: map[any]string{ "guide-model": "guide-model-select", "user-model": "user-model-select", diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go index 5ecfda3597e..71bc76d1f17 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai/model_catalog.go @@ -283,6 +283,7 @@ func (c *ModelCatalogService) GetModelDeployment( } var modelDeployment *AiModelDeployment + hasDefaultVersion := c.hasDefaultVersion(model) for _, location := range model.Locations { if modelDeployment != nil { @@ -304,7 +305,8 @@ func (c *ModelCatalogService) GetModelDeployment( if !slices.Contains(options.Versions, *location.Model.Model.Version) { continue } - } else { + } else if hasDefaultVersion { + // Not all models have a default version if location.Model.Model.IsDefaultVersion != nil && !*location.Model.Model.IsDefaultVersion { continue } @@ -338,6 +340,15 @@ func (c *ModelCatalogService) GetModelDeployment( return modelDeployment, nil } +func (c *ModelCatalogService) hasDefaultVersion(model *AiModel) bool { + for _, location := range model.Locations { + if location.Model.Model.IsDefaultVersion != nil && *location.Model.Model.IsDefaultVersion { + return true + } + } + return false +} + func filterDistinctModelData( models map[string]*AiModel, filterFunc func(*armcognitiveservices.Model) []string, From ac6a56036895c25dee742d7896bd5fdfe3037687 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Mar 2025 16:18:46 -0700 Subject: [PATCH 11/31] Adds WIP files for scaffolded services --- .../internal/cmd/start.go | 179 ++++++++++++++---- .../internal/pkg/qna/decision_tree.go | 12 +- .../internal/resources/resources.go | 11 ++ .../rag/chatbot/csharp/chatbot.csproj | 9 + .../scenarios/rag/chatbot/csharp/main.cs | 20 ++ .../scenarios/rag/chatbot/js/main.js | 18 ++ .../scenarios/rag/chatbot/js/package.json | 11 ++ .../scenarios/rag/chatbot/python/main.py | 16 ++ .../rag/chatbot/python/requirements.txt | 2 + .../scenarios/rag/chatbot/ts/main.ts | 18 ++ .../scenarios/rag/chatbot/ts/package.json | 11 ++ .../scenarios/rag/webapp/csharp/main.cs | 20 ++ .../scenarios/rag/webapp/csharp/webapp.csproj | 9 + .../resources/scenarios/rag/webapp/js/main.js | 18 ++ .../scenarios/rag/webapp/js/package.json | 11 ++ .../scenarios/rag/webapp/python/main.py | 16 ++ .../rag/webapp/python/requirements.txt | 2 + .../resources/scenarios/rag/webapp/ts/main.ts | 18 ++ .../scenarios/rag/webapp/ts/package.json | 11 ++ 19 files changed, 365 insertions(+), 47 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/chatbot.csproj create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/main.cs create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/main.js create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/package.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/main.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/requirements.txt create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/main.cs create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/webapp.csproj create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/main.js create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/package.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/main.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/requirements.txt create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/main.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/package.json diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 1b1f45303eb..4ee1fb81140 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -7,7 +7,11 @@ import ( "context" "encoding/json" "fmt" + "os" + "path" // added for POSIX path joining + "path/filepath" "slices" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -15,6 +19,7 @@ import ( "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/ux" @@ -39,11 +44,12 @@ type scenarioInput struct { DatabaseType string `json:"databaseType,omitempty"` StorageAccountId string `json:"storageAccountId,omitempty"` DatabaseId string `json:"databaseId,omitempty"` - MessagingType string `json:"messageType,omitempty"` + MessagingType string `json:"messagingType,omitempty"` MessagingId string `json:"messagingId,omitempty"` ModelTasks []string `json:"modelTasks,omitempty"` ModelSelections []string `json:"modelSelections,omitempty"` - AppTypes []string `json:"appTypes,omitempty"` + AppHostTypes []string `json:"appHostTypes,omitempty"` + AppLanguages []string `json:"appLanguages,omitempty"` AppResourceIds []string `json:"appResourceIds,omitempty"` VectorStoreType string `json:"vectorStoreType,omitempty"` VectorStoreId string `json:"vectorStoreId,omitempty"` @@ -147,7 +153,7 @@ func newStartCommand() *cobra.Command { fmt.Println("This tool will help you build an AI scenario using Azure services.") fmt.Println() - azureContext, err := ensureAzureContext(ctx, azdClient) + azureContext, projectConfig, err := ensureAzureContext(ctx, azdClient) if err != nil { return fmt.Errorf("failed to ensure azure context: %w", err) } @@ -166,6 +172,7 @@ func newStartCommand() *cobra.Command { azureContext: azureContext, azureClient: azure.NewAzureClient(credential), modelCatalogService: ai.NewModelCatalogService(credential), + projectConfig: projectConfig, scenarioData: &scenarioInput{}, } @@ -184,6 +191,7 @@ type startAction struct { azureContext *azdext.AzureContext modelCatalogService *ai.ModelCatalogService azureClient *azure.AzureClient + projectConfig *azdext.ProjectConfig scenarioData *scenarioInput modelCatalog map[string]*ai.AiModel } @@ -196,23 +204,27 @@ func (a *startAction) Run(ctx context.Context, args []string) error { } spinner := ux.NewSpinner(&ux.SpinnerOptions{ - Text: "Adding infrastructure resources to project", + Text: "Updating `azd` project configuration", ClearOnStop: true, }) + fmt.Println() if err := spinner.Start(ctx); err != nil { return fmt.Errorf("failed to start spinner: %w", err) } resourcesToAdd := []*azdext.ComposedResource{} + servicesToAdd := []*azdext.ServiceConfig{} // Add host resources such as container apps. for i, appKey := range a.scenarioData.InteractionTypes { - appType := a.scenarioData.AppTypes[i] + appType := a.scenarioData.AppHostTypes[i] if appType == "" || appType == "choose-app" { appType = "host.containerapp" } + languageType := a.scenarioData.AppLanguages[i] + appConfig := map[string]any{ "port": 8080, } @@ -228,6 +240,14 @@ func (a *startAction) Run(ctx context.Context, args []string) error { Config: appConfigJson, } + serviceConfig := &azdext.ServiceConfig{ + Name: appKey, + Language: languageType, + Host: strings.ReplaceAll(appType, "host.", ""), + RelativePath: filepath.Join("src", appKey), + } + + servicesToAdd = append(servicesToAdd, serviceConfig) resourcesToAdd = append(resourcesToAdd, appResource) } @@ -302,6 +322,41 @@ func (a *startAction) Run(ctx context.Context, args []string) error { resourcesToAdd = append(resourcesToAdd, aiProject) } + for _, service := range servicesToAdd { + _, err := a.azdClient.Project().AddService(ctx, &azdext.AddServiceRequest{ + Service: service, + }) + if err != nil { + return fmt.Errorf("failed to add service %s: %w", service.Name, err) + } + + // Copy files from the embedded resources to the local service path. + servicePath := filepath.Join(a.projectConfig.Path, service.RelativePath) + if err := os.MkdirAll(servicePath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create service path %s: %w", servicePath, err) + } + + // Determine the correct resource folder path using POSIX join. + resourceDir := path.Join("scenarios", a.scenarioData.SelectedScenario, service.Name, service.Language) + entries, err := resources.Scenarios.ReadDir(resourceDir) + if err != nil { + return fmt.Errorf("failed to read resource directory %s: %w", resourceDir, err) + } + + for _, entry := range entries { + srcPath := path.Join(resourceDir, entry.Name()) + destPath := filepath.Join(servicePath, entry.Name()) + data, err := resources.Scenarios.ReadFile(srcPath) + if err != nil { + return fmt.Errorf("failed to read resource file %s: %w", srcPath, err) + } + //nolint:gosec + if err := os.WriteFile(destPath, data, 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", destPath, err) + } + } + } + for _, resource := range resourcesToAdd { _, err := a.azdClient.Compose().AddResource(ctx, &azdext.AddResourceRequest{ Resource: resource, @@ -315,22 +370,45 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to stop spinner: %w", err) } - fmt.Println() - fmt.Println(output.WithSuccessFormat("SUCCESS! The following resources have been staged for provisioning:")) - for _, resource := range resourcesToAdd { - fmt.Printf(" - %s %s\n", resource.Name, output.WithGrayFormat("(%s)", resource.Type)) + fmt.Println(output.WithSuccessFormat("SUCCESS! The following have been staged for provisioning and deployment:")) + + if len(servicesToAdd) > 0 { + fmt.Println() + fmt.Println(output.WithHintFormat("Services")) + for _, service := range servicesToAdd { + fmt.Printf(" - %s %s\n", + service.Name, + output.WithGrayFormat( + "(Host: %s, Language: %s)", + service.Host, + service.Language, + ), + ) + } } - for _, modelDeployment := range models { - fmt.Printf(" - %s %s\n", - modelDeployment.Name, - output.WithGrayFormat( - "(Format: %s, Version: %s, SKU: %s)", - modelDeployment.Format, - modelDeployment.Version, - modelDeployment.Sku.Name, - ), - ) + if len(resourcesToAdd) > 0 { + fmt.Println() + fmt.Println(output.WithHintFormat("Resources")) + for _, resource := range resourcesToAdd { + fmt.Printf(" - %s %s\n", resource.Name, output.WithGrayFormat("(%s)", resource.Type)) + } + } + + if len(models) > 0 { + fmt.Println() + fmt.Println(output.WithHintFormat("AI Models")) + for _, modelDeployment := range models { + fmt.Printf(" - %s %s\n", + modelDeployment.Name, + output.WithGrayFormat( + "(Format: %s, Version: %s, SKU: %s)", + modelDeployment.Format, + modelDeployment.Version, + modelDeployment.Sku.Name, + ), + ) + } } fmt.Println() @@ -407,22 +485,25 @@ func (a *startAction) loadAiCatalog(ctx context.Context) error { return nil } -func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azdext.AzureContext, error) { - _, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) +func ensureAzureContext( + ctx context.Context, + azdClient *azdext.AzdClient, +) (*azdext.AzureContext, *azdext.ProjectConfig, error) { + getProjectResponse, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { - return nil, fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) + return nil, nil, fmt.Errorf("project not found. Run `azd init` to create a new project, %w", err) } envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) if err != nil { - return nil, fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) + return nil, nil, fmt.Errorf("environment not found. Run `azd env new` to create a new environment, %w", err) } envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ Name: envResponse.Environment.Name, }) if err != nil { - return nil, fmt.Errorf("failed to get environment values: %w", err) + return nil, nil, fmt.Errorf("failed to get environment values: %w", err) } envValueMap := make(map[string]string) @@ -445,7 +526,7 @@ func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azde subscriptionResponse, err := azdClient.Prompt().PromptSubscription(ctx, &azdext.PromptSubscriptionRequest{}) if err != nil { - return nil, fmt.Errorf("failed to prompt for subscription: %w", err) + return nil, nil, fmt.Errorf("failed to prompt for subscription: %w", err) } azureContext.Scope.SubscriptionId = subscriptionResponse.Subscription.Id @@ -458,7 +539,7 @@ func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azde Value: azureContext.Scope.TenantId, }) if err != nil { - return nil, fmt.Errorf("failed to set tenant ID in environment: %w", err) + return nil, nil, fmt.Errorf("failed to set tenant ID in environment: %w", err) } // Set the tenant ID in the environment @@ -468,7 +549,7 @@ func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azde Value: azureContext.Scope.SubscriptionId, }) if err != nil { - return nil, fmt.Errorf("failed to set subscription ID in environment: %w", err) + return nil, nil, fmt.Errorf("failed to set subscription ID in environment: %w", err) } } @@ -482,7 +563,7 @@ func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azde AzureContext: azureContext, }) if err != nil { - return nil, fmt.Errorf("failed to prompt for location: %w", err) + return nil, nil, fmt.Errorf("failed to prompt for location: %w", err) } azureContext.Scope.Location = locationResponse.Location.Name @@ -494,11 +575,11 @@ func ensureAzureContext(ctx context.Context, azdClient *azdext.AzdClient) (*azde Value: azureContext.Scope.Location, }) if err != nil { - return nil, fmt.Errorf("failed to set location in environment: %w", err) + return nil, nil, fmt.Errorf("failed to set location in environment: %w", err) } } - return azureContext, nil + return azureContext, getProjectResponse.Project, nil } func (a *startAction) createQuestions() map[string]qna.Question { @@ -736,7 +817,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { }, "rag-user-interaction": { Binding: &a.scenarioData.InteractionTypes, - Heading: "Application Hosting", + Heading: "User Interaction", Message: "Now we will figure out all the different ways users will interact with your application.", Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, @@ -801,20 +882,15 @@ func (a *startAction) createQuestions() map[string]qna.Question { Next: []qna.QuestionReference{{Key: "use-custom-data"}}, }, "choose-app-type": { - Binding: &a.scenarioData.AppTypes, + Binding: &a.scenarioData.AppHostTypes, BeforeAsk: func(ctx context.Context, q *qna.Question, value any) error { + q.Heading = fmt.Sprintf("Configure '%s' Application", value) + q.Message = fmt.Sprintf("Lets collect some information about your %s application.", value) q.State["interactionType"] = value return nil }, Prompt: &qna.SingleSelectPrompt{ - BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { - appName := q.State["interactionType"].(string) - p.Message = fmt.Sprintf( - "Which type of application do you want to build for %s?", - appName, - ) - return nil - }, + Message: "Which application host do you want to use?", Client: a.azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ @@ -829,6 +905,27 @@ func (a *startAction) createQuestions() map[string]qna.Question { Branches: map[any]string{ "host.containerapp": "choose-app-resource", }, + Next: []qna.QuestionReference{ + {Key: "choose-app-language"}, + }, + }, + "choose-app-language": { + Binding: &a.scenarioData.AppLanguages, + Prompt: &qna.SingleSelectPrompt{ + Client: a.azdClient, + Message: "Which programming language do you want to use?", + HelpMessage: "Select the programming language that best fits your needs.", + EnableFiltering: to.Ptr(false), + Choices: []qna.Choice{ + {Label: "Choose for me", Value: "python"}, + {Label: "C#", Value: "csharp"}, + {Label: "Python", Value: "python"}, + {Label: "JavaScript", Value: "js"}, + {Label: "TypeScript", Value: "ts"}, + {Label: "Java", Value: "java"}, + {Label: "Other", Value: "other"}, + }, + }, }, "choose-app-resource": { Binding: &a.scenarioData.AppResourceIds, @@ -903,7 +1000,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { "start-choose-models": { Heading: "AI Model Selection", Message: "Now we will figure out the best AI model(s) for your application.", - BeforeAsk: func(ctx context.Context, question *qna.Question, value any) error { + AfterAsk: func(ctx context.Context, question *qna.Question, value any) error { if err := a.loadAiCatalog(ctx); err != nil { return fmt.Errorf("failed to load AI model catalog: %w", err) } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go index 5260ad1c255..5a3790d289d 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna/decision_tree.go @@ -83,6 +83,12 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value question.State = map[string]any{} } + if question.BeforeAsk != nil { + if err := question.BeforeAsk(ctx, &question, value); err != nil { + return fmt.Errorf("before ask function failed: %w", err) + } + } + if question.Heading != "" { fmt.Println() fmt.Println(output.WithHintFormat(question.Heading)) @@ -93,12 +99,6 @@ func (t *DecisionTree) askQuestion(ctx context.Context, question Question, value fmt.Println() } - if question.BeforeAsk != nil { - if err := question.BeforeAsk(ctx, &question, value); err != nil { - return fmt.Errorf("before ask function failed: %w", err) - } - } - var response any var err error diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go new file mode 100644 index 00000000000..9236c4ee4ae --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package resources + +import ( + "embed" +) + +//go:embed scenarios/* +var Scenarios embed.FS diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/chatbot.csproj b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/chatbot.csproj new file mode 100644 index 00000000000..dfcf051ae23 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/chatbot.csproj @@ -0,0 +1,9 @@ + + + Exe + net6.0 + + + + + diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/main.cs b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/main.cs new file mode 100644 index 00000000000..343331c85ca --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/csharp/main.cs @@ -0,0 +1,20 @@ +using System; +using Azure; +using Azure.AI.OpenAI; + +namespace ChatBot { + class Program { + static void Main(string[] args) { + var endpoint = new Uri("https://your-resource.openai.azure.com/"); + var credential = new AzureKeyCredential("your-key"); + var client = new OpenAIClient(endpoint, credential); + var chatOptions = new ChatCompletionsOptions(); + chatOptions.Messages.Add(new ChatMessage(ChatRole.System, "You are an assistant.")); + chatOptions.Messages.Add(new ChatMessage(ChatRole.User, "Hello, world from chatbot!")); + var response = client.GetChatCompletions("deployment-id", chatOptions); + foreach (var choice in response.Value.Choices) { + Console.WriteLine(choice.Message.Content); + } + } + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/main.js b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/main.js new file mode 100644 index 00000000000..bc6a3a66ac7 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/main.js @@ -0,0 +1,18 @@ +const { OpenAIClient, AzureKeyCredential } = require("@azure/openai"); + +async function main() { + const endpoint = "https://your-resource.openai.azure.com/"; + const apiKey = "your-key"; + const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey)); + const deploymentId = "deployment-id"; + const chatOptions = { + messages: [ + { role: "system", content: "You are an assistant." }, + { role: "user", content: "Hello, world from chatbot!" } + ] + }; + const result = await client.getChatCompletions(deploymentId, chatOptions); + console.log(result.choices[0].message.content); +} + +main(); diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/package.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/package.json new file mode 100644 index 00000000000..2e899392e72 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/js/package.json @@ -0,0 +1,11 @@ +{ + "name": "chatbot-js", + "version": "1.0.0", + "main": "main.js", + "scripts": { + "start": "node main.js" + }, + "dependencies": { + "@azure/openai": "^1.0.0" + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/main.py b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/main.py new file mode 100644 index 00000000000..b6c684b82f2 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/main.py @@ -0,0 +1,16 @@ +from azure.ai.openai import OpenAIClient +from azure.core.credentials import AzureKeyCredential + +def main(): + endpoint = "https://your-resource.openai.azure.com/" + api_key = "your-key" + client = OpenAIClient(endpoint, AzureKeyCredential(api_key)) + deployment_id = "deployment-id" + response = client.get_chat_completions(deployment_id, messages=[ + {"role": "system", "content": "You are an assistant."}, + {"role": "user", "content": "Hello, world from chatbot!"} + ]) + print(response.choices[0].message.content) + +if __name__ == "__main__": + main() diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/requirements.txt b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/requirements.txt new file mode 100644 index 00000000000..fee1f2f2653 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/python/requirements.txt @@ -0,0 +1,2 @@ +# No external dependencies required. +azure-ai-openai==1.0.0 diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts new file mode 100644 index 00000000000..b4f095bc690 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts @@ -0,0 +1,18 @@ +import { OpenAIClient, AzureKeyCredential } from "@azure/openai"; + +async function main() { + const endpoint = "https://your-resource.openai.azure.com/"; + const apiKey = "your-key"; + const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey)); + const deploymentId = "deployment-id"; + const chatOptions = { + messages: [ + { role: "system", content: "You are an assistant." }, + { role: "user", content: "Hello, world from chatbot!" } + ] + }; + const result = await client.getChatCompletions(deploymentId, chatOptions); + console.log(result.choices[0].message.content); +} + +main(); diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json new file mode 100644 index 00000000000..49f9fda5eda --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json @@ -0,0 +1,11 @@ +{ + "name": "chatbot-ts", + "version": "1.0.0", + "main": "main.ts", + "scripts": { + "start": "ts-node main.ts" + }, + "dependencies": { + "@azure/openai": "^1.0.0" + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/main.cs b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/main.cs new file mode 100644 index 00000000000..a37caa29438 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/main.cs @@ -0,0 +1,20 @@ +using System; +using Azure; +using Azure.AI.OpenAI; + +namespace WebApp { + class Program { + static void Main(string[] args) { + var endpoint = new Uri("https://your-resource.openai.azure.com/"); + var credential = new AzureKeyCredential("your-key"); + var client = new OpenAIClient(endpoint, credential); + var chatOptions = new ChatCompletionsOptions(); + chatOptions.Messages.Add(new ChatMessage(ChatRole.System, "You are an assistant.")); + chatOptions.Messages.Add(new ChatMessage(ChatRole.User, "Hello, world!")); + var response = client.GetChatCompletions("deployment-id", chatOptions); + foreach (var choice in response.Value.Choices) { + Console.WriteLine(choice.Message.Content); + } + } + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/webapp.csproj b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/webapp.csproj new file mode 100644 index 00000000000..dfcf051ae23 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/csharp/webapp.csproj @@ -0,0 +1,9 @@ + + + Exe + net6.0 + + + + + diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/main.js b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/main.js new file mode 100644 index 00000000000..2f80b7538ce --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/main.js @@ -0,0 +1,18 @@ +const { OpenAIClient, AzureKeyCredential } = require("@azure/openai"); + +async function main() { + const endpoint = "https://your-resource.openai.azure.com/"; + const apiKey = "your-key"; + const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey)); + const deploymentId = "deployment-id"; + const chatOptions = { + messages: [ + { role: "system", content: "You are an assistant." }, + { role: "user", content: "Hello, world!" } + ] + }; + const result = await client.getChatCompletions(deploymentId, chatOptions); + console.log(result.choices[0].message.content); +} + +main(); diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/package.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/package.json new file mode 100644 index 00000000000..f0abc302b12 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/js/package.json @@ -0,0 +1,11 @@ +{ + "name": "webapp-js", + "version": "1.0.0", + "main": "main.js", + "scripts": { + "start": "node main.js" + }, + "dependencies": { + "@azure/openai": "^1.0.0" + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/main.py b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/main.py new file mode 100644 index 00000000000..e6ecf00a41f --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/main.py @@ -0,0 +1,16 @@ +from azure.ai.openai import OpenAIClient +from azure.core.credentials import AzureKeyCredential + +def main(): + endpoint = "https://your-resource.openai.azure.com/" + api_key = "your-key" + client = OpenAIClient(endpoint, AzureKeyCredential(api_key)) + deployment_id = "deployment-id" + response = client.get_chat_completions(deployment_id, messages=[ + {"role": "system", "content": "You are an assistant."}, + {"role": "user", "content": "Hello, world!"} + ]) + print(response.choices[0].message.content) + +if __name__ == "__main__": + main() diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/requirements.txt b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/requirements.txt new file mode 100644 index 00000000000..fee1f2f2653 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/requirements.txt @@ -0,0 +1,2 @@ +# No external dependencies required. +azure-ai-openai==1.0.0 diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/main.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/main.ts new file mode 100644 index 00000000000..3c6d6b2a685 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/main.ts @@ -0,0 +1,18 @@ +import { OpenAIClient, AzureKeyCredential } from "@azure/openai"; + +async function main() { + const endpoint = "https://your-resource.openai.azure.com/"; + const apiKey = "your-key"; + const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey)); + const deploymentId = "deployment-id"; + const chatOptions = { + messages: [ + { role: "system", content: "You are an assistant." }, + { role: "user", content: "Hello, world!" } + ] + }; + const result = await client.getChatCompletions(deploymentId, chatOptions); + console.log(result.choices[0].message.content); +} + +main(); diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/package.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/package.json new file mode 100644 index 00000000000..918934bb510 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/ts/package.json @@ -0,0 +1,11 @@ +{ + "name": "webapp-ts", + "version": "1.0.0", + "main": "main.ts", + "scripts": { + "start": "ts-node main.ts" + }, + "dependencies": { + "@azure/openai": "^1.0.0" + } +} From b5b5784497777d692684c7da4387e1a203ec9a2b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Mar 2025 16:35:50 -0700 Subject: [PATCH 12/31] Adds copy check --- .../internal/cmd/start.go | 36 ++++++++++++++++++- .../internal/pkg/util/util.go | 23 ++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 4ee1fb81140..474d4cda32b 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -19,6 +19,7 @@ import ( "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" + "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/output" @@ -204,7 +205,7 @@ func (a *startAction) Run(ctx context.Context, args []string) error { } spinner := ux.NewSpinner(&ux.SpinnerOptions{ - Text: "Updating `azd` project configuration", + Text: "Updating project configuration", ClearOnStop: true, }) @@ -336,6 +337,39 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to create service path %s: %w", servicePath, err) } + isEmpty, err := util.IsDirEmpty(servicePath) + if err != nil { + return fmt.Errorf("failed to check if directory is empty: %w", err) + } + + if !isEmpty { + if err := spinner.Stop(ctx); err != nil { + return fmt.Errorf("failed to stop spinner: %w", err) + } + + overwriteResponse, err := a.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + DefaultValue: to.Ptr(false), + Message: fmt.Sprintf( + "The directory %s is not empty. Do you want to overwrite it?", + output.WithHighLightFormat(service.RelativePath), + ), + }, + }) + + if err != nil { + return fmt.Errorf("failed to confirm overwrite: %w", err) + } + + if !*overwriteResponse.Value { + continue + } + + if err := spinner.Start(ctx); err != nil { + return fmt.Errorf("failed to start spinner: %w", err) + } + } + // Determine the correct resource folder path using POSIX join. resourceDir := path.Join("scenarios", a.scenarioData.SelectedScenario, service.Name, service.Language) entries, err := resources.Scenarios.ReadDir(resourceDir) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go new file mode 100644 index 00000000000..8fa272e5934 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package util + +import "os" + +func IsDirEmpty(dirPath string) (bool, error) { + dir, err := os.Open(dirPath) + if err != nil { + return false, err // Handle errors like "directory does not exist" + } + defer dir.Close() + + // Read at most 1 entry + entries, err := dir.Readdirnames(1) + if err != nil { + return false, err + } + + // If no entries found, directory is empty + return len(entries) == 0, nil +} From d7265c5f2316debcd37798d69497f69e918240e0 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Mar 2025 16:59:43 -0700 Subject: [PATCH 13/31] Adds AI search vector store --- .../internal/cmd/start.go | 23 ++++++++----------- .../internal/pkg/util/util.go | 14 ++++++----- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 474d4cda32b..26d58ac8199 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -261,13 +261,13 @@ func (a *startAction) Run(ctx context.Context, args []string) error { resourcesToAdd = append(resourcesToAdd, dbResource) } - // if a.scenarioData.VectorStoreType != "" { - // vectorStoreResource := &azdext.ComposedResource{ - // Name: "vectorStore", - // Type: a.scenarioData.VectorStoreType, - // } - // resourcesToAdd = append(resourcesToAdd, vectorStoreResource) - // } + if a.scenarioData.VectorStoreType != "" { + vectorStoreResource := &azdext.ComposedResource{ + Name: "vector-store", + Type: a.scenarioData.VectorStoreType, + } + resourcesToAdd = append(resourcesToAdd, vectorStoreResource) + } // Add storage resources if a.scenarioData.UseCustomData { @@ -337,12 +337,7 @@ func (a *startAction) Run(ctx context.Context, args []string) error { return fmt.Errorf("failed to create service path %s: %w", servicePath, err) } - isEmpty, err := util.IsDirEmpty(servicePath) - if err != nil { - return fmt.Errorf("failed to check if directory is empty: %w", err) - } - - if !isEmpty { + if !util.IsDirEmpty(servicePath) { if err := spinner.Stop(ctx); err != nil { return fmt.Errorf("failed to stop spinner: %w", err) } @@ -807,7 +802,7 @@ func (a *startAction) createQuestions() map[string]qna.Question { Client: a.azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ - {Label: "Choose for me", Value: "choose-vector-store"}, + {Label: "Choose for me", Value: "ai.search"}, {Label: "AI Search", Value: "ai.search"}, {Label: "CosmosDB", Value: "db.cosmos"}, }, diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go index 8fa272e5934..a304917c34e 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util/util.go @@ -3,21 +3,23 @@ package util -import "os" +import ( + "os" +) -func IsDirEmpty(dirPath string) (bool, error) { +func IsDirEmpty(dirPath string) bool { dir, err := os.Open(dirPath) if err != nil { - return false, err // Handle errors like "directory does not exist" + return false } defer dir.Close() // Read at most 1 entry entries, err := dir.Readdirnames(1) if err != nil { - return false, err + return true } - // If no entries found, directory is empty - return len(entries) == 0, nil + // If no entries, the directory is empty + return len(entries) == 0 } From 0aedd2b81f4045e46b37b7036b46d97c4a82b9ec Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 20 Mar 2025 13:21:25 -0700 Subject: [PATCH 14/31] Updates code samples for extension --- .../internal/cmd/start.go | 55 +- .../internal/resources/resources.go | 2 +- .../resources/scenarios/rag/chatbot/ts/.npmrc | 2 + .../resources/scenarios/rag/chatbot/ts/.nvmrc | 1 + .../scenarios/rag/chatbot/ts/.prettierignore | 2 + .../scenarios/rag/chatbot/ts/.prettierrc.json | 6 + .../scenarios/rag/chatbot/ts/Dockerfile | 17 + .../scenarios/rag/chatbot/ts/index.html | 13 + .../scenarios/rag/chatbot/ts/main.ts | 18 - .../rag/chatbot/ts/package-lock.json | 6208 +++++++++++++++++ .../scenarios/rag/chatbot/ts/package.json | 49 +- .../rag/chatbot/ts/public/favicon.ico | Bin 0 -> 4286 bytes .../scenarios/rag/chatbot/ts/src/api/api.ts | 191 + .../scenarios/rag/chatbot/ts/src/api/index.ts | 2 + .../rag/chatbot/ts/src/api/models.ts | 123 + .../rag/chatbot/ts/src/assets/applogo.svg | 1 + .../rag/chatbot/ts/src/authConfig.ts | 252 + .../AnalysisPanel/AnalysisPanel.module.css | 64 + .../AnalysisPanel/AnalysisPanel.tsx | 103 + .../AnalysisPanel/AnalysisPanelTabs.tsx | 5 + .../AnalysisPanel/ThoughtProcess.tsx | 43 + .../ts/src/components/AnalysisPanel/index.tsx | 2 + .../src/components/Answer/Answer.module.css | 143 + .../ts/src/components/Answer/Answer.tsx | 139 + .../ts/src/components/Answer/AnswerError.tsx | 23 + .../ts/src/components/Answer/AnswerIcon.tsx | 5 + .../src/components/Answer/AnswerLoading.tsx | 28 + .../ts/src/components/Answer/AnswerParser.tsx | 88 + .../components/Answer/SpeechOutputAzure.tsx | 81 + .../components/Answer/SpeechOutputBrowser.tsx | 85 + .../chatbot/ts/src/components/Answer/index.ts | 5 + .../ClearChatButton.module.css | 7 + .../ClearChatButton/ClearChatButton.tsx | 22 + .../src/components/ClearChatButton/index.tsx | 1 + .../src/components/Example/Example.module.css | 68 + .../ts/src/components/Example/Example.tsx | 15 + .../ts/src/components/Example/ExampleList.tsx | 26 + .../ts/src/components/Example/index.tsx | 2 + .../GPT4VSettings/GPT4VSettings.module.css | 3 + .../GPT4VSettings/GPT4VSettings.tsx | 79 + .../ts/src/components/GPT4VSettings/index.ts | 1 + .../components/HelpCallout/HelpCallout.tsx | 52 + .../ts/src/components/HelpCallout/index.ts | 1 + .../HistoryButton/HistoryButton.module.css | 7 + .../HistoryButton/HistoryButton.tsx | 22 + .../ts/src/components/HistoryButton/index.tsx | 1 + .../HistoryItem/HistoryItem.module.css | 120 + .../components/HistoryItem/HistoryItem.tsx | 59 + .../ts/src/components/HistoryItem/index.tsx | 1 + .../HistoryPanel/HistoryPanel.module.css | 14 + .../components/HistoryPanel/HistoryPanel.tsx | 172 + .../ts/src/components/HistoryPanel/index.tsx | 1 + .../components/HistoryProviders/CosmosDB.ts | 51 + .../HistoryProviders/HistoryManager.ts | 21 + .../components/HistoryProviders/IProvider.ts | 19 + .../components/HistoryProviders/IndexedDB.ts | 104 + .../src/components/HistoryProviders/None.ts | 20 + .../src/components/HistoryProviders/index.ts | 4 + .../LoginButton/LoginButton.module.css | 6 + .../components/LoginButton/LoginButton.tsx | 65 + .../ts/src/components/LoginButton/index.tsx | 1 + .../MarkdownViewer/MarkdownViewer.module.css | 49 + .../MarkdownViewer/MarkdownViewer.tsx | 81 + .../src/components/MarkdownViewer/index.tsx | 1 + .../QuestionInput/QuestionInput.module.css | 26 + .../QuestionInput/QuestionInput.tsx | 97 + .../components/QuestionInput/SpeechInput.tsx | 117 + .../ts/src/components/QuestionInput/index.ts | 1 + .../components/Settings/Settings.module.css | 3 + .../ts/src/components/Settings/Settings.tsx | 316 + .../SettingsButton/SettingsButton.module.css | 7 + .../SettingsButton/SettingsButton.tsx | 21 + .../src/components/SettingsButton/index.tsx | 1 + .../SupportingContent.module.css | 33 + .../SupportingContent/SupportingContent.tsx | 32 + .../SupportingContentParser.ts | 19 + .../src/components/SupportingContent/index.ts | 1 + .../TokenClaimsDisplay/TokenClaimsDisplay.tsx | 98 + .../components/TokenClaimsDisplay/index.tsx | 1 + .../UploadFile/UploadFile.module.css | 26 + .../src/components/UploadFile/UploadFile.tsx | 166 + .../ts/src/components/UploadFile/index.tsx | 1 + .../UserChatMessage.module.css | 17 + .../UserChatMessage/UserChatMessage.tsx | 13 + .../src/components/UserChatMessage/index.ts | 1 + .../VectorSettings/VectorSettings.module.css | 3 + .../VectorSettings/VectorSettings.tsx | 98 + .../ts/src/components/VectorSettings/index.ts | 1 + .../ts/src/i18n/LanguagePicker.module.css | 25 + .../chatbot/ts/src/i18n/LanguagePicker.tsx | 39 + .../rag/chatbot/ts/src/i18n/config.ts | 81 + .../rag/chatbot/ts/src/i18n/index.tsx | 1 + .../scenarios/rag/chatbot/ts/src/index.css | 56 + .../scenarios/rag/chatbot/ts/src/index.tsx | 45 + .../rag/chatbot/ts/src/layoutWrapper.tsx | 61 + .../ts/src/locales/da/translation.json | 139 + .../ts/src/locales/en/translation.json | 156 + .../ts/src/locales/es/translation.json | 157 + .../ts/src/locales/fr/translation.json | 157 + .../ts/src/locales/it/translation.json | 157 + .../ts/src/locales/ja/translation.json | 154 + .../ts/src/locales/nl/translation.json | 156 + .../ts/src/locales/ptBR/translation.json | 156 + .../ts/src/locales/tr/translation.json | 156 + .../rag/chatbot/ts/src/loginContext.tsx | 13 + .../rag/chatbot/ts/src/pages/NoPage.tsx | 5 + .../chatbot/ts/src/pages/ask/Ask.module.css | 87 + .../rag/chatbot/ts/src/pages/ask/Ask.tsx | 346 + .../chatbot/ts/src/pages/chat/Chat.module.css | 146 + .../rag/chatbot/ts/src/pages/chat/Chat.tsx | 529 ++ .../ts/src/pages/layout/Layout.module.css | 144 + .../chatbot/ts/src/pages/layout/Layout.tsx | 83 + .../rag/chatbot/ts/src/vite-env.d.ts | 1 + .../scenarios/rag/chatbot/ts/tsconfig.json | 21 + .../scenarios/rag/chatbot/ts/vite.config.ts | 44 + .../scenarios/rag/webapp/python/.dockerignore | 7 + .../scenarios/rag/webapp/python/Dockerfile | 11 + .../scenarios/rag/webapp/python/app.py | 769 ++ .../rag/webapp/python/approaches/__init__.py | 0 .../rag/webapp/python/approaches/approach.py | 300 + .../webapp/python/approaches/chatapproach.py | 125 + .../python/approaches/chatreadretrieveread.py | 225 + .../approaches/chatreadretrievereadvision.py | 241 + .../webapp/python/approaches/promptmanager.py | 76 + .../prompts/ask_answer_question.prompty | 42 + .../ask_answer_question_vision.prompty | 31 + .../prompts/chat_answer_question.prompty | 51 + .../chat_answer_question_vision.prompty | 48 + .../prompts/chat_query_rewrite.prompty | 44 + .../prompts/chat_query_rewrite_tools.json | 17 + .../python/approaches/retrievethenread.py | 150 + .../approaches/retrievethenreadvision.py | 178 + .../webapp/python/chat_history/__init__.py | 0 .../webapp/python/chat_history/cosmosdb.py | 239 + .../scenarios/rag/webapp/python/config.py | 29 + .../rag/webapp/python/core/__init__.py | 0 .../rag/webapp/python/core/authentication.py | 361 + .../rag/webapp/python/core/imageshelper.py | 40 + .../rag/webapp/python/core/sessionhelper.py | 12 + .../webapp/python/custom_uvicorn_worker.py | 47 + .../scenarios/rag/webapp/python/decorators.py | 58 + .../scenarios/rag/webapp/python/error.py | 27 + .../rag/webapp/python/gunicorn.conf.py | 18 + .../rag/webapp/python/load_azd_env.py | 29 + .../scenarios/rag/webapp/python/main.py | 24 +- .../scenarios/rag/webapp/python/prepdocs.py | 440 ++ .../rag/webapp/python/prepdocslib/__init__.py | 0 .../webapp/python/prepdocslib/blobmanager.py | 178 + .../webapp/python/prepdocslib/csvparser.py | 31 + .../webapp/python/prepdocslib/embeddings.py | 262 + .../python/prepdocslib/fileprocessor.py | 10 + .../webapp/python/prepdocslib/filestrategy.py | 150 + .../webapp/python/prepdocslib/htmlparser.py | 49 + .../integratedvectorizerstrategy.py | 187 + .../webapp/python/prepdocslib/jsonparser.py | 23 + .../python/prepdocslib/listfilestrategy.py | 177 + .../python/prepdocslib/mediadescriber.py | 107 + .../rag/webapp/python/prepdocslib/page.py | 28 + .../rag/webapp/python/prepdocslib/parser.py | 14 + .../webapp/python/prepdocslib/pdfparser.py | 250 + .../python/prepdocslib/searchmanager.py | 331 + .../rag/webapp/python/prepdocslib/strategy.py | 49 + .../webapp/python/prepdocslib/textparser.py | 30 + .../webapp/python/prepdocslib/textsplitter.py | 232 + .../rag/webapp/python/requirements.in | 34 + .../rag/webapp/python/requirements.txt | 469 +- 166 files changed, 19255 insertions(+), 61 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.npmrc create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.nvmrc create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierignore create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierrc.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/Dockerfile create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/index.html delete mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package-lock.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/public/favicon.ico create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/api.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/models.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/assets/applogo.svg create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/authConfig.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanelTabs.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/ThoughtProcess.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/Answer.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/Answer.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/AnswerError.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/AnswerIcon.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/AnswerLoading.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/AnswerParser.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/SpeechOutputAzure.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/SpeechOutputBrowser.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Answer/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/ClearChatButton/ClearChatButton.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/ClearChatButton/ClearChatButton.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/ClearChatButton/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Example/Example.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Example/Example.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Example/ExampleList.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Example/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/GPT4VSettings/GPT4VSettings.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/GPT4VSettings/GPT4VSettings.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/GPT4VSettings/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HelpCallout/HelpCallout.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HelpCallout/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryButton/HistoryButton.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryButton/HistoryButton.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryButton/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryItem/HistoryItem.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryItem/HistoryItem.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryItem/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryPanel/HistoryPanel.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryPanel/HistoryPanel.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryPanel/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/CosmosDB.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/HistoryManager.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/IProvider.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/IndexedDB.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/None.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/HistoryProviders/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/LoginButton/LoginButton.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/LoginButton/LoginButton.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/LoginButton/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/MarkdownViewer/MarkdownViewer.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/MarkdownViewer/MarkdownViewer.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/MarkdownViewer/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/QuestionInput/QuestionInput.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/QuestionInput/QuestionInput.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/QuestionInput/SpeechInput.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/QuestionInput/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Settings/Settings.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/Settings/Settings.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SettingsButton/SettingsButton.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SettingsButton/SettingsButton.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SettingsButton/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SupportingContent/SupportingContent.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SupportingContent/SupportingContent.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SupportingContent/SupportingContentParser.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/SupportingContent/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/TokenClaimsDisplay/TokenClaimsDisplay.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/TokenClaimsDisplay/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UploadFile/UploadFile.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UploadFile/UploadFile.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UploadFile/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UserChatMessage/UserChatMessage.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UserChatMessage/UserChatMessage.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/UserChatMessage/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/VectorSettings/VectorSettings.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/VectorSettings/VectorSettings.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/VectorSettings/index.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/i18n/LanguagePicker.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/i18n/LanguagePicker.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/i18n/config.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/i18n/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/index.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/index.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/layoutWrapper.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/da/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/en/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/es/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/fr/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/it/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/ja/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/nl/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/ptBR/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/locales/tr/translation.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/loginContext.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/NoPage.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/ask/Ask.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/ask/Ask.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/chat/Chat.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/chat/Chat.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/layout/Layout.module.css create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/pages/layout/Layout.tsx create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/vite-env.d.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/tsconfig.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/vite.config.ts create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/.dockerignore create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/Dockerfile create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/app.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/__init__.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/approach.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/chatapproach.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/chatreadretrieveread.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/chatreadretrievereadvision.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/promptmanager.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/ask_answer_question.prompty create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/ask_answer_question_vision.prompty create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/chat_answer_question.prompty create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/chat_answer_question_vision.prompty create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/chat_query_rewrite.prompty create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/prompts/chat_query_rewrite_tools.json create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/retrievethenread.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/approaches/retrievethenreadvision.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/chat_history/__init__.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/chat_history/cosmosdb.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/config.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/core/__init__.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/core/authentication.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/core/imageshelper.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/core/sessionhelper.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/custom_uvicorn_worker.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/decorators.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/error.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/gunicorn.conf.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/load_azd_env.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocs.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/__init__.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/blobmanager.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/csvparser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/embeddings.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/fileprocessor.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/filestrategy.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/htmlparser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/integratedvectorizerstrategy.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/jsonparser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/listfilestrategy.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/mediadescriber.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/page.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/parser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/pdfparser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/searchmanager.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/strategy.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/textparser.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/prepdocslib/textsplitter.py create mode 100644 cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/webapp/python/requirements.in diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go index 26d58ac8199..3a8b79ae19d 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "io/fs" "os" "path" // added for POSIX path joining "path/filepath" @@ -367,22 +368,8 @@ func (a *startAction) Run(ctx context.Context, args []string) error { // Determine the correct resource folder path using POSIX join. resourceDir := path.Join("scenarios", a.scenarioData.SelectedScenario, service.Name, service.Language) - entries, err := resources.Scenarios.ReadDir(resourceDir) - if err != nil { - return fmt.Errorf("failed to read resource directory %s: %w", resourceDir, err) - } - - for _, entry := range entries { - srcPath := path.Join(resourceDir, entry.Name()) - destPath := filepath.Join(servicePath, entry.Name()) - data, err := resources.Scenarios.ReadFile(srcPath) - if err != nil { - return fmt.Errorf("failed to read resource file %s: %w", srcPath, err) - } - //nolint:gosec - if err := os.WriteFile(destPath, data, 0644); err != nil { - return fmt.Errorf("failed to write file %s: %w", destPath, err) - } + if err := copyResourceDir(resourceDir, servicePath); err != nil { + return fmt.Errorf("failed to copy resource directory %s: %w", resourceDir, err) } } @@ -854,8 +841,8 @@ func (a *startAction) createQuestions() map[string]qna.Question { HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ - {Label: "Chatbot", Value: "chatbot"}, - {Label: "Web Application", Value: "webapp"}, + {Label: "Chatbot UI Frontend", Value: "chatbot"}, + {Label: "API Backend Application", Value: "webapp"}, }, }, Branches: map[any]string{ @@ -878,9 +865,9 @@ func (a *startAction) createQuestions() map[string]qna.Question { HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ - {Label: "Chatbot", Value: "chatbot"}, - {Label: "Web Application", Value: "webapp"}, - {Label: "Message Queue", Value: "messaging"}, + {Label: "Chatbot UI Frontend", Value: "chatbot"}, + {Label: "API Backend Application", Value: "webapp"}, + {Label: "Message based Backed Queue", Value: "messaging"}, }, }, Branches: map[any]string{ @@ -1353,3 +1340,29 @@ func (a *startAction) createQuestions() map[string]qna.Question { }, } } + +// New helper function to recursively copy resource directories. +func copyResourceDir(src, dest string) error { + return fs.WalkDir(resources.Scenarios, src, func(entryPath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(src, entryPath) + if err != nil { + return err + } + targetPath := filepath.Join(dest, relPath) + if d.IsDir() { + return os.MkdirAll(targetPath, os.ModePerm) + } + data, err := resources.Scenarios.ReadFile(entryPath) + if err != nil { + return fmt.Errorf("failed to read resource file %s: %w", entryPath, err) + } + //nolint:gosec + if err := os.WriteFile(targetPath, data, 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", targetPath, err) + } + return nil + }) +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go index 9236c4ee4ae..fecfa3a7f33 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/resources.go @@ -7,5 +7,5 @@ import ( "embed" ) -//go:embed scenarios/* +//go:embed scenarios var Scenarios embed.FS diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.npmrc b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.npmrc new file mode 100644 index 00000000000..727cdb26498 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +fund=false diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.nvmrc b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.nvmrc new file mode 100644 index 00000000000..946789e6195 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.nvmrc @@ -0,0 +1 @@ +16.0.0 diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierignore b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierignore new file mode 100644 index 00000000000..fc355bcdfb3 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierignore @@ -0,0 +1,2 @@ +# Ignore JSON +**/*.json diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierrc.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierrc.json new file mode 100644 index 00000000000..b7d67747c83 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "printWidth": 160, + "arrowParens": "avoid", + "trailingComma": "none" +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/Dockerfile b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/Dockerfile new file mode 100644 index 00000000000..27cab47dcb5 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build +FROM node:16-alpine AS builder +WORKDIR /app +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install +# Copy all source files +COPY . . +# Build the Vite app (adjust the script name if needed) +RUN npm run build + +# Stage 2: Serve with Nginx +FROM nginx:stable-alpine +# Remove default nginx configuration (if needed) +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/index.html b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/index.html new file mode 100644 index 00000000000..30205db90f0 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/index.html @@ -0,0 +1,13 @@ + + + + + + + Azure OpenAI + AI Search + + +
+ + + diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts deleted file mode 100644 index b4f095bc690..00000000000 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { OpenAIClient, AzureKeyCredential } from "@azure/openai"; - -async function main() { - const endpoint = "https://your-resource.openai.azure.com/"; - const apiKey = "your-key"; - const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey)); - const deploymentId = "deployment-id"; - const chatOptions = { - messages: [ - { role: "system", content: "You are an assistant." }, - { role: "user", content: "Hello, world from chatbot!" } - ] - }; - const result = await client.getChatCompletions(deploymentId, chatOptions); - console.log(result.choices[0].message.content); -} - -main(); diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package-lock.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package-lock.json new file mode 100644 index 00000000000..f577997725b --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package-lock.json @@ -0,0 +1,6208 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@azure/msal-browser": "^3.26.1", + "@azure/msal-react": "^2.2.0", + "@fluentui/react": "^8.112.5", + "@fluentui/react-components": "^9.56.2", + "@fluentui/react-icons": "^2.0.265", + "@react-spring/web": "^9.7.5", + "dompurify": "^3.2.0", + "i18next": "^24.2.0", + "i18next-browser-languagedetector": "^8.0.2", + "i18next-http-backend": "^3.0.1", + "idb": "^8.0.0", + "ndjson-readablestream": "^1.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", + "react-i18next": "^15.1.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.28.0", + "react-syntax-highlighter": "^15.6.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "scheduler": "^0.20.2" + }, + "devDependencies": { + "@types/dom-speech-recognition": "^0.0.4", + "@types/dompurify": "^3.0.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react": "^4.3.3", + "prettier": "^3.3.3", + "rollup-plugin-visualizer": "^5.12.0", + "typescript": "^5.6.3", + "vite": "^5.4.14" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.27.0.tgz", + "integrity": "sha512-+b4ZKSD8+vslCtVRVetkegEhOFMLP3rxDWJY212ct+2r6jVg6OSQKc1Qz3kCoXo0FgwaXkb+76TMZfpHp8QtgA==", + "dependencies": { + "@azure/msal-common": "14.16.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-2.2.0.tgz", + "integrity": "sha512-2V+9JXeXyyjYNF92y5u0tU4el9px/V1+vkRuN+DtoxyiMHCtYQpJoaFdGWArh43zhz5aqQqiGW/iajPDSu3QsQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^3.27.0", + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/devtools": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.1.tgz", + "integrity": "sha512-8PHJLbD6VhBh+LJ1uty/Bz30qs02NXCE5u8WpOhSewlYXUWl03GNXknr9AS2yaAWJEQaY27x7eByJs44gODBcw==", + "peerDependencies": { + "@floating-ui/dom": ">=1.5.4" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, + "node_modules/@fluentui/date-time-utilities": { + "version": "8.5.14", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/dom-utilities": { + "version": "2.2.12", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/font-icons-mdl2": { + "version": "8.5.26", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/foundation-legacy": { + "version": "8.2.46", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/keyboard-key": { + "version": "0.4.12", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/keyboard-keys": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz", + "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/merge-styles": { + "version": "8.5.13", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/priority-overflow": { + "version": "9.1.14", + "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.1.14.tgz", + "integrity": "sha512-tIH8EhvjZF4MhxSjqrWOyodrQQW+RlVZqxuNFQF5OWRdSqcIK8g+Z+UbC5fYHQooCgVsthk2mFurfGMKFtf9ug==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react": { + "version": "8.112.5", + "license": "MIT", + "dependencies": { + "@fluentui/date-time-utilities": "^8.5.14", + "@fluentui/font-icons-mdl2": "^8.5.26", + "@fluentui/foundation-legacy": "^8.2.46", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/react-focus": "^8.8.33", + "@fluentui/react-hooks": "^8.6.32", + "@fluentui/react-portal-compat-context": "^9.0.9", + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-accordion": { + "version": "9.5.8", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.5.8.tgz", + "integrity": "sha512-tYkHFbNfJG1/qSzkdagSGZoL9LlRp1/ei0TwezDq9M41rGZWHz+qDRkPlw/f66YWT006tR1zR1voJYhshsJ21g==", + "dependencies": { + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-motion-components-preview": "^0.3.0", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-alert": { + "version": "9.0.0-beta.124", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.124.tgz", + "integrity": "sha512-yFBo3B5H9hnoaXxlkuz8wRz04DEyQ+ElYA/p5p+Vojf19Zuta8DmFZZ6JtWdtxcdnnQ4LvAfC5OYYlzdReozPA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.6.29", + "@fluentui/react-button": "^9.3.83", + "@fluentui/react-icons": "^2.0.239", + "@fluentui/react-jsx-runtime": "^9.0.39", + "@fluentui/react-tabster": "^9.21.5", + "@fluentui/react-theme": "^9.1.19", + "@fluentui/react-utilities": "^9.18.10", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-aria": { + "version": "9.13.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.13.9.tgz", + "integrity": "sha512-YURuZ2Nh7hz5VlCQ9NHLvzyqdiJhElm4aW/F4JRmXAoMdeDCfgG0UGL82DDPZL6eNYIjhQN8WpRXH2tfxJ80HA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-utilities": "^9.18.17", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-avatar": { + "version": "9.6.43", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.6.43.tgz", + "integrity": "sha512-N/bHM7ZriCrUupZ0jgK+cUHuOymIvs3JMxME6z/6711xwHH9PRM0vpu17O+oYsnwatELDaGsN5MWV4T6x1UDVA==", + "dependencies": { + "@fluentui/react-badge": "^9.2.45", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-popover": "^9.9.25", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-tooltip": "^9.4.43", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-badge": { + "version": "9.2.45", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.2.45.tgz", + "integrity": "sha512-X1dDCs0ZjQNx46VUAWYVvVfufARNtOQoXmcdldtd8kWnLDA4aAVI+/CX4bhZ/+qV9hiIowffuW/QPhNXWSozVQ==", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-breadcrumb": { + "version": "9.0.43", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.0.43.tgz", + "integrity": "sha512-kVve9azEzJn/6aZU1Hv2KVd3INkoSbX5kbIVUzDdsMZYeFpYp0V9Fz/akwa9jhSkONdqCpKpI/BbT8wRjWky9g==", + "dependencies": { + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-link": "^9.3.2", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-button": { + "version": "9.3.95", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.3.95.tgz", + "integrity": "sha512-kvwxBrCLXeFkgVy1+n01BZmRnEE/uPtapkUSInIXf8qQgOZzpLirLfrDqjBsTMd1Wosv9zgh27gqbiw92cqQSg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-card": { + "version": "9.0.97", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.0.97.tgz", + "integrity": "sha512-E8Rjkn88muKdn3ACn+WzpTsQYX/ldgZvuRT42PTdrIXeFsQ9RAWJ6TkMf5/FURxKlR29ChT5kIyCH/EzZ+iB0g==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-text": "^9.4.27", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-carousel": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.3.1.tgz", + "integrity": "sha512-nDUOVPAADNRlwg7/KtXgYEgALfll/Zcx7MAIqZkwxtroPzuOqm2CjeMVBwWoekEQzs75i+PgNgL1eXAQwgsAAQ==", + "dependencies": { + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1", + "embla-carousel": "^8.3.0", + "embla-carousel-autoplay": "^8.3.0", + "embla-carousel-fade": "^8.3.0" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-checkbox": { + "version": "9.2.41", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.2.41.tgz", + "integrity": "sha512-+vmoZIaAnN7Z9pxilXSleQJKyLoGksrU0d00huNLIOKFGIgkJHscJzrmAWDWHzFOg1MeGUtpfYYlE3L1N6ypBw==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-combobox": { + "version": "9.13.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.13.12.tgz", + "integrity": "sha512-Y710laYoJHmMu09ynLx+13hwtCLhCGqUbVdLCCQmsMzd4hCVNCuhT+ED+sJBTMp/NnyVjMDECJ11Fk5iTkUd0g==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-components": { + "version": "9.56.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.56.2.tgz", + "integrity": "sha512-WcxdvJGPK/xhS9FnmG8QaEM5/Es1Hbggmas5DCkuj2XGEexz4zWZ73tESb7QNYpMxhOKKprln0HfbSpg6c4xOw==", + "dependencies": { + "@fluentui/react-accordion": "^9.5.8", + "@fluentui/react-alert": "9.0.0-beta.124", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-avatar": "^9.6.43", + "@fluentui/react-badge": "^9.2.45", + "@fluentui/react-breadcrumb": "^9.0.43", + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-card": "^9.0.97", + "@fluentui/react-carousel": "^9.3.1", + "@fluentui/react-checkbox": "^9.2.41", + "@fluentui/react-combobox": "^9.13.12", + "@fluentui/react-dialog": "^9.11.21", + "@fluentui/react-divider": "^9.2.77", + "@fluentui/react-drawer": "^9.6.1", + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-image": "^9.1.75", + "@fluentui/react-infobutton": "9.0.0-beta.102", + "@fluentui/react-infolabel": "^9.0.50", + "@fluentui/react-input": "^9.4.93", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-link": "^9.3.2", + "@fluentui/react-menu": "^9.14.20", + "@fluentui/react-message-bar": "^9.2.15", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-overflow": "^9.2.1", + "@fluentui/react-persona": "^9.2.102", + "@fluentui/react-popover": "^9.9.25", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-progress": "^9.1.91", + "@fluentui/react-provider": "^9.18.0", + "@fluentui/react-radio": "^9.2.36", + "@fluentui/react-rating": "^9.0.22", + "@fluentui/react-search": "^9.0.22", + "@fluentui/react-select": "^9.1.91", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-skeleton": "^9.1.20", + "@fluentui/react-slider": "^9.2.0", + "@fluentui/react-spinbutton": "^9.2.92", + "@fluentui/react-spinner": "^9.5.2", + "@fluentui/react-swatch-picker": "^9.1.13", + "@fluentui/react-switch": "^9.1.98", + "@fluentui/react-table": "^9.15.22", + "@fluentui/react-tabs": "^9.6.2", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-tag-picker": "^9.3.9", + "@fluentui/react-tags": "^9.3.23", + "@fluentui/react-teaching-popover": "^9.1.22", + "@fluentui/react-text": "^9.4.27", + "@fluentui/react-textarea": "^9.3.92", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-toast": "^9.3.59", + "@fluentui/react-toolbar": "^9.2.10", + "@fluentui/react-tooltip": "^9.4.43", + "@fluentui/react-tree": "^9.8.6", + "@fluentui/react-utilities": "^9.18.17", + "@fluentui/react-virtualizer": "9.0.0-alpha.87", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-context-selector": { + "version": "9.1.69", + "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.1.69.tgz", + "integrity": "sha512-g29PE3cya7vY85o1ZwYMhPtkUyb7Q14UdrBCeEUr7+KjTPKMbkF27GKh0fAwwFuh9talvmI6fEVkJ9odYI6Dog==", + "dependencies": { + "@fluentui/react-utilities": "^9.18.17", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": ">=0.19.0 <=0.23.0" + } + }, + "node_modules/@fluentui/react-dialog": { + "version": "9.11.21", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.11.21.tgz", + "integrity": "sha512-zTBZKGG2z5gV3O9o00coN3p2wemMfiXfgTaiAb866I+htjN8/62BmzKSg32yygfVFaQnvlU1DhKAXd4SpfFAeg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-divider": { + "version": "9.2.77", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.2.77.tgz", + "integrity": "sha512-mo1ZhkD05p1PC8m5NnQjttIxCZnIy33wtV7w3zEtdlrpqtKvaHmOrbfJPMVVerVEZqX8SL2t5mhXX8AE/kjWyw==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-drawer": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.6.1.tgz", + "integrity": "sha512-KDVwTnY72rTq7st8bAIU8vfPM1e+q2wsYOdTaxnD6qVU7EcJc5QxT/FmM0jZ300zqrwhf8r4evGMCe7KZv+I6A==", + "dependencies": { + "@fluentui/react-dialog": "^9.11.21", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-field": { + "version": "9.1.80", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.1.80.tgz", + "integrity": "sha512-e+rVWTq5NUV7bq+PkTx+nxEIQOgRdA1RGyr2GG70qxtfus/JQoEteYMFoOFPiK0oJ0I0BfJf4NQG1mwnov7X0w==", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-focus": { + "version": "8.8.33", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-key": "^0.4.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-hooks": { + "version": "8.6.32", + "license": "MIT", + "dependencies": { + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.265", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.265.tgz", + "integrity": "sha512-bpiB4LGKv7LA6BsTHYLWuK6IH7CqqJYooHJfjaQ1i90OPfXpTmV1G/HB+6dIsmbAdKS14Z2bKM6Qb+yP3Ojuyg==", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-image": { + "version": "9.1.75", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.1.75.tgz", + "integrity": "sha512-pw4vL+j5/Qc9jSivfKRZ2qocx7W7BsfIFu/h8l89dg2OSvcLjUygWLYT/1KBz9oXIE8eQy6aZV/mvI3swhEWqw==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infobutton": { + "version": "9.0.0-beta.102", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.102.tgz", + "integrity": "sha512-3kA4F0Vga8Ds6JGlBajLCCDOo/LmPuS786Wg7ui4ZTDYVIMzy1yp2XuVcZniifBFvEp0HQCUoDPWUV0VI3FfzQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.237", + "@fluentui/react-jsx-runtime": "^9.0.36", + "@fluentui/react-label": "^9.1.68", + "@fluentui/react-popover": "^9.9.6", + "@fluentui/react-tabster": "^9.21.0", + "@fluentui/react-theme": "^9.1.19", + "@fluentui/react-utilities": "^9.18.7", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infolabel": { + "version": "9.0.50", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.0.50.tgz", + "integrity": "sha512-NrEFOD5An+aD4SGx1q0sGdqnMT5eVURigEDW1tm1HPk+Hl0bgmwSlwQwLw9ejfaC5g5SoPwFaVVM2VKLfn9qzw==", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-popover": "^9.9.25", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-input": { + "version": "9.4.93", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.4.93.tgz", + "integrity": "sha512-lKxB2mWYzN5bAGlYS1BMUISdAoNqKtW4d+s6vUf8lJdMFyQK4iC7QtcbS4x9FTQnSDV6cfVogp5k8JvUWs1Hww==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-jsx-runtime": { + "version": "9.0.46", + "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.0.46.tgz", + "integrity": "sha512-hdzwiRPnFQ8dqmqj/Xtep7SP2I+mx+OFsP5glzdDhTFL6au5yBbnUTgI6XEiSAbisBAhl2V2qsp0mJ55gxU+sg==", + "dependencies": { + "@fluentui/react-utilities": "^9.18.17", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-label": { + "version": "9.1.78", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.1.78.tgz", + "integrity": "sha512-0Tv8Du78+lt17mjkAeoJRfsZgFVbfk2INiGVsQ2caN0n/r1IStbKQVqqWFSjyw//qpFdyw3FGOL9SalPmqIZMA==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-link": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.3.2.tgz", + "integrity": "sha512-JIq2vhcqWug+GFw0EA5hVDXGzcRz4CBd/W/Mr9swlHIsA1BLMNxfHyIfZ6kZMT9IIQltWHK4CBFx2X/5co8DcA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-menu": { + "version": "9.14.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.14.20.tgz", + "integrity": "sha512-zinFHhQi2bwhv7GL8JXHwAfRYWw3hJhlUuWejLGQK1QbmwPlBHN6UCKhhIvF+RwEJbzeoyqvZcAusiHjmCp6rw==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-message-bar": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.2.15.tgz", + "integrity": "sha512-+FPH3ciNjTWVk9hGIeo/G8QGHf/q+tFLle4g9hXuOuDuzuaHNK6g7SkXTLm0fiZVrkB3xhFZV5ZnfehiN93S1w==", + "dependencies": { + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-link": "^9.3.2", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-motion": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.6.1.tgz", + "integrity": "sha512-P/ZPEAXG24pGU/XY3vY6VOXxNMEztiN7lvJxqUHGDFbpMkgQwCOmfsBuNU4S6RLQy3PosbWfSsU/4N8Ga2XudQ==", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-utilities": "^9.18.17", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-motion-components-preview": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.3.0.tgz", + "integrity": "sha512-N888xO727bSogyH0WUSW2pkjQ2vXEpyDa0Ygj+4XQaTfHz8DecDiKfM83zUpQ7pZOhx8eQPUP76flijm+iVm8w==", + "dependencies": { + "@fluentui/react-motion": "*", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-overflow": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.2.1.tgz", + "integrity": "sha512-6u+bP9PV1RedOSDgL+cHs4o3GRRWlEpKTtjeDSgs+nI5fkfN6bF+J70Uk5QksWDUBydMbkSbsD4Ta5+U2G6yww==", + "dependencies": { + "@fluentui/priority-overflow": "^9.1.14", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-persona": { + "version": "9.2.102", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.2.102.tgz", + "integrity": "sha512-sIoKr2A/zMkFudmeO1+asG6FIItn0+FbKOXezgApHuucbq6iU8oKV8+OEHhCr/mHPulDAV8JZQYkhNHFhzSjdA==", + "dependencies": { + "@fluentui/react-avatar": "^9.6.43", + "@fluentui/react-badge": "^9.2.45", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-popover": { + "version": "9.9.25", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.9.25.tgz", + "integrity": "sha512-QPhbD6MTDU6JuYZl0221IwqKEF3TEoNaL6kdAGnrltLuXVGX2pLr4LerHdbBORolfZZFo/JkKX644ay5X7BnvQ==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal": { + "version": "9.4.38", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.4.38.tgz", + "integrity": "sha512-V4lvnjlmKqMloNK6tRXx7lDWR1g41ppFLAGMy+0KAMZRwvwiCNpWrr9oFVGTHqnh+3EuICgs1z0WiNUcbpviuA==", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1", + "use-disposable": "^1.0.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal-compat-context": { + "version": "9.0.9", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-positioning": { + "version": "9.15.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.15.12.tgz", + "integrity": "sha512-FqopxQpf8KibdovNFLNqcDzckMgaMO2EAwXhpzH1us1l9vNofVE33k0sGHr1kU+M9TXCKeJ9x31TdS5XzBMPzQ==", + "dependencies": { + "@floating-ui/devtools": "0.2.1", + "@floating-ui/dom": "^1.2.0", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-progress": { + "version": "9.1.91", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.1.91.tgz", + "integrity": "sha512-7+po8q+kR30g6QutHIpro91l8NTkmSoOZRMuoiPesuIblqeoFPoywlBanJFvLRMAAQefILi0QaTri8+PtHFZwQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-provider": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.18.0.tgz", + "integrity": "sha512-qJS2D/g3h2GwAiw2V1uWLePpAG2CKP0Pg8/iKy6vCdeNgToOGTt7ZinJSNzVzdN1y6kE2Na1glTkDLDwBj9IKg==", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/core": "^1.16.0", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-radio": { + "version": "9.2.36", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.2.36.tgz", + "integrity": "sha512-G6sYBcT6tEHmXELPvSqzOd/CJeNv6X/IAgnyg9dvXQUw4gBwG7qYuVDQQPDyG+vncA//845eSOf+o8mvBIRUfQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-rating": { + "version": "9.0.22", + "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.0.22.tgz", + "integrity": "sha512-0mlOL2LDt1IrGOq3yIiM5niOk8Nmrip/Xef1Rnc4Q/X6EM66qwBk2fS0ZYtk4BXFlCn2sdsHeGwCy+6Dj7wgsQ==", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-search": { + "version": "9.0.22", + "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.0.22.tgz", + "integrity": "sha512-+ZerMQVdnX7PhodaUF92SQTxv/6YJfcLQ/o6uJ2ppsYpBj8DX2bgWnmX7Ia0T9MReHHvIodRQXVTAFpJSBA+Gg==", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-input": "^9.4.93", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-select": { + "version": "9.1.91", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.1.91.tgz", + "integrity": "sha512-mrQORisf6xWKrooCX6F7qqvcgDT7ei4YMtH5KHVa+sCRyy5CC0jOAVD513rj7ysAVxLKv9TSuF/rdx/Cmc7Kzw==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-shared-contexts": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.21.0.tgz", + "integrity": "sha512-GtP9zM7wpZtKXnq6qMd8ww0IN+5ZctPClVz83zDA602rJTJjihGwkmJ1ga8f/YphOTKcE12dnRQDl4iRL5vJ4A==", + "dependencies": { + "@fluentui/react-theme": "^9.1.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-skeleton": { + "version": "9.1.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.1.20.tgz", + "integrity": "sha512-nK1rJGTriJdXR9y820NHmLNRJ6YAiJUVGAtVb7OIi7KoX7/IXt/qY/xx91jnECaWHOPGzlNO+S4hxYkLiU80iQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-slider": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.2.0.tgz", + "integrity": "sha512-96oT573BxYns4+dgGLQOT5j/4QfNIebXelvrw13AfBRBV2+WZlAApnpPujaTzv+DA86c8l+M3tqzAz11kznHzQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinbutton": { + "version": "9.2.92", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.2.92.tgz", + "integrity": "sha512-lDfjsN1sj4ol4DEnlt1JJ0vKb8lmSMWSEWil1zgPL+wQyVCP389UsROWZuzWpUqa4PxBY78Z4LaAUQx8DM7Y8Q==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinner": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.5.2.tgz", + "integrity": "sha512-eY6/WgrzTWFgebae5oE9/KS0TA7xrz9LRUccTEwcFBJQgrUFVUHo2jDNdIEaxzpWUGq0usCMQW10PFepnsKEqg==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-swatch-picker": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.1.13.tgz", + "integrity": "sha512-gegZCrF+JpPPGPo0GHeJK5267LdIuBQ7sV4b0kLMmIbdzEPe9OFykb5M3PdtSpVCbwbwCX1dVcXG5cQZhAKfVA==", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-switch": { + "version": "9.1.98", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.1.98.tgz", + "integrity": "sha512-vvU2XVU9BVlJb6GGiDOOIJ/7q3XsfxuuUx6sA4ROWhHxFd+oPq3a7S5g6BhPfBZapIRDn4XjlSSxAnKxZFi8SA==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-label": "^9.1.78", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-table": { + "version": "9.15.22", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.15.22.tgz", + "integrity": "sha512-XQEmigbpWvDBHJQILcWMa9aJ4Nskt3D8t00GPuVeuSJP+1pW7aAz6MHYzDOeeVSDj1P8nk7sTSUss3TNd4VP5g==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-avatar": "^9.6.43", + "@fluentui/react-checkbox": "^9.2.41", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-radio": "^9.2.36", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tabs": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.6.2.tgz", + "integrity": "sha512-RjlKoF+QzfZ3FN7y+NIgcTcwPqecZYGxV7ij1HeWH05wkQcT+SFnu5GEeMfN05Snia/85zDdtiwSjHW4rllm4Q==", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tabster": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.23.0.tgz", + "integrity": "sha512-YW9CcDDc4S2wV/fMex5VMZ+Nudxz0X67smSPo29sUFtCowEomZ+PRNbUhGkAgizrm7gTUCs+ITdvxm0vpl+bcQ==", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1", + "keyborg": "^2.6.0", + "tabster": "^8.2.0" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tag-picker": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.3.9.tgz", + "integrity": "sha512-CX8+dbd3UX2Z2vy1guduBUPzqc9vVvEcyB4LSKkTjin8s2QH4+uip7oWA6ba6EpueFIocbE3X3+BYRiwoo01LA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-combobox": "^9.13.12", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-tags": "^9.3.23", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tags": { + "version": "9.3.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.3.23.tgz", + "integrity": "sha512-XX9NcAqBqkhTrbP2iYFp9LGA0NG5ZDf5X8FxtD+uUyDo+P9v6m6Tqqd0EHYtGB26aZLHTZWZTJpuq6klx/KdAQ==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-avatar": "^9.6.43", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-teaching-popover": { + "version": "9.1.22", + "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.1.22.tgz", + "integrity": "sha512-chzQ251KL19FPi1VRGiDMYLu/BnTUhMEyes2vaCyX8oZwcxvu37N/1PIQcbd9KCPN0kXX4TY3wVLZI8CFfporA==", + "dependencies": { + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-popover": "^9.9.25", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-text": { + "version": "9.4.27", + "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.4.27.tgz", + "integrity": "sha512-/a1/eibyGYcWsc5M0i32vOAD/zf2gD5lDjaLXSiwoerF+e0j7GLgjbTi63ZK3K3Sh2repTrW/nsAHhqbeQhMyw==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-textarea": { + "version": "9.3.92", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.3.92.tgz", + "integrity": "sha512-Vmv0l8rGs34pjNSUDPKazZVN2yiWbda0PWy9PhOTIZsl9DdcLwyLcge3tKHnxHBvqEz6c1VzKxgK3+liLaSxpg==", + "dependencies": { + "@fluentui/react-field": "^9.1.80", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-theme": { + "version": "9.1.22", + "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.1.22.tgz", + "integrity": "sha512-+orOyOsI0I7m6ovkU20soe8BUOS6eESfVAr3iZ+P9NsqtnCRNnrkOnfEmuOIh+UkNhljEkY9pVUSF1JPq+XHtg==", + "dependencies": { + "@fluentui/tokens": "1.0.0-alpha.19", + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-toast": { + "version": "9.3.59", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.3.59.tgz", + "integrity": "sha512-42+MBvjkwCmEj46pvwN0+8HABXJ0tbm1gSuAlaiQO5zIO+xWCZKLeqlGtbJ2DH6G6ZcOwBkiOXioOLyRS7t03A==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-toolbar": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.2.10.tgz", + "integrity": "sha512-lTix5YU3u85JnI/ISSraNIQDdj3FX6n2Xuzd27lGC6cebpI799NsZVfaprwNr5ywOwLlJ/B+kQXflQMZAJ4NxA==", + "dependencies": { + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-divider": "^9.2.77", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-radio": "^9.2.36", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tooltip": { + "version": "9.4.43", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.4.43.tgz", + "integrity": "sha512-KUIrs7uxjC916HT6XJgCfcxoxlbABi6TlriOzi/aELh0Gu5zH/9UPgvKw5BzWQUUyFLpjVOBKjogqI5SdsQGRg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-portal": "^9.4.38", + "@fluentui/react-positioning": "^9.15.12", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tree": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.8.6.tgz", + "integrity": "sha512-iqT7wRz3uz/zgUkuxCc7LeDBhtVNmv2fA2e5AoEgcFGJRck3b97G9l8bqiyaitqt/1MXLCKOf0LlTqLpe7mVbQ==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.13.9", + "@fluentui/react-avatar": "^9.6.43", + "@fluentui/react-button": "^9.3.95", + "@fluentui/react-checkbox": "^9.2.41", + "@fluentui/react-context-selector": "^9.1.69", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-motion": "^9.6.1", + "@fluentui/react-motion-components-preview": "^0.3.0", + "@fluentui/react-radio": "^9.2.36", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-tabster": "^9.23.0", + "@fluentui/react-theme": "^9.1.22", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-utilities": { + "version": "9.18.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.18.17.tgz", + "integrity": "sha512-xW3e+sNd14njyXX1ovI2I8Sz/kjuieGzEbMbduNQONERp6Doc4JItPyxXUgv20qZ8eFYO6AykcI+xCTpHRkiBA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-shared-contexts": "^9.21.0", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.87", + "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.87.tgz", + "integrity": "sha512-NbeZ9COirzepBqSnUjfAJzgep7b9Z718Rqrr66vMFkBSKC5pfkeS4qrQIXyansNndSy6AUz8i0SI/JLGS8wyNw==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.46", + "@fluentui/react-shared-contexts": "^9.21.0", + "@fluentui/react-utilities": "^9.18.17", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-window-provider": { + "version": "2.2.16", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/set-version": { + "version": "8.2.12", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/style-utilities": { + "version": "8.9.19", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/theme": { + "version": "2.6.37", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.19", + "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.19.tgz", + "integrity": "sha512-Y1MI/d/SVhheFglzG/hyyNynbUk9vby7yU4oMLbIlqNRyQw03hPE3LhHb1k9/EHAuLxRioezEcEhRfOD8ej8dQ==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/utilities": { + "version": "8.13.20", + "license": "MIT", + "dependencies": { + "@fluentui/dom-utilities": "^2.2.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.18.0.tgz", + "integrity": "sha512-3Dkn6f7ULeSzJ1wLyLfN1vc+v3q5shuEejeMe4XymBozQo0l35WIfH8FWcwB+Xrgip4fLLOy1p3sYN85gFGZxw==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.2.0", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.25", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.25.tgz", + "integrity": "sha512-ZGiCdn71VIX56fd3AxM7ouCxgClPvunOFIpXxFKebGJ94/rdj4sIbahuI1QBUFuU4/bqUyD6QonjDEpFBl9ORw==", + "license": "MIT", + "dependencies": { + "@griffel/core": "^1.18.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.2.0.tgz", + "integrity": "sha512-x166MNw0vWe5l5qhinfNT4eyWOaP48iFzPyFOfIB0/BVidKTWsEe5PmqRJDDtrJFS3VHhd/tE0oM6tkEMh2tsg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/load-themed-styles": { + "version": "1.10.295", + "license": "MIT" + }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.3", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", + "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", + "dev": true + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.7", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.7", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz", + "integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.32", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", + "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "dev": true + }, + "node_modules/embla-carousel": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.4.0.tgz", + "integrity": "sha512-sUzm4DGGsdZCom7LEO38Uu6C7oQoFfPorKDf/f7j2EeRCMhHSOt3CvF+pHCaI6N+x5Y8/tfLueJ0WZlgUREnew==" + }, + "node_modules/embla-carousel-autoplay": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.4.0.tgz", + "integrity": "sha512-AJHXrnaY+Tf4tb/+oItmJSpz4P0WvS62GrW5Z4iFY3zsH0mkKcijzd04LIkj0P4DkTazIBEuXple+nUVmuMsrQ==", + "peerDependencies": { + "embla-carousel": "8.4.0" + } + }, + "node_modules/embla-carousel-fade": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.4.0.tgz", + "integrity": "sha512-d2/Pk/gHnlLCwE0MuwjLxLn22ngTf1rS17KT+TsYctVCApvDvxwgn5bDrwSpwg4BZhO4+xIrWw293rAHjCDewQ==", + "peerDependencies": { + "embla-carousel": "8.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fault": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/format": { + "version": "0.2.2", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-from-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-from-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", + "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/i18next": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.1.tgz", + "integrity": "sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + }, + "node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==", + "license": "MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyborg": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz", + "integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ndjson-readablestream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz", + "integrity": "sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "license": "MIT", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-information": { + "version": "5.6.0", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-i18next": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz", + "integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-markdown/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", + "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", + "dependencies": { + "@remix-run/router": "1.21.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", + "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", + "dependencies": { + "@remix-run/router": "1.21.0", + "react-router": "6.28.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "license": "MIT" + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/scheduler": { + "version": "0.20.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tabster": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.2.0.tgz", + "integrity": "sha512-Gvplk/Yl/12aVFA6FPOqGcq31Qv8hbPfYO0N+6IxrRgRT6eSLsipT6gkZBYjyOwGsp6BD5XlZAuJgupfG/GHoA==", + "dependencies": { + "keyborg": "2.6.0", + "tslib": "^2.3.1" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-disposable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/use-disposable/-/use-disposable-1.0.4.tgz", + "integrity": "sha512-j83t6AMLWUyb5zwlTDqf6dP9LezM9R0yTbI/b6olmdaGtCKQUe9pgJWV6dRaaQLcozypjIEp4EmZr2DkZGKLSg==", + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json index 49f9fda5eda..224d93867d8 100644 --- a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/package.json @@ -1,11 +1,50 @@ { - "name": "chatbot-ts", - "version": "1.0.0", - "main": "main.ts", + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=14.0.0" + }, "scripts": { - "start": "ts-node main.ts" + "dev": "vite --host 127.0.0.1", + "build": "tsc && vite build", + "preview": "vite preview" }, "dependencies": { - "@azure/openai": "^1.0.0" + "@azure/msal-browser": "^3.26.1", + "@azure/msal-react": "^2.2.0", + "@fluentui/react": "^8.112.5", + "@fluentui/react-components": "^9.56.2", + "@fluentui/react-icons": "^2.0.265", + "@react-spring/web": "^9.7.5", + "dompurify": "^3.2.0", + "i18next": "^24.2.0", + "i18next-browser-languagedetector": "^8.0.2", + "i18next-http-backend": "^3.0.1", + "idb": "^8.0.0", + "ndjson-readablestream": "^1.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.28.0", + "react-helmet-async": "^2.0.5", + "react-i18next": "^15.1.1", + "react-syntax-highlighter": "^15.6.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "scheduler": "^0.20.2" + }, + "devDependencies": { + "@types/dom-speech-recognition": "^0.0.4", + "@types/dompurify": "^3.0.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react": "^4.3.3", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.14", + "rollup-plugin-visualizer": "^5.12.0" } } diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/public/favicon.ico b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f1fe50511ca0c33d95783506d4af99426dfc10bf GIT binary patch literal 4286 zcmeH}dr(y88OA?KWOvysX{UdrX?5Dk2n0}AK@g0WnRH4JL8DO=m3RY_c8nwq(U1w# zblPb%a#2_oP(cAfxywyJkeD=zq)sBaX_L6ZDzLE2?us@Vd!D}Eo;|v0z+{sC8JwT* z@$8=Sd*0_P<1*$&e`d{M{BKsRW9;XQF@J(ivBd;G&wsM$wa4g1&W}I$8|c~T?c4qQ zLmRqMWkp?SJ~>@Ge6l;!e6l+@sXjS`p?#Yy_Wl*$JQ~=;HeYy=6c%Kf!Ux=ZJCigSyTk z@tp5xu(*$Rv#qzA zCYq+a+3TcxoJ^Ce$lKy}3f$YeO^%kWGB1yf?-Yd+b-GCy?h&Her946SKixNPvpa9IcjOKAV%$L~XVuJSBK@)riCNcWK@xgMq+ z?}$zr@0#{ippo;eLLg9`>`T4KB0qhVZRCBs{N(Qa>}`v?~M8s1&(j@Mdf-W`qI5^mopg#Jp315 zrrozt2Y-DO)|zmb%gA3A1T*Cra^>Jzf&`w0I%)NEdLGlRDcsjUKJFw0K9AAfeD`xN zc;nKm;@*6B4)6ZGUF1*o!oNxaVX3D5j|3ATFdwEIyo<6R!CXoiTv>usJe{7$4JoRJ z$#=UhMUL7{3ZZtRLg4ON+Py#hC%E!D!>4-~=-G?ilb?6rvz_>(29~;T3?H3?>y;t6 zULJyL1fS-^Ap-B@T3PTNnp1W!<*47}+fcJnff{o1&MT6XXrtdpzBljugO|N9kiowE z{53XyDU&%bq%&tj5*vFzd)6&`bEN36;u(mFIk?7g=OBNhSU$XXRg;InLv3i%1j#nLsCrxz|BZ_s13hTNRv zp%^(9>dG=)J(sfZ40HZ%t_add*F0z*U9W85pY=-eaw<4UDxq8iG;v_1tWvs%t#w|7 zr9KRn`cRD4k((H;B`C`<$Ub7={&qviNoaN@Xk4<&v4WzOz?$qwRZ0H z`mkU}X9UKY^%y%rG*X5}J;5`GolqZkYjfz7|94H7rEIObvTTj=p7W6C6%MUd3kS(x zuv~?`@#;czU8vD{DgxuJ^Dx%p&OkXVt}HaEld-1I^65O&oiAOZP2{^CTCD>29b8SU zqUXL!jRSO?{X1X0nWzzloAv*>(KBBdZwn{p2|UL*&*9EuY0|^mtWTKEGu?Ue`;gP= z@~zU4Q-flf#pEw}T7%aX_~6Lze}UU)qv1FeAy`j_b6{-`2W6o?!o>_NdWWqwQa+vM zLFbE~QJ0rIO`kRL6?u@KphiZN9B1t95$Z8m4lv1`#f0NBJTa3 zJjgZGYsOWYr;1jpVO*&};R+GsN-h5Oq!NFL_roYTZ@ses6Q|~jbWx5@^7A|d&tYqe z_{MjZsa#W?GbHL}7*=SkE_b3zP0 zKv{%|Zt9>r>TBPZzN^#oeFX`+xcvz_xDno1qQalz{9x^k!kzbG;CMRm>}=nfhTa|d<;SUz6D>;@op6L5%Yt)m;AZ0 zx*30u^)v0%D^Z*I8{o4gxb@E%O!m8>9Fr1#)XCZC&+Wa7{x9828UDOf8=JXQXZWm1 zpNq?jvnTtbvv2mtWZyjRp7ig@^U=At`lIu2o{N4`y6(sM;0ECS$(q^gm8H!hm{*pD zdG)H8cQ0e!CdOnEcE+?W=v`lWWzBTK*vz|(Es_uzdtCyI;T~CDfJCpiq{V*$i&S=H literal 0 HcmV?d00001 diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/api.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/api.ts new file mode 100644 index 00000000000..df95f801b5f --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/api.ts @@ -0,0 +1,191 @@ +const BACKEND_URI = ""; + +import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse, HistoryListApiResponse, HistoryApiResponse } from "./models"; +import { useLogin, getToken, isUsingAppServicesLogin } from "../authConfig"; + +export async function getHeaders(idToken: string | undefined): Promise> { + // If using login and not using app services, add the id token of the logged in account as the authorization + if (useLogin && !isUsingAppServicesLogin) { + if (idToken) { + return { Authorization: `Bearer ${idToken}` }; + } + } + + return {}; +} + +export async function configApi(): Promise { + const response = await fetch(`${BACKEND_URI}/config`, { + method: "GET" + }); + + return (await response.json()) as Config; +} + +export async function askApi(request: ChatAppRequest, idToken: string | undefined): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`${BACKEND_URI}/ask`, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(request) + }); + + if (response.status > 299 || !response.ok) { + throw Error(`Request failed with status ${response.status}`); + } + const parsedResponse: ChatAppResponseOrError = await response.json(); + if (parsedResponse.error) { + throw Error(parsedResponse.error); + } + + return parsedResponse as ChatAppResponse; +} + +export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined): Promise { + let url = `${BACKEND_URI}/chat`; + if (shouldStream) { + url += "/stream"; + } + const headers = await getHeaders(idToken); + return await fetch(url, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(request) + }); +} + +export async function getSpeechApi(text: string): Promise { + return await fetch("/speech", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + text: text + }) + }) + .then(response => { + if (response.status == 200) { + return response.blob(); + } else if (response.status == 400) { + console.log("Speech synthesis is not enabled."); + return null; + } else { + console.error("Unable to get speech synthesis."); + return null; + } + }) + .then(blob => (blob ? URL.createObjectURL(blob) : null)); +} + +export function getCitationFilePath(citation: string): string { + return `${BACKEND_URI}/content/${citation}`; +} + +export async function uploadFileApi(request: FormData, idToken: string): Promise { + const response = await fetch("/upload", { + method: "POST", + headers: await getHeaders(idToken), + body: request + }); + + if (!response.ok) { + throw new Error(`Uploading files failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; +} + +export async function deleteUploadedFileApi(filename: string, idToken: string): Promise { + const headers = await getHeaders(idToken); + const response = await fetch("/delete_uploaded", { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify({ filename }) + }); + + if (!response.ok) { + throw new Error(`Deleting file failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; +} + +export async function listUploadedFilesApi(idToken: string): Promise { + const response = await fetch(`/list_uploaded`, { + method: "GET", + headers: await getHeaders(idToken) + }); + + if (!response.ok) { + throw new Error(`Listing files failed: ${response.statusText}`); + } + + const dataResponse: string[] = await response.json(); + return dataResponse; +} + +export async function postChatHistoryApi(item: any, idToken: string): Promise { + const headers = await getHeaders(idToken); + const response = await fetch("/chat_history", { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(item) + }); + + if (!response.ok) { + throw new Error(`Posting chat history failed: ${response.statusText}`); + } + + const dataResponse: any = await response.json(); + return dataResponse; +} + +export async function getChatHistoryListApi(count: number, continuationToken: string | undefined, idToken: string): Promise { + const headers = await getHeaders(idToken); + let url = `${BACKEND_URI}/chat_history/sessions?count=${count}`; + if (continuationToken) { + url += `&continuationToken=${continuationToken}`; + } + + const response = await fetch(url.toString(), { + method: "GET", + headers: { ...headers, "Content-Type": "application/json" } + }); + + if (!response.ok) { + throw new Error(`Getting chat histories failed: ${response.statusText}`); + } + + const dataResponse: HistoryListApiResponse = await response.json(); + return dataResponse; +} + +export async function getChatHistoryApi(id: string, idToken: string): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`/chat_history/sessions/${id}`, { + method: "GET", + headers: { ...headers, "Content-Type": "application/json" } + }); + + if (!response.ok) { + throw new Error(`Getting chat history failed: ${response.statusText}`); + } + + const dataResponse: HistoryApiResponse = await response.json(); + return dataResponse; +} + +export async function deleteChatHistoryApi(id: string, idToken: string): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`/chat_history/sessions/${id}`, { + method: "DELETE", + headers: { ...headers, "Content-Type": "application/json" } + }); + + if (!response.ok) { + throw new Error(`Deleting chat history failed: ${response.statusText}`); + } +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/index.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/index.ts new file mode 100644 index 00000000000..0475d357adb --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./models"; diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/models.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/models.ts new file mode 100644 index 00000000000..f560271325e --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/api/models.ts @@ -0,0 +1,123 @@ +export const enum RetrievalMode { + Hybrid = "hybrid", + Vectors = "vectors", + Text = "text" +} + +export const enum GPT4VInput { + TextAndImages = "textAndImages", + Images = "images", + Texts = "texts" +} + +export const enum VectorFieldOptions { + Embedding = "embedding", + ImageEmbedding = "imageEmbedding", + Both = "both" +} + +export type ChatAppRequestOverrides = { + retrieval_mode?: RetrievalMode; + semantic_ranker?: boolean; + semantic_captions?: boolean; + include_category?: string; + exclude_category?: string; + seed?: number; + top?: number; + temperature?: number; + minimum_search_score?: number; + minimum_reranker_score?: number; + prompt_template?: string; + prompt_template_prefix?: string; + prompt_template_suffix?: string; + suggest_followup_questions?: boolean; + use_oid_security_filter?: boolean; + use_groups_security_filter?: boolean; + use_gpt4v?: boolean; + gpt4v_input?: GPT4VInput; + vector_fields: VectorFieldOptions[]; + language: string; +}; + +export type ResponseMessage = { + content: string; + role: string; +}; + +export type Thoughts = { + title: string; + description: any; // It can be any output from the api + props?: { [key: string]: string }; +}; + +export type ResponseContext = { + data_points: string[]; + followup_questions: string[] | null; + thoughts: Thoughts[]; +}; + +export type ChatAppResponseOrError = { + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; + error?: string; +}; + +export type ChatAppResponse = { + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; +}; + +export type ChatAppRequestContext = { + overrides?: ChatAppRequestOverrides; +}; + +export type ChatAppRequest = { + messages: ResponseMessage[]; + context?: ChatAppRequestContext; + session_state: any; +}; + +export type Config = { + showGPT4VOptions: boolean; + showSemanticRankerOption: boolean; + showVectorOption: boolean; + showUserUpload: boolean; + showLanguagePicker: boolean; + showSpeechInput: boolean; + showSpeechOutputBrowser: boolean; + showSpeechOutputAzure: boolean; + showChatHistoryBrowser: boolean; + showChatHistoryCosmos: boolean; +}; + +export type SimpleAPIResponse = { + message?: string; +}; + +export interface SpeechConfig { + speechUrls: (string | null)[]; + setSpeechUrls: (urls: (string | null)[]) => void; + audio: HTMLAudioElement; + isPlaying: boolean; + setIsPlaying: (isPlaying: boolean) => void; +} + +export type HistoryListApiResponse = { + sessions: { + id: string; + entra_oid: string; + title: string; + timestamp: number; + }[]; + continuation_token?: string; +}; + +export type HistoryApiResponse = { + id: string; + entra_oid: string; + answers: any; +}; diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/assets/applogo.svg b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/assets/applogo.svg new file mode 100644 index 00000000000..fb3a5b97126 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/assets/applogo.svg @@ -0,0 +1 @@ + diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/authConfig.ts b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/authConfig.ts new file mode 100644 index 00000000000..60de0e1a8c9 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/authConfig.ts @@ -0,0 +1,252 @@ +// Refactored from https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/blob/main/1-Authentication/1-sign-in/SPA/src/authConfig.js + +import { IPublicClientApplication } from "@azure/msal-browser"; + +const appServicesAuthTokenUrl = ".auth/me"; +const appServicesAuthTokenRefreshUrl = ".auth/refresh"; +const appServicesAuthLogoutUrl = ".auth/logout?post_logout_redirect_uri=/"; + +interface AppServicesToken { + id_token: string; + access_token: string; + user_claims: Record; + expires_on: string; +} + +interface AuthSetup { + // Set to true if login elements should be shown in the UI + useLogin: boolean; + // Set to true if access control is enforced by the application + requireAccessControl: boolean; + // Set to true if the application allows unauthenticated access (only applies for documents without access control) + enableUnauthenticatedAccess: boolean; + /** + * Configuration object to be passed to MSAL instance on creation. + * For a full list of MSAL.js configuration parameters, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md + */ + msalConfig: { + auth: { + clientId: string; // Client app id used for login + authority: string; // Directory to use for login https://learn.microsoft.com/entra/identity-platform/msal-client-application-configuration#authority + redirectUri: string; // Points to window.location.origin. You must register this URI on Azure Portal/App Registration. + postLogoutRedirectUri: string; // Indicates the page to navigate after logout. + navigateToLoginRequestUrl: boolean; // If "true", will navigate back to the original request location before processing the auth code response. + }; + cache: { + cacheLocation: string; // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. + storeAuthStateInCookie: boolean; // Set this to "true" if you are having issues on IE11 or Edge + }; + }; + loginRequest: { + /** + * Scopes you add here will be prompted for user consent during sign-in. + * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + * For more information about OIDC scopes, visit: + * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes + */ + scopes: Array; + }; + tokenRequest: { + scopes: Array; + }; +} + +// Fetch the auth setup JSON data from the API if not already cached +async function fetchAuthSetup(): Promise { + const response = await fetch("/auth_setup"); + if (!response.ok) { + throw new Error(`auth setup response was not ok: ${response.status}`); + } + return await response.json(); +} + +const authSetup = await fetchAuthSetup(); + +export const useLogin = authSetup.useLogin; + +export const requireAccessControl = authSetup.requireAccessControl; + +export const enableUnauthenticatedAccess = authSetup.enableUnauthenticatedAccess; + +export const requireLogin = requireAccessControl && !enableUnauthenticatedAccess; + +/** + * Configuration object to be passed to MSAL instance on creation. + * For a full list of MSAL.js configuration parameters, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md + */ +export const msalConfig = authSetup.msalConfig; + +/** + * Scopes you add here will be prompted for user consent during sign-in. + * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + * For more information about OIDC scopes, visit: + * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes + */ +export const loginRequest = authSetup.loginRequest; + +const tokenRequest = authSetup.tokenRequest; + +// Build an absolute redirect URI using the current window's location and the relative redirect URI from auth setup +export const getRedirectUri = () => { + return window.location.origin + authSetup.msalConfig.auth.redirectUri; +}; + +// Cache the app services token if it's available +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#global_context +declare global { + var cachedAppServicesToken: AppServicesToken | null; +} +globalThis.cachedAppServicesToken = null; + +/** + * Retrieves an access token if the user is logged in using app services authentication. + * Checks if the current token is expired and fetches a new token if necessary. + * Returns null if the app doesn't support app services authentication. + * + * @returns {Promise} A promise that resolves to an AppServicesToken if the user is authenticated, or null if authentication is not supported or fails. + */ +const getAppServicesToken = (): Promise => { + const checkNotExpired = (appServicesToken: AppServicesToken) => { + const currentDate = new Date(); + const expiresOnDate = new Date(appServicesToken.expires_on); + return expiresOnDate > currentDate; + }; + + if (globalThis.cachedAppServicesToken && checkNotExpired(globalThis.cachedAppServicesToken)) { + return Promise.resolve(globalThis.cachedAppServicesToken); + } + + const getAppServicesTokenFromMe: () => Promise = () => { + return fetch(appServicesAuthTokenUrl).then(r => { + if (r.ok) { + return r.json().then(json => { + if (json.length > 0) { + return { + id_token: json[0]["id_token"] as string, + access_token: json[0]["access_token"] as string, + user_claims: json[0]["user_claims"].reduce((acc: Record, item: Record) => { + acc[item.typ] = item.val; + return acc; + }, {}) as Record, + expires_on: json[0]["expires_on"] as string + } as AppServicesToken; + } + + return null; + }); + } + + return null; + }); + }; + + return getAppServicesTokenFromMe().then(token => { + if (token) { + if (checkNotExpired(token)) { + globalThis.cachedAppServicesToken = token; + return token; + } + + return fetch(appServicesAuthTokenRefreshUrl).then(r => { + if (r.ok) { + return getAppServicesTokenFromMe(); + } + return null; + }); + } + + return null; + }); +}; + +export const isUsingAppServicesLogin = (await getAppServicesToken()) != null; + +// Sign out of app services +// Learn more at https://learn.microsoft.com/azure/app-service/configure-authentication-customize-sign-in-out#sign-out-of-a-session +export const appServicesLogout = () => { + window.location.href = appServicesAuthLogoutUrl; +}; + +/** + * Determines if the user is logged in either via the MSAL public client application or the app services login. + * @param {IPublicClientApplication | undefined} client - The MSAL public client application instance, or undefined if not available. + * @returns {Promise} A promise that resolves to true if the user is logged in, false otherwise. + */ +export const checkLoggedIn = async (client: IPublicClientApplication | undefined): Promise => { + if (client) { + const activeAccount = client.getActiveAccount(); + if (activeAccount) { + return true; + } + } + + const appServicesToken = await getAppServicesToken(); + if (appServicesToken) { + return true; + } + + return false; +}; + +// Get an access token for use with the API server. +// ID token received when logging in may not be used for this purpose because it has the incorrect audience +// Use the access token from app services login if available +export const getToken = async (client: IPublicClientApplication): Promise => { + const appServicesToken = await getAppServicesToken(); + if (appServicesToken) { + return Promise.resolve(appServicesToken.access_token); + } + + return client + .acquireTokenSilent({ + ...tokenRequest, + redirectUri: getRedirectUri() + }) + .then(r => r.accessToken) + .catch(error => { + console.log(error); + return undefined; + }); +}; + +/** + * Retrieves the username of the active account. + * If no active account is found, attempts to retrieve the username from the app services login token if available. + * @param {IPublicClientApplication} client - The MSAL public client application instance. + * @returns {Promise} The username of the active account, or null if no username is found. + */ +export const getUsername = async (client: IPublicClientApplication): Promise => { + const activeAccount = client.getActiveAccount(); + if (activeAccount) { + return activeAccount.username; + } + + const appServicesToken = await getAppServicesToken(); + if (appServicesToken?.user_claims) { + return appServicesToken.user_claims.preferred_username; + } + + return null; +}; + +/** + * Retrieves the token claims of the active account. + * If no active account is found, attempts to retrieve the token claims from the app services login token if available. + * @param {IPublicClientApplication} client - The MSAL public client application instance. + * @returns {Promise | undefined>} A promise that resolves to the token claims of the active account, the user claims from the app services login token, or undefined if no claims are found. + */ +export const getTokenClaims = async (client: IPublicClientApplication): Promise | undefined> => { + const activeAccount = client.getActiveAccount(); + if (activeAccount) { + return activeAccount.idTokenClaims; + } + + const appServicesToken = await getAppServicesToken(); + if (appServicesToken) { + return appServicesToken.user_claims; + } + + return undefined; +}; diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.module.css b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.module.css new file mode 100644 index 00000000000..2d22130da45 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.module.css @@ -0,0 +1,64 @@ +.thoughtProcess { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + word-wrap: break-word; + padding-top: 0.75em; + padding-bottom: 0.75em; +} + +.tList { + padding: 1.25em 1.25em 0 1.25em; + display: inline-block; + background: #e9e9e9; +} + +.tListItem { + list-style: none; + margin: auto; + margin-left: 1.25em; + min-height: 3.125em; + border-left: 0.0625em solid #123bb6; + padding: 0 0 1.875em 1.875em; + position: relative; +} + +.tListItem:last-child { + border-left: 0; +} + +.tListItem::before { + position: absolute; + left: -18px; + top: -5px; + content: " "; + border: 8px solid #d1dbfa; + border-radius: 500%; + background: #123bb6; + height: 20px; + width: 20px; +} + +.tStep { + color: #123bb6; + position: relative; + font-size: 0.875em; + margin-bottom: 0.5em; +} + +.tCodeBlock { + max-height: 18.75em; +} + +.tProp { + background-color: #d7d7d7; + color: #333232; + font-size: 0.75em; + padding: 0.1875em 0.625em; + border-radius: 0.625em; + margin-bottom: 0.5em; +} + +.citationImg { + height: 28.125rem; + max-width: 100%; + object-fit: contain; +} diff --git a/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.tsx b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.tsx new file mode 100644 index 00000000000..2cee00c7610 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.ai.builder/internal/resources/scenarios/rag/chatbot/ts/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -0,0 +1,103 @@ +import { Stack, Pivot, PivotItem } from "@fluentui/react"; +import { useTranslation } from "react-i18next"; +import styles from "./AnalysisPanel.module.css"; + +import { SupportingContent } from "../SupportingContent"; +import { ChatAppResponse } from "../../api"; +import { AnalysisPanelTabs } from "./AnalysisPanelTabs"; +import { ThoughtProcess } from "./ThoughtProcess"; +import { MarkdownViewer } from "../MarkdownViewer"; +import { useMsal } from "@azure/msal-react"; +import { getHeaders } from "../../api"; +import { useLogin, getToken } from "../../authConfig"; +import { useState, useEffect } from "react"; + +interface Props { + className: string; + activeTab: AnalysisPanelTabs; + onActiveTabChanged: (tab: AnalysisPanelTabs) => void; + activeCitation: string | undefined; + citationHeight: string; + answer: ChatAppResponse; +} + +const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; + +export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged }: Props) => { + const isDisabledThoughtProcessTab: boolean = !answer.context.thoughts; + const isDisabledSupportingContentTab: boolean = !answer.context.data_points; + const isDisabledCitationTab: boolean = !activeCitation; + const [citation, setCitation] = useState(""); + + const client = useLogin ? useMsal().instance : undefined; + const { t } = useTranslation(); + + const fetchCitation = async () => { + const token = client ? await getToken(client) : undefined; + if (activeCitation) { + // Get hash from the URL as it may contain #page=N + // which helps browser PDF renderer jump to correct page N + const originalHash = activeCitation.indexOf("#") ? activeCitation.split("#")[1] : ""; + const response = await fetch(activeCitation, { + method: "GET", + headers: await getHeaders(token) + }); + const citationContent = await response.blob(); + let citationObjectUrl = URL.createObjectURL(citationContent); + // Add hash back to the new blob URL + if (originalHash) { + citationObjectUrl += "#" + originalHash; + } + setCitation(citationObjectUrl); + } + }; + useEffect(() => { + fetchCitation(); + }, []); + + const renderFileViewer = () => { + if (!activeCitation) { + return null; + } + + const fileExtension = activeCitation.split(".").pop()?.toLowerCase(); + switch (fileExtension) { + case "png": + return Citation Image; + case "md": + return ; + default: + return