Spaces:
Runtime error
Runtime error
Merge pull request #79 from EnvisionMindCa/codex/implement-glassmorphism-design
Browse files- frontend/src/app/globals.css +11 -2
- frontend/src/app/layout.tsx +1 -14
- frontend/src/components/ChatApp.tsx +15 -46
- frontend/src/components/MessageInput.tsx +31 -0
- frontend/src/components/MessageItem.tsx +21 -0
- frontend/src/components/MessageList.tsx +20 -0
- frontend/src/components/ui/GlassButton.tsx +16 -0
frontend/src/app/globals.css
CHANGED
@@ -8,8 +8,8 @@
|
|
8 |
@theme inline {
|
9 |
--color-background: var(--background);
|
10 |
--color-foreground: var(--foreground);
|
11 |
-
--font-sans:
|
12 |
-
--font-mono:
|
13 |
}
|
14 |
|
15 |
@media (prefers-color-scheme: dark) {
|
@@ -24,3 +24,12 @@ body {
|
|
24 |
color: var(--foreground);
|
25 |
font-family: Arial, Helvetica, sans-serif;
|
26 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
@theme inline {
|
9 |
--color-background: var(--background);
|
10 |
--color-foreground: var(--foreground);
|
11 |
+
--font-sans: ui-sans-serif, system-ui, sans-serif;
|
12 |
+
--font-mono: ui-monospace, monospace;
|
13 |
}
|
14 |
|
15 |
@media (prefers-color-scheme: dark) {
|
|
|
24 |
color: var(--foreground);
|
25 |
font-family: Arial, Helvetica, sans-serif;
|
26 |
}
|
27 |
+
|
28 |
+
@keyframes fadeIn {
|
29 |
+
from { opacity: 0; transform: translateY(5px); }
|
30 |
+
to { opacity: 1; transform: translateY(0); }
|
31 |
+
}
|
32 |
+
|
33 |
+
.animate-fadeIn {
|
34 |
+
animation: fadeIn 0.3s ease-in-out;
|
35 |
+
}
|
frontend/src/app/layout.tsx
CHANGED
@@ -1,17 +1,6 @@
|
|
1 |
import type { Metadata } from "next";
|
2 |
-
import { Geist, Geist_Mono } from "next/font/google";
|
3 |
import "./globals.css";
|
4 |
|
5 |
-
const geistSans = Geist({
|
6 |
-
variable: "--font-geist-sans",
|
7 |
-
subsets: ["latin"],
|
8 |
-
});
|
9 |
-
|
10 |
-
const geistMono = Geist_Mono({
|
11 |
-
variable: "--font-geist-mono",
|
12 |
-
subsets: ["latin"],
|
13 |
-
});
|
14 |
-
|
15 |
export const metadata: Metadata = {
|
16 |
title: "Create Next App",
|
17 |
description: "Generated by create next app",
|
@@ -24,9 +13,7 @@ export default function RootLayout({
|
|
24 |
}>) {
|
25 |
return (
|
26 |
<html lang="en">
|
27 |
-
<body
|
28 |
-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
29 |
-
>
|
30 |
{children}
|
31 |
</body>
|
32 |
</html>
|
|
|
1 |
import type { Metadata } from "next";
|
|
|
2 |
import "./globals.css";
|
3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
export const metadata: Metadata = {
|
5 |
title: "Create Next App",
|
6 |
description: "Generated by create next app",
|
|
|
13 |
}>) {
|
14 |
return (
|
15 |
<html lang="en">
|
16 |
+
<body className="antialiased">
|
|
|
|
|
17 |
{children}
|
18 |
</body>
|
19 |
</html>
|
frontend/src/components/ChatApp.tsx
CHANGED
@@ -1,26 +1,27 @@
|
|
1 |
'use client';
|
2 |
|
3 |
-
import { useState, useRef } from 'react';
|
4 |
import { streamChat, ChatRequest } from '../utils/api';
|
5 |
-
|
6 |
-
|
7 |
-
|
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
|
16 |
|
17 |
const scrollToEnd = () => {
|
18 |
-
|
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: '' }]);
|
@@ -36,57 +37,25 @@ export default function ChatApp() {
|
|
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
|
64 |
-
<div className="flex-1
|
65 |
-
{messages
|
66 |
-
|
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 |
}
|
|
|
1 |
'use client';
|
2 |
|
3 |
+
import { useState, useRef, useEffect } from 'react';
|
4 |
import { streamChat, ChatRequest } from '../utils/api';
|
5 |
+
import MessageList from './MessageList';
|
6 |
+
import MessageInput from './MessageInput';
|
7 |
+
import { Message } from './MessageItem';
|
|
|
|
|
8 |
|
9 |
export default function ChatApp() {
|
10 |
const [messages, setMessages] = useState<Message[]>([]);
|
11 |
const [input, setInput] = useState('');
|
12 |
const [loading, setLoading] = useState(false);
|
13 |
+
const listRef = useRef<HTMLDivElement>(null);
|
14 |
|
15 |
const scrollToEnd = () => {
|
16 |
+
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' });
|
17 |
};
|
18 |
|
19 |
+
useEffect(() => {
|
20 |
+
scrollToEnd();
|
21 |
+
}, [messages]);
|
22 |
+
|
23 |
const sendMessage = async () => {
|
24 |
if (!input.trim() || loading) return;
|
|
|
25 |
const prompt = input;
|
26 |
setInput('');
|
27 |
setMessages((prev) => [...prev, { role: 'user', content: prompt }, { role: 'assistant', content: '' }]);
|
|
|
37 |
};
|
38 |
return msgs;
|
39 |
});
|
|
|
40 |
}
|
41 |
} catch (err) {
|
42 |
console.error(err);
|
43 |
setMessages((prev) => {
|
44 |
const msgs = [...prev];
|
45 |
+
msgs[msgs.length - 1] = { role: 'assistant', content: 'Error retrieving response' };
|
|
|
|
|
|
|
46 |
return msgs;
|
47 |
});
|
48 |
} finally {
|
49 |
setLoading(false);
|
|
|
50 |
}
|
51 |
};
|
52 |
|
|
|
|
|
|
|
|
|
|
|
53 |
return (
|
54 |
+
<div className="flex flex-col items-center h-screen bg-gradient-to-br from-gray-950/60 to-gray-800/60 text-white p-4">
|
55 |
+
<div className="w-full max-w-3xl flex flex-col flex-1 backdrop-blur-lg rounded-lg border border-white/20 bg-white/10 shadow-xl overflow-hidden">
|
56 |
+
<MessageList ref={listRef} messages={messages} />
|
57 |
+
<MessageInput value={input} onChange={setInput} onSend={sendMessage} disabled={loading} />
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
</div>
|
60 |
);
|
61 |
}
|
frontend/src/components/MessageInput.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import GlassButton from './ui/GlassButton';
|
3 |
+
|
4 |
+
interface MessageInputProps {
|
5 |
+
value: string;
|
6 |
+
onChange: (value: string) => void;
|
7 |
+
onSend: () => void;
|
8 |
+
disabled?: boolean;
|
9 |
+
}
|
10 |
+
|
11 |
+
export default function MessageInput({ value, onChange, onSend, disabled }: MessageInputProps) {
|
12 |
+
const handleSubmit = (e: React.FormEvent) => {
|
13 |
+
e.preventDefault();
|
14 |
+
onSend();
|
15 |
+
};
|
16 |
+
|
17 |
+
return (
|
18 |
+
<form onSubmit={handleSubmit} className="flex gap-2 p-2">
|
19 |
+
<input
|
20 |
+
type="text"
|
21 |
+
className="flex-1 bg-white/20 backdrop-blur-md border border-white/30 text-white px-3 py-2 rounded-md focus:outline-none"
|
22 |
+
value={value}
|
23 |
+
onChange={(e) => onChange(e.target.value)}
|
24 |
+
placeholder="Type your message..."
|
25 |
+
/>
|
26 |
+
<GlassButton type="submit" disabled={disabled}>
|
27 |
+
Send
|
28 |
+
</GlassButton>
|
29 |
+
</form>
|
30 |
+
);
|
31 |
+
}
|
frontend/src/components/MessageItem.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
export interface Message {
|
4 |
+
role: 'user' | 'assistant';
|
5 |
+
content: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
interface MessageItemProps {
|
9 |
+
message: Message;
|
10 |
+
}
|
11 |
+
|
12 |
+
export default function MessageItem({ message }: MessageItemProps) {
|
13 |
+
const alignment = message.role === 'user' ? 'items-end' : 'items-start';
|
14 |
+
return (
|
15 |
+
<div className={`flex flex-col ${alignment} animate-fadeIn`}>
|
16 |
+
<div className="w-full max-w-xl p-3 my-2 bg-white/10 border border-white/30 backdrop-blur-lg rounded-lg shadow-md">
|
17 |
+
<p className="whitespace-pre-wrap">{message.content}</p>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
);
|
21 |
+
}
|
frontend/src/components/MessageList.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { forwardRef } from 'react';
|
2 |
+
import MessageItem, { Message } from './MessageItem';
|
3 |
+
|
4 |
+
interface MessageListProps {
|
5 |
+
messages: Message[];
|
6 |
+
}
|
7 |
+
|
8 |
+
const MessageList = forwardRef<HTMLDivElement, MessageListProps>(({ messages }, ref) => {
|
9 |
+
return (
|
10 |
+
<div className="flex-1 overflow-y-auto p-2" ref={ref}>
|
11 |
+
{messages.map((msg, idx) => (
|
12 |
+
<MessageItem key={idx} message={msg} />
|
13 |
+
))}
|
14 |
+
</div>
|
15 |
+
);
|
16 |
+
});
|
17 |
+
|
18 |
+
MessageList.displayName = 'MessageList';
|
19 |
+
|
20 |
+
export default MessageList;
|
frontend/src/components/ui/GlassButton.tsx
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
interface GlassButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
4 |
+
children: React.ReactNode;
|
5 |
+
}
|
6 |
+
|
7 |
+
export default function GlassButton({ children, className = '', ...props }: GlassButtonProps) {
|
8 |
+
return (
|
9 |
+
<button
|
10 |
+
className={`bg-white/20 backdrop-blur-md border border-white/30 text-white px-4 py-2 rounded-md shadow-lg transition-all hover:bg-white/30 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
|
11 |
+
{...props}
|
12 |
+
>
|
13 |
+
{children}
|
14 |
+
</button>
|
15 |
+
);
|
16 |
+
}
|