chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
+40
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user