ghostdrive1 commited on
Commit
1cf3e52
·
verified ·
1 Parent(s): aa9a63b

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -2,12 +2,27 @@ FROM python:3.12-slim
2
 
3
  WORKDIR /app
4
 
5
- # Install only what we need - lightweight and fast
6
- RUN pip install fastapi uvicorn httpx pydantic
 
 
 
 
7
 
8
- # Copy our application
 
 
 
9
  COPY main.py .
 
 
 
 
 
 
 
 
10
 
11
  EXPOSE 7860
12
 
13
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
2
 
3
  WORKDIR /app
4
 
5
+ # Install Redis + Node.js (for building UI)
6
+ RUN apt-get update && \
7
+ apt-get install -y --no-install-recommends redis-server curl && \
8
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
9
+ apt-get install -y nodejs && \
10
+ rm -rf /var/lib/apt/lists/*
11
 
12
+ # Install Python deps
13
+ RUN pip install --no-cache-dir fastapi uvicorn httpx pydantic redis
14
+
15
+ # Copy backend
16
  COPY main.py .
17
+ COPY start.sh .
18
+ RUN chmod +x start.sh
19
+
20
+ # Copy and build frontend
21
+ COPY node3-ui/ /tmp/ui/
22
+ RUN cd /tmp/ui && npm ci && npm run build && \
23
+ mv /tmp/ui/dist /app/static && \
24
+ rm -rf /tmp/ui
25
 
26
  EXPOSE 7860
27
 
28
+ CMD ["./start.sh"]
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Logic Engine
3
  emoji: 🤖
4
  colorFrom: blue
5
  colorTo: indigo
@@ -8,6 +8,6 @@ pinned: false
8
  app_port: 7860
9
  ---
10
 
11
- # Logic Engine - Node 2
12
 
13
- FastAPI backend for the Manus UI Clone. Exposes a /chat endpoint.
 
1
  ---
2
+ title: Manus Clone
3
  emoji: 🤖
4
  colorFrom: blue
5
  colorTo: indigo
 
8
  app_port: 7860
9
  ---
10
 
11
+ # Manus Clone Unified
12
 
13
+ React UI + FastAPI backend + Redis (2GB) in a single container.
main.py CHANGED
@@ -1,10 +1,13 @@
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
 
 
3
  from pydantic import BaseModel
4
  import os
5
  import httpx
 
6
 
7
- app = FastAPI(title="Logic Engine")
8
 
9
  app.add_middleware(
10
  CORSMiddleware,
@@ -14,6 +17,14 @@ app.add_middleware(
14
  allow_headers=["*"],
15
  )
16
 
 
 
 
 
 
 
 
 
17
  class ChatRequest(BaseModel):
18
  prompt: str
19
  model: str = "llama-3.1-8b-instant"
@@ -27,15 +38,18 @@ PROVIDER_BASES = {
27
  "HuggingFace": "https://api-inference.huggingface.co/v1",
28
  "OpenRouter": "https://openrouter.ai/api/v1",
29
  "Nvidia": "https://integrate.api.nvidia.com/v1",
 
 
30
  }
31
 
32
- @app.get("/")
33
- def root():
34
- return {"status": "Logic Engine is running", "endpoints": ["/chat", "/health"]}
 
 
35
 
36
  @app.post("/chat")
37
  async def chat_endpoint(request: ChatRequest):
38
- # Priority: key from frontend → env vars
39
  api_key = (
40
  request.api_key.strip()
41
  or os.environ.get("GROQ_API_KEY", "")
@@ -45,7 +59,7 @@ async def chat_endpoint(request: ChatRequest):
45
  if not api_key:
46
  return {
47
  "response": (
48
- f'[Logic Engine] Received: "{request.prompt}"\n\n'
49
  "No API key configured. Please add your key via the Providers panel."
50
  ),
51
  "doc": True
@@ -73,10 +87,25 @@ async def chat_endpoint(request: ChatRequest):
73
  resp.raise_for_status()
74
  data = resp.json()
75
  reply = data["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
76
  return {"response": reply, "doc": True}
77
 
78
  except httpx.HTTPStatusError as e:
79
  status = e.response.status_code
 
 
 
 
 
 
80
  if status == 401:
81
  detail = (
82
  f"401 Unauthorized from {request.provider}. "
@@ -84,14 +113,22 @@ async def chat_endpoint(request: ChatRequest):
84
  f"and that the model '{request.model}' is available on {request.provider}. "
85
  f"Tip: Make sure the model you selected belongs to the same provider as your API key."
86
  )
 
 
87
  elif status == 404:
88
  detail = f"404: Model '{request.model}' not found on {request.provider}. Select a model from the correct provider tab."
89
  else:
90
- detail = f"Provider error {status}: {e.response.text[:300]}"
91
  raise HTTPException(status_code=status, detail=detail)
92
  except Exception as e:
93
  raise HTTPException(status_code=500, detail=str(e))
94
 
95
- @app.get("/health")
96
- def health():
97
- return {"status": "Logic Engine Running"}
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
  from pydantic import BaseModel
6
  import os
7
  import httpx
8
+ import redis
9
 
10
+ app = FastAPI(title="Manus Clone")
11
 
12
  app.add_middleware(
13
  CORSMiddleware,
 
17
  allow_headers=["*"],
18
  )
19
 
20
+ # ── Redis connection (2GB, localhost) ──────────────────────────────────────────
21
+ try:
22
+ r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
23
+ r.ping()
24
+ REDIS_OK = True
25
+ except Exception:
26
+ REDIS_OK = False
27
+
28
  class ChatRequest(BaseModel):
29
  prompt: str
30
  model: str = "llama-3.1-8b-instant"
 
38
  "HuggingFace": "https://api-inference.huggingface.co/v1",
39
  "OpenRouter": "https://openrouter.ai/api/v1",
40
  "Nvidia": "https://integrate.api.nvidia.com/v1",
41
+ "Mistral": "https://api.mistral.ai/v1",
42
+ "Cohere": "https://api.cohere.ai/v1",
43
  }
44
 
45
+ # ── API endpoints ─────────────────────────────────────────────────────────────
46
+
47
+ @app.get("/health")
48
+ def health():
49
+ return {"status": "ok", "redis": REDIS_OK}
50
 
51
  @app.post("/chat")
52
  async def chat_endpoint(request: ChatRequest):
 
53
  api_key = (
54
  request.api_key.strip()
55
  or os.environ.get("GROQ_API_KEY", "")
 
59
  if not api_key:
60
  return {
61
  "response": (
62
+ f'[Manus] Received: "{request.prompt}"\n\n'
63
  "No API key configured. Please add your key via the Providers panel."
64
  ),
65
  "doc": True
 
87
  resp.raise_for_status()
88
  data = resp.json()
89
  reply = data["choices"][0]["message"]["content"]
90
+
91
+ # Cache to Redis if available
92
+ if REDIS_OK:
93
+ try:
94
+ cache_key = f"chat:{request.provider}:{request.model}:{request.prompt[:100]}"
95
+ r.setex(cache_key, 3600, reply)
96
+ except Exception:
97
+ pass
98
+
99
  return {"response": reply, "doc": True}
100
 
101
  except httpx.HTTPStatusError as e:
102
  status = e.response.status_code
103
+ try:
104
+ err_body = e.response.json()
105
+ err_msg = err_body.get("error", {}).get("message", e.response.text[:300])
106
+ except Exception:
107
+ err_msg = e.response.text[:300]
108
+
109
  if status == 401:
110
  detail = (
111
  f"401 Unauthorized from {request.provider}. "
 
113
  f"and that the model '{request.model}' is available on {request.provider}. "
114
  f"Tip: Make sure the model you selected belongs to the same provider as your API key."
115
  )
116
+ elif status == 400:
117
+ detail = f"400 Bad Request from {request.provider}: {err_msg}"
118
  elif status == 404:
119
  detail = f"404: Model '{request.model}' not found on {request.provider}. Select a model from the correct provider tab."
120
  else:
121
+ detail = f"Provider error {status}: {err_msg}"
122
  raise HTTPException(status_code=status, detail=detail)
123
  except Exception as e:
124
  raise HTTPException(status_code=500, detail=str(e))
125
 
126
+ # ── Serve React UI (MUST be last, catches all non-API routes) ─────────────────
127
+ static_dir = os.path.join(os.path.dirname(__file__), "static")
128
+ if os.path.isdir(static_dir):
129
+ @app.get("/{full_path:path}")
130
+ async def serve_spa(full_path: str):
131
+ file_path = os.path.join(static_dir, full_path)
132
+ if full_path and os.path.isfile(file_path):
133
+ return FileResponse(file_path)
134
+ return FileResponse(os.path.join(static_dir, "index.html"))
node3-ui/Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package*.json ./
7
+ RUN npm install
8
+ RUN npm install express
9
+
10
+ # Copy source code and build
11
+ COPY . .
12
+ RUN npm run build
13
+
14
+ # Expose port required by Hugging Face Spaces
15
+ EXPOSE 7860
16
+
17
+ # Start the express server
18
+ CMD ["node", "server.js"]
node3-ui/index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Manus Clone Interface</title>
7
+ </head>
8
+ <body class="bg-zinc-950 text-gray-100">
9
+ <script>
10
+ window.addEventListener('error', (e) => {
11
+ if(e.target && e.target.src) {
12
+ document.body.innerHTML = '<div style="color:white;background:red;padding:20px;"><h1>Failed to load resource</h1><p>' + e.target.src + '</p></div>';
13
+ }
14
+ }, true);
15
+ </script>
16
+ <div id="root"></div>
17
+ <script type="module" src="/src/main.jsx"></script>
18
+ </body>
19
+ </html>
node3-ui/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "manus-clone",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "lucide-react": "^0.394.0",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.3.1",
18
+ "autoprefixer": "^10.4.19",
19
+ "postcss": "^8.4.38",
20
+ "tailwindcss": "^3.4.4",
21
+ "vite": "^5.3.1"
22
+ }
23
+ }
node3-ui/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
node3-ui/src/App.jsx ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Bot, PlusSquare, Settings, Database, Plug, FileText, Plus, Monitor, ArrowUp, X, Play, SkipForward, Save, Trash2, Eye, EyeOff, ChevronDown, Search } from 'lucide-react';
3
+
4
+ // ─── Storage ──────────────────────────────────────────────────────────────────
5
+ const STORAGE_KEY = 'manus_config_v2';
6
+ const defaultConfig = {
7
+ providers: { OpenAI: [], Anthropic: [], Groq: [], HuggingFace: [], OpenRouter: [], Nvidia: [], Mistral: [], Cohere: [] },
8
+ activeProvider: 'Groq',
9
+ activeKey: '',
10
+ selectedModel: 'llama-3.1-8b-instant',
11
+ ragConfig: { vectorDbUrl: '', embeddingModel: 'text-embedding-3-small (OpenAI)', systemPrompt: '' },
12
+ mcpServers: [''],
13
+ };
14
+ function loadConfig() {
15
+ try { const s = localStorage.getItem(STORAGE_KEY); return s ? { ...defaultConfig, ...JSON.parse(s) } : defaultConfig; }
16
+ catch { return defaultConfig; }
17
+ }
18
+ function saveConfig(cfg) { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); }
19
+
20
+ // ─── Full Model Catalog ───────────────────────────────────────────────────────
21
+ const MODELS = {
22
+ OpenAI: [
23
+ 'gpt-4o', 'gpt-4o-2024-11-20', 'gpt-4o-2024-08-06', 'gpt-4o-2024-05-13',
24
+ 'gpt-4o-mini', 'gpt-4o-mini-2024-07-18', 'o1', 'o1-2024-12-17',
25
+ 'o1-mini', 'o1-mini-2024-09-12', 'o3-mini', 'o3-mini-2025-01-31',
26
+ 'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview',
27
+ 'gpt-4-0125-preview', 'gpt-4-1106-preview', 'gpt-4', 'gpt-4-0613',
28
+ 'gpt-4-0314', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-1106'
29
+ ],
30
+ Anthropic: [
31
+ 'claude-3-7-sonnet-20250219', 'claude-3-5-sonnet-20241022',
32
+ 'claude-3-5-sonnet-20240620', 'claude-3-5-haiku-20241022',
33
+ 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'
34
+ ],
35
+ Groq: [
36
+ 'llama-3.1-8b-instant', 'llama-3.3-70b-versatile',
37
+ 'llama3-8b-8192', 'llama3-70b-8192', 'mixtral-8x7b-32768',
38
+ 'qwen/qwen3-32b', 'qwen/qwen3.6-27b', 'groq/compound', 'groq/compound-mini',
39
+ 'deepseek-r1-distill-llama-70b', 'meta-llama/llama-4-scout-17b-16e-instruct'
40
+ ],
41
+ HuggingFace: [
42
+ 'meta-llama/Llama-3.3-70B-Instruct', 'meta-llama/Llama-3.1-8B-Instruct',
43
+ 'meta-llama/Llama-3.1-70B-Instruct', 'meta-llama/Meta-Llama-3-8B-Instruct',
44
+ 'mistralai/Mixtral-8x7B-Instruct-v0.1', 'mistralai/Mistral-7B-Instruct-v0.3',
45
+ 'google/gemma-2-9b-it', 'google/gemma-2-27b-it',
46
+ 'Qwen/Qwen2.5-72B-Instruct', 'Qwen/Qwen2.5-Coder-32B-Instruct',
47
+ 'microsoft/Phi-3.5-mini-instruct', 'microsoft/Phi-3-medium-128k-instruct'
48
+ ],
49
+ OpenRouter: [
50
+ 'openai/gpt-4o', 'openai/gpt-4o-mini', 'openai/o1', 'openai/o1-mini', 'openai/o3-mini',
51
+ 'anthropic/claude-3.7-sonnet', 'anthropic/claude-3.5-sonnet',
52
+ 'anthropic/claude-3.5-haiku', 'anthropic/claude-3-opus',
53
+ 'google/gemini-2.0-pro-exp-02-05', 'google/gemini-2.0-flash-exp',
54
+ 'google/gemini-1.5-pro', 'google/gemini-1.5-flash',
55
+ 'deepseek/deepseek-chat', 'deepseek/deepseek-coder', 'deepseek/deepseek-r1',
56
+ 'meta-llama/llama-3.3-70b-instruct', 'meta-llama/llama-3.1-405b-instruct',
57
+ 'x-ai/grok-2-1212', 'x-ai/grok-2-vision-1212',
58
+ 'mistralai/mistral-large', 'mistralai/mixtral-8x22b-instruct'
59
+ ],
60
+ Nvidia: [
61
+ 'minimaxai/minimax-m3', 'moonshotai/kimi-k2.6',
62
+ 'deepseek-ai/deepseek-v4-pro', 'z-ai/glm-5.1',
63
+ 'minimaxai/minimax-m1-40k', 'moonshotai/kimi-k2',
64
+ 'deepseek-ai/deepseek-r1-0528', 'z-ai/glm-4.5',
65
+ 'nvidia/llama-3.1-nemotron-70b-instruct', 'meta/llama-3.1-405b-instruct',
66
+ 'meta/llama-3.1-70b-instruct', 'mistralai/mistral-large-2-instruct',
67
+ 'mistralai/mixtral-8x22b-instruct', 'google/gemma-2-27b-it',
68
+ 'microsoft/phi-3-medium-128k-instruct', 'nvidia/nemotron-4-340b-instruct'
69
+ ],
70
+ Mistral: [
71
+ 'mistral-large-latest', 'mistral-large-2411', 'mistral-large-2407',
72
+ 'pixtral-large-latest', 'pixtral-large-2411',
73
+ 'ministral-3b-latest', 'ministral-8b-latest',
74
+ 'mistral-small-latest', 'mistral-small-2409',
75
+ 'codestral-latest', 'codestral-2501',
76
+ 'open-mistral-nemo', 'open-mixtral-8x22b'
77
+ ],
78
+ Cohere: [
79
+ 'command-r-plus-08-2024', 'command-r-08-2024',
80
+ 'command-r-plus', 'command-r', 'command', 'command-light'
81
+ ],
82
+ };
83
+
84
+ const EMBEDDING_MODELS = [
85
+ 'text-embedding-3-small (OpenAI)', 'text-embedding-3-large (OpenAI)',
86
+ 'nomic-embed-text (Nomic)', 'bge-m3 (BAAI)', 'e5-mistral-7b-instruct',
87
+ ];
88
+
89
+ // ─── Key Row ─────────────────────────────────────────────────────────────────
90
+ function KeyRow({ value, isActive, onToggleActive, onChange, onDelete, index }) {
91
+ const [show, setShow] = useState(!value); // auto-show when empty/new
92
+ const masked = value.length > 12 ? value.slice(0, 6) + '••••••••••••' + value.slice(-4) : '••••';
93
+
94
+ return (
95
+ <div className={`flex items-center gap-2 p-2.5 rounded-xl border transition-all ${isActive ? 'border-blue-500 bg-blue-950/40' : 'border-white/10 bg-white/5 hover:border-white/20'}`}>
96
+ {/* Active dot */}
97
+ <button title={isActive ? 'Active key' : 'Click to use this key'} onClick={onToggleActive}
98
+ className={`w-2.5 h-2.5 rounded-full flex-shrink-0 border-2 transition-all ${isActive ? 'bg-blue-400 border-blue-400 shadow-[0_0_6px_rgba(96,165,250,0.8)]' : 'bg-transparent border-gray-600 hover:border-blue-400'}`}
99
+ />
100
+ {/* Index */}
101
+ <span className="text-[10px] text-gray-600 flex-shrink-0 w-4">#{index + 1}</span>
102
+ {/* Input always present, toggle visibility */}
103
+ <div className="flex-grow relative">
104
+ <input
105
+ type={show ? 'text' : 'password'}
106
+ value={value}
107
+ onChange={e => onChange(e.target.value)}
108
+ placeholder="Paste your API key here..."
109
+ className="w-full bg-transparent text-white text-xs font-mono outline-none placeholder-gray-600 pr-2"
110
+ autoFocus={!value}
111
+ />
112
+ {!show && value && (
113
+ <span className="absolute inset-0 flex items-center text-xs font-mono text-gray-400 pointer-events-none">{masked}</span>
114
+ )}
115
+ </div>
116
+ {/* Eye toggle */}
117
+ <button onClick={() => setShow(s => !s)} className="text-gray-600 hover:text-gray-300 transition-colors flex-shrink-0">
118
+ {show ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
119
+ </button>
120
+ {/* Delete */}
121
+ <button onClick={onDelete} className="text-gray-700 hover:text-red-400 transition-colors flex-shrink-0">
122
+ <Trash2 className="w-3.5 h-3.5" />
123
+ </button>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // ─── Model Picker ────────────────────────────────────────────────────────────
129
+ function ModelPicker({ provider, value, onChange }) {
130
+ const [query, setQuery] = useState('');
131
+ const allModels = MODELS[provider] || [];
132
+ const filtered = query ? allModels.filter(m => m.toLowerCase().includes(query.toLowerCase())) : allModels;
133
+
134
+ return (
135
+ <div className="flex flex-col gap-2">
136
+ <div className="flex items-center gap-2 bg-white/5 border border-white/10 rounded-xl px-3 py-2">
137
+ <Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
138
+ <input
139
+ value={query}
140
+ onChange={e => setQuery(e.target.value)}
141
+ placeholder={`Search ${allModels.length} models...`}
142
+ className="bg-transparent text-white text-xs outline-none flex-grow placeholder-gray-600"
143
+ />
144
+ {query && <button onClick={() => setQuery('')} className="text-gray-600 hover:text-gray-300"><X className="w-3 h-3" /></button>}
145
+ </div>
146
+ <div className="max-h-48 overflow-y-auto rounded-xl border border-white/10 bg-white/5 divide-y divide-white/5">
147
+ {filtered.length === 0 && <p className="text-xs text-gray-600 text-center py-4">No models match "{query}"</p>}
148
+ {filtered.map(m => (
149
+ <button key={m} onClick={() => onChange(m)}
150
+ className={`w-full text-left px-3 py-2 text-xs font-mono transition-colors hover:bg-white/10 ${value === m ? 'text-blue-400 bg-blue-950/40' : 'text-gray-300'}`}>
151
+ {value === m && <span className="mr-2">●</span>}{m}
152
+ </button>
153
+ ))}
154
+ </div>
155
+ {value && <div className="text-[10px] text-gray-500 px-1">Selected: <span className="text-blue-400 font-mono">{value}</span></div>}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ // ─── Main App ────────────────────────────────────────────────────────────────
161
+ const App = () => {
162
+ const [config, setConfig] = useState(loadConfig);
163
+ const [showComputer, setShowComputer] = useState(true);
164
+ const [messages, setMessages] = useState([]);
165
+ const [inputText, setInputText] = useState('');
166
+ const [loading, setLoading] = useState(false);
167
+ const [activeModal, setActiveModal] = useState(null);
168
+ const [draft, setDraft] = useState(null);
169
+ const [providerTab, setProviderTab] = useState('Groq');
170
+
171
+ const openModal = (name) => {
172
+ setDraft(JSON.parse(JSON.stringify(config)));
173
+ setProviderTab(config.activeProvider || 'Groq');
174
+ setActiveModal(name);
175
+ };
176
+
177
+ const commitSave = () => { setConfig(draft); saveConfig(draft); setActiveModal(null); };
178
+
179
+ // Switch tab and update selected model if current one isn't in new provider
180
+ const switchTab = (tab) => {
181
+ setProviderTab(tab);
182
+ setDraft(d => {
183
+ const tabModels = MODELS[tab] || [];
184
+ const modelValid = tabModels.includes(d.selectedModel);
185
+ return modelValid ? d : { ...d, selectedModel: tabModels[0] || d.selectedModel };
186
+ });
187
+ };
188
+
189
+ const handleSend = async () => {
190
+ if (!inputText.trim() || loading) return;
191
+ setMessages(prev => [...prev, { role: 'user', text: inputText }]);
192
+ const prompt = inputText;
193
+ setInputText('');
194
+ setLoading(true);
195
+ try {
196
+ const res = await fetch('/chat', {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify({ prompt, model: config.selectedModel, api_key: config.activeKey, provider: config.activeProvider }),
200
+ });
201
+ const data = await res.json();
202
+ if (!res.ok) {
203
+ throw new Error(data.detail || `HTTP ${res.status}`);
204
+ }
205
+ setMessages(prev => [...prev, { role: 'agent', text: data.response, doc: true }]);
206
+ } catch (e) {
207
+ setMessages(prev => [...prev, { role: 'agent', text: `Error: ${e.message}` }]);
208
+ } finally { setLoading(false); }
209
+ };
210
+
211
+ // ── Providers Modal ───────────────────────────────────────────────────────────
212
+ const renderProviders = () => {
213
+ if (!draft || activeModal !== 'providers') return null;
214
+ const keys = draft.providers[providerTab] || [];
215
+
216
+ return (
217
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
218
+ <div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-2xl shadow-2xl flex flex-col" style={{ maxHeight: '90vh' }}>
219
+ {/* Header */}
220
+ <div className="flex justify-between items-center px-6 py-4 border-b border-white/10 flex-shrink-0">
221
+ <h2 className="text-base font-bold text-white">LLM Providers</h2>
222
+ <button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white transition-colors"><X className="w-5 h-5" /></button>
223
+ </div>
224
+
225
+ {/* Provider tabs */}
226
+ <div className="flex gap-1.5 px-6 pt-4 flex-wrap flex-shrink-0">
227
+ {Object.keys(MODELS).map(p => {
228
+ const count = (draft.providers[p] || []).filter(k => k.trim()).length;
229
+ return (
230
+ <button key={p} onClick={() => switchTab(p)}
231
+ className={`px-3 py-1.5 text-xs rounded-full border transition-all font-medium ${providerTab === p ? 'bg-blue-600 border-blue-500 text-white' : 'bg-white/5 border-white/10 text-gray-400 hover:text-white hover:border-white/25'}`}>
232
+ {p}
233
+ {count > 0 && <span className="ml-1.5 bg-green-500 text-black text-[9px] px-1.5 py-0.5 rounded-full font-bold">{count}</span>}
234
+ </button>
235
+ );
236
+ })}
237
+ </div>
238
+
239
+ {/* Scrollable body */}
240
+ <div className="flex-grow overflow-y-auto px-6 py-4 space-y-6">
241
+
242
+ {/* API Keys section */}
243
+ <div>
244
+ <div className="flex justify-between items-center mb-3">
245
+ <div>
246
+ <label className="text-xs font-semibold text-gray-300 uppercase tracking-widest">API Keys for {providerTab}</label>
247
+ <p className="text-[10px] text-gray-600 mt-0.5">Add multiple keys — click ● to set active</p>
248
+ </div>
249
+ <button onClick={() => setDraft(d => ({ ...d, providers: { ...d.providers, [providerTab]: [...(d.providers[providerTab] || []), ''] } }))}
250
+ className="flex items-center gap-1.5 text-xs text-blue-400 hover:text-blue-300 border border-blue-800 bg-blue-900/20 hover:bg-blue-900/40 px-3 py-1.5 rounded-lg transition-all">
251
+ <Plus className="w-3.5 h-3.5" /> Add Key
252
+ </button>
253
+ </div>
254
+ <div className="space-y-2">
255
+ {keys.length === 0
256
+ ? <div className="border border-dashed border-white/10 rounded-xl py-6 text-center text-xs text-gray-600">No keys yet — click <span className="text-blue-400">Add Key</span> above</div>
257
+ : keys.map((k, i) => (
258
+ <KeyRow key={i} index={i} value={k}
259
+ isActive={draft.activeProvider === providerTab && draft.activeKey === k && k !== ''}
260
+ onToggleActive={() => { if (k) setDraft(d => ({ ...d, activeProvider: providerTab, activeKey: k })); }}
261
+ onChange={val => setDraft(d => { const nk = [...(d.providers[providerTab] || [])]; nk[i] = val; return { ...d, providers: { ...d.providers, [providerTab]: nk } }; })}
262
+ onDelete={() => setDraft(d => { const nk = (d.providers[providerTab] || []).filter((_, idx) => idx !== i); return { ...d, providers: { ...d.providers, [providerTab]: nk }, activeKey: d.activeKey === k ? '' : d.activeKey }; })}
263
+ />
264
+ ))
265
+ }
266
+ </div>
267
+ </div>
268
+
269
+ {/* Model picker */}
270
+ <div>
271
+ <label className="text-xs font-semibold text-gray-300 uppercase tracking-widest block mb-3">
272
+ Select Model <span className="normal-case font-normal text-gray-600 ml-1">({MODELS[providerTab]?.length} available for {providerTab})</span>
273
+ </label>
274
+ <ModelPicker
275
+ provider={providerTab}
276
+ value={draft.selectedModel}
277
+ onChange={m => setDraft(d => {
278
+ // Auto-switch activeProvider to the tab where model was selected
279
+ const hasKeyForTab = (d.providers[providerTab] || []).some(k => k.trim());
280
+ return {
281
+ ...d,
282
+ selectedModel: m,
283
+ activeProvider: hasKeyForTab ? providerTab : d.activeProvider,
284
+ // If switching provider, clear active key so user picks one
285
+ activeKey: hasKeyForTab ? (d.activeProvider === providerTab ? d.activeKey : (d.providers[providerTab] || [])[0] || '') : d.activeKey,
286
+ };
287
+ })}
288
+ />
289
+ </div>
290
+
291
+ {/* Active config summary */}
292
+ {draft.activeKey ? (
293
+ <div className="bg-green-950/40 border border-green-800/50 rounded-xl p-3 text-xs flex items-center gap-3">
294
+ <div className="w-2 h-2 bg-green-400 rounded-full shadow-[0_0_6px_rgba(74,222,128,0.8)] flex-shrink-0" />
295
+ <div>
296
+ <span className="text-green-300 font-semibold">{draft.activeProvider}</span>
297
+ <span className="text-gray-500 mx-2">·</span>
298
+ <span className="text-gray-300 font-mono">{draft.selectedModel}</span>
299
+ <span className="text-gray-500 mx-2">·</span>
300
+ <span className="text-gray-400 font-mono">key ends in …{draft.activeKey.slice(-4)}</span>
301
+ </div>
302
+ </div>
303
+ ) : (
304
+ <div className="bg-yellow-950/30 border border-yellow-800/40 rounded-xl p-3 text-xs text-yellow-600">
305
+ No active key — add a key and click the ● dot to activate it
306
+ </div>
307
+ )}
308
+ </div>
309
+
310
+ {/* Footer */}
311
+ <div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3 flex-shrink-0">
312
+ <button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5 transition-colors">Cancel</button>
313
+ <button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2 font-medium transition-colors">
314
+ <Save className="w-4 h-4" /> Save & Apply
315
+ </button>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ );
320
+ };
321
+
322
+ // ── RAG Modal ────────────────────────────────────────────────────────────────
323
+ const renderRag = () => {
324
+ if (!draft || activeModal !== 'rag') return null;
325
+ return (
326
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
327
+ <div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl flex flex-col" style={{ maxHeight: '85vh' }}>
328
+ <div className="flex justify-between items-center px-6 py-4 border-b border-white/10">
329
+ <h2 className="text-base font-bold text-white">RAG Pipeline</h2>
330
+ <button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white"><X className="w-5 h-5" /></button>
331
+ </div>
332
+ <div className="p-6 space-y-4 overflow-y-auto flex-grow">
333
+ {[
334
+ { label: 'Vector Database URL', key: 'vectorDbUrl', placeholder: 'https://your-pinecone-or-weaviate-url...' },
335
+ ].map(({ label, key, placeholder }) => (
336
+ <div key={key}>
337
+ <label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">{label}</label>
338
+ <input type="text" placeholder={placeholder} value={draft.ragConfig?.[key] || ''}
339
+ onChange={e => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, [key]: e.target.value } }))}
340
+ className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors" />
341
+ </div>
342
+ ))}
343
+ <div>
344
+ <label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">Embedding Model</label>
345
+ <div className="space-y-1">
346
+ {EMBEDDING_MODELS.map(m => (
347
+ <button key={m} onClick={() => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, embeddingModel: m } }))}
348
+ className={`w-full text-left px-3 py-2 text-xs rounded-lg border transition-all ${draft.ragConfig?.embeddingModel === m ? 'border-blue-500 bg-blue-950/40 text-blue-300' : 'border-white/10 bg-white/5 text-gray-400 hover:text-white hover:border-white/20'}`}>
349
+ {draft.ragConfig?.embeddingModel === m && '● '}{m}
350
+ </button>
351
+ ))}
352
+ </div>
353
+ </div>
354
+ <div>
355
+ <label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">System Prompt Override</label>
356
+ <textarea rows="5" placeholder="You are an autonomous coding agent..." value={draft.ragConfig?.systemPrompt || ''}
357
+ onChange={e => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, systemPrompt: e.target.value } }))}
358
+ className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors resize-none" />
359
+ </div>
360
+ </div>
361
+ <div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3">
362
+ <button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5">Cancel</button>
363
+ <button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2"><Save className="w-4 h-4" /> Save</button>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ );
368
+ };
369
+
370
+ // ── MCP Modal ────────────────────────────────────────────────────────────────
371
+ const renderMcp = () => {
372
+ if (!draft || activeModal !== 'mcp') return null;
373
+ return (
374
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
375
+ <div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl flex flex-col" style={{ maxHeight: '85vh' }}>
376
+ <div className="flex justify-between items-center px-6 py-4 border-b border-white/10">
377
+ <h2 className="text-base font-bold text-white">MCP Servers</h2>
378
+ <button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white"><X className="w-5 h-5" /></button>
379
+ </div>
380
+ <div className="p-6 space-y-3 overflow-y-auto flex-grow">
381
+ <p className="text-xs text-gray-500">Each server exposes tools the agent can call via Model Context Protocol.</p>
382
+ {(draft.mcpServers || ['']).map((url, i) => (
383
+ <div key={i} className="flex gap-2">
384
+ <input type="text" value={url} placeholder={`https://mcp-server-${i + 1}.example.com`}
385
+ onChange={e => setDraft(d => { const s = [...(d.mcpServers || [])]; s[i] = e.target.value; return { ...d, mcpServers: s }; })}
386
+ className="flex-grow bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors font-mono" />
387
+ <button onClick={() => setDraft(d => ({ ...d, mcpServers: (d.mcpServers || []).filter((_, idx) => idx !== i) }))}
388
+ className="text-gray-600 hover:text-red-400 transition-colors px-2"><Trash2 className="w-4 h-4" /></button>
389
+ </div>
390
+ ))}
391
+ <button onClick={() => setDraft(d => ({ ...d, mcpServers: [...(d.mcpServers || []), ''] }))}
392
+ className="w-full py-2 text-xs text-blue-400 border border-dashed border-blue-900 bg-blue-900/10 hover:bg-blue-900/20 rounded-xl transition-colors flex items-center justify-center gap-1.5">
393
+ <Plus className="w-3.5 h-3.5" /> Add Server
394
+ </button>
395
+ </div>
396
+ <div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3">
397
+ <button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5">Cancel</button>
398
+ <button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2"><Save className="w-4 h-4" /> Save</button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ );
403
+ };
404
+
405
+ return (
406
+ <div className="flex h-screen overflow-hidden bg-[#0a0a0a] text-[#e8e8e8] font-sans">
407
+ {/* Sidebar */}
408
+ <div className="w-[220px] h-screen border-r border-white/5 bg-[#0d0d0d] flex flex-col p-3 shrink-0">
409
+ <div className="flex items-center gap-2 px-2 mb-6 font-bold text-sm">
410
+ <Bot className="w-4 h-4 text-blue-400" /> Manus Clone
411
+ </div>
412
+ {config.activeKey && (
413
+ <div className="mb-3 mx-2 px-2 py-2 bg-green-950/30 border border-green-900/50 rounded-lg">
414
+ <div className="text-[9px] font-bold text-green-400 uppercase tracking-widest">● Active</div>
415
+ <div className="text-[10px] text-gray-400 mt-0.5">{config.activeProvider}</div>
416
+ <div className="text-[9px] text-gray-600 font-mono truncate">{config.selectedModel}</div>
417
+ </div>
418
+ )}
419
+ <nav className="space-y-0.5">
420
+ {[
421
+ { icon: PlusSquare, label: 'New task', onClick: () => setMessages([]) },
422
+ { icon: Settings, label: 'Providers', onClick: () => openModal('providers') },
423
+ { icon: Database, label: 'RAG Settings', onClick: () => openModal('rag') },
424
+ { icon: Plug, label: 'MCP Servers', onClick: () => openModal('mcp') },
425
+ ].map(({ icon: Icon, label, onClick }) => (
426
+ <button key={label} onClick={onClick}
427
+ className="w-full flex items-center gap-3 px-2 py-2.5 rounded-lg text-gray-400 hover:text-white hover:bg-white/5 transition-colors text-sm text-left">
428
+ <Icon className="w-4 h-4 flex-shrink-0" /> {label}
429
+ </button>
430
+ ))}
431
+ </nav>
432
+ </div>
433
+
434
+ {/* Chat */}
435
+ <div className="flex-grow flex flex-col overflow-hidden relative">
436
+ <div className="flex-grow overflow-y-auto">
437
+ <div className="max-w-2xl mx-auto p-8 space-y-5 pb-32">
438
+ {messages.length === 0 && (
439
+ <div className="text-center mt-20">
440
+ <Bot className="w-10 h-10 mx-auto mb-4 text-gray-700" />
441
+ <p className="text-gray-600 text-lg font-light">How can I help?</p>
442
+ {!config.activeKey && (
443
+ <button onClick={() => openModal('providers')}
444
+ className="mt-4 text-xs text-yellow-600 border border-yellow-900/50 bg-yellow-950/20 px-4 py-2 rounded-lg hover:bg-yellow-950/40 transition-colors">
445
+ ⚠ No API key — click to configure
446
+ </button>
447
+ )}
448
+ </div>
449
+ )}
450
+ {messages.map((msg, i) => (
451
+ <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
452
+ {msg.doc
453
+ ? <div className="bg-[#111] border border-white/10 rounded-2xl p-5 w-full">
454
+ <div className="flex items-center gap-2 text-blue-400 mb-3 text-[10px] font-bold uppercase tracking-widest">
455
+ <FileText className="w-3 h-3" /> Agent Output
456
+ </div>
457
+ <p className="text-sm text-gray-300 whitespace-pre-wrap leading-relaxed">{msg.text}</p>
458
+ </div>
459
+ : <div className="bg-[#1a1a2e] border border-white/10 px-4 py-3 rounded-2xl rounded-br-sm max-w-[80%] text-sm">{msg.text}</div>
460
+ }
461
+ </div>
462
+ ))}
463
+ {loading && (
464
+ <div className="flex gap-1.5 px-4 py-3">
465
+ {[0, 150, 300].map(d => <div key={d} className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: `${d}ms` }} />)}
466
+ </div>
467
+ )}
468
+ </div>
469
+ </div>
470
+
471
+ {/* Input bar */}
472
+ <div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/90 to-transparent">
473
+ <div className="bg-[#141414] border border-white/10 rounded-2xl px-4 py-3 max-w-2xl mx-auto flex items-center gap-3">
474
+ <button onClick={() => setShowComputer(s => !s)} className="text-gray-600 hover:text-gray-300 transition-colors">
475
+ <Monitor className="w-4 h-4" />
476
+ </button>
477
+ <input type="text" value={inputText} onChange={e => setInputText(e.target.value)}
478
+ onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
479
+ placeholder={config.activeKey ? `Message (${config.selectedModel})…` : 'Set an API key first…'}
480
+ className="flex-grow bg-transparent text-sm text-white outline-none placeholder-gray-600" />
481
+ <button onClick={handleSend} disabled={loading || !inputText.trim()}
482
+ className="bg-blue-600 hover:bg-blue-500 disabled:bg-white/10 disabled:cursor-not-allowed p-2 rounded-xl transition-colors">
483
+ <ArrowUp className="w-4 h-4 text-white" />
484
+ </button>
485
+ </div>
486
+ </div>
487
+ </div>
488
+
489
+ {/* Computer panel */}
490
+ {showComputer && (
491
+ <div className="w-[38%] shrink-0 border-l border-white/5 bg-black h-screen flex flex-col">
492
+ <div className="px-4 py-3 border-b border-white/5 text-[10px] text-gray-600 font-mono flex justify-between items-center">
493
+ <span>Manus's computer › Editor</span>
494
+ <button onClick={() => setShowComputer(false)}><X className="w-3 h-3 hover:text-white" /></button>
495
+ </div>
496
+ <div className="flex-grow p-4 font-mono text-xs text-green-400 overflow-y-auto">
497
+ <span className="text-gray-700"># Logic Engine Connected</span><br /><br />
498
+ <span className="text-blue-400">def</span> <span className="text-yellow-300">run</span>():<br />
499
+ &nbsp;&nbsp;&nbsp;&nbsp;print(<span className="text-orange-300">"Agent ready…"</span>)<br />
500
+ <span className="animate-pulse text-gray-700">█</span>
501
+ </div>
502
+ <div className="px-4 py-3 border-t border-white/5 flex items-center gap-3 text-[10px] text-gray-700">
503
+ <Play className="w-3 h-3 hover:text-white cursor-pointer" />
504
+ <SkipForward className="w-3 h-3 hover:text-white cursor-pointer" />
505
+ <div className="flex-grow h-0.5 bg-white/5 rounded"><div className="h-full bg-blue-600 rounded w-full" /></div>
506
+ <span className="text-blue-500">Live</span>
507
+ </div>
508
+ </div>
509
+ )}
510
+
511
+ {renderProviders()}
512
+ {renderRag()}
513
+ {renderMcp()}
514
+ </div>
515
+ );
516
+ };
517
+
518
+ export default App;
node3-ui/src/index.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer utilities {
6
+ .no-scrollbar::-webkit-scrollbar {
7
+ display: none;
8
+ }
9
+ .no-scrollbar {
10
+ -ms-overflow-style: none;
11
+ scrollbar-width: none;
12
+ }
13
+ }
node3-ui/src/main.jsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ // Error Boundary / Global Error Handler to catch white-screen issues
7
+ window.addEventListener('error', (event) => {
8
+ document.body.innerHTML = `<div style="color:red; padding:20px; font-size:20px; background:black; height:100vh;">
9
+ <h1>React Crash Error:</h1>
10
+ <pre>${event.error ? event.error.stack : event.message}</pre>
11
+ </div>`;
12
+ });
13
+
14
+ window.addEventListener('unhandledrejection', (event) => {
15
+ document.body.innerHTML = `<div style="color:orange; padding:20px; font-size:20px; background:black; height:100vh;">
16
+ <h1>Unhandled Promise Rejection:</h1>
17
+ <pre>${event.reason}</pre>
18
+ </div>`;
19
+ });
20
+
21
+ ReactDOM.createRoot(document.getElementById('root')).render(
22
+ <React.StrictMode>
23
+ <App />
24
+ </React.StrictMode>,
25
+ )
node3-ui/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
node3-ui/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ base: './', // Use relative paths for assets to prevent proxy 404s
7
+ })
start.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "=== Starting Redis (2GB max, LRU eviction) ==="
5
+ redis-server \
6
+ --daemonize yes \
7
+ --maxmemory 2gb \
8
+ --maxmemory-policy allkeys-lru \
9
+ --save "" \
10
+ --loglevel warning
11
+
12
+ echo "=== Redis started on localhost:6379 ==="
13
+ redis-cli ping
14
+
15
+ echo "=== Starting FastAPI on port 7860 ==="
16
+ exec uvicorn main:app --host 0.0.0.0 --port 7860