mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
Add comprehensive tests for vector/semantic search functionality
- Implement full coverage tests for Embedder model loading and embedding generation - Add CRUD operations and caching tests for VectorStore - Include cosine similarity computation tests - Validate semantic search accuracy and relevance through various queries - Establish performance benchmarks for embedding and search operations - Ensure edge cases and error handling are covered - Test thread safety and concurrent access scenarios - Verify availability of semantic search dependencies
This commit is contained in:
@@ -330,6 +330,9 @@ async function showExecutionDetail(executionId, sourceDir) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Check if native session is available
|
||||
const hasNativeSession = conversation.hasNativeSession || conversation.nativeSessionId;
|
||||
|
||||
const modalContent = `
|
||||
<div class="cli-detail-header">
|
||||
<div class="cli-detail-info">
|
||||
@@ -344,6 +347,13 @@ async function showExecutionDetail(executionId, sourceDir) {
|
||||
<span><i data-lucide="calendar" class="w-3 h-3"></i> ${new Date(createdAt).toLocaleString()}</span>
|
||||
<span><i data-lucide="hash" class="w-3 h-3"></i> ${executionId.split('-')[0]}</span>
|
||||
</div>
|
||||
${hasNativeSession ? `
|
||||
<div class="cli-detail-native-action">
|
||||
<button class="btn btn-sm btn-primary" onclick="showNativeSessionDetail('${executionId}')">
|
||||
<i data-lucide="eye" class="w-3.5 h-3.5"></i> View Full Process Conversation
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${turnCount > 1 ? `
|
||||
<div class="cli-view-toggle">
|
||||
@@ -665,26 +675,52 @@ async function showNativeSessionDetail(executionId) {
|
||||
</span>`
|
||||
: '';
|
||||
|
||||
// Thoughts section
|
||||
// Thoughts section (collapsible)
|
||||
const thoughtsHtml = turn.thoughts && turn.thoughts.length > 0
|
||||
? `<div class="native-thoughts-section">
|
||||
<h5><i data-lucide="brain" class="w-3 h-3"></i> Thoughts</h5>
|
||||
<ul class="native-thoughts-list">
|
||||
${turn.thoughts.map(t => `<li>${escapeHtml(t)}</li>`).join('')}
|
||||
</ul>
|
||||
<details class="turn-thinking-details">
|
||||
<summary class="turn-thinking-summary">
|
||||
<i data-lucide="brain" class="w-3 h-3"></i>
|
||||
💭 Thinking Process (${turn.thoughts.length} thoughts)
|
||||
</summary>
|
||||
<div class="turn-thinking-content">
|
||||
<ul class="native-thoughts-list">
|
||||
${turn.thoughts.map(t => `<li>${escapeHtml(t)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// Tool calls section
|
||||
// Tool calls section (collapsible for each call)
|
||||
const toolCallsHtml = turn.toolCalls && turn.toolCalls.length > 0
|
||||
? `<div class="native-tools-section">
|
||||
<h5><i data-lucide="wrench" class="w-3 h-3"></i> Tool Calls (${turn.toolCalls.length})</h5>
|
||||
<div class="turn-tool-calls-header">
|
||||
<i data-lucide="wrench" class="w-3 h-3"></i>
|
||||
<strong>Tool Calls (${turn.toolCalls.length})</strong>
|
||||
</div>
|
||||
<div class="native-tools-list">
|
||||
${turn.toolCalls.map(tc => `
|
||||
<div class="native-tool-call">
|
||||
<span class="native-tool-name">${escapeHtml(tc.name)}</span>
|
||||
${tc.output ? `<pre class="native-tool-output">${escapeHtml(tc.output.substring(0, 500))}${tc.output.length > 500 ? '...' : ''}</pre>` : ''}
|
||||
</div>
|
||||
${turn.toolCalls.map((tc, tcIdx) => `
|
||||
<details class="turn-tool-call-details" ${tcIdx === 0 ? 'open' : ''}>
|
||||
<summary class="turn-tool-call-summary">
|
||||
<span class="native-tool-name">🔧 ${escapeHtml(tc.name)}</span>
|
||||
${tc.output ? `<span class="native-tool-size">(${tc.output.length} chars)</span>` : ''}
|
||||
</summary>
|
||||
<div class="turn-tool-call-content">
|
||||
${tc.input ? `
|
||||
<div class="turn-tool-input">
|
||||
<strong>Input:</strong>
|
||||
<pre>${escapeHtml(JSON.stringify(tc.input, null, 2))}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
${tc.output ? `
|
||||
<div class="turn-tool-output">
|
||||
<strong>Output:</strong>
|
||||
<pre class="native-tool-output">${escapeHtml(tc.output)}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</details>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>`
|
||||
@@ -758,7 +794,7 @@ async function showNativeSessionDetail(executionId) {
|
||||
// Store for export
|
||||
window._currentNativeSession = nativeSession;
|
||||
|
||||
showModal('Native Session Detail', modalContent, 'modal-lg');
|
||||
showModal('Native Session Detail', modalContent, { size: 'lg' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,15 @@ let smartContextMaxFiles = parseInt(localStorage.getItem('ccw-smart-context-max-
|
||||
// Native Resume settings
|
||||
let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false'; // default true
|
||||
|
||||
// LLM Enhancement settings for Semantic Search
|
||||
let llmEnhancementSettings = {
|
||||
enabled: localStorage.getItem('ccw-llm-enhancement-enabled') === 'true',
|
||||
tool: localStorage.getItem('ccw-llm-enhancement-tool') || 'gemini',
|
||||
fallbackTool: localStorage.getItem('ccw-llm-enhancement-fallback') || 'qwen',
|
||||
batchSize: parseInt(localStorage.getItem('ccw-llm-enhancement-batch-size') || '5', 10),
|
||||
timeoutMs: parseInt(localStorage.getItem('ccw-llm-enhancement-timeout') || '300000', 10)
|
||||
};
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliStatus() {
|
||||
// Load CLI status on init
|
||||
@@ -182,12 +191,17 @@ function renderCliStatus() {
|
||||
`;
|
||||
|
||||
// Semantic Search card (only show if CodexLens is installed)
|
||||
const llmStatusBadge = llmEnhancementSettings.enabled
|
||||
? `<span class="badge px-1.5 py-0.5 text-xs rounded bg-success/20 text-success">LLM</span>`
|
||||
: '';
|
||||
const semanticHtml = codexLensStatus.ready ? `
|
||||
<div class="cli-tool-card tool-semantic ${semanticStatus.available ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-card tool-semantic clickable ${semanticStatus.available ? 'available' : 'unavailable'}"
|
||||
onclick="openSemanticSettingsModal()">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${semanticStatus.available ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">Semantic Search</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded ${semanticStatus.available ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'}">AI</span>
|
||||
${llmStatusBadge}
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'}
|
||||
@@ -200,17 +214,27 @@ function renderCliStatus() {
|
||||
</div>
|
||||
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
|
||||
${!semanticStatus.available ? `
|
||||
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="openSemanticInstallWizard()">
|
||||
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="event.stopPropagation(); openSemanticInstallWizard()">
|
||||
<i data-lucide="brain" class="w-3 h-3"></i> Install AI Model
|
||||
</button>
|
||||
<div class="flex items-center justify-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
<span>~500MB download</span>
|
||||
<div class="flex items-center justify-between w-full mt-1">
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
<span>~500MB</span>
|
||||
</div>
|
||||
<button class="btn-sm btn-outline flex items-center gap-1" onclick="event.stopPropagation(); openSemanticSettingsModal()">
|
||||
<i data-lucide="settings" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="cpu" class="w-3 h-3"></i>
|
||||
<span>bge-small-en-v1.5</span>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="cpu" class="w-3 h-3"></i>
|
||||
<span>bge-small-en-v1.5</span>
|
||||
</div>
|
||||
<button class="btn-sm btn-outline flex items-center gap-1" onclick="event.stopPropagation(); openSemanticSettingsModal()">
|
||||
<i data-lucide="settings" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@@ -550,3 +574,535 @@ async function startSemanticInstall() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Semantic Search Settings Modal ==========
|
||||
function openSemanticSettingsModal() {
|
||||
const availableTools = Object.entries(cliToolStatus)
|
||||
.filter(function(entry) { return entry[1].available; })
|
||||
.map(function(entry) { return entry[0]; });
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'semanticSettingsModal';
|
||||
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
modal.onclick = function(e) { if (e.target === modal) closeSemanticSettingsModal(); };
|
||||
|
||||
const toolOptions = availableTools.map(function(tool) {
|
||||
return '<option value="' + tool + '"' + (llmEnhancementSettings.tool === tool ? ' selected' : '') + '>' +
|
||||
tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
|
||||
}).join('');
|
||||
|
||||
const fallbackOptions = '<option value="">None</option>' + availableTools.map(function(tool) {
|
||||
return '<option value="' + tool + '"' + (llmEnhancementSettings.fallbackTool === tool ? ' selected' : '') + '>' +
|
||||
tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
|
||||
}).join('');
|
||||
|
||||
const disabled = !llmEnhancementSettings.enabled ? 'disabled' : '';
|
||||
const opacityClass = !llmEnhancementSettings.enabled ? 'opacity-50' : '';
|
||||
|
||||
modal.innerHTML =
|
||||
'<div class="bg-card rounded-lg shadow-xl w-full max-w-lg mx-4 overflow-hidden" onclick="event.stopPropagation()">' +
|
||||
'<div class="p-6">' +
|
||||
'<div class="flex items-center gap-3 mb-4">' +
|
||||
'<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">' +
|
||||
'<i data-lucide="sparkles" class="w-5 h-5 text-primary"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h3 class="text-lg font-semibold">Semantic Search Settings</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">Configure LLM enhancement for semantic indexing</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-4">' +
|
||||
'<div class="flex items-center justify-between p-4 bg-muted/50 rounded-lg">' +
|
||||
'<div>' +
|
||||
'<h4 class="font-medium flex items-center gap-2">' +
|
||||
'<i data-lucide="brain" class="w-4 h-4"></i>LLM Enhancement</h4>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">Use LLM to generate code summaries for better semantic search</p>' +
|
||||
'</div>' +
|
||||
'<label class="cli-toggle">' +
|
||||
'<input type="checkbox" id="llmEnhancementToggle" ' + (llmEnhancementSettings.enabled ? 'checked' : '') +
|
||||
' onchange="toggleLlmEnhancement(this.checked)">' +
|
||||
'<span class="cli-toggle-slider"></span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="p-4 bg-muted/30 rounded-lg space-y-4 ' + opacityClass + '" id="llmSettingsSection">' +
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="cpu" class="w-3 h-3 inline mr-1"></i>Primary LLM Tool</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmToolSelect" onchange="updateLlmTool(this.value)" ' + disabled + '>' + toolOptions + '</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1"></i>Fallback Tool</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmFallbackSelect" onchange="updateLlmFallback(this.value)" ' + disabled + '>' + fallbackOptions + '</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="layers" class="w-3 h-3 inline mr-1"></i>Batch Size</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmBatchSelect" onchange="updateLlmBatchSize(this.value)" ' + disabled + '>' +
|
||||
'<option value="1"' + (llmEnhancementSettings.batchSize === 1 ? ' selected' : '') + '>1 file</option>' +
|
||||
'<option value="3"' + (llmEnhancementSettings.batchSize === 3 ? ' selected' : '') + '>3 files</option>' +
|
||||
'<option value="5"' + (llmEnhancementSettings.batchSize === 5 ? ' selected' : '') + '>5 files</option>' +
|
||||
'<option value="10"' + (llmEnhancementSettings.batchSize === 10 ? ' selected' : '') + '>10 files</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>Timeout</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmTimeoutSelect" onchange="updateLlmTimeout(this.value)" ' + disabled + '>' +
|
||||
'<option value="60000"' + (llmEnhancementSettings.timeoutMs === 60000 ? ' selected' : '') + '>1 min</option>' +
|
||||
'<option value="180000"' + (llmEnhancementSettings.timeoutMs === 180000 ? ' selected' : '') + '>3 min</option>' +
|
||||
'<option value="300000"' + (llmEnhancementSettings.timeoutMs === 300000 ? ' selected' : '') + '>5 min</option>' +
|
||||
'<option value="600000"' + (llmEnhancementSettings.timeoutMs === 600000 ? ' selected' : '') + '>10 min</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="bg-primary/5 border border-primary/20 rounded-lg p-3">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>' +
|
||||
'<div class="text-sm text-muted-foreground">' +
|
||||
'<p>LLM enhancement generates code summaries and keywords for each file, improving semantic search accuracy.</p>' +
|
||||
'<p class="mt-1">Run <code class="bg-muted px-1 rounded">codex-lens enhance</code> after enabling to process existing files.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="flex gap-2 pt-2">' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="runEnhanceCommand()" ' + disabled + '>' +
|
||||
'<i data-lucide="zap" class="w-3 h-3"></i>Run Enhance Now</button>' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="viewEnhanceStatus()">' +
|
||||
'<i data-lucide="bar-chart-2" class="w-3 h-3"></i>View Status</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
|
||||
'<button class="btn-outline px-4 py-2" onclick="closeSemanticSettingsModal()">Close</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
var handleEscape = function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSemanticSettingsModal();
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function closeSemanticSettingsModal() {
|
||||
var modal = document.getElementById('semanticSettingsModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function toggleLlmEnhancement(enabled) {
|
||||
llmEnhancementSettings.enabled = enabled;
|
||||
localStorage.setItem('ccw-llm-enhancement-enabled', enabled.toString());
|
||||
|
||||
var settingsSection = document.getElementById('llmSettingsSection');
|
||||
if (settingsSection) {
|
||||
settingsSection.classList.toggle('opacity-50', !enabled);
|
||||
settingsSection.querySelectorAll('select').forEach(function(el) { el.disabled = !enabled; });
|
||||
}
|
||||
|
||||
renderCliStatus();
|
||||
showRefreshToast('LLM Enhancement ' + (enabled ? 'enabled' : 'disabled'), 'success');
|
||||
}
|
||||
|
||||
function updateLlmTool(tool) {
|
||||
llmEnhancementSettings.tool = tool;
|
||||
localStorage.setItem('ccw-llm-enhancement-tool', tool);
|
||||
showRefreshToast('Primary LLM tool set to ' + tool, 'success');
|
||||
}
|
||||
|
||||
function updateLlmFallback(tool) {
|
||||
llmEnhancementSettings.fallbackTool = tool;
|
||||
localStorage.setItem('ccw-llm-enhancement-fallback', tool);
|
||||
showRefreshToast('Fallback tool set to ' + (tool || 'none'), 'success');
|
||||
}
|
||||
|
||||
function updateLlmBatchSize(size) {
|
||||
llmEnhancementSettings.batchSize = parseInt(size, 10);
|
||||
localStorage.setItem('ccw-llm-enhancement-batch-size', size);
|
||||
showRefreshToast('Batch size set to ' + size + ' files', 'success');
|
||||
}
|
||||
|
||||
function updateLlmTimeout(ms) {
|
||||
llmEnhancementSettings.timeoutMs = parseInt(ms, 10);
|
||||
localStorage.setItem('ccw-llm-enhancement-timeout', ms);
|
||||
var mins = parseInt(ms, 10) / 60000;
|
||||
showRefreshToast('Timeout set to ' + mins + ' minute' + (mins > 1 ? 's' : ''), 'success');
|
||||
}
|
||||
|
||||
async function runEnhanceCommand() {
|
||||
if (!llmEnhancementSettings.enabled) {
|
||||
showRefreshToast('Please enable LLM Enhancement first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showRefreshToast('Starting LLM enhancement...', 'info');
|
||||
closeSemanticSettingsModal();
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/enhance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: projectPath,
|
||||
tool: llmEnhancementSettings.tool,
|
||||
batchSize: llmEnhancementSettings.batchSize,
|
||||
timeoutMs: llmEnhancementSettings.timeoutMs
|
||||
})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
if (result.success) {
|
||||
var enhanced = result.result?.enhanced || 0;
|
||||
showRefreshToast('Enhanced ' + enhanced + ' files with LLM', 'success');
|
||||
} else {
|
||||
showRefreshToast('Enhance failed: ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Enhance error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function viewEnhanceStatus() {
|
||||
openSemanticMetadataViewer();
|
||||
}
|
||||
|
||||
// ========== Semantic Metadata Viewer ==========
|
||||
var semanticMetadataCache = {
|
||||
entries: [],
|
||||
total: 0,
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
loading: false
|
||||
};
|
||||
|
||||
async function openSemanticMetadataViewer() {
|
||||
closeSemanticSettingsModal();
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'semanticMetadataModal';
|
||||
modal.className = 'generic-modal-overlay';
|
||||
modal.onclick = function(e) { if (e.target === modal) closeSemanticMetadataViewer(); };
|
||||
|
||||
modal.innerHTML =
|
||||
'<div class="generic-modal large" onclick="event.stopPropagation()">' +
|
||||
'<div class="generic-modal-header">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<i data-lucide="database" class="w-5 h-5 text-primary"></i>' +
|
||||
'<h3 class="generic-modal-title">Semantic Metadata Browser</h3>' +
|
||||
'<span id="semanticMetadataCount" class="badge bg-muted text-muted-foreground px-2 py-0.5 text-xs rounded">Loading...</span>' +
|
||||
'</div>' +
|
||||
'<button class="generic-modal-close" onclick="closeSemanticMetadataViewer()">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="generic-modal-body p-0">' +
|
||||
'<div class="semantic-viewer-toolbar">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<select id="semanticToolFilter" class="cli-setting-select" onchange="filterSemanticByTool(this.value)">' +
|
||||
'<option value="">All Tools</option>' +
|
||||
'<option value="gemini">Gemini</option>' +
|
||||
'<option value="qwen">Qwen</option>' +
|
||||
'</select>' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1" onclick="refreshSemanticMetadata()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
|
||||
'<span id="semanticPaginationInfo">-</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="semanticMetadataTableContainer" class="semantic-table-container">' +
|
||||
'<div class="semantic-loading">' +
|
||||
'<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
||||
'<span>Loading metadata...</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="semantic-viewer-footer">' +
|
||||
'<button id="semanticPrevBtn" class="btn-sm btn-outline" onclick="semanticPrevPage()" disabled>' +
|
||||
'<i data-lucide="chevron-left" class="w-4 h-4"></i> Previous' +
|
||||
'</button>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<span class="text-sm text-muted-foreground">Page</span>' +
|
||||
'<select id="semanticPageSelect" class="cli-setting-select" onchange="semanticGoToPage(this.value)">' +
|
||||
'<option value="0">1</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<button id="semanticNextBtn" class="btn-sm btn-outline" onclick="semanticNextPage()" disabled>' +
|
||||
'Next <i data-lucide="chevron-right" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
modal.classList.add('active');
|
||||
});
|
||||
|
||||
var handleEscape = function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSemanticMetadataViewer();
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
await loadSemanticMetadata();
|
||||
}
|
||||
|
||||
function closeSemanticMetadataViewer() {
|
||||
var modal = document.getElementById('semanticMetadataModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(function() { modal.remove(); }, 200);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSemanticMetadata(offset, toolFilter) {
|
||||
offset = typeof offset === 'number' ? offset : semanticMetadataCache.offset;
|
||||
toolFilter = toolFilter !== undefined ? toolFilter : (document.getElementById('semanticToolFilter')?.value || '');
|
||||
|
||||
semanticMetadataCache.loading = true;
|
||||
|
||||
var container = document.getElementById('semanticMetadataTableContainer');
|
||||
if (container) {
|
||||
container.innerHTML =
|
||||
'<div class="semantic-loading">' +
|
||||
'<div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
||||
'<span>Loading metadata...</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
try {
|
||||
var url = '/api/codexlens/semantic/metadata?offset=' + offset + '&limit=' + semanticMetadataCache.limit;
|
||||
if (toolFilter) {
|
||||
url += '&tool=' + encodeURIComponent(toolFilter);
|
||||
}
|
||||
|
||||
var response = await fetch(url);
|
||||
var data = await response.json();
|
||||
|
||||
if (data.success && data.result) {
|
||||
semanticMetadataCache.entries = data.result.entries || [];
|
||||
semanticMetadataCache.total = data.result.total || 0;
|
||||
semanticMetadataCache.offset = offset;
|
||||
|
||||
renderSemanticMetadataTable();
|
||||
updateSemanticPagination();
|
||||
} else {
|
||||
container.innerHTML =
|
||||
'<div class="semantic-empty">' +
|
||||
'<i data-lucide="alert-circle" class="w-8 h-8 text-muted-foreground"></i>' +
|
||||
'<p>Error loading metadata: ' + (data.error || 'Unknown error') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} catch (err) {
|
||||
container.innerHTML =
|
||||
'<div class="semantic-empty">' +
|
||||
'<i data-lucide="alert-circle" class="w-8 h-8 text-muted-foreground"></i>' +
|
||||
'<p>Error: ' + err.message + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
semanticMetadataCache.loading = false;
|
||||
}
|
||||
|
||||
function escapeHtmlSemantic(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function renderSemanticMetadataTable() {
|
||||
var container = document.getElementById('semanticMetadataTableContainer');
|
||||
if (!container) return;
|
||||
|
||||
var entries = semanticMetadataCache.entries;
|
||||
|
||||
if (!entries.length) {
|
||||
container.innerHTML =
|
||||
'<div class="semantic-empty">' +
|
||||
'<i data-lucide="database" class="w-12 h-12 text-muted-foreground mb-3"></i>' +
|
||||
'<p class="text-lg font-medium">No semantic metadata found</p>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">Run \'codex-lens enhance\' to generate metadata for indexed files.</p>' +
|
||||
'<button class="btn-sm btn-primary mt-4" onclick="closeSemanticMetadataViewer(); runEnhanceCommand();">' +
|
||||
'<i data-lucide="zap" class="w-3 h-3 mr-1"></i> Run Enhance' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = entries.map(function(entry, idx) {
|
||||
var keywordsHtml = (entry.keywords || []).slice(0, 4).map(function(k) {
|
||||
return '<span class="semantic-keyword">' + escapeHtmlSemantic(k) + '</span>';
|
||||
}).join('');
|
||||
if ((entry.keywords || []).length > 4) {
|
||||
keywordsHtml += '<span class="semantic-keyword-more">+' + (entry.keywords.length - 4) + '</span>';
|
||||
}
|
||||
|
||||
var date = entry.generated_at ? new Date(entry.generated_at * 1000).toLocaleDateString() : '-';
|
||||
|
||||
return (
|
||||
'<tr class="semantic-row" onclick="toggleSemanticDetail(' + idx + ')">' +
|
||||
'<td class="semantic-cell-file">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="file-code" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="font-medium">' + escapeHtmlSemantic(entry.file_name || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground truncate" title="' + escapeHtmlSemantic(entry.full_path || '') + '">' +
|
||||
escapeHtmlSemantic(entry.full_path || '-') +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td class="semantic-cell-lang">' + escapeHtmlSemantic(entry.language || '-') + '</td>' +
|
||||
'<td class="semantic-cell-purpose">' + escapeHtmlSemantic((entry.purpose || '-').substring(0, 50)) +
|
||||
((entry.purpose || '').length > 50 ? '...' : '') + '</td>' +
|
||||
'<td class="semantic-cell-keywords">' + (keywordsHtml || '-') + '</td>' +
|
||||
'<td class="semantic-cell-tool">' +
|
||||
'<span class="tool-badge tool-' + (entry.llm_tool || 'unknown') + '">' +
|
||||
escapeHtmlSemantic(entry.llm_tool || '-') +
|
||||
'</span>' +
|
||||
'</td>' +
|
||||
'<td class="semantic-cell-date">' + date + '</td>' +
|
||||
'</tr>' +
|
||||
'<tr id="semanticDetail' + idx + '" class="semantic-detail-row hidden">' +
|
||||
'<td colspan="6">' +
|
||||
'<div class="semantic-detail-content">' +
|
||||
'<div class="semantic-detail-section">' +
|
||||
'<h4><i data-lucide="file-text" class="w-3 h-3"></i> Summary</h4>' +
|
||||
'<p>' + escapeHtmlSemantic(entry.summary || 'No summary available') + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="semantic-detail-section">' +
|
||||
'<h4><i data-lucide="tag" class="w-3 h-3"></i> All Keywords</h4>' +
|
||||
'<div class="semantic-keywords-full">' +
|
||||
(entry.keywords || []).map(function(k) {
|
||||
return '<span class="semantic-keyword">' + escapeHtmlSemantic(k) + '</span>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="semantic-detail-meta">' +
|
||||
'<span><i data-lucide="hash" class="w-3 h-3"></i> ' + (entry.line_count || 0) + ' lines</span>' +
|
||||
'<span><i data-lucide="cpu" class="w-3 h-3"></i> ' + escapeHtmlSemantic(entry.llm_tool || 'Unknown') + '</span>' +
|
||||
'<span><i data-lucide="calendar" class="w-3 h-3"></i> ' + date + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
container.innerHTML =
|
||||
'<table class="semantic-table">' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th>File</th>' +
|
||||
'<th>Language</th>' +
|
||||
'<th>Purpose</th>' +
|
||||
'<th>Keywords</th>' +
|
||||
'<th>Tool</th>' +
|
||||
'<th>Date</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function toggleSemanticDetail(idx) {
|
||||
var detailRow = document.getElementById('semanticDetail' + idx);
|
||||
if (detailRow) {
|
||||
detailRow.classList.toggle('hidden');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSemanticPagination() {
|
||||
var total = semanticMetadataCache.total;
|
||||
var offset = semanticMetadataCache.offset;
|
||||
var limit = semanticMetadataCache.limit;
|
||||
var entries = semanticMetadataCache.entries;
|
||||
|
||||
var countBadge = document.getElementById('semanticMetadataCount');
|
||||
if (countBadge) {
|
||||
countBadge.textContent = total + ' entries';
|
||||
}
|
||||
|
||||
var paginationInfo = document.getElementById('semanticPaginationInfo');
|
||||
if (paginationInfo) {
|
||||
if (total > 0) {
|
||||
paginationInfo.textContent = (offset + 1) + '-' + (offset + entries.length) + ' of ' + total;
|
||||
} else {
|
||||
paginationInfo.textContent = 'No entries';
|
||||
}
|
||||
}
|
||||
|
||||
var pageSelect = document.getElementById('semanticPageSelect');
|
||||
if (pageSelect) {
|
||||
var totalPages = Math.ceil(total / limit) || 1;
|
||||
var currentPage = Math.floor(offset / limit);
|
||||
|
||||
pageSelect.innerHTML = '';
|
||||
for (var i = 0; i < totalPages; i++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
opt.textContent = i + 1;
|
||||
if (i === currentPage) opt.selected = true;
|
||||
pageSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
var prevBtn = document.getElementById('semanticPrevBtn');
|
||||
var nextBtn = document.getElementById('semanticNextBtn');
|
||||
if (prevBtn) prevBtn.disabled = offset === 0;
|
||||
if (nextBtn) nextBtn.disabled = offset + limit >= total;
|
||||
}
|
||||
|
||||
function semanticPrevPage() {
|
||||
if (semanticMetadataCache.offset > 0) {
|
||||
loadSemanticMetadata(Math.max(0, semanticMetadataCache.offset - semanticMetadataCache.limit));
|
||||
}
|
||||
}
|
||||
|
||||
function semanticNextPage() {
|
||||
if (semanticMetadataCache.offset + semanticMetadataCache.limit < semanticMetadataCache.total) {
|
||||
loadSemanticMetadata(semanticMetadataCache.offset + semanticMetadataCache.limit);
|
||||
}
|
||||
}
|
||||
|
||||
function semanticGoToPage(pageIndex) {
|
||||
var offset = parseInt(pageIndex, 10) * semanticMetadataCache.limit;
|
||||
loadSemanticMetadata(offset);
|
||||
}
|
||||
|
||||
function filterSemanticByTool(tool) {
|
||||
loadSemanticMetadata(0, tool);
|
||||
}
|
||||
|
||||
function refreshSemanticMetadata() {
|
||||
loadSemanticMetadata(semanticMetadataCache.offset);
|
||||
}
|
||||
|
||||
function getLlmEnhancementSettings() {
|
||||
return Object.assign({}, llmEnhancementSettings);
|
||||
}
|
||||
|
||||
@@ -194,6 +194,50 @@ function handleNotification(data) {
|
||||
}
|
||||
break;
|
||||
|
||||
// CLI Review Events
|
||||
case 'CLI_REVIEW_UPDATED':
|
||||
if (typeof handleCliReviewUpdated === 'function') {
|
||||
handleCliReviewUpdated(payload);
|
||||
}
|
||||
// Also refresh CLI history to show review status
|
||||
if (typeof refreshCliHistory === 'function') {
|
||||
refreshCliHistory();
|
||||
}
|
||||
break;
|
||||
|
||||
// System Notify Events (from CLI commands)
|
||||
case 'REFRESH_REQUIRED':
|
||||
handleRefreshRequired(payload);
|
||||
break;
|
||||
|
||||
case 'MEMORY_UPDATED':
|
||||
if (typeof handleMemoryUpdated === 'function') {
|
||||
handleMemoryUpdated(payload);
|
||||
}
|
||||
// Force refresh of memory view if active
|
||||
if (getCurrentView && getCurrentView() === 'memory') {
|
||||
if (typeof loadMemoryStats === 'function') {
|
||||
loadMemoryStats().then(function() {
|
||||
if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'HISTORY_UPDATED':
|
||||
// Refresh CLI history when updated externally
|
||||
if (typeof refreshCliHistory === 'function') {
|
||||
refreshCliHistory();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'INSIGHT_GENERATED':
|
||||
// Refresh insights when new insight is generated
|
||||
if (typeof loadInsightsHistory === 'function') {
|
||||
loadInsightsHistory();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WS] Unknown notification type:', type);
|
||||
}
|
||||
@@ -427,6 +471,60 @@ async function refreshWorkspaceData(newData) {
|
||||
lastDataHash = calculateDataHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle REFRESH_REQUIRED events from CLI commands
|
||||
* @param {Object} payload - Contains scope (memory|history|insights|all)
|
||||
*/
|
||||
function handleRefreshRequired(payload) {
|
||||
const scope = payload?.scope || 'all';
|
||||
console.log('[WS] Refresh required for scope:', scope);
|
||||
|
||||
switch (scope) {
|
||||
case 'memory':
|
||||
// Refresh memory stats and graph
|
||||
if (typeof loadMemoryStats === 'function') {
|
||||
loadMemoryStats().then(function() {
|
||||
if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
|
||||
});
|
||||
}
|
||||
if (typeof loadMemoryGraph === 'function') {
|
||||
loadMemoryGraph();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'history':
|
||||
// Refresh CLI history
|
||||
if (typeof refreshCliHistory === 'function') {
|
||||
refreshCliHistory();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'insights':
|
||||
// Refresh insights history
|
||||
if (typeof loadInsightsHistory === 'function') {
|
||||
loadInsightsHistory();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
default:
|
||||
// Refresh everything
|
||||
refreshIfNeeded();
|
||||
if (typeof loadMemoryStats === 'function') {
|
||||
loadMemoryStats().then(function() {
|
||||
if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
|
||||
});
|
||||
}
|
||||
if (typeof refreshCliHistory === 'function') {
|
||||
refreshCliHistory();
|
||||
}
|
||||
if (typeof loadInsightsHistory === 'function') {
|
||||
loadInsightsHistory();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cleanup ==========
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
|
||||
@@ -6,6 +6,8 @@ var currentCliExecution = null;
|
||||
var cliExecutionOutput = '';
|
||||
var ccwInstallations = [];
|
||||
var ccwEndpointTools = [];
|
||||
var cliToolConfig = null; // Store loaded CLI config
|
||||
var predefinedModels = {}; // Store predefined models per tool
|
||||
|
||||
// ========== CCW Installations ==========
|
||||
async function loadCcwInstallations() {
|
||||
@@ -37,6 +39,271 @@ async function loadCcwEndpointTools() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CLI Tool Configuration ==========
|
||||
async function loadCliToolConfig() {
|
||||
try {
|
||||
var response = await fetch('/api/cli/config');
|
||||
if (!response.ok) throw new Error('Failed to load CLI config');
|
||||
var data = await response.json();
|
||||
cliToolConfig = data.config || null;
|
||||
predefinedModels = data.predefinedModels || {};
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI config:', err);
|
||||
cliToolConfig = null;
|
||||
predefinedModels = {};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCliToolConfig(tool, updates) {
|
||||
try {
|
||||
var response = await fetch('/api/cli/config/' + tool, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update CLI config');
|
||||
var data = await response.json();
|
||||
if (data.success && cliToolConfig && cliToolConfig.tools) {
|
||||
cliToolConfig.tools[tool] = data.config;
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to update CLI config:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Tool Configuration Modal ==========
|
||||
async function showToolConfigModal(toolName) {
|
||||
// Load config if not already loaded
|
||||
if (!cliToolConfig) {
|
||||
await loadCliToolConfig();
|
||||
}
|
||||
|
||||
var toolConfig = cliToolConfig && cliToolConfig.tools ? cliToolConfig.tools[toolName] : null;
|
||||
var models = predefinedModels[toolName] || [];
|
||||
var status = cliToolStatus[toolName] || {};
|
||||
|
||||
if (!toolConfig) {
|
||||
toolConfig = { enabled: true, primaryModel: '', secondaryModel: '' };
|
||||
}
|
||||
|
||||
var content = buildToolConfigModalContent(toolName, toolConfig, models, status);
|
||||
showModal('Configure ' + toolName.charAt(0).toUpperCase() + toolName.slice(1), content, { size: 'md' });
|
||||
|
||||
// Initialize event handlers after modal is shown
|
||||
setTimeout(function() {
|
||||
initToolConfigModalEvents(toolName, toolConfig, models);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function buildToolConfigModalContent(tool, config, models, status) {
|
||||
var isAvailable = status.available;
|
||||
var isEnabled = config.enabled;
|
||||
|
||||
// Check if model is custom (not in predefined list or empty)
|
||||
var isPrimaryCustom = !config.primaryModel || models.indexOf(config.primaryModel) === -1;
|
||||
var isSecondaryCustom = !config.secondaryModel || models.indexOf(config.secondaryModel) === -1;
|
||||
|
||||
var modelsOptionsHtml = function(selected, isCustom) {
|
||||
var html = '';
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
var m = models[i];
|
||||
html += '<option value="' + escapeHtml(m) + '"' + (m === selected && !isCustom ? ' selected' : '') + '>' + escapeHtml(m) + '</option>';
|
||||
}
|
||||
html += '<option value="__custom__"' + (isCustom ? ' selected' : '') + '>Custom...</option>';
|
||||
return html;
|
||||
};
|
||||
|
||||
return '<div class="tool-config-modal">' +
|
||||
// Status Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>Status</h4>' +
|
||||
'<div class="tool-config-badges">' +
|
||||
'<span class="badge ' + (isAvailable ? 'badge-success' : 'badge-muted') + '">' +
|
||||
'<i data-lucide="' + (isAvailable ? 'check-circle' : 'circle-dashed') + '" class="w-3 h-3"></i> ' +
|
||||
(isAvailable ? 'Installed' : 'Not Installed') +
|
||||
'</span>' +
|
||||
'<span class="badge ' + (isEnabled ? 'badge-primary' : 'badge-muted') + '">' +
|
||||
'<i data-lucide="' + (isEnabled ? 'toggle-right' : 'toggle-left') + '" class="w-3 h-3"></i> ' +
|
||||
(isEnabled ? 'Enabled' : 'Disabled') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Actions Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>Actions</h4>' +
|
||||
'<div class="tool-config-actions">' +
|
||||
'<button class="btn-sm ' + (isEnabled ? 'btn-outline' : 'btn-primary') + '" id="toggleEnableBtn" ' + (!isAvailable ? 'disabled' : '') + '>' +
|
||||
'<i data-lucide="' + (isEnabled ? 'toggle-left' : 'toggle-right') + '" class="w-3 h-3"></i> ' +
|
||||
(isEnabled ? 'Disable' : 'Enable') +
|
||||
'</button>' +
|
||||
'<button class="btn-sm ' + (isAvailable ? 'btn-outline btn-danger-outline' : 'btn-primary') + '" id="installBtn">' +
|
||||
'<i data-lucide="' + (isAvailable ? 'trash-2' : 'download') + '" class="w-3 h-3"></i> ' +
|
||||
(isAvailable ? 'Uninstall' : 'Install') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Primary Model Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>Primary Model <span class="text-muted">(CLI endpoint calls)</span></h4>' +
|
||||
'<div class="model-select-group">' +
|
||||
'<select id="primaryModelSelect" class="tool-config-select">' +
|
||||
modelsOptionsHtml(config.primaryModel, isPrimaryCustom) +
|
||||
'</select>' +
|
||||
'<input type="text" id="primaryModelCustom" class="tool-config-input" ' +
|
||||
'style="display: ' + (isPrimaryCustom ? 'block' : 'none') + ';" ' +
|
||||
'placeholder="Enter model name (e.g., gemini-2.5-pro)" ' +
|
||||
'value="' + (isPrimaryCustom && config.primaryModel ? escapeHtml(config.primaryModel) : '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Secondary Model Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>Secondary Model <span class="text-muted">(internal tools)</span></h4>' +
|
||||
'<div class="model-select-group">' +
|
||||
'<select id="secondaryModelSelect" class="tool-config-select">' +
|
||||
modelsOptionsHtml(config.secondaryModel, isSecondaryCustom) +
|
||||
'</select>' +
|
||||
'<input type="text" id="secondaryModelCustom" class="tool-config-input" ' +
|
||||
'style="display: ' + (isSecondaryCustom ? 'block' : 'none') + ';" ' +
|
||||
'placeholder="Enter model name (e.g., gemini-2.5-flash)" ' +
|
||||
'value="' + (isSecondaryCustom && config.secondaryModel ? escapeHtml(config.secondaryModel) : '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" id="saveConfigBtn">' +
|
||||
'<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('common.save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
// Toggle Enable/Disable
|
||||
var toggleBtn = document.getElementById('toggleEnableBtn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.onclick = async function() {
|
||||
var newEnabled = !currentConfig.enabled;
|
||||
try {
|
||||
await updateCliToolConfig(tool, { enabled: newEnabled });
|
||||
showRefreshToast(tool + ' ' + (newEnabled ? 'enabled' : 'disabled'), 'success');
|
||||
closeModal();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to update: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Install/Uninstall
|
||||
var installBtn = document.getElementById('installBtn');
|
||||
if (installBtn) {
|
||||
installBtn.onclick = async function() {
|
||||
var status = cliToolStatus[tool] || {};
|
||||
var endpoint = status.available ? '/api/cli/uninstall' : '/api/cli/install';
|
||||
var action = status.available ? 'uninstalling' : 'installing';
|
||||
|
||||
showRefreshToast(tool.charAt(0).toUpperCase() + tool.slice(1) + ' ' + action + '...', 'info');
|
||||
closeModal();
|
||||
|
||||
try {
|
||||
var response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: tool })
|
||||
});
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(result.message || (tool + ' ' + (status.available ? 'uninstalled' : 'installed')), 'success');
|
||||
await loadCliToolStatus();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} else {
|
||||
showRefreshToast(result.error || 'Operation failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Model select handlers
|
||||
var primarySelect = document.getElementById('primaryModelSelect');
|
||||
var primaryCustom = document.getElementById('primaryModelCustom');
|
||||
var secondarySelect = document.getElementById('secondaryModelSelect');
|
||||
var secondaryCustom = document.getElementById('secondaryModelCustom');
|
||||
|
||||
if (primarySelect && primaryCustom) {
|
||||
primarySelect.onchange = function() {
|
||||
if (this.value === '__custom__') {
|
||||
primaryCustom.style.display = 'block';
|
||||
primaryCustom.focus();
|
||||
} else {
|
||||
primaryCustom.style.display = 'none';
|
||||
primaryCustom.value = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (secondarySelect && secondaryCustom) {
|
||||
secondarySelect.onchange = function() {
|
||||
if (this.value === '__custom__') {
|
||||
secondaryCustom.style.display = 'block';
|
||||
secondaryCustom.focus();
|
||||
} else {
|
||||
secondaryCustom.style.display = 'none';
|
||||
secondaryCustom.value = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Save button
|
||||
var saveBtn = document.getElementById('saveConfigBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = async function() {
|
||||
var primaryModel = primarySelect.value === '__custom__'
|
||||
? primaryCustom.value.trim()
|
||||
: primarySelect.value;
|
||||
var secondaryModel = secondarySelect.value === '__custom__'
|
||||
? secondaryCustom.value.trim()
|
||||
: secondarySelect.value;
|
||||
|
||||
if (!primaryModel) {
|
||||
showRefreshToast('Primary model is required', 'error');
|
||||
return;
|
||||
}
|
||||
if (!secondaryModel) {
|
||||
showRefreshToast('Secondary model is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateCliToolConfig(tool, {
|
||||
primaryModel: primaryModel,
|
||||
secondaryModel: secondaryModel
|
||||
});
|
||||
showRefreshToast('Configuration saved', 'success');
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to save: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize lucide icons in modal
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
async function renderCliManager() {
|
||||
var container = document.getElementById('mainContent');
|
||||
@@ -94,12 +361,13 @@ function renderToolsSection() {
|
||||
var isAvailable = status.available;
|
||||
var isDefault = defaultCliTool === tool;
|
||||
|
||||
return '<div class="tool-item ' + (isAvailable ? 'available' : 'unavailable') + '">' +
|
||||
return '<div class="tool-item clickable ' + (isAvailable ? 'available' : 'unavailable') + '" onclick="showToolConfigModal(\'' + tool + '\')">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (isAvailable ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + tool.charAt(0).toUpperCase() + tool.slice(1) +
|
||||
(isDefault ? '<span class="tool-default-badge">' + t('cli.default') + '</span>' : '') +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-desc">' + toolDescriptions[tool] + '</div>' +
|
||||
'</div>' +
|
||||
@@ -109,7 +377,7 @@ function renderToolsSection() {
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> ' + t('cli.ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>') +
|
||||
(isAvailable && !isDefault
|
||||
? '<button class="btn-sm btn-outline" onclick="setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> ' + t('cli.setDefault') + '</button>'
|
||||
? '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> ' + t('cli.setDefault') + '</button>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -136,11 +404,13 @@ function renderToolsSection() {
|
||||
// Semantic Search item (only show if CodexLens is installed)
|
||||
var semanticHtml = '';
|
||||
if (codexLensStatus.ready) {
|
||||
semanticHtml = '<div class="tool-item ' + (semanticStatus.available ? 'available' : 'unavailable') + '">' +
|
||||
semanticHtml = '<div class="tool-item clickable ' + (semanticStatus.available ? 'available' : 'unavailable') + '" onclick="openSemanticSettingsModal()">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (semanticStatus.available ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span></div>' +
|
||||
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span>' +
|
||||
(llmEnhancementSettings.enabled ? '<span class="tool-type-badge llm">LLM</span>' : '') +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<div class="tool-item-desc">' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
@@ -148,7 +418,7 @@ function renderToolsSection() {
|
||||
(semanticStatus.available
|
||||
? '<span class="tool-status-text success"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + (semanticStatus.backend || 'Ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ async function loadPromptInsights() {
|
||||
|
||||
async function loadPromptInsightsHistory() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights?limit=20');
|
||||
var response = await fetch('/api/memory/insights?limit=20&path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load insights history');
|
||||
var data = await response.json();
|
||||
promptInsightsHistory = data.insights || [];
|
||||
@@ -699,6 +699,9 @@ async function triggerCliInsightsAnalysis() {
|
||||
console.log('[PromptHistory] Insights parsed:', promptInsights);
|
||||
}
|
||||
|
||||
// Reload insights history to show the new analysis result
|
||||
await loadPromptInsightsHistory();
|
||||
|
||||
showRefreshToast(t('toast.completed') + ' (' + tool + ')', 'success');
|
||||
} catch (err) {
|
||||
console.error('CLI insights analysis failed:', err);
|
||||
|
||||
Reference in New Issue
Block a user