diff --git a/.azure/modules/api-appservice.bicep b/.azure/modules/api-appservice.bicep index 81c296d..9c2bc80 100644 --- a/.azure/modules/api-appservice.bicep +++ b/.azure/modules/api-appservice.bicep @@ -41,16 +41,16 @@ param planId string ]) param kind string = 'app' -@description('The .NET version for the Web App. Allowed values: v4.8 (for .NET Framework), 6.0, 7.0, 8.0, 9.0, 10.0 (for .NET). Default is 10.0.') +@description('The .NET stack version for the Windows API App. Use v4.8 for .NET Framework, or v6.0/v7.0/v8.0/v9.0/v10.0 for modern .NET. Default is v10.0.') @allowed([ 'v4.8' - '6.0' - '7.0' - '8.0' - '9.0' - '10.0' + 'v6.0' + 'v7.0' + 'v8.0' + 'v9.0' + 'v10.0' ]) -param dotnetVersion string = '10.0' +param dotnetVersion string = 'v10.0' @description('Enable Always On for the App Service') param alwaysOn bool = false diff --git a/.azure/modules/azoai-azureopenai.bicep b/.azure/modules/azoai-azureopenai.bicep deleted file mode 100644 index 14f07b1..0000000 --- a/.azure/modules/azoai-azureopenai.bicep +++ /dev/null @@ -1,36 +0,0 @@ -@description('Name of the Azure OpenAI instance. Must be globally unique and 3-63 characters, using only lowercase letters, numbers, and hyphens, starting and ending with a letter or number.') -@minLength(3) -@maxLength(63) -param name string - -@description('Location for the Azure OpenAI resource.') -param location string - -@description('Tags to apply to the Azure OpenAI resource.') -param tags object = {} - -@description('SKU for Azure OpenAI. Allowed values: S0, Default is S0.') -@allowed([ - 'S0' -]) -param sku string = 'S0' - -resource azoaiResource 'Microsoft.CognitiveServices/accounts@2023-05-01' = { - name: name - location: location - tags: tags - kind: 'OpenAI' - sku: { - name: sku - } - properties: { - apiProperties: { - enableDynamicThrottling: true - } - networkAcls: { - defaultAction: 'Allow' - } - } -} - -output id string = azoaiResource.id diff --git a/.azure/modules/web-appservice.bicep b/.azure/modules/web-appservice.bicep index b77fd7f..fdabb7a 100644 --- a/.azure/modules/web-appservice.bicep +++ b/.azure/modules/web-appservice.bicep @@ -40,16 +40,16 @@ param planId string ]) param kind string = 'app' -@description('The .NET version for the Web App. Allowed values: v4.8 (for .NET Framework), 6.0, 7.0, 8.0, 9.0, 10.0 (for .NET). Default is 10.0.') +@description('The .NET stack version for the Windows Web App. Use v4.8 for .NET Framework, or v6.0/v7.0/v8.0/v9.0/v10.0 for modern .NET. Default is v10.0.') @allowed([ 'v4.8' - '6.0' - '7.0' - '8.0' - '9.0' - '10.0' + 'v6.0' + 'v7.0' + 'v8.0' + 'v9.0' + 'v10.0' ]) -param dotnetVersion string = '10.0' +param dotnetVersion string = 'v10.0' @description('Enable Always On for the App Service') param alwaysOn bool = false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d2865a5..33f0afd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -33,6 +33,11 @@ - **RBAC & Security:** Enforced in API layer, see `ConfigureServicesAuth.cs`. - **Auth-Triggered User Provisioning:** Keep the flow UI-tied (not middleware/pipeline) because OBO token acquisition requires user context on the main UI thread/circuit. +## UI Design Guidelines +- **ChatPage Layout:** Implement distinct desktop and mobile layouts with a strict row/column structure on mobile. +- **Input Visibility:** Ensure persistent visible input under a capped message list height. +- **Component Usage:** Use only components in `.razor` files (no raw HTML elements), except in `App.razor` where raw HTML is allowed. + ## References - [README.md](../README.md): Project overview and getting started diff --git a/.github/workflows/gtc-agent-standalone-web-api-sql.yml b/.github/workflows/gtc-agent-standalone-web-api-sql-aoai.yml similarity index 92% rename from .github/workflows/gtc-agent-standalone-web-api-sql.yml rename to .github/workflows/gtc-agent-standalone-web-api-sql-aoai.yml index ea38653..80774d6 100644 --- a/.github/workflows/gtc-agent-standalone-web-api-sql.yml +++ b/.github/workflows/gtc-agent-standalone-web-api-sql-aoai.yml @@ -4,13 +4,13 @@ on: branches: - main paths: - - .github/workflows/gtc-agent-standalone-web-api-sql.yml + - .github/workflows/gtc-agent-standalone-web-api-sql-aoai.yml - src/** push: branches: - main paths: - - .github/workflows/gtc-agent-standalone-web-api-sql.yml + - .github/workflows/gtc-agent-standalone-web-api-sql-aoai.yml - src/** workflow_dispatch: inputs: @@ -29,6 +29,14 @@ permissions: security-events: write env: + # Manual environment-specific values (update these first) + APPI_NAME: 'agent-dev-wus2-001-appi' + AZURE_APIAPP_NAME: 'agent-dev-wus2-001-api' + AZURE_WEBAPP_NAME: 'agent-dev-wus2-001-web' + AZURE_RG_NAME: 'gtc-agent-dev-wus2-001-rg' + SQL_NAME: 'agent-dev-wus2-001-sql' + SQLDB_NAME: 'agent-dev-wus2-001-sqldb' + API_ARTIFACT_OUTPUT: 'publish_output' WEB_ARTIFACT_OUTPUT: 'publish_web_output' API_ARTIFACT_NAME: 'api-artifact' @@ -94,7 +102,8 @@ jobs: - name: pipeline environment configuration run: | echo "ASPNETCORE_ENVIRONMENT=${{ env.RUNTIME_ENV }}" >> $GITHUB_ENV - echo "OpenAI:ApiKey=${{ secrets.OPENAI_APIKEY }}" >> $GITHUB_ENV + echo "AzureOpenAI:Endpoint=${{ secrets.AZURE_OPENAI_ENDPOINT }}" >> $GITHUB_ENV + echo "AzureOpenAI:ApiKey=${{ secrets.AZURE_OPENAI_APIKEY }}" >> $GITHUB_ENV shell: pwsh - name: App Settings Variable Substitution @@ -102,7 +111,8 @@ jobs: with: files: '${{ env.SRC_PATH }}/${{ env.API_PATH }}/appsettings.json, ${{ env.SRC_PATH }}/${{ env.API_PATH }}/appsettings.${{ env.RUNTIME_ENV }}.json, ${{ env.SRC_PATH }}/${{ env.TEST_PATH }}/appsettings.test.json' env: - OpenAI.ApiKey: ${{ secrets.OPENAI_APIKEY }} + AzureOpenAI.Endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AzureOpenAI.ApiKey: ${{ secrets.AZURE_OPENAI_APIKEY }} - name: Initialize CodeQL uses: github/codeql-action/init@v4 @@ -200,13 +210,7 @@ jobs: matrix: DOTNET_VERSION: ["10.x"] env: - APPI_NAME: 'agent-dev-wus2-001-appi' - AZURE_APIAPP_NAME: 'agent-dev-wus2-001-api' AZURE_WEBAPP_PACKAGE_PATH: '.' - AZURE_WEBAPP_NAME: 'agent-dev-wus2-001-web' - AZURE_RG_NAME: 'gtc-agent-dev-wus2-001-rg' - SQL_NAME: 'agent-dev-wus2-001-sql' - SQLDB_NAME: 'agent-dev-wus2-001-sqldb' RUNTIME_ENV: "Development" steps: @@ -268,7 +272,8 @@ jobs: az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings ASPNETCORE_ENVIRONMENT=${{ env.RUNTIME_ENV }} az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings EntraExternalId:TenantId=${{ secrets.EEID_TENANT_ID }} az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings EntraExternalId:ClientId=${{ secrets.API_CLIENT_ID }} - az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings OpenAI:ApiKey=${{ secrets.OPENAI_APIKEY }} + az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings AzureOpenAI:Endpoint=${{ secrets.AZURE_OPENAI_ENDPOINT }} + az webapp config appsettings set -g ${{ env.AZURE_RG_NAME }} -n ${{ env.AZURE_APIAPP_NAME }} --settings AzureOpenAI:ApiKey=${{ secrets.AZURE_OPENAI_APIKEY }} shell: pwsh - name: ${{ env.AZURE_APIAPP_NAME }} connection strings diff --git a/README.md b/README.md index d1c6eeb..759fc84 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Clean Architecture C# Blazor Microapp and Web API Microservice** -[![Web & API & SQL CI/CD](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-web-api-sql.yml/badge.svg)](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-web-api-sql.yml) +[![Web & API & SQL CI/CD](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-web-api-sql-aoai.yml/badge.svg)](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-web-api-sql-aoai.yml) Microsoft Agent Framework Quick-start is a enterprise-ready starter kit for building modern, agentic applications with C#, Blazor (Fluent UI), and ASP.NET Core Web API. This solution demonstrates how to use the Microsoft Agent Framework to create a Copilot-style chat client, fully integrated with SQL Server for persistent storage of authors, chat sessions, and messages—all orchestrated through a clean architecture pattern. diff --git a/Reset-EfDatabase.ps1 b/Reset-EfDatabase.ps1 new file mode 100644 index 0000000..4dda5a6 --- /dev/null +++ b/Reset-EfDatabase.ps1 @@ -0,0 +1,182 @@ +<# +===================================================================== +EF database reset script (drop + re-migrate) +Example usage: + 1. Open PowerShell in solution root + 2. Run: ./Reset-EfDatabase.ps1 + or: ./Reset-EfDatabase.ps1 -Products $customProducts + For each product: + 3. Script will drop the database + 4. Script will apply existing EF migrations to recreate schema + +Notes: + - This script does NOT create new migrations. + - This script does NOT run NSwag client generation. +===================================================================== +#> + +param ( + [Parameter(Mandatory = $false)] + [array]$Products = @( + @{ Name = "AgentFramework"; Root = ".\src"; Database = "AgentFramework"; ApiProject = "Presentation.Api" } + ), + [switch]$DropDatabase, + [switch]$DropTables, + [string]$DropTablesPath = ".\data\Admin\Drop Tables.sql" +) + +Push-Location +try { + function Invoke-DiagnosticCommand($cmd) { + Write-Host "[DIAG] Running: $cmd" -ForegroundColor Yellow + try { + Invoke-Expression $cmd + } + catch { + Write-Error "[ERROR] Command failed: $cmd" + Write-Error $_ + throw + } + } + + function Install-SqlCmd { + $sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue + if (-not $sqlcmd) { + Write-Host "sqlcmd not found. Installing via winget..." -ForegroundColor Yellow + winget install --id Microsoft.SQLServerCommandLineTools -e --silent + $sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue + if (-not $sqlcmd) { + throw "sqlcmd installation failed. Please install manually." + } + Write-Host "sqlcmd installed." -ForegroundColor Green + } + else { + Write-Host "sqlcmd is already installed." -ForegroundColor Green + } + } + + function Test-DatabaseExists { + param ( + [string]$DatabaseName + ) + + $query = "IF DB_ID('$DatabaseName') IS NOT NULL SELECT 1 ELSE SELECT 0" + $result = sqlcmd -S "(localdb)\MSSQLLocalDB" -Q $query -h -1 -W 2>$null + return ($result -eq '1') + } + + function Reset-DatabaseState { + param ( + [string]$Name, + [string]$Database, + [string]$Context, + [string]$Connection, + [string]$InfraProjAbs, + [string]$WebApiProjAbs + ) + + if ($DropDatabase -and $DropTables) { + throw "Choose either -DropDatabase or -DropTables, not both." + } + + $useDropDatabase = $DropDatabase + $useDropTables = $DropTables + if (-not $DropDatabase -and -not $DropTables) { + # Preserve existing behavior for this script: drop and recreate database. + $useDropDatabase = $true + } + + if ($useDropTables) { + Install-SqlCmd + if (-not (Test-Path $DropTablesPath)) { + throw "Drop tables script not found at $DropTablesPath" + } + if (Test-DatabaseExists -DatabaseName $Database) { + Write-Host "[STEP] Dropping all tables for $Name via $DropTablesPath..." -ForegroundColor Magenta + $dropResult = sqlcmd -S "(localdb)\MSSQLLocalDB" -d $Database -b -i $DropTablesPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Drop tables script failed for ${Name}: $dropResult" + } + Write-Host "[SUCCESS] All tables dropped for $Name." -ForegroundColor Green + } + else { + Write-Host "[WARN] Database '$Database' does not exist. Skipping drop tables step for $Name." -ForegroundColor Yellow + } + } + + if ($useDropDatabase) { + Write-Host "[STEP] Dropping database for $Name..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet ef database drop --project `"$InfraProjAbs`" --startup-project `"$WebApiProjAbs`" --context $Context --connection '$Connection' --force --verbose" + } + } + + # Ensure dotnet-ef tool is available (idempotent). + $srcPath = Join-Path $PSScriptRoot 'src' + $toolsManifest = Join-Path $srcPath 'dotnet-tools.json' + Push-Location $srcPath + try { + if (!(Test-Path -Path $toolsManifest)) { + Write-Host "[STEP] Creating dotnet tool manifest in src/..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet new tool-manifest --force" + } + + $toolList = & dotnet tool list --local | Out-String + if ($toolList -notmatch 'dotnet-ef') { + Write-Host "[STEP] Installing dotnet-ef as a local tool in src/..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet tool install dotnet-ef --local" + } + else { + Write-Host "[STEP] dotnet-ef already installed as a local tool." -ForegroundColor Green + } + + Write-Host "[STEP] Restoring local dotnet tools..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet tool restore" + } + finally { + Pop-Location + } + + foreach ($product in $Products) { + $name = $product.Name + $root = $product.Root + $database = $product.Database + $apiProject = $product.ApiProject + + $infraProj = "$root\Infrastructure.SqlServer\Infrastructure.SqlServer.csproj" + $webApiProj = "$root\$apiProject\$apiProject.csproj" + $context = "${name}Context" + $connection = "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=$database;Min Pool Size=3;MultipleActiveResultSets=True;Trusted_Connection=Yes;TrustServerCertificate=True;Encrypt=True;Connection Timeout=30;" + + Write-Host "[STEP] Product - $name" -ForegroundColor Cyan + Write-Host "[STEP] Context - $context" -ForegroundColor Cyan + Write-Host "[STEP] Infra project - $infraProj" -ForegroundColor Cyan + Write-Host "[STEP] WebApi project - $webApiProj" -ForegroundColor Cyan + + Invoke-DiagnosticCommand "dotnet clean $infraProj" + Invoke-DiagnosticCommand "dotnet restore $infraProj" + Invoke-DiagnosticCommand "dotnet build $infraProj --no-restore" + Invoke-DiagnosticCommand "dotnet clean $webApiProj" + Invoke-DiagnosticCommand "dotnet restore $webApiProj" + Invoke-DiagnosticCommand "dotnet build $webApiProj --no-restore" + + # Build absolute paths from $PSScriptRoot to avoid CWD-related path resolution issues. + $infraProjAbs = Join-Path $PSScriptRoot ($infraProj -replace '^\.[\\/]', '') + $webApiProjAbs = Join-Path $PSScriptRoot ($webApiProj -replace '^\.[\\/]', '') + + Push-Location $srcPath + try { + Reset-DatabaseState -Name $name -Database $database -Context $context -Connection $connection -InfraProjAbs $infraProjAbs -WebApiProjAbs $webApiProjAbs + + Write-Host "[STEP] Applying existing migrations for $name..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet ef database update --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --connection '$connection' --verbose" + } + finally { + Pop-Location + } + } + + Write-Host "[DONE] Database reset and migration update complete." -ForegroundColor Green +} +finally { + Pop-Location +} diff --git a/Reset-EfNswag.ps1 b/Reset-EfNswag.ps1 index b4daf76..6c50725 100644 --- a/Reset-EfNswag.ps1 +++ b/Reset-EfNswag.ps1 @@ -18,10 +18,12 @@ param ( [array]$Products = @( @{ Name = "AgentFramework"; Root = ".\src"; Database = "AgentFramework"; ApiProject = "Presentation.Api" } ), - [string] $dropTablesPath = ".\data\Admin\Drop Tables.sql" + [switch]$DropDatabase, + [switch]$DropTables, + [string]$dropTablesPath = ".\data\Admin\Drop Tables.sql" ) -function Ensure-SqlCmd { +function Install-SqlCmd { $sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue if (-not $sqlcmd) { Write-Host "sqlcmd not found. Installing via winget..." -ForegroundColor Yellow @@ -42,9 +44,8 @@ function Ensure-SqlCmd { } } -Ensure-SqlCmd +Install-SqlCmd -# STEP 1: Drop all tables using the provided SQL script, but only if the database exists function Test-DatabaseExists { param ( [string]$DatabaseName @@ -54,29 +55,21 @@ function Test-DatabaseExists { return ($result -eq '1') } -if (Test-Path $dropTablesPath) { - if ($Products -and $Products.Count -gt 0) { - $dbName = $Products[0].Database - } - if (Test-DatabaseExists -DatabaseName $dbName) { - Write-Host "[STEP 1] Dropping all tables via $dropTablesPath on database '$dbName'..." -ForegroundColor Magenta - $dropResult = sqlcmd -S "(localdb)\MSSQLLocalDB" -d $dbName -b -i $dropTablesPath 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Drop tables script failed: $dropResult" -ForegroundColor Red - throw "[FAIL-FAST] Database drop failed. Stopping script." - } - Write-Host "[SUCCESS] All tables dropped." -ForegroundColor Green - } else { - Write-Host "[WARN] Database does not exist. Skipping drop tables step." -ForegroundColor Yellow - } -} else { - Write-Host "[WARN] Drop script not found at $dropTablesPath — skipping database reset. Ensure the database is clean before continuing." -ForegroundColor Yellow +if ($DropDatabase -and $DropTables) { + throw "Choose either -DropDatabase or -DropTables, not both." +} + +# Preserve existing behavior for this script: drop tables unless explicitly overridden. +$useDropTables = $DropTables +$useDropDatabase = $DropDatabase +if (-not $DropDatabase -and -not $DropTables) { + $useDropTables = $true } Push-Location try { function Write-Diag($msg) { Write-Host "[DIAG] $msg" -ForegroundColor Yellow } - function Run-Verbose($cmd) { + function Invoke-DiagnosticCommand($cmd) { Write-Host "[DIAG] Running: " + $cmd -ForegroundColor Yellow try { Invoke-Expression $cmd @@ -94,17 +87,17 @@ try { try { if (!(Test-Path -Path $toolsManifest)) { Write-Host "[STEP] Creating dotnet tool manifest in src/..." -ForegroundColor Magenta - Run-Verbose "dotnet new tool-manifest --force" + Invoke-DiagnosticCommand "dotnet new tool-manifest --force" } $toolList = & dotnet tool list --local | Out-String if ($toolList -notmatch 'dotnet-ef') { Write-Host "[STEP] Installing dotnet-ef as a local tool in src/..." -ForegroundColor Magenta - Run-Verbose "dotnet tool install dotnet-ef --local" + Invoke-DiagnosticCommand "dotnet tool install dotnet-ef --local" } else { Write-Host "[STEP] dotnet-ef already installed as a local tool." -ForegroundColor Green } Write-Host "[STEP] Restoring local dotnet tools (dotnet-ef required) in src/..." -ForegroundColor Magenta - Run-Verbose "dotnet tool restore" + Invoke-DiagnosticCommand "dotnet tool restore" } finally { Pop-Location } @@ -128,12 +121,12 @@ try { Write-Host "[STEP] WebApi project - " + $webApiProj -ForegroundColor Cyan # Restore and build before migrations - Run-Verbose "dotnet clean $infraProj" - Run-Verbose "dotnet restore $infraProj" - Run-Verbose "dotnet build $infraProj --no-restore" - Run-Verbose "dotnet clean $webApiProj" - Run-Verbose "dotnet restore $webApiProj" - Run-Verbose "dotnet build $webApiProj --no-restore" + Invoke-DiagnosticCommand "dotnet clean $infraProj" + Invoke-DiagnosticCommand "dotnet restore $infraProj" + Invoke-DiagnosticCommand "dotnet build $infraProj --no-restore" + Invoke-DiagnosticCommand "dotnet clean $webApiProj" + Invoke-DiagnosticCommand "dotnet restore $webApiProj" + Invoke-DiagnosticCommand "dotnet build $webApiProj --no-restore" Write-Host "Removing migration files" Remove-Item $infraPath -ErrorAction SilentlyContinue @@ -142,10 +135,34 @@ try { # resolves against [Environment]::CurrentDirectory, not $PWD, causing wrong paths after Push-Location). $infraProjAbs = Join-Path $PSScriptRoot ($infraProj -replace '^\.[\\/]', '') $webApiProjAbs = Join-Path $PSScriptRoot ($webApiProj -replace '^\.[\\/]', '') + + if ($useDropTables) { + if (Test-Path $dropTablesPath) { + if (Test-DatabaseExists -DatabaseName $database) { + Write-Host "[STEP] Dropping all tables for $name via $dropTablesPath..." -ForegroundColor Magenta + $dropResult = sqlcmd -S "(localdb)\MSSQLLocalDB" -d $database -b -i $dropTablesPath 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Drop tables script failed for ${name}: $dropResult" -ForegroundColor Red + throw "[FAIL-FAST] Table drop failed for $name. Stopping script." + } + Write-Host "[SUCCESS] All tables dropped for $name." -ForegroundColor Green + } + else { + Write-Host "[WARN] Database '$database' does not exist. Skipping drop tables step for $name." -ForegroundColor Yellow + } + } + else { + throw "Drop script not found at $dropTablesPath" + } + } Push-Location $srcPath try { - Run-Verbose "dotnet ef migrations add InitialCreate-$context --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --verbose" - Run-Verbose "dotnet ef database update --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --connection '$connection' --verbose" + if ($useDropDatabase) { + Write-Host "[STEP] Dropping database for $name..." -ForegroundColor Magenta + Invoke-DiagnosticCommand "dotnet ef database drop --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --connection '$connection' --force --verbose" + } + Invoke-DiagnosticCommand "dotnet ef migrations add InitialCreate-$context --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --verbose" + Invoke-DiagnosticCommand "dotnet ef database update --project `"$infraProjAbs`" --startup-project `"$webApiProjAbs`" --context $context --connection '$connection' --verbose" } finally { Pop-Location } @@ -154,7 +171,7 @@ try { if (Test-Path $nswagScript) { Push-Location "$root\$apiProject" try { - Run-Verbose ".\Generate-NswagClientCode.ps1 -SkipBuildRestore" + Invoke-DiagnosticCommand ".\Generate-NswagClientCode.ps1 -SkipBuildRestore" } finally { Pop-Location } diff --git a/src/Presentation.Api/ConfigureServices.cs b/src/Presentation.Api/ConfigureServices.cs index 3cee019..8faacf5 100644 --- a/src/Presentation.Api/ConfigureServices.cs +++ b/src/Presentation.Api/ConfigureServices.cs @@ -113,9 +113,9 @@ private static OpenApiInfo CreateVersionInfo(ApiVersionDescription description) { var info = new OpenApiInfo { - Title = $"GoodToCode AgentFramework ({Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")})", + Title = $"GoodToCode Application API ({Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")})", Version = description.ApiVersion.ToString(), - Description = $"An API to interact with the AgentFramework", + Description = "An API to interact with this application", Contact = new OpenApiContact { Email = "developers@goodtocode.com", diff --git a/src/Presentation.Api/appsettings.local.json b/src/Presentation.Api/appsettings.local.json index eeaf27c..3623acc 100644 --- a/src/Presentation.Api/appsettings.local.json +++ b/src/Presentation.Api/appsettings.local.json @@ -34,7 +34,7 @@ }, "AzureOpenAI": { "ChatCompletionModelId": "openai-fast", // aka. gpt-5.4-mini - "Endpoint": "https://your-resource.openai.azure.com", + "Endpoint": "https://your-resource.openai.azure.com/openai/v1", "ApiKey": "" }, "AzureAIFoundry": { diff --git a/src/Presentation.Web/Features/Chat/ChatPage.razor b/src/Presentation.Web/Features/Chat/ChatPage.razor index a1ad029..2743360 100644 --- a/src/Presentation.Web/Features/Chat/ChatPage.razor +++ b/src/Presentation.Web/Features/Chat/ChatPage.razor @@ -15,116 +15,128 @@ Chat Session - - - - - - @if (sessionListOrientation == Orientation.Vertical) - { - - } - else - { - - } - + + @if (isMobileLayout) + { + + - - - What can I help you with? - - - @if (sessionListOrientation == Orientation.Vertical) - { - - } - else - { - - - } - + + + + + + + + What can I help you with? + + + + + + + + + + + + + } + else + { + + + + + + + What can I help you with? + + + + + + + + + + + + + + + + + + + + + + } @code { - private ChatSessionsModel chatSessions = new ChatSessionsModel(); - private ChatSessionList? chatSessionListRef; - private ChatSessionStrip? ChatSessionStripRef; - private Orientation sessionListOrientation = Orientation.Vertical; - private int sessionListVerticalGap; - private int sessionListHorizontalGap; - private bool wrapSessions = false; - private string chatContentStyle = "max-width:50vw;"; - - protected override async Task OnInitializedAsync() - { - var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - if (authState.User.Identity?.IsAuthenticated == true) - { - await UserSyncService.SyncUserAsync(authState.User); - } - - chatSessions = new ChatSessionsModel(); - chatSessions.AddRange(await chatService.GetChatSessionsAsync()); - StateHasChanged(); - } + private ChatSessionsModel chatSessions = new ChatSessionsModel(); + private ChatSessionList? chatSessionListRef; + private ChatSessionStrip? chatSessionStripRef; + private bool isMobileLayout; - private void HandleNewSessionPressed() + protected override async Task OnInitializedAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity?.IsAuthenticated == true) { - chatSessionListRef?.ClearSelection(); - ChatSessionStripRef?.ClearSelection(); - chatSessions.ClearActive(); - StateHasChanged(); + await UserSyncService.SyncUserAsync(authState.User); } - private void HandleSessionCreated(ChatSessionModel chatSession) - { - chatSessions.Add(chatSession); - chatSessions.SetActive(chatSession); - StateHasChanged(); - } + chatSessions = new ChatSessionsModel(); + chatSessions.AddRange(await chatService.GetChatSessionsAsync()); + StateHasChanged(); + } - private void HandleSessionSelected(ChatSessionModel chatSession) - { - chatSessions.ClearActive(); - chatSessions.SetActive(chatSession); - StateHasChanged(); - } + private void HandleNewSessionPressed() + { + chatSessionListRef?.ClearSelection(); + chatSessionStripRef?.ClearSelection(); + chatSessions.ClearActive(); + StateHasChanged(); + } - private async Task HandleMessageSubmitted() - { - chatSessions.RefreshItem(await chatService.GetChatSessionAsync(chatSessions?.ActiveSession?.Id ?? Guid.Empty)); - StateHasChanged(); - } + private void HandleSessionCreated(ChatSessionModel chatSession) + { + chatSessions.Add(chatSession); + chatSessions.SetActive(chatSession); + StateHasChanged(); + } + + private void HandleSessionSelected(ChatSessionModel chatSession) + { + chatSessions.ClearActive(); + chatSessions.SetActive(chatSession); + StateHasChanged(); + } + + private async Task HandleMessageSubmitted() + { + chatSessions.RefreshItem(await chatService.GetChatSessionAsync(chatSessions?.ActiveSession?.Id ?? Guid.Empty)); + StateHasChanged(); + } + + private void OnBreakpointEnterHandler(GridItemSize size) + { + isMobileLayout = size == GridItemSize.Xs || size == GridItemSize.Sm; + StateHasChanged(); + } - private void OnBreakpointEnterHandler(GridItemSize size) - { - wrapSessions = size == GridItemSize.Xs ? true : false; - if (size == GridItemSize.Xs || size == GridItemSize.Sm) - { - sessionListOrientation = Orientation.Horizontal; - sessionListVerticalGap = 0; - sessionListHorizontalGap = 8; - chatContentStyle = ""; - } - else - { - sessionListOrientation = Orientation.Vertical; - sessionListVerticalGap = 8; - sessionListHorizontalGap = 0; - chatContentStyle = "max-width:50vw;"; - } - StateHasChanged(); - } } \ No newline at end of file diff --git a/src/Presentation.Web/Features/Chat/Components/ChatMessageList.razor b/src/Presentation.Web/Features/Chat/Components/ChatMessageList.razor index aa14b52..5dc3a3d 100644 --- a/src/Presentation.Web/Features/Chat/Components/ChatMessageList.razor +++ b/src/Presentation.Web/Features/Chat/Components/ChatMessageList.razor @@ -1,14 +1,13 @@ @using Goodtocode.AgentFramework.Presentation.Web.Features.Chat.Models -@using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @inject IJSRuntime JSRuntime - + @foreach (var message in Messages ?? Enumerable.Empty()) { var isUser = IsUserMessage(message); - + @message.Content @@ -22,6 +21,9 @@ [Parameter] public IEnumerable Messages { get; set; } = new List(); + [Parameter] + public string Width { get; set; } = "100%"; + private bool IsUserMessage(ChatMessageModel message) => message?.Role?.ToLowerInvariant() == "user"; } \ No newline at end of file diff --git a/src/Presentation.Web/Features/Chat/Components/ChatSessionEditForm.razor b/src/Presentation.Web/Features/Chat/Components/ChatSessionEditForm.razor index 556ac23..de10765 100644 --- a/src/Presentation.Web/Features/Chat/Components/ChatSessionEditForm.razor +++ b/src/Presentation.Web/Features/Chat/Components/ChatSessionEditForm.razor @@ -7,7 +7,9 @@ Label="" Required="true" Autofocus="true" /> - + + + diff --git a/src/Presentation.Web/Features/Chat/Components/ChatSessionList.razor b/src/Presentation.Web/Features/Chat/Components/ChatSessionList.razor index c4b348a..5d81c62 100644 --- a/src/Presentation.Web/Features/Chat/Components/ChatSessionList.razor +++ b/src/Presentation.Web/Features/Chat/Components/ChatSessionList.razor @@ -8,7 +8,8 @@ { + SelectedItem="@selectedTreeItem" + Style="width:100%; min-width:30%;"> @{ var sessionItem = (ChatSessionTreeViewItem)item; @@ -25,16 +26,17 @@ } else if (isSelected) { - - + + @sessionItem.Text } else - { + { @sessionItem.Text } diff --git a/src/Presentation.Web/Features/Chat/Components/ChatSessionStrip.razor b/src/Presentation.Web/Features/Chat/Components/ChatSessionStrip.razor index dd96e94..b4e900d 100644 --- a/src/Presentation.Web/Features/Chat/Components/ChatSessionStrip.razor +++ b/src/Presentation.Web/Features/Chat/Components/ChatSessionStrip.razor @@ -5,33 +5,33 @@ @inject IChatService chatService @if (Sessions?.Any() == true) -{ - - @foreach (var session in Sessions) - { - - @if (editingSessionId == session.Id) - { - - } - else - { - - - @session.Title - - @if (selectedSession?.Id == session.Id) - { +{ + + + @foreach (var session in SessionItems) + { + + @if (editingSessionId == session.Id) + { + + } + else + { + + + @session.Title + - } - - } - - } - + + } + + } + + } else { @@ -48,6 +48,8 @@ else private EditContext? editContext; private ChatSessionModel? selectedSession; + private IReadOnlyList SessionItems => Sessions?.ToList() ?? []; + private async Task SelectSession(ChatSessionModel session) { selectedSession = session; diff --git a/src/Presentation.Web/Features/Chat/Components/NewChatMessageInput.razor b/src/Presentation.Web/Features/Chat/Components/NewChatMessageInput.razor index 0a5c923..138cf9b 100644 --- a/src/Presentation.Web/Features/Chat/Components/NewChatMessageInput.razor +++ b/src/Presentation.Web/Features/Chat/Components/NewChatMessageInput.razor @@ -4,7 +4,7 @@ @inject IChatService chatService - + Goodtocode Quick-start for Microsoft Agent Framework +GoodToCode Application - + - Goodtocode Quick-start for Microsoft Agent Framework + GoodToCode Application diff --git a/src/Presentation.Web/Features/Legal/AnalyticsNoticePage.razor b/src/Presentation.Web/Features/Legal/AnalyticsNoticePage.razor index 669edf1..2c57757 100644 --- a/src/Presentation.Web/Features/Legal/AnalyticsNoticePage.razor +++ b/src/Presentation.Web/Features/Legal/AnalyticsNoticePage.razor @@ -9,7 +9,7 @@ Analytics Notice | GoodToCode - + @@ -26,7 +26,7 @@
Provider -

