// Tiny-Army persona + war-diary prompts (mirrors the Python prompts.py for the // in-browser path). War-legend tone, not woid's. // The default persona system prompt. Editable at runtime from the Settings page — // see getPersonaSystem()/setPersonaSystem() below; personaPanel.js calls // getPersonaSystem() so a saved override wins. export const PERSONA_SYSTEM_DEFAULT = 'You invent tiny heroes for a fantasy auto-battler called Tiny Army, where every ' + 'fighter writes its own legend. Given a class and an optional seed, return ONE JSON ' + 'object and NOTHING else, with exactly these keys:\n' + ' "name": a short evocative hero name (2-4 words),\n' + ' "about": 1-2 short sentences of backstory (about 25 words) in a heroic, wry war-legend tone,\n' + ' "quote": one short punchy line they say aloud — a battle-cry or wry remark, ' + 'first person, under 15 words,\n' + ' "voice": one sentence describing how THIS hero sounds for a text-to-speech voice — ' + 'pick a gender, age, pitch, accent, texture, pace and emotion that FIT their class and ' + 'personality, and make it DISTINCT from a generic gruff soldier. Vary it widely between ' + 'heroes — e.g. a bright quick-tongued young woman, a wheezing ancient sage, a velvet-smooth ' + 'rogue, a booming jolly giant, a cold precise duelist, a sing-song forest spirit.\n' + 'Output strictly valid JSON. No preamble, no code fences, no commentary.' // Runtime-editable persona prompt (Settings page). A non-empty localStorage override // replaces the default; clearing it falls back to PERSONA_SYSTEM_DEFAULT. const PERSONA_PROMPT_KEY = 'tinyarmy.personaPrompt' export function getPersonaSystem() { try { return localStorage.getItem(PERSONA_PROMPT_KEY) || PERSONA_SYSTEM_DEFAULT } catch { return PERSONA_SYSTEM_DEFAULT } } export function setPersonaSystem(text) { try { if (text && text.trim() && text.trim() !== PERSONA_SYSTEM_DEFAULT.trim()) localStorage.setItem(PERSONA_PROMPT_KEY, text) else localStorage.removeItem(PERSONA_PROMPT_KEY) } catch { /* storage blocked — just use the default */ } } export function resetPersonaSystem() { try { localStorage.removeItem(PERSONA_PROMPT_KEY) } catch { /* ignore */ } } export function isPersonaSystemCustom() { try { return !!localStorage.getItem(PERSONA_PROMPT_KEY) } catch { return false } } export const DIARY_SYSTEM = 'You are a tiny hero in the auto-battler Tiny Army, writing a short first-person ' + 'war-diary entry. Given your name and traits, write just 1-2 vivid sentences (about ' + '60 words, no more) in first person about a day on the battlefield — heroic, grounded, ' + 'a touch of dark humor. Prose only: no headings, no lists, no preamble. Be brief.' export function personaUserPrompt(unitClass = '', seed = '') { const s = seed && seed.trim() ? ` Seed inspiration: "${seed.trim()}".` : '' return `Class: ${(unitClass || 'hero').trim()}.${s} Return the JSON object now.` } export function diaryUserPrompt(unit = '', traits = '') { const u = (unit || 'a nameless hero').trim() const t = (traits || 'untested').trim() return `Name: ${u}. Traits: ${t}. Write the diary entry.` } // LIVE/streaming strip: hide the model's reasoning as it streams, including a still-open // block (so the reasoning doesn't flash in the preview mid-thought). export function stripThink(text) { return String(text || '') .replace(/[\s\S]*?<\/think>/gi, '') .replace(/[\s\S]*$/i, '') .replace(/^\s+/, '') } // FINAL strip (after generation is done): drop completed blocks and any // stray, never-closed / tags, but KEEP the answer that follows. Some models // (Qwen3 on WebLLM/MLC) open and jump straight to the answer WITHOUT ever closing // it — the aggressive streaming strip above would delete the whole answer, so the parser // saw 0 chars. This keeps it. Use this for parsing/final display, not for the live preview. export function stripThinkFinal(text) { return String(text || '') .replace(/[\s\S]*?<\/think>/gi, '') .replace(/<\/?think>/gi, '') .trim() } // Qwen3 is a thinking model: left alone it burns the whole token budget on a // block and never reaches the JSON/answer. export const isThinking = (modelId) => /qwen3/i.test(String(modelId || '')) // `/no_think` soft-switch tells Qwen3 to skip reasoning. llama.cpp/Transformers.js // honour it (the model finishes in a few tokens); WebLLM's MLC template does NOT // reliably, so we ALSO budget extra tokens (see thinkMaxTokens) for those — enough to // reason AND still finish the answer. No-op for non-thinking models. export const noThink = (modelId) => (isThinking(modelId) ? ' /no_think' : '') // Token budget: thinking models may spend the budget reasoning, so give them headroom // to still complete the answer. (When /no_think works, they stop early anyway.) export const thinkMaxTokens = (modelId, base) => (isThinking(modelId) ? Math.max(base, 768) : base)