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
+8
View File
@@ -0,0 +1,8 @@
node_modules
.next
__pycache__
*.pyc
.git
.env
.env.local
agent/.venv
+49
View File
@@ -0,0 +1,49 @@
# 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*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.mastra/
# python
agent/venv/
__pycache__/
.venv/
+63
View File
@@ -0,0 +1,63 @@
# Stage 1: Build Next.js frontend
FROM node:20-slim AS frontend
WORKDIR /app
COPY package.json ./
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 ./
# 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: Production image with Python + Node
FROM python:3.12-slim AS runner
# Install Node.js 20 + uv
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/*
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
# Install Python dependencies using uv
COPY agent/pyproject.toml agent/uv.lock ./agent/
COPY agent/main.py ./agent/
COPY agent/src/ ./agent/src/
RUN cd agent && uv pip install --system -e .
# 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 agent source code
COPY agent/ ./agent/
# Copy entrypoint
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE 3000
ENV NODE_ENV=production
CMD ["./entrypoint.sh"]
+21
View File
@@ -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.
+118
View File
@@ -0,0 +1,118 @@
# CopilotKit <> Agno Starter
This is a starter template for building AI agents using [Agno](https://agno.com) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated investment analyst agent that can research stocks, analyze market data, and provide investment insights.
## Prerequisites
- Node.js 20+
- Python 3.12+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- OpenAI API Key (for the Agno agent)
- Any of the following package managers:
- npm (default)
- [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:** Installing the package dependencies will also install the agent's python dependencies via the `install:agent` script.
2. Set up your OpenAI API key:
```bash
export OPENAI_API_KEY="your-openai-api-key-here"
```
or create a `.env` file.
```bash
echo "OPENAI_API_KEY=your-openai-api-key-here" > .env
```
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 UI and agent servers 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:ui` - Starts only the Next.js UI server
- `dev:agent` - Starts only the Agno agent server
- `build` - Builds the Next.js application for production
- `start` - Starts the production server
- `install:agent` - Installs Python dependencies for the agent
## 📚 Documentation
The main UI component is in `src/app/page.tsx`. You can:
- Modify the theme colors and styling
- Add new frontend actions
- Customize the CopilotKit sidebar appearance
Otherwise, check out the documentation relevant to your task:
- [Agno Documentation](https://docs.agno.com/introduction) - Learn more about Agno and its features
- [CopilotKit Documentation](https://docs.copilotkit.ai) - Explore CopilotKit's capabilities
- [Next.js Documentation](https://nextjs.org/docs) - Learn about Next.js features and API
- [YFinance Documentation](https://pypi.org/project/yfinance/) - Financial data tools
## 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 Agno agent is running on port 8000
2. Your OpenAI API key is set correctly
3. Both servers started successfully
### Python Dependencies
If you encounter Python import errors:
```bash
cd agent
uv sync
```
+27
View File
@@ -0,0 +1,27 @@
"""
Example for serving you Agno agent as an AG-UI server
"""
from pathlib import Path
import dotenv
from agno.os import AgentOS
from agno.os.interfaces.agui import AGUI
from src.agent import agent
dotenv.load_dotenv(Path(__file__).resolve().parent.parent / ".env")
dotenv.load_dotenv()
# Build AgentOS and extract the app for serving
agent_os = AgentOS(agents=[agent], interfaces=[AGUI(agent=agent)])
app = agent_os.get_app()
@app.get("/health")
async def health():
return {"status": "ok"}
if __name__ == "__main__":
agent_os.serve(app="main:app", port=8000, reload=True)
@@ -0,0 +1,15 @@
[project]
name = "investment-analyst-agent"
version = "0.1.0"
description = "An Agno Agent with Finance tools exposed in an AG-UI compatible way"
requires-python = ">=3.12"
dependencies = [
"agno>=1.7.8",
"openai>=1.88.0",
"yfinance>=0.2.63",
"fastapi>=0.115.13",
"uvicorn>=0.34.3",
"ag-ui-protocol>=0.1.8",
"packaging>=25.0.0",
"python-dotenv>=1.0.0",
]
@@ -0,0 +1,25 @@
"""Example: Agno Agent with Finance tools
This example shows how to create an Agno Agent with tools (YFinanceTools) and expose it in an AG-UI compatible way.
"""
from agno.agent.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.yfinance import YFinanceTools
from .tools.backend import get_weather
from .tools.frontend import add_proverb, set_theme_color
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[
# Example of backend tools, defined and handled in your agno agent
YFinanceTools(),
get_weather,
# Example of frontend tools, handled in the frontend Next.js app
add_proverb,
set_theme_color,
],
description="You are an demonstrative agent for Agno and CopilotKit's integration.",
instructions="Format your response using markdown and use tables to display data where possible.",
)
@@ -0,0 +1,15 @@
from agno.tools import tool
@tool
def get_weather(location: str):
"""
Get the weather for the current location.
Args:
location (str): The location to get the weather for.
Returns:
str: The weather for the current location.
"""
return f"The weather in {location} is: 70 degrees and Sunny."
@@ -0,0 +1,21 @@
from agno.tools import tool
@tool(external_execution=True)
def set_theme_color(theme_color: str):
"""
Change the theme color of the chat.
Args:
background: str: The background color to change to.
"""
@tool(external_execution=True)
def add_proverb(proverb: str):
"""
Add a proverb to the chat.
Args:
proverb: str: The proverb to add to the chat.
"""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
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:
- OPENAI_API_KEY=test-key-for-aimock
- OPENAI_BASE_URL=http://aimock:4010/v1
depends_on:
aimock:
condition: service_started
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import socket; socket.create_connection(('localhost', 8000), 2)",
]
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:-agno}
- 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,17 @@
FROM python:3.12-slim
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
COPY main.py ./
COPY src/ ./src/
RUN uv sync
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,27 @@
FROM node:22-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json ./
RUN npm install --ignore-scripts
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
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 node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('output')){c=c.replace('};',' output: \"standalone\",\n};'); fs.writeFileSync(f,c);}"
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
set -e
# Start Python agent on port 8000 (override PORT which Railway sets to 3000)
cd /app/agent
PORT=8000 python -m uvicorn main:app --host 0.0.0.0 --port 8000 &
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,40 @@
{
"fixtures": [
{
"match": { "userMessage": "Hello" },
"response": {
"content": "Hello! I'm the agno AI assistant. How can I help you?"
}
},
{
"match": { "userMessage": "weather in San Francisco" },
"response": {
"content": "The weather in San Francisco is: 70 degrees and Sunny."
}
},
{
"match": { "userMessage": "theme to green" },
"response": {
"content": "Done! I've changed the theme color to green (#22c55e). The page background and accent colors should now reflect the new theme."
}
},
{
"match": { "userMessage": "price of Apple stock" },
"response": {
"content": "Here is the latest price information for Apple Inc. (AAPL):\n\n| Symbol | Price | Change | Change % |\n|--------|---------|---------|----------|\n| AAPL | $213.49 | +1.24 | +0.58% |\n\n*Note: you are running against aimock — this is a scripted fixture value, not a live quote.*"
}
},
{
"match": { "userMessage": "proverb about AI" },
"response": {
"content": "Added a new proverb: *\"An AI that learns from its mistakes is wiser than a human who repeats them.\"*\n\nYou should see it appear in the Proverbs panel on the left."
}
},
{
"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) Add your OPENAI_API_KEY to .env, (2) Remove or unset OPENAI_BASE_URL from your environment so requests go to OpenAI instead of aimock, (3) Restart with `pnpm dev`."
}
}
]
}
+21
View File
@@ -0,0 +1,21 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typescript: { ignoreBuildErrors: true },
serverExternalPackages: ["pino", "thread-stream"],
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). For a
// standalone/Docker image built without the token and injected at runtime, set
// COPILOTKIT_LICENSE_TOKEN at build time too (or gate the UI at runtime) so the
// baked flag reflects it.
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
+44
View File
@@ -0,0 +1,44 @@
{
"name": "agno-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:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"dev:ui": "next dev",
"build": "next build",
"start": "next start",
"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",
"@hono/node-server": "^1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4",
"lucide-react": "^0.525.0",
"next": "16.0.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"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
REM Navigate to the agent directory
cd /d %~dp0\..\agent
REM Run the agent using uv
uv run python main.py
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Run the agent using uv
uv run python main.py
@@ -0,0 +1,6 @@
@echo off
REM Navigate to the agent directory
cd /d "%~dp0\..\agent" || exit /b 1
REM Install dependencies using uv
uv sync
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Install dependencies using uv
uv sync
@@ -0,0 +1,44 @@
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").replace(/\/$/, "") +
"/agui",
}),
},
// --- 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",
}),
// Demo stub — replace with your real auth-derived user identity before any
// multi-user deployment, or all users share one thread history.
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,76 @@
@import "tailwindcss";
/*
* Threads panel theme — match to agno's CopilotKit chat (CopilotSidebar).
*
* The CopilotKit v2 chat renders LIGHT regardless of OS dark mode (neutral
* zinc/gray surfaces on white, ~1rem bubble radius, system-ui sans). To read as
* native to that chat, the SDK <CopilotThreadsDrawer> is held light by token
* inheritance: we drop agno's old `prefers-color-scheme: dark` override of
* --background/--foreground so the panel never goes dark while the chat beside
* it stays light. agno's own demo content sets all of its colors locally
* (text-white, bg-white/20, the indigo themeColor backdrop), so it is
* unaffected by this.
*
* Tokens are defined at :root (not a wrapper) so the drawer's delete-confirm
* dialog — which renders through a React portal to <body>, outside the panel —
* still resolves the same surfaces. The SDK drawer is driven entirely by these
* tokens; the indigo accent (#6366f1) is agno's own app accent, tying the panel
* to the surrounding product; the neutral zinc surfaces/borders/muted text
* mirror the chat's light chrome.
*/
:root {
/* Neutral surfaces — match the chat's white/zinc light palette */
--background: #ffffff;
--foreground: #18181b; /* zinc-900 */
--card: #ffffff;
--card-foreground: #18181b;
/* Agno's indigo accent for the primary new-thread button + active thread */
--primary: #6366f1;
--primary-foreground: #ffffff;
/* Tints / hovers — zinc scale, matching the chat's neutral chrome */
--secondary: #f4f4f5; /* zinc-100 */
--secondary-foreground: #18181b;
--muted: #f4f4f5;
--muted-foreground: #71717a; /* zinc-500 */
--accent: #eef2ff; /* indigo-50 — selected thread row tint */
--accent-foreground: #18181b;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e4e4e7; /* zinc-200 */
--input: #e4e4e7;
--ring: #a5b4fc; /* indigo-300 — focus ring keyed to the accent */
/* Match the chat's rounded-2xl (1rem) corner language */
--radius: 1rem;
/* Match the chat's system-ui sans stack */
--font-body:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
/* The CopilotKit v2 chat (CopilotSidebar) stays LIGHT regardless of the OS color
scheme, so the threads layout is held light too — otherwise the SDK drawer
would clash with the sidebar it sits beside. 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: "Agno + CopilotKit Starter",
description: "A starter demo connecting an Agno agent to CopilotKit.",
};
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;
}
}
+218
View File
@@ -0,0 +1,218 @@
"use client";
import {
useAgent,
useConfigureSuggestions,
useDefaultRenderTool,
useFrontendTool,
useRenderTool,
CopilotSidebar,
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
} from "@copilotkit/react-core/v2";
import React, { useEffect, useState } from "react";
import { z } from "zod";
import { DefaultToolComponent } from "@/components/default-tool-ui";
import { WeatherCard } from "@/components/weather";
import styles from "./page.module.css";
// agno registers a single agent under the key "default" (see
// src/app/api/copilotkit/[[...slug]]/route.ts), so the threads drawer + chat
// config provider must address that same agent id.
const AGENT_ID = "default";
export default function CopilotKitPage() {
const [themeColor, setThemeColor] = useState("#6366f1");
// 🪁 Frontend Actions: https://docs.copilotkit.ai/guides/frontend-actions
useFrontendTool({
name: "set_theme_color",
parameters: z.object({
theme_color: z
.string()
.describe("The theme color to set. Make sure to pick nice colors."),
}),
handler: async ({ theme_color }) => {
setThemeColor(theme_color);
return `Changing theme color to ${theme_color}`;
},
});
// 🪁 Suggestions: https://docs.copilotkit.ai/guides/suggestions
useConfigureSuggestions({
available: "always",
suggestions: [
{
title: "Generative UI",
message: "What's the weather in San Francisco?",
},
{
title: "Frontend Tools",
message: "Set the theme to green.",
},
{
title: "Default Tool Rendering",
message: "What's the latest price of Apple stock?",
},
{
title: "Writing Agent State",
message: "Add a proverb about AI.",
},
],
});
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 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={AGENT_ID}>
<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={AGENT_ID} />
<div className={styles.mainPanel}>
<main
style={
{
"--copilot-kit-primary-color": themeColor,
} as React.CSSProperties
}
>
<YourMainContent themeColor={themeColor} />
<CopilotSidebar
defaultOpen={true}
// Adds an initial message to the chat
labels={{
modalHeaderTitle: "Popup Assistant",
welcomeMessageText:
"👋 Hi, there! You're chatting with an Agno agent.",
}}
/>
{/* CopilotSidebar self-docks; main content renders as a sibling. */}
</main>
</div>
</div>
</CopilotChatConfigurationProvider>
);
}
// State of the agent, make sure this aligns with your agent's state.
type AgentState = {
proverbs: string[];
};
function YourMainContent({ themeColor }: { themeColor: string }) {
// 🪁 Shared State: https://docs.copilotkit.ai/coagents/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]);
// 🪁 Frontend Actions: https://docs.copilotkit.ai/agno/frontend-tools
useFrontendTool({
name: "add_proverb",
parameters: z.object({
proverb: z
.string()
.describe("The proverb to add. Make it witty, short and concise."),
}),
handler: async ({ proverb }) => {
// Read agent.state at call time so rapid successive adds don't drop
// earlier proverbs via a stale closure over `state`.
agent.setState({
proverbs: [
...((agent.state as AgentState | undefined)?.proverbs ?? []),
proverb,
],
});
return `Added proverb: ${proverb}`;
},
});
//🪁 Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
useRenderTool(
{
name: "get_weather",
parameters: z.object({
location: z.string(),
}),
render: ({ parameters }) => (
<WeatherCard themeColor={themeColor} location={parameters.location} />
),
},
[themeColor],
);
//🪁 Default Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
useDefaultRenderTool(
{
render: (props) => (
<DefaultToolComponent themeColor={themeColor} {...props} />
),
},
[themeColor],
);
return (
<div
style={{ backgroundColor: themeColor }}
className="h-screen flex justify-center items-center flex-col transition-colors duration-300"
>
<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">
{state.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: state.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>
{state.proverbs?.length === 0 && (
<p className="text-center text-white/80 italic my-8">
No proverbs yet. Ask the assistant to add some!
</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,166 @@
import { useState } from "react";
export type BackendToolsProps = {
name: string;
parameters: unknown;
status: "inProgress" | "executing" | "complete";
result: string | undefined;
themeColor: string;
};
export function DefaultToolComponent({
name,
parameters,
status,
result,
themeColor,
}: BackendToolsProps) {
const [showArgs, setShowArgs] = useState(false);
const [showResult, setShowResult] = useState(false);
const getStatusColor = () => {
switch (status) {
case "executing":
case "inProgress":
return "bg-blue-500/20 text-blue-300 border-blue-400/30";
case "complete":
return "bg-green-500/20 text-green-300 border-green-400/30";
default:
return "bg-white/20 text-white/70 border-white/30";
}
};
const getStatusIcon = () => {
switch (status) {
case "executing":
case "inProgress":
return (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
case "complete":
return (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
);
default:
return null;
}
};
return (
<div
style={{ backgroundColor: themeColor }}
className="backdrop-blur-sm rounded-lg p-4 mt-4 mb-4 max-w-md w-full border border-white/20"
>
{/* Header with tool name and status */}
<div className="flex items-center justify-between mb-3">
<h3 className="text-white font-medium text-sm">🔧 {name}</h3>
<span
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${getStatusColor()}`}
>
{getStatusIcon()}
{status}
</span>
</div>
{/* Arguments */}
{!!parameters &&
typeof parameters === "object" &&
Object.keys(parameters as Record<string, unknown>).length > 0 && (
<div className="mb-3">
<button
onClick={() => setShowArgs(!showArgs)}
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
>
<svg
className={`w-3 h-3 transition-transform ${showArgs ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
Arguments
</button>
{showArgs && (
<div className="bg-black/20 rounded p-2 space-y-1">
{Object.entries(parameters as Record<string, unknown>).map(
([key, value]) => (
<div key={key} className="flex gap-2">
<span className="text-white/50 text-xs">{key}:</span>
<span className="text-white/80 text-xs font-mono">
{JSON.stringify(value)}
</span>
</div>
),
)}
</div>
)}
</div>
)}
{/* Result */}
{result && (
<div>
<button
onClick={() => setShowResult(!showResult)}
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
>
<svg
className={`w-3 h-3 transition-transform ${showResult ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
Result
</button>
{showResult && (
<div className="bg-black/20 rounded p-2">
<pre className="text-white/80 text-xs font-mono whitespace-pre-wrap break-words">
{typeof result === "string"
? result
: JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</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,64 @@
interface WeatherCardProps {
location?: string;
themeColor: string;
}
function SunIcon() {
return (
<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>
);
}
export function WeatherCard({ location, themeColor }: WeatherCardProps) {
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">
<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,7 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+33
View File
@@ -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"]
}