polats Claude Opus 4.8 (1M context) commited on
Commit
cd43499
·
1 Parent(s): 627d835

Put the model picker in Gradio's OWN settings page (not a custom tab)

Browse files

Per the clarification: inject the engine/model picker into Gradio's native settings
page (footer "Settings" → ?view=settings) instead of a custom sidebar tab. Gradio's
settings page isn't an official extension point (footer_links only toggles which of
api/gradio/settings show), so settingsPanel.js anchors on its "Display Theme" section,
clones the section + heading classes, and appends a native-looking "Local AI Model"
section driving the shared runtime.js singleton. Reverts the custom Settings tab +
nav item + #settings-stage. Graceful no-op if Gradio's DOM changes (app still runs on
the default model).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (5) hide show
  1. app.py +5 -7
  2. web/settingsPanel.js +31 -18
  3. web/shell/nav.json +0 -6
  4. web/shell/persona.css +12 -7
  5. web/tiny.js +3 -2
app.py CHANGED
@@ -91,11 +91,11 @@ THEME = ('<style>'
91
  # Gradio still hides it (display:none on the inactive tab's ancestor).
92
  '.gradio-container .tabitem{padding:0 !important;}'
93
  '.gradio-container .tabs{border:0 !important;}'
94
- '#sprite-stage,#persona-stage,#diary-stage,#settings-stage{position:fixed !important;top:0;bottom:0;'
95
  'right:0;left:var(--tac-w,240px);height:auto !important;z-index:1;}'
96
  'body.tac-collapsed #sprite-stage,body.tac-collapsed #persona-stage,'
97
- 'body.tac-collapsed #diary-stage,body.tac-collapsed #settings-stage{left:0;}'
98
- '@media (max-width:768px){#sprite-stage,#persona-stage,#diary-stage,#settings-stage{left:0;}}'
99
  '</style>')
100
  HEAD = ('<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">'
101
  + HIDE_TABS + FONTS + THEME +
@@ -194,10 +194,8 @@ with gr.Blocks(title="Tiny Army") as demo:
194
  with gr.Tab("Personas"):
195
  # In-browser persona generator (web/personaPanel.js → wllama).
196
  gr.HTML('<div id="persona-stage" style="overflow:hidden"></div>')
197
- with gr.Tab("Settings"):
198
- # Engine + model picker + cache management (web/settingsPanel.js). Shared by
199
- # the Personas and War Diaries pages via the runtime.js singleton.
200
- gr.HTML('<div id="settings-stage" style="overflow:hidden"></div>')
201
 
202
  # Mount Gradio on FastAPI so we can also serve the JS module + the sprite assets.
203
  fastapi_app = FastAPI()
 
91
  # Gradio still hides it (display:none on the inactive tab's ancestor).
92
  '.gradio-container .tabitem{padding:0 !important;}'
93
  '.gradio-container .tabs{border:0 !important;}'
94
+ '#sprite-stage,#persona-stage,#diary-stage{position:fixed !important;top:0;bottom:0;'
95
  'right:0;left:var(--tac-w,240px);height:auto !important;z-index:1;}'
96
  'body.tac-collapsed #sprite-stage,body.tac-collapsed #persona-stage,'
97
+ 'body.tac-collapsed #diary-stage{left:0;}'
98
+ '@media (max-width:768px){#sprite-stage,#persona-stage,#diary-stage{left:0;}}'
99
  '</style>')
