mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: add Accordion component for UI and Zustand store for coordinator management
- Implemented Accordion component using Radix UI for collapsible sections. - Created Zustand store to manage coordinator execution state, command chains, logs, and interactive questions. - Added validation tests for CLI settings type definitions, ensuring type safety and correct behavior of helper functions.
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
updateMemory,
|
||||
deleteMemory,
|
||||
type CoreMemory,
|
||||
type MemoryResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
@@ -30,6 +29,8 @@ const STALE_TIME = 60 * 1000;
|
||||
export interface MemoryFilter {
|
||||
search?: string;
|
||||
tags?: string[];
|
||||
favorite?: boolean;
|
||||
archived?: boolean;
|
||||
}
|
||||
|
||||
export interface UseMemoryOptions {
|
||||
@@ -93,6 +94,26 @@ export function useMemory(options: UseMemoryOptions = {}): UseMemoryReturn {
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by favorite status (from metadata)
|
||||
if (filter?.favorite === true) {
|
||||
memories = memories.filter((m) => {
|
||||
if (!m.metadata) return false;
|
||||
try {
|
||||
const metadata = typeof m.metadata === 'string' ? JSON.parse(m.metadata) : m.metadata;
|
||||
return metadata.favorite === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by archived status
|
||||
if (filter?.archived === true) {
|
||||
memories = memories.filter((m) => m.archived === true);
|
||||
} else if (filter?.archived === false) {
|
||||
memories = memories.filter((m) => m.archived !== true);
|
||||
}
|
||||
|
||||
return memories;
|
||||
})();
|
||||
|
||||
@@ -202,6 +223,64 @@ export function useDeleteMemory(): UseDeleteMemoryReturn {
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseArchiveMemoryReturn {
|
||||
archiveMemory: (memoryId: string) => Promise<void>;
|
||||
isArchiving: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useArchiveMemory(): UseArchiveMemoryReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (memoryId: string) =>
|
||||
fetch(`/api/core-memory/memories/${encodeURIComponent(memoryId)}/archive?path=${encodeURIComponent(projectPath)}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
}).then(res => res.json()),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
archiveMemory: mutation.mutateAsync,
|
||||
isArchiving: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseUnarchiveMemoryReturn {
|
||||
unarchiveMemory: (memoryId: string) => Promise<void>;
|
||||
isUnarchiving: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useUnarchiveMemory(): UseUnarchiveMemoryReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (memoryId: string) =>
|
||||
fetch(`/api/core-memory/memories?path=${encodeURIComponent(projectPath)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ id: memoryId, archived: false }),
|
||||
}).then(res => res.json()),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
unarchiveMemory: mutation.mutateAsync,
|
||||
isUnarchiving: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for all memory mutations
|
||||
*/
|
||||
@@ -209,14 +288,20 @@ export function useMemoryMutations() {
|
||||
const create = useCreateMemory();
|
||||
const update = useUpdateMemory();
|
||||
const remove = useDeleteMemory();
|
||||
const archive = useArchiveMemory();
|
||||
const unarchive = useUnarchiveMemory();
|
||||
|
||||
return {
|
||||
createMemory: create.createMemory,
|
||||
updateMemory: update.updateMemory,
|
||||
deleteMemory: remove.deleteMemory,
|
||||
archiveMemory: archive.archiveMemory,
|
||||
unarchiveMemory: unarchive.unarchiveMemory,
|
||||
isCreating: create.isCreating,
|
||||
isUpdating: update.isUpdating,
|
||||
isDeleting: remove.isDeleting,
|
||||
isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
|
||||
isArchiving: archive.isArchiving,
|
||||
isUnarchiving: unarchive.isUnarchiving,
|
||||
isMutating: create.isCreating || update.isUpdating || remove.isDeleting || archive.isArchiving || unarchive.isUnarchiving,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
fetchPrompts,
|
||||
fetchPromptInsights,
|
||||
fetchInsightsHistory,
|
||||
analyzePrompts,
|
||||
deletePrompt,
|
||||
batchDeletePrompts,
|
||||
deleteInsight,
|
||||
type Prompt,
|
||||
type PromptInsight,
|
||||
type Pattern,
|
||||
type Suggestion,
|
||||
type PromptsResponse,
|
||||
type PromptInsightsResponse,
|
||||
type InsightsHistoryResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
@@ -24,6 +25,7 @@ export const promptHistoryKeys = {
|
||||
lists: () => [...promptHistoryKeys.all, 'list'] as const,
|
||||
list: (filters?: PromptHistoryFilter) => [...promptHistoryKeys.lists(), filters] as const,
|
||||
insights: () => [...promptHistoryKeys.all, 'insights'] as const,
|
||||
insightsHistory: () => [...promptHistoryKeys.all, 'insightsHistory'] as const,
|
||||
};
|
||||
|
||||
// Default stale time: 30 seconds (prompts update less frequently)
|
||||
@@ -32,6 +34,7 @@ const STALE_TIME = 30 * 1000;
|
||||
export interface PromptHistoryFilter {
|
||||
search?: string;
|
||||
intent?: string;
|
||||
project?: string;
|
||||
dateRange?: { start: Date | null; end: Date | null };
|
||||
}
|
||||
|
||||
@@ -43,12 +46,19 @@ export interface UsePromptHistoryOptions {
|
||||
|
||||
export interface UsePromptHistoryReturn {
|
||||
prompts: Prompt[];
|
||||
allPrompts: Prompt[];
|
||||
totalPrompts: number;
|
||||
promptsBySession: Record<string, Prompt[]>;
|
||||
stats: {
|
||||
totalCount: number;
|
||||
avgLength: number;
|
||||
topIntent: string | null;
|
||||
avgQualityScore?: number;
|
||||
qualityDistribution?: {
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
};
|
||||
};
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
@@ -96,6 +106,10 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
prompts = prompts.filter((p) => p.category === filter.intent);
|
||||
}
|
||||
|
||||
if (filter?.project) {
|
||||
prompts = prompts.filter((p) => p.project === filter.project);
|
||||
}
|
||||
|
||||
if (filter?.dateRange?.start || filter?.dateRange?.end) {
|
||||
prompts = prompts.filter((p) => {
|
||||
const date = new Date(p.createdAt);
|
||||
@@ -132,6 +146,34 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
}
|
||||
const topIntent = Object.entries(intentCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
|
||||
|
||||
// Calculate quality distribution
|
||||
const qualityDistribution = {
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
};
|
||||
let totalQualityScore = 0;
|
||||
let qualityScoreCount = 0;
|
||||
|
||||
for (const prompt of allPrompts) {
|
||||
if (prompt.quality_score !== undefined && prompt.quality_score !== null) {
|
||||
totalQualityScore += prompt.quality_score;
|
||||
qualityScoreCount++;
|
||||
|
||||
if (prompt.quality_score >= 80) {
|
||||
qualityDistribution.high++;
|
||||
} else if (prompt.quality_score >= 60) {
|
||||
qualityDistribution.medium++;
|
||||
} else {
|
||||
qualityDistribution.low++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avgQualityScore = qualityScoreCount > 0
|
||||
? totalQualityScore / qualityScoreCount
|
||||
: undefined;
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
@@ -142,12 +184,15 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
|
||||
return {
|
||||
prompts: filteredPrompts,
|
||||
allPrompts,
|
||||
totalPrompts: totalCount,
|
||||
promptsBySession,
|
||||
stats: {
|
||||
totalCount: allPrompts.length,
|
||||
avgLength,
|
||||
topIntent,
|
||||
avgQualityScore,
|
||||
qualityDistribution,
|
||||
},
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
@@ -157,6 +202,8 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hook for fetching prompt insights
|
||||
*/
|
||||
@@ -175,6 +222,28 @@ export function usePromptInsights(options: { enabled?: boolean; staleTime?: numb
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching insights history (past CLI analyses)
|
||||
*/
|
||||
export function useInsightsHistory(options: {
|
||||
limit?: number;
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
} = {}) {
|
||||
const { limit = 20, enabled = true, staleTime = STALE_TIME } = options;
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: promptHistoryKeys.insightsHistory(),
|
||||
queryFn: () => fetchInsightsHistory(projectPath, limit),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Mutations ==========
|
||||
|
||||
export interface UseAnalyzePromptsReturn {
|
||||
@@ -244,18 +313,120 @@ export function useDeletePrompt(): UseDeletePromptReturn {
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseBatchDeletePromptsReturn {
|
||||
batchDeletePrompts: (promptIds: string[]) => Promise<{ deleted: number }>;
|
||||
isBatchDeleting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useBatchDeletePrompts(): UseBatchDeletePromptsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: batchDeletePrompts,
|
||||
onMutate: async (promptIds) => {
|
||||
await queryClient.cancelQueries({ queryKey: promptHistoryKeys.all });
|
||||
const previousPrompts = queryClient.getQueryData<PromptsResponse>(promptHistoryKeys.list());
|
||||
|
||||
queryClient.setQueryData<PromptsResponse>(promptHistoryKeys.list(), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
prompts: old.prompts.filter((p) => !promptIds.includes(p.id)),
|
||||
total: old.total - promptIds.length,
|
||||
};
|
||||
});
|
||||
|
||||
return { previousPrompts };
|
||||
},
|
||||
onError: (_error, _promptIds, context) => {
|
||||
if (context?.previousPrompts) {
|
||||
queryClient.setQueryData(promptHistoryKeys.list(), context.previousPrompts);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
batchDeletePrompts: mutation.mutateAsync,
|
||||
isBatchDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeleteInsightReturn {
|
||||
deleteInsight: (insightId: string) => Promise<{ success: boolean }>;
|
||||
isDeleting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useDeleteInsight(): UseDeleteInsightReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (insightId: string) => deleteInsight(insightId, projectPath),
|
||||
onMutate: async (insightId) => {
|
||||
await queryClient.cancelQueries({ queryKey: promptHistoryKeys.insightsHistory() });
|
||||
const previousInsights = queryClient.getQueryData<InsightsHistoryResponse>(promptHistoryKeys.insightsHistory());
|
||||
|
||||
queryClient.setQueryData<InsightsHistoryResponse>(promptHistoryKeys.insightsHistory(), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
insights: old.insights.filter((i) => i.id !== insightId),
|
||||
};
|
||||
});
|
||||
|
||||
return { previousInsights };
|
||||
},
|
||||
onError: (_error, _insightId, context) => {
|
||||
if (context?.previousInsights) {
|
||||
queryClient.setQueryData(promptHistoryKeys.insightsHistory(), context.previousInsights);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: promptHistoryKeys.insightsHistory() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteInsight: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for all prompt history mutations
|
||||
*/
|
||||
export function usePromptHistoryMutations() {
|
||||
const analyze = useAnalyzePrompts();
|
||||
const remove = useDeletePrompt();
|
||||
const batchRemove = useBatchDeletePrompts();
|
||||
|
||||
return {
|
||||
analyzePrompts: analyze.analyzePrompts,
|
||||
deletePrompt: remove.deletePrompt,
|
||||
batchDeletePrompts: batchRemove.batchDeletePrompts,
|
||||
isAnalyzing: analyze.isAnalyzing,
|
||||
isDeleting: remove.isDeleting,
|
||||
isMutating: analyze.isAnalyzing || remove.isDeleting,
|
||||
isBatchDeleting: batchRemove.isBatchDeleting,
|
||||
isMutating: analyze.isAnalyzing || remove.isDeleting || batchRemove.isBatchDeleting,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract unique projects from prompts list
|
||||
*/
|
||||
export function extractUniqueProjects(prompts: Prompt[]): string[] {
|
||||
const projectsSet = new Set<string>();
|
||||
for (const prompt of prompts) {
|
||||
if (prompt.project) {
|
||||
projectsSet.add(prompt.project);
|
||||
}
|
||||
}
|
||||
return Array.from(projectsSet).sort();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNotificationStore } from '@/stores';
|
||||
import { useExecutionStore } from '@/stores/executionStore';
|
||||
import { useFlowStore } from '@/stores';
|
||||
import { useCliStreamStore } from '@/stores/cliStreamStore';
|
||||
import { useCoordinatorStore } from '@/stores/coordinatorStore';
|
||||
import {
|
||||
OrchestratorMessageSchema,
|
||||
type OrchestratorWebSocketMessage,
|
||||
@@ -60,6 +61,13 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
|
||||
// CLI stream store for CLI output handling
|
||||
const addOutput = useCliStreamStore((state) => state.addOutput);
|
||||
|
||||
// Coordinator store for coordinator state updates
|
||||
const updateNodeStatus = useCoordinatorStore((state) => state.updateNodeStatus);
|
||||
const addCoordinatorLog = useCoordinatorStore((state) => state.addLog);
|
||||
const setActiveQuestion = useCoordinatorStore((state) => state.setActiveQuestion);
|
||||
const markExecutionComplete = useCoordinatorStore((state) => state.markExecutionComplete);
|
||||
const coordinatorExecutionId = useCoordinatorStore((state) => state.currentExecutionId);
|
||||
|
||||
// Handle incoming WebSocket messages
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
@@ -143,6 +151,56 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Coordinator messages
|
||||
if (data.type?.startsWith('COORDINATOR_')) {
|
||||
// Only process messages for current coordinator execution
|
||||
if (coordinatorExecutionId && data.executionId !== coordinatorExecutionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch to coordinator store based on message type
|
||||
switch (data.type) {
|
||||
case 'COORDINATOR_STATE_UPDATE':
|
||||
// Check for completion
|
||||
if (data.status === 'completed') {
|
||||
markExecutionComplete(true);
|
||||
} else if (data.status === 'failed') {
|
||||
markExecutionComplete(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_COMMAND_STARTED':
|
||||
updateNodeStatus(data.nodeId, 'running');
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_COMMAND_COMPLETED':
|
||||
updateNodeStatus(data.nodeId, 'completed', data.result);
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_COMMAND_FAILED':
|
||||
updateNodeStatus(data.nodeId, 'failed', undefined, data.error);
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_LOG_ENTRY':
|
||||
addCoordinatorLog(
|
||||
data.log.message,
|
||||
data.log.level,
|
||||
data.log.nodeId,
|
||||
data.log.source
|
||||
);
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_QUESTION_ASKED':
|
||||
setActiveQuestion(data.question);
|
||||
break;
|
||||
|
||||
case 'COORDINATOR_ANSWER_RECEIVED':
|
||||
// Answer received - handled by submitAnswer in the store
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is an orchestrator message
|
||||
if (!data.type?.startsWith('ORCHESTRATOR_')) {
|
||||
return;
|
||||
@@ -210,6 +268,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
|
||||
},
|
||||
[
|
||||
currentExecution,
|
||||
coordinatorExecutionId,
|
||||
setWsLastMessage,
|
||||
setExecutionStatus,
|
||||
setNodeStarted,
|
||||
@@ -220,6 +279,10 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
|
||||
updateNode,
|
||||
addOutput,
|
||||
addA2UINotification,
|
||||
updateNodeStatus,
|
||||
addCoordinatorLog,
|
||||
setActiveQuestion,
|
||||
markExecutionComplete,
|
||||
onMessage,
|
||||
]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user