File size: 4,143 Bytes
dffe06d
 
 
f9dd2fe
dffe06d
 
 
 
627d835
dffe06d
2151ea1
575fb61
750ca83
1f1908e
627d835
 
 
 
 
 
 
 
 
 
 
 
dffe06d
 
 
 
cd43499
dffe06d
 
 
 
 
 
627d835
cd43499
 
 
 
dffe06d
750ca83
 
 
 
 
f9dd2fe
 
 
dffe06d
e648dca
 
 
2151ea1
 
 
575fb61
 
 
1f1908e
 
 
353687b
 
cd43499
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Inject our settings sections into Gradio's OWN settings page (footer "Settings" or the
// sidebar ⚙ button → ?view=settings). Not an official extension point, so we anchor on
// the "Display Theme" section, clone its styling, and prepend matching sections:
//   • Text Generation Model — the LLM engine/model picker (modelBar)
//   • Voice          — the read-aloud TTS engine/voice picker (ttsBar)
// Both drive the shared runtime.js / tts.js singletons, so every page uses the same
// choice. Fragile by nature (rides Gradio's DOM): if the structure changes the sections
// just won't appear (graceful no-op) and the app still runs on defaults.
import { mountModelBar } from '/web/modelBar.js'
import { mountTtsBar } from '/web/ttsBar.js'
import { mountImagenBar } from '/web/imagenBar.js'
import { mountPersonaPromptBar } from '/web/personaPromptBar.js'
import { mountQualityBar } from '/web/qualityBar.js'
import { mountCodingModelBar } from '/web/codingModelBar.js'

function el(tag, props = {}, kids = []) {
  const n = document.createElement(tag)
  for (const [k, v] of Object.entries(props)) {
    if (k === 'class') n.className = v
    else if (k.startsWith('on') && typeof v === 'function') n.addEventListener(k.slice(2), v)
    else if (v != null) n.setAttribute(k, v)
  }
  for (const kid of [].concat(kids)) if (kid != null) n.append(kid)
  return n
}

function injectSection(sample, id, title, intro, mountFn) {
  const list = sample.parentElement
  if (!list || list.querySelector('#' + id)) return
  const section = el('div', { class: sample.className + ' tac-set-section', id })
  const h = document.createElement('h2')
  h.className = sample.querySelector('h2')?.className || ''
  h.textContent = title
  const host = el('div')
  section.append(h, el('p', { class: 'tac-set-intro' }, intro), host)
  list.insertBefore(section, sample) // above Display Theme, below the title
  mountFn(host)
}

export function mountSettingsPanel() {
  const tryInject = () => {
    const sample = [...document.querySelectorAll('.banner-wrap')].find((e) => /Display Theme/i.test(e.textContent))
    if (!sample) return
    // Recommended (preset) first so it sits at the very top, then Model, Voice, etc.
    // Each injectSection inserts just above Display Theme, so call order = on-screen order.
    injectSection(sample, 'tac-quality-settings', 'Recommended settings',
      'Pick a quality preset — it sets the AI model and voice together, like graphics ' +
      'presets in a game. Changing either by hand switches to Custom.', mountQualityBar)
    injectSection(sample, 'tac-model-settings', 'Text Generation Model',
      'The model that writes your soldiers and their war diaries. Use browser-local ' +
      'models, a configured local server, or a ZeroGPU-hosted model.', mountModelBar)
    injectSection(sample, 'tac-voice-settings', 'Voice',
      'The provider that voices your heroes. Qwen3-TTS designs a voice from each hero’s ' +
      'description; Kokoro/Kitten run on your device with a named voice you pick per hero. ' +
      'The voice belongs to the hero, so there’s no global voice to choose here.', mountTtsBar)
    injectSection(sample, 'tac-image-settings', 'Portrait',
      'The model that paints hero portraits. Z-Image-Turbo runs on your GPU when you host ' +
      'the project locally; FLUX runs in the cloud otherwise.', mountImagenBar)
    injectSection(sample, 'tac-persona-prompt-settings', 'Persona Prompt',
      'The system prompt that writes each hero (name, about, quote and voice design). ' +
      'Edit it to change their style; Save uses it on the next “Recruit hero”.', mountPersonaPromptBar)
    injectSection(sample, 'tac-coding-model-settings', 'Coding Model',
      'The model that powers the Skill Forge — it writes a skill for a chosen hero. ' +
      'Nemotron 3 Nano (NVIDIA) runs via NVIDIA NIM; Mellum2 (JetBrains) runs as a ' +
      'ZeroGPU sidecar and falls back to Nemotron (NIM) if its sidecar is unavailable.',
      mountCodingModelBar)
  }
  new MutationObserver(tryInject).observe(document.body, { childList: true, subtree: true })
  tryInject()
}