forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchat.tsx
More file actions
117 lines (104 loc) · 4.2 KB
/
Copy pathchat.tsx
File metadata and controls
117 lines (104 loc) · 4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"use client";
/**
* The whole demo, in one screenful: two hooks turn a shadcn shell into a
* working chat. `useAgent` exposes the message log + run state for one
* agent; `useCopilotKit` runs it.
*/
import { useState } from "react";
import { useAgent, useCopilotKit } from "@copilotkit/react-core/v2";
import { Sparkles } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { AssistantBubble, UserBubble } from "./message-bubble";
import { Composer } from "./composer";
import { EmptyState } from "./empty-state";
import { TypingIndicator } from "./typing-indicator";
export function Chat() {
// @region[use-agent-simple]
const { agent } = useAgent({ agentId: "headless-simple" });
const { copilotkit } = useCopilotKit();
const [input, setInput] = useState("");
const send = (text: string) => {
const trimmed = text.trim();
if (!trimmed || agent.isRunning) return;
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: trimmed,
});
setInput("");
void copilotkit.runAgent({ agent }).catch((err) => {
// The Headless Simple demo is the canonical "two hooks, your
// design system" example users copy-paste as a starting point.
// Silently swallowing errors here would model broken practice;
// log so a network failure / runtime error / transport disconnect
// surfaces in the console for the developer.
console.error("[langgraph-python:headless-simple] runAgent failed", err);
});
};
// @endregion[use-agent-simple]
// Render only plain user/assistant text — Simple skips tool/system/etc.
const visible = agent.messages.flatMap((m) => {
if (m.role !== "user" && m.role !== "assistant") return [];
if (typeof m.content !== "string" || m.content.length === 0) return [];
return [{ id: m.id, role: m.role, content: m.content }];
});
const last = visible[visible.length - 1];
const showTyping = agent.isRunning && (!last || last.role === "user");
const hasMessages = visible.length > 0;
return (
<div className="flex h-screen w-full justify-center bg-background p-4 sm:p-6">
<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">
<CardHeader className="border-b border-border/60 py-4">
<CardTitle className="flex items-center gap-2 text-base">
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
Headless Chat
</CardTitle>
<CardDescription>
Two hooks, your design system — that's the whole demo.
</CardDescription>
</CardHeader>
<CardContent className="flex min-h-0 flex-1 flex-col p-0">
{!hasMessages ? (
// Render empty state OUTSIDE ScrollArea so flex-1 + justify-
// center can vertically center it. Radix ScrollArea wraps
// content in a `display: table` div that breaks `h-full`
// propagation, so a centered child inside it hugs the top.
<div className="flex min-h-0 flex-1 flex-col">
<EmptyState onPick={send} />
</div>
) : (
<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-4 px-4 py-4 sm:px-6">
{/* @region[message-list-simple] */}
{visible.map((m) =>
m.role === "user" ? (
<UserBubble key={m.id} content={m.content} />
) : (
<AssistantBubble key={m.id} content={m.content} />
),
)}
{/* @endregion[message-list-simple] */}
{showTyping && <TypingIndicator />}
</div>
</ScrollArea>
)}
<Separator className="bg-border/60" />
<Composer
value={input}
onChange={setInput}
onSend={() => send(input)}
disabled={!input.trim() || agent.isRunning}
/>
</CardContent>
</Card>
</div>
);
}