- Improve patent search service with expanded functionality - Update PatentSearchPanel UI component - Add new research_report.md - Update experimental protocol, literature review, paper outline, and theoretical framework Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
600 lines
24 KiB
TypeScript
600 lines
24 KiB
TypeScript
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
import { ConfigProvider, Layout, theme, Typography, Space, Tabs, Slider, Radio, Switch, Segmented } from 'antd';
|
|
import { ApartmentOutlined, ThunderboltOutlined, FilterOutlined, SwapOutlined, FileSearchOutlined, GlobalOutlined } from '@ant-design/icons';
|
|
import { ThemeToggle } from './components/ThemeToggle';
|
|
import { InputPanel } from './components/InputPanel';
|
|
import { TransformationInputPanel } from './components/TransformationInputPanel';
|
|
import { MindmapPanel } from './components/MindmapPanel';
|
|
import { TransformationPanel } from './components/TransformationPanel';
|
|
import { DeduplicationPanel } from './components/DeduplicationPanel';
|
|
import { PatentSearchPanel } from './components/PatentSearchPanel';
|
|
import { DualPathInputPanel } from './components/DualPathInputPanel';
|
|
import { DualPathMindmapPanel } from './components/DualPathMindmapPanel';
|
|
import { CrossoverPanel } from './components/CrossoverPanel';
|
|
import { useAttribute } from './hooks/useAttribute';
|
|
import { useDualPathAttribute } from './hooks/useDualPathAttribute';
|
|
import { getModels } from './services/api';
|
|
import { crossoverPairsToDAGs, type CrossoverDAGResult } from './utils/crossoverToDAG';
|
|
import { DualTransformationPanel } from './components/DualTransformationPanel';
|
|
import type { MindmapDAGRef } from './components/MindmapDAG';
|
|
import type { TransformationDAGRef } from './components/TransformationDAG';
|
|
import type { CategoryMode, ExpertSource, ExpertTransformationDAGResult, DeduplicationMethod, ExpertMode, CrossoverPair, PromptLanguage } from './types';
|
|
|
|
const { Header, Sider, Content } = Layout;
|
|
const { Title } = Typography;
|
|
|
|
interface VisualSettings {
|
|
nodeSpacing: number;
|
|
fontSize: number;
|
|
}
|
|
|
|
function App() {
|
|
const [isDark, setIsDark] = useState(true);
|
|
const [activeTab, setActiveTab] = useState<string>('attribute');
|
|
const [dualPathMode, setDualPathMode] = useState(false);
|
|
const [promptLanguage, setPromptLanguage] = useState<PromptLanguage>('zh');
|
|
|
|
// Single path hook
|
|
const { loading, progress, error, currentResult, history, analyze, loadFromHistory } = useAttribute();
|
|
|
|
// Dual path hook
|
|
const dualPath = useDualPathAttribute();
|
|
|
|
const [visualSettings, setVisualSettings] = useState<VisualSettings>({
|
|
nodeSpacing: 32,
|
|
fontSize: 14,
|
|
});
|
|
const mindmapRef = useRef<MindmapDAGRef>(null);
|
|
const transformationRef = useRef<TransformationDAGRef>(null);
|
|
|
|
// Dual path expert mode
|
|
const [expertMode, setExpertMode] = useState<ExpertMode>('shared');
|
|
const [selectedCrossoverPairs, setSelectedCrossoverPairs] = useState<CrossoverPair[]>([]);
|
|
|
|
// Convert selected crossover pairs to two separate DAGs for dual transformation
|
|
const crossoverDAGs = useMemo((): CrossoverDAGResult | null => {
|
|
if (selectedCrossoverPairs.length === 0) return null;
|
|
if (!dualPath.pathA.result || !dualPath.pathB.result) return null;
|
|
return crossoverPairsToDAGs(
|
|
selectedCrossoverPairs,
|
|
dualPath.pathA.result,
|
|
dualPath.pathB.result
|
|
);
|
|
}, [selectedCrossoverPairs, dualPath.pathA.result, dualPath.pathB.result]);
|
|
|
|
// Transformation Agent settings
|
|
const [transformModel, setTransformModel] = useState<string>('');
|
|
const [transformTemperature, setTransformTemperature] = useState<number>(0.95);
|
|
const [expertConfig, setExpertConfig] = useState<{
|
|
expert_count: number;
|
|
keywords_per_expert: number;
|
|
custom_experts?: string[];
|
|
}>({
|
|
expert_count: 3,
|
|
keywords_per_expert: 1,
|
|
custom_experts: undefined,
|
|
});
|
|
const [customExpertsInput, setCustomExpertsInput] = useState('');
|
|
const [expertSource, setExpertSource] = useState<ExpertSource>('llm');
|
|
const [expertLanguage, setExpertLanguage] = useState<'en' | 'zh'>('en');
|
|
const [shouldStartTransform, setShouldStartTransform] = useState(false);
|
|
const [transformLoading, setTransformLoading] = useState(false);
|
|
const [transformationResult, setTransformationResult] = useState<ExpertTransformationDAGResult | null>(null);
|
|
|
|
// Deduplication settings
|
|
const [deduplicationThreshold, setDeduplicationThreshold] = useState(0.85);
|
|
const [deduplicationMethod, setDeduplicationMethod] = useState<DeduplicationMethod>('embedding');
|
|
|
|
// Available models from API
|
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
|
|
|
// Fetch models on mount
|
|
useEffect(() => {
|
|
async function fetchModels() {
|
|
try {
|
|
const response = await getModels();
|
|
setAvailableModels(response.models);
|
|
// Set default model for transformation if not set
|
|
if (response.models.length > 0 && !transformModel) {
|
|
const defaultModel = response.models.find((m) => m.includes('qwen3')) || response.models[0];
|
|
setTransformModel(defaultModel);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch models:', err);
|
|
}
|
|
}
|
|
fetchModels();
|
|
}, []);
|
|
|
|
const handleAnalyze = async (
|
|
query: string,
|
|
model?: string,
|
|
temperature?: number,
|
|
chainCount?: number,
|
|
categoryMode?: CategoryMode,
|
|
customCategories?: string[],
|
|
suggestedCategoryCount?: number,
|
|
lang?: PromptLanguage
|
|
) => {
|
|
await analyze(query, model, temperature, chainCount, categoryMode, customCategories, suggestedCategoryCount, lang || promptLanguage);
|
|
};
|
|
|
|
const handleResetView = useCallback(() => {
|
|
mindmapRef.current?.resetView();
|
|
}, []);
|
|
|
|
const handleTransform = useCallback(() => {
|
|
setShouldStartTransform(true);
|
|
}, []);
|
|
|
|
// Dual path analysis handler
|
|
const handleDualPathAnalyze = useCallback(async (
|
|
queryA: string,
|
|
queryB: string,
|
|
options?: {
|
|
model?: string;
|
|
temperature?: number;
|
|
chainCount?: number;
|
|
categoryMode?: CategoryMode;
|
|
customCategories?: string[];
|
|
suggestedCategoryCount?: number;
|
|
lang?: PromptLanguage;
|
|
}
|
|
) => {
|
|
await dualPath.analyzeParallel(queryA, queryB, { ...options, lang: options?.lang || promptLanguage });
|
|
}, [dualPath, promptLanguage]);
|
|
|
|
// Handle mode switch
|
|
const handleModeSwitch = useCallback((checked: boolean) => {
|
|
setDualPathMode(checked);
|
|
// Reset to attribute tab when switching modes
|
|
setActiveTab('attribute');
|
|
}, []);
|
|
|
|
return (
|
|
<ConfigProvider
|
|
theme={{
|
|
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
|
}}
|
|
>
|
|
<Layout style={{ minHeight: '100vh' }}>
|
|
<Header
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '0 24px',
|
|
background: isDark
|
|
? 'linear-gradient(90deg, #141414 0%, #1f1f1f 50%, #141414 100%)'
|
|
: 'linear-gradient(90deg, #fff 0%, #fafafa 50%, #fff 100%)',
|
|
borderBottom: isDark ? '1px solid #303030' : '1px solid #f0f0f0',
|
|
boxShadow: isDark
|
|
? '0 2px 8px rgba(0, 0, 0, 0.3)'
|
|
: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
|
}}
|
|
>
|
|
<Space align="center" size="middle">
|
|
<ApartmentOutlined
|
|
style={{
|
|
fontSize: 24,
|
|
color: isDark ? '#177ddc' : '#1890ff',
|
|
}}
|
|
/>
|
|
<Title
|
|
level={4}
|
|
style={{
|
|
margin: 0,
|
|
background: isDark
|
|
? 'linear-gradient(90deg, #177ddc 0%, #69c0ff 100%)'
|
|
: 'linear-gradient(90deg, #1890ff 0%, #40a9ff 100%)',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
backgroundClip: 'text',
|
|
}}
|
|
>
|
|
Novelty Seeking
|
|
</Title>
|
|
</Space>
|
|
<Space align="center" size="middle">
|
|
<Space size="small">
|
|
<Typography.Text type="secondary">Single</Typography.Text>
|
|
<Switch
|
|
checked={dualPathMode}
|
|
onChange={handleModeSwitch}
|
|
checkedChildren={<SwapOutlined />}
|
|
unCheckedChildren={<ApartmentOutlined />}
|
|
/>
|
|
<Typography.Text type="secondary">Dual</Typography.Text>
|
|
</Space>
|
|
<Space size="small">
|
|
<GlobalOutlined style={{ color: isDark ? '#177ddc' : '#1890ff' }} />
|
|
<Segmented
|
|
size="small"
|
|
value={promptLanguage}
|
|
onChange={(value) => setPromptLanguage(value as PromptLanguage)}
|
|
options={[
|
|
{ label: '中文', value: 'zh' },
|
|
{ label: 'EN', value: 'en' },
|
|
]}
|
|
/>
|
|
</Space>
|
|
<ThemeToggle isDark={isDark} onToggle={setIsDark} />
|
|
</Space>
|
|
</Header>
|
|
<Layout>
|
|
<Content
|
|
style={{
|
|
padding: 16,
|
|
height: 'calc(100vh - 64px)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={setActiveTab}
|
|
style={{ height: '100%' }}
|
|
tabBarStyle={{ marginBottom: 8 }}
|
|
items={dualPathMode ? [
|
|
// ===== Dual Path Mode Tabs =====
|
|
{
|
|
key: 'attribute',
|
|
label: (
|
|
<span>
|
|
<SwapOutlined style={{ marginRight: 8 }} />
|
|
Dual Path Attribute
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<DualPathMindmapPanel
|
|
pathA={dualPath.pathA}
|
|
pathB={dualPath.pathB}
|
|
isDark={isDark}
|
|
visualSettings={visualSettings}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'crossover',
|
|
label: (
|
|
<span>
|
|
<SwapOutlined style={{ marginRight: 8 }} />
|
|
Crossover
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)', padding: 16 }}>
|
|
<CrossoverPanel
|
|
pathAResult={dualPath.pathA.result}
|
|
pathBResult={dualPath.pathB.result}
|
|
isDark={isDark}
|
|
expertMode={expertMode}
|
|
onExpertModeChange={setExpertMode}
|
|
onCrossoverReady={setSelectedCrossoverPairs}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'transformation',
|
|
label: (
|
|
<span>
|
|
<ThunderboltOutlined style={{ marginRight: 8 }} />
|
|
Transformation Agent
|
|
{crossoverDAGs && (
|
|
<span style={{ marginLeft: 4, fontSize: 10, opacity: 0.7 }}>
|
|
(A:{crossoverDAGs.pathA.nodes.length} / B:{crossoverDAGs.pathB.nodes.length})
|
|
</span>
|
|
)}
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<DualTransformationPanel
|
|
crossoverDAGA={crossoverDAGs?.pathA ?? null}
|
|
crossoverDAGB={crossoverDAGs?.pathB ?? null}
|
|
isDark={isDark}
|
|
model={transformModel}
|
|
temperature={transformTemperature}
|
|
expertConfig={expertConfig}
|
|
expertSource={expertSource}
|
|
expertLanguage={expertLanguage}
|
|
lang={promptLanguage}
|
|
shouldStartTransform={shouldStartTransform}
|
|
onTransformComplete={() => setShouldStartTransform(false)}
|
|
onLoadingChange={setTransformLoading}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'patent',
|
|
label: (
|
|
<span>
|
|
<FileSearchOutlined style={{ marginRight: 8 }} />
|
|
Patent Search
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<PatentSearchPanel
|
|
isDark={isDark}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
] : [
|
|
// ===== Single Path Mode Tabs =====
|
|
{
|
|
key: 'attribute',
|
|
label: (
|
|
<span>
|
|
<ApartmentOutlined style={{ marginRight: 8 }} />
|
|
Attribute Agent
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<MindmapPanel
|
|
ref={mindmapRef}
|
|
data={currentResult}
|
|
loading={loading}
|
|
error={error}
|
|
isDark={isDark}
|
|
visualSettings={visualSettings}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'transformation',
|
|
label: (
|
|
<span>
|
|
<ThunderboltOutlined style={{ marginRight: 8 }} />
|
|
Transformation Agent
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<TransformationPanel
|
|
ref={transformationRef}
|
|
attributeData={currentResult}
|
|
isDark={isDark}
|
|
model={transformModel}
|
|
temperature={transformTemperature}
|
|
expertConfig={expertConfig}
|
|
expertSource={expertSource}
|
|
expertLanguage={expertLanguage}
|
|
lang={promptLanguage}
|
|
shouldStartTransform={shouldStartTransform}
|
|
onTransformComplete={() => setShouldStartTransform(false)}
|
|
onLoadingChange={setTransformLoading}
|
|
onResultsChange={setTransformationResult}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'deduplication',
|
|
label: (
|
|
<span>
|
|
<FilterOutlined style={{ marginRight: 8 }} />
|
|
Deduplication
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<DeduplicationPanel
|
|
transformationResult={transformationResult}
|
|
isDark={isDark}
|
|
threshold={deduplicationThreshold}
|
|
onThresholdChange={setDeduplicationThreshold}
|
|
method={deduplicationMethod}
|
|
onMethodChange={setDeduplicationMethod}
|
|
lang={promptLanguage}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'patent',
|
|
label: (
|
|
<span>
|
|
<FileSearchOutlined style={{ marginRight: 8 }} />
|
|
Patent Search
|
|
</span>
|
|
),
|
|
children: (
|
|
<div style={{ height: 'calc(100vh - 140px)' }}>
|
|
<PatentSearchPanel
|
|
descriptions={transformationResult?.results.flatMap(r => r.descriptions)}
|
|
isDark={isDark}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Content>
|
|
<Sider
|
|
width={350}
|
|
theme={isDark ? 'dark' : 'light'}
|
|
style={{
|
|
height: 'calc(100vh - 64px)',
|
|
overflow: 'auto',
|
|
}}
|
|
>
|
|
{activeTab === 'attribute' && !dualPathMode && (
|
|
<InputPanel
|
|
loading={loading}
|
|
progress={progress}
|
|
history={history}
|
|
currentResult={currentResult}
|
|
onAnalyze={handleAnalyze}
|
|
onLoadHistory={(item, lang) => loadFromHistory(item, lang || promptLanguage)}
|
|
onResetView={handleResetView}
|
|
visualSettings={visualSettings}
|
|
onVisualSettingsChange={setVisualSettings}
|
|
lang={promptLanguage}
|
|
/>
|
|
)}
|
|
{activeTab === 'attribute' && dualPathMode && (
|
|
<DualPathInputPanel
|
|
onAnalyze={handleDualPathAnalyze}
|
|
loadingA={dualPath.pathA.loading}
|
|
loadingB={dualPath.pathB.loading}
|
|
progressA={dualPath.pathA.progress}
|
|
progressB={dualPath.pathB.progress}
|
|
availableModels={availableModels}
|
|
lang={promptLanguage}
|
|
/>
|
|
)}
|
|
{activeTab === 'crossover' && dualPathMode && (
|
|
<div style={{ padding: 16 }}>
|
|
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
|
<SwapOutlined style={{ marginRight: 8 }} />
|
|
Crossover Settings
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
Select attribute pairs in the main panel to create crossover combinations.
|
|
{selectedCrossoverPairs.length > 0 && (
|
|
<div style={{ marginTop: 8 }}>
|
|
<Typography.Text strong>
|
|
{selectedCrossoverPairs.length} pairs selected
|
|
</Typography.Text>
|
|
</div>
|
|
)}
|
|
</Typography.Text>
|
|
</div>
|
|
)}
|
|
{activeTab === 'transformation' && (
|
|
<TransformationInputPanel
|
|
onTransform={handleTransform}
|
|
loading={transformLoading}
|
|
hasData={dualPathMode ? !!crossoverDAGs : !!currentResult}
|
|
isDark={isDark}
|
|
model={transformModel}
|
|
temperature={transformTemperature}
|
|
expertConfig={expertConfig}
|
|
customExpertsInput={customExpertsInput}
|
|
expertSource={expertSource}
|
|
expertLanguage={expertLanguage}
|
|
onModelChange={setTransformModel}
|
|
onTemperatureChange={setTransformTemperature}
|
|
onExpertConfigChange={setExpertConfig}
|
|
onCustomExpertsInputChange={setCustomExpertsInput}
|
|
onExpertSourceChange={setExpertSource}
|
|
onExpertLanguageChange={setExpertLanguage}
|
|
availableModels={availableModels}
|
|
/>
|
|
)}
|
|
{activeTab === 'patent' && (
|
|
<div style={{ padding: 16 }}>
|
|
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
|
<FileSearchOutlined style={{ marginRight: 8 }} />
|
|
Patent Search Info
|
|
</Typography.Title>
|
|
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
|
Search patents using the Lens.org API to find prior art and similar inventions.
|
|
</Typography.Paragraph>
|
|
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>
|
|
How to Use
|
|
</Typography.Title>
|
|
<Typography.Paragraph style={{ fontSize: 12 }}>
|
|
<ol style={{ paddingLeft: 16, margin: 0 }}>
|
|
<li style={{ marginBottom: 8 }}>Click a generated description on the left to load it into the search box</li>
|
|
<li style={{ marginBottom: 8 }}>Edit the description to refine your search query</li>
|
|
<li style={{ marginBottom: 8 }}>Click "Search Patents" to find similar patents</li>
|
|
<li style={{ marginBottom: 8 }}>Results appear on the right - click to view on Lens.org</li>
|
|
</ol>
|
|
</Typography.Paragraph>
|
|
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>
|
|
Result Interpretation
|
|
</Typography.Title>
|
|
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
|
<strong>Many results:</strong> Query may overlap with existing prior art - consider making it more specific.
|
|
</Typography.Paragraph>
|
|
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
|
<strong>Few/no results:</strong> Potentially novel concept - good candidate for further exploration.
|
|
</Typography.Paragraph>
|
|
</div>
|
|
)}
|
|
{activeTab === 'deduplication' && (
|
|
<div style={{ padding: 16 }}>
|
|
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
|
<FilterOutlined style={{ marginRight: 8 }} />
|
|
Deduplication Settings
|
|
</Typography.Title>
|
|
|
|
{/* Method Selection */}
|
|
<div style={{ marginBottom: 20 }}>
|
|
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
|
|
Method
|
|
</Typography.Text>
|
|
<Radio.Group
|
|
value={deduplicationMethod}
|
|
onChange={(e) => setDeduplicationMethod(e.target.value)}
|
|
buttonStyle="solid"
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Radio.Button value="embedding" style={{ width: '50%', textAlign: 'center' }}>
|
|
Embedding
|
|
</Radio.Button>
|
|
<Radio.Button value="llm" style={{ width: '50%', textAlign: 'center' }}>
|
|
LLM Judge
|
|
</Radio.Button>
|
|
</Radio.Group>
|
|
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 8, fontSize: 12 }}>
|
|
{deduplicationMethod === 'embedding'
|
|
? 'Fast vector similarity comparison'
|
|
: 'Accurate but slower pairwise LLM comparison'}
|
|
</Typography.Text>
|
|
</div>
|
|
|
|
{/* Threshold Slider - Only for Embedding method */}
|
|
{deduplicationMethod === 'embedding' && (
|
|
<div style={{ marginBottom: 20 }}>
|
|
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
|
|
Similarity Threshold
|
|
</Typography.Text>
|
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12, fontSize: 12 }}>
|
|
Higher = stricter matching, fewer groups
|
|
</Typography.Text>
|
|
<Slider
|
|
min={0.5}
|
|
max={1.0}
|
|
step={0.05}
|
|
value={deduplicationThreshold}
|
|
onChange={setDeduplicationThreshold}
|
|
marks={{
|
|
0.5: '50%',
|
|
0.7: '70%',
|
|
0.85: '85%',
|
|
1.0: '100%',
|
|
}}
|
|
tooltip={{ formatter: (val) => `${((val ?? 0) * 100).toFixed(0)}%` }}
|
|
/>
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
Current: {(deduplicationThreshold * 100).toFixed(0)}% similarity required
|
|
</Typography.Text>
|
|
</div>
|
|
)}
|
|
|
|
{/* LLM Warning */}
|
|
{deduplicationMethod === 'llm' && (
|
|
<Typography.Text type="warning" style={{ display: 'block', fontSize: 12 }}>
|
|
Note: LLM method requires N*(N-1)/2 comparisons. May take longer for many descriptions.
|
|
</Typography.Text>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Sider>
|
|
</Layout>
|
|
</Layout>
|
|
</ConfigProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|