299 lines
8.0 KiB
TypeScript
299 lines
8.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
Empty,
|
||
Card,
|
||
Button,
|
||
Statistic,
|
||
Row,
|
||
Col,
|
||
Typography,
|
||
Space,
|
||
Badge,
|
||
Collapse,
|
||
Checkbox,
|
||
Radio,
|
||
} from 'antd';
|
||
import {
|
||
SwapOutlined,
|
||
CheckCircleOutlined,
|
||
ReloadOutlined,
|
||
UnorderedListOutlined,
|
||
TableOutlined,
|
||
} from '@ant-design/icons';
|
||
import type { AttributeDAG, CrossoverPair, ExpertMode } from '../types';
|
||
import { useAttributeCrossover } from '../hooks/useAttributeCrossover';
|
||
import { CrossoverCard } from './crossover/CrossoverCard';
|
||
import { CrossoverMatrix } from './crossover/CrossoverMatrix';
|
||
import { CrossoverPreview } from './crossover/CrossoverPreview';
|
||
|
||
const { Text } = Typography;
|
||
|
||
interface CrossoverPanelProps {
|
||
pathAResult: AttributeDAG | null;
|
||
pathBResult: AttributeDAG | null;
|
||
isDark: boolean;
|
||
expertMode: ExpertMode;
|
||
onExpertModeChange: (mode: ExpertMode) => void;
|
||
onCrossoverReady?: (selectedPairs: CrossoverPair[]) => void;
|
||
}
|
||
|
||
type ViewMode = 'list' | 'matrix';
|
||
|
||
export function CrossoverPanel({
|
||
pathAResult,
|
||
pathBResult,
|
||
isDark,
|
||
expertMode,
|
||
onExpertModeChange,
|
||
onCrossoverReady,
|
||
}: CrossoverPanelProps) {
|
||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||
|
||
const {
|
||
pairs,
|
||
selectedPairs,
|
||
pairsByType,
|
||
crossTypeStats,
|
||
applyPairs,
|
||
togglePairSelection,
|
||
selectPairsByType,
|
||
selectAll,
|
||
clearPairs,
|
||
} = useAttributeCrossover();
|
||
|
||
// Generate pairs when both results are available
|
||
useEffect(() => {
|
||
if (pathAResult && pathBResult) {
|
||
applyPairs(pathAResult, pathBResult);
|
||
} else {
|
||
clearPairs();
|
||
}
|
||
}, [pathAResult, pathBResult, applyPairs, clearPairs]);
|
||
|
||
// Notify parent when selection changes
|
||
useEffect(() => {
|
||
onCrossoverReady?.(selectedPairs);
|
||
}, [selectedPairs, onCrossoverReady]);
|
||
|
||
// Render when no data
|
||
if (!pathAResult || !pathBResult) {
|
||
return (
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
height: '100%',
|
||
}}>
|
||
<Empty
|
||
description={
|
||
<Space direction="vertical" align="center">
|
||
<Text>Complete both Path A and Path B analysis first</Text>
|
||
<Text type="secondary">
|
||
{!pathAResult && !pathBResult
|
||
? 'Neither path has been analyzed'
|
||
: !pathAResult
|
||
? 'Path A has not been analyzed'
|
||
: 'Path B has not been analyzed'}
|
||
</Text>
|
||
</Space>
|
||
}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Generate cross type labels dynamically
|
||
const getCrossTypeLabel = (crossType: string): string => {
|
||
if (crossType.startsWith('same-')) {
|
||
const category = crossType.replace('same-', '');
|
||
return `Same Category: ${category}`;
|
||
}
|
||
if (crossType.startsWith('cross-')) {
|
||
const parts = crossType.replace('cross-', '').split('-');
|
||
if (parts.length >= 2) {
|
||
return `Cross: ${parts[0]} × ${parts.slice(1).join('-')}`;
|
||
}
|
||
}
|
||
return crossType;
|
||
};
|
||
|
||
const renderListView = () => {
|
||
const crossTypes = Object.keys(pairsByType);
|
||
|
||
if (crossTypes.length === 0) {
|
||
return <Empty description="No crossover pairs generated" />;
|
||
}
|
||
|
||
const collapseItems = crossTypes.map(type => {
|
||
const typePairs = pairsByType[type];
|
||
const stats = crossTypeStats[type];
|
||
const label = getCrossTypeLabel(type);
|
||
|
||
return {
|
||
key: type,
|
||
label: (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Checkbox
|
||
checked={stats.selected === stats.total}
|
||
indeterminate={stats.selected > 0 && stats.selected < stats.total}
|
||
onClick={(e) => e.stopPropagation()}
|
||
onChange={(e) => selectPairsByType(type, e.target.checked)}
|
||
/>
|
||
<Text>{label}</Text>
|
||
<Badge
|
||
count={`${stats.selected}/${stats.total}`}
|
||
style={{
|
||
backgroundColor: stats.selected > 0 ? '#52c41a' : '#d9d9d9',
|
||
}}
|
||
/>
|
||
</div>
|
||
),
|
||
children: (
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||
gap: 8,
|
||
}}>
|
||
{typePairs.map(pair => (
|
||
<CrossoverCard
|
||
key={pair.id}
|
||
pair={pair}
|
||
onToggle={togglePairSelection}
|
||
isDark={isDark}
|
||
/>
|
||
))}
|
||
</div>
|
||
),
|
||
};
|
||
});
|
||
|
||
return (
|
||
<Collapse
|
||
items={collapseItems}
|
||
defaultActiveKey={crossTypes.filter(t => t.startsWith('same-'))}
|
||
/>
|
||
);
|
||
};
|
||
|
||
const renderMatrixView = () => {
|
||
return (
|
||
<CrossoverMatrix
|
||
dagA={pathAResult}
|
||
dagB={pathBResult}
|
||
pairs={pairs}
|
||
onTogglePair={togglePairSelection}
|
||
isDark={isDark}
|
||
/>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||
{/* Statistics Header */}
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<Row gutter={16}>
|
||
<Col span={6}>
|
||
<Statistic
|
||
title="Total Pairs"
|
||
value={pairs.length}
|
||
prefix={<SwapOutlined />}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Statistic
|
||
title="Selected"
|
||
value={selectedPairs.length}
|
||
prefix={<CheckCircleOutlined />}
|
||
valueStyle={{ color: '#52c41a' }}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Statistic
|
||
title="Path A Attrs"
|
||
value={pathAResult.nodes.length}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Statistic
|
||
title="Path B Attrs"
|
||
value={pathBResult.nodes.length}
|
||
/>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* Selection Preview */}
|
||
<CrossoverPreview
|
||
selectedPairs={selectedPairs}
|
||
dagA={pathAResult}
|
||
dagB={pathBResult}
|
||
isDark={isDark}
|
||
/>
|
||
|
||
{/* Expert Mode Selection */}
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<Space direction="vertical" style={{ width: '100%' }}>
|
||
<Text strong>Expert Team Mode</Text>
|
||
<Radio.Group
|
||
value={expertMode}
|
||
onChange={(e) => onExpertModeChange(e.target.value)}
|
||
buttonStyle="solid"
|
||
>
|
||
<Radio.Button value="shared">
|
||
Shared Experts
|
||
</Radio.Button>
|
||
<Radio.Button value="independent">
|
||
Independent Experts
|
||
</Radio.Button>
|
||
</Radio.Group>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{expertMode === 'shared'
|
||
? 'Both paths use the same expert team for crossover transformation'
|
||
: 'Each path uses its own expert team, combined for crossover'}
|
||
</Text>
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* Actions */}
|
||
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
|
||
<Button
|
||
icon={<CheckCircleOutlined />}
|
||
onClick={() => selectAll(true)}
|
||
>
|
||
Select All
|
||
</Button>
|
||
<Button
|
||
onClick={() => selectAll(false)}
|
||
>
|
||
Deselect All
|
||
</Button>
|
||
<Button
|
||
icon={<ReloadOutlined />}
|
||
onClick={() => applyPairs(pathAResult, pathBResult)}
|
||
>
|
||
Regenerate
|
||
</Button>
|
||
<div style={{ flex: 1 }} />
|
||
<Radio.Group
|
||
value={viewMode}
|
||
onChange={(e) => setViewMode(e.target.value)}
|
||
buttonStyle="solid"
|
||
size="small"
|
||
>
|
||
<Radio.Button value="list">
|
||
<UnorderedListOutlined /> List
|
||
</Radio.Button>
|
||
<Radio.Button value="matrix">
|
||
<TableOutlined /> Matrix
|
||
</Radio.Button>
|
||
</Radio.Group>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||
{viewMode === 'list' ? renderListView() : renderMatrixView()}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|