chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,7 @@
node_modules
.next
.git
.env
.env.local
agent/bin
agent/obj
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,59 @@
# Stage 1: Build Next.js frontend
FROM node:20-slim AS frontend
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN npm install --ignore-scripts
# Fix CVE: Railway scans package.json, so we update it too
RUN npm install next@latest && \
node -e "const p=require('./package.json'); p.dependencies.next='^16'; require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))"
COPY src/ ./src/
COPY public/ ./public/
COPY next.config.ts tsconfig.json postcss.config.mjs next-env.d.ts ./
# Patch next.config.ts to ignore TS errors during build (can't modify original source)
RUN node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('ignoreBuildErrors')){c=c.replace('};',' typescript: { ignoreBuildErrors: true },\n};'); fs.writeFileSync(f,c);}"
RUN npm run build
# Stage 2: Build .NET agent
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS dotnet-build
WORKDIR /agent
COPY agent/ ./
RUN dotnet publish -c Release -o /agent/out
# Stage 3: Production image
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runner
# Install Node.js 20
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy Next.js build output and production node_modules
COPY --from=frontend /app/.next ./.next
COPY --from=frontend /app/node_modules ./node_modules
COPY --from=frontend /app/package.json ./
COPY --from=frontend /app/public ./public
# Copy .NET agent build output
COPY --from=dotnet-build /agent/out ./agent/
# Copy entrypoint
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE 3000
ENV NODE_ENV=production
ENV ASPNETCORE_ENVIRONMENT=Production
CMD ["./entrypoint.sh"]
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) Atai Barkai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,256 @@
# CopilotKit <> Microsoft Agent Framework Starter
This is a starter template for building AI agents using [Microsoft Agent Framework](https://github.com/microsoft/agents) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated proverbs management agent that demonstrates AG-UI protocol features including shared state, generative UI, and human-in-the-loop workflows.
## Prerequisites
- **GitHub Personal Access Token** (for GitHub Models API)
- Retrieve from GitHub using [these instructions](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic).
- or generate via `gh auth token` in your CLI (requires [GitHub CLI](https://github.com/cli/cli?tab=readme-ov-file#installation))
- **.NET 9.0 SDK**
- [Download directly](https://dotnet.microsoft.com/download/dotnet/9.0)
- macOS/Linux
- [Install via Homebrew](https://formulae.brew.sh/formula/dotnet) (`brew install dotnet@9`) or
- <details><summary>Install via <code>curl</code> install script</summary><br />
```bash
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 9.0
export PATH="$HOME/.dotnet:$PATH"
```
</details>
- Windows
- [Install via WinGet](https://winstall.app/apps/Microsoft.DotNet.SDK.9) (`winget install --id=Microsoft.DotNet.SDK.9 -e`)
- **Node.js 20+**
- [Download directly](https://nodejs.org/en/download)
- macOS/Linux
- [Install via Homebrew](https://formulae.brew.sh/formula/node@24) (`brew install node@24`) or
- <details><summary>Install via <code>curl</code> install script</summary><br />
```bash
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 24
```
</details>
- Windows
- [Install via WinGet](https://winstall.app/apps/OpenJS.NodeJS) (`winget install --id=OpenJS.NodeJS -v "24.11.0" -e`)
- Any of the following package managers:
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) **(default)** (usually installed with Node.js)
- [pnpm](https://pnpm.io/installation)
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
- [bun](https://bun.sh/)
## Getting Started
1. Install dependencies using your preferred package manager:
```bash
# Using npm (default)
npm install
# Using pnpm
pnpm install
# Using yarn
yarn install
# Using bun
bun install
```
> **Note:** This will automatically setup the C# agent as well (restore NuGet packages).
>
> If you have manual issues, you can run:
>
> ```sh
> npm run install:agent
> ```
2. Set up your GitHub token for GitHub Models:
First, get your GitHub token:
```bash
gh auth token
```
Then, navigate to the agent directory and set it as a user secret:
```bash
cd agent
dotnet user-secrets set GitHubToken "<your-token>"
cd ..
```
Or set it in one command:
```bash
cd agent; dotnet user-secrets set GitHubToken "$(gh auth token)"; cd ..
```
3. Start the development server:
```bash
# Using npm (default)
npm run dev
# Using pnpm
pnpm dev
# Using yarn
yarn dev
# Using bun
bun run dev
```
This will start both the Next.js UI (port 3000) and C# agent server (port 8000) concurrently.
## Available Scripts
The following scripts can also be run using your preferred package manager:
- `dev` - Starts both UI and agent servers in development mode
- `dev:debug` - Starts development servers with debug logging enabled
- `dev:ui` - Starts only the Next.js UI server
- `dev:agent` - Starts only the C# agent server
- `build` - Builds the Next.js application for production
- `start` - Starts the production server
- `lint` - Runs ESLint for code linting
- `install:agent` - Restores NuGet packages for the C# agent
## Project Structure
```
├── agent/ # C# Agent (Microsoft Agent Framework)
│ ├── Program.cs # Main agent implementation with tools
│ ├── ProverbsAgent.csproj # .NET project file
│ └── Properties/ # Configuration (launch settings)
├── src/
│ ├── app/
│ │ ├── page.tsx # Main UI with CopilotKit sidebar
│ │ ├── layout.tsx # CopilotKit provider setup
│ │ └── api/
│ │ └── copilotkit/
│ │ └── route.ts # AG-UI integration endpoint
│ ├── components/ # UI components (weather, proverbs, moon)
│ └── lib/ # Types and utilities
└── scripts/ # Helper scripts for agent setup/run
```
## Features Demonstrated
This starter showcases key AG-UI protocol features:
- **🔄 Shared State**: Proverbs list synchronized between frontend and agent
- **🎨 Generative UI**: Weather card rendered from backend tool
- **👤 Human-in-the-Loop**: Moon card with approval workflow
- **🛠️ Frontend Actions**: Theme color changes from agent
- **💬 Agentic Chat**: Natural language interface with tool calling
## 📚 Documentation
- [Microsoft Agent Framework](https://github.com/microsoft/agents) - Learn about Microsoft's agent framework
- [AG-UI Protocol](https://github.com/copilotkit/ag-ui) - AG-UI protocol specification
- [CopilotKit Documentation](https://docs.copilotkit.ai) - CopilotKit features and API
- [Next.js Documentation](https://nextjs.org/docs) - Next.js features and API
- [GitHub Models](https://github.com/marketplace/models) - Free AI models via GitHub
## Contributing
Feel free to submit issues and enhancement requests! This starter is designed to be easily extensible.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Troubleshooting
### Agent Connection Issues
If you see "I'm having trouble connecting to my tools", make sure:
1. The C# agent is running on port 8000
2. Your GitHub token is set correctly via user secrets
3. Both servers started successfully (check terminal output)
### .NET SDK Not Installed
If you don't have .NET 9.0 installed:
**macOS/Linux (Homebrew):**
```bash
brew install dotnet@9
dotnet --version
```
**macOS/Linux (Install Script):**
```bash
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 9.0
export PATH="$HOME/.dotnet:$PATH"
```
**Windows (WinGet):**
```powershell
winget install --id=Microsoft.DotNet.SDK.9 -e
```
**Windows/macOS (Direct Download):**
- Visit https://dotnet.microsoft.com/download/dotnet/9.0
- Download and run the installer
### .NET SDK Issues
If you encounter .NET-related errors:
```bash
# Verify .NET SDK is installed
dotnet --version # Should be 9.0.x or higher
# Restore packages manually
cd agent
dotnet restore
dotnet run
```
### GitHub Token Issues
If the agent fails to start with "GitHubToken not found":
```bash
cd agent
dotnet user-secrets set GitHubToken "$(gh auth token)"
```
Or manually:
```bash
# Get your token
gh auth token
# Set it as a user secret
cd agent
dotnet user-secrets set GitHubToken "YOUR_TOKEN_HERE"
```
### Port Conflicts
If port 8000 is already in use, you can change it in:
- `agent/Properties/launchSettings.json` - Update `applicationUrl`
- `src/app/api/copilotkit/route.ts` - Update the HttpAgent URL
@@ -0,0 +1,21 @@
# Build results
bin/
obj/
# User-specific files
*.user
*.userosscache
*.suo
# Visual Studio
.vs/
# Rider
.idea/
# VS Code
.vscode/
# NuGet
*.nupkg
*.snupkg
@@ -0,0 +1,163 @@
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using OpenAI;
using System.ComponentModel;
using System.Text.Json.Serialization;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(ProverbsAgentSerializerContext.Default));
builder.Services.AddAGUI();
WebApplication app = builder.Build();
// Create the agent factory and map the AG-UI agent endpoint
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>();
var agentFactory = new ProverbsAgentFactory(builder.Configuration, loggerFactory, jsonOptions.Value.SerializerOptions);
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.MapAGUI("/", agentFactory.CreateProverbsAgent());
await app.RunAsync();
// =================
// State Management
// =================
public class ProverbsState
{
public List<string> Proverbs { get; set; } = [];
}
// =================
// Agent Factory
// =================
public class ProverbsAgentFactory
{
private readonly IConfiguration _configuration;
private readonly ProverbsState _state;
private readonly OpenAIClient _openAiClient;
private readonly ILogger _logger;
private readonly System.Text.Json.JsonSerializerOptions _jsonSerializerOptions;
public ProverbsAgentFactory(IConfiguration configuration, ILoggerFactory loggerFactory, System.Text.Json.JsonSerializerOptions jsonSerializerOptions)
{
_configuration = configuration;
_state = new();
_logger = loggerFactory.CreateLogger<ProverbsAgentFactory>();
_jsonSerializerOptions = jsonSerializerOptions;
// Get the GitHub token from configuration
var githubToken = _configuration["GitHubToken"]
?? throw new InvalidOperationException(
"GitHubToken not found in configuration. " +
"Please set it using: dotnet user-secrets set GitHubToken \"<your-token>\" " +
"or get it using: gh auth token");
_openAiClient = new(
new System.ClientModel.ApiKeyCredential(githubToken),
new OpenAIClientOptions
{
Endpoint = new Uri(Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "https://models.inference.ai.azure.com")
});
}
public AIAgent CreateProverbsAgent()
{
var chatClient = _openAiClient.GetChatClient("gpt-4o-mini").AsIChatClient();
var chatClientAgent = new ChatClientAgent(
chatClient,
name: "ProverbsAgent",
description: @"A helpful assistant that helps manage and discuss proverbs.
You have tools available to add, set, or retrieve proverbs from the list.
When discussing proverbs, ALWAYS use the get_proverbs tool to see the current list before mentioning, updating, or discussing proverbs with the user.",
tools: [
AIFunctionFactory.Create(GetProverbs, options: new() { Name = "get_proverbs", SerializerOptions = _jsonSerializerOptions }),
AIFunctionFactory.Create(AddProverbs, options: new() { Name = "add_proverbs", SerializerOptions = _jsonSerializerOptions }),
AIFunctionFactory.Create(SetProverbs, options: new() { Name = "set_proverbs", SerializerOptions = _jsonSerializerOptions }),
AIFunctionFactory.Create(GetWeather, options: new() { Name = "get_weather", SerializerOptions = _jsonSerializerOptions })
]);
return new SharedStateAgent(chatClientAgent, _jsonSerializerOptions);
}
// =================
// Tools
// =================
[Description("Get the current list of proverbs.")]
private List<string> GetProverbs()
{
_logger.LogInformation("📖 Getting proverbs: {Proverbs}", string.Join(", ", _state.Proverbs));
return _state.Proverbs;
}
[Description("Add new proverbs to the list.")]
private void AddProverbs([Description("The proverbs to add")] List<string> proverbs)
{
_logger.LogInformation(" Adding proverbs: {Proverbs}", string.Join(", ", proverbs));
_state.Proverbs.AddRange(proverbs);
}
[Description("Replace the entire list of proverbs.")]
private void SetProverbs([Description("The new list of proverbs")] List<string> proverbs)
{
_logger.LogInformation("📝 Setting proverbs: {Proverbs}", string.Join(", ", proverbs));
_state.Proverbs = [.. proverbs];
}
[Description("Get the weather for a given location. Ensure location is fully spelled out.")]
private WeatherInfo GetWeather([Description("The location to get the weather for")] string location)
{
_logger.LogInformation("🌤️ Getting weather for: {Location}", location);
return new()
{
Temperature = 20,
Conditions = "sunny",
Humidity = 50,
WindSpeed = 10,
FeelsLike = 25
};
}
}
// =================
// Data Models
// =================
public class ProverbsStateSnapshot
{
[JsonPropertyName("proverbs")]
public List<string> Proverbs { get; set; } = [];
}
public class WeatherInfo
{
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("conditions")]
public string Conditions { get; init; } = string.Empty;
[JsonPropertyName("humidity")]
public int Humidity { get; init; }
[JsonPropertyName("wind_speed")]
public int WindSpeed { get; init; }
[JsonPropertyName("feelsLike")]
public int FeelsLike { get; init; }
}
public partial class Program { }
// =================
// Serializer Context
// =================
[JsonSerializable(typeof(ProverbsStateSnapshot))]
[JsonSerializable(typeof(WeatherInfo))]
internal sealed partial class ProverbsAgentSerializerContext : JsonSerializerContext;
@@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:8000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>proverbs-agent-12345678-1234-1234-1234-123456789abc</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AGUI.AspNetCore" Version="1.0.0-preview.251110.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
<PackageReference Include="OpenAI" Version="2.6.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,102 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ProverbsAgentFactory")]
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
_jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}
var firstRunOptions = new ChatClientAgentRunOptions
{
ChatOptions = chatRunOptions.ChatOptions.Clone(),
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
ContinuationToken = chatRunOptions.ContinuationToken,
ChatClientFactory = chatRunOptions.ChatClientFactory,
};
// Configure JSON schema response format for structured state output
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<ProverbsStateSnapshot>(
schemaName: "ProverbsStateSnapshot",
schemaDescription: "A response containing the current list of proverbs");
ChatMessage stateUpdateMessage = new(
ChatRole.System,
[
new TextContent("Here is the current state in JSON format:"),
new TextContent(state.GetRawText()),
new TextContent("The new state is:")
]);
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentRunResponseUpdate>();
await foreach (var update in InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
// Yield all non-text updates (tool calls, etc.)
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
if (hasNonTextContent)
{
yield return update;
}
}
var response = allUpdates.ToAgentRunResponse();
if (response.TryDeserialize(_jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
_jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentRunResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
else
{
yield break;
}
var secondRunMessages = messages.Concat(response.Messages).Append(
new ChatMessage(
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,77 @@
# Docker Compose stack for e2e smoke testing with aimock.
# Mirrors the user experience: Next.js frontend + .NET agent + aimock as LLM.
# The .NET agent uses GitHubToken + models.inference.ai.azure.com endpoint,
# but for testing we redirect to aimock via OPENAI_BASE_URL override.
# Usage: docker compose -f docker-compose.test.yml up -d
services:
aimock:
image: ghcr.io/copilotkit/aimock:latest
volumes:
- ./fixtures:/fixtures:ro
command:
["--fixtures", "/fixtures", "--host", "0.0.0.0", "--validate-on-load"]
agent:
build:
context: ./agent
dockerfile: ../docker/Dockerfile.agent
environment:
- GitHubToken=test-key-for-aimock
- OPENAI_BASE_URL=http://aimock:4010/v1
depends_on:
aimock:
condition: service_started
healthcheck:
test:
[
"CMD-SHELL",
"curl -sf http://localhost:8000/ || test $(curl -so /dev/null -w '%{http_code}' http://localhost:8000/) -ge 200",
]
interval: 5s
timeout: 5s
retries: 30
start_period: 30s
app:
build:
context: .
dockerfile: docker/Dockerfile.app
environment:
- AGENT_URL=http://agent:8000
depends_on:
agent:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"node",
"-e",
"const http = require('http'); const req = http.get('http://localhost:3000/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.setTimeout(2000, () => { req.destroy(); process.exit(1); });",
]
interval: 5s
timeout: 10s
retries: 30
start_period: 60s
tests:
image: mcr.microsoft.com/playwright:v1.52.0-noble
working_dir: /tests
volumes:
- ../../../showcase/tests:/tests
- test-results:/tests/test-results
environment:
- STARTER=${STARTER:-ms-agent-framework-dotnet}
- STARTER_URL=http://app:3000
depends_on:
app:
condition: service_healthy
command:
[
"bash",
"-c",
"cd /tests && npm install --no-audit --no-fund 2>/dev/null && npx playwright install chromium && npx playwright test starter-smoke --reporter=list",
]
volumes:
test-results:
@@ -0,0 +1,25 @@
# Dockerfile for the MS Agent Framework .NET agent.
# Two-stage build: SDK for build, ASP.NET runtime for production.
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
# Patch hardcoded endpoint to read from OPENAI_BASE_URL env var
RUN sed -i 's|Endpoint = new Uri("https://models.inference.ai.azure.com")|Endpoint = new Uri(Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "https://models.inference.ai.azure.com")|' Program.cs
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app .
EXPOSE 8000
ENV ASPNETCORE_URLS="http://0.0.0.0:8000"
CMD ["./ProverbsAgent"]
@@ -0,0 +1,45 @@
# Dockerfile for the MS Agent Framework Next.js frontend.
# Builds standalone Next.js output from the root-level frontend.
FROM node:20-slim AS builder
WORKDIR /app
# Copy package files and install
COPY package.json pnpm-lock.yaml* ./
RUN npm install --ignore-scripts
# Copy source code
COPY src/ ./src/
COPY public/ ./public/
COPY next.config.ts tsconfig.json postcss.config.mjs ./
COPY next-env.d.ts ./
# Add standalone output mode for Docker
RUN node -e "\
const fs = require('fs'); \
const f = 'next.config.ts'; \
let c = fs.readFileSync(f, 'utf8'); \
if (!c.includes('standalone')) { \
c = c.replace('serverExternalPackages:', \"output: 'standalone',\\n serverExternalPackages:\"); \
fs.writeFileSync(f, c); \
}"
# Build
RUN npm run build
# Production stage
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
@@ -0,0 +1,17 @@
#!/bin/bash
set -e
# Start .NET agent on port 8000
cd /app/agent
ASPNETCORE_URLS="http://0.0.0.0:8000" ./ProverbsAgent &
AGENT_PID=$!
cd /app
# Start Next.js frontend
PORT=${PORT:-3000} npx next start --port ${PORT:-3000} &
NEXT_PID=$!
wait -n $AGENT_PID $NEXT_PID
EXIT_CODE=$?
kill $AGENT_PID $NEXT_PID 2>/dev/null || true
exit $EXIT_CODE
@@ -0,0 +1,70 @@
{
"fixtures": [
{
"match": { "userMessage": "Hello" },
"response": {
"content": "Hello! I'm the Proverbs Agent. I can help you manage a collection of proverbs, discuss their meanings, and even check the weather. What would you like to do?"
}
},
{
"match": { "userMessage": "proverbs" },
"response": {
"toolCalls": [
{
"name": "get_proverbs",
"arguments": "{}",
"id": "call_get_proverbs_001"
}
]
}
},
{
"match": { "toolCallId": "call_get_proverbs_001" },
"response": {
"content": "Your proverb collection is currently empty. Would you like me to add some proverbs? I can suggest some classic ones, or you can tell me your favorites!"
}
},
{
"match": { "userMessage": "add proverb" },
"response": {
"toolCalls": [
{
"name": "add_proverbs",
"arguments": "{\"proverbs\":[\"A stitch in time saves nine\",\"The early bird catches the worm\"]}",
"id": "call_add_proverbs_001"
}
]
}
},
{
"match": { "toolCallId": "call_add_proverbs_001" },
"response": {
"content": "I've added two proverbs to your collection:\n\n1. **A stitch in time saves nine**\n2. **The early bird catches the worm**\n\nWould you like to add more or discuss these proverbs?"
}
},
{
"match": { "userMessage": "weather" },
"response": {
"toolCalls": [
{
"name": "get_weather",
"arguments": "{\"location\":\"New York\"}",
"id": "call_get_weather_001"
}
]
}
},
{
"match": { "toolCallId": "call_get_weather_001" },
"response": {
"content": "The weather in New York is currently 20°C (68°F), sunny with 50% humidity, wind at 10 mph, and feels like 25°C (77°F). A lovely day!"
}
},
{
"match": {},
"response": {
"content": "You're currently running against aimock (a mock LLM server). This response is a catch-all for requests that don't match any test fixture. To use a real LLM: (1) Set your GitHubToken, (2) Remove or unset OPENAI_BASE_URL from your environment, (3) Restart the agent."
}
}
]
}
@@ -0,0 +1,18 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typescript: { ignoreBuildErrors: true },
serverExternalPackages: ["@copilotkit/runtime"],
env: {
// The public Threads UI flag is DERIVED from the server-side license token.
// Set COPILOTKIT_LICENSE_TOKEN (only) to enable Threads — do not set this flag
// directly. NOTE: NEXT_PUBLIC_* resolves at BUILD time while the runtime reads
// the token per-request, so the UI gate and runtime agree only when the token is
// present at build time (the standard `next dev` / host-build flow).
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
? "true"
: "false",
},
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
{
"name": "microsoft-agent-framework-starter",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
"dev:debug": "LOG_LEVEL=debug npm run dev",
"dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"dev:ui": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat",
"postinstall": "npm run install:agent"
},
"dependencies": {
"@ag-ui/client": "0.0.57",
"@copilotkit/react-core": "1.62.3",
"@copilotkit/runtime": "1.62.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4",
"lucide-react": "^0.525.0",
"next": "16.0.8",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"tailwind-merge": "^3.5.0",
"zod": "^3.24.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"tailwindcss": "^4",
"typescript": "^5"
},
"overrides": {
"@ag-ui/client": "0.0.57",
"@ag-ui/core": "0.0.57",
"@ag-ui/encoder": "0.0.57",
"@ag-ui/proto": "0.0.57"
}
}
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1,6 @@
@echo off
cd /d "%~dp0\..\agent"
echo 🚀 Starting C# Proverbs Agent on http://localhost:8000...
echo.
dotnet run --launch-profile http
@@ -0,0 +1,9 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Run the C# agent
echo "🚀 Starting C# Proverbs Agent on http://localhost:8000..."
echo ""
dotnet run --launch-profile http
@@ -0,0 +1,19 @@
@echo off
cd /d "%~dp0\..\agent"
REM Check if .NET is installed
where dotnet >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo ❌ .NET SDK not found. Install from: https://dotnet.microsoft.com/download
exit /b 1
)
REM Restore dependencies quietly
echo 🔧 Setting up C# agent...
dotnet restore --verbosity quiet >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo ✅ Agent setup complete
) else (
echo ⚠️ Setup completed with warnings (agent should still work)
)
@@ -0,0 +1,20 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Check if .NET is installed
if ! command -v dotnet &> /dev/null; then
echo "❌ .NET SDK not found. Install from: https://dotnet.microsoft.com/download"
exit 1
fi
# Restore dependencies quietly
echo "🔧 Setting up C# agent..."
dotnet restore --verbosity quiet > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✅ Agent setup complete"
else
echo "⚠️ Setup completed with warnings (agent should still work)"
fi
@@ -0,0 +1,40 @@
import {
CopilotRuntime,
CopilotKitIntelligence,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { handle } from "hono/vercel";
const runtime = new CopilotRuntime({
agents: {
default: new HttpAgent({
url: process.env.AGENT_URL || "http://localhost:8000/",
}),
},
// --- copilotkit:intelligence (remove this block to opt out) ---
...(process.env.COPILOTKIT_LICENSE_TOKEN
? {
intelligence: new CopilotKitIntelligence({
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
wsUrl:
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
}),
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
}
: { runner: new InMemoryAgentRunner() }),
// --- /copilotkit:intelligence ---
});
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,74 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
/* Threads-panel theme-map — values pulled verbatim from CopilotKit's V2
design system (the same one this example's CopilotSidebar renders with;
see @copilotkit/react-core/src/v2/styles/globals.css). The drawer is a
left-side companion to that LIGHT sidebar, so it inherits the sidebar's
exact neutral-gray palette, charcoal "primary" (NOT a brand accent — the
sidebar's primary buttons are near-black), and 0.625rem radius scale.
This keeps the threads panel reading as one product with the chat. */
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
/* Match the V2 sidebar's radius scale exactly (--radius: 0.625rem there). */
--radius: 0.625rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Drawer typography follows this example's app font stack (Arial — this
example does not load a custom webfont; see layout.tsx + body below). */
--font-body: Arial, Helvetica, sans-serif;
--font-code: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* This example's CopilotSidebar (CopilotKit's V2 chat) stays LIGHT regardless of
the OS color scheme, so the threads panel is held light too — otherwise it
would clash with the sidebar it sits beside. The panel reads --foreground for
its titles/body text, which the dark-mode block above flips. The demo content
(CopilotSidebar + ProverbsCard) uses its own colors, not these tokens, so
re-pinning a stable light foreground/background on the threads layout wrapper
keeps the panel readable without affecting anything else. The confirm dialog
renders in a portal on <body>, so re-pin there as well. Values mirror the V2
light theme (oklch(0.145 0 0) / oklch(1 0 0)). */
.threadsLayout,
body > [role="presentation"] {
--foreground: oklch(0.145 0 0);
--background: oklch(1 0 0);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
body,
html {
height: 100%;
}
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { CopilotKit } from "@copilotkit/react-core/v2";
import "./globals.css";
import "@copilotkit/react-core/v2/styles.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
{/*
suppressHydrationWarning: browser extensions (e.g. Grammarly) inject
attributes like data-gr-ext-installed onto <body> before React hydrates,
which would otherwise surface as a hydration mismatch on first load.
This only relaxes the check for <body>'s own attributes (one level deep);
everything rendered inside <body> is still fully hydration-checked.
*/}
<body className={"antialiased"} suppressHydrationWarning>
{/* Force REST transport so runtime-info + threads both hit the multi-route endpoint (auto-detect races the lazily-compiled API route in next dev). */}
<CopilotKit runtimeUrl="/api/copilotkit" useSingleEndpoint={false}>
{children}
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,50 @@
.layout {
display: grid;
/*
Reserve the desktop drawer's width (its default 320px) as a fixed first
column so the layout doesn't shift when the client-only <CopilotThreadsDrawer>
mounts after hydration. On mobile the drawer is an off-canvas overlay (out
of flow), so the column collapses and the content fills the width.
*/
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
/* Drawer sets --cpk-drawer-reserved-width to 0 on desktop-collapse, so the
reserved column collapses and the chat reclaims the space. */
transition: grid-template-columns 0.2s ease;
/*
Bound the single grid row to the viewport (minmax(0,1fr) lets it shrink
below content) so a long thread list scrolls INTERNALLY in the drawer with
the header pinned, instead of the drawer growing past the viewport and the
page scrolling the header away.
*/
grid-template-rows: minmax(0, 1fr);
height: 100dvh;
width: 100%;
overflow: hidden;
}
.mainPanel {
/*
Pin the content to the SECOND track explicitly. The client-only drawer
renders nothing during SSR, so without this the content would flow into the
reserved first column at first paint and then jump once the drawer mounts.
*/
grid-column: 2;
min-width: 0;
height: 100dvh;
overflow: hidden;
}
/*
Mobile (≤768px): the drawer is an off-canvas overlay — collapse to a single
track. MUST come after the base rules (media queries add no specificity, so a
later same-specificity base rule would otherwise win and leak the two-column
desktop layout onto mobile).
*/
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.mainPanel {
grid-column: auto;
}
}
@@ -0,0 +1,134 @@
"use client";
import { ProverbsCard } from "@/components/proverbs";
import { WeatherCard } from "@/components/weather";
import { MoonCard } from "@/components/moon";
import {
useAgent,
useFrontendTool,
useHumanInTheLoop,
CopilotSidebar,
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
} from "@copilotkit/react-core/v2";
import { useEffect, useState } from "react";
import { z } from "zod";
import type { AgentState } from "@/lib/types";
import styles from "./page.module.css";
export default function CopilotKitPage() {
const [themeColor, setThemeColor] = useState("#6366f1");
// 🪁 Frontend Actions: https://docs.copilotkit.ai/microsoft-agent-framework/frontend-actions
useFrontendTool({
name: "setThemeColor",
description: "Set the theme color of the application",
parameters: z.object({
themeColor: z
.string()
.describe("The theme color to set. Make sure to pick nice colors."),
}),
handler: async ({ themeColor }) => {
setThemeColor(themeColor);
return `Changing background to ${themeColor}`;
},
});
return (
/*
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop) owns
the active thread for the whole surface. The SDK <CopilotThreadsDrawer> drives it
directly — selecting a row sets the active thread, "+ New" resets to a
fresh thread — with no host thread-state. The proverbs/weather/moon content
and the CopilotSidebar read the same active thread from the provider (the
content's `useAgent()` falls back to it). A *controlled* provider would
block "+ New" from resetting, so uncontrolled-inside-provider is required.
`.threadsLayout` (globals.css) pins the light theme vars the drawer +
sidebar inherit; the SDK drawer follows them by token inheritance.
*/
<CopilotChatConfigurationProvider agentId="default">
<div className={`${styles.layout} threadsLayout`}>
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
<CopilotThreadsDrawer agentId="default" />
<div className={styles.mainPanel}>
<main
style={
{
"--copilot-kit-primary-color": themeColor,
} as React.CSSProperties
}
>
<YourMainContent themeColor={themeColor} />
<CopilotSidebar
defaultOpen={true}
labels={{
modalHeaderTitle: "Popup Assistant",
welcomeMessageText:
"👋 Hi, there! You're chatting with an agent.",
}}
/>
</main>
</div>
</div>
</CopilotChatConfigurationProvider>
);
}
function YourMainContent({ themeColor }: { themeColor: string }) {
// 🪁 Shared State: https://docs.copilotkit.ai/microsoft-agent-framework/shared-state
// V2: useAgent returns the agent; read agent.state and write via agent.setState.
const { agent } = useAgent({ agentId: "default" });
const state = (agent.state as AgentState | undefined) ?? { proverbs: [] };
const setState = (next: AgentState) => agent.setState(next);
// Seed an initial proverb once (the V2 agent starts with empty state).
useEffect(() => {
if ((agent.state as AgentState | undefined)?.proverbs === undefined) {
agent.setState({
proverbs: [
"CopilotKit may be new, but it's the best thing since sliced bread.",
],
});
}
}, [agent]);
//🪁 Generative UI: https://docs.copilotkit.ai/microsoft-agent-framework/generative-ui
useFrontendTool(
{
name: "get_weather",
description: "Get the weather for a given location.",
available: false,
parameters: z.object({
location: z.string(),
}),
render: ({ args }) => {
return <WeatherCard location={args.location} themeColor={themeColor} />;
},
},
[themeColor],
);
// 🪁 Human In the Loop: https://docs.copilotkit.ai/microsoft-agent-framework/human-in-the-loop/frontend-tool-based
useHumanInTheLoop(
{
name: "go_to_moon",
description: "Go to the moon on request.",
render: ({ respond, status }) => {
return (
<MoonCard themeColor={themeColor} status={status} respond={respond} />
);
},
},
[themeColor],
);
return (
<div
style={{ backgroundColor: themeColor }}
className="h-screen flex justify-center items-center flex-col transition-colors duration-300"
>
<ProverbsCard state={state} setState={setState} />
</div>
);
}
@@ -0,0 +1,84 @@
import { useState } from "react";
export interface MoonCardProps {
themeColor: string;
status: "inProgress" | "executing" | "complete";
respond?: (response: string) => void;
}
export function MoonCard({ themeColor, status, respond }: MoonCardProps) {
const [decision, setDecision] = useState<"launched" | "aborted" | null>(null);
const handleLaunch = () => {
setDecision("launched");
respond?.("You have permission to go to the moon.");
};
const handleAbort = () => {
setDecision("aborted");
respond?.(
"You do not have permission to go to the moon. The user you're talking to rejected the request.",
);
};
return (
<div
style={{ backgroundColor: themeColor }}
className="rounded-2xl shadow-xl max-w-md w-full mt-6"
>
<div className="bg-white/20 backdrop-blur-md p-8 w-full rounded-2xl">
{/* Show decision or prompt */}
{decision === "launched" ? (
<div className="text-center">
<div className="text-7xl mb-4">🌕</div>
<h2 className="text-2xl font-bold text-white mb-2">
Mission Launched
</h2>
<p className="text-white/90">We made it to the moon!</p>
</div>
) : decision === "aborted" ? (
<div className="text-center">
<div className="text-7xl mb-4"></div>
<h2 className="text-2xl font-bold text-white mb-2">
Mission Aborted
</h2>
<p className="text-white/90">Staying on Earth 🌍</p>
</div>
) : (
<>
<div className="text-center mb-6">
<div className="text-7xl mb-4">🚀</div>
<h2 className="text-2xl font-bold text-white mb-2">
Ready for Launch?
</h2>
<p className="text-white/90">Mission to the Moon 🌕</p>
</div>
{/* Launch Buttons */}
{status === "executing" && (
<div className="flex gap-3">
<button
onClick={handleLaunch}
className="flex-1 px-6 py-4 rounded-xl bg-white text-black font-bold
shadow-lg hover:shadow-xl transition-all
hover:scale-105 active:scale-95"
>
🚀 Launch!
</button>
<button
onClick={handleAbort}
className="flex-1 px-6 py-4 rounded-xl bg-black/20 text-white font-bold
border-2 border-white/30 shadow-lg
transition-all hover:scale-105 active:scale-95
hover:bg-black/30"
>
Abort
</button>
</div>
)}
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,49 @@
import { AgentState } from "@/lib/types";
export interface ProverbsCardProps {
state: AgentState;
setState: (state: AgentState) => void;
}
export function ProverbsCard({ state, setState }: ProverbsCardProps) {
// `state` is undefined until the agent syncs (V2 useAgent), so guard it.
const proverbs = state?.proverbs ?? [];
return (
<div className="bg-white/20 backdrop-blur-md p-8 rounded-2xl shadow-xl max-w-2xl w-full">
<h1 className="text-4xl font-bold text-white mb-2 text-center">
Proverbs
</h1>
<p className="text-gray-200 text-center italic mb-6">
This is a demonstrative page, but it could be anything you want! 🪁
</p>
<hr className="border-white/20 my-6" />
<div className="flex flex-col gap-3">
{proverbs.map((proverb, index) => (
<div
key={index}
className="bg-white/15 p-4 rounded-xl text-white relative group hover:bg-white/20 transition-all"
>
<p className="pr-8">{proverb}</p>
<button
onClick={() =>
setState({
...state,
proverbs: proverbs.filter((_, i) => i !== index),
})
}
className="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity
bg-red-500 hover:bg-red-600 text-white rounded-full h-6 w-6 flex items-center justify-center"
>
</button>
</div>
))}
</div>
{proverbs.length === 0 && (
<p className="text-center text-white/80 italic my-8">
No proverbs yet. Ask the assistant to add some!
</p>
)}
</div>
);
}
@@ -0,0 +1,52 @@
import * as React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
{
variants: {
variant: {
default:
"bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90",
secondary:
"bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-80",
outline:
"border border-[var(--border)] bg-[var(--background)] hover:bg-[var(--secondary)]",
ghost:
"hover:bg-[var(--secondary)] hover:text-[var(--secondary-foreground)]",
destructive:
"bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:opacity-90",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
),
);
Button.displayName = "Button";
export { Button, buttonVariants };
@@ -0,0 +1,85 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)] shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-[var(--muted-foreground)]", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
};
@@ -0,0 +1,69 @@
// Simple sun icon for the weather card
function SunIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-14 h-14 text-yellow-200"
>
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
strokeWidth="2"
stroke="currentColor"
/>
</svg>
);
}
// Weather card component where the location and themeColor are based on what the agent
// sets via tool calls.
export function WeatherCard({
location,
themeColor,
}: {
location?: string;
themeColor: string;
}) {
return (
<div
style={{ backgroundColor: themeColor }}
className="rounded-xl shadow-xl mt-6 mb-4 max-w-md w-full"
>
<div className="bg-white/20 p-4 w-full">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-bold text-white capitalize">
{location}
</h3>
<p className="text-white">Current Weather</p>
</div>
<SunIcon />
</div>
<div className="mt-4 flex items-end justify-between">
<div className="text-3xl font-bold text-white">70°</div>
<div className="text-sm text-white">Clear skies</div>
</div>
<div className="mt-4 pt-4 border-t border-white">
<div className="grid grid-cols-3 gap-2 text-center">
<div>
<p className="text-white text-xs">Humidity</p>
<p className="text-white font-medium">45%</p>
</div>
<div>
<p className="text-white text-xs">Wind</p>
<p className="text-white font-medium">5 mph</p>
</div>
<div>
<p className="text-white text-xs">Feels Like</p>
<p className="text-white font-medium">72°</p>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,4 @@
// State of the agent, make sure this aligns with your agent's state.
export type AgentState = {
proverbs: string[];
};
@@ -0,0 +1,7 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}