Files
Claude-Code-Workflow/ccw/frontend/src/pages/IssueHubPage.tsx
catlog22 4344e79e68 Add benchmark results for fast3 and fast4, implement KeepAliveLspBridge, and add tests for staged strategies
- Added new benchmark result files: compare_2026-02-09_score_fast3.json and compare_2026-02-09_score_fast4.json.
- Implemented KeepAliveLspBridge to maintain a persistent LSP connection across multiple queries, improving performance.
- Created unit tests for staged clustering strategies in test_staged_stage3_fast_strategies.py, ensuring correct behavior of score and dir_rr strategies.
2026-02-09 20:45:29 +08:00

227 lines
8.7 KiB
TypeScript

// ========================================
// Issue Hub Page
// ========================================
// Unified page for issues, queue, and discovery with tab navigation
import { useState, useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
Plus,
RefreshCw,
Github,
Loader2,
} from 'lucide-react';
import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs';
import { IssuesPanel } from '@/components/issue/hub/IssuesPanel';
import { IssueBoardPanel } from '@/components/issue/hub/IssueBoardPanel';
import { QueuePanel } from '@/components/issue/hub/QueuePanel';
import { DiscoveryPanel } from '@/components/issue/hub/DiscoveryPanel';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
import { useIssues, useIssueMutations, useIssueQueue } from '@/hooks';
import { pullIssuesFromGitHub } from '@/lib/api';
import type { Issue } from '@/lib/api';
import { cn } from '@/lib/utils';
function NewIssueDialog({ open, onOpenChange, onSubmit, isCreating }: {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: { title: string; context?: string; priority?: Issue['priority'] }) => void;
isCreating: boolean;
}) {
const { formatMessage } = useIntl();
const [title, setTitle] = useState('');
const [context, setContext] = useState('');
const [priority, setPriority] = useState<Issue['priority']>('medium');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (title.trim()) {
onSubmit({ title: title.trim(), context: context.trim() || undefined, priority });
setTitle('');
setContext('');
setPriority('medium');
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{formatMessage({ id: 'issues.createDialog.title' })}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.title' })}</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={formatMessage({ id: 'issues.createDialog.placeholders.title' })}
className="mt-1"
required
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.context' })}</label>
<textarea
value={context}
onChange={(e) => setContext(e.target.value)}
placeholder={formatMessage({ id: 'issues.createDialog.placeholders.context' })}
className="mt-1 w-full min-h-[100px] p-3 bg-background border border-input rounded-md text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.priority' })}</label>
<Select value={priority} onValueChange={(v) => setPriority(v as Issue['priority'])}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">{formatMessage({ id: 'issues.priority.low' })}</SelectItem>
<SelectItem value="medium">{formatMessage({ id: 'issues.priority.medium' })}</SelectItem>
<SelectItem value="high">{formatMessage({ id: 'issues.priority.high' })}</SelectItem>
<SelectItem value="critical">{formatMessage({ id: 'issues.priority.critical' })}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
{formatMessage({ id: 'issues.createDialog.buttons.cancel' })}
</Button>
<Button type="submit" disabled={isCreating || !title.trim()}>
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{formatMessage({ id: 'issues.createDialog.buttons.creating' })}
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'issues.createDialog.buttons.create' })}
</>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
export function IssueHubPage() {
const { formatMessage } = useIntl();
const [searchParams, setSearchParams] = useSearchParams();
const currentTab = (searchParams.get('tab') as IssueTab) || 'issues';
const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
const [isGithubSyncing, setIsGithubSyncing] = useState(false);
// Issues data
const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues();
// Queue data
const { refetch: refetchQueue, isFetching: isFetchingQueue } = useIssueQueue();
const { createIssue, isCreating } = useIssueMutations();
const setCurrentTab = (tab: IssueTab) => {
setSearchParams({ tab });
};
// Issues tab handlers
const handleIssuesRefresh = useCallback(() => {
refetchIssues();
}, [refetchIssues]);
const handleGithubSync = useCallback(async () => {
setIsGithubSyncing(true);
try {
const result = await pullIssuesFromGitHub({ state: 'open', limit: 100 });
console.log('GitHub sync result:', result);
await refetchIssues();
} catch (error) {
console.error('GitHub sync failed:', error);
} finally {
setIsGithubSyncing(false);
}
}, [refetchIssues]);
const handleCreateIssue = async (data: { title: string; context?: string; priority?: Issue['priority'] }) => {
await createIssue(data);
setIsNewIssueOpen(false);
};
// Queue tab handler
const handleQueueRefresh = useCallback(() => {
refetchQueue();
}, [refetchQueue]);
// Render action buttons based on current tab
const renderActionButtons = () => {
switch (currentTab) {
case 'issues':
case 'board':
return (
<>
<Button variant="outline" onClick={handleIssuesRefresh} disabled={isFetchingIssues}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetchingIssues && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button variant="outline" onClick={handleGithubSync} disabled={isGithubSyncing}>
<Github className={cn('w-4 h-4 mr-2', isGithubSyncing && 'animate-spin')} />
{formatMessage({ id: 'issues.actions.github' })}
</Button>
<Button onClick={() => setIsNewIssueOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'issues.actions.create' })}
</Button>
</>
);
case 'queue':
return (
<>
<Button variant="outline" onClick={handleQueueRefresh} disabled={isFetchingQueue}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetchingQueue && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
</>
);
case 'discovery':
return null; // Discovery panel has its own controls
default:
return null;
}
};
return (
<div className="space-y-6">
{/* Header and action buttons on same row */}
<div className="flex items-center justify-between">
<IssueHubHeader currentTab={currentTab} />
{/* Action buttons - dynamic based on current tab */}
{renderActionButtons() && (
<div className="flex gap-2">
{renderActionButtons()}
</div>
)}
</div>
<IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} />
{currentTab === 'issues' && <IssuesPanel onCreateIssue={() => setIsNewIssueOpen(true)} />}
{currentTab === 'board' && <IssueBoardPanel />}
{currentTab === 'queue' && <QueuePanel />}
{currentTab === 'discovery' && <DiscoveryPanel />}
<NewIssueDialog open={isNewIssueOpen} onOpenChange={setIsNewIssueOpen} onSubmit={handleCreateIssue} isCreating={isCreating} />
</div>
);
}
export default IssueHubPage;