Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

300 lines
7.8 KiB
TypeScript

"use client";
import { useAui, AuiProvider, Suggestions } from "@assistant-ui/react";
import {
useA2ATask,
useA2AArtifacts,
useA2AAgentCard,
type A2ATaskState,
type A2APart,
} from "@assistant-ui/react-a2a";
import { Thread } from "@/components/assistant-ui/thread";
const STATE_CONFIG: Record<
string,
{ label: string; color: string; bg: string }
> = {
submitted: {
label: "Submitted",
color: "text-blue-600",
bg: "bg-blue-100 dark:bg-blue-900/30",
},
working: {
label: "Working",
color: "text-amber-600",
bg: "bg-amber-100 dark:bg-amber-900/30",
},
completed: {
label: "Completed",
color: "text-green-600",
bg: "bg-green-100 dark:bg-green-900/30",
},
failed: {
label: "Failed",
color: "text-red-600",
bg: "bg-red-100 dark:bg-red-900/30",
},
canceled: {
label: "Canceled",
color: "text-gray-600",
bg: "bg-gray-100 dark:bg-gray-900/30",
},
rejected: {
label: "Rejected",
color: "text-red-600",
bg: "bg-red-100 dark:bg-red-900/30",
},
input_required: {
label: "Input Required",
color: "text-purple-600",
bg: "bg-purple-100 dark:bg-purple-900/30",
},
auth_required: {
label: "Auth Required",
color: "text-orange-600",
bg: "bg-orange-100 dark:bg-orange-900/30",
},
};
function StateBadge({ state }: { state: A2ATaskState }) {
const config = STATE_CONFIG[state] ?? {
label: state,
color: "text-gray-600",
bg: "bg-gray-100",
};
const isActive = state === "working" || state === "submitted";
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${config.color} ${config.bg}`}
>
{isActive ? (
<span className="relative flex size-1.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-current opacity-75" />
<span className="relative inline-flex size-1.5 rounded-full bg-current" />
</span>
) : (
<span className="inline-flex size-1.5 rounded-full bg-current" />
)}
{config.label}
</span>
);
}
function TaskStatusBar() {
const task = useA2ATask();
if (!task) return null;
const { state } = task.status;
const isError = state === "failed" || state === "rejected";
const errorText = isError ? task.status.message?.parts?.[0]?.text : undefined;
return (
<div className="flex items-center gap-2 border-b px-4 py-2 text-sm">
<StateBadge state={state} />
<span className="text-muted-foreground">Task {task.id.slice(0, 8)}</span>
{errorText && (
<span className="ml-auto text-xs text-red-600">{errorText}</span>
)}
</div>
);
}
function ArtifactPartView({ part }: { part: A2APart }) {
if (part.text !== undefined) {
return (
<pre className="bg-background overflow-x-auto rounded border p-2 font-mono text-xs whitespace-pre-wrap">
{part.text}
</pre>
);
}
if (part.data !== undefined) {
return (
<div className="bg-background rounded border p-2">
<div className="text-muted-foreground mb-1 text-xs font-medium">
Data
</div>
<pre className="overflow-x-auto font-mono text-xs whitespace-pre-wrap">
{JSON.stringify(part.data, null, 2)}
</pre>
</div>
);
}
if (part.url !== undefined) {
const isImage = part.mediaType?.startsWith("image/");
if (isImage) {
return (
<img
src={part.url}
alt={part.filename ?? "image"}
className="max-h-64 rounded border"
/>
);
}
return (
<a
href={part.url}
target="_blank"
rel="noopener noreferrer"
className="bg-background hover:bg-muted inline-flex items-center gap-2 rounded border p-2 text-sm"
>
<span>📄</span>
<span>{part.filename ?? "Download"}</span>
{part.mediaType && (
<span className="text-muted-foreground text-xs">
{part.mediaType}
</span>
)}
</a>
);
}
if (part.raw !== undefined) {
const isImage = part.mediaType?.startsWith("image/");
if (isImage) {
return (
<img
src={`data:${part.mediaType};base64,${part.raw}`}
alt={part.filename ?? "image"}
className="max-h-64 rounded border"
/>
);
}
const dataUri = `data:${part.mediaType ?? "application/octet-stream"};base64,${part.raw}`;
return (
<a
href={dataUri}
download={part.filename ?? "download"}
className="bg-background hover:bg-muted inline-flex items-center gap-2 rounded border p-2 text-sm"
>
<span>📎</span>
<span>{part.filename ?? "File"}</span>
{part.mediaType && (
<span className="text-muted-foreground text-xs">
{part.mediaType}
</span>
)}
</a>
);
}
return null;
}
function ArtifactPanel() {
const artifacts = useA2AArtifacts();
if (artifacts.length === 0) return null;
return (
<div className="max-h-80 overflow-y-auto border-t px-4 py-3">
<h3 className="text-muted-foreground mb-2 text-xs font-medium">
Artifacts ({artifacts.length})
</h3>
<div className="space-y-2">
{artifacts.map((artifact) => (
<div
key={artifact.artifactId}
className="bg-muted/30 overflow-hidden rounded-md border"
>
<div className="bg-muted/50 flex items-center gap-2 border-b px-3 py-1.5">
<span className="text-xs font-medium">
{artifact.name ?? artifact.artifactId}
</span>
{artifact.description && (
<span className="text-muted-foreground text-xs">
{artifact.description}
</span>
)}
</div>
<div className="space-y-2 p-3">
{artifact.parts.map((part, i) => (
<ArtifactPartView key={i} part={part} />
))}
</div>
</div>
))}
</div>
</div>
);
}
function AgentCardBanner() {
const card = useA2AAgentCard();
if (!card) return null;
return (
<div className="bg-muted/30 flex items-center gap-3 border-b px-4 py-2">
<div className="flex-1">
<div className="text-sm font-medium">{card.name}</div>
<div className="text-muted-foreground text-xs">{card.description}</div>
</div>
<div className="flex flex-wrap gap-1">
{card.skills.map((skill) => (
<span
key={skill.id}
className="bg-muted rounded-md px-2 py-0.5 text-xs"
title={skill.description}
>
{skill.name}
</span>
))}
</div>
<div className="text-muted-foreground text-xs">
v{card.version}
{card.capabilities.streaming && " · Streaming"}
</div>
</div>
);
}
function ThreadWithSuggestions() {
const aui = useAui({
suggestions: Suggestions([
{
title: "Chat",
label: "say hello",
prompt: "Hello! What can you do?",
},
{
title: "Generate artifacts",
label: "code + data + file",
prompt: "/artifacts a fibonacci function in Python",
},
{
title: "Multi-step",
label: "input-required flow",
prompt: "/multistep",
},
{
title: "Failure demo",
label: "test error handling",
prompt: "/fail",
},
{
title: "Slow task",
label: "test cancellation",
prompt: "/slow",
},
]),
});
return (
<AuiProvider value={aui}>
<Thread />
</AuiProvider>
);
}
export default function Home() {
return (
<main className="flex h-dvh flex-col">
<AgentCardBanner />
<TaskStatusBar />
<div className="min-h-0 flex-1">
<ThreadWithSuggestions />
</div>
<ArtifactPanel />
</main>
);
}