Paramjit Singh commited on
Commit
88a8f67
Β·
unverified Β·
2 Parent(s): 9594045d288d71

Merge pull request #90 from akshy-yy/feat/typing-indicator-clean

Browse files
frontend/src/components/chat/ChatPanel.tsx CHANGED
@@ -34,6 +34,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
34
  const [messages, setMessages] = useState<ChatMsg[]>([]);
35
  const [input, setInput] = useState("");
36
  const [streaming, setStreaming] = useState(false);
 
37
  const textareaRef = useRef<HTMLTextAreaElement>(null);
38
  const bottomRef = useRef<HTMLDivElement>(null);
39
  const prevDocId = useRef<string | null>(null);
@@ -116,17 +117,12 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
116
  };
117
  setMessages((prev) => [...prev, userMsg]);
118
 
119
- // Add placeholder assistant message
120
  const assistantId = `assistant-${Date.now()}`;
121
- const assistantMsg: ChatMsg = {
122
- id: assistantId,
123
- role: "assistant",
124
- content: "",
125
- sources: [],
126
- isStreaming: true,
127
- };
128
- setMessages((prev) => [...prev, assistantMsg]);
129
  setStreaming(true);
 
130
 
131
  try {
132
  const stream = api.streamPost("/api/v1/chat/ask/stream", {
@@ -136,13 +132,29 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
136
 
137
  for await (const event of stream) {
138
  if (event.type === "token") {
139
- setMessages((prev) =>
140
- prev.map((m) =>
141
- m.id === assistantId
142
- ? { ...m, content: m.content + (event.data as string) }
143
- : m
144
- )
145
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  } else if (event.type === "sources") {
147
  setMessages((prev) =>
148
  prev.map((m) =>
@@ -152,6 +164,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
152
  )
153
  );
154
  } else if (event.type === "error") {
 
155
  setMessages((prev) =>
156
  prev.map((m) =>
157
  m.id === assistantId
@@ -168,6 +181,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
168
  }
169
  }
170
  } catch (err) {
 
171
  setMessages((prev) =>
172
  prev.map((m) =>
173
  m.id === assistantId
@@ -181,6 +195,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
181
  );
182
  } finally {
183
  setStreaming(false);
 
184
  }
185
  };
186
 
@@ -190,7 +205,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
190
  await api.delete(`/api/v1/chat/history/${activeDoc.id}`);
191
  setMessages([]);
192
  } catch {
193
- // silently fail
194
  }
195
  };
196
 
@@ -204,8 +219,8 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
204
  return (
205
  <div className="h-full flex flex-col">
206
  {/* ── Chat Messages ──────────────────────────── */}
207
- <div className="flex-1 px-4 overflow-y-auto custom-scrollbar">
208
- {messages.length === 0 ? (
209
  <div className="h-full flex flex-col items-center justify-center py-20">
210
  <div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-4">
211
  <MessageSquare className="w-8 h-8 text-primary/60" />
@@ -231,6 +246,13 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
231
  )}
232
  </div>
233
  ))}
 
 
 
 
 
 
 
234
  </div>
235
  )}
236
  <div ref={bottomRef} className="h-4" />
@@ -283,4 +305,4 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
283
  </div>
284
  </div>
285
  );
286
- }
 
34
  const [messages, setMessages] = useState<ChatMsg[]>([]);
35
  const [input, setInput] = useState("");
36
  const [streaming, setStreaming] = useState(false);
37
+ const [isTyping, setIsTyping] = useState(false);
38
  const textareaRef = useRef<HTMLTextAreaElement>(null);
39
  const bottomRef = useRef<HTMLDivElement>(null);
40
  const prevDocId = useRef<string | null>(null);
 
117
  };
118
  setMessages((prev) => [...prev, userMsg]);
119
 
120
+
121
  const assistantId = `assistant-${Date.now()}`;
122
+ let assistantCreated = false;
123
+
 
 
 
 
 
 
124
  setStreaming(true);
125
+ setIsTyping(true);
126
 
127
  try {
128
  const stream = api.streamPost("/api/v1/chat/ask/stream", {
 
132
 
133
  for await (const event of stream) {
134
  if (event.type === "token") {
135
+ // Create assistant message only when first token arrives
136
+ if (!assistantCreated) {
137
+ assistantCreated = true;
138
+ setIsTyping(false);
139
+
140
+ const assistantMsg: ChatMsg = {
141
+ id: assistantId,
142
+ role: "assistant",
143
+ content: event.data as string,
144
+ sources: [],
145
+ isStreaming: true,
146
+ };
147
+
148
+ setMessages((prev) => [...prev, assistantMsg]);
149
+ } else {
150
+ setMessages((prev) =>
151
+ prev.map((m) =>
152
+ m.id === assistantId
153
+ ? { ...m, content: m.content + (event.data as string) }
154
+ : m
155
+ )
156
+ );
157
+ }
158
  } else if (event.type === "sources") {
159
  setMessages((prev) =>
160
  prev.map((m) =>
 
164
  )
165
  );
166
  } else if (event.type === "error") {
167
+ setIsTyping(false);
168
  setMessages((prev) =>
169
  prev.map((m) =>
170
  m.id === assistantId
 
181
  }
182
  }
183
  } catch (err) {
184
+ setIsTyping(false);
185
  setMessages((prev) =>
186
  prev.map((m) =>
187
  m.id === assistantId
 
195
  );
196
  } finally {
197
  setStreaming(false);
198
+ setIsTyping(false);
199
  }
200
  };
201
 
 
205
  await api.delete(`/api/v1/chat/history/${activeDoc.id}`);
206
  setMessages([]);
207
  } catch {
208
+ //silent fail
209
  }
210
  };
211
 
 
219
  return (
220
  <div className="h-full flex flex-col">
221
  {/* ── Chat Messages ──────────────────────────── */}
222
+ <div className="flex-1 px-4 overflow-y-auto custom-scrollbar">
223
+ {messages.length === 0 && !isTyping ? (
224
  <div className="h-full flex flex-col items-center justify-center py-20">
225
  <div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-4">
226
  <MessageSquare className="w-8 h-8 text-primary/60" />
 
246
  )}
247
  </div>
248
  ))}
249
+ {isTyping && (
250
+ <div className="flex items-center gap-1 ml-10 py-2">
251
+ <span className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.3s]" />
252
+ <span className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.15s]" />
253
+ <span className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" />
254
+ </div>
255
+ )}
256
  </div>
257
  )}
258
  <div ref={bottomRef} className="h-4" />
 
305
  </div>
306
  </div>
307
  );
308
+ }