Spaces:
Runtime error
Runtime error
tech-envision
commited on
Commit
·
a40a50f
1
Parent(s):
71fac34
Add Next.js chat frontend
Browse files- frontend/.env.example +1 -0
- frontend/src/app/page.tsx +2 -100
- frontend/src/components/ChatApp.tsx +92 -0
- frontend/src/utils/api.ts +31 -0
frontend/.env.example
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
NEXT_PUBLIC_API_URL=http://localhost:8000
|
frontend/src/app/page.tsx
CHANGED
@@ -1,103 +1,5 @@
|
|
1 |
-
import
|
2 |
|
3 |
export default function Home() {
|
4 |
-
return
|
5 |
-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
6 |
-
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
7 |
-
<Image
|
8 |
-
className="dark:invert"
|
9 |
-
src="/next.svg"
|
10 |
-
alt="Next.js logo"
|
11 |
-
width={180}
|
12 |
-
height={38}
|
13 |
-
priority
|
14 |
-
/>
|
15 |
-
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
16 |
-
<li className="mb-2 tracking-[-.01em]">
|
17 |
-
Get started by editing{" "}
|
18 |
-
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
19 |
-
src/app/page.tsx
|
20 |
-
</code>
|
21 |
-
.
|
22 |
-
</li>
|
23 |
-
<li className="tracking-[-.01em]">
|
24 |
-
Save and see your changes instantly.
|
25 |
-
</li>
|
26 |
-
</ol>
|
27 |
-
|
28 |
-
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
29 |
-
<a
|
30 |
-
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
31 |
-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
32 |
-
target="_blank"
|
33 |
-
rel="noopener noreferrer"
|
34 |
-
>
|
35 |
-
<Image
|
36 |
-
className="dark:invert"
|
37 |
-
src="/vercel.svg"
|
38 |
-
alt="Vercel logomark"
|
39 |
-
width={20}
|
40 |
-
height={20}
|
41 |
-
/>
|
42 |
-
Deploy now
|
43 |
-
</a>
|
44 |
-
<a
|
45 |
-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
46 |
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
47 |
-
target="_blank"
|
48 |
-
rel="noopener noreferrer"
|
49 |
-
>
|
50 |
-
Read our docs
|
51 |
-
</a>
|
52 |
-
</div>
|
53 |
-
</main>
|
54 |
-
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
55 |
-
<a
|
56 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
57 |
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
58 |
-
target="_blank"
|
59 |
-
rel="noopener noreferrer"
|
60 |
-
>
|
61 |
-
<Image
|
62 |
-
aria-hidden
|
63 |
-
src="/file.svg"
|
64 |
-
alt="File icon"
|
65 |
-
width={16}
|
66 |
-
height={16}
|
67 |
-
/>
|
68 |
-
Learn
|
69 |
-
</a>
|
70 |
-
<a
|
71 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
72 |
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
73 |
-
target="_blank"
|
74 |
-
rel="noopener noreferrer"
|
75 |
-
>
|
76 |
-
<Image
|
77 |
-
aria-hidden
|
78 |
-
src="/window.svg"
|
79 |
-
alt="Window icon"
|
80 |
-
width={16}
|
81 |
-
height={16}
|
82 |
-
/>
|
83 |
-
Examples
|
84 |
-
</a>
|
85 |
-
<a
|
86 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
87 |
-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
88 |
-
target="_blank"
|
89 |
-
rel="noopener noreferrer"
|
90 |
-
>
|
91 |
-
<Image
|
92 |
-
aria-hidden
|
93 |
-
src="/globe.svg"
|
94 |
-
alt="Globe icon"
|
95 |
-
width={16}
|
96 |
-
height={16}
|
97 |
-
/>
|
98 |
-
Go to nextjs.org →
|
99 |
-
</a>
|
100 |
-
</footer>
|
101 |
-
</div>
|
102 |
-
);
|
103 |
}
|
|
|
1 |
+
import ChatApp from "../components/ChatApp";
|
2 |
|
3 |
export default function Home() {
|
4 |
+
return <ChatApp />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
}
|
frontend/src/components/ChatApp.tsx
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { useState, useRef } from 'react';
|
4 |
+
import { streamChat, ChatRequest } from '../utils/api';
|
5 |
+
|
6 |
+
interface Message {
|
7 |
+
role: 'user' | 'assistant';
|
8 |
+
content: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
export default function ChatApp() {
|
12 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
13 |
+
const [input, setInput] = useState('');
|
14 |
+
const [loading, setLoading] = useState(false);
|
15 |
+
const endRef = useRef<HTMLDivElement>(null);
|
16 |
+
|
17 |
+
const scrollToEnd = () => {
|
18 |
+
endRef.current?.scrollIntoView({ behavior: 'smooth' });
|
19 |
+
};
|
20 |
+
|
21 |
+
const sendMessage = async () => {
|
22 |
+
if (!input.trim() || loading) return;
|
23 |
+
|
24 |
+
const prompt = input;
|
25 |
+
setInput('');
|
26 |
+
setMessages((prev) => [...prev, { role: 'user', content: prompt }, { role: 'assistant', content: '' }]);
|
27 |
+
setLoading(true);
|
28 |
+
try {
|
29 |
+
const req: ChatRequest = { user: 'demo', session: 'default', prompt };
|
30 |
+
for await (const chunk of streamChat(req)) {
|
31 |
+
setMessages((prev) => {
|
32 |
+
const msgs = [...prev];
|
33 |
+
msgs[msgs.length - 1] = {
|
34 |
+
role: 'assistant',
|
35 |
+
content: msgs[msgs.length - 1].content + chunk,
|
36 |
+
};
|
37 |
+
return msgs;
|
38 |
+
});
|
39 |
+
scrollToEnd();
|
40 |
+
}
|
41 |
+
} catch (err) {
|
42 |
+
console.error(err);
|
43 |
+
setMessages((prev) => {
|
44 |
+
const msgs = [...prev];
|
45 |
+
msgs[msgs.length - 1] = {
|
46 |
+
role: 'assistant',
|
47 |
+
content: 'Error retrieving response',
|
48 |
+
};
|
49 |
+
return msgs;
|
50 |
+
});
|
51 |
+
} finally {
|
52 |
+
setLoading(false);
|
53 |
+
scrollToEnd();
|
54 |
+
}
|
55 |
+
};
|
56 |
+
|
57 |
+
const onSubmit = (e: React.FormEvent) => {
|
58 |
+
e.preventDefault();
|
59 |
+
void sendMessage();
|
60 |
+
};
|
61 |
+
|
62 |
+
return (
|
63 |
+
<div className="flex flex-col max-w-2xl mx-auto h-screen p-4">
|
64 |
+
<div className="flex-1 overflow-y-auto space-y-4">
|
65 |
+
{messages.map((msg, idx) => (
|
66 |
+
<div key={idx} className={msg.role === 'user' ? 'text-right' : 'text-left'}>
|
67 |
+
<span className="px-3 py-2 inline-block rounded bg-gray-200 dark:bg-gray-700">
|
68 |
+
{msg.content}
|
69 |
+
</span>
|
70 |
+
</div>
|
71 |
+
))}
|
72 |
+
<div ref={endRef} />
|
73 |
+
</div>
|
74 |
+
<form onSubmit={onSubmit} className="mt-4 flex gap-2">
|
75 |
+
<input
|
76 |
+
type="text"
|
77 |
+
className="flex-1 border rounded px-3 py-2 text-black"
|
78 |
+
value={input}
|
79 |
+
onChange={(e) => setInput(e.target.value)}
|
80 |
+
placeholder="Type your message..."
|
81 |
+
/>
|
82 |
+
<button
|
83 |
+
type="submit"
|
84 |
+
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
85 |
+
disabled={loading}
|
86 |
+
>
|
87 |
+
Send
|
88 |
+
</button>
|
89 |
+
</form>
|
90 |
+
</div>
|
91 |
+
);
|
92 |
+
}
|
frontend/src/utils/api.ts
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface ChatRequest {
|
2 |
+
user: string;
|
3 |
+
session: string;
|
4 |
+
prompt: string;
|
5 |
+
}
|
6 |
+
|
7 |
+
export async function* streamChat(
|
8 |
+
req: ChatRequest
|
9 |
+
): AsyncGenerator<string> {
|
10 |
+
const url = `${process.env.NEXT_PUBLIC_API_URL}/chat/stream`;
|
11 |
+
const res = await fetch(url, {
|
12 |
+
method: 'POST',
|
13 |
+
headers: {
|
14 |
+
'Content-Type': 'application/json',
|
15 |
+
},
|
16 |
+
body: JSON.stringify(req),
|
17 |
+
});
|
18 |
+
|
19 |
+
if (!res.ok || !res.body) {
|
20 |
+
throw new Error('API request failed');
|
21 |
+
}
|
22 |
+
|
23 |
+
const reader = res.body.getReader();
|
24 |
+
const decoder = new TextDecoder();
|
25 |
+
|
26 |
+
while (true) {
|
27 |
+
const { value, done } = await reader.read();
|
28 |
+
if (done) break;
|
29 |
+
yield decoder.decode(value);
|
30 |
+
}
|
31 |
+
}
|