mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
Add internationalization support for help view and implement help rendering logic
- Introduced `help-i18n.js` for managing translations in Chinese and English for the help view. - Created `help.js` to render the help view, including command categories, workflow diagrams, and CodexLens quick-start. - Implemented search functionality with debounce for command filtering. - Added workflow diagram rendering with Cytoscape.js integration. - Developed tests for write-file verification, ensuring proper handling of small and large JSON files.
This commit is contained in:
308
ccw/src/core/routes/help-routes.ts
Normal file
308
ccw/src/core/routes/help-routes.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Help Routes Module
|
||||
* Handles all Help-related API endpoints for command guide and CodexLens docs
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { readFileSync, existsSync, watch } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
// ========== In-Memory Cache ==========
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const CACHE_TTL = 300000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Get cached data or load from file
|
||||
*/
|
||||
function getCachedData(key: string, filePath: string): any {
|
||||
const now = Date.now();
|
||||
const cached = cache.get(key);
|
||||
|
||||
// Return cached data if valid
|
||||
if (cached && (now - cached.timestamp) < CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
// Load fresh data
|
||||
try {
|
||||
if (!existsSync(filePath)) {
|
||||
console.error(`Help data file not found: ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// Update cache
|
||||
cache.set(key, { data, timestamp: now });
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load help data from ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific key
|
||||
*/
|
||||
function invalidateCache(key: string): void {
|
||||
cache.delete(key);
|
||||
console.log(`Cache invalidated: ${key}`);
|
||||
}
|
||||
|
||||
// ========== File Watchers ==========
|
||||
let watchersInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize file watchers for JSON indexes
|
||||
*/
|
||||
function initializeFileWatchers(): void {
|
||||
if (watchersInitialized) return;
|
||||
|
||||
const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
|
||||
|
||||
if (!existsSync(indexDir)) {
|
||||
console.warn(`Command guide index directory not found: ${indexDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Watch all JSON files in index directory
|
||||
const watcher = watch(indexDir, { recursive: false }, (eventType, filename) => {
|
||||
if (!filename || !filename.endsWith('.json')) return;
|
||||
|
||||
console.log(`File change detected: ${filename} (${eventType})`);
|
||||
|
||||
// Invalidate relevant cache entries
|
||||
if (filename === 'all-commands.json') {
|
||||
invalidateCache('all-commands');
|
||||
} else if (filename === 'command-relationships.json') {
|
||||
invalidateCache('command-relationships');
|
||||
} else if (filename === 'by-category.json') {
|
||||
invalidateCache('by-category');
|
||||
}
|
||||
});
|
||||
|
||||
watchersInitialized = true;
|
||||
console.log(`File watchers initialized for: ${indexDir}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize file watchers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Filter commands by search query
|
||||
*/
|
||||
function filterCommands(commands: any[], query: string): any[] {
|
||||
if (!query) return commands;
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return commands.filter(cmd =>
|
||||
cmd.name?.toLowerCase().includes(lowerQuery) ||
|
||||
cmd.command?.toLowerCase().includes(lowerQuery) ||
|
||||
cmd.description?.toLowerCase().includes(lowerQuery) ||
|
||||
cmd.category?.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group commands by category with subcategories
|
||||
*/
|
||||
function groupCommandsByCategory(commands: any[]): any {
|
||||
const grouped: any = {};
|
||||
|
||||
for (const cmd of commands) {
|
||||
const category = cmd.category || 'general';
|
||||
const subcategory = cmd.subcategory || null;
|
||||
|
||||
if (!grouped[category]) {
|
||||
grouped[category] = {
|
||||
name: category,
|
||||
commands: [],
|
||||
subcategories: {}
|
||||
};
|
||||
}
|
||||
|
||||
if (subcategory) {
|
||||
if (!grouped[category].subcategories[subcategory]) {
|
||||
grouped[category].subcategories[subcategory] = [];
|
||||
}
|
||||
grouped[category].subcategories[subcategory].push(cmd);
|
||||
} else {
|
||||
grouped[category].commands.push(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// ========== API Routes ==========
|
||||
|
||||
/**
|
||||
* Handle Help routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res } = ctx;
|
||||
|
||||
// Initialize file watchers on first request
|
||||
initializeFileWatchers();
|
||||
|
||||
const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
|
||||
|
||||
// API: Get all commands with optional search
|
||||
if (pathname === '/api/help/commands') {
|
||||
const searchQuery = url.searchParams.get('q') || '';
|
||||
const filePath = join(indexDir, 'all-commands.json');
|
||||
|
||||
let commands = getCachedData('all-commands', filePath);
|
||||
|
||||
if (!commands) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Commands data not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Filter by search query if provided
|
||||
if (searchQuery) {
|
||||
commands = filterCommands(commands, searchQuery);
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const grouped = groupCommandsByCategory(commands);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
commands: commands,
|
||||
grouped: grouped,
|
||||
total: commands.length
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get workflow command relationships
|
||||
if (pathname === '/api/help/workflows') {
|
||||
const filePath = join(indexDir, 'command-relationships.json');
|
||||
const relationships = getCachedData('command-relationships', filePath);
|
||||
|
||||
if (!relationships) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Workflow relationships not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(relationships));
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get commands by category
|
||||
if (pathname === '/api/help/commands/by-category') {
|
||||
const filePath = join(indexDir, 'by-category.json');
|
||||
const byCategory = getCachedData('by-category', filePath);
|
||||
|
||||
if (!byCategory) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Category data not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(byCategory));
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get CodexLens documentation metadata
|
||||
if (pathname === '/api/help/codexlens') {
|
||||
// Return CodexLens quick-start guide data
|
||||
const codexLensData = {
|
||||
title: 'CodexLens Quick Start',
|
||||
description: 'Fast code indexing and semantic search for large codebases',
|
||||
sections: [
|
||||
{
|
||||
title: 'Key Concepts',
|
||||
items: [
|
||||
{
|
||||
name: 'Indexing',
|
||||
description: 'CodexLens builds a semantic index of your codebase for fast retrieval',
|
||||
command: 'codex_lens(action="init", path=".")'
|
||||
},
|
||||
{
|
||||
name: 'Search Modes',
|
||||
description: 'Text search for exact matches, semantic search for concept-based queries',
|
||||
command: 'codex_lens(action="search", query="authentication logic", mode="semantic")'
|
||||
},
|
||||
{
|
||||
name: 'Symbol Navigation',
|
||||
description: 'Extract and navigate code symbols (functions, classes, interfaces)',
|
||||
command: 'codex_lens(action="symbol", file="path/to/file.py")'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Common Commands',
|
||||
items: [
|
||||
{
|
||||
name: 'Initialize Index',
|
||||
command: 'codex_lens(action="init", path=".")',
|
||||
description: 'Index the current directory'
|
||||
},
|
||||
{
|
||||
name: 'Text Search',
|
||||
command: 'codex_lens(action="search", query="function name", path=".")',
|
||||
description: 'Search for exact text matches'
|
||||
},
|
||||
{
|
||||
name: 'Semantic Search',
|
||||
command: 'codex_lens(action="search", query="user authentication", mode="semantic")',
|
||||
description: 'Search by concept or meaning'
|
||||
},
|
||||
{
|
||||
name: 'Check Status',
|
||||
command: 'codex_lens(action="status")',
|
||||
description: 'View indexing status for all projects'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Best Practices',
|
||||
items: [
|
||||
{ description: 'Index large codebases (>500 files) for optimal performance' },
|
||||
{ description: 'Use semantic search for exploratory tasks' },
|
||||
{ description: 'Combine with smart_search for medium-sized projects' },
|
||||
{ description: 'Re-index after major code changes' }
|
||||
]
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{ text: 'Full Documentation', url: 'https://github.com/yourusername/codex-lens' },
|
||||
{ text: 'Tool Selection Guide', url: '/.claude/rules/tool-selection.md' }
|
||||
]
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(codexLensData));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { handleRulesRoutes } from './routes/rules-routes.js';
|
||||
import { handleSessionRoutes } from './routes/session-routes.js';
|
||||
import { handleCcwRoutes } from './routes/ccw-routes.js';
|
||||
import { handleClaudeRoutes } from './routes/claude-routes.js';
|
||||
import { handleHelpRoutes } from './routes/help-routes.js';
|
||||
|
||||
// Import WebSocket handling
|
||||
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
|
||||
@@ -65,12 +66,14 @@ const MODULE_CSS_FILES = [
|
||||
'12-skills-rules.css',
|
||||
'13-claude-manager.css',
|
||||
'14-graph-explorer.css',
|
||||
'15-mcp-manager.css'
|
||||
'15-mcp-manager.css',
|
||||
'16-help.css'
|
||||
];
|
||||
|
||||
// Modular JS files in dependency order
|
||||
const MODULE_FILES = [
|
||||
'i18n.js', // Must be loaded first for translations
|
||||
'help-i18n.js', // Help page translations
|
||||
'utils.js',
|
||||
'state.js',
|
||||
'api.js',
|
||||
@@ -113,6 +116,7 @@ const MODULE_FILES = [
|
||||
'views/rules-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/graph-explorer.js',
|
||||
'views/help.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
@@ -300,6 +304,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleRulesRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Help routes (/api/help/*)
|
||||
if (pathname.startsWith('/api/help/')) {
|
||||
if (await handleHelpRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Session routes (/api/session-detail, /api/update-task-status, /api/bulk-update-task-status)
|
||||
if (pathname.includes('session') || pathname.includes('task-status')) {
|
||||
if (await handleSessionRoutes(routeContext)) return;
|
||||
|
||||
Reference in New Issue
Block a user