mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +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 { URL } from 'url';
|
||||||
import { getCoreMemoryStore } from '../core-memory-store.js';
|
import { getCoreMemoryStore } from '../core-memory-store.js';
|
||||||
import type { CoreMemory, SessionCluster, ClusterMember, ClusterRelation } 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
|
* Route context interface
|
||||||
@@ -301,6 +304,62 @@ export async function handleCoreMemoryRoutes(ctx: RouteContext): Promise<boolean
|
|||||||
return true;
|
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
|
// API: Create new cluster
|
||||||
if (pathname === '/api/core-memory/clusters' && req.method === 'POST') {
|
if (pathname === '/api/core-memory/clusters' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
|||||||
@@ -1645,3 +1645,56 @@
|
|||||||
background: rgba(15, 23, 42, 0.5);
|
background: rgba(15, 23, 42, 0.5);
|
||||||
border-color: #334155;
|
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.clusteringInProgress': 'Clustering in progress...',
|
||||||
'coreMemory.clusteringComplete': 'Created {created} clusters with {sessions} sessions',
|
'coreMemory.clusteringComplete': 'Created {created} clusters with {sessions} sessions',
|
||||||
'coreMemory.clusteringError': 'Auto-clustering failed',
|
'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.enterClusterName': 'Enter cluster name:',
|
||||||
'coreMemory.clusterCreated': 'Cluster created',
|
'coreMemory.clusterCreated': 'Cluster created',
|
||||||
'coreMemory.clusterCreateError': 'Failed to create cluster',
|
'coreMemory.clusterCreateError': 'Failed to create cluster',
|
||||||
@@ -2594,6 +2602,14 @@ const i18n = {
|
|||||||
'coreMemory.clusteringInProgress': '聚类进行中...',
|
'coreMemory.clusteringInProgress': '聚类进行中...',
|
||||||
'coreMemory.clusteringComplete': '创建了 {created} 个聚类,包含 {sessions} 个 session',
|
'coreMemory.clusteringComplete': '创建了 {created} 个聚类,包含 {sessions} 个 session',
|
||||||
'coreMemory.clusteringError': '自动聚类失败',
|
'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.enterClusterName': '请输入聚类名称:',
|
||||||
'coreMemory.clusterCreated': '聚类已创建',
|
'coreMemory.clusterCreated': '聚类已创建',
|
||||||
'coreMemory.clusterCreateError': '创建聚类失败',
|
'coreMemory.clusterCreateError': '创建聚类失败',
|
||||||
|
|||||||
@@ -5,12 +5,30 @@
|
|||||||
// Global state
|
// Global state
|
||||||
var clusterList = [];
|
var clusterList = [];
|
||||||
var selectedCluster = null;
|
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
|
* Fetch and render cluster list
|
||||||
*/
|
*/
|
||||||
async function loadClusters() {
|
async function loadClusters() {
|
||||||
try {
|
try {
|
||||||
|
// Check embedding status first
|
||||||
|
await checkEmbeddingStatus();
|
||||||
|
|
||||||
const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}`);
|
const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
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
|
* Render cluster list in sidebar
|
||||||
*/
|
*/
|
||||||
@@ -30,8 +108,12 @@ function renderClusterList() {
|
|||||||
const container = document.getElementById('clusterListContainer');
|
const container = document.getElementById('clusterListContainer');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// Add embedding status hint at top
|
||||||
|
const embeddingHint = renderEmbeddingHint();
|
||||||
|
|
||||||
if (clusterList.length === 0) {
|
if (clusterList.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
|
${embeddingHint}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i data-lucide="folder-tree"></i>
|
<i data-lucide="folder-tree"></i>
|
||||||
<p>${t('coreMemory.noClusters')}</p>
|
<p>${t('coreMemory.noClusters')}</p>
|
||||||
@@ -45,7 +127,7 @@ function renderClusterList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = clusterList.map(cluster => `
|
container.innerHTML = embeddingHint + clusterList.map(cluster => `
|
||||||
<div class="cluster-item ${selectedCluster?.id === cluster.id ? 'active' : ''}"
|
<div class="cluster-item ${selectedCluster?.id === cluster.id ? 'active' : ''}"
|
||||||
onclick="selectCluster('${cluster.id}')">
|
onclick="selectCluster('${cluster.id}')">
|
||||||
<div class="cluster-icon">
|
<div class="cluster-icon">
|
||||||
|
|||||||
Reference in New Issue
Block a user