ArunKr commited on
Commit
b907ec1
·
verified ·
1 Parent(s): cad3752

Upload folder using huggingface_hub

Browse files
Files changed (7) hide show
  1. PLANS.md +1 -0
  2. TASKS.md +1 -0
  3. app/routes/base.py +33 -1
  4. app/server.py +6 -1
  5. static/dashboard.html +11 -55
  6. static/dashboard.js +53 -46
  7. 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(lifespan=lifespan)
 
 
 
 
 
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="switchMode('chat'); openSettings();"
144
  class="shrink-0 px-3 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 text-sm font-medium transition">
145
- Provider 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) Pick a provider</div>
152
- <div class="text-sm text-gray-400 mt-1">Open Chat Settings choose a preset, set Base URL + API key, fetch models.</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,55 +225,10 @@
225
  <button onclick="closeSettings()" class="text-gray-400 hover:text-white px-2 py-1 rounded hover:bg-white/10">&times;</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="space-y-3">
229
- <div>
230
- <label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">Saved Providers</label>
231
- <div class="flex gap-2">
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 your chat provider settings). Device auth is available in the terminal: <code>codex login --device-auth</code>.
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">Pick a provider in Settings, then send a message.</div>
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">&times;</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 provider)
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
- // Apply defaults if fields are empty
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 apiKey = document.getElementById('chat-api-key').value;
2250
- const baseUrl = document.getElementById('chat-base-url').value;
2251
- const model = document.getElementById('chat-model').value;
 
 
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 (!m) return;
2331
- const main = document.getElementById('chat-model');
2332
- if (main) main.value = m;
2333
  const quick = document.getElementById('chat-model-quick');
2334
- if (quick) quick.value = m;
2335
  const agentQuick = document.getElementById('agent-model-quick');
2336
- if (agentQuick) agentQuick.value = m;
2337
  }
2338
 
2339
  function syncQuickModelFromSettings() {
2340
- const main = document.getElementById('chat-model');
2341
  const quick = document.getElementById('chat-model-quick');
 
2342
  const agentQuick = document.getElementById('agent-model-quick');
2343
- if (main && quick) quick.value = main.value || '';
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 apiKey = document.getElementById('chat-api-key').value;
2829
- const baseUrl = document.getElementById('chat-base-url').value;
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 = document.getElementById('chat-api-key')?.value || '';
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 modelEl = document.getElementById('chat-model');
3286
- if (modelEl && !modelEl.value && ids[0]) {
3287
- modelEl.value = ids[0];
3288
- syncQuickModelFromSettings();
 
 
 
 
3289
  }
3290
 
3291
  if (!quiet) alert(`Fetched ${ids.length} models.`);
@@ -4782,9 +4789,9 @@ let supabase;
4782
 
4783
  function getProviderInputs() {
4784
  return {
4785
- apiKey: document.getElementById('chat-api-key')?.value || '',
4786
- baseUrl: document.getElementById('chat-base-url')?.value || '',
4787
- model: document.getElementById('chat-model')?.value || 'gpt-3.5-turbo',
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="prose prose-invert max-w-none">
36
- <h1>Get started</h1>
37
- <p>This app uses <strong>Supabase Auth</strong>. In Hugging Face Spaces, configure secrets first.</p>
 
 
 
 
 
 
 
38
 
39
- <h2>Hugging Face Spaces setup</h2>
40
- <ol>
41
- <li>Go to <strong>Settings Variables and secrets Secrets</strong>.</li>
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
- <h2>Login / Register</h2>
52
- <ul>
53
- <li>Open <a href="/login">/login</a> and sign in.</li>
54
- <li>To return to the app automatically, use <a href="/login?next=%2Fapp">/login?next=/app</a>.</li>
55
- </ul>
 
 
 
 
 
 
 
 
56
 
57
- <h2>Password reset</h2>
58
- <ul>
59
- <li>Ensure Supabase Auth redirect URLs include <code>/login</code> for your Space domain.</li>
60
- <li>The login page supports both recovery link formats (<code>#access_token=…</code> and <code>?code=</code>).</li>
61
- </ul>
62
 
63
- <h2>More docs</h2>
64
- <ul>
65
- <li><a href="https://github.com/Akt-AI/autonomy-labs/blob/main/README.md" target="_blank" rel="noreferrer">README</a></li>
66
- <li><a href="https://github.com/Akt-AI/autonomy-labs/blob/main/docs/TROUBLESHOOTING.md" target="_blank" rel="noreferrer">Troubleshooting</a></li>
67
- <li><a href="https://github.com/Akt-AI/autonomy-labs/blob/main/docs/SECURITY_DEPLOYMENT.md" target="_blank" rel="noreferrer">Security deployment</a></li>
68
- </ul>
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>