Princeaka commited on
Commit
ace186a
Β·
verified Β·
1 Parent(s): 220d87f

Upload 21 files

Browse files
.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
- FROM python:3.10-bullseye
2
-
3
- ENV DEBIAN_FRONTEND=noninteractive \
4
- HF_HUB_DISABLE_TELEMETRY=1 \
5
- HF_HUB_ENABLE_HF_TRANSFER=1 \
6
- TOKENIZERS_PARALLELISM=false \
7
- PIP_DISABLE_PIP_VERSION_CHECK=1 \
8
- PIP_NO_CACHE_DIR=1
9
-
10
- # --- Install system dependencies ---
11
- RUN apt-get update && apt-get install -y \
12
- ffmpeg \
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
- COPY . /app
39
-
40
- # Create writable directories
41
- RUN mkdir -p /app/model_cache /app/tmp && chown -R 1000:1000 /app
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'}})