Skip to content

Commit 67964da

Browse files
committed
feat(docs-ai): persist chat session and improve message rendering
1 parent eee2398 commit 67964da

File tree

5 files changed

+506
-88
lines changed

5 files changed

+506
-88
lines changed

docs/components/ai-panel/ai-panel-provider.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
"use client";
22

3+
import { Chat } from "@ai-sdk/react";
4+
import { DefaultChatTransport, type UIMessage } from "ai";
35
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
46

57
interface AIPanelContextType {
68
isOpen: boolean;
79
toggle: () => void;
810
open: () => void;
911
close: () => void;
12+
chat: Chat<UIMessage>;
1013
}
1114

1215
const AIPanelContext = createContext<AIPanelContextType | null>(null);
1316

1417
const STORAGE_KEY = "seed-ai-panel-open";
18+
const CHAT_ID = "seed-ai-panel-chat";
19+
const chatTransport = new DefaultChatTransport({
20+
api: "/api/chat",
21+
});
1522

1623
export function AIPanelProvider({ children }: { children: ReactNode }) {
1724
const [isOpen, setIsOpen] = useState(true);
1825
const [hydrated, setHydrated] = useState(false);
26+
const [chat] = useState(
27+
() =>
28+
new Chat<UIMessage>({
29+
id: CHAT_ID,
30+
transport: chatTransport,
31+
}),
32+
);
1933

2034
useEffect(() => {
2135
try {
@@ -47,7 +61,7 @@ export function AIPanelProvider({ children }: { children: ReactNode }) {
4761
const close = useCallback(() => setIsOpen(false), []);
4862

4963
return (
50-
<AIPanelContext.Provider value={{ isOpen, toggle, open, close }}>
64+
<AIPanelContext.Provider value={{ isOpen, toggle, open, close, chat }}>
5165
{children}
5266
</AIPanelContext.Provider>
5367
);

docs/components/ai-panel/chat-interface.tsx

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"use client";
22

3-
import { DefaultChatTransport } from "ai";
43
import { useChat } from "@ai-sdk/react";
54
import { useAIPanel } from "./ai-panel-provider";
65
import { ChatMessage } from "./chat-message";
76
import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon";
8-
import { IconXmarkLine } from "@karrotmarket/react-monochrome-icon";
7+
import { IconTrashcanLine, IconXmarkLine } from "@karrotmarket/react-monochrome-icon";
98
import { Icon } from "@seed-design/react";
109
import { ActionButton } from "seed-design/ui/action-button";
10+
import { ProgressCircle } from "seed-design/ui/progress-circle";
1111
import { TextField, TextFieldInput } from "seed-design/ui/text-field";
1212
import { useEffect, useRef, useState, type FormEvent } from "react";
1313

@@ -17,16 +17,12 @@ const SUGGESTIONS = [
1717
"SEED Design 색상 토큰은 어떻게 사용해?",
1818
];
1919

20-
const chatTransport = new DefaultChatTransport({
21-
api: "/api/chat",
22-
});
23-
2420
export function ChatInterface() {
25-
const { close } = useAIPanel();
21+
const { close, chat } = useAIPanel();
2622
const [input, setInput] = useState("");
2723

28-
const { messages, sendMessage, status } = useChat({
29-
transport: chatTransport,
24+
const { messages, sendMessage, setMessages, status, stop } = useChat({
25+
chat,
3026
});
3127

3228
const isLoading = status === "submitted" || status === "streaming";
@@ -56,6 +52,19 @@ export function ChatInterface() {
5652
setInput("");
5753
};
5854

55+
const handleClearConversation = () => {
56+
if (!window.confirm("현재 대화를 모두 삭제할까요?")) {
57+
return;
58+
}
59+
60+
if (isLoading) {
61+
stop();
62+
}
63+
64+
setMessages([]);
65+
setInput("");
66+
};
67+
5968
return (
6069
<div className="flex flex-col h-full bg-fd-background">
6170
{/* Header */}
@@ -64,18 +73,33 @@ export function ChatInterface() {
6473
<IconSparkle2 width={18} height={18} />
6574
<span className="font-semibold text-sm">SEED Assistant</span>
6675
</div>
67-
<ActionButton
68-
type="button"
69-
onClick={close}
70-
variant="ghost"
71-
layout="iconOnly"
72-
size="xsmall"
73-
bleedX="asPadding"
74-
bleedY="asPadding"
75-
aria-label="패널 닫기"
76-
>
77-
<Icon svg={<IconXmarkLine />} />
78-
</ActionButton>
76+
<div className="flex items-center gap-1">
77+
<ActionButton
78+
type="button"
79+
onClick={handleClearConversation}
80+
variant="ghost"
81+
layout="iconOnly"
82+
size="xsmall"
83+
bleedX="asPadding"
84+
bleedY="asPadding"
85+
aria-label="대화 삭제"
86+
disabled={messages.length === 0}
87+
>
88+
<Icon svg={<IconTrashcanLine />} />
89+
</ActionButton>
90+
<ActionButton
91+
type="button"
92+
onClick={close}
93+
variant="ghost"
94+
layout="iconOnly"
95+
size="xsmall"
96+
bleedX="asPadding"
97+
bleedY="asPadding"
98+
aria-label="패널 닫기"
99+
>
100+
<Icon svg={<IconXmarkLine />} />
101+
</ActionButton>
102+
</div>
79103
</div>
80104

81105
{/* Messages */}
@@ -109,16 +133,9 @@ export function ChatInterface() {
109133

110134
{isLoading && messages.length > 0 && messages[messages.length - 1]?.role === "user" && (
111135
<div className="flex justify-start">
112-
<div className="flex items-center gap-1.5 text-sm text-fd-muted-foreground">
113-
<span className="inline-block size-1.5 rounded-full bg-fd-muted-foreground animate-pulse" />
114-
<span
115-
className="inline-block size-1.5 rounded-full bg-fd-muted-foreground animate-pulse"
116-
style={{ animationDelay: "0.2s" }}
117-
/>
118-
<span
119-
className="inline-block size-1.5 rounded-full bg-fd-muted-foreground animate-pulse"
120-
style={{ animationDelay: "0.4s" }}
121-
/>
136+
<div className="flex items-center gap-2 text-sm text-fd-muted-foreground">
137+
<ProgressCircle size="24" value={undefined} />
138+
응답 생성 중...
122139
</div>
123140
</div>
124141
)}

0 commit comments

Comments
 (0)