mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add CLI Viewer Page with multi-pane layout and state management
- Implemented the CliViewerPage component for displaying CLI outputs in a configurable multi-pane layout. - Integrated Zustand for state management, allowing for dynamic layout changes and tab management. - Added layout options: single, split horizontal, split vertical, and 2x2 grid. - Created viewerStore for managing layout, panes, and tabs, including actions for adding/removing panes and tabs. - Added CoordinatorPage barrel export for easier imports.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import {
|
||||
ProviderList,
|
||||
ProviderModal,
|
||||
@@ -198,26 +198,21 @@ export function ApiSettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabbed Interface */}
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="providers">
|
||||
{formatMessage({ id: 'apiSettings.tabs.providers' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="endpoints">
|
||||
{formatMessage({ id: 'apiSettings.tabs.endpoints' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cache">
|
||||
{formatMessage({ id: 'apiSettings.tabs.cache' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="modelPools">
|
||||
{formatMessage({ id: 'apiSettings.tabs.modelPools' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cliSettings">
|
||||
{formatMessage({ id: 'apiSettings.tabs.cliSettings' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsNavigation
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as TabType)}
|
||||
tabs={[
|
||||
{ value: 'providers', label: formatMessage({ id: 'apiSettings.tabs.providers' }) },
|
||||
{ value: 'endpoints', label: formatMessage({ id: 'apiSettings.tabs.endpoints' }) },
|
||||
{ value: 'cache', label: formatMessage({ id: 'apiSettings.tabs.cache' }) },
|
||||
{ value: 'modelPools', label: formatMessage({ id: 'apiSettings.tabs.modelPools' }) },
|
||||
{ value: 'cliSettings', label: formatMessage({ id: 'apiSettings.tabs.cliSettings' }) },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TabsContent value="providers">
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'providers' && (
|
||||
<div className="mt-4">
|
||||
<ProviderList
|
||||
onAddProvider={handleAddProvider}
|
||||
onEditProvider={handleEditProvider}
|
||||
@@ -225,33 +220,41 @@ export function ApiSettingsPage() {
|
||||
onSyncToCodexLens={handleSyncToCodexLens}
|
||||
onManageModels={handleManageModels}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="endpoints">
|
||||
{activeTab === 'endpoints' && (
|
||||
<div className="mt-4">
|
||||
<EndpointList
|
||||
onAddEndpoint={handleAddEndpoint}
|
||||
onEditEndpoint={handleEditEndpoint}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="cache">
|
||||
{activeTab === 'cache' && (
|
||||
<div className="mt-4">
|
||||
<CacheSettings />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="modelPools">
|
||||
{activeTab === 'modelPools' && (
|
||||
<div className="mt-4">
|
||||
<ModelPoolList
|
||||
onAddPool={handleAddPool}
|
||||
onEditPool={handleEditPool}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="cliSettings">
|
||||
{activeTab === 'cliSettings' && (
|
||||
<div className="mt-4">
|
||||
<CliSettingsList
|
||||
onAddCliSettings={handleAddCliSettings}
|
||||
onEditCliSettings={handleEditCliSettings}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<ProviderModal
|
||||
|
||||
266
ccw/frontend/src/pages/CliViewerPage.tsx
Normal file
266
ccw/frontend/src/pages/CliViewerPage.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
// ========================================
|
||||
// CLI Viewer Page
|
||||
// ========================================
|
||||
// Multi-pane CLI output viewer with configurable layouts
|
||||
// Integrates with viewerStore for state management
|
||||
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Terminal,
|
||||
LayoutGrid,
|
||||
Columns,
|
||||
Rows,
|
||||
Square,
|
||||
ChevronDown,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/Dropdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LayoutContainer } from '@/components/cli-viewer';
|
||||
import {
|
||||
useViewerStore,
|
||||
useViewerLayout,
|
||||
useViewerPanes,
|
||||
useFocusedPaneId,
|
||||
type AllotmentLayout,
|
||||
} from '@/stores/viewerStore';
|
||||
|
||||
// ========================================
|
||||
// Types
|
||||
// ========================================
|
||||
|
||||
export type LayoutType = 'single' | 'split-h' | 'split-v' | 'grid-2x2';
|
||||
|
||||
interface LayoutOption {
|
||||
id: LayoutType;
|
||||
icon: React.ElementType;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Constants
|
||||
// ========================================
|
||||
|
||||
const LAYOUT_OPTIONS: LayoutOption[] = [
|
||||
{ id: 'single', icon: Square, labelKey: 'cliViewer.layout.single' },
|
||||
{ id: 'split-h', icon: Columns, labelKey: 'cliViewer.layout.splitH' },
|
||||
{ id: 'split-v', icon: Rows, labelKey: 'cliViewer.layout.splitV' },
|
||||
{ id: 'grid-2x2', icon: LayoutGrid, labelKey: 'cliViewer.layout.grid' },
|
||||
];
|
||||
|
||||
const DEFAULT_LAYOUT: LayoutType = 'split-h';
|
||||
|
||||
// ========================================
|
||||
// Helper Functions
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Detect layout type from AllotmentLayout structure
|
||||
*/
|
||||
function detectLayoutType(layout: AllotmentLayout): LayoutType {
|
||||
const childCount = layout.children.length;
|
||||
|
||||
// Empty or single pane
|
||||
if (childCount === 0 || childCount === 1) {
|
||||
return 'single';
|
||||
}
|
||||
|
||||
// Two panes at root level
|
||||
if (childCount === 2) {
|
||||
const hasNestedGroups = layout.children.some(
|
||||
(child) => typeof child !== 'string'
|
||||
);
|
||||
|
||||
// If no nested groups, it's a simple split
|
||||
if (!hasNestedGroups) {
|
||||
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
|
||||
}
|
||||
|
||||
// Check for grid layout (2x2)
|
||||
const allNested = layout.children.every(
|
||||
(child) => typeof child !== 'string'
|
||||
);
|
||||
if (allNested) {
|
||||
return 'grid-2x2';
|
||||
}
|
||||
}
|
||||
|
||||
// Default to current direction
|
||||
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total panes in layout
|
||||
*/
|
||||
function countPanes(layout: AllotmentLayout): number {
|
||||
let count = 0;
|
||||
const traverse = (children: (string | AllotmentLayout)[]) => {
|
||||
for (const child of children) {
|
||||
if (typeof child === 'string') {
|
||||
count++;
|
||||
} else {
|
||||
traverse(child.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(layout.children);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Main Component
|
||||
// ========================================
|
||||
|
||||
export function CliViewerPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Store hooks
|
||||
const layout = useViewerLayout();
|
||||
const panes = useViewerPanes();
|
||||
const focusedPaneId = useFocusedPaneId();
|
||||
const { initializeDefaultLayout, addTab, reset } = useViewerStore();
|
||||
|
||||
// Detect current layout type from store
|
||||
const currentLayoutType = useMemo(() => detectLayoutType(layout), [layout]);
|
||||
|
||||
// Count active sessions (tabs across all panes)
|
||||
const activeSessionCount = useMemo(() => {
|
||||
return Object.values(panes).reduce((count, pane) => count + pane.tabs.length, 0);
|
||||
}, [panes]);
|
||||
|
||||
// Initialize layout if empty
|
||||
useEffect(() => {
|
||||
const paneCount = countPanes(layout);
|
||||
if (paneCount === 0) {
|
||||
initializeDefaultLayout(DEFAULT_LAYOUT);
|
||||
}
|
||||
}, [layout, initializeDefaultLayout]);
|
||||
|
||||
// Handle executionId from URL params
|
||||
useEffect(() => {
|
||||
const executionId = searchParams.get('executionId');
|
||||
if (executionId && focusedPaneId) {
|
||||
// Add tab to focused pane
|
||||
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
|
||||
|
||||
// Clear the URL param after processing
|
||||
setSearchParams((prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.delete('executionId');
|
||||
return newParams;
|
||||
});
|
||||
}
|
||||
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
|
||||
|
||||
// Handle layout change
|
||||
const handleLayoutChange = useCallback(
|
||||
(layoutType: LayoutType) => {
|
||||
initializeDefaultLayout(layoutType);
|
||||
},
|
||||
[initializeDefaultLayout]
|
||||
);
|
||||
|
||||
// Handle reset
|
||||
const handleReset = useCallback(() => {
|
||||
reset();
|
||||
initializeDefaultLayout(DEFAULT_LAYOUT);
|
||||
}, [reset, initializeDefaultLayout]);
|
||||
|
||||
// Get current layout option for display
|
||||
const currentLayoutOption =
|
||||
LAYOUT_OPTIONS.find((l) => l.id === currentLayoutType) || LAYOUT_OPTIONS[1];
|
||||
const CurrentLayoutIcon = currentLayoutOption.icon;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col -m-4 md:-m-6">
|
||||
{/* ======================================== */}
|
||||
{/* Toolbar */}
|
||||
{/* ======================================== */}
|
||||
<div className="flex items-center justify-between gap-3 p-3 bg-card border-b border-border">
|
||||
{/* Page Title */}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Terminal className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliViewer.page.title' })}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage(
|
||||
{ id: 'cliViewer.page.subtitle' },
|
||||
{ count: activeSessionCount }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Reset Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
title={formatMessage({ id: 'cliViewer.toolbar.clearAll' })}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Layout Selector */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<CurrentLayoutIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{formatMessage({ id: currentLayoutOption.labelKey })}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
{formatMessage({ id: 'cliViewer.layout.title' })}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{LAYOUT_OPTIONS.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onClick={() => handleLayoutChange(option.id)}
|
||||
className={cn(
|
||||
'gap-2',
|
||||
currentLayoutType === option.id && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{formatMessage({ id: option.labelKey })}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ======================================== */}
|
||||
{/* Layout Container */}
|
||||
{/* ======================================== */}
|
||||
<div className="flex-1 min-h-0 bg-background">
|
||||
<LayoutContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CliViewerPage;
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
@@ -176,26 +176,21 @@ export function CodexLensManagerPage() {
|
||||
)}
|
||||
|
||||
{/* Tabbed Interface */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">
|
||||
{formatMessage({ id: 'codexlens.tabs.overview' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings">
|
||||
{formatMessage({ id: 'codexlens.tabs.settings' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="models">
|
||||
{formatMessage({ id: 'codexlens.tabs.models' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="search">
|
||||
{formatMessage({ id: 'codexlens.tabs.search' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="advanced">
|
||||
{formatMessage({ id: 'codexlens.tabs.advanced' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsNavigation
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
tabs={[
|
||||
{ value: 'overview', label: formatMessage({ id: 'codexlens.tabs.overview' }) },
|
||||
{ value: 'settings', label: formatMessage({ id: 'codexlens.tabs.settings' }) },
|
||||
{ value: 'models', label: formatMessage({ id: 'codexlens.tabs.models' }) },
|
||||
{ value: 'search', label: formatMessage({ id: 'codexlens.tabs.search' }) },
|
||||
{ value: 'advanced', label: formatMessage({ id: 'codexlens.tabs.advanced' }) },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TabsContent value="overview">
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="mt-4">
|
||||
<OverviewTab
|
||||
installed={installed}
|
||||
status={status}
|
||||
@@ -203,24 +198,32 @@ export function CodexLensManagerPage() {
|
||||
isLoading={isLoading}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="settings">
|
||||
{activeTab === 'settings' && (
|
||||
<div className="mt-4">
|
||||
<SettingsTab enabled={installed} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="models">
|
||||
{activeTab === 'models' && (
|
||||
<div className="mt-4">
|
||||
<ModelsTab installed={installed} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="search">
|
||||
{activeTab === 'search' && (
|
||||
<div className="mt-4">
|
||||
<SearchTab enabled={installed} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="advanced">
|
||||
{activeTab === 'advanced' && (
|
||||
<div className="mt-4">
|
||||
<AdvancedTab enabled={installed} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Semantic Install Dialog */}
|
||||
<SemanticInstallDialog
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Activity,
|
||||
Clock,
|
||||
@@ -16,10 +17,12 @@ import {
|
||||
ListTree,
|
||||
History,
|
||||
List,
|
||||
Monitor,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { ExecutionMonitor } from './orchestrator/ExecutionMonitor';
|
||||
import { useExecutionStore } from '@/stores/executionStore';
|
||||
import type { ExecutionStatus } from '@/types/execution';
|
||||
@@ -86,9 +89,14 @@ function formatDateTime(dateString: string): string {
|
||||
|
||||
export function ExecutionMonitorPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const currentExecution = useExecutionStore((state) => state.currentExecution);
|
||||
const [selectedView, setSelectedView] = useState<'workflow' | 'timeline' | 'list'>('workflow');
|
||||
|
||||
const handleOpenCliViewer = () => {
|
||||
navigate('/cli-viewer');
|
||||
};
|
||||
|
||||
// Calculate statistics
|
||||
const stats = useMemo(() => {
|
||||
const total = mockExecutionHistory.length;
|
||||
@@ -126,14 +134,20 @@ export function ExecutionMonitorPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
{formatMessage({ id: 'executionMonitor.page.title' })}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'executionMonitor.page.subtitle' })}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
{formatMessage({ id: 'executionMonitor.page.title' })}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'executionMonitor.page.subtitle' })}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleOpenCliViewer} className="gap-2">
|
||||
<Monitor className="w-4 h-4" />
|
||||
{formatMessage({ id: 'executionMonitor.actions.openCliViewer' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Current Execution Area */}
|
||||
@@ -230,24 +244,31 @@ export function ExecutionMonitorPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={selectedView} onValueChange={(v) => setSelectedView(v as typeof selectedView)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="workflow">
|
||||
<ListTree className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'executionMonitor.history.tabs.byWorkflow' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="timeline">
|
||||
<History className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'executionMonitor.history.tabs.timeline' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="list">
|
||||
<List className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'executionMonitor.history.tabs.list' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsNavigation
|
||||
value={selectedView}
|
||||
onValueChange={(v) => setSelectedView(v as typeof selectedView)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'workflow',
|
||||
label: formatMessage({ id: 'executionMonitor.history.tabs.byWorkflow' }),
|
||||
icon: <ListTree className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
value: 'timeline',
|
||||
label: formatMessage({ id: 'executionMonitor.history.tabs.timeline' }),
|
||||
icon: <History className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: formatMessage({ id: 'executionMonitor.history.tabs.list' }),
|
||||
icon: <List className="w-4 h-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* By Workflow View */}
|
||||
<TabsContent value="workflow" className="mt-4">
|
||||
{/* By Workflow View */}
|
||||
{selectedView === 'workflow' && (
|
||||
<div className="mt-4">
|
||||
{workflowGroups.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{formatMessage({ id: 'executionMonitor.history.empty' })}
|
||||
@@ -302,10 +323,11 @@ export function ExecutionMonitorPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline View */}
|
||||
<TabsContent value="timeline" className="mt-4">
|
||||
{/* Timeline View */}
|
||||
{selectedView === 'timeline' && (
|
||||
<div className="space-y-3">
|
||||
{mockExecutionHistory.map((exec, index) => (
|
||||
<div key={exec.execId} className="flex gap-4">
|
||||
@@ -359,11 +381,11 @@ export function ExecutionMonitorPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* List View */}
|
||||
<TabsContent value="list" className="mt-4">
|
||||
<div className="space-y-2">
|
||||
{/* List View */}
|
||||
{selectedView === 'list' && (
|
||||
<div className="space-y-2">
|
||||
{mockExecutionHistory.map((exec) => (
|
||||
<Card key={exec.execId} className="hover:border-primary/50 transition-colors">
|
||||
<CardContent className="p-4">
|
||||
@@ -402,9 +424,8 @@ export function ExecutionMonitorPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
// ========================================
|
||||
// HomePage Component
|
||||
// ========================================
|
||||
// Dashboard home page with stat cards and recent sessions
|
||||
// Dashboard home page with combined stats, workflow status, and activity heatmap
|
||||
|
||||
import * as React from 'react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
|
||||
import { DashboardGridContainer } from '@/components/dashboard/DashboardGridContainer';
|
||||
import { DetailedStatsWidget } from '@/components/dashboard/widgets/DetailedStatsWidget';
|
||||
import { WorkflowTaskWidget } from '@/components/dashboard/widgets/WorkflowTaskWidget';
|
||||
import { RecentSessionsWidget } from '@/components/dashboard/widgets/RecentSessionsWidget';
|
||||
import { ChartSkeleton } from '@/components/charts';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useUserDashboardLayout } from '@/hooks/useUserDashboardLayout';
|
||||
import { WIDGET_IDS } from '@/components/dashboard/defaultLayouts';
|
||||
|
||||
// Code-split chart widgets for better initial load performance
|
||||
const WorkflowStatusPieChartWidget = lazy(() => import('@/components/dashboard/widgets/WorkflowStatusPieChartWidget'));
|
||||
const ActivityLineChartWidget = lazy(() => import('@/components/dashboard/widgets/ActivityLineChartWidget'));
|
||||
const TaskTypeBarChartWidget = lazy(() => import('@/components/dashboard/widgets/TaskTypeBarChartWidget'));
|
||||
|
||||
/**
|
||||
* HomePage component - Dashboard overview with widget-based layout
|
||||
* HomePage component - Dashboard overview with fixed widget layout
|
||||
*/
|
||||
export function HomePage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const { resetLayout } = useUserDashboardLayout();
|
||||
|
||||
// Track errors from widgets (optional, for future enhancements)
|
||||
const [hasError, _setHasError] = React.useState(false);
|
||||
|
||||
const handleRefresh = () => {
|
||||
// Trigger refetch by reloading the page or using React Query's invalidateQueries
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleResetLayout = () => {
|
||||
resetLayout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -47,7 +31,6 @@ export function HomePage() {
|
||||
titleKey="home.dashboard.title"
|
||||
descriptionKey="home.dashboard.description"
|
||||
onRefresh={handleRefresh}
|
||||
onResetLayout={handleResetLayout}
|
||||
/>
|
||||
|
||||
{/* Error alert (optional, shown if widgets encounter critical errors) */}
|
||||
@@ -66,29 +49,14 @@ export function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dashboard Grid with Widgets */}
|
||||
<DashboardGridContainer isDraggable={true} isResizable={true}>
|
||||
{/* Widget 1: Detailed Stats */}
|
||||
<DetailedStatsWidget key={WIDGET_IDS.STATS} />
|
||||
{/* Dashboard Widgets - Simple flex layout for dynamic height */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Row 1: Combined Stats + Workflow Status + Task Details */}
|
||||
<WorkflowTaskWidget />
|
||||
|
||||
{/* Widget 2: Recent Sessions */}
|
||||
<RecentSessionsWidget key={WIDGET_IDS.RECENT_SESSIONS} />
|
||||
|
||||
{/* Widget 3: Workflow Status Pie Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="pie" height={280} />}>
|
||||
<WorkflowStatusPieChartWidget key={WIDGET_IDS.WORKFLOW_STATUS} />
|
||||
</Suspense>
|
||||
|
||||
{/* Widget 4: Activity Line Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="line" height={280} />}>
|
||||
<ActivityLineChartWidget key={WIDGET_IDS.ACTIVITY} />
|
||||
</Suspense>
|
||||
|
||||
{/* Widget 5: Task Type Bar Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="bar" height={280} />}>
|
||||
<TaskTypeBarChartWidget key={WIDGET_IDS.TASK_TYPES} />
|
||||
</Suspense>
|
||||
</DashboardGridContainer>
|
||||
{/* Row 2: Recent Sessions */}
|
||||
<RecentSessionsWidget />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ import { Flowchart } from '@/components/shared/Flowchart';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { Tabs, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/Collapsible';
|
||||
import type { LiteTask, LiteTaskSession } from '@/lib/api';
|
||||
|
||||
@@ -330,53 +331,68 @@ export function LiteTaskDetailPage() {
|
||||
|
||||
{/* Session Type-Specific Tabs */}
|
||||
{isMultiCli ? (
|
||||
<Tabs value={multiCliActiveTab} onValueChange={(v) => setMultiCliActiveTab(v as MultiCliTab)}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="tasks" className="flex-1 gap-1">
|
||||
<ListTodo className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="discussion" className="flex-1 gap-1">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.discussion' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="context" className="flex-1 gap-1">
|
||||
<Package className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.context' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="summary" className="flex-1 gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<TabsNavigation
|
||||
value={multiCliActiveTab}
|
||||
onValueChange={(v) => setMultiCliActiveTab(v as MultiCliTab)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'tasks',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.tasks' }),
|
||||
icon: <ListTodo className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'discussion',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.discussion' }),
|
||||
icon: <MessageSquare className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'context',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.context' }),
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'summary',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.summary' }),
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Tabs value={litePlanActiveTab} onValueChange={(v) => setLitePlanActiveTab(v as LitePlanTab)}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="tasks" className="flex-1 gap-1">
|
||||
<ListTodo className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="plan" className="flex-1 gap-1">
|
||||
<Ruler className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.plan' })}
|
||||
</TabsTrigger>
|
||||
{isLiteFix && (
|
||||
<TabsTrigger value="diagnoses" className="flex-1 gap-1">
|
||||
<Stethoscope className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.diagnoses' })}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="context" className="flex-1 gap-1">
|
||||
<Package className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.context' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="summary" className="flex-1 gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
{formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<TabsNavigation
|
||||
value={litePlanActiveTab}
|
||||
onValueChange={(v) => setLitePlanActiveTab(v as LitePlanTab)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'tasks',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.tasks' }),
|
||||
icon: <ListTodo className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'plan',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.plan' }),
|
||||
icon: <Ruler className="h-4 w-4" />,
|
||||
},
|
||||
...(isLiteFix
|
||||
? [
|
||||
{
|
||||
value: 'diagnoses' as const,
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.diagnoses' }),
|
||||
icon: <Stethoscope className="h-4 w-4" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
value: 'context',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.context' }),
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'summary',
|
||||
label: formatMessage({ id: 'liteTasksDetail.tabs.summary' }),
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task List with Multi-Tab Content */}
|
||||
@@ -390,15 +406,11 @@ export function LiteTaskDetailPage() {
|
||||
<Card key={taskId} className="overflow-hidden">
|
||||
{/* Task Header */}
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
{/* Left: Task ID, Title, Description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-base font-medium flex items-center gap-2 flex-wrap">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold bg-primary/10 text-primary border border-primary/20">{taskId}</span>
|
||||
<Badge
|
||||
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : 'secondary'}
|
||||
>
|
||||
{task.status}
|
||||
</Badge>
|
||||
{task.priority && (
|
||||
<Badge variant="outline" className="text-xs">{task.priority}</Badge>
|
||||
)}
|
||||
@@ -414,28 +426,77 @@ export function LiteTaskDetailPage() {
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">{task.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Meta Information */}
|
||||
<div className="flex flex-col items-end gap-2 text-xs text-muted-foreground flex-shrink-0">
|
||||
{/* Row 1: Status Badge */}
|
||||
<Badge
|
||||
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : task.status === 'blocked' ? 'destructive' : 'secondary'}
|
||||
className="w-fit"
|
||||
>
|
||||
{task.status}
|
||||
</Badge>
|
||||
|
||||
{/* Row 2: Metadata */}
|
||||
<div className="flex items-center gap-3 flex-wrap justify-end">
|
||||
{/* Dependencies Count */}
|
||||
{task.context?.depends_on && task.context.depends_on.length > 0 && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
||||
<span className="font-mono font-semibold text-foreground">{task.context.depends_on.length}</span>
|
||||
<span>dep{task.context.depends_on.length > 1 ? 's' : ''}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Target Files Count */}
|
||||
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
||||
<span className="font-mono font-semibold text-foreground">{task.flow_control.target_files.length}</span>
|
||||
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Focus Paths Count */}
|
||||
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
||||
<span className="font-mono font-semibold text-foreground">{task.context.focus_paths.length}</span>
|
||||
<span>focus</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Acceptance Criteria Count */}
|
||||
{task.context?.acceptance && task.context.acceptance.length > 0 && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
||||
<span className="font-mono font-semibold text-foreground">{task.context.acceptance.length}</span>
|
||||
<span>criteria</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* Multi-Tab Content */}
|
||||
<Tabs
|
||||
value={activeTaskTab}
|
||||
onValueChange={(v) => handleTaskTabChange(taskId, v as TaskTabValue)}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full rounded-none border-y border-border bg-muted/50 px-4">
|
||||
<TabsTrigger value="task" className="flex-1 gap-1.5">
|
||||
<ListTodo className="h-4 w-4" />
|
||||
Task
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="context" className="flex-1 gap-1.5">
|
||||
<Package className="h-4 w-4" />
|
||||
Context
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="w-full">
|
||||
<TabsNavigation
|
||||
value={activeTaskTab}
|
||||
onValueChange={(v) => handleTaskTabChange(taskId, v as TaskTabValue)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'task',
|
||||
label: 'Task',
|
||||
icon: <ListTodo className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'context',
|
||||
label: 'Context',
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Task Tab - Implementation Details */}
|
||||
<TabsContent value="task" className="p-4 space-y-4">
|
||||
{activeTaskTab === 'task' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Flowchart */}
|
||||
{hasFlowchart && task.flow_control && (
|
||||
<div>
|
||||
@@ -478,10 +539,12 @@ export function LiteTaskDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context Tab - Planning Context */}
|
||||
<TabsContent value="context" className="p-4 space-y-4">
|
||||
{activeTaskTab === 'context' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Focus Paths */}
|
||||
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
|
||||
<div>
|
||||
@@ -547,8 +610,9 @@ export function LiteTaskDetailPage() {
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -30,12 +30,16 @@ import {
|
||||
Stethoscope,
|
||||
FolderOpen,
|
||||
FileText,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { Tabs, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import { TaskDrawer } from '@/components/shared/TaskDrawer';
|
||||
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext } from '@/lib/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -482,6 +486,19 @@ export function LiteTasksPage() {
|
||||
const taskCount = session.tasks?.length || 0;
|
||||
const isExpanded = expandedSessionId === session.id;
|
||||
|
||||
// Calculate task status distribution
|
||||
const taskStats = React.useMemo(() => {
|
||||
const tasks = session.tasks || [];
|
||||
return {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
||||
};
|
||||
}, [session.tasks]);
|
||||
|
||||
const firstTask = session.tasks?.[0];
|
||||
|
||||
return (
|
||||
<div key={session.id}>
|
||||
<Card
|
||||
@@ -507,6 +524,43 @@ export function LiteTasksPage() {
|
||||
{formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Task preview - first task title */}
|
||||
{firstTask?.title && (
|
||||
<div className="mb-3 pb-3 border-b border-border/50">
|
||||
<p className="text-sm text-foreground line-clamp-1">{firstTask.title}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task status distribution */}
|
||||
<div className="flex items-center flex-wrap gap-2 mb-3">
|
||||
{taskStats.completed > 0 && (
|
||||
<Badge variant="success" className="gap-1 text-xs">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.inProgress > 0 && (
|
||||
<Badge variant="warning" className="gap-1 text-xs">
|
||||
<Clock className="h-3 w-3" />
|
||||
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.blocked > 0 && (
|
||||
<Badge variant="destructive" className="gap-1 text-xs">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.pending > 0 && (
|
||||
<Badge variant="secondary" className="gap-1 text-xs">
|
||||
<Activity className="h-3 w-3" />
|
||||
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date and task count */}
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
{session.createdAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -546,6 +600,18 @@ export function LiteTasksPage() {
|
||||
const status = latestSynthesis.status || session.status || 'analyzing';
|
||||
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
|
||||
|
||||
// Calculate task status distribution
|
||||
const taskStats = React.useMemo(() => {
|
||||
const tasks = session.tasks || [];
|
||||
return {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
||||
total: tasks.length,
|
||||
};
|
||||
}, [session.tasks]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={session.id}
|
||||
@@ -575,6 +641,37 @@ export function LiteTasksPage() {
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
<span className="line-clamp-1">{topicTitle}</span>
|
||||
</div>
|
||||
|
||||
{/* Task status distribution for multi-cli */}
|
||||
{taskStats.total > 0 && (
|
||||
<div className="flex items-center flex-wrap gap-2 mb-3">
|
||||
{taskStats.completed > 0 && (
|
||||
<Badge variant="success" className="gap-1 text-xs">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.inProgress > 0 && (
|
||||
<Badge variant="warning" className="gap-1 text-xs">
|
||||
<Clock className="h-3 w-3" />
|
||||
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.blocked > 0 && (
|
||||
<Badge variant="destructive" className="gap-1 text-xs">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
||||
</Badge>
|
||||
)}
|
||||
{taskStats.pending > 0 && (
|
||||
<Badge variant="secondary" className="gap-1 text-xs">
|
||||
<Activity className="h-3 w-3" />
|
||||
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{createdAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -651,30 +748,30 @@ export function LiteTasksPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as LiteTaskTab)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="lite-plan">
|
||||
<FileEdit className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'liteTasks.type.plan' })}
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{litePlan.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="lite-fix">
|
||||
<Wrench className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'liteTasks.type.fix' })}
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{liteFix.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="multi-cli-plan">
|
||||
<MessagesSquare className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'liteTasks.type.multiCli' })}
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{multiCliPlan.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsNavigation
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as LiteTaskTab)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'lite-plan',
|
||||
label: formatMessage({ id: 'liteTasks.type.plan' }),
|
||||
icon: <FileEdit className="h-4 w-4" />,
|
||||
badge: <Badge variant="secondary" className="ml-2">{litePlan.length}</Badge>,
|
||||
},
|
||||
{
|
||||
value: 'lite-fix',
|
||||
label: formatMessage({ id: 'liteTasks.type.fix' }),
|
||||
icon: <Wrench className="h-4 w-4" />,
|
||||
badge: <Badge variant="secondary" className="ml-2">{liteFix.length}</Badge>,
|
||||
},
|
||||
{
|
||||
value: 'multi-cli-plan',
|
||||
label: formatMessage({ id: 'liteTasks.type.multiCli' }),
|
||||
icon: <MessagesSquare className="h-4 w-4" />,
|
||||
badge: <Badge variant="secondary" className="ml-2">{multiCliPlan.length}</Badge>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Search and Sort Toolbar */}
|
||||
<div className="mt-4 flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
|
||||
@@ -729,86 +826,91 @@ export function LiteTasksPage() {
|
||||
</div>
|
||||
|
||||
{/* Lite Plan Tab */}
|
||||
<TabsContent value="lite-plan" className="mt-4">
|
||||
{litePlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredLitePlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredLitePlan.map(renderLiteTaskCard)}</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
{activeTab === 'lite-plan' && (
|
||||
<div className="mt-4">
|
||||
{litePlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredLitePlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredLitePlan.map(renderLiteTaskCard)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lite Fix Tab */}
|
||||
<TabsContent value="lite-fix" className="mt-4">
|
||||
{liteFix.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredLiteFix.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredLiteFix.map(renderLiteTaskCard)}</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
{activeTab === 'lite-fix' && (
|
||||
<div className="mt-4">
|
||||
{liteFix.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredLiteFix.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredLiteFix.map(renderLiteTaskCard)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-CLI Plan Tab */}
|
||||
<TabsContent value="multi-cli-plan" className="mt-4">
|
||||
{multiCliPlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredMultiCliPlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredMultiCliPlan.map(renderMultiCliCard)}</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{activeTab === 'multi-cli-plan' && (
|
||||
<div className="mt-4">
|
||||
{multiCliPlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.empty.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredMultiCliPlan.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Search className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'liteTasks.noResults.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'liteTasks.noResults.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">{filteredMultiCliPlan.map(renderMultiCliCard)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TaskDrawer */}
|
||||
<TaskDrawer
|
||||
|
||||
@@ -228,158 +228,161 @@ export function ProjectOverviewPage() {
|
||||
const { technologyStack, architecture, keyComponents, developmentIndex, guidelines, metadata } = projectOverview;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Project Header */}
|
||||
<div className="space-y-4">
|
||||
{/* Project Header + Technology Stack - Combined */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<CardContent className="p-4">
|
||||
{/* Header Row */}
|
||||
<div className="flex items-start justify-between mb-4 pb-3 border-b border-border">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">
|
||||
<h1 className="text-base font-semibold text-foreground mb-1">
|
||||
{projectOverview.projectName}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{projectOverview.description || formatMessage({ id: 'projectOverview.noDescription' })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground text-right">
|
||||
<div className="text-xs text-muted-foreground text-right">
|
||||
<div>
|
||||
{formatMessage({ id: 'projectOverview.header.initialized' })}:{' '}
|
||||
{formatDate(projectOverview.initializedAt)}
|
||||
</div>
|
||||
{metadata?.analysis_mode && (
|
||||
<div className="mt-1">
|
||||
<span className="font-mono text-xs px-2 py-0.5 bg-muted rounded">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 bg-muted rounded">
|
||||
{metadata.analysis_mode}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Technology Stack */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<Code2 className="w-5 h-5" />
|
||||
{/* Technology Stack */}
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
|
||||
<Code2 className="w-4 h-4" />
|
||||
{formatMessage({ id: 'projectOverview.techStack.title' })}
|
||||
</h3>
|
||||
|
||||
{/* Languages */}
|
||||
<div className="mb-5">
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
{formatMessage({ id: 'projectOverview.techStack.languages' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{technologyStack?.languages && technologyStack.languages.length > 0 ? (
|
||||
technologyStack.languages.map((lang: { name: string; file_count: number; primary?: boolean }) => (
|
||||
<div
|
||||
key={lang.name}
|
||||
className={`flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${
|
||||
lang.primary ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="font-semibold text-foreground">{lang.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{lang.file_count} files</span>
|
||||
{lang.primary && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">
|
||||
{formatMessage({ id: 'projectOverview.techStack.primary' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{formatMessage({ id: 'projectOverview.techStack.noLanguages' })}
|
||||
</span>
|
||||
)}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* Languages */}
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.techStack.languages' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{technologyStack?.languages && technologyStack.languages.length > 0 ? (
|
||||
technologyStack.languages.map((lang: { name: string; file_count: number; primary?: boolean }) => (
|
||||
<div
|
||||
key={lang.name}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 bg-background border border-border rounded text-xs ${
|
||||
lang.primary ? 'ring-1 ring-primary' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium text-foreground">{lang.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{lang.file_count}</span>
|
||||
{lang.primary && (
|
||||
<span className="text-[9px] px-1 py-0.5 bg-primary text-primary-foreground rounded">
|
||||
{formatMessage({ id: 'projectOverview.techStack.primary' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{formatMessage({ id: 'projectOverview.techStack.noLanguages' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frameworks */}
|
||||
<div className="mb-5">
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
{formatMessage({ id: 'projectOverview.techStack.frameworks' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{technologyStack?.frameworks && technologyStack.frameworks.length > 0 ? (
|
||||
technologyStack.frameworks.map((fw: string) => (
|
||||
<Badge key={fw} variant="success" className="px-3 py-1.5">
|
||||
{fw}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{formatMessage({ id: 'projectOverview.techStack.noFrameworks' })}
|
||||
</span>
|
||||
)}
|
||||
{/* Frameworks */}
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.techStack.frameworks' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{technologyStack?.frameworks && technologyStack.frameworks.length > 0 ? (
|
||||
technologyStack.frameworks.map((fw: string) => (
|
||||
<Badge key={fw} variant="success" className="px-2 py-0.5 text-[10px]">
|
||||
{fw}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{formatMessage({ id: 'projectOverview.techStack.noFrameworks' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Build Tools */}
|
||||
{technologyStack?.build_tools && technologyStack.build_tools.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
{/* Build Tools */}
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.techStack.buildTools' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{technologyStack.build_tools.map((tool: string) => (
|
||||
<Badge key={tool} variant="warning" className="px-3 py-1.5">
|
||||
{tool}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{technologyStack?.build_tools && technologyStack.build_tools.length > 0 ? (
|
||||
technologyStack.build_tools.map((tool: string) => (
|
||||
<Badge key={tool} variant="warning" className="px-2 py-0.5 text-[10px]">
|
||||
{tool}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Frameworks */}
|
||||
{technologyStack?.test_frameworks && technologyStack.test_frameworks.length > 0 && (
|
||||
{/* Test Frameworks */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.techStack.testFrameworks' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{technologyStack.test_frameworks.map((fw: string) => (
|
||||
<Badge key={fw} variant="default" className="px-3 py-1.5">
|
||||
{fw}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{technologyStack?.test_frameworks && technologyStack.test_frameworks.length > 0 ? (
|
||||
technologyStack.test_frameworks.map((fw: string) => (
|
||||
<Badge key={fw} variant="default" className="px-2 py-0.5 text-[10px]">
|
||||
{fw}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Architecture */}
|
||||
{architecture && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<Blocks className="w-5 h-5" />
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
|
||||
<Blocks className="w-4 h-4" />
|
||||
{formatMessage({ id: 'projectOverview.architecture.title' })}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Style */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.architecture.style' })}
|
||||
</h4>
|
||||
<div className="px-3 py-2 bg-background border border-border rounded-lg">
|
||||
<span className="text-foreground font-medium">{architecture.style}</span>
|
||||
<div className="px-2 py-1.5 bg-background border border-border rounded">
|
||||
<span className="text-foreground font-medium text-xs">{architecture.style}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layers */}
|
||||
{architecture.layers && architecture.layers.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.architecture.layers' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{architecture.layers.map((layer: string) => (
|
||||
<span key={layer} className="px-2 py-1 bg-muted text-foreground rounded text-sm">
|
||||
<span key={layer} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
|
||||
{layer}
|
||||
</span>
|
||||
))}
|
||||
@@ -390,12 +393,12 @@ export function ProjectOverviewPage() {
|
||||
{/* Patterns */}
|
||||
{architecture.patterns && architecture.patterns.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{architecture.patterns.map((pattern: string) => (
|
||||
<span key={pattern} className="px-2 py-1 bg-muted text-foreground rounded text-sm">
|
||||
<span key={pattern} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
|
||||
{pattern}
|
||||
</span>
|
||||
))}
|
||||
@@ -410,33 +413,33 @@ export function ProjectOverviewPage() {
|
||||
{/* Key Components */}
|
||||
{keyComponents && keyComponents.length > 0 && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<Component className="w-5 h-5" />
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
|
||||
<Component className="w-4 h-4" />
|
||||
{formatMessage({ id: 'projectOverview.components.title' })}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
{keyComponents.map((comp: KeyComponent) => {
|
||||
const importance = comp.importance || 'low';
|
||||
const importanceColors: Record<string, string> = {
|
||||
high: 'border-l-4 border-l-destructive bg-destructive/5',
|
||||
medium: 'border-l-4 border-l-warning bg-warning/5',
|
||||
low: 'border-l-4 border-l-muted-foreground bg-muted',
|
||||
high: 'border-l-2 border-l-destructive bg-destructive/5',
|
||||
medium: 'border-l-2 border-l-warning bg-warning/5',
|
||||
low: 'border-l-2 border-l-muted-foreground bg-muted',
|
||||
};
|
||||
const importanceBadges: Record<string, React.ReactElement> = {
|
||||
high: (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
|
||||
{formatMessage({ id: 'projectOverview.components.importance.high' })}
|
||||
</Badge>
|
||||
),
|
||||
medium: (
|
||||
<Badge variant="warning" className="text-xs">
|
||||
<Badge variant="warning" className="text-[10px] px-1.5 py-0">
|
||||
{formatMessage({ id: 'projectOverview.components.importance.medium' })}
|
||||
</Badge>
|
||||
),
|
||||
low: (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{formatMessage({ id: 'projectOverview.components.importance.low' })}
|
||||
</Badge>
|
||||
),
|
||||
@@ -445,17 +448,17 @@ export function ProjectOverviewPage() {
|
||||
return (
|
||||
<div
|
||||
key={comp.name}
|
||||
className={`p-4 rounded-lg ${importanceColors[importance] || importanceColors.low}`}
|
||||
className={`p-2.5 rounded ${importanceColors[importance] || importanceColors.low}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-semibold text-foreground">{comp.name}</h4>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h4 className="font-medium text-foreground text-xs">{comp.name}</h4>
|
||||
{importanceBadges[importance]}
|
||||
</div>
|
||||
{comp.description && (
|
||||
<p className="text-sm text-muted-foreground mb-2">{comp.description}</p>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">{comp.description}</p>
|
||||
)}
|
||||
{comp.responsibility && comp.responsibility.length > 0 && (
|
||||
<ul className="text-xs text-muted-foreground list-disc list-inside">
|
||||
<ul className="text-[10px] text-muted-foreground list-disc list-inside">
|
||||
{comp.responsibility.map((resp: string, i: number) => (
|
||||
<li key={i}>{resp}</li>
|
||||
))}
|
||||
@@ -472,20 +475,20 @@ export function ProjectOverviewPage() {
|
||||
{/* Development Index */}
|
||||
{developmentIndex && totalEntries > 0 && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<GitBranch className="w-5 h-5" />
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
{formatMessage({ id: 'projectOverview.devIndex.title' })}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{devIndexCategories.map((cat) => {
|
||||
const count = devIndexTotals[cat.key];
|
||||
if (count === 0) return null;
|
||||
const Icon = cat.icon;
|
||||
return (
|
||||
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'}>
|
||||
<Icon className="w-3 h-3 mr-1" />
|
||||
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'} className="text-[10px] px-1.5 py-0">
|
||||
<Icon className="w-2.5 h-2.5 mr-0.5" />
|
||||
{count}
|
||||
</Badge>
|
||||
);
|
||||
@@ -494,21 +497,21 @@ export function ProjectOverviewPage() {
|
||||
</div>
|
||||
|
||||
<Tabs value={devIndexView} onValueChange={(v) => setDevIndexView(v as DevIndexView)}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="category">
|
||||
<LayoutGrid className="w-3.5 h-3.5 mr-1" />
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<TabsList className="h-7">
|
||||
<TabsTrigger value="category" className="text-xs px-2 py-1 h-6">
|
||||
<LayoutGrid className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.devIndex.categories' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="timeline">
|
||||
<GitCommitHorizontal className="w-3.5 h-3.5 mr-1" />
|
||||
<TabsTrigger value="timeline" className="text-xs px-2 py-1 h-6">
|
||||
<GitCommitHorizontal className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.devIndex.timeline' })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="category">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{devIndexCategories.map((cat) => {
|
||||
const entries = developmentIndex?.[cat.key] || [];
|
||||
if (entries.length === 0) return null;
|
||||
@@ -516,38 +519,38 @@ export function ProjectOverviewPage() {
|
||||
|
||||
return (
|
||||
<div key={cat.key}>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<span>{formatMessage({ id: cat.i18nKey })}</span>
|
||||
<Badge variant="secondary">{entries.length}</Badge>
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0">{entries.length}</Badge>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{entries.slice(0, 5).map((entry: DevelopmentIndexEntry & { type?: string; typeLabel?: string; typeIcon?: React.ElementType; typeColor?: string; date?: string }, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow"
|
||||
className="p-2 bg-background border border-border rounded hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<div className="flex items-start justify-between mb-0.5">
|
||||
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{formatDate(entry.archivedAt || entry.date || entry.implemented_at)}
|
||||
</span>
|
||||
</div>
|
||||
{entry.description && (
|
||||
<p className="text-sm text-muted-foreground mb-1">{entry.description}</p>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
<div className="flex items-center gap-1.5 text-[10px] flex-wrap">
|
||||
{entry.sessionId && (
|
||||
<span className="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">
|
||||
<span className="px-1.5 py-0.5 bg-primary-light text-primary rounded font-mono">
|
||||
{entry.sessionId}
|
||||
</span>
|
||||
)}
|
||||
{entry.sub_feature && (
|
||||
<span className="px-2 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
|
||||
<span className="px-1.5 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
|
||||
)}
|
||||
{entry.status && (
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded ${
|
||||
className={`px-1.5 py-0.5 rounded ${
|
||||
entry.status === 'completed'
|
||||
? 'bg-success-light text-success'
|
||||
: 'bg-warning-light text-warning'
|
||||
@@ -560,7 +563,7 @@ export function ProjectOverviewPage() {
|
||||
</div>
|
||||
))}
|
||||
{entries.length > 5 && (
|
||||
<div className="text-sm text-muted-foreground text-center py-2">
|
||||
<div className="text-xs text-muted-foreground text-center py-1">
|
||||
... and {entries.length - 5} more
|
||||
</div>
|
||||
)}
|
||||
@@ -572,24 +575,24 @@ export function ProjectOverviewPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="timeline">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{allDevEntries.slice(0, 20).map((entry, i) => {
|
||||
const Icon = entry.typeIcon;
|
||||
return (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full bg-${entry.typeColor}-light text-${entry.typeColor} flex items-center justify-center`}
|
||||
className={`w-6 h-6 rounded-full bg-${entry.typeColor}-light text-${entry.typeColor} flex items-center justify-center`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<Icon className="w-3 h-3" />
|
||||
</div>
|
||||
{i < Math.min(allDevEntries.length, 20) - 1 && (
|
||||
<div className="w-0.5 flex-1 bg-border mt-2" />
|
||||
<div className="w-0.5 flex-1 bg-border mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pb-4">
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 pb-3">
|
||||
<div className="flex items-start justify-between mb-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={
|
||||
entry.typeColor === 'primary'
|
||||
@@ -598,31 +601,31 @@ export function ProjectOverviewPage() {
|
||||
? 'destructive'
|
||||
: 'secondary'
|
||||
}
|
||||
className="text-xs"
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{entry.typeLabel}
|
||||
</Badge>
|
||||
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
|
||||
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
<span className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{formatDate(entry.date)}
|
||||
</span>
|
||||
</div>
|
||||
{entry.description && (
|
||||
<p className="text-sm text-muted-foreground mb-2">{entry.description}</p>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-[10px]">
|
||||
{entry.sessionId && (
|
||||
<span className="px-2 py-0.5 bg-muted rounded font-mono">
|
||||
<span className="px-1.5 py-0.5 bg-muted rounded font-mono">
|
||||
{entry.sessionId}
|
||||
</span>
|
||||
)}
|
||||
{entry.sub_feature && (
|
||||
<span className="px-2 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
|
||||
<span className="px-1.5 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
|
||||
)}
|
||||
{entry.tags &&
|
||||
entry.tags.slice(0, 3).map((tag) => (
|
||||
<span key={tag} className="px-2 py-0.5 bg-accent rounded">
|
||||
<span key={tag} className="px-1.5 py-0.5 bg-accent rounded">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -632,7 +635,7 @@ export function ProjectOverviewPage() {
|
||||
);
|
||||
})}
|
||||
{allDevEntries.length > 20 && (
|
||||
<div className="text-sm text-muted-foreground text-center py-4">
|
||||
<div className="text-xs text-muted-foreground text-center py-2">
|
||||
... and {allDevEntries.length - 20} more entries
|
||||
</div>
|
||||
)}
|
||||
@@ -646,26 +649,26 @@ export function ProjectOverviewPage() {
|
||||
{/* Guidelines */}
|
||||
{guidelines && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<ScrollText className="w-5 h-5" />
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
|
||||
<ScrollText className="w-4 h-4" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
{!isEditMode ? (
|
||||
<Button variant="outline" size="sm" onClick={handleEditStart}>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditStart}>
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={handleEditCancel} disabled={isUpdating}>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditCancel} disabled={isUpdating}>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
|
||||
</Button>
|
||||
<Button variant="default" size="sm" onClick={handleSave} disabled={isUpdating}>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
<Button variant="default" size="sm" className="h-7 text-xs px-2" onClick={handleSave} disabled={isUpdating}>
|
||||
<Save className="w-3 h-3 mr-1" />
|
||||
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
|
||||
</Button>
|
||||
</>
|
||||
@@ -673,17 +676,17 @@ export function ProjectOverviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{!isEditMode ? (
|
||||
<>
|
||||
{/* Read-only Mode - Conventions */}
|
||||
{guidelines.conventions && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<BookMarked className="w-4 h-4" />
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||
<BookMarked className="w-3.5 h-3.5" />
|
||||
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{Object.entries(guidelines.conventions).map(([key, items]) => {
|
||||
const itemList = Array.isArray(items) ? items : [];
|
||||
if (itemList.length === 0) return null;
|
||||
@@ -692,12 +695,12 @@ export function ProjectOverviewPage() {
|
||||
{itemList.map((item: string, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
|
||||
>
|
||||
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
|
||||
{key}
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{item}</span>
|
||||
<span className="text-xs text-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -710,11 +713,11 @@ export function ProjectOverviewPage() {
|
||||
{/* Read-only Mode - Constraints */}
|
||||
{guidelines.constraints && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||
<ShieldAlert className="w-3.5 h-3.5" />
|
||||
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{Object.entries(guidelines.constraints).map(([key, items]) => {
|
||||
const itemList = Array.isArray(items) ? items : [];
|
||||
if (itemList.length === 0) return null;
|
||||
@@ -723,12 +726,12 @@ export function ProjectOverviewPage() {
|
||||
{itemList.map((item: string, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
|
||||
>
|
||||
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
|
||||
{key}
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{item}</span>
|
||||
<span className="text-xs text-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ========================================
|
||||
// ReviewSessionPage Component
|
||||
// ========================================
|
||||
// Review session detail page with findings display and multi-select
|
||||
// Review session detail page with findings display, multi-select, dimension tabs, and fix progress carousel
|
||||
|
||||
import * as React from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
Download,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronLeft as ChevronLeftIcon,
|
||||
ChevronRight as ChevronRightIcon,
|
||||
} from 'lucide-react';
|
||||
import { useReviewSession } from '@/hooks/useReviewSession';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -42,6 +44,229 @@ interface FindingWithSelection {
|
||||
impact?: string;
|
||||
}
|
||||
|
||||
// Fix Progress Types
|
||||
interface FixStage {
|
||||
stage: number;
|
||||
status: 'completed' | 'in-progress' | 'pending';
|
||||
groups: string[];
|
||||
}
|
||||
|
||||
interface FixProgressData {
|
||||
fix_session_id: string;
|
||||
phase: 'planning' | 'execution' | 'completion';
|
||||
total_findings: number;
|
||||
fixed_count: number;
|
||||
failed_count: number;
|
||||
in_progress_count: number;
|
||||
pending_count: number;
|
||||
percent_complete: number;
|
||||
current_stage: number;
|
||||
total_stages: number;
|
||||
stages: FixStage[];
|
||||
active_agents: Array<{
|
||||
agent_id: string;
|
||||
group_id: string;
|
||||
current_finding: { finding_title: string } | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix Progress Carousel Component
|
||||
* Displays fix progress with polling and carousel navigation
|
||||
*/
|
||||
function FixProgressCarousel({ sessionId }: { sessionId: string }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [fixProgressData, setFixProgressData] = React.useState<FixProgressData | null>(null);
|
||||
const [currentSlide, setCurrentSlide] = React.useState(0);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
// Fetch fix progress data
|
||||
const fetchFixProgress = React.useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/fix-progress?sessionId=${encodeURIComponent(sessionId)}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
setFixProgressData(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
setFixProgressData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch fix progress:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
// Poll for fix progress updates
|
||||
React.useEffect(() => {
|
||||
fetchFixProgress();
|
||||
|
||||
// Stop polling if phase is completion
|
||||
if (fixProgressData?.phase === 'completion') {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchFixProgress();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchFixProgress, fixProgressData?.phase]);
|
||||
|
||||
// Navigate carousel
|
||||
const navigateSlide = (direction: 'prev' | 'next' | number) => {
|
||||
if (!fixProgressData) return;
|
||||
|
||||
const totalSlides = fixProgressData.active_agents.length > 0 ? 3 : 2;
|
||||
if (typeof direction === 'number') {
|
||||
setCurrentSlide(direction);
|
||||
} else if (direction === 'next') {
|
||||
setCurrentSlide((prev) => (prev + 1) % totalSlides);
|
||||
} else if (direction === 'prev') {
|
||||
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !fixProgressData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="h-32 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!fixProgressData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { phase, total_findings, fixed_count, failed_count, in_progress_count, pending_count, percent_complete, current_stage, total_stages, stages, active_agents } = fixProgressData;
|
||||
|
||||
const phaseIcon = phase === 'planning' ? '📝' : phase === 'execution' ? '⚡' : '✅';
|
||||
const totalSlides = active_agents.length > 0 ? 3 : 2;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🔧</span>
|
||||
<span className="font-semibold text-sm">{formatMessage({ id: 'reviewSession.fixProgress.title' })}</span>
|
||||
</div>
|
||||
{/* Stage Dots */}
|
||||
<div className="flex gap-1">
|
||||
{stages.map((stage, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
stage.status === 'completed' ? 'bg-green-500' :
|
||||
stage.status === 'in-progress' ? 'bg-blue-500' :
|
||||
'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
title={`Stage ${i + 1}: ${stage.status}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
<div className="overflow-hidden">
|
||||
<div
|
||||
className="flex transition-transform duration-300 ease-in-out"
|
||||
style={{ transform: `translateX(-${currentSlide * 100}%)` }}
|
||||
>
|
||||
{/* Slide 1: Overview */}
|
||||
<div className="w-full flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge variant={phase === 'planning' ? 'secondary' : phase === 'execution' ? 'default' : 'success'}>
|
||||
{phaseIcon} {formatMessage({ id: `reviewSession.fixProgress.phase.${phase}` })}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">{fixProgressData.fix_session_id}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${percent_complete}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{formatMessage({ id: 'reviewSession.fixProgress.complete' }, { percent: percent_complete.toFixed(0) })} · {formatMessage({ id: 'reviewSession.fixProgress.stage' })} {current_stage}/{total_stages}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slide 2: Stats */}
|
||||
<div className="w-full flex-shrink-0">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="text-lg font-bold">{total_findings}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.total' })}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-green-100 dark:bg-green-900/20 rounded">
|
||||
<div className="text-lg font-bold text-green-600 dark:text-green-400">{fixed_count}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.fixed' })}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-red-100 dark:bg-red-900/20 rounded">
|
||||
<div className="text-lg font-bold text-red-600 dark:text-red-400">{failed_count}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.failed' })}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-yellow-100 dark:bg-yellow-900/20 rounded">
|
||||
<div className="text-lg font-bold text-yellow-600 dark:text-yellow-400">{pending_count + in_progress_count}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.pending' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slide 3: Active Agents (if any) */}
|
||||
{active_agents.length > 0 && (
|
||||
<div className="w-full flex-shrink-0">
|
||||
<div className="text-sm font-semibold mb-2">
|
||||
{active_agents.length} {active_agents.length === 1 ? formatMessage({ id: 'reviewSession.fixProgress.activeAgents' }) : formatMessage({ id: 'reviewSession.fixProgress.activeAgentsPlural' })}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{active_agents.slice(0, 2).map((agent, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 bg-muted rounded">
|
||||
<span>🤖</span>
|
||||
<span className="text-sm">{agent.current_finding?.finding_title || formatMessage({ id: 'reviewSession.fixProgress.working' })}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carousel Navigation */}
|
||||
{totalSlides > 1 && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => navigateSlide('prev')}>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: totalSlides }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
currentSlide === i ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
onClick={() => navigateSlide(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigateSlide('next')}>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReviewSessionPage component - Display review session findings
|
||||
*/
|
||||
@@ -61,11 +286,13 @@ export function ReviewSessionPage() {
|
||||
const [severityFilter, setSeverityFilter] = React.useState<Set<SeverityFilter>>(
|
||||
new Set(['critical', 'high', 'medium', 'low'])
|
||||
);
|
||||
const [dimensionFilter, setDimensionFilter] = React.useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [sortField, setSortField] = React.useState<SortField>('severity');
|
||||
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
||||
const [selectedFindings, setSelectedFindings] = React.useState<Set<string>>(new Set());
|
||||
const [expandedFindings, setExpandedFindings] = React.useState<Set<string>>(new Set());
|
||||
const [selectedFindingId, setSelectedFindingId] = React.useState<string | null>(null);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/sessions');
|
||||
@@ -83,6 +310,12 @@ export function ReviewSessionPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setSeverityFilter(new Set(['critical', 'high', 'medium', 'low']));
|
||||
setDimensionFilter('all');
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
const toggleSelectFinding = (findingId: string) => {
|
||||
setSelectedFindings(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -104,6 +337,22 @@ export function ReviewSessionPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const selectVisibleFindings = () => {
|
||||
const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined);
|
||||
setSelectedFindings(new Set(validIds));
|
||||
};
|
||||
|
||||
const selectBySeverity = (severity: FindingWithSelection['severity']) => {
|
||||
const criticalIds = flattenedFindings
|
||||
.filter(f => f.severity === severity && f.id !== undefined)
|
||||
.map(f => f.id!);
|
||||
setSelectedFindings(prev => {
|
||||
const next = new Set(prev);
|
||||
criticalIds.forEach(id => next.add(id));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleExpandFinding = (findingId: string) => {
|
||||
setExpandedFindings(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -116,6 +365,10 @@ export function ReviewSessionPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleFindingClick = (findingId: string) => {
|
||||
setSelectedFindingId(findingId);
|
||||
};
|
||||
|
||||
const exportSelectedAsJson = () => {
|
||||
const selected = flattenedFindings.filter(f => f.id !== undefined && selectedFindings.has(f.id));
|
||||
if (selected.length === 0) return;
|
||||
@@ -148,12 +401,26 @@ export function ReviewSessionPage() {
|
||||
// Severity order for sorting
|
||||
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
|
||||
// Calculate dimension counts
|
||||
const dimensionCounts = React.useMemo(() => {
|
||||
const counts: Record<string, number> = { all: flattenedFindings.length };
|
||||
flattenedFindings.forEach(f => {
|
||||
counts[f.dimension] = (counts[f.dimension] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}, [flattenedFindings]);
|
||||
|
||||
// Filter and sort findings
|
||||
const filteredFindings = React.useMemo(() => {
|
||||
let filtered = flattenedFindings;
|
||||
|
||||
// Apply dimension filter
|
||||
if (dimensionFilter !== 'all') {
|
||||
filtered = filtered.filter(f => f.dimension === dimensionFilter);
|
||||
}
|
||||
|
||||
// Apply severity filter
|
||||
if (severityFilter.size > 0 && !severityFilter.has('all' as SeverityFilter)) {
|
||||
if (severityFilter.size > 0) {
|
||||
filtered = filtered.filter(f => severityFilter.has(f.severity));
|
||||
}
|
||||
|
||||
@@ -186,7 +453,7 @@ export function ReviewSessionPage() {
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [flattenedFindings, severityFilter, searchQuery, sortField, sortOrder]);
|
||||
}, [flattenedFindings, severityFilter, dimensionFilter, searchQuery, sortField, sortOrder]);
|
||||
|
||||
// Get severity badge props
|
||||
const getSeverityBadge = (severity: FindingWithSelection['severity']) => {
|
||||
@@ -256,6 +523,11 @@ export function ReviewSessionPage() {
|
||||
const dimensions = reviewSession.reviewDimensions || [];
|
||||
const totalFindings = flattenedFindings.length;
|
||||
|
||||
// Determine session status (ACTIVE or ARCHIVED)
|
||||
const isActive = reviewSession._isActive !== false;
|
||||
const sessionStatus = isActive ? 'ACTIVE' : 'ARCHIVED';
|
||||
const phase = reviewSession.phase || 'in-progress';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -266,65 +538,99 @@ export function ReviewSessionPage() {
|
||||
{formatMessage({ id: 'common.actions.back' })}
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">
|
||||
{formatMessage({ id: 'reviewSession.title' })}
|
||||
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||
🔍 {reviewSession.session_id}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">{reviewSession.session_id}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="review">Review</Badge>
|
||||
<Badge variant={isActive ? "success" : "secondary"} className="text-xs">
|
||||
{sessionStatus}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="info">
|
||||
{formatMessage({ id: 'reviewSession.type' })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-foreground">{totalFindings}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.total' })}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-destructive">{severityCounts.critical}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.severity.critical' })}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-warning">{severityCounts.high}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.severity.high' })}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-2xl font-bold text-foreground">{dimensions.length}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.dimensions' })}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Review Progress Section */}
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
{/* Review Progress Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">📊</span>
|
||||
<span className="font-semibold">{formatMessage({ id: 'reviewSession.progress.title' })}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{phase.toUpperCase()}</Badge>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="flex items-center gap-3 p-3 bg-muted rounded-lg">
|
||||
<span className="text-2xl">📊</span>
|
||||
<div>
|
||||
<div className="text-lg font-bold">{totalFindings}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.totalFindings' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-red-100 dark:bg-red-900/20 rounded-lg">
|
||||
<span className="text-2xl">🔴</span>
|
||||
<div>
|
||||
<div className="text-lg font-bold text-red-600 dark:text-red-400">{severityCounts.critical}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.critical' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-orange-100 dark:bg-orange-900/20 rounded-lg">
|
||||
<span className="text-2xl">🟠</span>
|
||||
<div>
|
||||
<div className="text-lg font-bold text-orange-600 dark:text-orange-400">{severityCounts.high}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.high' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||
<span className="text-2xl">🎯</span>
|
||||
<div>
|
||||
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">{dimensions.length}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.dimensions' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Fix Progress Carousel */}
|
||||
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
|
||||
|
||||
{/* Filters and Controls */}
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
{/* Severity Filters */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
|
||||
const isEnabled = severityFilter.has(severity);
|
||||
const badge = getSeverityBadge(severity);
|
||||
return (
|
||||
<Badge
|
||||
key={severity}
|
||||
variant={isEnabled ? badge.variant : 'outline'}
|
||||
className={`cursor-pointer ${isEnabled ? '' : 'opacity-50'}`}
|
||||
onClick={() => toggleSeverity(severity)}
|
||||
>
|
||||
<badge.icon className="h-3 w-3 mr-1" />
|
||||
{badge.label}: {severityCounts[severity]}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
{/* Checkbox-style Severity Filters */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium">{formatMessage({ id: 'reviewSession.filters.severity' })}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
|
||||
const isEnabled = severityFilter.has(severity);
|
||||
return (
|
||||
<label
|
||||
key={severity}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full border cursor-pointer transition-colors ${
|
||||
isEnabled
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isEnabled}
|
||||
onChange={() => toggleSeverity(severity)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{formatMessage({ id: `reviewSession.severity.${severity}` })}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Sort */}
|
||||
@@ -355,6 +661,9 @@ export function ReviewSessionPage() {
|
||||
>
|
||||
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={resetFilters}>
|
||||
{formatMessage({ id: 'reviewSession.filters.reset' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Selection Controls */}
|
||||
@@ -368,6 +677,12 @@ export function ReviewSessionPage() {
|
||||
? formatMessage({ id: 'reviewSession.selection.clearAll' })
|
||||
: formatMessage({ id: 'reviewSession.selection.selectAll' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={selectVisibleFindings}>
|
||||
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')}>
|
||||
{formatMessage({ id: 'reviewSession.selection.selectCritical' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -384,12 +699,39 @@ export function ReviewSessionPage() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{formatMessage({ id: 'reviewSession.export' })}
|
||||
🔧 {formatMessage({ id: 'reviewSession.export' })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dimension Tabs */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
dimensionFilter === 'all'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
}`}
|
||||
onClick={() => setDimensionFilter('all')}
|
||||
>
|
||||
{formatMessage({ id: 'reviewSession.dimensionTabs.all' })} ({dimensionCounts.all || 0})
|
||||
</button>
|
||||
{dimensions.map(dim => (
|
||||
<button
|
||||
key={dim.name}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
dimensionFilter === dim.name
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
}`}
|
||||
onClick={() => setDimensionFilter(dim.name)}
|
||||
>
|
||||
{dim.name} ({dim.findings?.length || 0})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Findings List */}
|
||||
{filteredFindings.length === 0 ? (
|
||||
<Card>
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ReviewTab } from './session-detail/ReviewTab';
|
||||
import { TaskDrawer } from '@/components/shared/TaskDrawer';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import type { TaskData } from '@/types/store';
|
||||
|
||||
type TabValue = 'tasks' | 'context' | 'summary' | 'impl-plan' | 'conflict' | 'review';
|
||||
@@ -103,6 +103,43 @@ export function SessionDetailPage() {
|
||||
const completedTasks = tasks.filter((t) => t.status === 'completed').length;
|
||||
const hasReview = session.has_review || session.review;
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
value: 'tasks',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.tasks' }),
|
||||
icon: <ListChecks className="h-4 w-4" />,
|
||||
badge: <Badge variant="secondary" className="ml-2">{tasks.length}</Badge>,
|
||||
},
|
||||
{
|
||||
value: 'context',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.context' }),
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'summary',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.summary' }),
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'impl-plan',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.implPlan' }),
|
||||
icon: <Ruler className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'conflict',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.conflict' }),
|
||||
icon: <Scale className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
if (hasReview) {
|
||||
tabs.push({
|
||||
value: 'review',
|
||||
label: formatMessage({ id: 'sessionDetail.tabs.review' }),
|
||||
icon: <Search className="h-4 w-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -148,65 +185,48 @@ export function SessionDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabValue)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="tasks">
|
||||
<ListChecks className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.tasks' })}
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{tasks.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="context">
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.context' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="summary">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.summary' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="impl-plan">
|
||||
<Ruler className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.implPlan' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="conflict">
|
||||
<Scale className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.conflict' })}
|
||||
</TabsTrigger>
|
||||
{hasReview && (
|
||||
<TabsTrigger value="review">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'sessionDetail.tabs.review' })}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<TabsNavigation
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as TabValue)}
|
||||
tabs={tabs}
|
||||
/>
|
||||
|
||||
<TabsContent value="tasks" className="mt-4">
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="mt-4">
|
||||
<TaskListTab session={session} onTaskClick={setSelectedTask} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="context" className="mt-4">
|
||||
{activeTab === 'context' && (
|
||||
<div className="mt-4">
|
||||
<ContextTab context={context} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="summary" className="mt-4">
|
||||
{activeTab === 'summary' && (
|
||||
<div className="mt-4">
|
||||
<SummaryTab summary={summary} summaries={summaries} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="impl-plan" className="mt-4">
|
||||
{activeTab === 'impl-plan' && (
|
||||
<div className="mt-4">
|
||||
<ImplPlanTab implPlan={implPlan} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="conflict" className="mt-4">
|
||||
{activeTab === 'conflict' && (
|
||||
<div className="mt-4">
|
||||
<ConflictTab conflicts={conflicts as any} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasReview && (
|
||||
<TabsContent value="review" className="mt-4">
|
||||
<ReviewTab review={review as any} />
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
{hasReview && activeTab === 'review' && (
|
||||
<div className="mt-4">
|
||||
<ReviewTab review={review as any} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description (if exists) */}
|
||||
{session.description && (
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
} from '@/components/ui/Dropdown';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SessionMetadata } from '@/types/store';
|
||||
|
||||
@@ -174,13 +174,15 @@ export function SessionsPage() {
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
{/* Location tabs */}
|
||||
<Tabs value={locationFilter} onValueChange={(v) => setLocationFilter(v as LocationFilter)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="active">{formatMessage({ id: 'sessions.filters.active' })}</TabsTrigger>
|
||||
<TabsTrigger value="archived">{formatMessage({ id: 'sessions.filters.archived' })}</TabsTrigger>
|
||||
<TabsTrigger value="all">{formatMessage({ id: 'sessions.filters.all' })}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<TabsNavigation
|
||||
value={locationFilter}
|
||||
onValueChange={(v) => setLocationFilter(v as LocationFilter)}
|
||||
tabs={[
|
||||
{ value: 'active', label: formatMessage({ id: 'sessions.filters.active' }) },
|
||||
{ value: 'archived', label: formatMessage({ id: 'sessions.filters.archived' }) },
|
||||
{ value: 'all', label: formatMessage({ id: 'sessions.filters.all' }) },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="flex-1 max-w-sm relative">
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// ========================================
|
||||
// Coordinator Page
|
||||
// Coordinator Page - Merged Layout
|
||||
// ========================================
|
||||
// Page for monitoring and managing coordinator workflow execution with timeline, logs, and node details
|
||||
// Unified page for task list overview and execution details with timeline, logs, and node details
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Play } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Play, CheckCircle2, XCircle, Clock, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
CoordinatorInputModal,
|
||||
CoordinatorTimeline,
|
||||
CoordinatorLogStream,
|
||||
NodeDetailsPanel,
|
||||
CoordinatorEmptyState,
|
||||
} from '@/components/coordinator';
|
||||
import {
|
||||
useCoordinatorStore,
|
||||
@@ -21,11 +21,164 @@ import {
|
||||
selectCoordinatorStatus,
|
||||
selectIsPipelineLoaded,
|
||||
} from '@/stores/coordinatorStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========================================
|
||||
// Types
|
||||
// ========================================
|
||||
|
||||
interface CoordinatorTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||||
progress: {
|
||||
completed: number;
|
||||
total: number;
|
||||
};
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Mock Data (temporary - will be replaced by store)
|
||||
// ========================================
|
||||
|
||||
const MOCK_TASKS: CoordinatorTask[] = [
|
||||
{
|
||||
id: 'task-1',
|
||||
name: 'Feature Auth',
|
||||
status: 'running',
|
||||
progress: { completed: 3, total: 5 },
|
||||
startedAt: '2026-02-03T14:23:00Z',
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
name: 'API Integration',
|
||||
status: 'completed',
|
||||
progress: { completed: 8, total: 8 },
|
||||
startedAt: '2026-02-03T10:00:00Z',
|
||||
completedAt: '2026-02-03T10:15:00Z',
|
||||
},
|
||||
{
|
||||
id: 'task-3',
|
||||
name: 'Performance Test',
|
||||
status: 'failed',
|
||||
progress: { completed: 2, total: 6 },
|
||||
startedAt: '2026-02-03T09:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ========================================
|
||||
// Task Card Component (inline)
|
||||
// ========================================
|
||||
|
||||
interface TaskCardProps {
|
||||
task: CoordinatorTask;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function TaskCard({ task, isSelected, onClick }: TaskCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const statusConfig = {
|
||||
pending: {
|
||||
icon: Clock,
|
||||
color: 'text-muted-foreground',
|
||||
bg: 'bg-muted/50',
|
||||
},
|
||||
running: {
|
||||
icon: Loader2,
|
||||
color: 'text-blue-500',
|
||||
bg: 'bg-blue-500/10',
|
||||
},
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
color: 'text-green-500',
|
||||
bg: 'bg-green-500/10',
|
||||
},
|
||||
failed: {
|
||||
icon: XCircle,
|
||||
color: 'text-red-500',
|
||||
bg: 'bg-red-500/10',
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[task.status];
|
||||
const StatusIcon = config.icon;
|
||||
const progressPercent = Math.round((task.progress.completed / task.progress.total) * 100);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex flex-col p-3 rounded-lg border transition-all text-left w-full min-w-[160px] max-w-[200px]',
|
||||
'hover:border-primary/50 hover:shadow-sm',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 shadow-sm'
|
||||
: 'border-border bg-card'
|
||||
)}
|
||||
>
|
||||
{/* Task Name */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<StatusIcon
|
||||
className={cn(
|
||||
'w-4 h-4 flex-shrink-0',
|
||||
config.color,
|
||||
task.status === 'running' && 'animate-spin'
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{task.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium mb-2 w-fit',
|
||||
config.bg,
|
||||
config.color
|
||||
)}
|
||||
>
|
||||
{formatMessage({ id: `coordinator.status.${task.status}` })}
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
{task.progress.completed}/{task.progress.total}
|
||||
</span>
|
||||
<span>{progressPercent}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
task.status === 'completed' && 'bg-green-500',
|
||||
task.status === 'running' && 'bg-blue-500',
|
||||
task.status === 'failed' && 'bg-red-500',
|
||||
task.status === 'pending' && 'bg-muted-foreground'
|
||||
)}
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Main Component
|
||||
// ========================================
|
||||
|
||||
export function CoordinatorPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
|
||||
// Store selectors
|
||||
const commandChain = useCoordinatorStore(selectCommandChain);
|
||||
@@ -33,7 +186,11 @@ export function CoordinatorPage() {
|
||||
const status = useCoordinatorStore(selectCoordinatorStatus);
|
||||
const isPipelineLoaded = useCoordinatorStore(selectIsPipelineLoaded);
|
||||
const syncStateFromServer = useCoordinatorStore((state) => state.syncStateFromServer);
|
||||
const reset = useCoordinatorStore((state) => state.reset);
|
||||
|
||||
// Mock tasks (temporary - will be replaced by store)
|
||||
const tasks = useMemo(() => MOCK_TASKS, []);
|
||||
const hasTasks = tasks.length > 0;
|
||||
const selectedTask = tasks.find((t) => t.id === selectedTaskId);
|
||||
|
||||
// Sync state on mount (for page refresh scenarios)
|
||||
useEffect(() => {
|
||||
@@ -52,12 +209,21 @@ export function CoordinatorPage() {
|
||||
setSelectedNode(nodeId);
|
||||
}, []);
|
||||
|
||||
// Handle task selection
|
||||
const handleTaskClick = useCallback((taskId: string) => {
|
||||
setSelectedTaskId((prev) => (prev === taskId ? null : taskId));
|
||||
setSelectedNode(null);
|
||||
}, []);
|
||||
|
||||
// Get selected node object
|
||||
const selectedNodeObject = commandChain.find((node) => node.id === selectedNode) || currentNode || null;
|
||||
const selectedNodeObject =
|
||||
commandChain.find((node) => node.id === selectedNode) || currentNode || null;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col -m-4 md:-m-6">
|
||||
{/* ======================================== */}
|
||||
{/* Toolbar */}
|
||||
{/* ======================================== */}
|
||||
<div className="flex items-center gap-3 p-3 bg-card border-b border-border">
|
||||
{/* Page Title and Status */}
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
@@ -68,9 +234,12 @@ export function CoordinatorPage() {
|
||||
</span>
|
||||
{isPipelineLoaded && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'coordinator.page.status' }, {
|
||||
status: formatMessage({ id: `coordinator.status.${status}` }),
|
||||
})}
|
||||
{formatMessage(
|
||||
{ id: 'coordinator.page.status' },
|
||||
{
|
||||
status: formatMessage({ id: `coordinator.status.${status}` }),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -90,43 +259,90 @@ export function CoordinatorPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area - 3 Panel Layout */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Panel: Timeline */}
|
||||
<div className="w-1/3 min-w-[300px] border-r border-border bg-card">
|
||||
<CoordinatorTimeline
|
||||
autoScroll={true}
|
||||
onNodeClick={handleNodeClick}
|
||||
className="h-full"
|
||||
{/* ======================================== */}
|
||||
{/* Main Content Area */}
|
||||
{/* ======================================== */}
|
||||
{!hasTasks ? (
|
||||
/* Empty State - No tasks */
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<CoordinatorEmptyState
|
||||
onStart={handleOpenInputModal}
|
||||
disabled={status === 'running' || status === 'initializing'}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* ======================================== */}
|
||||
{/* Task List Area */}
|
||||
{/* ======================================== */}
|
||||
<div className="p-4 border-b border-border bg-background">
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{tasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
isSelected={selectedTaskId === task.id}
|
||||
onClick={() => handleTaskClick(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center Panel: Log Stream */}
|
||||
<div className="flex-1 min-w-0 bg-card">
|
||||
<CoordinatorLogStream />
|
||||
</div>
|
||||
{/* ======================================== */}
|
||||
{/* Task Detail Area (shown when task is selected) */}
|
||||
{/* ======================================== */}
|
||||
{selectedTask ? (
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Panel: Timeline */}
|
||||
<div className="w-1/3 min-w-[300px] border-r border-border bg-card">
|
||||
<CoordinatorTimeline
|
||||
autoScroll={true}
|
||||
onNodeClick={handleNodeClick}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Node Details */}
|
||||
<div className="w-80 min-w-[320px] max-w-[400px] border-l border-border bg-card overflow-y-auto">
|
||||
{selectedNodeObject ? (
|
||||
<NodeDetailsPanel
|
||||
node={selectedNodeObject}
|
||||
isExpanded={true}
|
||||
onToggle={(expanded) => {
|
||||
if (!expanded) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* Center Panel: Log Stream */}
|
||||
<div className="flex-1 min-w-0 flex flex-col bg-card">
|
||||
<div className="flex-1 min-h-0">
|
||||
<CoordinatorLogStream />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Node Details */}
|
||||
<div className="w-80 min-w-[320px] max-w-[400px] border-l border-border bg-card overflow-y-auto">
|
||||
{selectedNodeObject ? (
|
||||
<NodeDetailsPanel
|
||||
node={selectedNodeObject}
|
||||
isExpanded={true}
|
||||
onToggle={(expanded) => {
|
||||
if (!expanded) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm p-4 text-center">
|
||||
{formatMessage({ id: 'coordinator.page.noNodeSelected' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm p-4 text-center">
|
||||
{formatMessage({ id: 'coordinator.page.noNodeSelected' })}
|
||||
/* No task selected - show selection prompt */
|
||||
<div className="flex-1 flex items-center justify-center bg-muted/30">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'coordinator.taskDetail.noSelection' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ======================================== */}
|
||||
{/* Coordinator Input Modal */}
|
||||
{/* ======================================== */}
|
||||
<CoordinatorInputModal
|
||||
open={isInputModalOpen}
|
||||
onClose={() => setIsInputModalOpen(false)}
|
||||
|
||||
6
ccw/frontend/src/pages/coordinator/index.ts
Normal file
6
ccw/frontend/src/pages/coordinator/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// ========================================
|
||||
// Coordinator Page Export
|
||||
// ========================================
|
||||
// Barrel export for CoordinatorPage component
|
||||
|
||||
export { CoordinatorPage } from './CoordinatorPage';
|
||||
@@ -27,10 +27,10 @@ export { ReviewSessionPage } from './ReviewSessionPage';
|
||||
export { McpManagerPage } from './McpManagerPage';
|
||||
export { EndpointsPage } from './EndpointsPage';
|
||||
export { InstallationsPage } from './InstallationsPage';
|
||||
export { ExecutionMonitorPage } from './ExecutionMonitorPage';
|
||||
export { RulesManagerPage } from './RulesManagerPage';
|
||||
export { PromptHistoryPage } from './PromptHistoryPage';
|
||||
export { ExplorerPage } from './ExplorerPage';
|
||||
export { GraphExplorerPage } from './GraphExplorerPage';
|
||||
export { CodexLensManagerPage } from './CodexLensManagerPage';
|
||||
export { ApiSettingsPage } from './ApiSettingsPage';
|
||||
export { CliViewerPage } from './CliViewerPage';
|
||||
|
||||
@@ -257,12 +257,6 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
||||
|
||||
{/* Row 2: Meta info */}
|
||||
<div className="flex items-center gap-3 flex-wrap justify-end text-xs text-muted-foreground">
|
||||
{priority && (
|
||||
<Badge variant={priority.variant} className="text-xs gap-1">
|
||||
<Zap className="h-3 w-3" />
|
||||
{priority.label}
|
||||
</Badge>
|
||||
)}
|
||||
{taskType && (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded">{taskType}</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user