Skip to content

Commit f38e07f

Browse files
committed
add ui
1 parent 6a36f44 commit f38e07f

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

templates/types/streaming/nextjs/app/components/ui/chat/chat-message-content.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useChatUI,
77
} from "@llamaindex/chat-ui";
88
import { Markdown } from "./custom/markdown";
9+
import { WriterCard } from "./custom/writer-card";
910
import { ToolAnnotations } from "./tools/chat-tools";
1011

1112
export function ChatMessageContent() {
@@ -22,6 +23,11 @@ export function ChatMessageContent() {
2223
/>
2324
),
2425
},
26+
// add the writer card
27+
{
28+
position: ContentPosition.AFTER_EVENTS,
29+
component: <WriterCard message={message} />,
30+
},
2531
{
2632
// add the tool annotations after events
2733
position: ContentPosition.AFTER_EVENTS,
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Message } from "@llamaindex/chat-ui";
2+
import {
3+
AlertCircle,
4+
CheckCircle2,
5+
ChevronDown,
6+
CircleDashed,
7+
Clock,
8+
NotebookPen,
9+
Search,
10+
} from "lucide-react";
11+
import { useEffect, useState } from "react";
12+
import {
13+
Collapsible,
14+
CollapsibleContent,
15+
CollapsibleTrigger,
16+
} from "../../collapsible";
17+
18+
type EventState = "pending" | "inprogress" | "done" | "error";
19+
20+
type WriterEvent = {
21+
type: "retrieve" | "analyze" | "answer";
22+
state: EventState;
23+
data: {
24+
id?: string;
25+
question?: string;
26+
answer?: string | null;
27+
};
28+
};
29+
30+
type QuestionState = {
31+
id: string;
32+
question: string;
33+
answer: string | null;
34+
state: EventState;
35+
isOpen: boolean;
36+
};
37+
38+
type WriterState = {
39+
retrieve: {
40+
state: EventState | null;
41+
};
42+
analyze: {
43+
state: EventState | null;
44+
questions: QuestionState[];
45+
};
46+
};
47+
48+
// Update the state based on the event
49+
const updateState = (state: WriterState, event: WriterEvent): WriterState => {
50+
switch (event.type) {
51+
case "answer": {
52+
const { id, question, answer } = event.data;
53+
if (!id || !question) return state;
54+
55+
const questions = state.analyze.questions;
56+
const existingQuestion = questions.find((q) => q.id === id);
57+
58+
const updatedQuestions = existingQuestion
59+
? questions.map((q) =>
60+
q.id === id
61+
? {
62+
...existingQuestion,
63+
state: event.state,
64+
answer: answer || existingQuestion.answer,
65+
}
66+
: q,
67+
)
68+
: [
69+
...questions,
70+
{
71+
id,
72+
question,
73+
answer: answer || null,
74+
state: event.state,
75+
isOpen: false,
76+
},
77+
];
78+
79+
return {
80+
...state,
81+
analyze: {
82+
...state.analyze,
83+
questions: updatedQuestions,
84+
},
85+
};
86+
}
87+
88+
case "retrieve":
89+
case "analyze":
90+
return {
91+
...state,
92+
[event.type]: {
93+
...state[event.type],
94+
state: event.state,
95+
},
96+
};
97+
98+
default:
99+
return state;
100+
}
101+
};
102+
103+
export function WriterCard({ message }: { message: Message }) {
104+
const [state, setState] = useState<WriterState>({
105+
retrieve: { state: null },
106+
analyze: { state: null, questions: [] },
107+
});
108+
109+
const writerEvents = message.annotations as WriterEvent[] | undefined;
110+
111+
useEffect(() => {
112+
if (writerEvents?.length) {
113+
writerEvents.forEach((event) => {
114+
setState((currentState) => updateState(currentState, event));
115+
});
116+
}
117+
}, [writerEvents]);
118+
119+
const getStateIcon = (state: EventState | null) => {
120+
switch (state) {
121+
case "pending":
122+
return <Clock className="w-4 h-4 text-yellow-500" />;
123+
case "inprogress":
124+
return <CircleDashed className="w-4 h-4 text-blue-500 animate-spin" />;
125+
case "done":
126+
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
127+
case "error":
128+
return <AlertCircle className="w-4 h-4 text-red-500" />;
129+
default:
130+
return null;
131+
}
132+
};
133+
134+
if (!writerEvents?.length) {
135+
return null;
136+
}
137+
138+
return (
139+
<div className="bg-white rounded-2xl shadow-xl p-5 space-y-6 text-gray-800 w-full">
140+
{state.retrieve.state !== null && (
141+
<div className="border-t border-gray-200 pt-4">
142+
<h3 className="font-bold text-lg mb-2 flex items-center gap-2">
143+
<Search className="w-5 h-5" />
144+
<span>
145+
{state.retrieve.state === "inprogress"
146+
? "Searching..."
147+
: "Search completed"}
148+
</span>
149+
</h3>
150+
</div>
151+
)}
152+
153+
{state.analyze.state !== null && (
154+
<div className="border-t border-gray-200 pt-4">
155+
<h3 className="font-bold text-lg mb-2 flex items-center gap-2">
156+
<NotebookPen className="w-5 h-5" />
157+
<span>
158+
{state.analyze.state === "inprogress"
159+
? "Analyzing..."
160+
: "Analysis"}
161+
</span>
162+
</h3>
163+
{state.analyze.questions.length > 0 && (
164+
<div className="space-y-2">
165+
{state.analyze.questions.map((question) => (
166+
<Collapsible key={question.id}>
167+
<CollapsibleTrigger className="w-full">
168+
<div className="flex items-center gap-2 p-3 hover:bg-gray-50 transition-colors rounded-lg border border-gray-200">
169+
<div className="flex-shrink-0">
170+
{getStateIcon(question.state)}
171+
</div>
172+
<span className="font-medium text-left flex-1">
173+
{question.question}
174+
</span>
175+
<ChevronDown className="w-5 h-5 transition-transform ui-expanded:rotate-180" />
176+
</div>
177+
</CollapsibleTrigger>
178+
{question.answer && (
179+
<CollapsibleContent>
180+
<div className="p-3 text-gray-600 text-left border border-t-0 border-gray-200 rounded-b-lg">
181+
{question.answer}
182+
</div>
183+
</CollapsibleContent>
184+
)}
185+
</Collapsible>
186+
))}
187+
</div>
188+
)}
189+
</div>
190+
)}
191+
</div>
192+
);
193+
}

0 commit comments

Comments
 (0)