Skip to content

Commit 022da9a

Browse files
Feat: Use streaming response in chatbot (#151)
* Feat: Use streaming response in chatbot * Fix formatting
1 parent 871fee3 commit 022da9a

File tree

3 files changed

+144
-40
lines changed

3 files changed

+144
-40
lines changed

app/[lang]/(hyperjump)/components/landing-ai-agent.tsx

Lines changed: 117 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Image from "next/image";
66
import { useCallback, useEffect, useRef, useState } from "react";
77
import { toast } from "sonner";
88
import { motion, AnimatePresence } from "framer-motion";
9+
import markdownit from "markdown-it";
910
import { v4 as uuid } from "uuid";
1011
import { sendGAEvent } from "@next/third-parties/google";
1112

@@ -28,6 +29,7 @@ const DEFAULT_MESSAGES = [
2829
{ id: 2, text: "Show me examples of past projects" },
2930
{ id: 3, text: "Schedule a free consultation" }
3031
];
32+
const ENABLE_STREAMING = true;
3133

3234
// Functions
3335
// APIs
@@ -86,6 +88,74 @@ const fetchAsk = async (payload: {
8688
return data;
8789
};
8890

91+
const fetchAskStream = async (
92+
payload: {
93+
chatInput: string;
94+
sessionId: string;
95+
},
96+
onChunk: (chunk: string) => void
97+
): Promise<void> => {
98+
const { chatInput, sessionId } = payload;
99+
const url = new URL(
100+
String(process.env.NEXT_PUBLIC_LANDING_POST_CHATS_WEBHOOK)
101+
);
102+
url.searchParams.set("stream", "true");
103+
104+
const response = await fetch(url, {
105+
method: "POST",
106+
headers: {
107+
"Content-Type": "application/json"
108+
},
109+
body: JSON.stringify({
110+
chatInput,
111+
sessionId
112+
})
113+
});
114+
115+
if (!response.ok) {
116+
throw new Error("Failed to ask question");
117+
}
118+
119+
const reader = response.body?.getReader();
120+
const decoder = new TextDecoder();
121+
122+
if (!reader) {
123+
throw new Error("No response body");
124+
}
125+
126+
let buffer = "";
127+
128+
while (true) {
129+
const { done, value } = await reader.read();
130+
if (done) break;
131+
132+
buffer += decoder.decode(value, { stream: true });
133+
const lines = buffer.split("\n");
134+
135+
// Keep the last incomplete line in the buffer
136+
buffer = lines.pop() || "";
137+
138+
for (const line of lines) {
139+
if (line.startsWith("data: ")) {
140+
try {
141+
const jsonStr = line.slice(6); // Remove "data: " prefix
142+
const data = JSON.parse(jsonStr) as { chunk: string; done: boolean };
143+
144+
if (data.chunk) {
145+
onChunk(data.chunk);
146+
}
147+
148+
if (data.done) {
149+
return;
150+
}
151+
} catch (error) {
152+
console.error("Failed to parse SSE data:", error);
153+
}
154+
}
155+
}
156+
}
157+
};
158+
89159
// Cookies
90160
function setCookie(cname: string, cvalue: string, exdays: number) {
91161
const d = new Date();
@@ -210,7 +280,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
210280

211281
// Function to handle form submission
212282
const handleSubmit = useCallback(
213-
async (text: string) => {
283+
async (text: string, useStreaming: boolean = true) => {
214284
if (!text.length) return;
215285
if (!text.length || !inputRef.current) return;
216286

@@ -226,16 +296,36 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
226296
const prevMessages = messages;
227297
const newMessages = [...prevMessages, { human: chatInput, ai: "" }];
228298
setMessages(newMessages);
229-
const { output } = await fetchAsk({
230-
chatInput,
231-
sessionId: sessionId as string
232-
});
233299

234-
const newMessagesFromAI = [
235-
...prevMessages,
236-
{ human: chatInput, ai: output }
237-
];
238-
setMessages(newMessagesFromAI);
300+
if (useStreaming) {
301+
let accumulatedOutput = "";
302+
303+
await fetchAskStream(
304+
{
305+
chatInput,
306+
sessionId: sessionId as string
307+
},
308+
(chunk) => {
309+
accumulatedOutput += chunk;
310+
const updatedMessages = [
311+
...prevMessages,
312+
{ human: chatInput, ai: accumulatedOutput }
313+
];
314+
setMessages(updatedMessages);
315+
}
316+
);
317+
} else {
318+
const { output } = await fetchAsk({
319+
chatInput,
320+
sessionId: sessionId as string
321+
});
322+
323+
const newMessagesFromAI = [
324+
...prevMessages,
325+
{ human: chatInput, ai: output }
326+
];
327+
setMessages(newMessagesFromAI);
328+
}
239329

240330
scrollToBottom();
241331
} catch (error) {
@@ -257,7 +347,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
257347

258348
// Wait for the chat to open and then submit
259349
setTimeout(() => {
260-
handleSubmit(message);
350+
handleSubmit(message, ENABLE_STREAMING);
261351
}, 300);
262352
};
263353

@@ -303,16 +393,17 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
303393
)}
304394
{m.ai && (
305395
<div className="max-w-[80%] self-start rounded-xl bg-white p-3">
306-
{isSubmitting && i === messages.length - 1 ? (
307-
<div className="flex gap-1">
308-
<span className="sr-only">Loading...</span>
309-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
310-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
311-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500" />
312-
</div>
313-
) : (
314-
<MarkdownContent input={m.ai} />
315-
)}
396+
<MarkdownContent input={m.ai} />
397+
</div>
398+
)}
399+
{!m.ai && isSubmitting && i === messages.length - 1 && (
400+
<div className="max-w-[80%] self-start rounded-xl bg-white p-3">
401+
<div className="flex gap-1">
402+
<span className="sr-only">Loading...</span>
403+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
404+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
405+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500" />
406+
</div>
316407
</div>
317408
)}
318409
</div>
@@ -339,15 +430,6 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
339430
</div>
340431
)}
341432

342-
{isSubmitting && (
343-
<div className="mt-3 mb-5 flex items-center gap-2 bg-transparent text-xs text-gray-500">
344-
<span className="sr-only">Loading...</span>
345-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
346-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
347-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-500" />
348-
</div>
349-
)}
350-
351433
<div className="relative flex w-full items-center">
352434
<input
353435
ref={inputRef}
@@ -359,7 +441,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
359441
onKeyDown={(e) => {
360442
if (e.key === "Enter" && !e.shiftKey) {
361443
e.preventDefault();
362-
handleSubmit(text);
444+
handleSubmit(text, ENABLE_STREAMING);
363445
}
364446
}}
365447
placeholder="Ask me about services, success stories, or your challenges"
@@ -370,7 +452,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
370452
variant="default"
371453
disabled={isSubmitting}
372454
onClick={() => {
373-
handleSubmit(text);
455+
handleSubmit(text, ENABLE_STREAMING);
374456
}}>
375457
<Image
376458
alt="Send message to AI"
@@ -447,6 +529,8 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
447529
const MarkdownContent = ({ input }: { input: string }) => (
448530
<div
449531
className="prose prose-sm text-sm"
450-
dangerouslySetInnerHTML={{ __html: input }}
532+
dangerouslySetInnerHTML={{
533+
__html: markdownit({ html: true }).render(input)
534+
}}
451535
/>
452536
);

bun.lock

Lines changed: 25 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@
3434
"@radix-ui/react-separator": "1.1.7",
3535
"@radix-ui/react-slot": "1.2.3",
3636
"@radix-ui/react-toast": "1.2.15",
37+
"@types/markdown-it": "^14.1.2",
3738
"class-variance-authority": "0.7.1",
3839
"clsx": "2.1.1",
3940
"framer-motion": "12.23.24",
4041
"lucide-react": "0.546.0",
42+
"markdown-it": "^14.1.0",
4143
"marked": "16.4.1",
4244
"next": "16.0.10",
4345
"next-themes": "0.4.6",

0 commit comments

Comments
 (0)