Files
novelty-seeking/frontend/src/components/CrossoverPanel.tsx
2026-01-05 22:32:08 +08:00

299 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}