feat: Add embedding status hint in clustering UI

- Add embedding status check API endpoints (embed-status, embed)
- Display embedding hint in cluster list view:
  - Warning when vector model not installed
  - Progress indicator when embeddings pending
  - Generate button for quick embedding
- Add i18n translations (EN/ZH)
- Add CSS styles for embedding hint components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-20 13:15:25 +08:00
parent 31cc060837
commit 6b62b5b5a9
4 changed files with 211 additions and 1 deletions

View File

@@ -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<boolean
return true;
}
// API: Get embedding status
if (pathname === '/api/core-memory/embed-status' && req.method === 'GET') {
const projectPath = url.searchParams.get('path') || initialPath;
try {
if (!isEmbedderAvailable()) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(null));
return true;
}
const paths = StoragePaths.project(projectPath);
const dbPath = join(paths.root, 'core-memory', 'core_memory.db');
const status = await getEmbeddingStatus(dbPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(status));
} catch (error: unknown) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
}
return true;
}
// API: Generate embeddings
if (pathname === '/api/core-memory/embed' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
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) => {

View File

@@ -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;
}

View File

@@ -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': '创建聚类失败',

View File

@@ -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 `
<div class="embedding-hint warning">
<i data-lucide="alert-triangle"></i>
<span>${t('coreMemory.embeddingNotAvailable')}</span>
<a href="#" onclick="window.open('https://github.com/anthropics/claude-code', '_blank'); return false;" class="hint-link">
${t('coreMemory.installGuide')}
</a>
</div>
`;
}
if (embeddingStatus.pending_chunks > 0) {
const pct = Math.round((embeddingStatus.embedded_chunks / embeddingStatus.total_chunks) * 100);
return `
<div class="embedding-hint info">
<i data-lucide="cpu"></i>
<span>${t('coreMemory.embeddingProgress', { pct, pending: embeddingStatus.pending_chunks })}</span>
<button class="btn btn-xs" onclick="triggerEmbedding()">
${t('coreMemory.generateEmbeddings')}
</button>
</div>
`;
}
if (embeddingStatus.total_chunks === 0) {
return `
<div class="embedding-hint info">
<i data-lucide="info"></i>
<span>${t('coreMemory.noChunksYet')}</span>
</div>
`;
}
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}
<div class="empty-state">
<i data-lucide="folder-tree"></i>
<p>${t('coreMemory.noClusters')}</p>
@@ -45,7 +127,7 @@ function renderClusterList() {
return;
}
container.innerHTML = clusterList.map(cluster => `
container.innerHTML = embeddingHint + clusterList.map(cluster => `
<div class="cluster-item ${selectedCluster?.id === cluster.id ? 'active' : ''}"
onclick="selectCluster('${cluster.id}')">
<div class="cluster-icon">