Princeaka commited on
Commit
0090113
·
verified ·
1 Parent(s): 8536603

Delete app

Browse files
app/.env.example DELETED
@@ -1,6 +0,0 @@
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=*
 
 
 
 
 
 
 
app/Dockerfile DELETED
@@ -1,22 +0,0 @@
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"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/Procfile DELETED
@@ -1 +0,0 @@
1
- web: uvicorn app:app --host 0.0.0.0 --port 7860
 
 
app/app.py DELETED
@@ -1,28 +0,0 @@
1
- import os
2
- from fastapi import FastAPI
3
- from fastapi.staticfiles import StaticFiles
4
- from fastapi.responses import FileResponse, JSONResponse
5
- from backend.app import create_app, run_migrations
6
-
7
- app: FastAPI = create_app()
8
-
9
- @app.on_event("startup")
10
- async def startup_event():
11
- try:
12
- run_migrations()
13
- except Exception as e:
14
- print("Migration error:", e)
15
-
16
- FRONTEND_DIR = os.path.join(os.getcwd(), "frontend_dist")
17
- if os.path.isdir(FRONTEND_DIR):
18
- app.mount("/assets", StaticFiles(directory=os.path.join(FRONTEND_DIR, "assets")), name="assets")
19
- @app.get("/{full_path:path}")
20
- async def serve_spa(full_path: str):
21
- index_path = os.path.join(FRONTEND_DIR, "index.html")
22
- if os.path.exists(index_path):
23
- return FileResponse(index_path)
24
- return JSONResponse({"detail": "Frontend not built."}, status_code=501)
25
- else:
26
- @app.get("/")
27
- async def root_info():
28
- return {"message": "Backend running. Frontend not built."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/__init__.py DELETED
File without changes
app/backend/app.py DELETED
@@ -1,36 +0,0 @@
1
- from fastapi import FastAPI
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from .core.config import settings
4
- from .database import Base, engine
5
- from .routes import auth, apikey, chat, about
6
-
7
- def run_migrations():
8
- try:
9
- from alembic import command
10
- from alembic.config import Config
11
- import os
12
- cfg = Config(os.path.join(os.path.dirname(__file__), "migrations", "alembic.ini"))
13
- cfg.set_main_option("script_location", os.path.join(os.path.dirname(__file__), "migrations"))
14
- cfg.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
15
- command.upgrade(cfg, "head")
16
- except Exception as e:
17
- print("Alembic migration error:", e)
18
-
19
- def create_app() -> FastAPI:
20
- app = FastAPI(title=settings.APP_NAME)
21
-
22
- app.add_middleware(
23
- CORSMiddleware,
24
- allow_origins=[settings.CORS_ORIGINS] if settings.CORS_ORIGINS != "*" else ["*"],
25
- allow_credentials=True,
26
- allow_methods=["*"],
27
- allow_headers=["*"],
28
- )
29
-
30
- Base.metadata.bind = engine
31
-
32
- app.include_router(auth.router)
33
- app.include_router(apikey.router)
34
- app.include_router(chat.router)
35
- app.include_router(about.router)
36
- return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/core/config.py DELETED
@@ -1,14 +0,0 @@
1
- import os
2
- from pydantic import BaseModel
3
- from dotenv import load_dotenv
4
-
5
- load_dotenv()
6
-
7
- class Settings(BaseModel):
8
- APP_NAME: str = os.getenv("APP_NAME", "CHB App")
9
- DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./app.db")
10
- JWT_SECRET: str = os.getenv("JWT_SECRET", "dev-secret")
11
- JWT_EXPIRES_MINUTES: int = int(os.getenv("JWT_EXPIRES_MINUTES", "60"))
12
- CORS_ORIGINS: str = os.getenv("CORS_ORIGINS", "*")
13
-
14
- settings = Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/core/security.py DELETED
@@ -1,40 +0,0 @@
1
- from datetime import datetime, timedelta
2
- from typing import Optional
3
- from jose import jwt, JWTError
4
- from passlib.context import CryptContext
5
- from fastapi import Depends, HTTPException, status
6
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
- from sqlalchemy.orm import Session
8
- from .config import settings
9
- from ..database import get_db
10
- from ..models import User
11
-
12
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
13
- auth_scheme = HTTPBearer()
14
-
15
- def hash_password(p: str) -> str:
16
- return pwd_context.hash(p)
17
-
18
- def verify_password(p: str, hashed: str) -> bool:
19
- return pwd_context.verify(p, hashed)
20
-
21
- def create_access_token(sub: str, expires_minutes: int = None) -> str:
22
- expire = datetime.utcnow() + timedelta(minutes=expires_minutes or settings.JWT_EXPIRES_MINUTES)
23
- to_encode = {"sub": sub, "exp": expire}
24
- return jwt.encode(to_encode, settings.JWT_SECRET, algorithm="HS256")
25
-
26
- def decode_token(token: str) -> Optional[str]:
27
- try:
28
- payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
29
- return payload.get("sub")
30
- except JWTError:
31
- return None
32
-
33
- def get_current_user(creds: HTTPAuthorizationCredentials = Depends(auth_scheme), db: Session = Depends(get_db)) -> User:
34
- email = decode_token(creds.credentials)
35
- if not email:
36
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
37
- user = db.query(User).filter(User.email == email).first()
38
- if not user:
39
- raise HTTPException(status_code=401, detail="User not found")
40
- return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/database.py DELETED
@@ -1,16 +0,0 @@
1
- from sqlalchemy import create_engine
2
- from sqlalchemy.orm import sessionmaker, DeclarativeBase
3
- from .core.config import settings
4
-
5
- class Base(DeclarativeBase):
6
- pass
7
-
8
- engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {})
9
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
10
-
11
- def get_db():
12
- db = SessionLocal()
13
- try:
14
- yield db
15
- finally:
16
- db.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/migrations/alembic.ini DELETED
@@ -1,3 +0,0 @@
1
- [alembic]
2
- script_location = migrations
3
- sqlalchemy.url = sqlite:///./app.db
 
 
 
 
app/backend/migrations/env.py DELETED
@@ -1,23 +0,0 @@
1
- from logging.config import fileConfig
2
- from sqlalchemy import engine_from_config, pool
3
- from alembic import context
4
- import os, sys
5
- config = context.config
6
- if config.config_file_name is not None:
7
- fileConfig(config.config_file_name)
8
- target_metadata = None
9
- def run_migrations_offline():
10
- url = config.get_main_option("sqlalchemy.url")
11
- context.configure(url=url, literal_binds=True)
12
- with context.begin_transaction():
13
- context.run_migrations()
14
- def run_migrations_online():
15
- connectable = engine_from_config(config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool)
16
- with connectable.connect() as connection:
17
- context.configure(connection=connection)
18
- with context.begin_transaction():
19
- context.run_migrations()
20
- if context.is_offline_mode():
21
- run_migrations_offline()
22
- else:
23
- run_migrations_online()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/migrations/versions/0001_init.py DELETED
@@ -1,37 +0,0 @@
1
- from alembic import op
2
- import sqlalchemy as sa
3
-
4
- revision = '0001_init'
5
- down_revision = None
6
- branch_labels = None
7
- depends_on = None
8
-
9
- def upgrade():
10
- op.create_table(
11
- 'users',
12
- sa.Column('id', sa.Integer(), primary_key=True),
13
- sa.Column('email', sa.String(255), nullable=False, unique=True),
14
- sa.Column('password_hash', sa.String(255), nullable=False),
15
- sa.Column('created_at', sa.DateTime(), nullable=True),
16
- )
17
- op.create_table(
18
- 'api_keys',
19
- sa.Column('id', sa.Integer(), primary_key=True),
20
- sa.Column('key', sa.String(255), nullable=False, unique=True),
21
- sa.Column('revoked', sa.Boolean(), default=False),
22
- sa.Column('created_at', sa.DateTime(), nullable=True),
23
- sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'))
24
- )
25
- op.create_table(
26
- 'chat_history',
27
- sa.Column('id', sa.Integer(), primary_key=True),
28
- sa.Column('role', sa.String(20)),
29
- sa.Column('content', sa.Text(), nullable=False),
30
- sa.Column('created_at', sa.DateTime(), nullable=True),
31
- sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'))
32
- )
33
-
34
- def downgrade():
35
- op.drop_table('chat_history')
36
- op.drop_table('api_keys')
37
- op.drop_table('users')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/models.py DELETED
@@ -1,34 +0,0 @@
1
- from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text
2
- from sqlalchemy.orm import relationship, Mapped, mapped_column
3
- from datetime import datetime
4
- from .database import Base
5
-
6
- class User(Base):
7
- __tablename__ = "users"
8
- id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
9
- email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
10
- password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
11
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
12
-
13
- api_keys = relationship("APIKey", back_populates="owner", cascade="all, delete-orphan")
14
- messages = relationship("ChatMessage", back_populates="user", cascade="all, delete-orphan")
15
-
16
- class APIKey(Base):
17
- __tablename__ = "api_keys"
18
- id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
19
- key: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
20
- revoked: Mapped[bool] = mapped_column(Boolean, default=False)
21
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
22
-
23
- user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
24
- owner = relationship("User", back_populates="api_keys")
25
-
26
- class ChatMessage(Base):
27
- __tablename__ = "chat_history"
28
- id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
29
- role: Mapped[str] = mapped_column(String(20), default="user") # 'user' or 'assistant'
30
- content: Mapped[str] = mapped_column(Text, nullable=False)
31
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
32
-
33
- user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
34
- user = relationship("User", back_populates="messages")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/routes/about.py DELETED
@@ -1,17 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- router = APIRouter(prefix="/api/info", tags=["Info"])
4
-
5
- @router.get("/about")
6
- def about():
7
- return {"name": "CHB", "tagline": "Multimodal Assistant", "version": "1.0.0"}
8
-
9
- @router.get("/howto")
10
- def howto():
11
- return {
12
- "steps": [
13
- "Sign up or log in",
14
- "Use Chat to send messages, long-press the paperclip to switch modes",
15
- "Go to API Keys to generate or revoke keys"
16
- ]
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/routes/apikey.py DELETED
@@ -1,32 +0,0 @@
1
- import secrets
2
- from fastapi import APIRouter, Depends, HTTPException
3
- from sqlalchemy.orm import Session
4
- from ..database import get_db
5
- from ..models import APIKey
6
- from ..schemas import APIKeyOut, KeyRevokeIn
7
- from ..core.security import get_current_user
8
-
9
- router = APIRouter(prefix="/api/keys", tags=["API Keys"])
10
-
11
- @router.get("", response_model=list[APIKeyOut])
12
- def list_keys(db: Session = Depends(get_db), user = Depends(get_current_user)):
13
- rows = db.query(APIKey).filter(APIKey.user_id == user.id).order_by(APIKey.created_at.desc()).all()
14
- return rows
15
-
16
- @router.post("/generate", response_model=APIKeyOut)
17
- def generate_key(db: Session = Depends(get_db), user = Depends(get_current_user)):
18
- key = "chb_" + secrets.token_urlsafe(32)
19
- row = APIKey(key=key, user_id=user.id)
20
- db.add(row)
21
- db.commit()
22
- db.refresh(row)
23
- return row
24
-
25
- @router.post("/revoke")
26
- def revoke_key(payload: KeyRevokeIn, db: Session = Depends(get_db), user = Depends(get_current_user)):
27
- row = db.query(APIKey).filter(APIKey.user_id == user.id, APIKey.key == payload.key).first()
28
- if not row:
29
- raise HTTPException(status_code=404, detail="Key not found")
30
- row.revoked = True
31
- db.commit()
32
- return {"revoked": payload.key}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/routes/auth.py DELETED
@@ -1,27 +0,0 @@
1
- from fastapi import APIRouter, HTTPException, Depends
2
- from sqlalchemy.orm import Session
3
- from ..schemas import UserCreate, UserOut, LoginIn, Token
4
- from ..models import User
5
- from ..database import get_db
6
- from ..core.security import hash_password, verify_password, create_access_token
7
-
8
- router = APIRouter(prefix="/api/auth", tags=["Auth"])
9
-
10
- @router.post("/signup", response_model=Token)
11
- def signup(payload: UserCreate, db: Session = Depends(get_db)):
12
- if db.query(User).filter(User.email == payload.email).first():
13
- raise HTTPException(status_code=400, detail="User already exists")
14
- user = User(email=payload.email, password_hash=hash_password(payload.password))
15
- db.add(user)
16
- db.commit()
17
- db.refresh(user)
18
- token = create_access_token(sub=user.email)
19
- return {"access_token": token}
20
-
21
- @router.post("/login", response_model=Token)
22
- def login(payload: LoginIn, db: Session = Depends(get_db)):
23
- user = db.query(User).filter(User.email == payload.email).first()
24
- if not user or not verify_password(payload.password, user.password_hash):
25
- raise HTTPException(status_code=401, detail="Invalid credentials")
26
- token = create_access_token(sub=user.email)
27
- return {"access_token": token}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/routes/chat.py DELETED
@@ -1,28 +0,0 @@
1
- from fastapi import APIRouter, Depends, UploadFile, File
2
- from sqlalchemy.orm import Session
3
- from ..database import get_db
4
- from ..schemas import ChatIn, ChatOut, ChatMessageOut
5
- from ..models import ChatMessage
6
- from ..core.security import get_current_user
7
-
8
- router = APIRouter(prefix="/api/chat", tags=["Chat"])
9
-
10
- @router.get("/history", response_model=list[ChatMessageOut])
11
- def history(db: Session = Depends(get_db), user = Depends(get_current_user)):
12
- rows = db.query(ChatMessage).filter(ChatMessage.user_id == user.id).order_by(ChatMessage.created_at.asc()).all()
13
- return rows
14
-
15
- @router.post("", response_model=ChatOut)
16
- def send(payload: ChatIn, db: Session = Depends(get_db), user = Depends(get_current_user)):
17
- um = ChatMessage(user_id=user.id, role="user", content=payload.message)
18
- db.add(um)
19
- reply_text = f"CHB: You said '{payload.message}'"
20
- bm = ChatMessage(user_id=user.id, role="assistant", content=reply_text)
21
- db.add(bm)
22
- db.commit()
23
- return {"reply": reply_text}
24
-
25
- @router.post("/upload")
26
- async def upload(file: UploadFile = File(...), user = Depends(get_current_user)):
27
- # stub: accept file and return name; real storage to be implemented
28
- return {"filename": file.filename, "detail":"file received"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/backend/schemas.py DELETED
@@ -1,45 +0,0 @@
1
- from pydantic import BaseModel, EmailStr, Field
2
- from typing import List, Optional
3
- from datetime import datetime
4
-
5
- class UserCreate(BaseModel):
6
- email: EmailStr
7
- password: str = Field(min_length=6)
8
-
9
- class UserOut(BaseModel):
10
- id: int
11
- email: EmailStr
12
- created_at: datetime
13
- class Config:
14
- from_attributes = True
15
-
16
- class Token(BaseModel):
17
- access_token: str
18
- token_type: str = "bearer"
19
-
20
- class LoginIn(BaseModel):
21
- email: EmailStr
22
- password: str
23
-
24
- class APIKeyOut(BaseModel):
25
- key: str
26
- revoked: bool
27
- created_at: datetime
28
- class Config:
29
- from_attributes = True
30
-
31
- class KeyRevokeIn(BaseModel):
32
- key: str
33
-
34
- class ChatIn(BaseModel):
35
- message: str
36
-
37
- class ChatOut(BaseModel):
38
- reply: str
39
-
40
- class ChatMessageOut(BaseModel):
41
- role: str
42
- content: str
43
- created_at: datetime
44
- class Config:
45
- from_attributes = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/frontend/index.html DELETED
@@ -1 +0,0 @@
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>
 
 
app/frontend/package.json DELETED
@@ -1,24 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/frontend/postcss.config.js DELETED
@@ -1 +0,0 @@
1
- export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
 
 
app/frontend/src/App.tsx DELETED
@@ -1,23 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/frontend/src/components/ChatBox.tsx DELETED
@@ -1,8 +0,0 @@
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>)}
 
 
 
 
 
 
 
 
 
