chore: save local changes

This commit is contained in:
2026-01-05 22:32:08 +08:00
parent bc281b8e0a
commit ec48709755
42 changed files with 5576 additions and 254 deletions

View 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>
);
}