100
  HEAD = ('<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">'
101
  + HIDE_TABS + FONTS + THEME +
 
194
  with gr.Tab("Personas"):
195
  # In-browser persona generator (web/personaPanel.js → wllama).
196
  gr.HTML('<div id="persona-stage" style="overflow:hidden"></div>')
197
+ # NOTE: the engine/model picker is injected into Gradio's OWN settings page
198
+ # (footer "Settings" ?view=settings) by web/settingsPanel.js not a tab.
 
 
199
 
200
  # Mount Gradio on FastAPI so we can also serve the JS module + the sprite assets.
201
  fastapi_app = FastAPI()
web/settingsPanel.js CHANGED
@@ -1,7 +1,11 @@
1
- // Settings the in-browser LLM engine + model picker and cache management, shared by
2
- // BOTH the Personas and War Diaries pages (they read the same runtime.js singleton).
3
- // Moved here out of the individual panels so there's ONE place to choose the model and
4
- // it's guaranteed identical on both pages.
 
 
 
 
5
  import { mountModelBar } from '/web/modelBar.js'
6
 
7
  function el(tag, props = {}, kids = []) {
@@ -15,20 +19,29 @@ function el(tag, props = {}, kids = []) {
15
  return n
16
  }
17
 
18
- export function mountSettingsPanel(host) {
 
 
 
 
 
 
 
 
 
 
19
  const modelHost = el('div')
20
- const pane = el('div', { class: 'settings-pane' }, [
21
- el('h2', { class: 'persona-name settings-title' }, 'Settings'),
22
- el('p', { class: 'persona-about settings-intro' },
23
- 'Pick the in-browser LLM that writes your soldiers and their war diaries. ' +
24
- 'Everything runs on your device — no cloud. This choice applies to both ' +
25
- 'Personas and War Diaries.'),
26
- el('label', { class: 'persona-label' }, 'Local LLM'),
27
- modelHost,
28
- el('p', { class: 'settings-note' },
29
- 'Models download once and are cached in your browser; use 🗑 delete to free space. ' +
30
- 'wllama (llama.cpp) is the most reliable; WebLLM and Transformers.js are here to benchmark.'),
31
- ])
32
- host.appendChild(el('div', { class: 'persona-view settings-view' }, [pane]))
33
  mountModelBar(modelHost)
34
  }
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inject the in-browser LLM engine/model picker (and cache management) into Gradio's
2
+ // OWN settings page the footer "Settings" link (?view=settings). That page isn't an
3
+ // official extension point, so we anchor on its "Display Theme" section, clone its
4
+ // styling, and append a matching "Local AI Model" section. The picker drives the shared
5
+ // runtime.js singleton, so Personas + War Diaries both use whatever is chosen here.
6
+ //
7
+ // Fragile by nature (it rides Gradio's internal DOM): if the structure changes the
8
+ // section simply won't appear (graceful no-op) and the app still runs on defaults.
9
  import { mountModelBar } from '/web/modelBar.js'
10
 
11
  function el(tag, props = {}, kids = []) {
 
19
  return n
20
  }
21
 
22
+ function injectInto(sampleSection) {
23
+ const list = sampleSection.parentElement
24
+ if (!list || list.querySelector('#tac-model-settings')) return
25
+ // Clone Gradio's own section + heading classes so it looks native.
26
+ const section = el('div', { class: sampleSection.className, id: 'tac-model-settings' })
27
+ const h = document.createElement('h2')
28
+ h.className = sampleSection.querySelector('h2')?.className || ''
29
+ h.textContent = 'Local AI Model'
30
+ const intro = el('p', { class: 'tac-set-intro' },
31
+ 'The in-browser model that writes your soldiers and their war diaries — shared by ' +
32
+ 'Personas and War Diaries. Runs on your device; models cache in your browser.')
33
  const modelHost = el('div')
34
+ section.append(h, intro, modelHost)
35
+ list.appendChild(section)
 
 
 
 
 
 
 
 
 
 
 
36
  mountModelBar(modelHost)
37
  }
38
+
39
+ export function mountSettingsPanel() {
40
+ const tryInject = () => {
41
+ const sample = [...document.querySelectorAll('.banner-wrap')].find((e) => /Display Theme/i.test(e.textContent))
42
+ if (sample) injectInto(sample)
43
+ }
44
+ // The settings modal mounts/unmounts on demand, so watch the DOM and (re)inject.
45
+ new MutationObserver(tryInject).observe(document.body, { childList: true, subtree: true })
46
+ tryInject()
47
+ }
web/shell/nav.json CHANGED
@@ -27,12 +27,6 @@
27
  { "label": "War Diaries", "icon": "📓", "space": "Barracks" },
28
  { "label": "Personas", "icon": "🪖", "space": "Personas" }
29
  ]
30
- },
31
- {
32
- "title": "App",
33
- "items": [
34
- { "label": "Settings", "icon": "⚙", "space": "Settings" }
35
- ]
36
  }
