feat: Implement resume strategy engine and session content parser

- Added `resume-strategy.ts` to determine optimal resume approaches including native, prompt concatenation, and hybrid modes.
- Introduced `determineResumeStrategy` function to evaluate various resume scenarios.
- Created utility functions for building context prefixes and formatting outputs in plain, YAML, and JSON formats.
- Added `session-content-parser.ts` to parse native CLI tool session files supporting Gemini/Qwen JSON and Codex JSONL formats.
- Implemented parsing logic for different session formats, including error handling for invalid lines.
- Provided functions to format conversations and extract user-assistant pairs from parsed sessions.
This commit is contained in:
catlog22
2025-12-13 20:29:19 +08:00
parent 32217f87fd
commit 52935d4b8e
26 changed files with 9387 additions and 86 deletions

View File

@@ -291,6 +291,19 @@ function renderCliSettingsSection() {
'</div>' +
'<p class="cli-setting-desc">' + t('cli.smartContextDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="refresh-cw" class="w-3 h-3"></i>' +
t('cli.nativeResume') +
'</label>' +
'<div class="cli-setting-control">' +
'<label class="cli-toggle">' +
'<input type="checkbox"' + (nativeResumeEnabled ? ' checked' : '') + ' onchange="setNativeResumeEnabled(this.checked)">' +
'<span class="cli-toggle-slider"></span>' +
'</label>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.nativeResumeDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item' + (!smartContextEnabled ? ' disabled' : '') + '">' +
'<label class="cli-setting-label">' +
'<i data-lucide="files" class="w-3 h-3"></i>' +

View File

@@ -11,10 +11,8 @@ async function renderHookManager() {
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Load hook config if not already loaded
if (!hookConfig.global.hooks && !hookConfig.project.hooks) {
await loadHookConfig();
}
// Always reload hook config to get latest data
await loadHookConfig();
const globalHooks = hookConfig.global?.hooks || {};
const projectHooks = hookConfig.project?.hooks || {};
@@ -84,8 +82,9 @@ async function renderHookManager() {
<span class="text-sm text-muted-foreground">${t('hook.wizardsDesc')}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${renderWizardCard('memory-update')}
${renderWizardCard('memory-setup')}
${renderWizardCard('skill-context')}
</div>
</div>
@@ -206,8 +205,10 @@ function renderWizardCard(wizardId) {
// Get translated wizard name and description
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') :
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') :
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
// Translate options
@@ -216,6 +217,11 @@ function renderWizardCard(wizardId) {
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
}
if (wizardId === 'memory-setup') {
if (optId === 'file-read') return t('hook.wizard.fileReadTracker');
if (optId === 'file-write') return t('hook.wizard.fileWriteTracker');
if (optId === 'prompts') return t('hook.wizard.promptTracker');
}
if (wizardId === 'skill-context') {
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
if (optId === 'auto') return t('hook.wizard.autoDetection');
@@ -228,6 +234,11 @@ function renderWizardCard(wizardId) {
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
}
if (wizardId === 'memory-setup') {
if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc');
if (optId === 'file-write') return t('hook.wizard.fileWriteTrackerDesc');
if (optId === 'prompts') return t('hook.wizard.promptTrackerDesc');
}
if (wizardId === 'skill-context') {
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
@@ -236,8 +247,9 @@ function renderWizardCard(wizardId) {
};
// Determine what to show in the tools/skills section
const toolsSection = wizard.requiresSkillDiscovery
? `
let toolsSection = '';
if (wizard.requiresSkillDiscovery) {
toolsSection = `
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.event')}</span>
<span class="px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded">UserPromptSubmit</span>
@@ -246,8 +258,12 @@ function renderWizardCard(wizardId) {
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
<span class="text-muted-foreground ml-2">${t('hook.wizard.loading')}</span>
</div>
`
: `
`;
} else if (wizard.multiSelect) {
// memory-setup: lightweight tracking, no CLI tools
toolsSection = '';
} else {
toolsSection = `
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.cliTools')}</span>
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded">gemini</span>
@@ -255,6 +271,7 @@ function renderWizardCard(wizardId) {
<span class="px-2 py-0.5 bg-green-500/10 text-green-500 rounded">codex</span>
</div>
`;
}
return `
<div class="hook-wizard-card bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-5 hover:shadow-lg transition-all">
@@ -308,8 +325,10 @@ function renderHooksByEvent(hooks, scope) {
return hookList.map((hook, index) => {
const matcher = hook.matcher || 'All tools';
const command = hook.command || 'N/A';
// Support both old format (hook.command) and new Claude Code format (hook.hooks[0].command)
const command = hook.hooks?.[0]?.command || hook.command || 'N/A';
const args = hook.args || [];
const timeout = hook.hooks?.[0]?.timeout || hook.timeout;
return `
<div class="hook-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all">
@@ -424,18 +443,28 @@ function isHookTemplateInstalled(templateId) {
const template = HOOK_TEMPLATES[templateId];
if (!template) return false;
// Build expected command string
const templateCmd = template.command + (template.args ? ' ' + template.args.join(' ') : '');
// Check project hooks
const projectHooks = hookConfig.project?.hooks?.[template.event];
if (projectHooks) {
const hookList = Array.isArray(projectHooks) ? projectHooks : [projectHooks];
if (hookList.some(h => h.command === template.command)) return true;
if (hookList.some(h => {
// Check both old format (h.command) and new format (h.hooks[0].command)
const cmd = h.hooks?.[0]?.command || h.command || '';
return cmd.includes(template.command);
})) return true;
}
// Check global hooks
const globalHooks = hookConfig.global?.hooks?.[template.event];
if (globalHooks) {
const hookList = Array.isArray(globalHooks) ? globalHooks : [globalHooks];
if (hookList.some(h => h.command === template.command)) return true;
if (hookList.some(h => {
const cmd = h.hooks?.[0]?.command || h.command || '';
return cmd.includes(template.command);
})) return true;
}
return false;
@@ -448,6 +477,12 @@ async function installHookTemplate(templateId, scope) {
return;
}
// Check if already installed
if (isHookTemplateInstalled(templateId)) {
showRefreshToast('Hook already installed', 'info');
return;
}
const hookData = {
command: template.command,
args: template.args

View File

@@ -0,0 +1,588 @@
// Memory Module View
// Three-column layout: Context Hotspots | Memory Graph | Recent Context
// ========== Memory State ==========
var memoryStats = null;
var memoryGraphData = null;
var recentContext = [];
var memoryTimeFilter = 'all'; // 'today', 'week', 'all'
var selectedNode = null;
// ========== Main Render Function ==========
async function renderMemoryView() {
var container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and search for memory view
var statsGrid = document.getElementById('statsGrid');
var searchInput = document.getElementById('searchInput');
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Show loading state
container.innerHTML = '<div class="memory-view loading">' +
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
'<p>' + t('common.loading') + '</p>' +
'</div>';
// Load data
await Promise.all([
loadMemoryStats(),
loadMemoryGraph(),
loadRecentContext()
]);
// Render three-column layout
container.innerHTML = '<div class="memory-view">' +
'<div class="memory-columns">' +
'<div class="memory-column left" id="memory-hotspots"></div>' +
'<div class="memory-column center" id="memory-graph"></div>' +
'<div class="memory-column right" id="memory-context"></div>' +
'</div>' +
'</div>';
// Render each column
renderHotspotsColumn();
renderGraphColumn();
renderContextColumn();
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
// ========== Data Loading ==========
async function loadMemoryStats() {
try {
var response = await fetch('/api/memory/stats?filter=' + memoryTimeFilter);
if (!response.ok) throw new Error('Failed to load memory stats');
var data = await response.json();
memoryStats = data.stats || { mostRead: [], mostEdited: [] };
return memoryStats;
} catch (err) {
console.error('Failed to load memory stats:', err);
memoryStats = { mostRead: [], mostEdited: [] };
return memoryStats;
}
}
async function loadMemoryGraph() {
try {
var response = await fetch('/api/memory/graph');
if (!response.ok) throw new Error('Failed to load memory graph');
var data = await response.json();
memoryGraphData = data.graph || { nodes: [], edges: [] };
return memoryGraphData;
} catch (err) {
console.error('Failed to load memory graph:', err);
memoryGraphData = { nodes: [], edges: [] };
return memoryGraphData;
}
}
async function loadRecentContext() {
try {
var response = await fetch('/api/memory/recent');
if (!response.ok) throw new Error('Failed to load recent context');
var data = await response.json();
recentContext = data.recent || [];
return recentContext;
} catch (err) {
console.error('Failed to load recent context:', err);
recentContext = [];
return [];
}
}
// ========== Left Column: Context Hotspots ==========
function renderHotspotsColumn() {
var container = document.getElementById('memory-hotspots');
if (!container) return;
var mostRead = memoryStats.mostRead || [];
var mostEdited = memoryStats.mostEdited || [];
container.innerHTML = '<div class="memory-section">' +
'<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="flame" class="w-4 h-4"></i> ' + t('memory.contextHotspots') + '</h3>' +
'</div>' +
'<div class="section-header-actions">' +
'<button class="btn-icon" onclick="refreshMemoryData()" title="' + t('common.refresh') + '">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="memory-filters">' +
'<button class="filter-btn ' + (memoryTimeFilter === 'today' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'today\')">' + t('memory.today') + '</button>' +
'<button class="filter-btn ' + (memoryTimeFilter === 'week' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'week\')">' + t('memory.week') + '</button>' +
'<button class="filter-btn ' + (memoryTimeFilter === 'all' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'all\')">' + t('memory.allTime') + '</button>' +
'</div>' +
'<div class="hotspot-lists">' +
'<div class="hotspot-list-container">' +
'<h4 class="hotspot-list-title"><i data-lucide="eye" class="w-3.5 h-3.5"></i> ' + t('memory.mostRead') + '</h4>' +
renderHotspotList(mostRead, 'read') +
'</div>' +
'<div class="hotspot-list-container">' +
'<h4 class="hotspot-list-title"><i data-lucide="pencil" class="w-3.5 h-3.5"></i> ' + t('memory.mostEdited') + '</h4>' +
renderHotspotList(mostEdited, 'edit') +
'</div>' +
'</div>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
function renderHotspotList(items, type) {
if (!items || items.length === 0) {
return '<div class="hotspot-empty">' +
'<i data-lucide="inbox" class="w-6 h-6"></i>' +
'<p>' + t('memory.noData') + '</p>' +
'</div>';
}
return '<div class="hotspot-list">' +
items.map(function(item, index) {
var heat = item.heat || item.count || 0;
var heatClass = heat > 50 ? 'high' : heat > 20 ? 'medium' : 'low';
var path = item.path || item.file || 'Unknown';
var fileName = path.split('/').pop().split('\\').pop();
return '<div class="hotspot-item" onclick="highlightNode(\'' + escapeHtml(path) + '\')">' +
'<div class="hotspot-rank">' + (index + 1) + '</div>' +
'<div class="hotspot-info">' +
'<div class="hotspot-name" title="' + escapeHtml(path) + '">' + escapeHtml(fileName) + '</div>' +
'<div class="hotspot-path">' + escapeHtml(path.substring(0, path.lastIndexOf(fileName))) + '</div>' +
'</div>' +
'<div class="hotspot-heat ' + heatClass + '">' +
'<span class="heat-badge">' + heat + '</span>' +
'<i data-lucide="' + (type === 'read' ? 'eye' : 'pencil') + '" class="w-3 h-3"></i>' +
'</div>' +
'</div>';
}).join('') +
'</div>';
}
// ========== Center Column: Memory Graph ==========
function renderGraphColumn() {
var container = document.getElementById('memory-graph');
if (!container) return;
container.innerHTML = '<div class="memory-section">' +
'<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="network" class="w-4 h-4"></i> ' + t('memory.memoryGraph') + '</h3>' +
'<span class="section-count">' + (memoryGraphData.nodes || []).length + ' ' + t('memory.nodes') + '</span>' +
'</div>' +
'<div class="section-header-actions">' +
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('memory.resetView') + '">' +
'<i data-lucide="maximize-2" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="memory-graph-container" id="memoryGraphSvg"></div>' +
'<div class="memory-graph-legend">' +
'<div class="legend-item"><span class="legend-dot file"></span> ' + t('memory.file') + '</div>' +
'<div class="legend-item"><span class="legend-dot module"></span> ' + t('memory.module') + '</div>' +
'<div class="legend-item"><span class="legend-dot component"></span> ' + t('memory.component') + '</div>' +
'</div>' +
'</div>';
// Render D3 graph
renderMemoryGraph(memoryGraphData);
if (window.lucide) lucide.createIcons();
}
function renderMemoryGraph(graphData) {
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
var container = document.getElementById('memoryGraphSvg');
if (container) {
container.innerHTML = '<div class="graph-empty-state">' +
'<i data-lucide="network" class="w-12 h-12"></i>' +
'<p>' + t('memory.noGraphData') + '</p>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
return;
}
// Check if D3 is available
if (typeof d3 === 'undefined') {
var container = document.getElementById('memoryGraphSvg');
if (container) {
container.innerHTML = '<div class="graph-error">' +
'<i data-lucide="alert-triangle" class="w-8 h-8"></i>' +
'<p>' + t('memory.d3NotLoaded') + '</p>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
return;
}
var container = document.getElementById('memoryGraphSvg');
if (!container) return;
var width = container.clientWidth || 600;
var height = container.clientHeight || 500;
// Clear existing
container.innerHTML = '';
var svg = d3.select('#memoryGraphSvg')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'memory-graph-svg');
// Create force simulation
var simulation = d3.forceSimulation(graphData.nodes)
.force('link', d3.forceLink(graphData.edges).id(function(d) { return d.id; }).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(function(d) { return (d.heat || 10) + 5; }));
// Draw edges
var link = svg.append('g')
.selectAll('line')
.data(graphData.edges)
.enter()
.append('line')
.attr('class', 'graph-edge')
.attr('stroke-width', function(d) { return Math.sqrt(d.weight || 1); });
// Draw nodes
var node = svg.append('g')
.selectAll('circle')
.data(graphData.nodes)
.enter()
.append('circle')
.attr('class', function(d) { return 'graph-node ' + (d.type || 'file'); })
.attr('r', function(d) { return (d.heat || 10); })
.attr('data-id', function(d) { return d.id; })
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('click', function(event, d) {
selectNode(d);
});
// Node labels
var label = svg.append('g')
.selectAll('text')
.data(graphData.nodes)
.enter()
.append('text')
.attr('class', 'graph-label')
.text(function(d) { return d.name || d.id; })
.attr('x', 8)
.attr('y', 3);
// Update positions on simulation tick
simulation.on('tick', function() {
link
.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
node
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
label
.attr('x', function(d) { return d.x + 8; })
.attr('y', function(d) { return d.y + 3; });
});
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
function selectNode(node) {
selectedNode = node;
// Highlight in graph
d3.selectAll('.graph-node').classed('selected', false);
d3.selectAll('.graph-node[data-id="' + node.id + '"]').classed('selected', true);
// Show node details in context column
showNodeDetails(node);
}
function highlightNode(path) {
var node = memoryGraphData.nodes.find(function(n) { return n.path === path || n.id === path; });
if (node) {
selectNode(node);
// Center graph on node if possible
if (typeof d3 !== 'undefined') {
var container = document.getElementById('memoryGraphSvg');
if (container) {
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
}
function resetGraphView() {
selectedNode = null;
d3.selectAll('.graph-node').classed('selected', false);
renderContextColumn();
}
// ========== Right Column: Recent Context ==========
function renderContextColumn() {
var container = document.getElementById('memory-context');
if (!container) return;
if (selectedNode) {
showNodeDetails(selectedNode);
return;
}
container.innerHTML = '<div class="memory-section">' +
'<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="clock" class="w-4 h-4"></i> ' + t('memory.recentContext') + '</h3>' +
'<span class="section-count">' + recentContext.length + ' ' + t('memory.activities') + '</span>' +
'</div>' +
'</div>' +
'<div class="context-search">' +
'<input type="text" id="contextSearchInput" class="context-search-input" placeholder="' + t('memory.searchContext') + '" onkeyup="filterRecentContext(this.value)">' +
'<i data-lucide="search" class="w-4 h-4 search-icon"></i>' +
'</div>' +
renderContextTimeline(recentContext) +
renderContextStats() +
'</div>';
if (window.lucide) lucide.createIcons();
}
function renderContextTimeline(prompts) {
if (!prompts || prompts.length === 0) {
return '<div class="context-empty">' +
'<i data-lucide="inbox" class="w-8 h-8"></i>' +
'<p>' + t('memory.noRecentActivity') + '</p>' +
'</div>';
}
return '<div class="context-timeline">' +
prompts.map(function(item) {
var timestamp = item.timestamp ? formatTimestamp(item.timestamp) : 'Unknown time';
var type = item.type || 'unknown';
var typeIcon = type === 'read' ? 'eye' : type === 'edit' ? 'pencil' : 'file-text';
var files = item.files || [];
return '<div class="timeline-item">' +
'<div class="timeline-icon ' + type + '">' +
'<i data-lucide="' + typeIcon + '" class="w-3.5 h-3.5"></i>' +
'</div>' +
'<div class="timeline-content">' +
'<div class="timeline-header">' +
'<span class="timeline-type">' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)) + '</span>' +
'<span class="timeline-time">' + timestamp + '</span>' +
'</div>' +
'<div class="timeline-prompt">' + escapeHtml(item.prompt || item.description || 'No description') + '</div>' +
(files.length > 0 ? '<div class="timeline-files">' +
files.slice(0, 3).map(function(f) {
return '<span class="file-tag" onclick="highlightNode(\'' + escapeHtml(f) + '\')">' +
'<i data-lucide="file" class="w-3 h-3"></i> ' + escapeHtml(f.split('/').pop().split('\\').pop()) +
'</span>';
}).join('') +
(files.length > 3 ? '<span class="file-tag more">+' + (files.length - 3) + ' more</span>' : '') +
'</div>' : '') +
'</div>' +
'</div>';
}).join('') +
'</div>';
}
function renderContextStats() {
var totalReads = recentContext.filter(function(c) { return c.type === 'read'; }).length;
var totalEdits = recentContext.filter(function(c) { return c.type === 'edit'; }).length;
var totalPrompts = recentContext.filter(function(c) { return c.type === 'prompt'; }).length;
return '<div class="context-stats">' +
'<div class="context-stat-item">' +
'<i data-lucide="eye" class="w-4 h-4"></i>' +
'<span class="stat-label">' + t('memory.reads') + '</span>' +
'<span class="stat-value">' + totalReads + '</span>' +
'</div>' +
'<div class="context-stat-item">' +
'<i data-lucide="pencil" class="w-4 h-4"></i>' +
'<span class="stat-label">' + t('memory.edits') + '</span>' +
'<span class="stat-value">' + totalEdits + '</span>' +
'</div>' +
'<div class="context-stat-item">' +
'<i data-lucide="message-square" class="w-4 h-4"></i>' +
'<span class="stat-label">' + t('memory.prompts') + '</span>' +
'<span class="stat-value">' + totalPrompts + '</span>' +
'</div>' +
'</div>';
}
function showNodeDetails(node) {
var container = document.getElementById('memory-context');
if (!container) return;
var associations = memoryGraphData.edges
.filter(function(e) { return e.source.id === node.id || e.target.id === node.id; })
.map(function(e) {
var other = e.source.id === node.id ? e.target : e.source;
return { node: other, weight: e.weight || 1 };
})
.sort(function(a, b) { return b.weight - a.weight; });
container.innerHTML = '<div class="memory-section">' +
'<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="info" class="w-4 h-4"></i> ' + t('memory.nodeDetails') + '</h3>' +
'</div>' +
'<div class="section-header-actions">' +
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('common.close') + '">' +
'<i data-lucide="x" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="node-details">' +
'<div class="node-detail-header">' +
'<div class="node-detail-icon ' + (node.type || 'file') + '">' +
'<i data-lucide="' + (node.type === 'module' ? 'package' : node.type === 'component' ? 'box' : 'file') + '" class="w-5 h-5"></i>' +
'</div>' +
'<div class="node-detail-info">' +
'<div class="node-detail-name">' + escapeHtml(node.name || node.id) + '</div>' +
'<div class="node-detail-path">' + escapeHtml(node.path || node.id) + '</div>' +
'</div>' +
'</div>' +
'<div class="node-detail-stats">' +
'<div class="detail-stat">' +
'<span class="detail-stat-label">' + t('memory.heat') + '</span>' +
'<span class="detail-stat-value">' + (node.heat || 0) + '</span>' +
'</div>' +
'<div class="detail-stat">' +
'<span class="detail-stat-label">' + t('memory.associations') + '</span>' +
'<span class="detail-stat-value">' + associations.length + '</span>' +
'</div>' +
'<div class="detail-stat">' +
'<span class="detail-stat-label">' + t('memory.type') + '</span>' +
'<span class="detail-stat-value">' + (node.type || 'file') + '</span>' +
'</div>' +
'</div>' +
(associations.length > 0 ? '<div class="node-associations">' +
'<h4 class="associations-title">' + t('memory.relatedNodes') + '</h4>' +
'<div class="associations-list">' +
associations.slice(0, 10).map(function(a) {
return '<div class="association-item" onclick="selectNode(' + JSON.stringify(a.node).replace(/"/g, '&quot;') + ')">' +
'<div class="association-node">' +
'<i data-lucide="' + (a.node.type === 'module' ? 'package' : a.node.type === 'component' ? 'box' : 'file') + '" class="w-3.5 h-3.5"></i>' +
'<span>' + escapeHtml(a.node.name || a.node.id) + '</span>' +
'</div>' +
'<div class="association-weight">' + a.weight + '</div>' +
'</div>';
}).join('') +
(associations.length > 10 ? '<div class="associations-more">+' + (associations.length - 10) + ' more</div>' : '') +
'</div>' +
'</div>' : '<div class="node-no-associations">' + t('memory.noAssociations') + '</div>') +
'</div>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
// ========== Actions ==========
async function setMemoryTimeFilter(filter) {
memoryTimeFilter = filter;
await loadMemoryStats();
renderHotspotsColumn();
}
async function refreshMemoryData() {
await Promise.all([
loadMemoryStats(),
loadMemoryGraph(),
loadRecentContext()
]);
renderHotspotsColumn();
renderGraphColumn();
renderContextColumn();
}
function filterRecentContext(query) {
var filtered = recentContext;
if (query && query.trim()) {
var q = query.toLowerCase();
filtered = recentContext.filter(function(item) {
var promptMatch = (item.prompt || '').toLowerCase().includes(q);
var filesMatch = (item.files || []).some(function(f) { return f.toLowerCase().includes(q); });
return promptMatch || filesMatch;
});
}
var container = document.getElementById('memory-context');
if (!container) return;
var timeline = container.querySelector('.context-timeline');
if (timeline) {
timeline.outerHTML = renderContextTimeline(filtered);
if (window.lucide) lucide.createIcons();
}
}
// ========== WebSocket Event Handlers ==========
function handleMemoryUpdated(payload) {
// Refresh graph and stats without full re-render
if (payload.type === 'stats') {
loadMemoryStats().then(function() { renderHotspotsColumn(); });
} else if (payload.type === 'graph') {
loadMemoryGraph().then(function() { renderGraphColumn(); });
} else if (payload.type === 'context') {
loadRecentContext().then(function() { renderContextColumn(); });
} else {
// Full refresh
refreshMemoryData();
}
// Highlight updated node if provided
if (payload.nodeId) {
highlightNode(payload.nodeId);
}
}
// ========== Utilities ==========
function formatTimestamp(timestamp) {
var date = new Date(timestamp);
var now = new Date();
var diff = now - date;
// Less than 1 minute
if (diff < 60000) {
return t('memory.justNow');
}
// Less than 1 hour
if (diff < 3600000) {
var minutes = Math.floor(diff / 60000);
return minutes + ' ' + t('memory.minutesAgo');
}
// Less than 1 day
if (diff < 86400000) {
var hours = Math.floor(diff / 3600000);
return hours + ' ' + t('memory.hoursAgo');
}
// Otherwise show date
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}

View File

@@ -0,0 +1,538 @@
// Prompt History View
// Displays prompt history and optimization insights
// ========== State ==========
var promptHistoryData = [];
var promptInsights = null;
var promptHistorySearch = '';
var promptHistoryDateFilter = null;
var promptHistoryProjectFilter = null;
var selectedPromptId = null;
// ========== Data Loading ==========
async function loadPromptHistory() {
try {
// Use native Claude history.jsonl as primary source
var response = await fetch('/api/memory/native-history?path=' + encodeURIComponent(projectPath) + '&limit=200');
if (!response.ok) throw new Error('Failed to load prompt history');
var data = await response.json();
promptHistoryData = data.prompts || [];
console.log('[PromptHistory] Loaded', promptHistoryData.length, 'prompts from native history');
return promptHistoryData;
} catch (err) {
console.error('Failed to load prompt history:', err);
promptHistoryData = [];
return [];
}
}
async function loadPromptInsights() {
try {
var response = await fetch('/api/memory/insights?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load insights');
var data = await response.json();
promptInsights = data.insights || null;
return promptInsights;
} catch (err) {
console.error('Failed to load insights:', err);
promptInsights = null;
return null;
}
}
// ========== Rendering ==========
async function renderPromptHistoryView() {
var container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and search
var statsGrid = document.getElementById('statsGrid');
var searchInput = document.getElementById('searchInput');
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Load data
await Promise.all([loadPromptHistory(), loadPromptInsights()]);
// Calculate stats
var totalPrompts = promptHistoryData.length;
var intentDistribution = calculateIntentDistribution(promptHistoryData);
var avgLength = calculateAverageLength(promptHistoryData);
var qualityDistribution = calculateQualityDistribution(promptHistoryData);
container.innerHTML = '<div class="prompt-history-view">' +
'<div class="prompt-history-header">' +
renderStatsSection(totalPrompts, intentDistribution, avgLength, qualityDistribution) +
'</div>' +
'<div class="prompt-history-content">' +
'<div class="prompt-history-left">' +
renderPromptTimeline() +
'</div>' +
'<div class="prompt-history-right">' +
renderInsightsPanel() +
'</div>' +
'</div>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
function renderStatsSection(totalPrompts, intentDist, avgLength, qualityDist) {
var topIntent = intentDist.length > 0 ? intentDist[0].intent : 'N/A';
var topIntentCount = intentDist.length > 0 ? intentDist[0].count : 0;
var intentLabel = t('prompt.intent.' + topIntent) || topIntent;
return '<div class="prompt-stats-grid">' +
'<div class="prompt-stat-card">' +
'<div class="stat-icon"><i data-lucide="message-square" class="w-5 h-5"></i></div>' +
'<div class="stat-content">' +
'<div class="stat-value">' + totalPrompts + '</div>' +
'<div class="stat-label">' + (isZh() ? '总提示词' : 'Total Prompts') + '</div>' +
'</div>' +
'</div>' +
'<div class="prompt-stat-card">' +
'<div class="stat-icon"><i data-lucide="target" class="w-5 h-5"></i></div>' +
'<div class="stat-content">' +
'<div class="stat-value">' + intentLabel + '</div>' +
'<div class="stat-label">' + (isZh() ? '主要意图' : 'Top Intent') + ' (' + topIntentCount + ')</div>' +
'</div>' +
'</div>' +
'<div class="prompt-stat-card">' +
'<div class="stat-icon"><i data-lucide="align-left" class="w-5 h-5"></i></div>' +
'<div class="stat-content">' +
'<div class="stat-value">' + avgLength + '</div>' +
'<div class="stat-label">' + (isZh() ? '平均长度' : 'Avg Length') + '</div>' +
'</div>' +
'</div>' +
'<div class="prompt-stat-card">' +
'<div class="stat-icon"><i data-lucide="bar-chart-2" class="w-5 h-5"></i></div>' +
'<div class="stat-content">' +
'<div class="stat-value">' + renderQualityBadge(qualityDist.average) + '</div>' +
'<div class="stat-label">' + t('prompt.quality') + '</div>' +
'</div>' +
'</div>' +
'</div>';
}
function renderPromptTimeline() {
var filteredPrompts = filterPrompts(promptHistoryData);
var groupedBySession = groupPromptsBySession(filteredPrompts);
var html = '<div class="prompt-timeline-header">' +
'<h3><i data-lucide="clock" class="w-4 h-4"></i> ' + t('prompt.timeline') + '</h3>' +
'<div class="prompt-timeline-filters">' +
'<div class="prompt-search-wrapper">' +
'<i data-lucide="search" class="w-4 h-4"></i>' +
'<input type="text" class="prompt-search-input" placeholder="' + t('prompt.searchPlaceholder') + '" ' +
'value="' + escapeHtml(promptHistorySearch) + '" ' +
'oninput="searchPrompts(this.value)">' +
'</div>' +
'<button class="btn-icon" onclick="refreshPromptHistory()" title="' + t('common.refresh') + '">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>';
if (filteredPrompts.length === 0) {
html += '<div class="prompt-empty-state">' +
'<i data-lucide="message-circle-off" class="w-12 h-12"></i>' +
'<h3>' + t('prompt.noPromptsFound') + '</h3>' +
'<p>' + t('prompt.noPromptsText') + '</p>' +
'</div>';
} else {
html += '<div class="prompt-timeline-list">';
for (var sessionId in groupedBySession) {
html += renderSessionGroup(sessionId, groupedBySession[sessionId]);
}
html += '</div>';
}
return html;
}
function renderSessionGroup(sessionId, prompts) {
var sessionDate = new Date(prompts[0].timestamp).toLocaleDateString();
var shortSessionId = sessionId.substring(0, 8);
var html = '<div class="prompt-session-group">' +
'<div class="prompt-session-header">' +
'<span class="prompt-session-id">' +
'<i data-lucide="layers" class="w-3.5 h-3.5"></i> ' +
shortSessionId +
'</span>' +
'<span class="prompt-session-date">' + sessionDate + '</span>' +
'<span class="prompt-session-count">' + prompts.length + ' prompt' + (prompts.length > 1 ? 's' : '') + '</span>' +
'</div>' +
'<div class="prompt-session-items">';
for (var i = 0; i < prompts.length; i++) {
html += renderPromptItem(prompts[i]);
}
html += '</div></div>';
return html;
}
function renderPromptItem(prompt) {
var isExpanded = selectedPromptId === prompt.id;
var timeAgo = getTimeAgo(new Date(prompt.timestamp));
var preview = prompt.text.substring(0, 120) + (prompt.text.length > 120 ? '...' : '');
var qualityClass = getQualityClass(prompt.quality_score);
var html = '<div class="prompt-item' + (isExpanded ? ' prompt-item-expanded' : '') + '" ' +
'onclick="togglePromptExpand(\'' + prompt.id + '\')">' +
'<div class="prompt-item-header">' +
'<span class="prompt-intent-tag">' + (prompt.intent || 'unknown') + '</span>' +
'<span class="prompt-quality-badge ' + qualityClass + '">' +
'<i data-lucide="sparkles" class="w-3 h-3"></i> ' + (prompt.quality_score || 0) +
'</span>' +
'<span class="prompt-time">' + timeAgo + '</span>' +
'</div>' +
'<div class="prompt-item-preview">' + escapeHtml(preview) + '</div>';
if (isExpanded) {
html += '<div class="prompt-item-full">' +
'<div class="prompt-full-text">' + escapeHtml(prompt.text) + '</div>' +
'<div class="prompt-item-meta">' +
'<span><i data-lucide="type" class="w-3 h-3"></i> ' + prompt.text.length + ' chars</span>' +
(prompt.project ? '<span><i data-lucide="folder" class="w-3 h-3"></i> ' + escapeHtml(prompt.project) + '</span>' : '') +
'</div>' +
'<div class="prompt-item-actions-full">' +
'<button class="btn btn-sm btn-outline" onclick="event.stopPropagation(); copyPrompt(\'' + prompt.id + '\')">' +
'<i data-lucide="copy" class="w-3.5 h-3.5"></i> ' + t('common.copy') +
'</button>' +
'</div>' +
'</div>';
}
html += '</div>';
return html;
}
function renderInsightsPanel() {
var html = '<div class="insights-panel-header">' +
'<h3><i data-lucide="lightbulb" class="w-4 h-4"></i> ' + t('prompt.insights') + '</h3>' +
'<div class="insights-actions">' +
'<select id="insightsTool" class="insights-tool-select">' +
'<option value="gemini">Gemini</option>' +
'<option value="qwen">Qwen</option>' +
'</select>' +
'<button class="btn btn-sm btn-primary" onclick="triggerCliInsightsAnalysis()" id="analyzeBtn">' +
'<i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + t('prompt.analyze') +
'</button>' +
'</div>' +
'</div>';
// Show loading state
if (window.insightsAnalyzing) {
html += '<div class="insights-loading">' +
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i></div>' +
'<p>' + t('prompt.loadingInsights') + '</p>' +
'</div>';
return html;
}
if (!promptInsights || !promptInsights.patterns || promptInsights.patterns.length === 0) {
html += '<div class="insights-empty-state">' +
'<i data-lucide="brain" class="w-10 h-10"></i>' +
'<p>' + t('prompt.noInsights') + '</p>' +
'<p class="insights-hint">' + t('prompt.noInsightsText') + '</p>' +
'</div>';
} else {
html += '<div class="insights-list">';
// Render detected patterns
if (promptInsights.patterns && promptInsights.patterns.length > 0) {
html += '<div class="insights-section">' +
'<h4><i data-lucide="alert-circle" class="w-4 h-4"></i> Detected Patterns</h4>';
for (var i = 0; i < promptInsights.patterns.length; i++) {
html += renderPatternCard(promptInsights.patterns[i]);
}
html += '</div>';
}
// Render suggestions
if (promptInsights.suggestions && promptInsights.suggestions.length > 0) {
html += '<div class="insights-section">' +
'<h4><i data-lucide="zap" class="w-4 h-4"></i> Optimization Suggestions</h4>';
for (var j = 0; j < promptInsights.suggestions.length; j++) {
html += renderSuggestionCard(promptInsights.suggestions[j]);
}
html += '</div>';
}
// Render similar successful prompts
if (promptInsights.similar_prompts && promptInsights.similar_prompts.length > 0) {
html += '<div class="insights-section">' +
'<h4><i data-lucide="stars" class="w-4 h-4"></i> Similar Successful Prompts</h4>';
for (var k = 0; k < promptInsights.similar_prompts.length; k++) {
html += renderSimilarPromptCard(promptInsights.similar_prompts[k]);
}
html += '</div>';
}
html += '</div>';
}
return html;
}
function renderPatternCard(pattern) {
var iconMap = {
'vague': 'help-circle',
'correction': 'rotate-ccw',
'repetitive': 'repeat',
'incomplete': 'alert-triangle'
};
var icon = iconMap[pattern.type] || 'info';
var severityClass = pattern.severity || 'medium';
return '<div class="pattern-card pattern-' + severityClass + '">' +
'<div class="pattern-header">' +
'<i data-lucide="' + icon + '" class="w-4 h-4"></i>' +
'<span class="pattern-type">' + (pattern.type || 'Unknown') + '</span>' +
'<span class="pattern-count">' + (pattern.occurrences || 0) + 'x</span>' +
'</div>' +
'<div class="pattern-description">' + escapeHtml(pattern.description || '') + '</div>' +
'<div class="pattern-suggestion">' +
'<i data-lucide="arrow-right" class="w-3 h-3"></i> ' +
escapeHtml(pattern.suggestion || '') +
'</div>' +
'</div>';
}
function renderSuggestionCard(suggestion) {
return '<div class="suggestion-card">' +
'<div class="suggestion-title">' +
'<i data-lucide="sparkle" class="w-3.5 h-3.5"></i> ' +
escapeHtml(suggestion.title || 'Suggestion') +
'</div>' +
'<div class="suggestion-description">' + escapeHtml(suggestion.description || '') + '</div>' +
(suggestion.example ?
'<div class="suggestion-example">' +
'<div class="suggestion-example-label">Example:</div>' +
'<code>' + escapeHtml(suggestion.example) + '</code>' +
'</div>' : '') +
'</div>';
}
function renderSimilarPromptCard(prompt) {
var similarity = Math.round((prompt.similarity || 0) * 100);
var preview = prompt.text.substring(0, 80) + (prompt.text.length > 80 ? '...' : '');
return '<div class="similar-prompt-card" onclick="showPromptDetail(\'' + prompt.id + '\')">' +
'<div class="similar-prompt-header">' +
'<span class="similar-prompt-similarity">' + similarity + '% match</span>' +
'<span class="similar-prompt-intent">' + (prompt.intent || 'unknown') + '</span>' +
'</div>' +
'<div class="similar-prompt-preview">' + escapeHtml(preview) + '</div>' +
'<div class="similar-prompt-meta">' +
'<span class="similar-prompt-quality">' +
'<i data-lucide="star" class="w-3 h-3"></i> ' + (prompt.quality_score || 0) +
'</span>' +
'</div>' +
'</div>';
}
function renderProjectOptions() {
var projects = getUniqueProjects(promptHistoryData);
var html = '';
for (var i = 0; i < projects.length; i++) {
var selected = projects[i] === promptHistoryProjectFilter ? 'selected' : '';
html += '<option value="' + escapeHtml(projects[i]) + '" ' + selected + '>' +
escapeHtml(projects[i]) + '</option>';
}
return html;
}
function renderQualityBadge(score) {
if (score >= 80) return '<span class="quality-badge high">' + score + '</span>';
if (score >= 60) return '<span class="quality-badge medium">' + score + '</span>';
return '<span class="quality-badge low">' + score + '</span>';
}
// ========== Helper Functions ==========
function calculateIntentDistribution(prompts) {
var distribution = {};
for (var i = 0; i < prompts.length; i++) {
var intent = prompts[i].intent || 'unknown';
distribution[intent] = (distribution[intent] || 0) + 1;
}
var result = [];
for (var key in distribution) {
result.push({ intent: key, count: distribution[key] });
}
result.sort(function(a, b) { return b.count - a.count; });
return result;
}
function calculateAverageLength(prompts) {
if (prompts.length === 0) return 0;
var total = 0;
for (var i = 0; i < prompts.length; i++) {
total += (prompts[i].text || '').length;
}
return Math.round(total / prompts.length);
}
function calculateQualityDistribution(prompts) {
if (prompts.length === 0) return { average: 0, distribution: {} };
var total = 0;
var distribution = { high: 0, medium: 0, low: 0 };
for (var i = 0; i < prompts.length; i++) {
var score = prompts[i].quality_score || 0;
total += score;
if (score >= 80) distribution.high++;
else if (score >= 60) distribution.medium++;
else distribution.low++;
}
return {
average: Math.round(total / prompts.length),
distribution: distribution
};
}
function getQualityClass(score) {
if (score >= 80) return 'quality-high';
if (score >= 60) return 'quality-medium';
return 'quality-low';
}
function filterPrompts(prompts) {
return prompts.filter(function(prompt) {
var matchesSearch = !promptHistorySearch ||
prompt.text.toLowerCase().includes(promptHistorySearch.toLowerCase());
var matchesProject = !promptHistoryProjectFilter ||
prompt.project === promptHistoryProjectFilter;
var matchesDate = !promptHistoryDateFilter ||
isSameDay(new Date(prompt.timestamp), promptHistoryDateFilter);
return matchesSearch && matchesProject && matchesDate;
});
}
function groupPromptsBySession(prompts) {
var grouped = {};
for (var i = 0; i < prompts.length; i++) {
var sessionId = prompts[i].session_id || 'unknown';
if (!grouped[sessionId]) grouped[sessionId] = [];
grouped[sessionId].push(prompts[i]);
}
return grouped;
}
function getUniqueProjects(prompts) {
var projects = {};
for (var i = 0; i < prompts.length; i++) {
if (prompts[i].project) projects[prompts[i].project] = true;
}
return Object.keys(projects).sort();
}
function isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
// ========== Actions ==========
function searchPrompts(query) {
promptHistorySearch = query;
renderPromptHistoryView();
}
function filterByProject(project) {
promptHistoryProjectFilter = project || null;
renderPromptHistoryView();
}
function togglePromptExpand(promptId) {
if (selectedPromptId === promptId) {
selectedPromptId = null;
} else {
selectedPromptId = promptId;
}
renderPromptHistoryView();
}
function copyPrompt(promptId) {
var prompt = promptHistoryData.find(function(p) { return p.id === promptId; });
if (!prompt) return;
if (navigator.clipboard) {
navigator.clipboard.writeText(prompt.text).then(function() {
showRefreshToast('Prompt copied to clipboard', 'success');
}).catch(function() {
showRefreshToast('Failed to copy prompt', 'error');
});
}
}
function showPromptDetail(promptId) {
togglePromptExpand(promptId);
}
async function refreshPromptHistory() {
await Promise.all([loadPromptHistory(), loadPromptInsights()]);
renderPromptHistoryView();
showRefreshToast('Prompt history refreshed', 'success');
}
// ========== CLI-based Insights Analysis ==========
async function triggerCliInsightsAnalysis() {
if (promptHistoryData.length === 0) {
showRefreshToast(t('prompt.noPromptsFound'), 'error');
return;
}
var toolSelect = document.getElementById('insightsTool');
var tool = toolSelect ? toolSelect.value : 'gemini';
var analyzeBtn = document.getElementById('analyzeBtn');
// Show loading state
window.insightsAnalyzing = true;
if (analyzeBtn) {
analyzeBtn.disabled = true;
analyzeBtn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i> ' + t('prompt.analyzing');
}
renderPromptHistoryView();
try {
var response = await fetch('/api/memory/insights/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: projectPath,
tool: tool,
lang: getLang(), // Send current language preference
prompts: promptHistoryData.slice(0, 50) // Send top 50 prompts for analysis
})
});
var data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// Update insights with CLI analysis results
if (data.insights) {
promptInsights = data.insights;
console.log('[PromptHistory] Insights parsed:', promptInsights);
}
showRefreshToast(t('toast.completed') + ' (' + tool + ')', 'success');
} catch (err) {
console.error('CLI insights analysis failed:', err);
showRefreshToast(t('prompt.insightsError') + ': ' + err.message, 'error');
} finally {
window.insightsAnalyzing = false;
renderPromptHistoryView();
}
}