|
|
|
@@ -28,6 +28,11 @@
|
|
|
|
|
#admin-content { display: grid; gap: 1rem; }
|
|
|
|
|
#admin-content section, #admin-content article { border: 1px solid currentColor; border-radius: .35rem; padding: .75rem; }
|
|
|
|
|
#admin-content article > button { display: block; max-width: 100%; margin-block: .5rem; text-align: start; white-space: normal; }
|
|
|
|
|
#admin-content article > .admin-story-reply { display: none; }
|
|
|
|
|
#admin-content .admin-simple-conversation > h2,
|
|
|
|
|
#admin-content .admin-simple-conversation > p:not(.admin-story-empty),
|
|
|
|
|
#admin-content .admin-simple-conversation article > :not(.admin-story-reply) { display: none; }
|
|
|
|
|
#admin-content .admin-simple-conversation article > .admin-story-reply { display: block; margin-block: .85rem; white-space: pre-wrap; }
|
|
|
|
|
#admin-content pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 32rem; overflow: auto; }
|
|
|
|
|
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
|
|
|
|
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
|
|
|
@@ -168,6 +173,7 @@
|
|
|
|
|
let adminOffset = 0;
|
|
|
|
|
const adminPageSize = 50;
|
|
|
|
|
let adminTraceOffset = 0;
|
|
|
|
|
let adminSimpleConversation = false;
|
|
|
|
|
let presenceTimer = null;
|
|
|
|
|
let playerMessagesPrimed = false;
|
|
|
|
|
const seenPlayerMessageIds = new Set();
|
|
|
|
@@ -178,6 +184,22 @@
|
|
|
|
|
if (!response.ok) throw new Error(body?.error || `HTTP ${response.status}`);
|
|
|
|
|
return body;
|
|
|
|
|
}
|
|
|
|
|
async function graphqlQuery(query, variables = {}) {
|
|
|
|
|
const response = await request('/graphql', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'content-type': 'application/json'},
|
|
|
|
|
body: JSON.stringify({query, variables}),
|
|
|
|
|
});
|
|
|
|
|
if (response.errors?.length) throw new Error(response.errors[0].message || 'GraphQL-förfrågan misslyckades');
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
|
|
|
|
async function getAdminTrace(accountId = null, limit = 200, offset = 0) {
|
|
|
|
|
const data = await graphqlQuery(
|
|
|
|
|
'query AdminPlayerTrace($accountId: String, $limit: Int!, $offset: Int!) { adminPlayerTrace(accountId: $accountId, limit: $limit, offset: $offset) }',
|
|
|
|
|
{accountId, limit, offset},
|
|
|
|
|
);
|
|
|
|
|
return data.adminPlayerTrace;
|
|
|
|
|
}
|
|
|
|
|
function appendLine(role, content, options = {}) {
|
|
|
|
|
const line = document.createElement('li');
|
|
|
|
|
line.dataset.role = role;
|
|
|
|
@@ -652,7 +674,8 @@
|
|
|
|
|
const profile = await request(`/api/admin/players/${encodeURIComponent(accountId)}`);
|
|
|
|
|
adminJson(`${profile.username} · profil och biografi`, profile);
|
|
|
|
|
adminTraceOffset = 0;
|
|
|
|
|
const trace = await request(`/api/admin/players/${encodeURIComponent(accountId)}/trace?limit=200&offset=0`);
|
|
|
|
|
adminSimpleConversation = false;
|
|
|
|
|
const trace = await getAdminTrace(accountId, 200, 0);
|
|
|
|
|
renderAdminConversations(trace, accountId);
|
|
|
|
|
}
|
|
|
|
|
function renderAdminConversations(trace, accountId, existingRoot, hasMoreOverride) {
|
|
|
|
@@ -670,7 +693,21 @@
|
|
|
|
|
const conversations = [...groups.values()].sort((a, b) => String(a.at).localeCompare(String(b.at)));
|
|
|
|
|
adminElement('h2', 'Samtal och beslut', root);
|
|
|
|
|
adminElement('p', `Visar ${conversations.length} korrelerade samtalsturer från den här sidan. Välj en berättarreplik eller ett AI-svar för att öppna hela registrerade beslutskedjan. Registreringstid, världstid och commitordning visas var för sig.`, root);
|
|
|
|
|
if (conversations.length === 0) { adminElement('p', 'Det finns inga sparade dialogturer för kontot ännu.', root); return; }
|
|
|
|
|
const readingToggle = adminElement('button', adminSimpleConversation ? 'Visa felsökningsspår' : 'Lättläst konversation', root);
|
|
|
|
|
readingToggle.type = 'button';
|
|
|
|
|
readingToggle.setAttribute('aria-pressed', String(adminSimpleConversation));
|
|
|
|
|
readingToggle.addEventListener('click', () => {
|
|
|
|
|
adminSimpleConversation = !adminSimpleConversation;
|
|
|
|
|
root.classList.toggle('admin-simple-conversation', adminSimpleConversation);
|
|
|
|
|
readingToggle.textContent = adminSimpleConversation ? 'Visa felsökningsspår' : 'Lättläst konversation';
|
|
|
|
|
readingToggle.setAttribute('aria-pressed', String(adminSimpleConversation));
|
|
|
|
|
});
|
|
|
|
|
root.classList.toggle('admin-simple-conversation', adminSimpleConversation);
|
|
|
|
|
if (conversations.length === 0) {
|
|
|
|
|
const empty = adminElement('p', 'Det finns inga sparade berättarrepliker för kontot ännu.', root);
|
|
|
|
|
empty.className = 'admin-story-empty';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const [index, group] of conversations.entries()) {
|
|
|
|
|
group.events.sort((a, b) => (a.commit_sequence ?? 0) - (b.commit_sequence ?? 0));
|
|
|
|
|
group.messages.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
|
|
|
@@ -683,6 +720,8 @@
|
|
|
|
|
if (payload.player_message) { adminElement('p', `Spelaren: ${payload.player_message}`, card); showedPlayerInput = true; }
|
|
|
|
|
const reply = payload.reply || payload.npc_reply;
|
|
|
|
|
if (reply) {
|
|
|
|
|
const story = adminElement('p', `Berättelsen: ${reply}`, card);
|
|
|
|
|
story.className = 'admin-story-reply';
|
|
|
|
|
const button = adminElement('button', `Berättaren${payload.npc_reply ? ' / NPC' : ''}: ${reply}`, card);
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
button.setAttribute('aria-expanded', 'false');
|
|
|
|
@@ -719,13 +758,13 @@
|
|
|
|
|
}
|
|
|
|
|
const pageHasMore = hasMoreOverride ?? trace.has_more ?? (Math.max(trace.events?.length || 0, trace.ai_interactions?.length || 0, trace.player_messages?.length || 0) >= (trace.limit || 200));
|
|
|
|
|
if (pageHasMore) {
|
|
|
|
|
const older = adminElement('button', 'Visa äldre samtalsturer', root);
|
|
|
|
|
const older = adminElement('button', adminSimpleConversation ? 'Visa äldre berättarrepliker' : 'Visa äldre samtalsturer', root);
|
|
|
|
|
older.type = 'button';
|
|
|
|
|
older.addEventListener('click', async () => {
|
|
|
|
|
older.disabled = true;
|
|
|
|
|
older.textContent = 'Hämtar äldre samtal…';
|
|
|
|
|
adminTraceOffset += trace.limit || 200;
|
|
|
|
|
const next = await request(`/api/admin/players/${encodeURIComponent(accountId)}/trace?limit=${trace.limit || 200}&offset=${adminTraceOffset}`);
|
|
|
|
|
const next = await getAdminTrace(accountId, trace.limit || 200, adminTraceOffset);
|
|
|
|
|
const merge = (key, idKey) => [...(trace[key] || []), ...(next[key] || [])].filter((item, index, all) => all.findIndex(candidate => candidate[idKey] === item[idKey]) === index);
|
|
|
|
|
renderAdminConversations({events: merge('events', 'event_id'), ai_interactions: merge('ai_interactions', 'id'), player_messages: merge('player_messages', 'id'), limit: next.limit, offset: next.offset}, accountId, root, next.has_more);
|
|
|
|
|
});
|
|
|
|
@@ -977,12 +1016,53 @@
|
|
|
|
|
messageField.disabled = false;
|
|
|
|
|
messageField.focus();
|
|
|
|
|
}
|
|
|
|
|
function savedDialogueLine(line) {
|
|
|
|
|
const element = document.createElement('li');
|
|
|
|
|
element.dataset.role = line.role === 'user' ? 'user' : line.role === 'system' ? 'system' : 'assistant';
|
|
|
|
|
element.textContent = `${line.speaker || (line.role === 'user' ? 'Du' : 'Berättelsen')}: ${line.content}`;
|
|
|
|
|
return element;
|
|
|
|
|
}
|
|
|
|
|
function dialogueLinesFromPage(page) {
|
|
|
|
|
return page.edges.flatMap(edge => edge.node.lines);
|
|
|
|
|
}
|
|
|
|
|
function addOlderDialogueControl(cursor) {
|
|
|
|
|
const item = document.createElement('li');
|
|
|
|
|
item.dataset.role = 'system';
|
|
|
|
|
const button = document.createElement('button');
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
button.textContent = 'Hämta äldre konversation';
|
|
|
|
|
button.addEventListener('click', async () => {
|
|
|
|
|
button.disabled = true;
|
|
|
|
|
button.textContent = 'Hämtar äldre konversation…';
|
|
|
|
|
try {
|
|
|
|
|
const page = (await graphqlQuery(
|
|
|
|
|
'query OlderDialogue($before: String!) { dialogue(first: 50, before: $before) { edges { cursor node { lines { role content speaker } } } pageInfo { hasPreviousPage startCursor } } }',
|
|
|
|
|
{before: cursor},
|
|
|
|
|
)).dialogue;
|
|
|
|
|
const oldLines = dialogueLinesFromPage(page);
|
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
|
for (const line of oldLines) fragment.append(savedDialogueLine(line));
|
|
|
|
|
item.remove();
|
|
|
|
|
transcript.prepend(fragment);
|
|
|
|
|
if (page.pageInfo.hasPreviousPage && page.pageInfo.startCursor) addOlderDialogueControl(page.pageInfo.startCursor);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
button.disabled = false;
|
|
|
|
|
button.textContent = `Kunde inte hämta äldre repliker: ${error.message}. Försök igen`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
item.append(button);
|
|
|
|
|
transcript.prepend(item);
|
|
|
|
|
}
|
|
|
|
|
async function loadSavedDialogue() {
|
|
|
|
|
const lines = await request('/api/dialogue');
|
|
|
|
|
const page = (await graphqlQuery(
|
|
|
|
|
'query CurrentDialogue { dialogue(first: 100) { edges { cursor node { lines { role content speaker } } } pageInfo { hasPreviousPage startCursor } } }',
|
|
|
|
|
)).dialogue;
|
|
|
|
|
const lines = dialogueLinesFromPage(page);
|
|
|
|
|
transcript.replaceChildren();
|
|
|
|
|
latestAssistantText = '';
|
|
|
|
|
latestAssistantText = [...lines].reverse().find(line => line.role === 'assistant')?.content || '';
|
|
|
|
|
latestPlayerAction = [...lines].reverse().find(line => line.role === 'user')?.content || '';
|
|
|
|
|
for (const line of lines) appendLine(line.role === 'user' ? 'user' : line.role === 'system' ? 'system' : 'assistant', line.content, {speaker: line.speaker, speak: false});
|
|
|
|
|
for (const line of lines) transcript.append(savedDialogueLine(line));
|
|
|
|
|
if (page.pageInfo.hasPreviousPage && page.pageInfo.startCursor) addOlderDialogueControl(page.pageInfo.startCursor);
|
|
|
|
|
refreshActionTools();
|
|
|
|
|
if (lines.length > 0 && latestAssistantText && location.pathname !== '/admin') {
|
|
|
|
|
voiceController?.speak(latestAssistantText, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
@@ -1143,7 +1223,7 @@
|
|
|
|
|
debugRefreshBusy = true;
|
|
|
|
|
debugStatus.textContent = 'Hämtar revisionsloggad spårning…';
|
|
|
|
|
try {
|
|
|
|
|
const trace = await request('/api/admin/self/trace?limit=200');
|
|
|
|
|
const trace = await getAdminTrace(null, 200, 0);
|
|
|
|
|
renderDecisionTrace(trace);
|
|
|
|
|
debugStatus.textContent = `Visar ${debugTrace.childElementCount} korrelerade händelsegrupper. Uppdaterad ${new Date().toLocaleTimeString('sv-SE')}.`;
|
|
|
|
|
} catch (error) {
|
|
|
|
|