Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +19 -4
- README.md +3 -3
- main.py +47 -10
- node3-ui/Dockerfile +18 -0
- node3-ui/index.html +19 -0
- node3-ui/package.json +23 -0
- node3-ui/postcss.config.js +6 -0
- node3-ui/src/App.jsx +518 -0
- node3-ui/src/index.css +13 -0
- node3-ui/src/main.jsx +25 -0
- node3-ui/tailwind.config.js +11 -0
- node3-ui/vite.config.js +7 -0
- start.sh +16 -0
Dockerfile
CHANGED
|
@@ -2,12 +2,27 @@ FROM python:3.12-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
# Install
|
| 6 |
-
RUN
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
#
|
|
|
|
|
|
|
|
|
|
| 9 |
COPY main.py .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
EXPOSE 7860
|
| 12 |
|
| 13 |
-
CMD ["
|
|
|
|
| 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:
|
| 3 |
emoji: 🤖
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
@@ -8,6 +8,6 @@ pinned: false
|
|
| 8 |
app_port: 7860
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
-
|
|
|
|
| 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="
|
| 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 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
| 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'[
|
| 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}: {
|
| 91 |
raise HTTPException(status_code=status, detail=detail)
|
| 92 |
except Exception as e:
|
| 93 |
raise HTTPException(status_code=500, detail=str(e))
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
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
|