Spaces:
Running
Running
Cap persona + diary at 100 tokens; shorten prompts; stream the diary
Browse files- maxTokens 100 for both persona and diary (was 220/768 via thinkMaxTokens) so models
can't ramble past the answer. Prompts tightened to match: persona about → "1-2 short
sentences (~25 words)"; diary → "1-2 vivid sentences (~60 words), be brief".
- War diary now streams visibly: it used the aggressive stripThink live, which an
unclosed <think> emptied — so it showed blank until the end. Use stripThinkFinal in
the live onToken so the prose (and narration) appear token-by-token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- web/diaryPanel.js +6 -4
- web/personaPanel.js +4 -3
- web/personaPrompts.js +4 -4
web/diaryPanel.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
| 5 |
import { streamChat, ensureModel, currentModel, currentModelId, getEngineId, backendLabel } from '/web/runtime.js'
|
| 6 |
import { mountTtsBar } from '/web/ttsBar.js'
|
| 7 |
import { makeNarrator, ensureTts } from '/web/tts.js'
|
| 8 |
-
import { DIARY_SYSTEM, diaryUserPrompt,
|
|
|
|
|
|
|
| 9 |
|
| 10 |
function el(tag, props = {}, kids = []) {
|
| 11 |
const n = document.createElement(tag)
|
|
@@ -54,7 +56,7 @@ export function mountDiaryPanel(host) {
|
|
| 54 |
'=== TINY ARMY · DIARY DEBUG ===',
|
| 55 |
`engine: ${getEngineId()} · ${backendLabel()}`,
|
| 56 |
`model: ${currentModelId()} (${currentModel().label})`,
|
| 57 |
-
`input: unit=${unit.value} traits=${traits.value} maxTokens=${
|
| 58 |
`outcome: ${outcome}`,
|
| 59 |
`--- raw output (${(raw || '').length} chars) ---`,
|
| 60 |
raw || '(empty)',
|
|
@@ -139,10 +141,10 @@ export function mountDiaryPanel(host) {
|
|
| 139 |
await ensureModel((frac, label) => { status.textContent = label || `downloading ${currentModel().label}… ${Math.round(frac * 100)}% (one-time)` })
|
| 140 |
status.textContent = `writing on your device with ${currentModel().label}…`
|
| 141 |
await streamChat(DIARY_SYSTEM, diaryUserPrompt(unit.value, traits.value) + noThink(currentModelId()), {
|
| 142 |
-
maxTokens:
|
| 143 |
onToken: (piece) => {
|
| 144 |
raw += piece
|
| 145 |
-
lastBody =
|
| 146 |
out.textContent = header + lastBody
|
| 147 |
if (live) { const delta = lastBody.slice(spokenLen); if (delta) { live.push(delta); spokenLen = lastBody.length } }
|
| 148 |
},
|
|
|
|
| 5 |
import { streamChat, ensureModel, currentModel, currentModelId, getEngineId, backendLabel } from '/web/runtime.js'
|
| 6 |
import { mountTtsBar } from '/web/ttsBar.js'
|
| 7 |
import { makeNarrator, ensureTts } from '/web/tts.js'
|
| 8 |
+
import { DIARY_SYSTEM, diaryUserPrompt, stripThinkFinal, noThink } from '/web/personaPrompts.js'
|
| 9 |
+
|
| 10 |
+
const MAX_TOKENS = 100 // short diary entries — cap matches the "~60 words" prompt
|
| 11 |
|
| 12 |
function el(tag, props = {}, kids = []) {
|
| 13 |
const n = document.createElement(tag)
|
|
|
|
| 56 |
'=== TINY ARMY · DIARY DEBUG ===',
|
| 57 |
`engine: ${getEngineId()} · ${backendLabel()}`,
|
| 58 |
`model: ${currentModelId()} (${currentModel().label})`,
|
| 59 |
+
`input: unit=${unit.value} traits=${traits.value} maxTokens=${MAX_TOKENS}`,
|
| 60 |
`outcome: ${outcome}`,
|
| 61 |
`--- raw output (${(raw || '').length} chars) ---`,
|
| 62 |
raw || '(empty)',
|
|
|
|
| 141 |
await ensureModel((frac, label) => { status.textContent = label || `downloading ${currentModel().label}… ${Math.round(frac * 100)}% (one-time)` })
|
| 142 |
status.textContent = `writing on your device with ${currentModel().label}…`
|
| 143 |
await streamChat(DIARY_SYSTEM, diaryUserPrompt(unit.value, traits.value) + noThink(currentModelId()), {
|
| 144 |
+
maxTokens: MAX_TOKENS, temperature: 0.9,
|
| 145 |
onToken: (piece) => {
|
| 146 |
raw += piece
|
| 147 |
+
lastBody = stripThinkFinal(raw)
|
| 148 |
out.textContent = header + lastBody
|
| 149 |
if (live) { const delta = lastBody.slice(spokenLen); if (delta) { live.push(delta); spokenLen = lastBody.length } }
|
| 150 |
},
|
web/personaPanel.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
| 5 |
import { streamChat, ensureModel, currentModel, currentModelId, getEngineId, backendLabel } from '/web/runtime.js'
|
| 6 |
import { extractLivePersona } from '/web/personaStream.js'
|
| 7 |
import { parsePersonaJson } from '/web/personaParse.js'
|
| 8 |
-
import { PERSONA_SYSTEM, personaUserPrompt, stripThink, stripThinkFinal, noThink
|
| 9 |
|
| 10 |
const CLASSES = ['Warrior', 'Ranger', 'Monk', 'Assassin', 'Mage', 'Paladin', 'Cleric', 'Knight']
|
|
|
|
| 11 |
|
| 12 |
function el(tag, props = {}, kids = []) {
|
| 13 |
const n = document.createElement(tag)
|
|
@@ -59,7 +60,7 @@ export function mountPersonaPanel(host) {
|
|
| 59 |
'=== TINY ARMY · PERSONA DEBUG ===',
|
| 60 |
`engine: ${getEngineId()} · ${backendLabel()}`,
|
| 61 |
`model: ${currentModelId()} (${currentModel().label})`,
|
| 62 |
-
`input: class=${sel.value} seed=${seed.value || '(none)'} maxTokens=${
|
| 63 |
`outcome: ${outcome}`,
|
| 64 |
`--- raw output (${(acc || '').length} chars) ---`,
|
| 65 |
acc || '(empty)',
|
|
@@ -95,7 +96,7 @@ export function mountPersonaPanel(host) {
|
|
| 95 |
await ensureModel((frac, label) => { status.textContent = label || `downloading ${currentModel().label}… ${Math.round(frac * 100)}% (one-time)` })
|
| 96 |
status.textContent = `writing on your device with ${currentModel().label}…`
|
| 97 |
await streamChat(PERSONA_SYSTEM, personaUserPrompt(sel.value, seed.value) + noThink(currentModelId()), {
|
| 98 |
-
maxTokens:
|
| 99 |
onToken: (piece) => {
|
| 100 |
acc += piece
|
| 101 |
thinkEl.textContent = acc // raw view shows the model's <think> reasoning too
|
|
|
|
| 5 |
import { streamChat, ensureModel, currentModel, currentModelId, getEngineId, backendLabel } from '/web/runtime.js'
|
| 6 |
import { extractLivePersona } from '/web/personaStream.js'
|
| 7 |
import { parsePersonaJson } from '/web/personaParse.js'
|
| 8 |
+
import { PERSONA_SYSTEM, personaUserPrompt, stripThink, stripThinkFinal, noThink } from '/web/personaPrompts.js'
|
| 9 |
|
| 10 |
const CLASSES = ['Warrior', 'Ranger', 'Monk', 'Assassin', 'Mage', 'Paladin', 'Cleric', 'Knight']
|
| 11 |
+
const MAX_TOKENS = 100 // short personas — keep models from rambling past the JSON
|
| 12 |
|
| 13 |
function el(tag, props = {}, kids = []) {
|
| 14 |
const n = document.createElement(tag)
|
|
|
|
| 60 |
'=== TINY ARMY · PERSONA DEBUG ===',
|
| 61 |
`engine: ${getEngineId()} · ${backendLabel()}`,
|
| 62 |
`model: ${currentModelId()} (${currentModel().label})`,
|
| 63 |
+
`input: class=${sel.value} seed=${seed.value || '(none)'} maxTokens=${MAX_TOKENS}`,
|
| 64 |
`outcome: ${outcome}`,
|
| 65 |
`--- raw output (${(acc || '').length} chars) ---`,
|
| 66 |
acc || '(empty)',
|
|
|
|
| 96 |
await ensureModel((frac, label) => { status.textContent = label || `downloading ${currentModel().label}… ${Math.round(frac * 100)}% (one-time)` })
|
| 97 |
status.textContent = `writing on your device with ${currentModel().label}…`
|
| 98 |
await streamChat(PERSONA_SYSTEM, personaUserPrompt(sel.value, seed.value) + noThink(currentModelId()), {
|
| 99 |
+
maxTokens: MAX_TOKENS,
|
| 100 |
onToken: (piece) => {
|
| 101 |
acc += piece
|
| 102 |
thinkEl.textContent = acc // raw view shows the model's <think> reasoning too
|
web/personaPrompts.js
CHANGED
|
@@ -6,7 +6,7 @@ export const PERSONA_SYSTEM =
|
|
| 6 |
'fighter writes its own legend. Given a class and an optional seed, return ONE JSON ' +
|
| 7 |
'object and NOTHING else, with exactly these keys:\n' +
|
| 8 |
' "name": a short evocative soldier name (2-4 words),\n' +
|
| 9 |
-
' "about": 1-
|
| 10 |
' "specialty": a 1-3 word combat specialty,\n' +
|
| 11 |
' "personality": a 1-3 word personality tag,\n' +
|
| 12 |
' "vibe": a 1-3 word vibe.\n' +
|
|
@@ -14,9 +14,9 @@ export const PERSONA_SYSTEM =
|
|
| 14 |
|
| 15 |
export const DIARY_SYSTEM =
|
| 16 |
'You are a tiny soldier in the auto-battler Tiny Army, writing a short first-person ' +
|
| 17 |
-
'war-diary entry. Given your name and traits, write
|
| 18 |
-
'person about a day on the battlefield — heroic, grounded,
|
| 19 |
-
'Prose only: no headings, no lists, no preamble.'
|
| 20 |
|
| 21 |
export function personaUserPrompt(unitClass = '', seed = '') {
|
| 22 |
const s = seed && seed.trim() ? ` Seed inspiration: "${seed.trim()}".` : ''
|
|
|
|
| 6 |
'fighter writes its own legend. Given a class and an optional seed, return ONE JSON ' +
|
| 7 |
'object and NOTHING else, with exactly these keys:\n' +
|
| 8 |
' "name": a short evocative soldier name (2-4 words),\n' +
|
| 9 |
+
' "about": 1-2 short sentences of backstory (about 25 words) in a heroic, wry war-legend tone,\n' +
|
| 10 |
' "specialty": a 1-3 word combat specialty,\n' +
|
| 11 |
' "personality": a 1-3 word personality tag,\n' +
|
| 12 |
' "vibe": a 1-3 word vibe.\n' +
|
|
|
|
| 14 |
|
| 15 |
export const DIARY_SYSTEM =
|
| 16 |
'You are a tiny soldier in the auto-battler Tiny Army, writing a short first-person ' +
|
| 17 |
+
'war-diary entry. Given your name and traits, write just 1-2 vivid sentences (about ' +
|
| 18 |
+
'60 words, no more) in first person about a day on the battlefield — heroic, grounded, ' +
|
| 19 |
+
'a touch of dark humor. Prose only: no headings, no lists, no preamble. Be brief.'
|
| 20 |
|
| 21 |
export function personaUserPrompt(unitClass = '', seed = '') {
|
| 22 |
const s = seed && seed.trim() ? ` Seed inspiration: "${seed.trim()}".` : ''
|