mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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': '创建聚类失败',
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user