chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,119 @@
# AG-UI Handoff Workflow Demo
This demo is a full custom AG-UI application built on top of the new workflow abstractions in `agent_framework_ag_ui`.
It includes:
- A **backend** FastAPI AG-UI endpoint serving a **HandoffBuilder workflow** with:
- `triage_agent`
- `refund_agent`
- `order_agent`
- Required **tool approval checkpoints**:
- `submit_refund` (`approval_mode="always_require"`)
- `submit_replacement` (`approval_mode="always_require"`)
- A second **request-info resume** step (order agent asks for shipping preference)
- A **frontend** React app that consumes AG-UI SSE events, renders workflow cards, and sends `resume.interrupts` payloads.
The backend uses Azure OpenAI responses and supports intent-driven, non-linear handoff routing.
This demo keeps workflow state per `thread_id`. When the assistant ends a case with `Case complete.`, the UI blocks
later top-level input on that same thread and asks the user to start a new case explicitly instead of resuming a
terminated workflow.
## Folder Layout
- `backend/server.py` - FastAPI + AG-UI endpoint + Handoff workflow
- `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_workflow_handoff/backend/server.py
```
Backend default URL:
- `http://127.0.0.1:8891`
- AG-UI endpoint: `POST http://127.0.0.1:8891/handoff_demo`
## 2) Install Frontend Packages (npm)
From the `python/` directory (where Step 1 left you):
```bash
cd samples/05-end-to-end/ag_ui_workflow_handoff/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:8891 npm run dev
```
## 4) Demo Flow to Verify
1. Click one of the starter prompts (or type a refund request).
2. Refund Agent asks for an order number; reply with a numeric ID (for example: `987654`).
3. If your initial request did not explicitly choose refund vs replacement, the agent asks a clarifying choice question.
4. Wait for the `submit_refund` reviewer interrupt (built from your provided order ID).
5. In the **HITL Reviewer Console** modal, click **Approve Tool Call**.
6. If you asked for replacement, the Order agent asks for shipping preference; reply in the chat input (for example: `expedited`).
7. When replacement is requested, wait for the `submit_replacement` reviewer interrupt and approve/reject it.
8. If you asked for refund-only, the flow should close without replacement/shipping prompts.
9. Confirm the case snapshot updates and workflow completion.
10. After the case closes, another top-level message on the same thread is rejected with a notice.
11. Click **Start New Case** to begin a fresh thread.
## Important: `require_per_service_call_history_persistence`
All agents participating in a handoff workflow **must** be constructed with
`require_per_service_call_history_persistence=True`. The `HandoffBuilder` will
raise a `ValueError` at build time if any participant is missing this flag.
**Why this is required:** Handoff workflows use middleware that short-circuits
tool calls via `MiddlewareTermination` when a handoff tool is invoked. Without
per-service-call history persistence, local history providers would persist tool
results that the service never received, causing call/result mismatches on
subsequent turns.
```python
agent = Agent(
client=client,
name="my_agent",
require_per_service_call_history_persistence=True, # Required for handoff
)
```
## What This Validates
- `add_agent_framework_fastapi_endpoint(...)` with `AgentFrameworkWorkflow(workflow_factory=...)`
- Thread-scoped workflow state across turns
- `RUN_FINISHED.interrupt` pause behavior
- `resume.interrupts` continuation behavior
- JSON resume payload coercion for `Content` and `list[Message]` workflow response types
- Intent-driven routing between triage, refund, and order specialists (no forced linear path)
- Multiple HITL approvals in one case (`submit_refund` + `submit_replacement`)
@@ -0,0 +1,366 @@
# Copyright (c) Microsoft. All rights reserved.
"""AG-UI handoff workflow demo backend.
This demo exposes a dynamic HandoffBuilder workflow through AG-UI.
It intentionally includes two interrupt styles:
1. Tool approval (`function_approval_request`) for `submit_refund` and `submit_replacement`
2. Follow-up human input (`HandoffAgentUserRequest`) when an agent needs user details
Run this server and pair it with the frontend in `../frontend`.
"""
from __future__ import annotations
import logging
import logging.handlers
import os
import random
from collections.abc import AsyncGenerator
from typing import Any
import uvicorn
from agent_framework import (
Agent,
Message,
Workflow,
WorkflowBuilder,
WorkflowContext,
executor,
tool,
)
from agent_framework.ag_ui import AgentFrameworkWorkflow, add_agent_framework_fastapi_endpoint
from agent_framework.orchestrations import HandoffBuilder
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
load_dotenv()
logger = logging.getLogger(__name__)
@tool(approval_mode="always_require")
def submit_refund(refund_description: str, amount: str, order_id: str) -> str:
"""Capture a refund request for manual review before processing."""
return f"refund recorded for order {order_id} (amount: {amount}) with details: {refund_description}"
@tool(approval_mode="always_require")
def submit_replacement(order_id: str, shipping_preference: str, replacement_note: str) -> str:
"""Capture a replacement request for manual review before processing."""
return (
f"replacement recorded for order {order_id} (shipping: {shipping_preference}) with details: {replacement_note}"
)
@tool(approval_mode="never_require")
def lookup_order_details(order_id: str) -> dict[str, str]:
"""Return synthetic order details for a given order ID."""
normalized_order_id = "".join(ch for ch in order_id if ch.isdigit()) or order_id
rng = random.Random(normalized_order_id)
catalog = [
"Wireless Headphones",
"Mechanical Keyboard",
"Gaming Mouse",
"27-inch Monitor",
"USB-C Dock",
"Bluetooth Speaker",
"Laptop Stand",
]
item_name = catalog[rng.randrange(len(catalog))]
amount = f"${rng.randint(39, 349)}.{rng.randint(0, 99):02d}"
purchase_date = f"2025-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}"
return {
"order_id": normalized_order_id,
"item_name": item_name,
"amount": amount,
"currency": "USD",
"purchase_date": purchase_date,
"status": "delivered",
}
def create_agents() -> tuple[Agent, Agent, Agent]:
"""Create triage, refund, and order agents for the handoff workflow."""
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(),
)
triage = Agent(
id="triage_agent",
name="triage_agent",
instructions=(
"You are the customer support triage agent.\n"
"Routing policy:\n"
"1. Route refund-related requests to refund_agent.\n"
"2. Route replacement/shipping requests to order_agent.\n"
"3. Do not force replacement if the user asked for refund only.\n"
"4. If the issue is fully resolved, send a concise wrap-up that ends with exactly: Case complete."
),
client=client,
require_per_service_call_history_persistence=True,
)
refund = Agent(
id="refund_agent",
name="refund_agent",
instructions=(
"You are the refund specialist.\n"
"Workflow policy:\n"
"1. If order_id is missing, ask only for order_id.\n"
"2. Once order_id is available, call lookup_order_details(order_id) to retrieve item and amount.\n"
"3. Do not ask the customer how much they paid unless lookup_order_details fails.\n"
"4. If user intent is ambiguous, ask one clear choice question and wait for the answer:\n"
" refund only, replacement only, or both.\n"
" Do not call submit_refund until this choice is known.\n"
"5. Gather a short refund reason from user context if needed.\n"
"6. If the user wants a refund (refund-only or both),\n"
" call submit_refund with order_id, amount (from lookup), and refund_description.\n"
"7. After approval and successful refund submission:\n"
" - If the user explicitly requested replacement/exchange, handoff to order_agent.\n"
" - If the user asked for refund only, do not hand off for replacement.\n"
" Finalize in this agent and end with exactly: Case complete.\n"
"8. If the user wants replacement only and no refund, handoff to order_agent directly."
),
client=client,
tools=[lookup_order_details, submit_refund],
require_per_service_call_history_persistence=True,
)
order = Agent(
id="order_agent",
name="order_agent",
instructions=(
"You are the order specialist.\n"
"Only handle replacement/exchange/shipping tasks.\n"
"1. If replacement intent is confirmed but shipping preference is missing,\n"
" ask for shipping preference (standard or expedited).\n"
"2. If order_id is missing, ask for order_id.\n"
"3. Once order_id and shipping preference are known,\n"
" call submit_replacement(order_id, shipping_preference, replacement_note).\n"
"4. While the replacement tool call is pending approval, do not claim completion.\n"
"5. If you receive a submit_replacement function result,\n"
" approval has already occurred and submission succeeded.\n"
"6. Immediately send a final customer-facing confirmation and end with exactly: Case complete.\n"
"If the user wants refund only and no replacement, do not ask shipping questions.\n"
"Acknowledge and hand off back to triage_agent for final closure.\n"
"Do not fabricate tool outputs."
),
client=client,
tools=[lookup_order_details, submit_replacement],
require_per_service_call_history_persistence=True,
)
return triage, refund, order
def is_case_complete_text(text: str) -> bool:
"""Return True when a message ends with the explicit demo completion marker."""
return text.strip().lower().endswith("case complete.")
def _termination_condition(conversation: list[Message]) -> bool:
"""Stop when any assistant emits an explicit completion marker."""
for message in reversed(conversation):
if message.role != "assistant":
continue
if is_case_complete_text(message.text or ""):
return True
return False
def create_handoff_workflow() -> Workflow:
"""Build the demo HandoffBuilder workflow."""
triage, refund, order = create_agents()
builder = HandoffBuilder(
name="ag_ui_handoff_workflow_demo",
participants=[triage, refund, order],
termination_condition=_termination_condition,
)
# Explicit handoff topology (instead of default mesh) so routing is enforced in orchestration,
# not only implied by prompt instructions.
(
builder
.add_handoff(
triage,
[refund],
description="Route when the user requests refunds, damaged-item claims, or refund status updates.",
)
.add_handoff(
triage,
[order],
description="Route when the user requests replacement, exchange, shipping preference, or shipment changes.",
)
.add_handoff(
refund,
[order],
description="Route after refund work only if replacement/exchange logistics are explicitly needed.",
)
.add_handoff(
refund,
[triage],
description="Route back for final case closure when refund-only work is complete.",
)
.add_handoff(
order,
[triage],
description="Route back after replacement/shipping tasks are complete for final closure.",
)
.add_handoff(
order,
[refund],
description="Route to refund specialist if the user pivots from replacement to refund processing.",
)
)
return builder.with_start_agent(triage).build()
def create_closed_case_notice_workflow() -> Workflow:
"""Build a tiny workflow that explains why a completed case cannot continue."""
@executor(id="closed_case_notice")
async def closed_case_notice(message: Message | None, ctx: WorkflowContext[None, str]) -> None:
del message
await ctx.yield_output(
"Your case is complete, but you're trying to do something new. Please start a new thread."
)
return WorkflowBuilder(start_executor=closed_case_notice).build()
class DemoHandoffWorkflow(AgentFrameworkWorkflow):
"""Workflow wrapper that blocks new top-level input on completed demo threads."""
def __init__(self) -> None:
super().__init__(
workflow_factory=lambda _thread_id: create_handoff_workflow(),
name="ag_ui_handoff_workflow_demo",
description="Dynamic handoff workflow demo with tool approvals and request_info resumes.",
)
self._completed_threads: set[str] = set()
self._closed_case_notice_runner = AgentFrameworkWorkflow(workflow=create_closed_case_notice_workflow())
async def run(self, input_data: dict[str, Any]) -> AsyncGenerator[Any]:
"""Intercept completed threads and return a helpful notice instead of resuming them."""
thread_id = self._thread_id_from_input(input_data)
has_messages = isinstance(input_data.get("messages"), list) and len(input_data.get("messages", [])) > 0
has_resume = input_data.get("resume") is not None
if thread_id in self._completed_threads and has_messages and not has_resume:
async for event in self._closed_case_notice_runner.run(input_data):
yield event
return
message_text_by_id: dict[str, str] = {}
case_completed_this_run = False
async for event in super().run(input_data):
event_type = getattr(event, "type", None)
if event_type == "TEXT_MESSAGE_START":
message_id = getattr(event, "message_id", None)
if isinstance(message_id, str):
message_text_by_id[message_id] = ""
elif event_type == "TEXT_MESSAGE_CONTENT":
message_id = getattr(event, "message_id", None)
delta = getattr(event, "delta", None)
if isinstance(message_id, str) and isinstance(delta, str):
message_text_by_id[message_id] = f"{message_text_by_id.get(message_id, '')}{delta}"
elif event_type == "TEXT_MESSAGE_END":
message_id = getattr(event, "message_id", None)
if isinstance(message_id, str):
final_text = message_text_by_id.pop(message_id, "")
if is_case_complete_text(final_text):
case_completed_this_run = True
yield event
if case_completed_this_run:
self._completed_threads.add(thread_id)
self.clear_thread_workflow(thread_id)
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
app = FastAPI(title="AG-UI Handoff Workflow 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=["*"],
)
demo_workflow = DemoHandoffWorkflow()
add_agent_framework_fastapi_endpoint(
app=app,
agent=demo_workflow,
path="/handoff_demo",
)
@app.get("/healthz")
async def healthz() -> dict[str, str]: # pyright: ignore[reportUnusedFunction]
return {"status": "ok"}
return app
app = create_app()
def main() -> None:
"""Run the AG-UI demo backend."""
# Configure logging format
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Configure root logger
logging.basicConfig(level=logging.INFO, format=log_format)
# Add file handler for persistent logging
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ag_ui_handoff_demo.log")
try:
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10485760,
backupCount=5, # 10MB max size, keep 5 backups
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(log_format))
# Add file handler to root logger
logging.getLogger().addHandler(file_handler)
print(f"Logging to file: {log_file}")
except Exception as e:
print(f"Warning: Failed to set up file logging: {e}")
host = os.getenv("HOST", "127.0.0.1")
port = int(os.getenv("PORT", "8891"))
print(f"AG-UI handoff demo backend running at http://{host}:{port}")
print("AG-UI endpoint: POST /handoff_demo")
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 Handoff Workflow Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
{
"name": "ag-ui-handoff-workflow-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"
}
}
File diff suppressed because it is too large Load Diff
@@ -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,559 @@
/* Copyright (c) Microsoft. All rights reserved. */
:root {
--page-bg: #edf4f8;
--panel-bg: #fdfdfd;
--ink: #132534;
--muted: #607487;
--line: #c6d6e2;
--teal: #1f9d8b;
--teal-dark: #11756a;
--amber: #ff9a3c;
--salmon: #ef6b57;
--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;
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: 72ch;
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: 180px;
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);
}
.layout {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 20px;
}
.card {
background: var(--panel-bg);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: var(--shadow);
padding: 18px;
}
.dashboard-panel {
display: grid;
gap: 16px;
align-content: start;
}
.card h2 {
margin: 0 0 14px;
font-size: 1.1rem;
}
.snapshot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.snapshot-grid div {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px;
background: linear-gradient(180deg, #fefefe 0%, #f2f7fa 100%);
}
.snapshot-grid span {
display: block;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 6px;
}
.snapshot-grid strong[data-state="approved"] {
color: var(--teal-dark);
}
.snapshot-grid strong[data-state="rejected"] {
color: #aa3228;
}
.diagnostics-body {
display: grid;
gap: 10px;
}
.diagnostics-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.diagnostics-grid div {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px;
background: linear-gradient(180deg, #fefefe 0%, #f2f7fa 100%);
}
.diagnostics-grid span {
display: block;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 6px;
}
.diagnostics-timestamp {
margin: 0;
}
.diagnostics-raw {
border: 1px solid var(--line);
border-radius: 12px;
background: #f5f9fb;
padding: 10px;
}
.diagnostics-raw summary {
cursor: pointer;
font-weight: 700;
}
.diagnostics-raw pre {
margin: 10px 0 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.82rem;
}
.diagnostics-history {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px;
background: #fff;
display: grid;
gap: 8px;
}
.diagnostics-history h3 {
margin: 0;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.diagnostics-history-item {
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 0.88rem;
}
.agent-pills {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.agent-pill {
border: 1px solid var(--line);
border-radius: 999px;
background: #f5fafc;
color: var(--muted);
font-weight: 600;
padding: 8px 12px;
}
.agent-pill[data-seen="true"] {
color: #35506a;
}
.agent-pill[data-active="true"] {
border-color: var(--teal);
color: var(--teal-dark);
background: rgb(31 157 139 / 10%);
}
.interrupt-body {
display: grid;
gap: 12px;
}
.interrupt-body p {
margin: 0;
line-height: 1.45;
}
.approval-details {
border: 1px solid var(--line);
border-radius: 12px;
background: #f5f9fb;
padding: 10px;
width: 100%;
min-width: 0;
overflow: hidden;
}
.approval-details pre {
margin: 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.82rem;
max-width: 100%;
}
.approval-inline {
display: grid;
gap: 10px;
}
.approval-launch {
width: fit-content;
border: 1px solid var(--teal);
border-radius: 10px;
background: rgb(31 157 139 / 12%);
color: var(--teal-dark);
font-weight: 700;
padding: 10px 14px;
cursor: pointer;
}
.approval-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
flex-wrap: wrap;
}
.approval-actions button,
.case-reset,
.starter-prompts button,
.chat-input button {
border: 0;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, opacity 120ms ease;
}
.approval-actions button:disabled,
.case-reset:disabled,
.starter-prompts button:disabled,
.chat-input button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.approval-actions .approve {
background: var(--teal);
color: #fff;
padding: 10px 14px;
}
.approval-actions .defer {
background: #ecf3f8;
border: 1px solid #bdcfdc;
color: #345267;
padding: 10px 14px;
}
.approval-actions .reject {
background: var(--salmon);
color: #fff;
padding: 10px 14px;
}
.approval-modal-backdrop {
position: fixed;
inset: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgb(7 18 29 / 52%);
backdrop-filter: blur(2px);
}
.approval-modal {
width: min(860px, calc(100vw - 40px));
border-radius: 18px;
border: 1px solid #89a7ba;
background: #fdfefe;
box-shadow: 0 28px 60px rgb(5 18 30 / 38%);
display: grid;
gap: 14px;
padding: 18px;
}
.approval-modal-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
}
.approval-modal-header h3 {
margin: 2px 0 0;
font-size: 1.15rem;
}
.approval-modal-label {
margin: 0;
font-size: 0.72rem;
color: var(--teal-dark);
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
}
.approval-modal-close {
border: 1px solid var(--line);
border-radius: 10px;
background: #f4f8fb;
color: #3d5a70;
font-weight: 700;
padding: 8px 12px;
cursor: pointer;
}
.starter-prompts {
display: grid;
gap: 10px;
}
.pending-empty-state {
display: grid;
gap: 10px;
}
.case-reset {
width: fit-content;
border: 1px solid #bdcfdc;
background: #ecf3f8;
color: #345267;
padding: 10px 14px;
}
.starter-prompts button {
text-align: left;
background: linear-gradient(125deg, #fff8ef 0%, #ffe7cf 100%);
border: 1px solid #f0ca97;
padding: 10px 12px;
color: #7b4a12;
}
.chat-panel {
background: #fefefe;
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: var(--shadow);
display: grid;
grid-template-rows: 1fr auto;
min-height: 640px;
}
.chat-scroll {
padding: 16px;
overflow-y: auto;
display: grid;
align-content: start;
grid-auto-rows: max-content;
gap: 12px;
}
.empty-state {
border: 1px dashed var(--line);
border-radius: 12px;
padding: 14px;
color: var(--muted);
}
.chat-bubble {
max-width: 84%;
border-radius: 16px;
padding: 10px 12px;
border: 1px solid #dbe8f1;
background: #fff;
}
.chat-bubble header {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.68rem;
font-weight: 700;
margin-bottom: 6px;
color: var(--muted);
}
.chat-bubble p {
margin: 0;
white-space: pre-wrap;
line-height: 1.45;
}
.chat-bubble[data-role="assistant"] {
justify-self: start;
background: #f4f9fc;
}
.chat-bubble[data-role="user"] {
justify-self: end;
border-color: #94d2c6;
background: #dff5ef;
}
.chat-bubble[data-role="system"] {
justify-self: center;
max-width: 100%;
border-style: dashed;
background: #fef6f2;
}
.chat-input {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
padding: 12px;
border-top: 1px solid var(--line);
background: #f8fbfd;
}
.chat-input input {
border: 1px solid #b7cad8;
border-radius: 10px;
padding: 10px 12px;
font-size: 0.96rem;
color: var(--ink);
background: #fff;
}
.chat-input button {
background: linear-gradient(125deg, var(--teal) 0%, var(--teal-dark) 100%);
color: #fff;
padding: 10px 16px;
}
.muted {
color: var(--muted);
font-size: 0.92rem;
}
@media (max-width: 1050px) {
.layout {
grid-template-columns: 1fr;
}
.chat-panel {
min-height: 520px;
}
.hero {
align-items: flex-start;
flex-direction: column;
}
}
@media (max-width: 640px) {
.page-shell {
padding: 14px;
}
.snapshot-grid {
grid-template-columns: 1fr;
}
.diagnostics-grid {
grid-template-columns: 1fr;
}
.chat-bubble {
max-width: 100%;
}
.approval-actions {
flex-direction: column;
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
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,
},
});