chore: save local changes
This commit is contained in:
298
frontend/src/components/CrossoverPanel.tsx
Normal file
298
frontend/src/components/CrossoverPanel.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user