37
  ]
38
  }
 
27
  { "label": "War Diaries", "icon": "📓", "space": "Barracks" },
28
  { "label": "Personas", "icon": "🪖", "space": "Personas" }
29
  ]
 
 
 
 
 
 
30
  }
31
  ]
32
  }
web/shell/persona.css CHANGED
@@ -118,13 +118,18 @@
118
  .persona-go-alt:hover { background: var(--p-paper-2) !important; color: var(--p-ink) !important; }
119
  .tts-status { min-height: 14px; }
120
 
121
- /* ── Settings page (engine + model picker, shared by both pages) ───────────── */
122
- .settings-view { background: var(--p-paper); overflow-y: auto; }
123
- .settings-pane { max-width: 560px; width: 100%; margin: 0 auto; padding: 48px 28px; }
124
- .settings-title { font-size: 30px; margin-bottom: 8px; }
125
- .settings-intro { font-size: 15px; max-width: none; margin-bottom: 26px; }
126
- .settings-note { font-family: var(--p-mono); font-size: 10px; line-height: 1.6; color: var(--p-muted); margin-top: 14px; }
127
- @media (max-width: 768px) { .settings-pane { padding: 54px 16px 28px; } }
 
 
 
 
 
128
 
129
  /* Collapsible control sections (model / voice). Desktop: no toggle, always shown. */
130
  .ctl-collapse > summary { display: none; }
 
118
  .persona-go-alt:hover { background: var(--p-paper-2) !important; color: var(--p-ink) !important; }
119
  .tts-status { min-height: 14px; }
120
 
121
+ /* ── "Local AI Model" section injected into Gradio's own Settings page ──────── */
122
+ /* The model bar's styles use --p-* vars (normally scoped to .persona-view); define
123
+ them here too so the picker renders correctly inside Gradio's settings modal. */
124
+ #tac-model-settings {
125
+ --p-ink: #141821; --p-muted: #6d6a5f; --p-paper: #f3ebdc; --p-paper-2: #ece2cc;
126
+ --p-card: #fbf6ea; --p-transmit: #d8271a;
127
+ --p-sans: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
128
+ --p-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
129
+ }
130
+ #tac-model-settings * { box-sizing: border-box; }
131
+ #tac-model-settings .model-bar { border-bottom: 0; padding-bottom: 0; }
132
+ .tac-set-intro { font-size: 14px; line-height: 1.5; opacity: .75; margin: 2px 0 14px; }
133
 
134
  /* Collapsible control sections (model / voice). Desktop: no toggle, always shown. */
135
  .ctl-collapse > summary { display: none; }
web/tiny.js CHANGED
@@ -58,8 +58,9 @@ whenEl('sprite-stage', async (el) => {
58
  // ── Personas + War Diary tabs — in-browser llama.cpp (wllama), runs on the device ──
59
  whenEl('persona-stage', (el) => { mountPersonaPanel(el) })
60
  whenEl('diary-stage', (el) => { mountDiaryPanel(el) })
61
- // Engine + model live in Settings (shared by both Personas and War Diaries).
62
- whenEl('settings-stage', (el) => { mountSettingsPanel(el) })
 
63
 
64
  // ── Battle tab (real sprites, reusing the engine + shared renderer) ──────────
65
  const PLAYERS = [
 
58
  // ── Personas + War Diary tabs — in-browser llama.cpp (wllama), runs on the device ──
59
  whenEl('persona-stage', (el) => { mountPersonaPanel(el) })
60
  whenEl('diary-stage', (el) => { mountDiaryPanel(el) })
61
+ // Engine + model picker is injected into Gradio's own Settings page (footer link),
62
+ // shared by both Personas and War Diaries via the runtime.js singleton.
63
+ mountSettingsPanel()
64
 
65
  // ── Battle tab (real sprites, reusing the engine + shared renderer) ──────────
66
  const PLAYERS = [