mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-14 17:41:22 +08:00
feat: Implement DeepWiki documentation generation tools
- Added `__init__.py` in `codexlens/tools` for documentation generation. - Created `deepwiki_generator.py` to handle symbol extraction and markdown generation. - Introduced `MockMarkdownGenerator` for testing purposes. - Implemented `DeepWikiGenerator` class for managing documentation generation and file processing. - Added unit tests for `DeepWikiStore` to ensure proper functionality and error handling. - Created tests for DeepWiki TypeScript types matching.
This commit is contained in:
80
ccw/frontend/src/hooks/__tests__/useCommands.ux.test.ts
Normal file
80
ccw/frontend/src/hooks/__tests__/useCommands.ux.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// ========================================
|
||||
// UX Tests: Error Handling in Hooks
|
||||
// ========================================
|
||||
// Tests for UX feedback patterns: error handling with toast notifications in hooks
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useCommands } from '../useCommands';
|
||||
import { useNotificationStore } from '../../stores/notificationStore';
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../lib/api', () => ({
|
||||
executeCommand: vi.fn(),
|
||||
deleteCommand: vi.fn(),
|
||||
createCommand: vi.fn(),
|
||||
updateCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('UX Pattern: Error Handling in useCommands Hook', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store state before each test
|
||||
useNotificationStore.setState({
|
||||
toasts: [],
|
||||
a2uiSurfaces: new Map(),
|
||||
currentQuestion: null,
|
||||
persistentNotifications: [],
|
||||
isPanelVisible: false,
|
||||
});
|
||||
localStorage.removeItem('ccw_notifications');
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Error notification on command execution failure', () => {
|
||||
it('should show error toast when command execution fails', async () => {
|
||||
const { executeCommand } = await import('../../lib/api');
|
||||
vi.mocked(executeCommand).mockRejectedValueOnce(new Error('Command failed'));
|
||||
|
||||
const { result } = renderHook(() => useCommands());
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.executeCommand('test-command', {});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
// Console error should be logged
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should sanitize error messages before showing to user', async () => {
|
||||
const { executeCommand } = await import('../../lib/api');
|
||||
const nastyError = new Error('Internal: Database connection failed at postgres://localhost:5432 with password=admin123');
|
||||
vi.mocked(executeCommand).mockRejectedValueOnce(nastyError);
|
||||
|
||||
const { result } = renderHook(() => useCommands());
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.executeCommand('test-command', {});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
// Full error logged to console
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Database connection failed'),
|
||||
nastyError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
250
ccw/frontend/src/hooks/__tests__/useNotifications.ux.test.ts
Normal file
250
ccw/frontend/src/hooks/__tests__/useNotifications.ux.test.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
// ========================================
|
||||
// UX Tests: useNotifications Hook
|
||||
// ========================================
|
||||
// Tests for UX feedback patterns: error/success/warning toast notifications
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useNotifications } from '../useNotifications';
|
||||
import { useNotificationStore } from '../../stores/notificationStore';
|
||||
|
||||
describe('UX Pattern: Toast Notifications (useNotifications)', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store state before each test
|
||||
useNotificationStore.setState({
|
||||
toasts: [],
|
||||
a2uiSurfaces: new Map(),
|
||||
currentQuestion: null,
|
||||
persistentNotifications: [],
|
||||
isPanelVisible: false,
|
||||
});
|
||||
localStorage.removeItem('ccw_notifications');
|
||||
});
|
||||
|
||||
describe('Error Notifications', () => {
|
||||
it('should add error toast with default persistent duration (0)', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.error('Operation Failed', 'Something went wrong');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]).toMatchObject({
|
||||
type: 'error',
|
||||
title: 'Operation Failed',
|
||||
message: 'Something went wrong',
|
||||
duration: 0, // Persistent by default for errors
|
||||
dismissible: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add error toast without message', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.error('Error Title');
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].title).toBe('Error Title');
|
||||
expect(result.current.toasts[0].message).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return toast ID for error notification', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
let toastId: string = '';
|
||||
act(() => {
|
||||
toastId = result.current.error('Error');
|
||||
});
|
||||
|
||||
expect(toastId).toBeDefined();
|
||||
expect(typeof toastId).toBe('string');
|
||||
expect(result.current.toasts[0].id).toBe(toastId);
|
||||
});
|
||||
|
||||
it('should preserve console logging alongside toast notifications', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.error('Sync failed', 'Network error occurred');
|
||||
});
|
||||
|
||||
// Toast notification added
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].type).toBe('error');
|
||||
|
||||
// Console logging should also be called (handled by caller)
|
||||
// This test verifies the hook doesn't interfere with console logging
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Success Notifications', () => {
|
||||
it('should add success toast', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.success('Success', 'Operation completed');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]).toMatchObject({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
message: 'Operation completed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add success toast without message', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.success('Created');
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0]).toMatchObject({
|
||||
type: 'success',
|
||||
title: 'Created',
|
||||
message: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Warning Notifications', () => {
|
||||
it('should add warning toast for partial success scenarios', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.warning('Partial Success', 'Issue created but attachments failed');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]).toMatchObject({
|
||||
type: 'warning',
|
||||
title: 'Partial Success',
|
||||
message: 'Issue created but attachments failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Info Notifications', () => {
|
||||
it('should add info toast', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.info('Information', 'Here is some info');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]).toMatchObject({
|
||||
type: 'info',
|
||||
title: 'Information',
|
||||
message: 'Here is some info',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toast Removal', () => {
|
||||
it('should remove toast by ID', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
let toastId: string = '';
|
||||
act(() => {
|
||||
toastId = result.current.error('Error');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
result.current.removeToast(toastId);
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should clear all toasts', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.success('Success 1');
|
||||
result.current.error('Error 1');
|
||||
result.current.warning('Warning 1');
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(3);
|
||||
|
||||
act(() => {
|
||||
result.current.clearAllToasts();
|
||||
});
|
||||
|
||||
expect(result.current.toasts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UX Pattern: Multiple toast types in sequence', () => {
|
||||
it('should handle issue creation workflow with success and partial success', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
// Simulate: Issue created successfully
|
||||
act(() => {
|
||||
result.current.success('Created', 'Issue created successfully');
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].type).toBe('success');
|
||||
|
||||
// Simulate: Attachment upload warning
|
||||
act(() => {
|
||||
result.current.warning('Partial Success', 'Issue created but attachments failed to upload');
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].type).toBe('warning');
|
||||
|
||||
// Simulate: Error case
|
||||
act(() => {
|
||||
result.current.error('Failed', 'Failed to create issue');
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].type).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UX Pattern: Toast options', () => {
|
||||
it('should support custom duration via addToast', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.addToast('info', 'Temporary', 'Will auto-dismiss', { duration: 3000 });
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].duration).toBe(3000);
|
||||
});
|
||||
|
||||
it('should support dismissible option', () => {
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.addToast('info', 'Info', 'Message', { dismissible: false });
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].dismissible).toBe(false);
|
||||
});
|
||||
|
||||
it('should support action button', () => {
|
||||
const mockAction = vi.fn();
|
||||
const { result } = renderHook(() => useNotifications());
|
||||
|
||||
act(() => {
|
||||
result.current.addToast('info', 'Info', 'Message', {
|
||||
action: { label: 'Retry', onClick: mockAction },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.toasts[0].action).toEqual({
|
||||
label: 'Retry',
|
||||
onClick: mockAction,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -222,6 +222,7 @@ export {
|
||||
cliInstallationsKeys,
|
||||
useHooks,
|
||||
useToggleHook,
|
||||
useDeleteHook,
|
||||
hooksKeys,
|
||||
useRules,
|
||||
useToggleRule,
|
||||
@@ -396,3 +397,27 @@ export type {
|
||||
SkillCacheResponse,
|
||||
SkillHubStats,
|
||||
} from './useSkillHub';
|
||||
|
||||
// ========== DeepWiki ==========
|
||||
export {
|
||||
useDeepWikiFiles,
|
||||
useDeepWikiDoc,
|
||||
useDeepWikiStats,
|
||||
useDeepWikiSearch,
|
||||
deepWikiKeys,
|
||||
} from './useDeepWiki';
|
||||
export type {
|
||||
DeepWikiFile,
|
||||
DeepWikiSymbol,
|
||||
DeepWikiDoc,
|
||||
DeepWikiStats,
|
||||
DocumentResponse,
|
||||
UseDeepWikiFilesOptions,
|
||||
UseDeepWikiFilesReturn,
|
||||
UseDeepWikiDocOptions,
|
||||
UseDeepWikiDocReturn,
|
||||
UseDeepWikiStatsOptions,
|
||||
UseDeepWikiStatsReturn,
|
||||
UseDeepWikiSearchOptions,
|
||||
UseDeepWikiSearchReturn,
|
||||
} from './useDeepWiki';
|
||||
|
||||
@@ -410,6 +410,7 @@ export function useUpgradeCliTool() {
|
||||
import {
|
||||
fetchHooks,
|
||||
toggleHook,
|
||||
deleteHook,
|
||||
type Hook,
|
||||
type HooksResponse,
|
||||
} from '../lib/api';
|
||||
@@ -511,6 +512,41 @@ export function useToggleHook() {
|
||||
};
|
||||
}
|
||||
|
||||
export function useDeleteHook() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (hookName: string) => deleteHook(hookName),
|
||||
onMutate: async (hookName) => {
|
||||
await queryClient.cancelQueries({ queryKey: hooksKeys.all });
|
||||
const previousHooks = queryClient.getQueryData<HooksResponse>(hooksKeys.lists());
|
||||
|
||||
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
hooks: old.hooks.filter((h) => h.name !== hookName),
|
||||
};
|
||||
});
|
||||
|
||||
return { previousHooks };
|
||||
},
|
||||
onError: (_error, _hookName, context) => {
|
||||
if (context?.previousHooks) {
|
||||
queryClient.setQueryData(hooksKeys.lists(), context.previousHooks);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: hooksKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteHook: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// useRules Hook
|
||||
// ========================================
|
||||
|
||||
272
ccw/frontend/src/hooks/useDeepWiki.ts
Normal file
272
ccw/frontend/src/hooks/useDeepWiki.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
// ========================================
|
||||
// useDeepWiki Hook
|
||||
// ========================================
|
||||
// TanStack Query hooks for DeepWiki documentation system
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Types
|
||||
export interface DeepWikiFile {
|
||||
id?: number;
|
||||
path: string;
|
||||
contentHash: string;
|
||||
lastIndexed: string;
|
||||
symbolsCount: number;
|
||||
docsGenerated: boolean;
|
||||
}
|
||||
|
||||
export interface DeepWikiSymbol {
|
||||
id?: number;
|
||||
name: string;
|
||||
type: string;
|
||||
sourceFile: string;
|
||||
docFile: string;
|
||||
anchor: string;
|
||||
lineRange: [number, number];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface DeepWikiDoc {
|
||||
id?: number;
|
||||
path: string;
|
||||
contentHash: string;
|
||||
symbols: string[];
|
||||
generatedAt: string;
|
||||
llmTool?: string;
|
||||
}
|
||||
|
||||
export interface DeepWikiStats {
|
||||
available: boolean;
|
||||
files: number;
|
||||
symbols: number;
|
||||
docs: number;
|
||||
filesNeedingDocs?: number;
|
||||
dbPath?: string;
|
||||
}
|
||||
|
||||
export interface DocumentResponse {
|
||||
doc: DeepWikiDoc | null;
|
||||
content: string;
|
||||
symbols: DeepWikiSymbol[];
|
||||
}
|
||||
|
||||
// Query key factory
|
||||
export const deepWikiKeys = {
|
||||
all: ['deepWiki'] as const,
|
||||
files: () => [...deepWikiKeys.all, 'files'] as const,
|
||||
doc: (path: string) => [...deepWikiKeys.all, 'doc', path] as const,
|
||||
stats: () => [...deepWikiKeys.all, 'stats'] as const,
|
||||
search: (query: string) => [...deepWikiKeys.all, 'search', query] as const,
|
||||
};
|
||||
|
||||
// Default stale time: 2 minutes
|
||||
const STALE_TIME = 2 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Fetch list of documented files
|
||||
*/
|
||||
async function fetchDeepWikiFiles(): Promise<DeepWikiFile[]> {
|
||||
const response = await fetch('/api/deepwiki/files');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch document by source file path
|
||||
*/
|
||||
async function fetchDeepWikiDoc(filePath: string): Promise<DocumentResponse> {
|
||||
const response = await fetch(`/api/deepwiki/doc?path=${encodeURIComponent(filePath)}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return { doc: null, content: '', symbols: [] };
|
||||
}
|
||||
throw new Error(`Failed to fetch document: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DeepWiki statistics
|
||||
*/
|
||||
async function fetchDeepWikiStats(): Promise<DeepWikiStats> {
|
||||
const response = await fetch('/api/deepwiki/stats');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search symbols by query
|
||||
*/
|
||||
async function searchDeepWikiSymbols(query: string, limit = 50): Promise<DeepWikiSymbol[]> {
|
||||
const response = await fetch(`/api/deepwiki/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to search symbols: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ========== Hooks ==========
|
||||
|
||||
export interface UseDeepWikiFilesOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseDeepWikiFilesReturn {
|
||||
files: DeepWikiFile[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching list of documented files
|
||||
*/
|
||||
export function useDeepWikiFiles(options: UseDeepWikiFilesOptions = {}): UseDeepWikiFilesReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: deepWikiKeys.files(),
|
||||
queryFn: fetchDeepWikiFiles,
|
||||
staleTime,
|
||||
enabled: enabled && !!projectPath,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
return {
|
||||
files: query.data ?? [],
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch: async () => {
|
||||
await query.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeepWikiDocOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseDeepWikiDocReturn {
|
||||
doc: DeepWikiDoc | null;
|
||||
content: string;
|
||||
symbols: DeepWikiSymbol[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching a document by source file path
|
||||
*/
|
||||
export function useDeepWikiDoc(filePath: string | null, options: UseDeepWikiDocOptions = {}): UseDeepWikiDocReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: deepWikiKeys.doc(filePath ?? ''),
|
||||
queryFn: () => fetchDeepWikiDoc(filePath!),
|
||||
staleTime,
|
||||
enabled: enabled && !!filePath,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
return {
|
||||
doc: query.data?.doc ?? null,
|
||||
content: query.data?.content ?? '',
|
||||
symbols: query.data?.symbols ?? [],
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch: async () => {
|
||||
await query.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeepWikiStatsOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseDeepWikiStatsReturn {
|
||||
stats: DeepWikiStats | null;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching DeepWiki statistics
|
||||
*/
|
||||
export function useDeepWikiStats(options: UseDeepWikiStatsOptions = {}): UseDeepWikiStatsReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: deepWikiKeys.stats(),
|
||||
queryFn: fetchDeepWikiStats,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
return {
|
||||
stats: query.data ?? null,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch: async () => {
|
||||
await query.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeepWikiSearchOptions {
|
||||
limit?: number;
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseDeepWikiSearchReturn {
|
||||
symbols: DeepWikiSymbol[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for searching symbols
|
||||
*/
|
||||
export function useDeepWikiSearch(query: string, options: UseDeepWikiSearchOptions = {}): UseDeepWikiSearchReturn {
|
||||
const { limit = 50, staleTime = STALE_TIME, enabled = true } = options;
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: deepWikiKeys.search(query),
|
||||
queryFn: () => searchDeepWikiSymbols(query, limit),
|
||||
staleTime,
|
||||
enabled: enabled && query.length > 0,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
return {
|
||||
symbols: queryResult.data ?? [],
|
||||
isLoading: queryResult.isLoading,
|
||||
isFetching: queryResult.isFetching,
|
||||
error: queryResult.error,
|
||||
refetch: async () => {
|
||||
await queryResult.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user