Spaces:
Running
Running
Upload 21 files
Browse files- .env.example +6 -0
- Dockerfile +20 -44
- frontend/index.html +1 -0
- frontend/package.json +24 -0
- frontend/postcss.config.js +1 -0
- frontend/src/App.tsx +23 -0
- frontend/src/components/ChatBox.tsx +8 -0
- frontend/src/components/Loader.tsx +1 -0
- frontend/src/components/Onboarding.tsx +2 -0
- frontend/src/components/Sidebar.tsx +2 -0
- frontend/src/main.tsx +12 -0
- frontend/src/pages/About.tsx +2 -0
- frontend/src/pages/ApiKeys.tsx +3 -0
- frontend/src/pages/Chat.tsx +4 -0
- frontend/src/pages/HowToUse.tsx +2 -0
- frontend/src/pages/Login.tsx +3 -0
- frontend/src/pages/NotFound.tsx +1 -0
- frontend/src/pages/Signup.tsx +3 -0
- frontend/src/styles.css +1 -0
- frontend/tailwind.config.js +1 -0
- frontend/vite.config.js +3 -0
.env.example
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Backend config
|
2 |
+
DATABASE_URL=sqlite:///./app.db
|
3 |
+
JWT_SECRET=your_super_secret_key_here
|
4 |
+
JWT_EXPIRES_MINUTES=60
|
5 |
+
APP_NAME=CHB App
|
6 |
+
CORS_ORIGINS=*
|
Dockerfile
CHANGED
@@ -1,46 +1,22 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
libgl1 \
|
14 |
-
libglib2.0-0 \
|
15 |
-
libsndfile1 \
|
16 |
-
libsm6 \
|
17 |
-
libxext6 \
|
18 |
-
libxrender1 \
|
19 |
-
git \
|
20 |
-
git-lfs \
|
21 |
-
cmake \
|
22 |
-
rsync \
|
23 |
-
&& rm -rf /var/lib/apt/lists/* \
|
24 |
-
&& git lfs install
|
25 |
-
|
26 |
-
# --- Install Python build tools early for speed ---
|
27 |
-
RUN pip install --upgrade pip setuptools wheel
|
28 |
-
|
29 |
-
# --- Copy only requirements first for better caching ---
|
30 |
-
COPY requirements.txt /app/requirements.txt
|
31 |
-
|
32 |
-
# Install dependencies with faster resolution
|
33 |
-
RUN pip install --upgrade pip \
|
34 |
-
&& pip install --prefer-binary --use-deprecated=legacy-resolver -r /app/requirements.txt
|
35 |
-
|
36 |
-
# --- Copy application code ---
|
37 |
WORKDIR /app
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
43 |
EXPOSE 7860
|
44 |
-
|
45 |
-
CMD ["python", "app.py", "--api", "--host", "0.0.0.0", "--port", "7860"]
|
46 |
-
|
|
|
1 |
+
# Build frontend (React + Vite + Tailwind)
|
2 |
+
FROM node:20-alpine AS frontend
|
3 |
+
WORKDIR /ui
|
4 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
5 |
+
RUN npm ci --silent
|
6 |
+
COPY frontend ./
|
7 |
+
RUN npm run build
|
8 |
+
|
9 |
+
# Backend image
|
10 |
+
FROM python:3.11-slim
|
11 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
12 |
+
ENV PYTHONUNBUFFERED=1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
WORKDIR /app
|
14 |
+
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates git && rm -rf /var/lib/apt/lists/*
|
15 |
+
COPY requirements.txt ./
|
16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
17 |
+
COPY backend ./backend
|
18 |
+
COPY app.py Procfile .env.example ./
|
19 |
+
# Bring built frontend
|
20 |
+
COPY --from=frontend /ui/dist ./frontend_dist
|
21 |
EXPOSE 7860
|
22 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
frontend/index.html
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>CHB</title></head><body class="bg-slate-900 text-slate-100"><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
frontend/package.json
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "chb-frontend",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"private": true,
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"preview": "vite preview"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"react": "^18.2.0",
|
13 |
+
"react-dom": "^18.2.0",
|
14 |
+
"react-router-dom": "^6.23.1",
|
15 |
+
"framer-motion": "^11.2.10"
|
16 |
+
},
|
17 |
+
"devDependencies": {
|
18 |
+
"typescript": "^5.5.4",
|
19 |
+
"vite": "^5.3.3",
|
20 |
+
"tailwindcss": "^3.4.7",
|
21 |
+
"postcss": "^8.4.41",
|
22 |
+
"autoprefixer": "^10.4.20"
|
23 |
+
}
|
24 |
+
}
|
frontend/postcss.config.js
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
|
frontend/src/App.tsx
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Routes, Route, Navigate } from 'react-router-dom'
|
2 |
+
import Login from './pages/Login'
|
3 |
+
import Signup from './pages/Signup'
|
4 |
+
import Chat from './pages/Chat'
|
5 |
+
import ApiKeys from './pages/ApiKeys'
|
6 |
+
import About from './pages/About'
|
7 |
+
import HowToUse from './pages/HowToUse'
|
8 |
+
import NotFound from './pages/NotFound'
|
9 |
+
const isAuthed = () => !!localStorage.getItem('token')
|
10 |
+
export default function App(){
|
11 |
+
return (
|
12 |
+
<Routes>
|
13 |
+
<Route path="/" element={isAuthed()?<Navigate to="/chat"/>:<Navigate to="/login"/>} />
|
14 |
+
<Route path="/login" element={<Login/>} />
|
15 |
+
<Route path="/signup" element={<Signup/>} />
|
16 |
+
<Route path="/chat" element={isAuthed()?<Chat/>:<Navigate to="/login"/>} />
|
17 |
+
<Route path="/apikeys" element={isAuthed()?<ApiKeys/>:<Navigate to="/login"/>} />
|
18 |
+
<Route path="/about" element={<About/>} />
|
19 |
+
<Route path="/howto" element={<HowToUse/>} />
|
20 |
+
<Route path="*" element={<NotFound/>} />
|
21 |
+
</Routes>
|
22 |
+
)
|
23 |
+
}
|
frontend/src/components/ChatBox.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useRef, useState } from 'react'
|
2 |
+
const API=(p:string)=>'/api'+p
|
3 |
+
type Msg={role:'user'|'assistant',content:string}
|
4 |
+
export default function ChatBox(){const [messages,setMessages]=useState<Msg[]>([]);const [text,setText]=useState('');const [mode,setMode]=useState<'file'|'mic'|'video'>('file');const pressTimer=useRef<number|undefined>(undefined);const feedRef=useRef<HTMLDivElement>(null);const token=localStorage.getItem('token')||'';const headers={'Content-Type':'application/json','Authorization':'Bearer '+token};useEffect(()=>{fetch(API('/chat/history'),{headers}).then(r=>r.json()).then((data)=>{setMessages(data.map((m:any)=>({role:m.role,content:m.content})))})},[]);useEffect(()=>{feedRef.current?.scrollTo({top:feedRef.current.scrollHeight})},[messages]);const send=async()=>{if(!text.trim()) return; setMessages(m=>[...m,{role:'user',content:text}]); setText(''); const r=await fetch(API('/chat'),{method:'POST',headers,body:JSON.stringify({message:text})}); const data=await r.json(); setMessages(m=>[...m,{role:'assistant',content:data.reply}])}
|
5 |
+
const cycle=()=>setMode(m=> m==='file'?'mic': m==='mic'?'video':'file')
|
6 |
+
const onMouseDown=()=>{pressTimer.current=window.setTimeout(cycle,450)}
|
7 |
+
const onMouseUp=()=>{if(pressTimer.current){clearTimeout(pressTimer.current); pressTimer.current=undefined}}
|
8 |
+
return (<div className="flex flex-col h-full"><div ref={feedRef} className="flex-1 overflow-y-auto p-3 border border-slate-800 rounded-xl bg-slate-900/40 space-y-2">{messages.map((m,i)=>(<div key={i} className={`max-w-[70%] px-3 py-2 rounded-2xl ${m.role==='user'?'self-end bg-blue-600 text-white rounded-br-sm':'self-start bg-slate-800 border border-slate-700 rounded-bl-sm'}`}>{m.content}</div>))}</div><div className="grid grid-cols-[44px_1fr_60px] gap-2 mt-2 items-center"><button onMouseDown={onMouseDown} onMouseUp={onMouseUp} onTouchStart={onMouseDown} onTouchEnd={onMouseUp} onClick={()=>{if(mode==='file'){alert('Upload placeholder')}else if(mode==='mic'){alert('Record audio placeholder')}else{alert('Record video placeholder')}}} className="w-11 h-11 rounded-full border border-slate-700 bg-slate-800">{mode==='file'?'π':mode==='mic'?'π€':'πΉ'}</button><input value={text} onChange={e=>setText((e.target as any).value)} placeholder="Type a message" className="px-3 py-2 rounded-xl border border-slate-700 bg-slate-800 outline-none"/><button onClick={send} className="px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Send</button></div></div>)}
|
frontend/src/components/Loader.tsx
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default function Loader(){return (<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50"><div className="w-14 h-14 rounded-full border-4 border-white/30 border-t-[color:var(--primary)] animate-spin"/></div>)}
|
frontend/src/components/Onboarding.tsx
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from 'react'
|
2 |
+
export default function Onboarding(){const [show,setShow]=useState(false);useEffect(()=>{if(localStorage.getItem('first_time')==='1'){setShow(true)}},[]); if(!show) return null; return (<div className="fixed inset-0 z-40"><div className="absolute inset-0 bg-black/40"/><div className="absolute left-64 top-24 p-3 rounded-xl border border-slate-600 bg-slate-800">Tap here to open Chat</div><div className="absolute left-56 top-20 w-12 h-12 rounded-full bg-amber-200 shadow animate-pulse"/><button onClick={()=>{localStorage.removeItem('first_time'); setShow(false)}} className="absolute bottom-6 right-6 px-3 py-2 rounded-xl border border-slate-600 bg-slate-800">Skip</button></div>)}
|
frontend/src/components/Sidebar.tsx
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
import { Link, useLocation } from 'react-router-dom'
|
2 |
+
export default function Sidebar(){const { pathname } = useLocation(); const item=(to:string,label:string)=> (<Link to={to} className={`block px-3 py-2 rounded-xl border mb-2 ${pathname===to?'border-sky-400 bg-sky-400/10':'border-slate-700 bg-slate-800/40'}`}>{label}</Link>); return (<aside className="w-64 p-4 border-r border-slate-800 bg-slate-900/60"><div className="font-extrabold tracking-widest text-xl mb-3">CHB</div>{item('/chat','π¬ Chat')}{item('/apikeys','π API Keys')}{item('/about','βΉοΈ About Us')}{item('/howto','β How to Use')}<button onClick={()=>{localStorage.removeItem('token'); location.href='/login'}} className="mt-2 w-full px-3 py-2 rounded-xl border border-red-700 bg-red-900/40">β Logout</button></aside>)}
|
frontend/src/main.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react'
|
2 |
+
import ReactDOM from 'react-dom/client'
|
3 |
+
import { BrowserRouter } from 'react-router-dom'
|
4 |
+
import App from './App'
|
5 |
+
import './styles.css'
|
6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
7 |
+
<React.StrictMode>
|
8 |
+
<BrowserRouter>
|
9 |
+
<App />
|
10 |
+
</BrowserRouter>
|
11 |
+
</React.StrictMode>
|
12 |
+
)
|
frontend/src/pages/About.tsx
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
import Sidebar from '../components/Sidebar'
|
2 |
+
export default function About(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><h2 className="text-xl font-semibold mb-2">About Us</h2><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><p>CHB Multimodal Assistant. Built for chat, uploads, audio/video, and developer APIs.</p></div></main></div>)}
|
frontend/src/pages/ApiKeys.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Sidebar from '../components/Sidebar'
|
2 |
+
import { useState } from 'react'
|
3 |
+
export default function ApiKeys(){const [gen,setGen]=useState('');const [revokeResult,setRevoke]=useState('');const [list,setList]=useState<any[]>([]);const token=localStorage.getItem('token')||'';const headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'};const API=(p:string)=>'/api'+p;const doGen=async()=>{const r=await fetch(API('/keys/generate'),{method:'POST',headers});setGen(JSON.stringify(await r.json(),null,2))};const doRevoke=async()=>{const key=prompt('Enter key to revoke');if(!key) return;const r=await fetch(API('/keys/revoke'),{method:'POST',headers,body:JSON.stringify({key})});setRevoke(JSON.stringify(await r.json(),null,2))};const doList=async()=>{const r=await fetch(API('/keys'),{headers:{'Authorization':'Bearer '+token}});setList(await r.json())};return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4 space-y-3"><header className="flex items-center justify-between"><h2 className="text-xl font-semibold">API Keys</h2></header><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-1">Guide</h3><ol className="list-decimal pl-5 text-slate-300"><li><b>Generate new key:</b> Creates a fresh key for API access.</li><li><b>Revoke key:</b> Immediately disables a key.</li><li><b>My key details:</b> View your active keys and status.</li></ol></div><div className="grid md:grid-cols-3 gap-3"><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">Generate new key</h3><button onClick={doGen} className="px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Generate</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{gen}</pre></div><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">Revoke key</h3><button onClick={doRevoke} className="px-3 py-2 rounded-xl bg-red-400 text-slate-900 font-semibold">Revoke</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{revokeResult}</pre></div><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">My key details</h3><button onClick={doList} className="px-3 py-2 rounded-xl bg-slate-200/80 text-slate-900 font-semibold">Refresh</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{JSON.stringify(list,null,2)}</pre></div></div></main></div>)}
|
frontend/src/pages/Chat.tsx
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Sidebar from '../components/Sidebar'
|
2 |
+
import Onboarding from '../components/Onboarding'
|
3 |
+
import ChatBox from '../components/ChatBox'
|
4 |
+
export default function Chat(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><header className="flex items-center justify-between mb-2"><h2 className="text-xl font-semibold">Chat</h2></header><section className="h-[calc(100vh-80px)]"><ChatBox /></section></main><Onboarding /></div>)}
|
frontend/src/pages/HowToUse.tsx
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
import Sidebar from '../components/Sidebar'
|
2 |
+
export default function HowToUse(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><h2 className="text-xl font-semibold mb-2">How to Use</h2><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><ol className="list-decimal pl-5 space-y-1"><li>Sign up or log in (email + password).</li><li>Use Chat to talk to CHB. Long-press the paperclip to switch between upload / voice / video.</li><li>Manage API keys in <b>API Keys</b>.</li></ol></div></main></div>)}
|
frontend/src/pages/Login.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { useState } from 'react'
|
2 |
+
import Loader from '../components/Loader'
|
3 |
+
export default function Login(){const [email,setEmail]=useState('');const [password,setPassword]=useState('');const [loading,setLoading]=useState(false);const submit=async(e:any)=>{e.preventDefault();setLoading(true);const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password})});const data=await r.json();setLoading(false);if(r.ok){localStorage.setItem('token',data.access_token);location.href='/chat'}else{alert(data.detail||'Login failed')}};return(<div className="min-h-screen flex items-center justify-center p-4 bg-slate-900">{loading&&<Loader/>}<div className="w-full max-w-md border border-slate-800 rounded-2xl p-6 bg-slate-900/60"><h1 className="text-2xl font-bold mb-1 tracking-widest">CHB</h1><p className="text-slate-400 mb-4">Sign in to continue</p><form onSubmit={submit} className="space-y-3"><input value={email} onChange={e=>setEmail((e.target as any).value)} type="email" placeholder="[email protected]" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><input value={password} onChange={e=>setPassword((e.target as any).value)} type="password" placeholder="β’β’β’β’β’β’β’β’" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><button className="w-full px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Log in</button></form><div className="mt-3 text-sm text-slate-400">No account? <a href="/signup" className="text-sky-300">Sign up</a></div></div></div>)}
|
frontend/src/pages/NotFound.tsx
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default function NotFound(){return <div className='min-h-screen grid place-items-center text-slate-200'>Not Found</div>}
|
frontend/src/pages/Signup.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { useState } from 'react'
|
2 |
+
import Loader from '../components/Loader'
|
3 |
+
export default function Signup(){const [email,setEmail]=useState('');const [password,setPassword]=useState('');const [loading,setLoading]=useState(false);const submit=async(e:any)=>{e.preventDefault();setLoading(true);const r=await fetch('/api/auth/signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password})});const data=await r.json();setLoading(false);if(r.ok){localStorage.setItem('token',data.access_token);localStorage.setItem('first_time','1');location.href='/chat'}else{alert(data.detail||'Signup failed')}};return(<div className="min-h-screen flex items-center justify-center p-4 bg-slate-900">{loading&&<Loader/>}<div className="w-full max-w-md border border-slate-800 rounded-2xl p-6 bg-slate-900/60"><h1 className="text-2xl font-bold mb-1 tracking-widest">CHB</h1><p className="text-slate-400 mb-4">Create an account</p><form onSubmit={submit} className="space-y-3"><input value={email} onChange={e=>setEmail((e.target as any).value)} type="email" placeholder="[email protected]" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><input value={password} onChange={e=>setPassword((e.target as any).value)} type="password" placeholder="Create a password" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><button className="w-full px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Create account</button></form></div></div>)}
|
frontend/src/styles.css
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
@tailwind base;@tailwind components;@tailwind utilities;:root{--primary:#3ab4f2}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-thumb{background:#1f3047;border-radius:8px}
|
frontend/tailwind.config.js
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default {content:['./index.html','./src/**/*.{ts,tsx}'], theme:{extend:{}}, plugins:[]}
|
frontend/vite.config.js
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vite'
|
2 |
+
import react from '@vitejs/plugin-react'
|
3 |
+
export default defineConfig({plugins:[react()], server:{port:5173, proxy:{'/api':'http://localhost:7860'}}, build:{outDir:'dist'}})
|