This site uses Microsoft Clarity for analytics and usability insights.

+

This application uses Microsoft Clarity for analytics and usability insights.

diff --git a/src/Presentation.Web/Features/Legal/PrivacyPolicyPage.razor b/src/Presentation.Web/Features/Legal/PrivacyPolicyPage.razor index 3d1b3f1..b6e698d 100644 --- a/src/Presentation.Web/Features/Legal/PrivacyPolicyPage.razor +++ b/src/Presentation.Web/Features/Legal/PrivacyPolicyPage.razor @@ -6,10 +6,10 @@ Privacy Policy | GoodToCode - + - + @@ -23,12 +23,12 @@
What we collect -

We may collect usage, device, and interaction data needed to operate, secure, and improve this website experience.

+

We may collect usage, device, and interaction data needed to operate, secure, and improve this application experience.

How we use data -

Data is used for site operation, performance monitoring, security, and product improvement.

+

Data is used for application operation, performance monitoring, security, and product improvement.

@@ -43,7 +43,7 @@
-

This policy is a template baseline and should be reviewed by legal counsel before production use.

+

This policy is baseline guidance and should be reviewed by legal counsel before production use.

diff --git a/src/Presentation.Web/Features/Legal/TermsOfUsePage.razor b/src/Presentation.Web/Features/Legal/TermsOfUsePage.razor index 3b41db1..9ff9f02 100644 --- a/src/Presentation.Web/Features/Legal/TermsOfUsePage.razor +++ b/src/Presentation.Web/Features/Legal/TermsOfUsePage.razor @@ -6,10 +6,10 @@ Terms of Use | GoodToCode - + - + @@ -22,23 +22,23 @@
- Use of website -

You agree to use this website lawfully and not attempt to disrupt service availability, integrity, or security.

+ Use of application +

You agree to use this application lawfully and not attempt to disrupt service availability, integrity, or security.

Content and ownership -

All template content, branding, and code samples remain subject to applicable ownership and licensing terms.

+

All content, branding, and code samples remain subject to applicable ownership and licensing terms.

No warranty -

This template is provided as-is without warranties of availability, fitness, or uninterrupted operation.

+

This application is provided as-is without warranties of availability, fitness, or uninterrupted operation.

Limitation of liability -

To the maximum extent allowed by law, the website operator is not liable for indirect or consequential damages from site use.

+

To the maximum extent allowed by law, the application operator is not liable for indirect or consequential damages from application use.