frontend: rewrite Last Prompt as Prompt History viewer
- status.js: replace loadLastPrompt() with loadPromptHistory() + helpers - fetch /prompts with optional source filter, populate dropdown - selectPromptEntry() renders metadata bar + collapsible subsections - parsePromptSections() splits full_prompt into System/Context/Conversation - buildCollapsibleSection() with toggle arrows (▼/▶) - copyPromptToClipboard() copies raw text - toggleMiddleTruncation() truncates response from middle - togglePromptHistoryCollapse() collapses entire section - legacy loadLastPrompt() delegates to loadPromptHistory() - core.js: add promptInterval to polling (10s), visibility resume - update switchPromptSource() for 'all' filter + new button IDs - update initPromptSourceToggle() default to 'all' - declare promptInterval variable
This commit is contained in:
@@ -29,6 +29,7 @@ let notificationTimer = null;
|
|||||||
let statusInterval = null;
|
let statusInterval = null;
|
||||||
let logsInterval = null;
|
let logsInterval = null;
|
||||||
let argsInterval = null;
|
let argsInterval = null;
|
||||||
|
let promptInterval = null;
|
||||||
|
|
||||||
// Mood emoji mapping
|
// Mood emoji mapping
|
||||||
const MOOD_EMOJIS = {
|
const MOOD_EMOJIS = {
|
||||||
@@ -211,12 +212,14 @@ function startPolling() {
|
|||||||
if (!statusInterval) statusInterval = setInterval(loadStatus, 10000);
|
if (!statusInterval) statusInterval = setInterval(loadStatus, 10000);
|
||||||
if (!logsInterval) logsInterval = setInterval(loadLogs, 5000);
|
if (!logsInterval) logsInterval = setInterval(loadLogs, 5000);
|
||||||
if (!argsInterval) argsInterval = setInterval(loadActiveArguments, 5000);
|
if (!argsInterval) argsInterval = setInterval(loadActiveArguments, 5000);
|
||||||
|
if (!promptInterval) promptInterval = setInterval(loadPromptHistory, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
clearInterval(statusInterval); statusInterval = null;
|
clearInterval(statusInterval); statusInterval = null;
|
||||||
clearInterval(logsInterval); logsInterval = null;
|
clearInterval(logsInterval); logsInterval = null;
|
||||||
clearInterval(argsInterval); argsInterval = null;
|
clearInterval(argsInterval); argsInterval = null;
|
||||||
|
clearInterval(promptInterval); promptInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -248,7 +251,7 @@ function initVisibilityPolling() {
|
|||||||
stopPolling();
|
stopPolling();
|
||||||
console.log('⏸ Tab hidden — polling paused');
|
console.log('⏸ Tab hidden — polling paused');
|
||||||
} else {
|
} else {
|
||||||
loadStatus(); loadLogs(); loadActiveArguments();
|
loadStatus(); loadLogs(); loadActiveArguments(); loadPromptHistory();
|
||||||
startPolling();
|
startPolling();
|
||||||
console.log('▶️ Tab visible — polling resumed');
|
console.log('▶️ Tab visible — polling resumed');
|
||||||
}
|
}
|
||||||
@@ -296,9 +299,11 @@ function initModalAccessibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initPromptSourceToggle() {
|
function initPromptSourceToggle() {
|
||||||
const saved = localStorage.getItem('miku-prompt-source') || 'cat';
|
const saved = localStorage.getItem('miku-prompt-source') || 'all';
|
||||||
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
document.getElementById(`prompt-src-${saved}`).classList.add('active');
|
const btnId = saved === 'all' ? 'prompt-src-all' : `prompt-src-${saved}`;
|
||||||
|
const btn = document.getElementById(btnId);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function initLogsScrollDetection() {
|
function initLogsScrollDetection() {
|
||||||
@@ -360,8 +365,10 @@ async function loadLogs() {
|
|||||||
function switchPromptSource(source) {
|
function switchPromptSource(source) {
|
||||||
localStorage.setItem('miku-prompt-source', source);
|
localStorage.setItem('miku-prompt-source', source);
|
||||||
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
document.getElementById(`prompt-src-${source}`).classList.add('active');
|
const btnId = source === 'all' ? 'prompt-src-all' : `prompt-src-${source}`;
|
||||||
loadLastPrompt();
|
const btn = document.getElementById(btnId);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
loadPromptHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -57,33 +57,252 @@ async function loadStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Last Prompt =====
|
// ===== Prompt History =====
|
||||||
|
|
||||||
async function loadLastPrompt() {
|
let _promptHistoryCache = []; // cached history entries from last fetch
|
||||||
const source = localStorage.getItem('miku-prompt-source') || 'cat';
|
let _selectedPromptId = null; // currently selected entry ID
|
||||||
const promptEl = document.getElementById('last-prompt');
|
let _middleTruncation = false; // whether middle-truncation is active
|
||||||
const infoEl = document.getElementById('prompt-cat-info');
|
|
||||||
|
async function loadPromptHistory() {
|
||||||
|
const source = localStorage.getItem('miku-prompt-source') || 'all';
|
||||||
|
const selectEl = document.getElementById('prompt-history-select');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (source === 'cat') {
|
const url = source === 'all' ? '/prompts' : `/prompts?source=${source}`;
|
||||||
const result = await apiCall('/prompt/cat');
|
const result = await apiCall(url);
|
||||||
if (result.timestamp) {
|
_promptHistoryCache = result.history || [];
|
||||||
infoEl.innerHTML = `<strong>User:</strong> ${escapeHtml(result.user || '?')} | <strong>Mood:</strong> ${escapeHtml(result.mood || '?')} | <strong>Time:</strong> ${new Date(result.timestamp).toLocaleString()}`;
|
|
||||||
promptEl.textContent = result.full_prompt + `\n\n${'═'.repeat(60)}\n[Cat Response]\n${result.response}`;
|
// Populate dropdown
|
||||||
|
const currentValue = selectEl.value;
|
||||||
|
selectEl.innerHTML = '';
|
||||||
|
if (_promptHistoryCache.length === 0) {
|
||||||
|
selectEl.innerHTML = '<option value="">-- No prompts yet --</option>';
|
||||||
} else {
|
} else {
|
||||||
infoEl.textContent = '';
|
_promptHistoryCache.forEach(entry => {
|
||||||
promptEl.textContent = result.full_prompt || 'No Cheshire Cat interaction yet.';
|
const ts = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '?';
|
||||||
|
const srcLabel = entry.source === 'cat' ? '🐱' : '🤖';
|
||||||
|
const user = entry.user || '?';
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = entry.id;
|
||||||
|
option.textContent = `${srcLabel} #${entry.id} — ${user} — ${ts}`;
|
||||||
|
selectEl.appendChild(option);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore or auto-select the latest entry
|
||||||
|
if (_selectedPromptId && _promptHistoryCache.some(e => e.id === _selectedPromptId)) {
|
||||||
|
selectEl.value = _selectedPromptId;
|
||||||
|
} else if (_promptHistoryCache.length > 0) {
|
||||||
|
selectEl.value = _promptHistoryCache[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectEl.value) {
|
||||||
|
await selectPromptEntry(selectEl.value);
|
||||||
} else {
|
} else {
|
||||||
infoEl.textContent = '';
|
clearPromptDisplay();
|
||||||
const result = await apiCall('/prompt');
|
|
||||||
promptEl.textContent = result.prompt;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load last prompt:', error);
|
console.error('Failed to load prompt history:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectPromptEntry(promptId) {
|
||||||
|
if (!promptId) {
|
||||||
|
clearPromptDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedPromptId = parseInt(promptId);
|
||||||
|
|
||||||
|
// Try cache first
|
||||||
|
let entry = _promptHistoryCache.find(e => e.id === _selectedPromptId);
|
||||||
|
|
||||||
|
// Fall back to API call if not in cache
|
||||||
|
if (!entry) {
|
||||||
|
try {
|
||||||
|
entry = await apiCall(`/prompts/${_selectedPromptId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load prompt entry:', error);
|
||||||
|
clearPromptDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
clearPromptDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPromptEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPromptDisplay() {
|
||||||
|
document.getElementById('prompt-metadata').innerHTML = '';
|
||||||
|
document.getElementById('last-prompt').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPromptEntry(entry) {
|
||||||
|
// Metadata bar
|
||||||
|
const metaEl = document.getElementById('prompt-metadata');
|
||||||
|
const ts = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : '?';
|
||||||
|
const sourceIcon = entry.source === 'cat' ? '🐱 Cat' : '🤖 Fallback';
|
||||||
|
metaEl.innerHTML = `
|
||||||
|
<span><span class="prompt-meta-label">#</span><span class="prompt-meta-value">${entry.id}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Source:</span> <span class="prompt-meta-value">${sourceIcon}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">User:</span> <span class="prompt-meta-value">${escapeHtml(entry.user || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Mood:</span> <span class="prompt-meta-value">${escapeHtml(entry.mood || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Guild:</span> <span class="prompt-meta-value">${escapeHtml(entry.guild || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Channel:</span> <span class="prompt-meta-value">${escapeHtml(entry.channel || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Model:</span> <span class="prompt-meta-value">${escapeHtml(entry.model || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Type:</span> <span class="prompt-meta-value">${escapeHtml(entry.response_type || '?')}</span></span>
|
||||||
|
<span><span class="prompt-meta-label">Time:</span> <span class="prompt-meta-value">${ts}</span></span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Parse full_prompt into sections
|
||||||
|
const sections = parsePromptSections(entry.full_prompt || '');
|
||||||
|
|
||||||
|
// Build display HTML with collapsible subsections
|
||||||
|
let displayHtml = '';
|
||||||
|
|
||||||
|
if (sections.system) {
|
||||||
|
displayHtml += buildCollapsibleSection('System Prompt', sections.system, 'system');
|
||||||
|
}
|
||||||
|
if (sections.context) {
|
||||||
|
displayHtml += buildCollapsibleSection('Context (Memories & Tools)', sections.context, 'context');
|
||||||
|
}
|
||||||
|
if (sections.conversation) {
|
||||||
|
displayHtml += buildCollapsibleSection('Conversation', sections.conversation, 'conversation');
|
||||||
|
}
|
||||||
|
if (!sections.system && !sections.context && !sections.conversation) {
|
||||||
|
// Fallback: show raw full_prompt
|
||||||
|
displayHtml += `<pre style="white-space: pre-wrap; word-break: break-word; margin: 0;">${escapeHtml(entry.full_prompt || '')}</pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response section
|
||||||
|
if (entry.response) {
|
||||||
|
let responseText = entry.response;
|
||||||
|
if (_middleTruncation && responseText.length > 400) {
|
||||||
|
responseText = responseText.substring(0, 200) + '\n\n... [truncated middle] ...\n\n' + responseText.substring(responseText.length - 200);
|
||||||
|
}
|
||||||
|
displayHtml += buildCollapsibleSection('Response', escapeHtml(responseText), 'response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render into the prompt-display div (using innerHTML for collapsible structure)
|
||||||
|
const displayEl = document.getElementById('prompt-display');
|
||||||
|
displayEl.innerHTML = displayHtml;
|
||||||
|
|
||||||
|
// Also set the raw text into the <pre> for copy functionality
|
||||||
|
let rawText = entry.full_prompt || '';
|
||||||
|
if (entry.response) {
|
||||||
|
rawText += `\n\n${'═'.repeat(60)}\n[Response]\n${entry.response}`;
|
||||||
|
}
|
||||||
|
document.getElementById('last-prompt').textContent = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePromptSections(fullPrompt) {
|
||||||
|
const sections = { system: null, context: null, conversation: null };
|
||||||
|
|
||||||
|
if (!fullPrompt) return sections;
|
||||||
|
|
||||||
|
// Try to split on known section markers
|
||||||
|
const contextMatch = fullPrompt.match(/# Context\s*\n([\s\S]*?)(?=\n# Conversation|\nHuman:|\n$)/);
|
||||||
|
const convMatch = fullPrompt.match(/# Conversation until now:\s*\n([\s\S]*)/);
|
||||||
|
|
||||||
|
if (contextMatch) {
|
||||||
|
// Everything before # Context is the system prompt
|
||||||
|
const contextIdx = fullPrompt.indexOf('# Context');
|
||||||
|
if (contextIdx > 0) {
|
||||||
|
sections.system = fullPrompt.substring(0, contextIdx).trim();
|
||||||
|
}
|
||||||
|
sections.context = contextMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convMatch) {
|
||||||
|
sections.conversation = convMatch[1].trim();
|
||||||
|
} else {
|
||||||
|
// Try alternative: "Human:" at the end
|
||||||
|
const humanMatch = fullPrompt.match(/\nHuman:([\s\S]*)/);
|
||||||
|
if (humanMatch && fullPrompt.indexOf('Human:') > fullPrompt.indexOf('# Context')) {
|
||||||
|
sections.conversation = 'Human:' + humanMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no # Context marker, try "System:" prefix (fallback prompts)
|
||||||
|
if (!sections.system && !sections.context) {
|
||||||
|
const sysMatch = fullPrompt.match(/^System:\s*([\s\S]*?)(?=\nMessages:)/);
|
||||||
|
const msgMatch = fullPrompt.match(/Messages:\s*([\s\S]*)/);
|
||||||
|
if (sysMatch) {
|
||||||
|
sections.system = sysMatch[1].trim();
|
||||||
|
}
|
||||||
|
if (msgMatch) {
|
||||||
|
sections.conversation = msgMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCollapsibleSection(title, content, sectionId) {
|
||||||
|
const uniqueId = `prompt-section-${sectionId}-${Date.now()}`;
|
||||||
|
return `
|
||||||
|
<div class="prompt-subsection-header" onclick="togglePromptSubsection('${uniqueId}')">
|
||||||
|
▼ ${escapeHtml(title)}
|
||||||
|
</div>
|
||||||
|
<div class="prompt-subsection-body" id="${uniqueId}">
|
||||||
|
<pre style="white-space: pre-wrap; word-break: break-word; background: #1a1a1a; padding: 0.5rem; border-radius: 4px; font-size: 0.8rem; line-height: 1.4; margin: 0.25rem 0;">${content}</pre>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePromptSubsection(id) {
|
||||||
|
const body = document.getElementById(id);
|
||||||
|
if (!body) return;
|
||||||
|
const header = body.previousElementSibling;
|
||||||
|
if (body.classList.contains('collapsed')) {
|
||||||
|
body.classList.remove('collapsed');
|
||||||
|
if (header) header.innerHTML = header.innerHTML.replace('▶', '▼');
|
||||||
|
} else {
|
||||||
|
body.classList.add('collapsed');
|
||||||
|
if (header) header.innerHTML = header.innerHTML.replace('▼', '▶');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePromptHistoryCollapse() {
|
||||||
|
const section = document.getElementById('prompt-history-section');
|
||||||
|
const toggle = document.getElementById('prompt-history-toggle');
|
||||||
|
if (section.classList.contains('collapsed')) {
|
||||||
|
section.classList.remove('collapsed');
|
||||||
|
toggle.textContent = '▼ Prompt History';
|
||||||
|
} else {
|
||||||
|
section.classList.add('collapsed');
|
||||||
|
toggle.textContent = '▶ Prompt History';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPromptToClipboard() {
|
||||||
|
const rawText = document.getElementById('last-prompt').textContent;
|
||||||
|
if (!rawText) return;
|
||||||
|
navigator.clipboard.writeText(rawText).then(() => {
|
||||||
|
showNotification('Prompt copied to clipboard', 'success');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
showNotification('Failed to copy', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMiddleTruncation() {
|
||||||
|
_middleTruncation = document.getElementById('prompt-truncate-toggle').checked;
|
||||||
|
// Re-render current entry
|
||||||
|
if (_selectedPromptId) {
|
||||||
|
selectPromptEntry(_selectedPromptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility — called from core.js on page load / tab switch
|
||||||
|
// Redirects to the new loadPromptHistory()
|
||||||
|
async function loadLastPrompt() {
|
||||||
|
await loadPromptHistory();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Autonomous Stats =====
|
// ===== Autonomous Stats =====
|
||||||
|
|
||||||
async function loadAutonomousStats() {
|
async function loadAutonomousStats() {
|
||||||
|
|||||||
Reference in New Issue
Block a user