app/frontend/src/components/Loader.tsx DELETED
@@ -1 +0,0 @@
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>)}
 
 
app/frontend/src/components/Onboarding.tsx DELETED
@@ -1,2 +0,0 @@
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>)}
 
 
 
app/frontend/src/components/Sidebar.tsx DELETED
@@ -1,2 +0,0 @@
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>)}
 
 
 
app/frontend/src/main.tsx DELETED
@@ -1,12 +0,0 @@
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
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
app/frontend/src/pages/About.tsx DELETED
@@ -1,2 +0,0 @@
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>)}
 
 
 
app/frontend/src/pages/ApiKeys.tsx DELETED
@@ -1,3 +0,0 @@
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>)}
 
 
 
 
app/frontend/src/pages/Chat.tsx DELETED
@@ -1,4 +0,0 @@
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>)}
 
 
 
 
 
app/frontend/src/pages/HowToUse.tsx DELETED
@@ -1,2 +0,0 @@
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>)}
 
 
 
app/frontend/src/pages/Login.tsx DELETED
@@ -1,3 +0,0 @@
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>)}
 
 
 
 
app/frontend/src/pages/NotFound.tsx DELETED
@@ -1 +0,0 @@
1
- export default function NotFound(){return <div className='min-h-screen grid place-items-center text-slate-200'>Not Found</div>}
 
 
app/frontend/src/pages/Signup.tsx DELETED
@@ -1,3 +0,0 @@
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>)}
 
 
 
 
app/frontend/src/styles.css DELETED
@@ -1 +0,0 @@
1
- @tailwind base;@tailwind components;@tailwind utilities;:root{--primary:#3ab4f2}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-thumb{background:#1f3047;border-radius:8px}
 
 
app/frontend/tailwind.config.js DELETED
@@ -1 +0,0 @@
1
- export default {content:['./index.html','./src/**/*.{ts,tsx}'], theme:{extend:{}}, plugins:[]}
 
 
app/frontend/vite.config.js DELETED
@@ -1,3 +0,0 @@
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'}})
 
 
 
 
app/requirements.txt DELETED
@@ -1,10 +0,0 @@
1
- fastapi==0.111.0
2
- uvicorn[standard]==0.30.1
3
- SQLAlchemy==2.0.31
4
- alembic==1.13.2
5
- pydantic[email]==2.8.2
6
- passlib[bcrypt]==1.7.4
7
- python-jose[cryptography]==3.3.0
8
- python-dotenv==1.0.1
9
- jinja2==3.1.4
10
- python-multipart==0.0.9