Spaces:
Running
Running
File size: 6,279 Bytes
1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e 6e155d8 1f1908e | 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | // Skill Forge — the Sandbox surface that uses the Coding Model to author a combat
// skill for a chosen hero. Pick a recruited persona, describe the skill you want, and
// the coding model (Nemotron via NIM, or Mellum2 via ZeroGPU — see Settings → Coding
// Model) writes a self-contained skill definition tailored to that hero. For now it
// just generates and shows the skill; wiring it into the battle engine comes later.
import { streamCoding, currentCodingModel, onCodingModelChange } from '/web/codingModel.js'
import { listPersonas, getPersona, onRosterChange } from '/web/personaStore.js'
import { stripThink, stripThinkFinal } from '/web/personaPrompts.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
}
const SYSTEM = [
'You are the Skill Forge for a fantasy auto-battler. You author ONE combat skill for a',
'specific hero, tailored to their class, story and personality. Respond with exactly:',
'a one-line skill name, a one-sentence flavour description, then a fenced ```json block',
'with fields {name, type:"active"|"passive", target:"self"|"ally"|"enemy"|"area",',
'cooldown:<int seconds>, effect:"<short mechanical description>"}.',
'Keep it concise and balanced. Output only the skill — no preamble, no commentary.',
].join(' ')
function personaBlock(p) {
if (!p) return ''
return [
`Hero: ${p.name || 'Unnamed'}${p.unitClass ? ` — ${p.unitClass}` : ''}`,
p.about ? `About: ${p.about}` : '',
p.personality ? `Personality: ${p.personality}` : '',
p.specialty ? `Specialty: ${p.specialty}` : '',
].filter(Boolean).join('\n')
}
export function mountSkillForgePanel(host) {
const sel = el('select', { class: 'persona-input skillforge-hero' })
const req = el('textarea', { class: 'persona-prompt-edit skillforge-req', rows: 4,
placeholder: 'what skill should this hero learn? (e.g. “a defensive shout that shields nearby allies”)' })
const btn = el('button', { class: 'persona-go', type: 'button' }, '⚒ Forge skill')
const status = el('div', { class: 'persona-status' })
const out = el('pre', { class: 'persona-think skillforge-out' })
const empty = el('div', { class: 'persona-roster-empty' },
'No heroes yet — recruit one in the Personas tab, then come back to forge its skills.')
// "Show thinking" reveals the reasoning models' <think> trace (Nemotron, BLS) in the debug
// panel below; off by default so forging stays fast/clean. (Mellum2 has no reasoning.)
const thinkChk = el('input', { type: 'checkbox', class: 'skillforge-think' })
const thinkLabel = el('label', { class: 'persona-label skillforge-think-label' },
[thinkChk, ' show model thinking'])
const dbgEl = el('pre', { class: 'persona-think' })
const copyBtn = el('button', { class: 'persona-copy', type: 'button',
onclick: () => navigator.clipboard?.writeText(dbgEl.textContent || '') }, 'copy')
const dbgWrap = el('details', { class: 'persona-think-wrap' },
[el('summary', {}, 'model thinking / raw'), copyBtn, dbgEl])
dbgWrap.style.display = thinkChk.checked ? '' : 'none'
const controls = el('aside', { class: 'persona-controls skillforge' }, [
el('div', { class: 'persona-sec' }, [el('div', { class: 'persona-sec-title' }, 'Skill Forge'), el('span')]),
el('label', { class: 'persona-label' }, 'Hero'), sel,
empty,
el('label', { class: 'persona-label' }, 'Skill request'), req,
thinkLabel,
el('div', { class: 'persona-prompt-actions' }, [btn]),
status,
el('label', { class: 'persona-label' }, 'Forged skill'), out,
dbgWrap,
])
host.append(controls)
thinkChk.addEventListener('change', () => { dbgWrap.style.display = thinkChk.checked ? '' : 'none' })
function refreshHeroes() {
const people = listPersonas()
const prev = sel.value
sel.replaceChildren(...people.map((p) => el('option', { value: p.id }, p.name || 'Unnamed hero')))
if (people.some((p) => p.id === prev)) sel.value = prev
const none = people.length === 0
empty.style.display = none ? '' : 'none'
sel.style.display = none ? 'none' : ''
btn.disabled = none
}
function refreshStatus() {
if (!status.dataset.busy) status.textContent = `Coding model: ${currentCodingModel().label}`
}
let running = false
async function forge() {
if (running) return
const p = getPersona(sel.value)
if (!p) { status.textContent = 'Pick a hero first.'; return }
const ask = req.value.trim()
if (!ask) { status.textContent = 'Describe the skill you want.'; return }
running = true; status.dataset.busy = '1'; btn.disabled = true
out.textContent = ''; dbgEl.textContent = ''
status.textContent = `Forging with ${currentCodingModel().label}…`
const user = `${personaBlock(p)}\n\nSkill to create: ${ask}`
const showThink = thinkChk.checked
let raw = ''
try {
const { stats } = await streamCoding(SYSTEM, user, {
maxTokens: 512,
temperature: 0.6,
think: showThink,
// Reasoning streams inside <think>…</think>; show the raw trace in the debug panel and
// the stripped answer in the output (same split the persona panel uses).
onToken: (t) => {
raw += t
out.textContent = stripThink(raw)
if (showThink) { dbgEl.textContent = raw; dbgWrap.open = true; dbgEl.scrollTop = dbgEl.scrollHeight }
},
})
out.textContent = stripThinkFinal(raw)
const tps = stats && stats.tokPerSec ? ` · ${stats.tokPerSec} tok/s` : ''
status.textContent = `Done${tps}.`
} catch (e) {
status.textContent = 'Forge failed: ' + (e && e.message ? e.message : e)
} finally {
running = false; delete status.dataset.busy; btn.disabled = listPersonas().length === 0
}
}
btn.addEventListener('click', forge)
onRosterChange(refreshHeroes)
onCodingModelChange(refreshStatus)
refreshHeroes(); refreshStatus()
return { refresh: refreshHeroes }
}
|