Spaces:
Runtime error
Runtime error
Merge pull request #73 from EnvisionMindCa/codex/code-modern-react.js-front-end-with-glassmorphism
Browse files- frontend/.gitignore +24 -0
- frontend/README.md +12 -0
- frontend/eslint.config.js +33 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +28 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +42 -0
- frontend/src/App.jsx +8 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/ChatWindow.jsx +47 -0
- frontend/src/components/InputBar.jsx +31 -0
- frontend/src/components/MessageBubble.jsx +13 -0
- frontend/src/components/MessageList.jsx +23 -0
- frontend/src/index.css +20 -0
- frontend/src/main.jsx +10 -0
- frontend/src/styles/chat.css +87 -0
- frontend/vite.config.js +7 -0
frontend/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
frontend/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# React + Vite
|
2 |
+
|
3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
4 |
+
|
5 |
+
Currently, two official plugins are available:
|
6 |
+
|
7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
9 |
+
|
10 |
+
## Expanding the ESLint configuration
|
11 |
+
|
12 |
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/eslint.config.js
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import js from '@eslint/js'
|
2 |
+
import globals from 'globals'
|
3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
5 |
+
|
6 |
+
export default [
|
7 |
+
{ ignores: ['dist'] },
|
8 |
+
{
|
9 |
+
files: ['**/*.{js,jsx}'],
|
10 |
+
languageOptions: {
|
11 |
+
ecmaVersion: 2020,
|
12 |
+
globals: globals.browser,
|
13 |
+
parserOptions: {
|
14 |
+
ecmaVersion: 'latest',
|
15 |
+
ecmaFeatures: { jsx: true },
|
16 |
+
sourceType: 'module',
|
17 |
+
},
|
18 |
+
},
|
19 |
+
plugins: {
|
20 |
+
'react-hooks': reactHooks,
|
21 |
+
'react-refresh': reactRefresh,
|
22 |
+
},
|
23 |
+
rules: {
|
24 |
+
...js.configs.recommended.rules,
|
25 |
+
...reactHooks.configs.recommended.rules,
|
26 |
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
27 |
+
'react-refresh/only-export-components': [
|
28 |
+
'warn',
|
29 |
+
{ allowConstantExport: true },
|
30 |
+
],
|
31 |
+
},
|
32 |
+
},
|
33 |
+
]
|
frontend/index.html
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>LLM Chat</title>
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<div id="root"></div>
|
11 |
+
<script type="module" src="/src/main.jsx"></script>
|
12 |
+
</body>
|
13 |
+
</html>
|
frontend/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
frontend/package.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "frontend",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"lint": "eslint .",
|
10 |
+
"preview": "vite preview"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"prop-types": "^15.8.1",
|
14 |
+
"react": "^19.1.0",
|
15 |
+
"react-dom": "^19.1.0"
|
16 |
+
},
|
17 |
+
"devDependencies": {
|
18 |
+
"@eslint/js": "^9.25.0",
|
19 |
+
"@types/react": "^19.1.2",
|
20 |
+
"@types/react-dom": "^19.1.2",
|
21 |
+
"@vitejs/plugin-react": "^4.4.1",
|
22 |
+
"eslint": "^9.25.0",
|
23 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
24 |
+
"eslint-plugin-react-refresh": "^0.4.19",
|
25 |
+
"globals": "^16.0.0",
|
26 |
+
"vite": "^6.3.5"
|
27 |
+
}
|
28 |
+
}
|
frontend/public/vite.svg
ADDED
|
frontend/src/App.css
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#root {
|
2 |
+
max-width: 1280px;
|
3 |
+
margin: 0 auto;
|
4 |
+
padding: 2rem;
|
5 |
+
text-align: center;
|
6 |
+
}
|
7 |
+
|
8 |
+
.logo {
|
9 |
+
height: 6em;
|
10 |
+
padding: 1.5em;
|
11 |
+
will-change: filter;
|
12 |
+
transition: filter 300ms;
|
13 |
+
}
|
14 |
+
.logo:hover {
|
15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
16 |
+
}
|
17 |
+
.logo.react:hover {
|
18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
19 |
+
}
|
20 |
+
|
21 |
+
@keyframes logo-spin {
|
22 |
+
from {
|
23 |
+
transform: rotate(0deg);
|
24 |
+
}
|
25 |
+
to {
|
26 |
+
transform: rotate(360deg);
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
@media (prefers-reduced-motion: no-preference) {
|
31 |
+
a:nth-of-type(2) .logo {
|
32 |
+
animation: logo-spin infinite 20s linear;
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
.card {
|
37 |
+
padding: 2em;
|
38 |
+
}
|
39 |
+
|
40 |
+
.read-the-docs {
|
41 |
+
color: #888;
|
42 |
+
}
|
frontend/src/App.jsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import ChatWindow from './components/ChatWindow'
|
2 |
+
import './index.css'
|
3 |
+
|
4 |
+
function App() {
|
5 |
+
return <ChatWindow />
|
6 |
+
}
|
7 |
+
|
8 |
+
export default App
|
frontend/src/assets/react.svg
ADDED
|
frontend/src/components/ChatWindow.jsx
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState } from 'react'
|
2 |
+
import MessageList from './MessageList'
|
3 |
+
import InputBar from './InputBar'
|
4 |
+
import '../styles/chat.css'
|
5 |
+
|
6 |
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/chat/stream'
|
7 |
+
|
8 |
+
async function fetchStream(prompt, onChunk) {
|
9 |
+
const res = await fetch(API_URL, {
|
10 |
+
method: 'POST',
|
11 |
+
headers: { 'Content-Type': 'application/json' },
|
12 |
+
body: JSON.stringify({ user: 'demo', session: 'default', prompt }),
|
13 |
+
})
|
14 |
+
const reader = res.body.getReader()
|
15 |
+
const decoder = new TextDecoder()
|
16 |
+
while (true) {
|
17 |
+
const { value, done } = await reader.read()
|
18 |
+
if (done) break
|
19 |
+
onChunk(decoder.decode(value))
|
20 |
+
}
|
21 |
+
}
|
22 |
+
|
23 |
+
function ChatWindow() {
|
24 |
+
const [messages, setMessages] = useState([])
|
25 |
+
|
26 |
+
const sendMessage = async (text) => {
|
27 |
+
const userMsg = { role: 'user', content: text }
|
28 |
+
setMessages((prev) => [...prev, userMsg, { role: 'assistant', content: '' }])
|
29 |
+
const index = messages.length + 1
|
30 |
+
await fetchStream(text, (chunk) => {
|
31 |
+
setMessages((prev) => {
|
32 |
+
const copy = [...prev]
|
33 |
+
copy[index] = { ...copy[index], content: copy[index].content + chunk }
|
34 |
+
return copy
|
35 |
+
})
|
36 |
+
})
|
37 |
+
}
|
38 |
+
|
39 |
+
return (
|
40 |
+
<div className="chat-container">
|
41 |
+
<MessageList messages={messages} />
|
42 |
+
<InputBar onSend={sendMessage} />
|
43 |
+
</div>
|
44 |
+
)
|
45 |
+
}
|
46 |
+
|
47 |
+
export default ChatWindow
|
frontend/src/components/InputBar.jsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState } from 'react'
|
2 |
+
import PropTypes from 'prop-types'
|
3 |
+
|
4 |
+
function InputBar({ onSend }) {
|
5 |
+
const [text, setText] = useState('')
|
6 |
+
|
7 |
+
const handleSubmit = (e) => {
|
8 |
+
e.preventDefault()
|
9 |
+
if (!text.trim()) return
|
10 |
+
onSend(text)
|
11 |
+
setText('')
|
12 |
+
}
|
13 |
+
|
14 |
+
return (
|
15 |
+
<form className="input-bar" onSubmit={handleSubmit}>
|
16 |
+
<input
|
17 |
+
type="text"
|
18 |
+
value={text}
|
19 |
+
onChange={(e) => setText(e.target.value)}
|
20 |
+
placeholder="Type a message..."
|
21 |
+
/>
|
22 |
+
<button type="submit">Send</button>
|
23 |
+
</form>
|
24 |
+
)
|
25 |
+
}
|
26 |
+
|
27 |
+
InputBar.propTypes = {
|
28 |
+
onSend: PropTypes.func.isRequired,
|
29 |
+
}
|
30 |
+
|
31 |
+
export default InputBar
|
frontend/src/components/MessageBubble.jsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import PropTypes from 'prop-types'
|
2 |
+
import '../styles/chat.css'
|
3 |
+
|
4 |
+
function MessageBubble({ role, content }) {
|
5 |
+
return <div className={`message ${role}`}>{content}</div>
|
6 |
+
}
|
7 |
+
|
8 |
+
MessageBubble.propTypes = {
|
9 |
+
role: PropTypes.string.isRequired,
|
10 |
+
content: PropTypes.string.isRequired,
|
11 |
+
}
|
12 |
+
|
13 |
+
export default MessageBubble
|
frontend/src/components/MessageList.jsx
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import PropTypes from 'prop-types'
|
2 |
+
import MessageBubble from './MessageBubble'
|
3 |
+
|
4 |
+
function MessageList({ messages }) {
|
5 |
+
return (
|
6 |
+
<div className="message-list">
|
7 |
+
{messages.map((m, i) => (
|
8 |
+
<MessageBubble key={i} role={m.role} content={m.content} />
|
9 |
+
))}
|
10 |
+
</div>
|
11 |
+
)
|
12 |
+
}
|
13 |
+
|
14 |
+
MessageList.propTypes = {
|
15 |
+
messages: PropTypes.arrayOf(
|
16 |
+
PropTypes.shape({
|
17 |
+
role: PropTypes.string.isRequired,
|
18 |
+
content: PropTypes.string.isRequired,
|
19 |
+
})
|
20 |
+
).isRequired,
|
21 |
+
}
|
22 |
+
|
23 |
+
export default MessageList
|
frontend/src/index.css
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--glass-bg: rgba(255, 255, 255, 0.2);
|
3 |
+
--glass-border: rgba(255, 255, 255, 0.4);
|
4 |
+
--blur: 16px;
|
5 |
+
}
|
6 |
+
|
7 |
+
* {
|
8 |
+
box-sizing: border-box;
|
9 |
+
}
|
10 |
+
|
11 |
+
body {
|
12 |
+
margin: 0;
|
13 |
+
padding: 0;
|
14 |
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
15 |
+
background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
|
16 |
+
display: flex;
|
17 |
+
justify-content: center;
|
18 |
+
align-items: center;
|
19 |
+
min-height: 100vh;
|
20 |
+
}
|
frontend/src/main.jsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { StrictMode } from 'react'
|
2 |
+
import { createRoot } from 'react-dom/client'
|
3 |
+
import './index.css'
|
4 |
+
import App from './App.jsx'
|
5 |
+
|
6 |
+
createRoot(document.getElementById('root')).render(
|
7 |
+
<StrictMode>
|
8 |
+
<App />
|
9 |
+
</StrictMode>,
|
10 |
+
)
|
frontend/src/styles/chat.css
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.chat-container {
|
2 |
+
width: 420px;
|
3 |
+
max-width: 90vw;
|
4 |
+
height: 600px;
|
5 |
+
display: flex;
|
6 |
+
flex-direction: column;
|
7 |
+
background: var(--glass-bg);
|
8 |
+
border: 1px solid var(--glass-border);
|
9 |
+
backdrop-filter: blur(var(--blur)) saturate(180%);
|
10 |
+
-webkit-backdrop-filter: blur(var(--blur)) saturate(180%);
|
11 |
+
border-radius: 16px;
|
12 |
+
padding: 1rem;
|
13 |
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
14 |
+
}
|
15 |
+
|
16 |
+
.message-list {
|
17 |
+
flex: 1;
|
18 |
+
overflow-y: auto;
|
19 |
+
display: flex;
|
20 |
+
flex-direction: column;
|
21 |
+
gap: 0.5rem;
|
22 |
+
padding-right: 4px;
|
23 |
+
}
|
24 |
+
|
25 |
+
.message {
|
26 |
+
width: fit-content;
|
27 |
+
max-width: 80%;
|
28 |
+
padding: 0.5rem 1rem;
|
29 |
+
background: var(--glass-bg);
|
30 |
+
border: 1px solid var(--glass-border);
|
31 |
+
border-radius: 20px;
|
32 |
+
backdrop-filter: blur(var(--blur));
|
33 |
+
-webkit-backdrop-filter: blur(var(--blur));
|
34 |
+
animation: jelly 0.5s ease;
|
35 |
+
}
|
36 |
+
|
37 |
+
.message.user {
|
38 |
+
align-self: flex-end;
|
39 |
+
}
|
40 |
+
|
41 |
+
.input-bar {
|
42 |
+
display: flex;
|
43 |
+
gap: 0.5rem;
|
44 |
+
margin-top: 0.5rem;
|
45 |
+
}
|
46 |
+
|
47 |
+
.input-bar input {
|
48 |
+
flex: 1;
|
49 |
+
padding: 0.5rem 1rem;
|
50 |
+
border: 1px solid var(--glass-border);
|
51 |
+
background: var(--glass-bg);
|
52 |
+
border-radius: 20px;
|
53 |
+
backdrop-filter: blur(var(--blur));
|
54 |
+
-webkit-backdrop-filter: blur(var(--blur));
|
55 |
+
color: #000;
|
56 |
+
}
|
57 |
+
|
58 |
+
.input-bar button {
|
59 |
+
padding: 0.5rem 1rem;
|
60 |
+
border: none;
|
61 |
+
border-radius: 20px;
|
62 |
+
background: rgba(255, 255, 255, 0.6);
|
63 |
+
color: #000;
|
64 |
+
font-weight: bold;
|
65 |
+
cursor: pointer;
|
66 |
+
transition: transform 0.2s;
|
67 |
+
}
|
68 |
+
|
69 |
+
.input-bar button:hover {
|
70 |
+
transform: scale(1.05);
|
71 |
+
}
|
72 |
+
|
73 |
+
.input-bar button:active {
|
74 |
+
transform: scale(0.95);
|
75 |
+
}
|
76 |
+
|
77 |
+
@keyframes jelly {
|
78 |
+
0% {
|
79 |
+
transform: scale(0.8);
|
80 |
+
}
|
81 |
+
50% {
|
82 |
+
transform: scale(1.1);
|
83 |
+
}
|
84 |
+
100% {
|
85 |
+
transform: scale(1);
|
86 |
+
}
|
87 |
+
}
|
frontend/vite.config.js
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vite'
|
2 |
+
import react from '@vitejs/plugin-react'
|
3 |
+
|
4 |
+
// https://vite.dev/config/
|
5 |
+
export default defineConfig({
|
6 |
+
plugins: [react()],
|
7 |
+
})
|