Skip to content

Commit 0c91f14

Browse files
tylerslatonclaude
andcommitted
feat(showcase/google-adk/headless-simple): port frontend from langgraph-python
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d87e86b commit 0c91f14

6 files changed

Lines changed: 297 additions & 153 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use client";
2+
3+
/**
4+
* The whole demo, in one screenful: two hooks turn a shadcn shell into a
5+
* working chat. `useAgent` exposes the message log + run state for one
6+
* agent; `useCopilotKit` runs it.
7+
*/
8+
9+
import { useState } from "react";
10+
import { useAgent, useCopilotKit } from "@copilotkit/react-core/v2";
11+
import { Sparkles } from "lucide-react";
12+
13+
import {
14+
Card,
15+
CardContent,
16+
CardDescription,
17+
CardHeader,
18+
CardTitle,
19+
} from "@/components/ui/card";
20+
import { ScrollArea } from "@/components/ui/scroll-area";
21+
import { Separator } from "@/components/ui/separator";
22+
23+
import { AssistantBubble, UserBubble } from "./message-bubble";
24+
import { Composer } from "./composer";
25+
import { EmptyState } from "./empty-state";
26+
import { TypingIndicator } from "./typing-indicator";
27+
28+
export function Chat() {
29+
const { agent } = useAgent({ agentId: "headless-simple" });
30+
const { copilotkit } = useCopilotKit();
31+
const [input, setInput] = useState("");
32+
33+
const send = (text: string) => {
34+
const trimmed = text.trim();
35+
if (!trimmed || agent.isRunning) return;
36+
agent.addMessage({
37+
id: crypto.randomUUID(),
38+
role: "user",
39+
content: trimmed,
40+
});
41+
setInput("");
42+
void copilotkit.runAgent({ agent }).catch(() => {});
43+
};
44+
45+
// Render only plain user/assistant text — Simple skips tool/system/etc.
46+
const visible = agent.messages.flatMap((m) => {
47+
if (m.role !== "user" && m.role !== "assistant") return [];
48+
if (typeof m.content !== "string" || m.content.length === 0) return [];
49+
return [{ id: m.id, role: m.role, content: m.content }];
50+
});
51+
52+
const last = visible[visible.length - 1];
53+
const showTyping = agent.isRunning && (!last || last.role === "user");
54+
55+
const hasMessages = visible.length > 0;
56+
57+
return (
58+
<div className="flex h-screen w-full justify-center bg-background p-4 sm:p-6">
59+
<Card className="flex h-full w-full max-w-3xl flex-col gap-0 overflow-hidden border-border py-0 shadow-2xl shadow-black/10">
60+
<CardHeader className="border-b border-border/60 py-4">
61+
<CardTitle className="flex items-center gap-2 text-base">
62+
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
63+
Headless Chat
64+
</CardTitle>
65+
<CardDescription>
66+
Two hooks, your design system — that&apos;s the whole demo.
67+
</CardDescription>
68+
</CardHeader>
69+
70+
<CardContent className="flex min-h-0 flex-1 flex-col p-0">
71+
{!hasMessages ? (
72+
// Render empty state OUTSIDE ScrollArea so flex-1 + justify-
73+
// center can vertically center it. Radix ScrollArea wraps
74+
// content in a `display: table` div that breaks `h-full`
75+
// propagation, so a centered child inside it hugs the top.
76+
<div className="flex min-h-0 flex-1 flex-col">
77+
<EmptyState onPick={send} />
78+
</div>
79+
) : (
80+
<ScrollArea className="min-h-0 flex-1">
81+
<div className="flex flex-col gap-4 px-4 py-4 sm:px-6">
82+
{visible.map((m) =>
83+
m.role === "user" ? (
84+
<UserBubble key={m.id} content={m.content} />
85+
) : (
86+
<AssistantBubble key={m.id} content={m.content} />
87+
),
88+
)}
89+
{showTyping && <TypingIndicator />}
90+
</div>
91+
</ScrollArea>
92+
)}
93+
94+
<Separator className="bg-border/60" />
95+
96+
<Composer
97+
value={input}
98+
onChange={setInput}
99+
onSend={() => send(input)}
100+
disabled={!input.trim() || agent.isRunning}
101+
/>
102+
</CardContent>
103+
</Card>
104+
</div>
105+
);
106+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
/** Bottom-of-chat input. Enter sends, Shift+Enter inserts a newline. */
4+
5+
import { ArrowUp } from "lucide-react";
6+
import { Button } from "@/components/ui/button";
7+
import { Textarea } from "@/components/ui/textarea";
8+
import { cn } from "@/lib/utils";
9+
10+
export function Composer({
11+
value,
12+
onChange,
13+
onSend,
14+
disabled,
15+
}: {
16+
value: string;
17+
onChange: (next: string) => void;
18+
onSend: () => void;
19+
disabled: boolean;
20+
}) {
21+
return (
22+
<div data-testid="headless-composer" className="bg-background p-3 sm:p-4">
23+
<div
24+
className={cn(
25+
"flex items-end gap-2 rounded-2xl border border-border/60 bg-muted/50 p-2",
26+
"focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/40",
27+
"transition-colors",
28+
)}
29+
>
30+
<Textarea
31+
rows={1}
32+
value={value}
33+
onChange={(e) => onChange(e.target.value)}
34+
onKeyDown={(e) => {
35+
if (e.key === "Enter" && !e.shiftKey) {
36+
e.preventDefault();
37+
if (!disabled) onSend();
38+
}
39+
}}
40+
placeholder="Message the agent..."
41+
className={cn(
42+
"min-h-[40px] max-h-40 flex-1 resize-none border-0 bg-transparent",
43+
"px-2 py-2 text-sm shadow-none",
44+
"focus-visible:ring-0 focus-visible:border-transparent",
45+
)}
46+
/>
47+
<Button
48+
type="button"
49+
size="icon"
50+
onClick={onSend}
51+
disabled={disabled}
52+
aria-label="Send message"
53+
className="h-9 w-9 shrink-0 rounded-xl"
54+
>
55+
<ArrowUp className="h-4 w-4" />
56+
</Button>
57+
</div>
58+
</div>
59+
);
60+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
/** First-load pane: heading, sparkles icon, and three sample prompt chips. */
4+
5+
import { Sparkles } from "lucide-react";
6+
import { Badge } from "@/components/ui/badge";
7+
8+
const SAMPLES = [
9+
"Say hello in one short sentence.",
10+
"Tell me a one-line joke.",
11+
"Give me a fun fact.",
12+
];
13+
14+
export function EmptyState({ onPick }: { onPick: (text: string) => void }) {
15+
return (
16+
<div className="flex h-full flex-1 flex-col items-center justify-center gap-6 px-8 py-10 text-center">
17+
<div className="flex flex-col items-center gap-2">
18+
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
19+
<Sparkles className="h-6 w-6" aria-hidden="true" />
20+
</div>
21+
<h2 className="text-lg font-semibold tracking-tight">
22+
Start a conversation
23+
</h2>
24+
<p className="max-w-sm text-sm text-muted-foreground">
25+
Two hooks, one shadcn shell — text in, text out.
26+
</p>
27+
</div>
28+
<div className="flex w-full max-w-md flex-wrap items-center justify-center gap-2">
29+
{SAMPLES.map((s) => (
30+
<Badge
31+
key={s}
32+
asChild
33+
variant="secondary"
34+
className="cursor-pointer px-3 py-1.5 text-xs font-normal hover:bg-secondary/80"
35+
>
36+
<button type="button" onClick={() => onPick(s)}>
37+
{s}
38+
</button>
39+
</Badge>
40+
))}
41+
</div>
42+
</div>
43+
);
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
/** User and assistant bubbles. Plain text only — Simple skips markdown. */
4+
5+
import { Bot, User } from "lucide-react";
6+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
7+
8+
export function UserBubble({ content }: { content: string }) {
9+
return (
10+
<div
11+
data-testid="headless-message-user"
12+
data-message-role="user"
13+
className="flex w-full items-start gap-3 flex-row-reverse"
14+
>
15+
<Avatar className="h-8 w-8 shrink-0 border bg-primary text-primary-foreground">
16+
<AvatarFallback className="bg-primary text-primary-foreground">
17+
<User className="h-4 w-4" />
18+
</AvatarFallback>
19+
</Avatar>
20+
<div className="max-w-[80%] rounded-2xl rounded-tr-sm bg-primary px-4 py-2.5 text-sm leading-relaxed text-primary-foreground shadow-sm">
21+
<p className="whitespace-pre-wrap break-words">{content}</p>
22+
</div>
23+
</div>
24+
);
25+
}
26+
27+
export function AssistantBubble({ content }: { content: string }) {
28+
return (
29+
<div
30+
data-testid="headless-message-assistant"
31+
data-message-role="assistant"
32+
className="flex w-full items-start gap-3"
33+
>
34+
<Avatar className="h-8 w-8 shrink-0 border bg-muted text-muted-foreground">
35+
<AvatarFallback className="bg-muted text-muted-foreground">
36+
<Bot className="h-4 w-4" />
37+
</AvatarFallback>
38+
</Avatar>
39+
<div className="max-w-[80%] rounded-2xl rounded-tl-sm bg-muted px-4 py-2.5 text-sm leading-relaxed text-foreground shadow-sm">
40+
<p className="whitespace-pre-wrap break-words">{content}</p>
41+
</div>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)