diff --git a/ccw/src/core/routes/core-memory-routes.ts b/ccw/src/core/routes/core-memory-routes.ts index ca2f0e19..3efc4804 100644 --- a/ccw/src/core/routes/core-memory-routes.ts +++ b/ccw/src/core/routes/core-memory-routes.ts @@ -2,6 +2,9 @@ import * as http from 'http'; import { URL } from 'url'; import { getCoreMemoryStore } from '../core-memory-store.js'; import type { CoreMemory, SessionCluster, ClusterMember, ClusterRelation } from '../core-memory-store.js'; +import { getEmbeddingStatus, generateEmbeddings, isEmbedderAvailable } from '../memory-embedder-bridge.js'; +import { StoragePaths } from '../../config/storage-paths.js'; +import { join } from 'path'; /** * Route context interface @@ -301,6 +304,62 @@ export async function handleCoreMemoryRoutes(ctx: RouteContext): Promise { + const { sourceId, force, batchSize, path: projectPath } = body; + const basePath = projectPath || initialPath; + + try { + if (!isEmbedderAvailable()) { + return { error: 'Embedder not available. Install CodexLens first.', status: 503 }; + } + + const paths = StoragePaths.project(basePath); + const dbPath = join(paths.root, 'core-memory', 'core_memory.db'); + + const result = await generateEmbeddings(dbPath, { + sourceId, + force: force || false, + batchSize: batchSize || 8 + }); + + return { + success: result.success, + chunks_processed: result.chunks_processed, + elapsed_time: result.elapsed_time + }; + } catch (error: unknown) { + return { error: (error as Error).message, status: 500 }; + } + }); + return true; + } + // API: Create new cluster if (pathname === '/api/core-memory/clusters' && req.method === 'POST') { handlePostRequest(req, res, async (body) => { diff --git a/ccw/src/templates/dashboard-css/30-core-memory.css b/ccw/src/templates/dashboard-css/30-core-memory.css index a20556a5..ebd38353 100644 --- a/ccw/src/templates/dashboard-css/30-core-memory.css +++ b/ccw/src/templates/dashboard-css/30-core-memory.css @@ -1645,3 +1645,56 @@ background: rgba(15, 23, 42, 0.5); border-color: #334155; } + +/* Embedding Hint Styles */ +.embedding-hint { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + margin-bottom: 12px; + border-radius: 8px; + font-size: 12px; + line-height: 1.4; +} + +.embedding-hint i { + flex-shrink: 0; + width: 16px; + height: 16px; +} + +.embedding-hint.warning { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + color: #d97706; +} + +.embedding-hint.info { + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + color: #3b82f6; +} + +.embedding-hint .hint-link { + margin-left: auto; + color: inherit; + text-decoration: underline; + white-space: nowrap; +} + +.embedding-hint .btn-xs { + margin-left: auto; + padding: 4px 8px; + font-size: 11px; +} + +[data-theme="dark"] .embedding-hint.warning { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +[data-theme="dark"] .embedding-hint.info { + background: rgba(59, 130, 246, 0.15); + color: #60a5fa; +} diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 133b14ae..598ba820 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1281,6 +1281,14 @@ const i18n = { 'coreMemory.clusteringInProgress': 'Clustering in progress...', 'coreMemory.clusteringComplete': 'Created {created} clusters with {sessions} sessions', 'coreMemory.clusteringError': 'Auto-clustering failed', + 'coreMemory.embeddingNotAvailable': 'Vector model not installed. Install to improve clustering accuracy.', + 'coreMemory.installGuide': 'Install Guide', + 'coreMemory.embeddingProgress': 'Embeddings: {pct}% ({pending} pending)', + 'coreMemory.generateEmbeddings': 'Generate', + 'coreMemory.noChunksYet': 'No memories chunked yet. Run "ccw memory embed" to enable semantic clustering.', + 'coreMemory.embeddingInProgress': 'Generating embeddings...', + 'coreMemory.embeddingComplete': 'Generated embeddings for {count} chunks', + 'coreMemory.embeddingError': 'Failed to generate embeddings', 'coreMemory.enterClusterName': 'Enter cluster name:', 'coreMemory.clusterCreated': 'Cluster created', 'coreMemory.clusterCreateError': 'Failed to create cluster', @@ -2594,6 +2602,14 @@ const i18n = { 'coreMemory.clusteringInProgress': '聚类进行中...', 'coreMemory.clusteringComplete': '创建了 {created} 个聚类,包含 {sessions} 个 session', 'coreMemory.clusteringError': '自动聚类失败', + 'coreMemory.embeddingNotAvailable': '向量模型未安装。安装后可提升聚类准确度。', + 'coreMemory.installGuide': '安装指南', + 'coreMemory.embeddingProgress': '嵌入进度: {pct}% (待处理: {pending})', + 'coreMemory.generateEmbeddings': '生成', + 'coreMemory.noChunksYet': '暂无记忆分块。运行 "ccw memory embed" 启用语义聚类。', + 'coreMemory.embeddingInProgress': '正在生成嵌入...', + 'coreMemory.embeddingComplete': '已为 {count} 个分块生成嵌入', + 'coreMemory.embeddingError': '生成嵌入失败', 'coreMemory.enterClusterName': '请输入聚类名称:', 'coreMemory.clusterCreated': '聚类已创建', 'coreMemory.clusterCreateError': '创建聚类失败', diff --git a/ccw/src/templates/dashboard-js/views/core-memory-clusters.js b/ccw/src/templates/dashboard-js/views/core-memory-clusters.js index 7933ddc2..944ce77a 100644 --- a/ccw/src/templates/dashboard-js/views/core-memory-clusters.js +++ b/ccw/src/templates/dashboard-js/views/core-memory-clusters.js @@ -5,12 +5,30 @@ // Global state var clusterList = []; var selectedCluster = null; +var embeddingStatus = null; + +/** + * Check embedding status for better clustering + */ +async function checkEmbeddingStatus() { + try { + const response = await fetch(`/api/core-memory/embed-status?path=${encodeURIComponent(projectPath)}`); + if (response.ok) { + embeddingStatus = await response.json(); + } + } catch (error) { + console.log('Embedding status check skipped:', error.message); + } +} /** * Fetch and render cluster list */ async function loadClusters() { try { + // Check embedding status first + await checkEmbeddingStatus(); + const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -23,6 +41,66 @@ async function loadClusters() { } } +/** + * Render embedding status hint + */ +function renderEmbeddingHint() { + if (!embeddingStatus) { + return ` +
+ + ${t('coreMemory.embeddingNotAvailable')} + + ${t('coreMemory.installGuide')} + +
+ `; + } + + if (embeddingStatus.pending_chunks > 0) { + const pct = Math.round((embeddingStatus.embedded_chunks / embeddingStatus.total_chunks) * 100); + return ` +
+ + ${t('coreMemory.embeddingProgress', { pct, pending: embeddingStatus.pending_chunks })} + +
+ `; + } + + if (embeddingStatus.total_chunks === 0) { + return ` +
+ + ${t('coreMemory.noChunksYet')} +
+ `; + } + + return ''; +} + +/** + * Trigger embedding generation + */ +async function triggerEmbedding() { + try { + showNotification(t('coreMemory.embeddingInProgress'), 'info'); + const response = await fetch(`/api/core-memory/embed?path=${encodeURIComponent(projectPath)}`, { + method: 'POST' + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const result = await response.json(); + showNotification(t('coreMemory.embeddingComplete', { count: result.chunks_processed }), 'success'); + await loadClusters(); + } catch (error) { + console.error('Embedding failed:', error); + showNotification(t('coreMemory.embeddingError'), 'error'); + } +} + /** * Render cluster list in sidebar */ @@ -30,8 +108,12 @@ function renderClusterList() { const container = document.getElementById('clusterListContainer'); if (!container) return; + // Add embedding status hint at top + const embeddingHint = renderEmbeddingHint(); + if (clusterList.length === 0) { container.innerHTML = ` + ${embeddingHint}

${t('coreMemory.noClusters')}

@@ -45,7 +127,7 @@ function renderClusterList() { return; } - container.innerHTML = clusterList.map(cluster => ` + container.innerHTML = embeddingHint + clusterList.map(cluster => `