Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 157f37a5a7 |
@@ -0,0 +1,85 @@
|
||||
# AG-UI Single Agent Demo
|
||||
|
||||
The simplest possible AG-UI integration: a **single chat agent** with **no tools** and **no context providers**,
|
||||
served over the AG-UI protocol and consumed by a small React client.
|
||||
|
||||
Use this sample as the starting point for AG-UI. For a richer, multi-agent example with tool-approval checkpoints
|
||||
and human-in-the-loop resumes, see [`../ag_ui_workflow_handoff`](../ag_ui_workflow_handoff/README.md).
|
||||
|
||||
## Folder Layout
|
||||
|
||||
- `backend/server.py` - FastAPI + AG-UI endpoint wrapping a single `Agent`
|
||||
- `frontend/` - Vite + React AG-UI client UI
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
- npm 9+
|
||||
- Azure AI project + model deployment configured in environment variables:
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`
|
||||
- `FOUNDRY_MODEL`
|
||||
|
||||
## 1) Run Backend
|
||||
|
||||
From the Python repo root:
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv sync
|
||||
uv run python samples/05-end-to-end/ag_ui_single_agent/backend/server.py
|
||||
```
|
||||
|
||||
Backend default URL:
|
||||
|
||||
- `http://127.0.0.1:8892`
|
||||
- AG-UI endpoint: `POST http://127.0.0.1:8892/agent`
|
||||
|
||||
## 2) Install Frontend Packages (npm)
|
||||
|
||||
From the `python/` directory (where Step 1 left you):
|
||||
|
||||
```bash
|
||||
cd samples/05-end-to-end/ag_ui_single_agent/frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
## 3) Run Frontend Locally
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend default URL:
|
||||
|
||||
- `http://127.0.0.1:5173`
|
||||
|
||||
If you changed backend host/port, run with:
|
||||
|
||||
```bash
|
||||
VITE_BACKEND_URL=http://127.0.0.1:8892 npm run dev
|
||||
```
|
||||
|
||||
## 4) Demo Flow to Verify
|
||||
|
||||
1. Click one of the starter prompts (or type your own message).
|
||||
2. Watch the assistant response stream in token by token.
|
||||
3. Send a follow-up that depends on the previous turn (for example: "summarize what you just told me").
|
||||
The client only sends the newest message plus the `thread_id`; the server replays the stored history.
|
||||
4. Click **New Thread** to start a fresh conversation (a new `thread_id`).
|
||||
|
||||
## Conversation History
|
||||
|
||||
The client only ever sends the **newest message** plus a `thread_id`. The backend retains history **server-side**,
|
||||
keyed by that `thread_id`, using an `InMemoryAGUIThreadSnapshotStore`. Because an AG-UI thread id is not an
|
||||
authorization boundary, a `snapshot_scope_resolver` is required whenever a snapshot store is configured; this
|
||||
single-tenant demo maps every request to one shared `"demo"` scope.
|
||||
|
||||
The in-memory store is process-local and not durable. Swap in your own `AGUIThreadSnapshotStore` implementation
|
||||
(and a real scope resolver) for production.
|
||||
|
||||
## What This Validates
|
||||
|
||||
- `add_agent_framework_fastapi_endpoint(...)` with a plain `Agent` (no `AgentFrameworkWorkflow` wrapper)
|
||||
- Streaming assistant text via `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` AG-UI events
|
||||
- Server-side conversation history keyed by `thread_id` via a snapshot store
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""AG-UI single-agent demo backend.
|
||||
|
||||
This is the simplest possible AG-UI integration: a single chat agent with no
|
||||
tools and no context providers, exposed over the AG-UI protocol.
|
||||
|
||||
Run this server and pair it with the frontend in `../frontend`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from agent_framework import Agent
|
||||
from agent_framework.ag_ui import (
|
||||
InMemoryAGUIThreadSnapshotStore,
|
||||
add_agent_framework_fastapi_endpoint,
|
||||
)
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_agent() -> Agent:
|
||||
"""Create a single chat agent with no tools and no context providers."""
|
||||
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
return Agent(
|
||||
id="assistant",
|
||||
name="assistant",
|
||||
instructions="You are a helpful, concise assistant. Answer the user's questions directly.",
|
||||
client=client,
|
||||
)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Create and configure the FastAPI application."""
|
||||
|
||||
app = FastAPI(title="AG-UI Single Agent Demo")
|
||||
|
||||
cors_origins = [
|
||||
origin.strip() for origin in os.getenv("CORS_ORIGINS", "http://127.0.0.1:5173").split(",") if origin.strip()
|
||||
]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
add_agent_framework_fastapi_endpoint(
|
||||
app=app,
|
||||
agent=create_agent(),
|
||||
path="/agent",
|
||||
# Persist conversation history server-side, keyed by thread_id, so the
|
||||
# client only ever sends the newest message plus its thread_id.
|
||||
snapshot_store=InMemoryAGUIThreadSnapshotStore(),
|
||||
# AG-UI thread ids are not an authorization boundary, so a scope is required
|
||||
# when a snapshot store is configured. This demo is single-tenant, so every
|
||||
# request maps to one shared scope.
|
||||
snapshot_scope_resolver=lambda _request: "demo",
|
||||
)
|
||||
|
||||
@app.get("/healthz")
|
||||
async def healthz() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the AG-UI single-agent demo backend."""
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
port = int(os.getenv("PORT", "8892"))
|
||||
|
||||
print(f"AG-UI single-agent demo backend running at http://{host}:{port}")
|
||||
print("AG-UI endpoint: POST /agent")
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# build artifacts
|
||||
*.tsbuildinfo
|
||||
vite.config.js
|
||||
vite.config.d.ts
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<!-- Copyright (c) Microsoft. All rights reserved. -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AG-UI Single Agent Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1031
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "ag-ui-single-agent-demo-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type AgUiEvent = Record<string, unknown> & { type: string };
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
role: "assistant" | "user" | "system";
|
||||
text: string;
|
||||
}
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ?? "http://127.0.0.1:8892";
|
||||
const ENDPOINT = `${BACKEND_URL}/agent`;
|
||||
|
||||
const STARTER_PROMPTS = [
|
||||
"Explain the AG-UI protocol in two sentences.",
|
||||
"Give me three tips for writing clear commit messages.",
|
||||
];
|
||||
|
||||
function randomId(): string {
|
||||
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `id-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function safeParseJson(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [statusText, setStatusText] = useState("Ready");
|
||||
|
||||
const threadIdRef = useRef<string>(randomId());
|
||||
const streamingMessageIdRef = useRef<string | null>(null);
|
||||
const transcriptRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const canSend = useMemo(() => draft.trim().length > 0 && !isRunning, [draft, isRunning]);
|
||||
|
||||
useEffect(() => {
|
||||
const node = transcriptRef.current;
|
||||
if (node) {
|
||||
node.scrollTop = node.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const pushMessage = (message: ChatMessage): void => {
|
||||
setMessages((prev) => [...prev, message]);
|
||||
};
|
||||
|
||||
const appendToStreamingMessage = (messageId: string, delta: string): void => {
|
||||
setMessages((prev) => {
|
||||
const existing = prev.find((message) => message.id === messageId);
|
||||
if (existing) {
|
||||
return prev.map((message) =>
|
||||
message.id === messageId ? { ...message, text: `${message.text}${delta}` } : message,
|
||||
);
|
||||
}
|
||||
return [...prev, { id: messageId, role: "assistant", text: delta }];
|
||||
});
|
||||
};
|
||||
|
||||
const handleEvent = (event: AgUiEvent): void => {
|
||||
switch (event.type) {
|
||||
case "RUN_STARTED":
|
||||
setStatusText("Thinking");
|
||||
break;
|
||||
case "TEXT_MESSAGE_START": {
|
||||
const messageId = typeof event.message_id === "string" ? event.message_id : randomId();
|
||||
streamingMessageIdRef.current = messageId;
|
||||
break;
|
||||
}
|
||||
case "TEXT_MESSAGE_CONTENT": {
|
||||
const messageId =
|
||||
typeof event.message_id === "string" ? event.message_id : streamingMessageIdRef.current ?? randomId();
|
||||
const delta = typeof event.delta === "string" ? event.delta : "";
|
||||
if (delta.length > 0) {
|
||||
setStatusText("Responding");
|
||||
appendToStreamingMessage(messageId, delta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "TEXT_MESSAGE_END":
|
||||
streamingMessageIdRef.current = null;
|
||||
break;
|
||||
case "RUN_FINISHED":
|
||||
setStatusText("Ready");
|
||||
setIsRunning(false);
|
||||
break;
|
||||
case "RUN_ERROR": {
|
||||
const errorText = typeof event.message === "string" ? event.message : "The run failed.";
|
||||
pushMessage({ id: randomId(), role: "system", text: `Error: ${errorText}` });
|
||||
setStatusText("Error");
|
||||
setIsRunning(false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const streamRun = async (body: Record<string, unknown>): Promise<void> => {
|
||||
const response = await fetch(ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`Request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
const processSseChunk = (rawChunk: string): void => {
|
||||
const dataLines = rawChunk
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).trim());
|
||||
|
||||
if (dataLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = safeParseJson(dataLines.join("\n"));
|
||||
if (isObject(parsed) && typeof parsed.type === "string") {
|
||||
handleEvent(parsed as AgUiEvent);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
while (true) {
|
||||
const boundaryIndex = buffer.indexOf("\n\n");
|
||||
if (boundaryIndex < 0) {
|
||||
break;
|
||||
}
|
||||
const rawEvent = buffer.slice(0, boundaryIndex);
|
||||
buffer = buffer.slice(boundaryIndex + 2);
|
||||
processSseChunk(rawEvent);
|
||||
}
|
||||
}
|
||||
|
||||
const tail = buffer.trim();
|
||||
if (tail.length > 0) {
|
||||
processSseChunk(tail);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (text: string): Promise<void> => {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length === 0 || isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushMessage({ id: randomId(), role: "user", text: trimmed });
|
||||
setDraft("");
|
||||
setIsRunning(true);
|
||||
setStatusText("Connecting");
|
||||
streamingMessageIdRef.current = null;
|
||||
|
||||
try {
|
||||
await streamRun({
|
||||
thread_id: threadIdRef.current,
|
||||
run_id: randomId(),
|
||||
messages: [{ role: "user", content: trimmed }],
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
pushMessage({ id: randomId(), role: "system", text: `Network error: ${message}` });
|
||||
setStatusText("Network error");
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
void sendMessage(draft);
|
||||
};
|
||||
|
||||
const startNewThread = (): void => {
|
||||
threadIdRef.current = randomId();
|
||||
streamingMessageIdRef.current = null;
|
||||
setMessages([]);
|
||||
setDraft("");
|
||||
setStatusText("Ready");
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-shell">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Agent Framework · AG-UI</p>
|
||||
<h1>Single Agent Chat</h1>
|
||||
<p className="subtitle">
|
||||
The simplest AG-UI integration: one chat agent with no tools and no context providers, streamed to a React
|
||||
client over Server-Sent Events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="status-pill" data-running={isRunning}>
|
||||
<span>Status</span>
|
||||
<strong>{statusText}</strong>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="card chat-card">
|
||||
<div className="chat-toolbar">
|
||||
<h2>Conversation</h2>
|
||||
<button type="button" className="ghost-button" onClick={startNewThread} disabled={isRunning}>
|
||||
New Thread
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="transcript" ref={transcriptRef}>
|
||||
{messages.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>Start the conversation with a prompt:</p>
|
||||
<div className="starter-prompts">
|
||||
{STARTER_PROMPTS.map((prompt) => (
|
||||
<button
|
||||
key={prompt}
|
||||
type="button"
|
||||
className="starter-prompt"
|
||||
onClick={() => void sendMessage(prompt)}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div key={message.id} className={`bubble bubble-${message.role}`}>
|
||||
<span className="bubble-role">{message.role}</span>
|
||||
<p>{message.text}</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form className="composer" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={draft}
|
||||
placeholder="Send a message..."
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<button type="submit" className="send-button" disabled={!canSend}>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,259 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
:root {
|
||||
--page-bg: #edf4f8;
|
||||
--panel-bg: #fdfdfd;
|
||||
--ink: #132534;
|
||||
--muted: #607487;
|
||||
--line: #c6d6e2;
|
||||
--teal: #1f9d8b;
|
||||
--teal-dark: #11756a;
|
||||
--shadow: 0 20px 45px rgb(15 35 51 / 14%);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans", "Avenir Next", "Helvetica Neue", sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at 12% 8%, rgb(31 157 139 / 20%) 0%, transparent 28%),
|
||||
radial-gradient(circle at 88% 18%, rgb(255 154 60 / 20%) 0%, transparent 30%),
|
||||
linear-gradient(150deg, #eff6fa 0%, #dceaf3 46%, #e7f1f6 100%);
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
min-height: 100vh;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
animation: fade-in 320ms ease-out;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 0.72rem;
|
||||
color: var(--teal-dark);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 6px 0 8px;
|
||||
font-size: clamp(1.6rem, 2.8vw, 2.4rem);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
max-width: 60ch;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 150px;
|
||||
box-shadow: 0 8px 20px rgb(19 37 52 / 8%);
|
||||
}
|
||||
|
||||
.status-pill span {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-pill strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-pill[data-running="true"] {
|
||||
border-color: var(--teal);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.chat-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chat-toolbar h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--teal-dark);
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.transcript {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 6px 2px;
|
||||
max-height: 52vh;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.starter-prompts {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.starter-prompt {
|
||||
text-align: left;
|
||||
border: 1px dashed var(--line);
|
||||
background: #f6fafc;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.starter-prompt:hover:not(:disabled) {
|
||||
border-color: var(--teal);
|
||||
}
|
||||
|
||||
.starter-prompt:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
max-width: 82%;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bubble p {
|
||||
margin: 4px 0 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.bubble-role {
|
||||
font-size: 0.68rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
align-self: flex-end;
|
||||
background: var(--teal);
|
||||
border-color: var(--teal-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bubble-user .bubble-role {
|
||||
color: rgb(255 255 255 / 80%);
|
||||
}
|
||||
|
||||
.bubble-assistant {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.bubble-system {
|
||||
align-self: center;
|
||||
background: #fff4e6;
|
||||
border-color: #ffcf99;
|
||||
color: #8a5200;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.composer input {
|
||||
flex: 1;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.composer input:focus {
|
||||
outline: none;
|
||||
border-color: var(--teal);
|
||||
}
|
||||
|
||||
.send-button {
|
||||
border: none;
|
||||
background: var(--teal);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 12px 22px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 5173,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ag_ui_single_agent",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user