|
|
@@ -38,6 +38,16 @@
|
|
|
|
#debug-trace details { border: 1px solid currentColor; border-radius: .3rem; padding: .5rem; }
|
|
|
|
#debug-trace details { border: 1px solid currentColor; border-radius: .3rem; padding: .5rem; }
|
|
|
|
#debug-trace summary { cursor: pointer; overflow-wrap: anywhere; }
|
|
|
|
#debug-trace summary { cursor: pointer; overflow-wrap: anywhere; }
|
|
|
|
#debug-trace pre { white-space: pre-wrap; overflow-wrap: anywhere; font: .8rem/1.45 ui-monospace, monospace; }
|
|
|
|
#debug-trace pre { white-space: pre-wrap; overflow-wrap: anywhere; font: .8rem/1.45 ui-monospace, monospace; }
|
|
|
|
|
|
|
|
.trace-chain { display: grid; gap: .65rem; margin-block: .75rem; }
|
|
|
|
|
|
|
|
.trace-step { border: 1px solid currentColor; border-inline-start: .35rem solid currentColor; border-radius: .3rem; padding: .65rem; }
|
|
|
|
|
|
|
|
.trace-step[data-engine="player"] { border-inline-start-color: #64748b; }
|
|
|
|
|
|
|
|
.trace-step[data-engine="ai"] { border-inline-start-color: #2563eb; }
|
|
|
|
|
|
|
|
.trace-step[data-engine="luau"] { border-inline-start-color: #9333ea; }
|
|
|
|
|
|
|
|
.trace-step[data-engine="rust"] { border-inline-start-color: #c2410c; }
|
|
|
|
|
|
|
|
.trace-step h3 { font-size: 1rem; margin: 0 0 .5rem; }
|
|
|
|
|
|
|
|
.trace-step .trace-engine { font-weight: 750; }
|
|
|
|
|
|
|
|
.trace-step pre { margin: .25rem 0 .75rem; }
|
|
|
|
|
|
|
|
.trace-raw { margin-block-start: .75rem; }
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
body.debug-layout { height: auto; overflow: auto; }
|
|
|
|
body.debug-layout { height: auto; overflow: auto; }
|
|
|
|
#play-layout.debug-enabled { grid-template-columns: minmax(0, 1fr); height: auto; }
|
|
|
|
#play-layout.debug-enabled { grid-template-columns: minmax(0, 1fr); height: auto; }
|
|
|
@@ -70,7 +80,7 @@
|
|
|
|
<form id="message-form">
|
|
|
|
<form id="message-form">
|
|
|
|
<label for="message" id="message-label">Skriv ett meddelande</label>
|
|
|
|
<label for="message" id="message-label">Skriv ett meddelande</label>
|
|
|
|
<textarea id="message" name="message" maxlength="4000" aria-describedby="message-help" autocomplete="off"></textarea>
|
|
|
|
<textarea id="message" name="message" maxlength="4000" aria-describedby="message-help" autocomplete="off"></textarea>
|
|
|
|
<div id="message-tools"><button type="submit" id="send-message">Skicka</button> <button type="button" id="microphone" aria-pressed="false" aria-describedby="voice-status">Starta mikrofon</button></div>
|
|
|
|
<div id="message-tools"><button type="submit" id="send-message">Skicka</button> <button type="button" id="microphone" aria-pressed="false" aria-describedby="voice-status">Starta mikrofon</button> <button type="button" id="redo-action" disabled>Gör om senaste</button> <button type="button" id="dry-run-action" disabled>Torrkör senaste</button></div>
|
|
|
|
<span id="message-help" class="sr-only">Tryck på Skicka eller Enter för att skicka. Tryck Skift och Enter för ny rad.</span>
|
|
|
|
<span id="message-help" class="sr-only">Tryck på Skicka eller Enter för att skicka. Tryck Skift och Enter för ny rad.</span>
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
<p id="voice-status" role="status" aria-live="polite"></p>
|
|
|
|
<p id="voice-status" role="status" aria-live="polite"></p>
|
|
|
@@ -82,7 +92,7 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<aside id="debug-panel" aria-labelledby="debug-heading" hidden>
|
|
|
|
<aside id="debug-panel" aria-labelledby="debug-heading" hidden>
|
|
|
|
<h2 id="debug-heading">Beslutsspårning</h2>
|
|
|
|
<h2 id="debug-heading">Beslutsspårning</h2>
|
|
|
|
<p>Administratörsvy för ditt eget spelkonto. Promptar, modelltrafik, regelresultat och Rust-commit visas från revisionsloggad API-data. Hemligheter och credentials ska vara maskerade av servern.</p>
|
|
|
|
<p>Administratörsvy för ditt eget spelkonto. Varje steg visar sparad indata och utdata i kedjan: spelare (grå), AI (blå), Luau (lila) och Rust (orange). Promptar, modelltrafik, regelresultat och commit visas från revisionsloggad API-data. Hemligheter och credentials maskeras av servern.</p>
|
|
|
|
<button type="button" id="debug-refresh">Uppdatera spår</button>
|
|
|
|
<button type="button" id="debug-refresh">Uppdatera spår</button>
|
|
|
|
<p id="debug-status" role="status" aria-live="polite">Hämtar spårning…</p>
|
|
|
|
<p id="debug-status" role="status" aria-live="polite">Hämtar spårning…</p>
|
|
|
|
<div id="debug-trace" aria-label="Kronologiskt beslutsspår"></div>
|
|
|
|
<div id="debug-trace" aria-label="Kronologiskt beslutsspår"></div>
|
|
|
@@ -126,6 +136,7 @@
|
|
|
|
let submitBusy = false;
|
|
|
|
let submitBusy = false;
|
|
|
|
let pendingDialogue = null;
|
|
|
|
let pendingDialogue = null;
|
|
|
|
let latestAssistantText = '';
|
|
|
|
let latestAssistantText = '';
|
|
|
|
|
|
|
|
let latestPlayerAction = '';
|
|
|
|
let latestAssistantVoice = {role: 'narrator', voiceIdentity: 'neutral'};
|
|
|
|
let latestAssistantVoice = {role: 'narrator', voiceIdentity: 'neutral'};
|
|
|
|
let speechRecoveryElement = null;
|
|
|
|
let speechRecoveryElement = null;
|
|
|
|
const voiceSettingsKey = 'ai-spel.voice-settings.v2';
|
|
|
|
const voiceSettingsKey = 'ai-spel.voice-settings.v2';
|
|
|
@@ -439,6 +450,7 @@
|
|
|
|
: next === 'world-draft'
|
|
|
|
: next === 'world-draft'
|
|
|
|
? 'Ändra startförslaget eller skriv “godkänn”'
|
|
|
|
? 'Ändra startförslaget eller skriv “godkänn”'
|
|
|
|
: 'Skriv vad du vill göra eller fråga';
|
|
|
|
: 'Skriv vad du vill göra eller fråga';
|
|
|
|
|
|
|
|
refreshActionTools();
|
|
|
|
messageField.focus();
|
|
|
|
messageField.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function showAuth() {
|
|
|
|
function showAuth() {
|
|
|
@@ -673,7 +685,9 @@
|
|
|
|
const lines = await request('/api/dialogue');
|
|
|
|
const lines = await request('/api/dialogue');
|
|
|
|
transcript.replaceChildren();
|
|
|
|
transcript.replaceChildren();
|
|
|
|
latestAssistantText = '';
|
|
|
|
latestAssistantText = '';
|
|
|
|
for (const line of lines) appendLine(line.role === 'user' ? 'user' : 'assistant', line.content, {speak: false});
|
|
|
|
latestPlayerAction = [...lines].reverse().find(line => line.role === 'user')?.content || '';
|
|
|
|
|
|
|
|
for (const line of lines) appendLine(line.role === 'user' ? 'user' : 'assistant', line.content, {speaker: line.speaker, speak: false});
|
|
|
|
|
|
|
|
refreshActionTools();
|
|
|
|
if (lines.length > 0 && latestAssistantText && location.pathname !== '/admin') {
|
|
|
|
if (lines.length > 0 && latestAssistantText && location.pathname !== '/admin') {
|
|
|
|
voiceController?.speak(latestAssistantText, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
voiceController?.speak(latestAssistantText, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
} else if (lines.length === 0 && gameState?.world) {
|
|
|
|
} else if (lines.length === 0 && gameState?.world) {
|
|
|
@@ -707,7 +721,7 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function traceTimestamp(record) {
|
|
|
|
function traceTimestamp(record) {
|
|
|
|
return record.recorded_at || record.requested_at || record.completed_at || '';
|
|
|
|
return record.recorded_at || record.requested_at || record.completed_at || record.created_at || '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function renderDecisionTrace(data) {
|
|
|
|
function renderDecisionTrace(data) {
|
|
|
|
const groups = new Map();
|
|
|
|
const groups = new Map();
|
|
|
@@ -736,9 +750,88 @@
|
|
|
|
const eventNames = group.events.map(event => `${event.event_type} (${event.commit_status || 'okänt utfall'})`).join(', ');
|
|
|
|
const eventNames = group.events.map(event => `${event.event_type} (${event.commit_status || 'okänt utfall'})`).join(', ');
|
|
|
|
const prompts = group.ai_interactions.map(interaction => `${interaction.prompt_id} v${interaction.prompt_version}: ${interaction.outcome}`).join(', ');
|
|
|
|
const prompts = group.ai_interactions.map(interaction => `${interaction.prompt_id} v${interaction.prompt_version}: ${interaction.outcome}`).join(', ');
|
|
|
|
summary.textContent = `${group.latest || 'Tid saknas'} · ${group.correlation_id}${eventNames ? ` · Rust: ${eventNames}` : ''}${prompts ? ` · AI: ${prompts}` : ''}`;
|
|
|
|
summary.textContent = `${group.latest || 'Tid saknas'} · ${group.correlation_id}${eventNames ? ` · Rust: ${eventNames}` : ''}${prompts ? ` · AI: ${prompts}` : ''}`;
|
|
|
|
|
|
|
|
const chain = document.createElement('div');
|
|
|
|
|
|
|
|
chain.className = 'trace-chain';
|
|
|
|
|
|
|
|
const addStep = (engine, title, input, output, note = '') => {
|
|
|
|
|
|
|
|
const card = document.createElement('article');
|
|
|
|
|
|
|
|
card.className = 'trace-step';
|
|
|
|
|
|
|
|
card.dataset.engine = engine;
|
|
|
|
|
|
|
|
const heading = document.createElement('h3');
|
|
|
|
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
|
|
|
label.className = 'trace-engine';
|
|
|
|
|
|
|
|
label.textContent = `${engine === 'luau' ? 'Luau' : engine === 'ai' ? 'AI' : engine === 'rust' ? 'Rust' : 'Spelare'} · `;
|
|
|
|
|
|
|
|
heading.append(label, document.createTextNode(title));
|
|
|
|
|
|
|
|
card.append(heading);
|
|
|
|
|
|
|
|
if (note) {
|
|
|
|
|
|
|
|
const noteElement = document.createElement('p');
|
|
|
|
|
|
|
|
noteElement.textContent = note;
|
|
|
|
|
|
|
|
card.append(noteElement);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const addValue = (name, value) => {
|
|
|
|
|
|
|
|
const valueHeading = document.createElement('strong');
|
|
|
|
|
|
|
|
valueHeading.textContent = name;
|
|
|
|
|
|
|
|
const pre = document.createElement('pre');
|
|
|
|
|
|
|
|
pre.textContent = typeof value === 'string' ? value : JSON.stringify(value ?? 'Ingen uppgift sparad', null, 2);
|
|
|
|
|
|
|
|
card.append(valueHeading, pre);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
addValue('Indata', input);
|
|
|
|
|
|
|
|
addValue('Utdata', output);
|
|
|
|
|
|
|
|
chain.append(card);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const orderedEvents = [...group.events].sort((a, b) => traceTimestamp(a).localeCompare(traceTimestamp(b)));
|
|
|
|
|
|
|
|
const orderedInteractions = [...group.ai_interactions].sort((a, b) => traceTimestamp(a).localeCompare(traceTimestamp(b)));
|
|
|
|
|
|
|
|
const playerMessages = [
|
|
|
|
|
|
|
|
...orderedEvents.map(event => event.payload?.player_message).filter(Boolean),
|
|
|
|
|
|
|
|
...group.player_messages.map(message => message.message).filter(Boolean),
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const [index, message] of playerMessages.entries()) {
|
|
|
|
|
|
|
|
addStep('player', `Spelarens avsikt ${playerMessages.length > 1 ? index + 1 : ''}`, message, {submitted: true});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const interaction of orderedInteractions) {
|
|
|
|
|
|
|
|
const external = interaction.external_call || {};
|
|
|
|
|
|
|
|
addStep('ai', `${interaction.prompt_id} · v${interaction.prompt_version} · ${interaction.outcome}`,
|
|
|
|
|
|
|
|
{prompt_yaml: interaction.prompt_yaml, rendered_messages: interaction.rendered_messages, provider: external.service, model: external.model, thinking_effort: external.thinking_effort, provider_request: external.request},
|
|
|
|
|
|
|
|
{response: interaction.response, outcome: interaction.outcome, provider_status: external.response_status, provider_response: external.response_body, provider_error: external.error, duration_ms: external.duration_ms, rust_interpretation_and_validation: interaction.metadata},
|
|
|
|
|
|
|
|
'AI-förslag och providertrafik. Förslaget är inte i sig ett fastställt världsfaktum.');
|
|
|
|
|
|
|
|
const metadata = interaction.metadata || {};
|
|
|
|
|
|
|
|
if (metadata.validation_inputs || metadata.validation_results || metadata.decision?.includes?.('rule')) {
|
|
|
|
|
|
|
|
addStep('luau', `Regelutkast och tester · ${metadata.rule_key || metadata.activity_key || interaction.prompt_id}`,
|
|
|
|
|
|
|
|
{rule_draft: interaction.response, test_inputs: metadata.validation_inputs},
|
|
|
|
|
|
|
|
{decision: metadata.decision || interaction.outcome, test_results: metadata.validation_results, source_hash: metadata.source_hash, rule_id: metadata.rule_id, rule_version: metadata.rule_version},
|
|
|
|
|
|
|
|
'Luau körs med isolerade testindata; resultatet kontrolleras av Rust innan regeln aktiveras.');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const event of orderedEvents) {
|
|
|
|
|
|
|
|
const payload = event.payload || {};
|
|
|
|
|
|
|
|
const domain = payload.details || {};
|
|
|
|
|
|
|
|
const luau = domain.luau_trace;
|
|
|
|
|
|
|
|
if (luau) {
|
|
|
|
|
|
|
|
addStep('luau', `Regelkörning · ${domain.activity_key || domain.rule_id || 'regel'}`,
|
|
|
|
|
|
|
|
{source: event.rule_source || 'Källkod saknas i denna spårpost.', invocation: luau.input},
|
|
|
|
|
|
|
|
luau.error ? {error: luau.error, output: luau.output} : luau.output,
|
|
|
|
|
|
|
|
`Regel ${domain.rule_id || 'okänd'} version ${domain.rule_version ?? 'okänd'} · hash ${domain.rule_hash || 'saknas'}`);
|
|
|
|
|
|
|
|
addStep('rust', 'Effektvalidering och världskommando',
|
|
|
|
|
|
|
|
{proposed_effects: domain.rust_validation?.input_effects || luau.output, activity: domain.activity_key, world_version_before: luau.input?.snapshot?.version},
|
|
|
|
|
|
|
|
{validation: domain.rust_validation, committed_event: event.event_type, commit_status: event.commit_status, commit_sequence: event.commit_sequence, world_time: event.world_time, committed_details: domain},
|
|
|
|
|
|
|
|
'Rust verifierar typade effekter och skriver den godkända förändringen atomärt.');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
addStep('rust', `Domänbeslut och commit · ${event.event_type}`,
|
|
|
|
|
|
|
|
{player_intent: payload.player_message, interpreted_intent: payload.intent, details: domain, world_version: payload.world_version},
|
|
|
|
|
|
|
|
{commit_status: event.commit_status, commit_sequence: event.commit_sequence, world_time: event.world_time, event_id: event.event_id},
|
|
|
|
|
|
|
|
'Rust fastställer om handlingen kan committas till den kanoniska världen.');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const raw = document.createElement('details');
|
|
|
|
|
|
|
|
raw.className = 'trace-raw';
|
|
|
|
|
|
|
|
const rawSummary = document.createElement('summary');
|
|
|
|
|
|
|
|
rawSummary.textContent = 'Visa all rå spårdata';
|
|
|
|
const pre = document.createElement('pre');
|
|
|
|
const pre = document.createElement('pre');
|
|
|
|
pre.textContent = JSON.stringify(group, null, 2);
|
|
|
|
pre.textContent = JSON.stringify(group, null, 2);
|
|
|
|
details.append(summary, pre);
|
|
|
|
raw.append(rawSummary, pre);
|
|
|
|
|
|
|
|
details.append(summary, chain, raw);
|
|
|
|
debugTrace.append(details);
|
|
|
|
debugTrace.append(details);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -815,7 +908,12 @@
|
|
|
|
messageField.disabled = false;
|
|
|
|
messageField.disabled = false;
|
|
|
|
messageField.focus();
|
|
|
|
messageField.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
async function handleGameMessage(message) {
|
|
|
|
function refreshActionTools() {
|
|
|
|
|
|
|
|
const enabled = stage === 'game' && Boolean(latestPlayerAction) && !submitBusy;
|
|
|
|
|
|
|
|
document.querySelector('#redo-action').disabled = !enabled;
|
|
|
|
|
|
|
|
document.querySelector('#dry-run-action').disabled = !enabled;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
async function handleGameMessage(message, actionMode = 'normal') {
|
|
|
|
if (/^logga ut[.!]?$/i.test(message.trim())) {
|
|
|
|
if (/^logga ut[.!]?$/i.test(message.trim())) {
|
|
|
|
appendLine('user', message);
|
|
|
|
appendLine('user', message);
|
|
|
|
await handleLogout();
|
|
|
|
await handleLogout();
|
|
|
@@ -835,12 +933,13 @@
|
|
|
|
else await proposeWorld(message);
|
|
|
|
else await proposeWorld(message);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (await handlePlayerMessageCommand(message)) return;
|
|
|
|
if (actionMode === 'normal' && await handlePlayerMessageCommand(message)) return;
|
|
|
|
const idempotencyKey = pendingDialogue?.message === message
|
|
|
|
if (actionMode === 'normal') latestPlayerAction = message;
|
|
|
|
|
|
|
|
const idempotencyKey = actionMode === 'normal' && pendingDialogue?.message === message
|
|
|
|
? pendingDialogue.idempotencyKey
|
|
|
|
? pendingDialogue.idempotencyKey
|
|
|
|
: crypto.randomUUID();
|
|
|
|
: crypto.randomUUID();
|
|
|
|
pendingDialogue = {message, idempotencyKey};
|
|
|
|
if (actionMode === 'normal') pendingDialogue = {message, idempotencyKey};
|
|
|
|
appendLine('user', message);
|
|
|
|
appendLine('user', message, actionMode === 'dry_run' ? {speaker: 'Torrkörning'} : actionMode === 'redo' ? {speaker: 'Gör om'} : {});
|
|
|
|
messageField.disabled = true;
|
|
|
|
messageField.disabled = true;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
let result;
|
|
|
|
let result;
|
|
|
@@ -849,7 +948,7 @@
|
|
|
|
result = await request('/api/dialogue', {
|
|
|
|
result = await request('/api/dialogue', {
|
|
|
|
method: 'POST',
|
|
|
|
method: 'POST',
|
|
|
|
headers: {'content-type': 'application/json'},
|
|
|
|
headers: {'content-type': 'application/json'},
|
|
|
|
body: JSON.stringify({message, expected_world_version: gameState.world.version, idempotency_key: idempotencyKey}),
|
|
|
|
body: JSON.stringify({message, expected_world_version: gameState.world.version, idempotency_key: idempotencyKey, forget_previous_turn: actionMode !== 'normal', dry_run: actionMode === 'dry_run'}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
@@ -858,19 +957,27 @@
|
|
|
|
if (!gameState.world) throw new Error('world_not_available');
|
|
|
|
if (!gameState.world) throw new Error('world_not_available');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gameState.world.version = result.world_version;
|
|
|
|
if (result.status !== 'dry_run') {
|
|
|
|
gameState.world.world_time = result.world_time;
|
|
|
|
gameState.world.version = result.world_version;
|
|
|
|
pendingDialogue = null;
|
|
|
|
gameState.world.world_time = result.world_time;
|
|
|
|
if (result.intent === 'move' || result.intent === 'meet') {
|
|
|
|
pendingDialogue = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.status !== 'dry_run' && (result.intent === 'move' || result.intent === 'meet')) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const refreshed = await request('/api/world');
|
|
|
|
const refreshed = await request('/api/world');
|
|
|
|
if (refreshed.world) gameState = refreshed;
|
|
|
|
if (refreshed.world) gameState = refreshed;
|
|
|
|
} catch (_) { /* The committed dialogue stays successful if this optional view refresh fails. */ }
|
|
|
|
} catch (_) { /* The committed dialogue stays successful if this optional view refresh fails. */ }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const speakingNpc = ['speak', 'meet'].includes(result.intent) ? gameState.world.npc : null;
|
|
|
|
const speakingNpc = result.status !== 'dry_run' && ['speak', 'meet'].includes(result.intent) ? gameState.world.npc : null;
|
|
|
|
appendLine('assistant', result.reply, speakingNpc
|
|
|
|
for (const ruleName of result.debug_rules_created || []) {
|
|
|
|
|
|
|
|
appendLine('assistant', ruleName, {speaker: 'Nya regler', speak: false});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
appendLine('assistant', result.reply, result.status === 'dry_run'
|
|
|
|
|
|
|
|
? {speaker: 'Torrkörning', voice: {role: 'narrator', voiceIdentity: 'neutral'}}
|
|
|
|
|
|
|
|
: speakingNpc
|
|
|
|
? {speaker: speakingNpc.name, voice: {characterId: speakingNpc.id, voiceIdentity: speakingNpc.voice_identity}}
|
|
|
|
? {speaker: speakingNpc.name, voice: {characterId: speakingNpc.id, voiceIdentity: speakingNpc.voice_identity}}
|
|
|
|
: {speaker: 'Berättelsen', voice: {role: 'narrator', voiceIdentity: 'neutral'}});
|
|
|
|
: {speaker: 'Berättaren', voice: {role: 'narrator', voiceIdentity: 'neutral'}});
|
|
|
|
|
|
|
|
refreshActionTools();
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
messageField.value = message;
|
|
|
|
messageField.value = message;
|
|
|
|
appendLine('assistant', `${explainDialogueError(error)} Ditt meddelande ligger kvar i inmatningen.`);
|
|
|
|
appendLine('assistant', `${explainDialogueError(error)} Ditt meddelande ligger kvar i inmatningen.`);
|
|
|
@@ -895,11 +1002,11 @@
|
|
|
|
if (location.pathname !== '/admin') voiceController.speak(status.textContent, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
if (location.pathname !== '/admin') voiceController.speak(status.textContent, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
async function submitMessage(input, source = 'text') {
|
|
|
|
async function submitMessage(input, source = 'text', actionMode = 'normal') {
|
|
|
|
if (submitBusy || messageField.disabled) return;
|
|
|
|
if (submitBusy || messageField.disabled) return;
|
|
|
|
const message = String(input || '').trim();
|
|
|
|
const message = String(input || '').trim();
|
|
|
|
if (!message) return;
|
|
|
|
if (!message) return;
|
|
|
|
if (handleVoiceCommand(message, source)) { messageField.value = ''; return; }
|
|
|
|
if (actionMode === 'normal' && handleVoiceCommand(message, source)) { messageField.value = ''; return; }
|
|
|
|
submitBusy = true;
|
|
|
|
submitBusy = true;
|
|
|
|
refreshMicrophone();
|
|
|
|
refreshMicrophone();
|
|
|
|
voiceController.stopListening();
|
|
|
|
voiceController.stopListening();
|
|
|
@@ -911,11 +1018,12 @@
|
|
|
|
messageField.value = '';
|
|
|
|
messageField.value = '';
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (stage === 'biography') await handleBiography(message);
|
|
|
|
if (stage === 'biography') await handleBiography(message);
|
|
|
|
else await handleGameMessage(message);
|
|
|
|
else await handleGameMessage(message, actionMode);
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
appendLine('assistant', `Det gick inte att genomföra begäran: ${error.message}`);
|
|
|
|
appendLine('assistant', `Det gick inte att genomföra begäran: ${error.message}`);
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
submitBusy = false;
|
|
|
|
submitBusy = false;
|
|
|
|
|
|
|
|
refreshActionTools();
|
|
|
|
refreshMicrophone();
|
|
|
|
refreshMicrophone();
|
|
|
|
if (currentAccount?.is_admin) await refreshDecisionTrace();
|
|
|
|
if (currentAccount?.is_admin) await refreshDecisionTrace();
|
|
|
|
if (!play.hidden && !messageField.disabled) messageField.focus();
|
|
|
|
if (!play.hidden && !messageField.disabled) messageField.focus();
|
|
|
@@ -1006,6 +1114,12 @@
|
|
|
|
event.preventDefault();
|
|
|
|
event.preventDefault();
|
|
|
|
await submitMessage(messageField.value);
|
|
|
|
await submitMessage(messageField.value);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
document.querySelector('#redo-action').addEventListener('click', () => {
|
|
|
|
|
|
|
|
if (latestPlayerAction) submitMessage(latestPlayerAction, 'button', 'redo');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
document.querySelector('#dry-run-action').addEventListener('click', () => {
|
|
|
|
|
|
|
|
if (latestPlayerAction) submitMessage(latestPlayerAction, 'button', 'dry_run');
|
|
|
|
|
|
|
|
});
|
|
|
|
document.querySelectorAll('[data-admin-view]').forEach(button => button.addEventListener('click', async () => {
|
|
|
|
document.querySelectorAll('[data-admin-view]').forEach(button => button.addEventListener('click', async () => {
|
|
|
|
adminContent.setAttribute('aria-busy', 'true');
|
|
|
|
adminContent.setAttribute('aria-busy', 'true');
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|