Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- PLANS.md +1 -0
- TASKS.md +1 -0
- app/routes/base.py +33 -1
- app/server.py +6 -1
- static/dashboard.html +11 -55
- static/dashboard.js +53 -46
- static/docs.html +106 -31
PLANS.md
CHANGED
|
@@ -109,3 +109,4 @@ Note: see `docs/PASSWORD_MANAGER_SCOPE.md` for the current (non-vault) stance an
|
|
| 109 |
- Open App and Login should point to login page.
|
| 110 |
- Provider setting on path /app should be only on settings page.
|
| 111 |
- Forget password not working. Fix.
|
|
|
|
|
|
| 109 |
- Open App and Login should point to login page.
|
| 110 |
- Provider setting on path /app should be only on settings page.
|
| 111 |
- Forget password not working. Fix.
|
| 112 |
+
- Remove provider settings from dashboard UI (configure via `DEFAULT_*` secrets).
|
TASKS.md
CHANGED
|
@@ -22,6 +22,7 @@ Legend:
|
|
| 22 |
## P2 — UI/UX, settings, admin, landing
|
| 23 |
- [x] Landing + route split (`/` landing, `/login`, `/app`) and UI redirects updated.
|
| 24 |
- [x] Add `/docs` page and route `Get started` there.
|
|
|
|
| 25 |
- [x] Split `static/dashboard.html` into JS/CSS files (`static/dashboard.js`, `static/dashboard.css`).
|
| 26 |
- [x] Theme tokens shared across login + dashboard (single source of truth via `static/theme.css`).
|
| 27 |
- [x] Separate Settings vs Admin pages (route-focused `/settings` and `/admin` views).
|
|
|
|
| 22 |
## P2 — UI/UX, settings, admin, landing
|
| 23 |
- [x] Landing + route split (`/` landing, `/login`, `/app`) and UI redirects updated.
|
| 24 |
- [x] Add `/docs` page and route `Get started` there.
|
| 25 |
+
- [x] Publish docs + API docs endpoints (`/api/app-docs`, `/api/docs`).
|
| 26 |
- [x] Split `static/dashboard.html` into JS/CSS files (`static/dashboard.js`, `static/dashboard.css`).
|
| 27 |
- [x] Theme tokens shared across login + dashboard (single source of truth via `static/theme.css`).
|
| 28 |
- [x] Separate Settings vs Admin pages (route-focused `/settings` and `/admin` views).
|
app/routes/base.py
CHANGED
|
@@ -3,13 +3,21 @@ from __future__ import annotations
|
|
| 3 |
import os
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
-
from fastapi import APIRouter
|
| 7 |
from fastapi.responses import FileResponse
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
| 11 |
_ROOT = Path(__file__).resolve().parents[2]
|
| 12 |
_STATIC = _ROOT / "static"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
def _config_payload() -> dict:
|
|
@@ -31,6 +39,30 @@ async def get_config():
|
|
| 31 |
async def get_api_config():
|
| 32 |
return _config_payload()
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
@router.get("/")
|
| 35 |
async def read_index():
|
| 36 |
return FileResponse(str(_STATIC / "landing.html"))
|
|
|
|
| 3 |
import os
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
+
from fastapi import APIRouter, HTTPException
|
| 7 |
from fastapi.responses import FileResponse
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
| 11 |
_ROOT = Path(__file__).resolve().parents[2]
|
| 12 |
_STATIC = _ROOT / "static"
|
| 13 |
+
_DOCS_ROOT = _ROOT / "docs"
|
| 14 |
+
|
| 15 |
+
_DOC_PAGES: dict[str, tuple[str, Path]] = {
|
| 16 |
+
"architecture": ("Architecture", _DOCS_ROOT / "ARCHITECTURE.md"),
|
| 17 |
+
"troubleshooting": ("Troubleshooting", _DOCS_ROOT / "TROUBLESHOOTING.md"),
|
| 18 |
+
"security-deployment": ("Security deployment", _DOCS_ROOT / "SECURITY_DEPLOYMENT.md"),
|
| 19 |
+
"password-manager-scope": ("Password manager scope", _DOCS_ROOT / "PASSWORD_MANAGER_SCOPE.md"),
|
| 20 |
+
}
|
| 21 |
|
| 22 |
|
| 23 |
def _config_payload() -> dict:
|
|
|
|
| 39 |
async def get_api_config():
|
| 40 |
return _config_payload()
|
| 41 |
|
| 42 |
+
|
| 43 |
+
@router.get("/api/app-docs")
|
| 44 |
+
async def list_app_docs():
|
| 45 |
+
return {
|
| 46 |
+
"pages": [
|
| 47 |
+
{"slug": slug, "title": title}
|
| 48 |
+
for slug, (title, _path) in sorted(_DOC_PAGES.items(), key=lambda kv: kv[1][0].lower())
|
| 49 |
+
]
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@router.get("/api/app-docs/{slug}")
|
| 54 |
+
async def get_app_doc(slug: str):
|
| 55 |
+
entry = _DOC_PAGES.get(slug)
|
| 56 |
+
if not entry:
|
| 57 |
+
raise HTTPException(status_code=404, detail="Doc not found")
|
| 58 |
+
title, path = entry
|
| 59 |
+
try:
|
| 60 |
+
markdown = path.read_text(encoding="utf-8")
|
| 61 |
+
except FileNotFoundError as e:
|
| 62 |
+
raise HTTPException(status_code=404, detail="Doc not found") from e
|
| 63 |
+
return {"slug": slug, "title": title, "markdown": markdown}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
@router.get("/")
|
| 67 |
async def read_index():
|
| 68 |
return FileResponse(str(_STATIC / "landing.html"))
|
app/server.py
CHANGED
|
@@ -106,7 +106,12 @@ async def lifespan(app: FastAPI):
|
|
| 106 |
|
| 107 |
def create_app() -> FastAPI:
|
| 108 |
_ensure_supabase_asset()
|
| 109 |
-
app = FastAPI(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
@app.exception_handler(StarletteHTTPException)
|
| 112 |
async def _http_exception_handler(_request, exc: StarletteHTTPException):
|
|
|
|
| 106 |
|
| 107 |
def create_app() -> FastAPI:
|
| 108 |
_ensure_supabase_asset()
|
| 109 |
+
app = FastAPI(
|
| 110 |
+
lifespan=lifespan,
|
| 111 |
+
docs_url="/api/docs",
|
| 112 |
+
redoc_url="/api/redoc",
|
| 113 |
+
openapi_url="/api/openapi.json",
|
| 114 |
+
)
|
| 115 |
|
| 116 |
@app.exception_handler(StarletteHTTPException)
|
| 117 |
async def _http_exception_handler(_request, exc: StarletteHTTPException):
|
static/dashboard.html
CHANGED
|
@@ -140,16 +140,16 @@
|
|
| 140 |
<h2 class="text-lg font-semibold text-gray-100">Getting started</h2>
|
| 141 |
<p class="text-sm text-gray-400 mt-1">A quick checklist to get productive in under a minute.</p>
|
| 142 |
</div>
|
| 143 |
-
<button onclick="
|
| 144 |
class="shrink-0 px-3 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 text-sm font-medium transition">
|
| 145 |
-
|
| 146 |
</button>
|
| 147 |
</div>
|
| 148 |
|
| 149 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
|
| 150 |
<div class="p-3 rounded-xl bg-gray-900/40 border border-gray-700">
|
| 151 |
-
<div class="text-sm font-semibold text-gray-200">1)
|
| 152 |
-
<div class="text-sm text-gray-400 mt-1">
|
| 153 |
</div>
|
| 154 |
<div class="p-3 rounded-xl bg-gray-900/40 border border-gray-700">
|
| 155 |
<div class="text-sm font-semibold text-gray-200">2) Start chatting</div>
|
|
@@ -225,55 +225,10 @@
|
|
| 225 |
<button onclick="closeSettings()" class="text-gray-400 hover:text-white px-2 py-1 rounded hover:bg-white/10">×</button>
|
| 226 |
</div>
|
| 227 |
<div id="settings-content" class="p-4 space-y-4 overflow-y-auto" style="max-height: calc(100% - 56px);">
|
| 228 |
-
<div class="
|
| 229 |
-
<div>
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
<select id="saved-providers" onchange="loadSelectedProvider(this.value)"
|
| 233 |
-
class="flex-grow bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500">
|
| 234 |
-
<option value="">Select...</option>
|
| 235 |
-
</select>
|
| 236 |
-
<button onclick="saveProvider()"
|
| 237 |
-
class="bg-green-600 hover:bg-green-700 text-white px-2 rounded text-xs">Save</button>
|
| 238 |
-
<button onclick="deleteProvider()"
|
| 239 |
-
class="bg-red-600 hover:bg-red-700 text-white px-2 rounded text-xs">Del</button>
|
| 240 |
-
</div>
|
| 241 |
-
</div>
|
| 242 |
-
<div>
|
| 243 |
-
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">Provider Preset</label>
|
| 244 |
-
<div class="flex gap-2">
|
| 245 |
-
<select id="provider-presets" onchange="applyProviderPreset(this.value)"
|
| 246 |
-
class="flex-grow bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500">
|
| 247 |
-
<option value="">Select preset...</option>
|
| 248 |
-
</select>
|
| 249 |
-
<button onclick="applyProviderPreset(document.getElementById('provider-presets').value)"
|
| 250 |
-
class="bg-gray-600 hover:bg-gray-700 text-white px-2 rounded text-xs">Apply</button>
|
| 251 |
-
</div>
|
| 252 |
-
</div>
|
| 253 |
-
<div>
|
| 254 |
-
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">API Key</label>
|
| 255 |
-
<input type="password" id="chat-api-key" placeholder="sk-..."
|
| 256 |
-
class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500">
|
| 257 |
-
<div class="text-xs text-gray-500 mt-1">Stored per-user when you click Save.</div>
|
| 258 |
-
</div>
|
| 259 |
-
<div class="grid grid-cols-2 gap-3">
|
| 260 |
-
<div>
|
| 261 |
-
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">Base URL</label>
|
| 262 |
-
<input type="text" id="chat-base-url" placeholder="https://api..."
|
| 263 |
-
class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500">
|
| 264 |
-
</div>
|
| 265 |
-
<div>
|
| 266 |
-
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">Model</label>
|
| 267 |
-
<div class="flex gap-1">
|
| 268 |
-
<input type="text" id="chat-model"
|
| 269 |
-
class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500"
|
| 270 |
-
list="model-list">
|
| 271 |
-
<datalist id="model-list"></datalist>
|
| 272 |
-
<button onclick="fetchModels()"
|
| 273 |
-
class="bg-blue-600 hover:bg-blue-700 text-white px-2 rounded text-xs">↻</button>
|
| 274 |
-
</div>
|
| 275 |
-
</div>
|
| 276 |
-
</div>
|
| 277 |
</div>
|
| 278 |
|
| 279 |
<div class="pt-4 border-t border-gray-700">
|
|
@@ -336,7 +291,7 @@
|
|
| 336 |
</div>
|
| 337 |
</div>
|
| 338 |
<div class="text-xs text-gray-400 mb-2">
|
| 339 |
-
Uses OpenAI Codex SDK locally (separate from
|
| 340 |
</div>
|
| 341 |
<div class="grid grid-cols-2 gap-3">
|
| 342 |
<div>
|
|
@@ -723,7 +678,7 @@
|
|
| 723 |
<div id="chat-history" class="flex-grow p-4 overflow-y-auto space-y-4 scroll-smooth">
|
| 724 |
<div id="chat-empty-state" class="text-center text-gray-500 mt-10">
|
| 725 |
<div class="text-lg font-semibold text-gray-300">Start a conversation</div>
|
| 726 |
-
<div class="text-sm text-gray-500 mt-1">
|
| 727 |
</div>
|
| 728 |
</div>
|
| 729 |
|
|
@@ -745,6 +700,7 @@
|
|
| 745 |
<input id="chat-model-quick" type="text" list="model-list" placeholder="Model"
|
| 746 |
class="bg-gray-700 h-10 w-44 px-2 rounded-lg border border-gray-600 text-sm text-white outline-none focus:border-blue-500 hidden sm:block"
|
| 747 |
onchange="setQuickModel(this.value)" title="Model" />
|
|
|
|
| 748 |
<input id="chat-file-input" type="file" accept="image/*" multiple class="hidden" />
|
| 749 |
<button onclick="document.getElementById('chat-file-input').click()"
|
| 750 |
class="bg-gray-700 h-10 w-10 rounded-lg hover:bg-gray-600 transition inline-flex items-center justify-center text-gray-200"
|
|
|
|
| 140 |
<h2 class="text-lg font-semibold text-gray-100">Getting started</h2>
|
| 141 |
<p class="text-sm text-gray-400 mt-1">A quick checklist to get productive in under a minute.</p>
|
| 142 |
</div>
|
| 143 |
+
<button onclick="window.location.href = routeUrl('settings');"
|
| 144 |
class="shrink-0 px-3 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 text-sm font-medium transition">
|
| 145 |
+
Settings
|
| 146 |
</button>
|
| 147 |
</div>
|
| 148 |
|
| 149 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
|
| 150 |
<div class="p-3 rounded-xl bg-gray-900/40 border border-gray-700">
|
| 151 |
+
<div class="text-sm font-semibold text-gray-200">1) Configure provider</div>
|
| 152 |
+
<div class="text-sm text-gray-400 mt-1">Set <code>DEFAULT_BASE_URL</code> + <code>DEFAULT_API_KEY</code> in deployment secrets. Use the Model box in chat to override.</div>
|
| 153 |
</div>
|
| 154 |
<div class="p-3 rounded-xl bg-gray-900/40 border border-gray-700">
|
| 155 |
<div class="text-sm font-semibold text-gray-200">2) Start chatting</div>
|
|
|
|
| 225 |
<button onclick="closeSettings()" class="text-gray-400 hover:text-white px-2 py-1 rounded hover:bg-white/10">×</button>
|
| 226 |
</div>
|
| 227 |
<div id="settings-content" class="p-4 space-y-4 overflow-y-auto" style="max-height: calc(100% - 56px);">
|
| 228 |
+
<div class="bg-gray-800/40 border border-gray-700 rounded-lg px-3 py-2">
|
| 229 |
+
<div class="text-sm text-gray-100">Chat provider</div>
|
| 230 |
+
<div class="text-xs text-gray-400 mt-0.5">Configured via deployment secrets (<code>DEFAULT_BASE_URL</code>, <code>DEFAULT_API_KEY</code>, <code>DEFAULT_MODEL</code>).</div>
|
| 231 |
+
<div class="text-xs text-gray-500 mt-1">Use the Model input in the chat bar to override per-message.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
</div>
|
| 233 |
|
| 234 |
<div class="pt-4 border-t border-gray-700">
|
|
|
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
<div class="text-xs text-gray-400 mb-2">
|
| 294 |
+
Uses OpenAI Codex SDK locally (separate from chat). Device auth is available in the terminal: <code>codex login --device-auth</code>.
|
| 295 |
</div>
|
| 296 |
<div class="grid grid-cols-2 gap-3">
|
| 297 |
<div>
|
|
|
|
| 678 |
<div id="chat-history" class="flex-grow p-4 overflow-y-auto space-y-4 scroll-smooth">
|
| 679 |
<div id="chat-empty-state" class="text-center text-gray-500 mt-10">
|
| 680 |
<div class="text-lg font-semibold text-gray-300">Start a conversation</div>
|
| 681 |
+
<div class="text-sm text-gray-500 mt-1" id="chat-empty-hint">Configure <code>DEFAULT_BASE_URL</code>, then send a message.</div>
|
| 682 |
</div>
|
| 683 |
</div>
|
| 684 |
|
|
|
|
| 700 |
<input id="chat-model-quick" type="text" list="model-list" placeholder="Model"
|
| 701 |
class="bg-gray-700 h-10 w-44 px-2 rounded-lg border border-gray-600 text-sm text-white outline-none focus:border-blue-500 hidden sm:block"
|
| 702 |
onchange="setQuickModel(this.value)" title="Model" />
|
| 703 |
+
<datalist id="model-list"></datalist>
|
| 704 |
<input id="chat-file-input" type="file" accept="image/*" multiple class="hidden" />
|
| 705 |
<button onclick="document.getElementById('chat-file-input').click()"
|
| 706 |
class="bg-gray-700 h-10 w-10 rounded-lg hover:bg-gray-600 transition inline-flex items-center justify-center text-gray-200"
|
static/dashboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
let supabase;
|
|
|
|
| 2 |
// --- Multi-Terminal Logic ---
|
| 3 |
let terminals = {}; // { id: { term, fitAddon, ws, containerId, scope } }
|
| 4 |
let activeTerminalId = null; // scope: 'main'
|
|
@@ -469,6 +470,17 @@ let supabase;
|
|
| 469 |
throw lastError || new Error('Config fetch failed');
|
| 470 |
}
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
async function getAccessToken() {
|
| 473 |
const direct = (window.__sbAccessToken || '').trim();
|
| 474 |
if (direct) return direct;
|
|
@@ -1132,6 +1144,7 @@ let supabase;
|
|
| 1132 |
async function init() {
|
| 1133 |
try {
|
| 1134 |
const config = await fetchConfig();
|
|
|
|
| 1135 |
if (!config.supabase_url || !config.supabase_key) throw new Error('Supabase Config Missing');
|
| 1136 |
await requireSupabaseLibrary();
|
| 1137 |
supabase = window.__supabaseClient || window.supabase.createClient(config.supabase_url, config.supabase_key);
|
|
@@ -1148,6 +1161,7 @@ let supabase;
|
|
| 1148 |
const savedTheme = localStorage.getItem('theme_v1');
|
| 1149 |
if (savedTheme) applyTheme(savedTheme);
|
| 1150 |
else applyTheme('dark');
|
|
|
|
| 1151 |
|
| 1152 |
// Admin/UI capabilities
|
| 1153 |
const me = await loadMe();
|
|
@@ -1170,7 +1184,7 @@ let supabase;
|
|
| 1170 |
if (localStorage.getItem('auth_allow_signup_v1') == null) {
|
| 1171 |
localStorage.setItem('auth_allow_signup_v1', '1');
|
| 1172 |
}
|
| 1173 |
-
// Codex SDK defaults (separate from chat
|
| 1174 |
if (localStorage.getItem('codex_sdk_base_url_v1') == null) {
|
| 1175 |
// Leave blank by default. For device-auth, Codex CLI uses its own auth flow
|
| 1176 |
// and default endpoints; setting a base URL without an API key can cause 401s.
|
|
@@ -1361,26 +1375,8 @@ let supabase;
|
|
| 1361 |
});
|
| 1362 |
}
|
| 1363 |
|
| 1364 |
-
|
| 1365 |
-
if (config.default_base_url && !document.getElementById('chat-base-url').value) {
|
| 1366 |
-
document.getElementById('chat-base-url').value = config.default_base_url;
|
| 1367 |
-
}
|
| 1368 |
-
if (config.default_api_key && !document.getElementById('chat-api-key').value) {
|
| 1369 |
-
document.getElementById('chat-api-key').value = config.default_api_key;
|
| 1370 |
-
}
|
| 1371 |
-
if (config.default_model && !document.getElementById('chat-model').value) {
|
| 1372 |
-
document.getElementById('chat-model').value = config.default_model;
|
| 1373 |
-
}
|
| 1374 |
-
|
| 1375 |
-
// Populate model picker from the configured base URL
|
| 1376 |
-
const baseUrlEl = document.getElementById('chat-base-url');
|
| 1377 |
-
if (baseUrlEl) {
|
| 1378 |
-
baseUrlEl.addEventListener('change', () => fetchModels().catch(() => { }));
|
| 1379 |
-
baseUrlEl.addEventListener('blur', () => fetchModels().catch(() => { }));
|
| 1380 |
-
}
|
| 1381 |
fetchModels().catch(() => { });
|
| 1382 |
-
|
| 1383 |
-
loadProviders();
|
| 1384 |
await initNotes();
|
| 1385 |
await loadChatHistoryList();
|
| 1386 |
createTerminalTab(); // Init first tab
|
|
@@ -2246,9 +2242,11 @@ let supabase;
|
|
| 2246 |
if (currentSessionId) await supabase.from('chat_messages').insert({ session_id: currentSessionId, role: 'user', content: message || (parts.length ? '[attachment]' : '') });
|
| 2247 |
|
| 2248 |
// AI Request
|
| 2249 |
-
const
|
| 2250 |
-
|
| 2251 |
-
|
|
|
|
|
|
|
| 2252 |
const aiMsgEl = addMessageToUI('assistant', '...');
|
| 2253 |
let aiContent = '';
|
| 2254 |
|
|
@@ -2258,7 +2256,7 @@ let supabase;
|
|
| 2258 |
const res = await fetch('/api/chat', {
|
| 2259 |
method: 'POST',
|
| 2260 |
headers: { 'Content-Type': 'application/json' },
|
| 2261 |
-
body: JSON.stringify({ messages: chatHistory, apiKey, baseUrl, model }),
|
| 2262 |
signal: chatAbortController.signal
|
| 2263 |
});
|
| 2264 |
|
|
@@ -2325,23 +2323,31 @@ let supabase;
|
|
| 2325 |
if (sel) sel.value = t;
|
| 2326 |
}
|
| 2327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2328 |
function setQuickModel(model) {
|
| 2329 |
const m = (model || '').trim();
|
| 2330 |
-
if (
|
| 2331 |
-
|
| 2332 |
-
|
| 2333 |
const quick = document.getElementById('chat-model-quick');
|
| 2334 |
-
if (quick) quick.value =
|
| 2335 |
const agentQuick = document.getElementById('agent-model-quick');
|
| 2336 |
-
if (agentQuick) agentQuick.value =
|
| 2337 |
}
|
| 2338 |
|
| 2339 |
function syncQuickModelFromSettings() {
|
| 2340 |
-
const
|
| 2341 |
const quick = document.getElementById('chat-model-quick');
|
|
|
|
| 2342 |
const agentQuick = document.getElementById('agent-model-quick');
|
| 2343 |
-
if (
|
| 2344 |
-
if (main && agentQuick) agentQuick.value = main.value || '';
|
| 2345 |
}
|
| 2346 |
|
| 2347 |
function getCodexThreadId() {
|
|
@@ -2675,8 +2681,6 @@ let supabase;
|
|
| 2675 |
renderAgentAttachments();
|
| 2676 |
scrollAgentToBottom();
|
| 2677 |
|
| 2678 |
-
const model = document.getElementById('chat-model').value;
|
| 2679 |
-
|
| 2680 |
const aiMsgEl = addAgentMessageToUI('assistant', '...');
|
| 2681 |
let aiContent = '';
|
| 2682 |
try {
|
|
@@ -2825,12 +2829,12 @@ let supabase;
|
|
| 2825 |
renderMarkdownInto(aiMsgEl, aiContent || (progress.length ? progress.slice(-18).join('\n') : ''));
|
| 2826 |
agentChatHistory.push({ role: 'assistant', content: aiContent || '' });
|
| 2827 |
} else {
|
| 2828 |
-
const
|
| 2829 |
-
|
| 2830 |
const res = await fetch('/api/chat', {
|
| 2831 |
method: 'POST',
|
| 2832 |
headers: { 'Content-Type': 'application/json' },
|
| 2833 |
-
body: JSON.stringify({ messages: agentChatHistory, apiKey, baseUrl, model }),
|
| 2834 |
signal: agentAbortController.signal
|
| 2835 |
});
|
| 2836 |
const reader = res.body.getReader();
|
|
@@ -3254,8 +3258,7 @@ let supabase;
|
|
| 3254 |
|
| 3255 |
// --- Provider & Models ---
|
| 3256 |
async function fetchModels({ quiet = true } = {}) {
|
| 3257 |
-
const apiKey =
|
| 3258 |
-
const baseUrl = document.getElementById('chat-base-url')?.value || '';
|
| 3259 |
if (!baseUrl) return;
|
| 3260 |
|
| 3261 |
const res = await fetch('/api/proxy/models', {
|
|
@@ -3282,10 +3285,14 @@ let supabase;
|
|
| 3282 |
list.appendChild(opt);
|
| 3283 |
});
|
| 3284 |
|
| 3285 |
-
const
|
| 3286 |
-
|
| 3287 |
-
|
| 3288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3289 |
}
|
| 3290 |
|
| 3291 |
if (!quiet) alert(`Fetched ${ids.length} models.`);
|
|
@@ -4782,9 +4789,9 @@ let supabase;
|
|
| 4782 |
|
| 4783 |
function getProviderInputs() {
|
| 4784 |
return {
|
| 4785 |
-
apiKey:
|
| 4786 |
-
baseUrl:
|
| 4787 |
-
model:
|
| 4788 |
};
|
| 4789 |
}
|
| 4790 |
|
|
|
|
| 1 |
let supabase;
|
| 2 |
+
let appConfig = {};
|
| 3 |
// --- Multi-Terminal Logic ---
|
| 4 |
let terminals = {}; // { id: { term, fitAddon, ws, containerId, scope } }
|
| 5 |
let activeTerminalId = null; // scope: 'main'
|
|
|
|
| 470 |
throw lastError || new Error('Config fetch failed');
|
| 471 |
}
|
| 472 |
|
| 473 |
+
function updateChatEmptyHint() {
|
| 474 |
+
const el = document.getElementById('chat-empty-hint');
|
| 475 |
+
if (!el) return;
|
| 476 |
+
const baseUrl = (appConfig?.default_base_url || '').trim();
|
| 477 |
+
if (!baseUrl) {
|
| 478 |
+
el.innerHTML = 'Configure <code>DEFAULT_BASE_URL</code> in deployment secrets, then send a message.';
|
| 479 |
+
return;
|
| 480 |
+
}
|
| 481 |
+
el.textContent = 'Send a message to start.';
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
async function getAccessToken() {
|
| 485 |
const direct = (window.__sbAccessToken || '').trim();
|
| 486 |
if (direct) return direct;
|
|
|
|
| 1144 |
async function init() {
|
| 1145 |
try {
|
| 1146 |
const config = await fetchConfig();
|
| 1147 |
+
appConfig = config || {};
|
| 1148 |
if (!config.supabase_url || !config.supabase_key) throw new Error('Supabase Config Missing');
|
| 1149 |
await requireSupabaseLibrary();
|
| 1150 |
supabase = window.__supabaseClient || window.supabase.createClient(config.supabase_url, config.supabase_key);
|
|
|
|
| 1161 |
const savedTheme = localStorage.getItem('theme_v1');
|
| 1162 |
if (savedTheme) applyTheme(savedTheme);
|
| 1163 |
else applyTheme('dark');
|
| 1164 |
+
updateChatEmptyHint();
|
| 1165 |
|
| 1166 |
// Admin/UI capabilities
|
| 1167 |
const me = await loadMe();
|
|
|
|
| 1184 |
if (localStorage.getItem('auth_allow_signup_v1') == null) {
|
| 1185 |
localStorage.setItem('auth_allow_signup_v1', '1');
|
| 1186 |
}
|
| 1187 |
+
// Codex SDK defaults (separate from chat)
|
| 1188 |
if (localStorage.getItem('codex_sdk_base_url_v1') == null) {
|
| 1189 |
// Leave blank by default. For device-auth, Codex CLI uses its own auth flow
|
| 1190 |
// and default endpoints; setting a base URL without an API key can cause 401s.
|
|
|
|
| 1375 |
});
|
| 1376 |
}
|
| 1377 |
|
| 1378 |
+
syncQuickModelFromSettings();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
fetchModels().catch(() => { });
|
|
|
|
|
|
|
| 1380 |
await initNotes();
|
| 1381 |
await loadChatHistoryList();
|
| 1382 |
createTerminalTab(); // Init first tab
|
|
|
|
| 2242 |
if (currentSessionId) await supabase.from('chat_messages').insert({ session_id: currentSessionId, role: 'user', content: message || (parts.length ? '[attachment]' : '') });
|
| 2243 |
|
| 2244 |
// AI Request
|
| 2245 |
+
const provider = getProviderInputs();
|
| 2246 |
+
if (!provider.baseUrl) {
|
| 2247 |
+
alert('Chat provider is not configured. Set DEFAULT_BASE_URL (and DEFAULT_API_KEY) in deployment secrets.');
|
| 2248 |
+
return;
|
| 2249 |
+
}
|
| 2250 |
const aiMsgEl = addMessageToUI('assistant', '...');
|
| 2251 |
let aiContent = '';
|
| 2252 |
|
|
|
|
| 2256 |
const res = await fetch('/api/chat', {
|
| 2257 |
method: 'POST',
|
| 2258 |
headers: { 'Content-Type': 'application/json' },
|
| 2259 |
+
body: JSON.stringify({ messages: chatHistory, apiKey: provider.apiKey, baseUrl: provider.baseUrl, model: provider.model }),
|
| 2260 |
signal: chatAbortController.signal
|
| 2261 |
});
|
| 2262 |
|
|
|
|
| 2323 |
if (sel) sel.value = t;
|
| 2324 |
}
|
| 2325 |
|
| 2326 |
+
function getQuickModelOverride() {
|
| 2327 |
+
return (localStorage.getItem('chat_model_override_v1') || '').trim();
|
| 2328 |
+
}
|
| 2329 |
+
|
| 2330 |
+
function getEffectiveChatModel() {
|
| 2331 |
+
return getQuickModelOverride() || (appConfig?.default_model || '').trim() || 'gpt-3.5-turbo';
|
| 2332 |
+
}
|
| 2333 |
+
|
| 2334 |
function setQuickModel(model) {
|
| 2335 |
const m = (model || '').trim();
|
| 2336 |
+
if (m) localStorage.setItem('chat_model_override_v1', m);
|
| 2337 |
+
else localStorage.removeItem('chat_model_override_v1');
|
| 2338 |
+
const next = getEffectiveChatModel();
|
| 2339 |
const quick = document.getElementById('chat-model-quick');
|
| 2340 |
+
if (quick) quick.value = next;
|
| 2341 |
const agentQuick = document.getElementById('agent-model-quick');
|
| 2342 |
+
if (agentQuick) agentQuick.value = next;
|
| 2343 |
}
|
| 2344 |
|
| 2345 |
function syncQuickModelFromSettings() {
|
| 2346 |
+
const next = getEffectiveChatModel();
|
| 2347 |
const quick = document.getElementById('chat-model-quick');
|
| 2348 |
+
if (quick && !quick.value) quick.value = next;
|
| 2349 |
const agentQuick = document.getElementById('agent-model-quick');
|
| 2350 |
+
if (agentQuick && !agentQuick.value) agentQuick.value = next;
|
|
|
|
| 2351 |
}
|
| 2352 |
|
| 2353 |
function getCodexThreadId() {
|
|
|
|
| 2681 |
renderAgentAttachments();
|
| 2682 |
scrollAgentToBottom();
|
| 2683 |
|
|
|
|
|
|
|
| 2684 |
const aiMsgEl = addAgentMessageToUI('assistant', '...');
|
| 2685 |
let aiContent = '';
|
| 2686 |
try {
|
|
|
|
| 2829 |
renderMarkdownInto(aiMsgEl, aiContent || (progress.length ? progress.slice(-18).join('\n') : ''));
|
| 2830 |
agentChatHistory.push({ role: 'assistant', content: aiContent || '' });
|
| 2831 |
} else {
|
| 2832 |
+
const provider = getProviderInputs();
|
| 2833 |
+
if (!provider.baseUrl) throw new Error('Chat provider is not configured (DEFAULT_BASE_URL).');
|
| 2834 |
const res = await fetch('/api/chat', {
|
| 2835 |
method: 'POST',
|
| 2836 |
headers: { 'Content-Type': 'application/json' },
|
| 2837 |
+
body: JSON.stringify({ messages: agentChatHistory, apiKey: provider.apiKey, baseUrl: provider.baseUrl, model: provider.model }),
|
| 2838 |
signal: agentAbortController.signal
|
| 2839 |
});
|
| 2840 |
const reader = res.body.getReader();
|
|
|
|
| 3258 |
|
| 3259 |
// --- Provider & Models ---
|
| 3260 |
async function fetchModels({ quiet = true } = {}) {
|
| 3261 |
+
const { apiKey, baseUrl } = getProviderInputs();
|
|
|
|
| 3262 |
if (!baseUrl) return;
|
| 3263 |
|
| 3264 |
const res = await fetch('/api/proxy/models', {
|
|
|
|
| 3285 |
list.appendChild(opt);
|
| 3286 |
});
|
| 3287 |
|
| 3288 |
+
const hasOverride = !!getQuickModelOverride();
|
| 3289 |
+
const hasDefault = !!(appConfig?.default_model || '').trim();
|
| 3290 |
+
if (!hasOverride && !hasDefault && ids[0]) {
|
| 3291 |
+
localStorage.setItem('chat_model_override_v1', ids[0]);
|
| 3292 |
+
const quick = document.getElementById('chat-model-quick');
|
| 3293 |
+
if (quick && !quick.value) quick.value = ids[0];
|
| 3294 |
+
const agentQuick = document.getElementById('agent-model-quick');
|
| 3295 |
+
if (agentQuick && !agentQuick.value) agentQuick.value = ids[0];
|
| 3296 |
}
|
| 3297 |
|
| 3298 |
if (!quiet) alert(`Fetched ${ids.length} models.`);
|
|
|
|
| 4789 |
|
| 4790 |
function getProviderInputs() {
|
| 4791 |
return {
|
| 4792 |
+
apiKey: (appConfig?.default_api_key || '').trim(),
|
| 4793 |
+
baseUrl: (appConfig?.default_base_url || '').trim(),
|
| 4794 |
+
model: getEffectiveChatModel(),
|
| 4795 |
};
|
| 4796 |
}
|
| 4797 |
|
static/docs.html
CHANGED
|
@@ -32,43 +32,118 @@
|
|
| 32 |
</header>
|
| 33 |
|
| 34 |
<main class="mx-auto max-w-4xl px-5 py-10">
|
| 35 |
-
<div class="
|
| 36 |
-
<
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
<
|
| 40 |
-
|
| 41 |
-
<
|
| 42 |
-
<li>Set:
|
| 43 |
-
<ul>
|
| 44 |
-
<li><code>SUPABASE_URL</code></li>
|
| 45 |
-
<li><code>SUPABASE_KEY</code> (anon key) or <code>SUPABASE_ANON_KEY</code></li>
|
| 46 |
-
</ul>
|
| 47 |
-
</li>
|
| 48 |
-
<li>Restart the Space.</li>
|
| 49 |
-
</ol>
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
<
|
| 68 |
-
</
|
| 69 |
</div>
|
| 70 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</body>
|
| 72 |
|
| 73 |
</html>
|
| 74 |
-
|
|
|
|
| 32 |
</header>
|
| 33 |
|
| 34 |
<main class="mx-auto max-w-4xl px-5 py-10">
|
| 35 |
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
| 36 |
+
<aside class="md:col-span-1">
|
| 37 |
+
<div class="text-xs font-semibold text-gray-400 uppercase">Docs</div>
|
| 38 |
+
<div id="docs-nav" class="mt-2 space-y-1 text-sm"></div>
|
| 39 |
+
<div class="mt-6 text-xs font-semibold text-gray-400 uppercase">API</div>
|
| 40 |
+
<div class="mt-2 space-y-1 text-sm">
|
| 41 |
+
<a class="text-blue-300 hover:text-blue-200" href="/api/docs">API docs (Swagger)</a>
|
| 42 |
+
<a class="text-blue-300 hover:text-blue-200" href="/api/openapi.json">OpenAPI JSON</a>
|
| 43 |
+
</div>
|
| 44 |
+
</aside>
|
| 45 |
|
| 46 |
+
<article id="docs-content" class="md:col-span-3 prose prose-invert max-w-none">
|
| 47 |
+
<h1>Get started</h1>
|
| 48 |
+
<p>This app uses <strong>Supabase Auth</strong>. In Hugging Face Spaces, configure secrets first.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
<h2>Hugging Face Spaces setup</h2>
|
| 51 |
+
<ol>
|
| 52 |
+
<li>Go to <strong>Settings → Variables and secrets → Secrets</strong>.</li>
|
| 53 |
+
<li>Set:
|
| 54 |
+
<ul>
|
| 55 |
+
<li><code>SUPABASE_URL</code></li>
|
| 56 |
+
<li><code>SUPABASE_KEY</code> (anon key) or <code>SUPABASE_ANON_KEY</code></li>
|
| 57 |
+
<li><code>DEFAULT_BASE_URL</code> (OpenAI-compatible, e.g. <code>https://router.huggingface.co/v1</code>)</li>
|
| 58 |
+
<li><code>DEFAULT_API_KEY</code> (optional)</li>
|
| 59 |
+
</ul>
|
| 60 |
+
</li>
|
| 61 |
+
<li>Restart the Space.</li>
|
| 62 |
+
</ol>
|
| 63 |
|
| 64 |
+
<h2>Login / Register</h2>
|
| 65 |
+
<ul>
|
| 66 |
+
<li>Open <a href="/login">/login</a> and sign in.</li>
|
| 67 |
+
<li>To return to the app automatically, use <a href="/login?next=%2Fapp">/login?next=/app</a>.</li>
|
| 68 |
+
</ul>
|
| 69 |
|
| 70 |
+
<h2>Password reset</h2>
|
| 71 |
+
<ul>
|
| 72 |
+
<li>Ensure Supabase Auth redirect URLs include <code>/login</code> for your Space domain.</li>
|
| 73 |
+
<li>The login page supports both recovery link formats (<code>#access_token=…</code> and <code>?code=…</code>).</li>
|
| 74 |
+
</ul>
|
| 75 |
+
</article>
|
| 76 |
</div>
|
| 77 |
</main>
|
| 78 |
+
|
| 79 |
+
<script>
|
| 80 |
+
const navEl = document.getElementById('docs-nav');
|
| 81 |
+
const contentEl = document.getElementById('docs-content');
|
| 82 |
+
let cachedStart = null;
|
| 83 |
+
|
| 84 |
+
function escapeHtml(text) {
|
| 85 |
+
const div = document.createElement('div');
|
| 86 |
+
div.textContent = text == null ? '' : String(text);
|
| 87 |
+
return div.innerHTML;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function showStart() {
|
| 91 |
+
if (cachedStart != null) {
|
| 92 |
+
contentEl.innerHTML = cachedStart;
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
async function showDoc(slug) {
|
| 97 |
+
const res = await fetch(`/api/app-docs/${encodeURIComponent(slug)}`);
|
| 98 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 99 |
+
const data = await res.json();
|
| 100 |
+
const title = data?.title || slug;
|
| 101 |
+
const markdown = data?.markdown || '';
|
| 102 |
+
contentEl.innerHTML = `
|
| 103 |
+
<h1>${escapeHtml(title)}</h1>
|
| 104 |
+
<p><a href="#" id="docs-back">← Back</a></p>
|
| 105 |
+
<pre class="whitespace-pre-wrap bg-black/30 border border-white/10 rounded-xl p-4 text-sm">${escapeHtml(markdown)}</pre>
|
| 106 |
+
`;
|
| 107 |
+
const back = document.getElementById('docs-back');
|
| 108 |
+
if (back) back.onclick = (e) => { e.preventDefault(); showStart(); };
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
async function loadNav() {
|
| 112 |
+
if (!navEl || !contentEl) return;
|
| 113 |
+
cachedStart = contentEl.innerHTML;
|
| 114 |
+
navEl.innerHTML = '<div class="text-gray-500 text-sm">Loading…</div>';
|
| 115 |
+
try {
|
| 116 |
+
const res = await fetch('/api/app-docs');
|
| 117 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 118 |
+
const data = await res.json();
|
| 119 |
+
const pages = Array.isArray(data?.pages) ? data.pages : [];
|
| 120 |
+
if (!pages.length) {
|
| 121 |
+
navEl.innerHTML = '<div class="text-gray-500 text-sm">No docs found.</div>';
|
| 122 |
+
return;
|
| 123 |
+
}
|
| 124 |
+
navEl.innerHTML = '';
|
| 125 |
+
pages.forEach((p) => {
|
| 126 |
+
const a = document.createElement('a');
|
| 127 |
+
a.href = '#';
|
| 128 |
+
a.className = 'block px-2 py-1 rounded-lg hover:bg-white/5 text-gray-200';
|
| 129 |
+
a.textContent = p.title || p.slug;
|
| 130 |
+
a.onclick = async (e) => {
|
| 131 |
+
e.preventDefault();
|
| 132 |
+
try {
|
| 133 |
+
await showDoc(p.slug);
|
| 134 |
+
} catch (err) {
|
| 135 |
+
alert(`Failed to load doc: ${err?.message || err}`);
|
| 136 |
+
}
|
| 137 |
+
};
|
| 138 |
+
navEl.appendChild(a);
|
| 139 |
+
});
|
| 140 |
+
} catch (err) {
|
| 141 |
+
navEl.innerHTML = '<div class="text-red-300 text-sm">Failed to load docs API.</div>';
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
loadNav();
|
| 146 |
+
</script>
|
| 147 |
</body>
|
| 148 |
|
| 149 |
</html>
|
|
|