Spaces:
Running
Running
Put the model picker in Gradio's OWN settings page (not a custom tab)
Browse filesPer 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>
- app.py +5 -7
- web/settingsPanel.js +31 -18
- web/shell/nav.json +0 -6
- web/shell/persona.css +12 -7
- 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
|
| 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
|
| 98 |
-
'@media (max-width:768px){#sprite-stage,#persona-stage,#diary-stage
|
| 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 |
-
|
| 198 |
-
|
| 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 |
-
//
|
| 2 |
-
//
|
| 3 |
-
//
|
| 4 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
const modelHost = el('div')
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
/* ──
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 62 |
-
|
|
|
|
| 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 = [
|