Spaces:
Sleeping
Sleeping
Commit ·
e03f122
1
Parent(s): cb610aa
Configure multi-stage Docker build to serve frontend and fix API URL
Browse files- Dockerfile +14 -1
- frontend/src/App.tsx +4 -4
- main.py +30 -7
Dockerfile
CHANGED
|
@@ -1,7 +1,17 @@
|
|
| 1 |
# ==============================================================
|
| 2 |
-
# Tech Disciples AI
|
| 3 |
# ==============================================================
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
FROM python:3.10-slim
|
| 6 |
|
| 7 |
# Environment setup
|
|
@@ -23,6 +33,9 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 23 |
# Copy all source files
|
| 24 |
COPY . .
|
| 25 |
|
|
|
|
|
|
|
|
|
|
| 26 |
# Define Hugging Face token env var (Spaces injects the secret automatically)
|
| 27 |
ENV HUGGINGFACEHUB_API_TOKEN=""
|
| 28 |
|
|
|
|
| 1 |
# ==============================================================
|
| 2 |
+
# Tech Disciples AI — Dockerfile (for Hugging Face Spaces)
|
| 3 |
# ==============================================================
|
| 4 |
|
| 5 |
+
# Stage 1: Build the React frontend
|
| 6 |
+
FROM node:18-alpine AS frontend-builder
|
| 7 |
+
WORKDIR /app/frontend
|
| 8 |
+
COPY frontend/package*.json ./
|
| 9 |
+
# Clean install to ensure all dependencies are properly resolved
|
| 10 |
+
RUN npm install --legacy-peer-deps
|
| 11 |
+
COPY frontend/ ./
|
| 12 |
+
RUN npm run build
|
| 13 |
+
|
| 14 |
+
# Stage 2: Set up the FastAPI backend
|
| 15 |
FROM python:3.10-slim
|
| 16 |
|
| 17 |
# Environment setup
|
|
|
|
| 33 |
# Copy all source files
|
| 34 |
COPY . .
|
| 35 |
|
| 36 |
+
# Copy the built frontend from Stage 1
|
| 37 |
+
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 38 |
+
|
| 39 |
# Define Hugging Face token env var (Spaces injects the secret automatically)
|
| 40 |
ENV HUGGINGFACEHUB_API_TOKEN=""
|
| 41 |
|
frontend/src/App.tsx
CHANGED
|
@@ -28,8 +28,8 @@ const App: React.FC = () => {
|
|
| 28 |
setIsLoading(true);
|
| 29 |
|
| 30 |
try {
|
| 31 |
-
// Note:
|
| 32 |
-
const response = await axios.post('
|
| 33 |
query: userMessage,
|
| 34 |
session_id: sessionId,
|
| 35 |
}, {
|
|
@@ -105,8 +105,8 @@ const App: React.FC = () => {
|
|
| 105 |
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
| 106 |
</div>
|
| 107 |
<div className={`p-4 rounded-2xl ${msg.role === 'user'
|
| 108 |
-
|
| 109 |
-
|
| 110 |
}`}>
|
| 111 |
<div className="markdown-content prose prose-invert prose-sm max-w-none">
|
| 112 |
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
|
|
|
| 28 |
setIsLoading(true);
|
| 29 |
|
| 30 |
try {
|
| 31 |
+
// Note: Use relative path for Hugging Face deployment so it hits the same origin backend
|
| 32 |
+
const response = await axios.post('/ai-chat', {
|
| 33 |
query: userMessage,
|
| 34 |
session_id: sessionId,
|
| 35 |
}, {
|
|
|
|
| 105 |
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
| 106 |
</div>
|
| 107 |
<div className={`p-4 rounded-2xl ${msg.role === 'user'
|
| 108 |
+
? 'bg-purple-600/30 text-white rounded-tr-none border border-purple-500/20'
|
| 109 |
+
: 'bg-white/5 text-white/90 rounded-tl-none border border-white/10 backdrop-blur-md'
|
| 110 |
}`}>
|
| 111 |
<div className="markdown-content prose prose-invert prose-sm max-w-none">
|
| 112 |
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
main.py
CHANGED
|
@@ -5,7 +5,9 @@
|
|
| 5 |
import os
|
| 6 |
import logging
|
| 7 |
from typing import List, Optional
|
| 8 |
-
from fastapi import FastAPI, HTTPException, Header
|
|
|
|
|
|
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
from pydantic import BaseModel
|
| 11 |
from dotenv import load_dotenv
|
|
@@ -99,12 +101,25 @@ class QueryInput(BaseModel):
|
|
| 99 |
query: str
|
| 100 |
session_id: Optional[str] = "default"
|
| 101 |
|
| 102 |
-
#
|
| 103 |
-
#
|
| 104 |
-
#
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
async def root():
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
@app.post("/ai-chat")
|
| 110 |
async def ai_chat(data: QueryInput, x_api_key: str = Header(None)):
|
|
@@ -157,4 +172,12 @@ async def ai_chat(data: QueryInput, x_api_key: str = Header(None)):
|
|
| 157 |
|
| 158 |
except Exception as e:
|
| 159 |
logger.error(f"⚠️ Model runtime error: {e}")
|
| 160 |
-
raise HTTPException(status_code=500, detail=f"Theo encountered an issue: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import os
|
| 6 |
import logging
|
| 7 |
from typing import List, Optional
|
| 8 |
+
from fastapi import FastAPI, HTTPException, Header, Request
|
| 9 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
| 10 |
+
from fastapi.staticfiles import StaticFiles
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
from pydantic import BaseModel
|
| 13 |
from dotenv import load_dotenv
|
|
|
|
| 101 |
query: str
|
| 102 |
session_id: Optional[str] = "default"
|
| 103 |
|
| 104 |
+
# Mount the frontend directory as static files
|
| 105 |
+
# In a typical Vite React app, the build output is in 'dist'.
|
| 106 |
+
# If it's just raw HTML/JS/CSS we point to 'frontend'.
|
| 107 |
+
frontend_dir = os.path.join(os.path.dirname(__file__), "frontend")
|
| 108 |
+
dist_dir = os.path.join(frontend_dir, "dist")
|
| 109 |
+
|
| 110 |
+
if os.path.isdir(dist_dir):
|
| 111 |
+
serve_dir = dist_dir
|
| 112 |
+
else:
|
| 113 |
+
serve_dir = frontend_dir
|
| 114 |
+
|
| 115 |
+
app.mount("/assets", StaticFiles(directory=os.path.join(serve_dir, "assets"), html=True), name="assets")
|
| 116 |
+
|
| 117 |
+
@app.get("/", response_class=HTMLResponse)
|
| 118 |
async def root():
|
| 119 |
+
index_path = os.path.join(serve_dir, "index.html")
|
| 120 |
+
if os.path.exists(index_path):
|
| 121 |
+
return FileResponse(index_path)
|
| 122 |
+
return {"message": "✅ Theo AI (TechDisciples CLCC) API is online. Frontend not built yet."}
|
| 123 |
|
| 124 |
@app.post("/ai-chat")
|
| 125 |
async def ai_chat(data: QueryInput, x_api_key: str = Header(None)):
|
|
|
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
logger.error(f"⚠️ Model runtime error: {e}")
|
| 175 |
+
raise HTTPException(status_code=500, detail=f"Theo encountered an issue: {str(e)}")
|
| 176 |
+
|
| 177 |
+
# Catch-all route to serve the SPA index.html for unknown paths
|
| 178 |
+
@app.exception_handler(404)
|
| 179 |
+
async def custom_404_handler(request: Request, exc: HTTPException):
|
| 180 |
+
index_path = os.path.join(serve_dir, "index.html")
|
| 181 |
+
if os.path.exists(index_path) and not request.url.path.startswith("/api"):
|
| 182 |
+
return FileResponse(index_path)
|
| 183 |
+
return HTMLResponse(content="Not Found", status_code=404)
|