Files
AIclinicalresearch/docs/03-业务模块/AIA-AI智能问答/04-开发计划/03-前端组件设计.md
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Changes:
- Add StreamingService with OpenAI Compatible format
- Upgrade Chat component V2 with Ant Design X integration
- Implement AIA module with 12 intelligent agents
- Update API routes to unified /api/v1 prefix
- Update system documentation

Backend (~1300 lines):
- common/streaming: OpenAI Compatible adapter
- modules/aia: 12 agents, conversation service, streaming integration
- Update route versions (RVW, PKB to v1)

Frontend (~3500 lines):
- modules/aia: AgentHub + ChatWorkspace (100% prototype restoration)
- shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook
- Update API endpoints to v1

Documentation:
- AIA module status guide
- Universal capabilities catalog
- System overview updates
- All module documentation sync

Tested: Stream response verified, authentication working
Status: AIA V2.0 core completed (85%)
2026-01-14 19:15:01 +08:00

887 lines
22 KiB
Markdown
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.
# AIA V2.1 <20>滨垢蝏<E59EA2>辣霈曇恣
> **<2A><>𧋦**嚗间2.1
> **<2A>𥕦遣<F0A595A6><EFBFBD>**嚗?026-01-11
> **<2A><><EFBFBD><EFBFBD>**嚗鑹eact 19 + TypeScript 5 + Ant Design 6 + Ant Design X 2.1
---
## <20><><E79285>蝏𤘪<E89D8F>
```
frontend-v2/src/modules/aia/
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages/
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> Dashboard.tsx # <20><EFBFBD>雿枏之<E69E8F><E4B98B><EFBFBD>擐㚚△嚗?
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> Workspace.tsx # 撖寡<E69296>撌乩<E6928C><E4B9A9>?
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> components/
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> AgentPipeline/
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> index.tsx # 5<>嗆挾瘚<E68CBE>偌蝥踹捆<E8B8B9>?
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> StageColumn.tsx # <20>閖𧫴畾萄<E795BE>
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> AgentCard.tsx # <20><EFBFBD>雿枏㨃<E69E8F>?
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> IntentSearch/
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> index.tsx # <20>誩㦛<E8AAA9>𦦵揣獢?
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> SuggestionDropdown.tsx # 撱箄悅銝𧢲<E98A9D>獢?
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> ConversationList/
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> index.tsx # <20><>蟮隡朞<E99AA1><E69C9E>𡑒”
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> ConversationItem.tsx # 隡朞<E99AA1>憿?
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> MessageList/
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> index.tsx # 瘨<><E798A8><EFBFBD>𡑒”
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> UserMessage.tsx # <20><EFBFBD><EFBFBD><E798A8>
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> AssistantMessage.tsx # AI<41>𧼮<EFBFBD>
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> ThinkingBlock.tsx # 瘛勗漲<E58B97><EFBFBD><E882BD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> Attachment/
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> AttachmentUpload.tsx # <20><>辣銝𠹺<E98A9D>
<EFBFBD>? <20>? <20><EFBFBD><E98EBF><EFBFBD> AttachmentCard.tsx # <20><><EFBFBD><EFBFBD>
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> AttachmentPreview.tsx # <20><>辣憸<E8BEA3><E686B8>
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> SlashCommands/
<EFBFBD>? <20>? <20><EFBFBD><E5A999><EFBFBD> index.tsx # 敹急㭘<E680A5><E3AD98><EFBFBD>𨅯<EFBFBD>
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> ActionBar/
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> index.tsx # 蝏𤘪<E89D8F><F0A498AA><EFBFBD><E6BBA2>?
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> hooks/
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> useConversation.ts # 撖寡<E69296>蝞∠<E89D9E>
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> useAgents.ts # <20><EFBFBD>雿𤘪㺭<F0A498AA>?
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> useIntentRouter.ts # <20>誩㦛頝舐眏
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> useStreamMessage.ts # 瘚<><E7989A><EFBFBD><E798A8>
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> useAttachment.ts # <20><>辣銝𠹺<E98A9D>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> api/
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> index.ts # API 撠<><E692A0>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> types/
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> index.ts # TypeScript 蝐餃<E89D90>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> styles/
<EFBFBD>? <20><EFBFBD><E98EBF><EFBFBD> dashboard.module.css # Dashboard <20><EFBFBD>
<EFBFBD>? <20><EFBFBD><E5A999><EFBFBD> workspace.module.css # Workspace <20><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> index.tsx # 璅<E79285><E288AA>亙藁 + 頝舐眏
```
---
## <20>綫 霈曇恣閫<E681A3><E996AB>
### <20>脣蔗蝟餌<E89D9F>
```css
:root {
/* 5<>嗆挾瘚<E68CBE>偌蝥蹂蜓憸䁅𠧧 */
--stage-design: #3B82F6; /* <20>肽𠧧 - <20>𠉛弦霈曇恣 */
--stage-data: #8B5CF6; /* 蝝怨𠧧 - <20>唳旿<E594B3><E697BF><EFBFBD> */
--stage-analysis: #10B981; /* 蝏輯𠧧 - 蝏蠘恣<E8A098><E681A3><EFBFBD> */
--stage-write: #F59E0B; /* 璈躰𠧧 - 霈箸<E99C88><E7AEB8><EFBFBD> */
--stage-publish: #EF4444; /* 蝥Z𠧧 - <20><EFBFBD><E99E89><EFBFBD> */
/* <20><EFBFBD><E8A098>?*/
--ai-assistant: #6366F1; /* AI<41><EFBFBD>銝餉𠧧 */
--thinking-bg: #F3F4F6; /* <20><EFBFBD><E882BD><EFBFBD><EFBFBD>峕艶 */
--thinking-border: #E5E7EB; /* <20><EFBFBD><E882BD><EFBFBD>颲寞<E9A2B2> */
/* Gemini 憌擧聢 */
--bg-primary: #FFFFFF;
--bg-secondary: #F9FAFB;
--text-primary: #111827;
--text-secondary: #6B7280;
--border-light: #E5E7EB;
}
```
### <20><EFBFBD>蝟餌<E89D9F>
```css
/* <20>萄儐 8px 蝵烐聢 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
```
### <20><EFBFBD>
```css
/* 蝘餃𢆡隡睃<E99AA1> */
--breakpoint-sm: 640px; /* <20>𧢲㦤 */
--breakpoint-md: 768px; /* 撟單踎 */
--breakpoint-lg: 1024px; /* 獢屸𢒰 */
--breakpoint-xl: 1280px; /* 憭批<E686AD> */
```
---
## <20><> 憿菟𢒰霈曇恣
### 1. Dashboard嚗<64><EFBFBD><EFBFBD>憭批<E686AD>嚗?
```tsx
// pages/Dashboard.tsx
import { IntentSearch } from '../components/IntentSearch';
import { AgentPipeline } from '../components/AgentPipeline';
export const Dashboard: React.FC = () => {
return (
<div className={styles.dashboard}>
{/* 憿園<E686BF><E59C92>誩㦛<E8AAA9>𦦵揣獢?*/}
<header className={styles.header}>
<h1>AI <EFBFBD><EFBFBD><EFBFBD><EFBFBD></h1>
<p><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></p>
<IntentSearch />
</header>
{/* 5<>嗆挾<E59786><EFBFBD>雿𤘪<E99BBF>瘞渡瑪 */}
<main className={styles.main}>
<AgentPipeline />
</main>
</div>
);
};
```
**撣<><E692A3><EFBFBD><EFBFBD>**嚗?
- 憿園<E686BF><EFBFBD>葉憭扳<E686AD><E89D9D>
- 5<>嗆挾瘚<E68CBE>偌蝥踵赤<E8B8B5>穃像<E7A983><EFBFBD>獢屸𢒰嚗? 蝥萄<E89DA5>皛𡁜𢆡嚗<F0A286A1><EFBFBD><EFBFBD>
- Gemini 憌擧聢憭抒<E686AD><E68A92>?
---
### 2. Workspace嚗<65>笆霂嘥極雿𨅯蝱嚗?
```tsx
// pages/Workspace.tsx
import { ChatContainer } from '@/shared/components/Chat';
import { ConversationList } from '../components/ConversationList';
import { ThinkingBlock } from '../components/MessageList/ThinkingBlock';
import { AttachmentUpload } from '../components/Attachment/AttachmentUpload';
export const Workspace: React.FC = () => {
const { conversationId } = useParams();
const { conversation, messages, sendMessage } = useConversation(conversationId);
return (
<div className={styles.workspace}>
{/* 撌虫儒颲寞<E9A2B2> - <20><>蟮隡朞<E99AA1> */}
<aside className={styles.sidebar}>
<ConversationList />
</aside>
{/* 銝餃笆霂嘥躹 */}
<main className={styles.main}>
{/* 撖寡<E69296>憭湧<E686AD> */}
<header className={styles.header}>
<AgentAvatar agent={conversation?.agent} />
<h2>{conversation?.agent?.name}</h2>
</header>
{/* 瘨<><E798A8><EFBFBD>𡑒” - 憭滨鍂<E6BBA8>𡁶鍂 Chat 蝏<>辣 */}
<ChatContainer
messages={messages}
onSend={sendMessage}
renderMessage={(msg) => (
<div className={styles.message}>
{/* 瘛勗漲<E58B97><EFBFBD><E882BD><EFBFBD> */}
{msg.thinkingContent && (
<ThinkingBlock content={msg.thinkingContent} />
)}
{/* 瘨<><E798A8><EFBFBD><EFBFBD>捆 */}
<MarkdownRenderer content={msg.content} />
{/* <20><><EFBFBD><EFBFBD> */}
{msg.attachments?.map(att => (
<AttachmentCard key={att.id} attachment={att} />
))}
</div>
)}
inputFooter={<AttachmentUpload />}
/>
</main>
</div>
);
};
```
---
## <20>妝 蝏<>辣霂衣<E99C82>霈曇恣
### 1. AgentPipeline嚗?<3F>嗆挾瘚<E68CBE>偌蝥選<E89DA5>
```tsx
// components/AgentPipeline/index.tsx
interface AgentPipelineProps {
onAgentClick: (agentId: string) => void;
}
export const AgentPipeline: React.FC<AgentPipelineProps> = ({ onAgentClick }) => {
const { agents } = useAgents();
const stages = [
{ key: 'design', title: '<27>𠉛弦霈曇恣', color: 'var(--stage-design)' },
{ key: 'data', title: '<27>唳旿<E594B3><E697BF><EFBFBD>', color: 'var(--stage-data)' },
{ key: 'analysis', title: '蝏蠘恣<E8A098><E681A3><EFBFBD>', color: 'var(--stage-analysis)' },
{ key: 'write', title: '霈箸<E99C88><E7AEB8><EFBFBD>', color: 'var(--stage-write)' },
{ key: 'publish', title: '<27><EFBFBD><E99E89><EFBFBD>', color: 'var(--stage-publish)' },
];
return (
<div className={styles.pipeline}>
{stages.map((stage, index) => (
<StageColumn
key={stage.key}
title={stage.title}
color={stage.color}
agents={agents.filter(a => a.stage === stage.key)}
onAgentClick={onAgentClick}
showConnector={index < stages.length - 1}
/>
))}
</div>
);
};
```
**<EFBFBD><EFBFBD><EFBFBD><EFBFBD>**嚗?
```css
.pipeline {
display: flex;
gap: var(--spacing-lg);
overflow-x: auto;
padding: var(--spacing-xl) 0;
}
/* 蝘餃𢆡蝡舐熊<E88890><EFBFBD><EFBFBD> */
@media (max-width: 768px) {
.pipeline {
flex-direction: column;
overflow-x: visible;
}
}
```
---
### 2. IntentSearch嚗<68><E59A97><EFBFBD><EFBFBD><E89D9D>嚗?
```tsx
// components/IntentSearch/index.tsx
export const IntentSearch: React.FC = () => {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<IntentSuggestion[]>([]);
const { routeIntent, isLoading } = useIntentRouter();
const navigate = useNavigate();
// 500ms <20><EFBFBD>
const debouncedQuery = useDebouncedValue(query, 500);
useEffect(() => {
if (debouncedQuery.length >= 2) {
routeIntent(debouncedQuery).then(setSuggestions);
}
}, [debouncedQuery]);
const handleSelect = (suggestion: IntentSuggestion) => {
// 頝唾蓮<E594BE>啣笆摨娍惣<E5A88D><EFBFBD>
navigate(`/aia/workspace?agent=${suggestion.agentId}&prompt=${encodeURIComponent(suggestion.prefillPrompt)}`);
};
return (
<div className={styles.searchContainer}>
<Input.Search
size="large"
placeholder="<22>讛膩<E8AE9B><EFBFBD><E587BD><EFBFBD><EFBFBD><E79899>AI 撠<><EFBFBD>冽綫<E586BD>𣂼<EFBFBD><F0A382BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
value={query}
onChange={(e) => setQuery(e.target.value)}
loading={isLoading}
className={styles.searchInput}
/>
{suggestions.length > 0 && (
<SuggestionDropdown
suggestions={suggestions}
onSelect={handleSelect}
/>
)}
</div>
);
};
```
---
### 3. ThinkingBlock嚗<6B>楛摨行<E691A8><EFBFBD><E882BD><EFBFBD><EFBFBD><EFBFBD>嚗?
```tsx
// components/MessageList/ThinkingBlock.tsx
interface ThinkingBlockProps {
content: string;
duration?: number; // <20><EFBFBD><E882BD><EFBFBD>埈𧒄嚗<F0A79284><E59A97>嚗?
isStreaming?: boolean; // <20>臬炏甇<E7828F><EFBFBD><E98A81><EFBFBD>
}
export const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
content,
duration,
isStreaming = false,
}) => {
// <20><><EFBFBD>銝剖<E98A9D><EFBFBD><EFBFBD><E59A97><EFBFBD>𣂼<EFBFBD><F0A382BC>芸𢆡<E88AB8>嗉絲
const [expanded, setExpanded] = useState(isStreaming);
useEffect(() => {
if (!isStreaming && expanded) {
// 摰峕<E691B0><E5B395>?1.5s <20>芸𢆡<E88AB8>嗉絲
const timer = setTimeout(() => setExpanded(false), 1500);
return () => clearTimeout(timer);
}
}, [isStreaming]);
return (
<div className={styles.thinkingBlock}>
<div
className={styles.header}
onClick={() => setExpanded(!expanded)}
>
<span className={styles.icon}>
{isStreaming ? <LoadingOutlined spin /> : <BulbOutlined />}
</span>
<span className={styles.title}>
{isStreaming ? '甇<>銁瘛勗漲<E58B97><EFBFBD>?..' : `撌脫楛摨行<EFBFBD><EFBFBD>?(<28>埈𧒄 ${duration?.toFixed(1)}s)`}
</span>
<span className={styles.expandIcon}>
{expanded ? <UpOutlined /> : <DownOutlined />}
</span>
</div>
{expanded && (
<div className={styles.content}>
<Typography.Text type="secondary">
{content}
</Typography.Text>
</div>
)}
</div>
);
};
```
**<EFBFBD><EFBFBD>**嚗?
```css
.thinkingBlock {
background: var(--thinking-bg);
border: 1px solid var(--thinking-border);
border-radius: 8px;
margin-bottom: var(--spacing-md);
}
.header {
display: flex;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
user-select: none;
}
.content {
padding: var(--spacing-md);
padding-top: 0;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
}
```
---
### 4. AttachmentUpload嚗<64><E59A97>隞嗡<E99A9E>隡𩤃<E99AA1>
```tsx
// components/Attachment/AttachmentUpload.tsx
interface AttachmentUploadProps {
conversationId: string;
onUploadComplete: (attachment: Attachment) => void;
maxCount?: number; // 暺䁅恕 5
}
export const AttachmentUpload: React.FC<AttachmentUploadProps> = ({
conversationId,
onUploadComplete,
maxCount = 5,
}) => {
const { uploadFile, uploading, progress } = useAttachment();
const [attachments, setAttachments] = useState<Attachment[]>([]);
const handleUpload = async (file: File) => {
if (attachments.length >= maxCount) {
message.error(`<EFBFBD><EFBFBD>憭帋<EFBFBD>隡?${maxCount} 銝芷<E98A9D>隞跆);
return false;
}
// <20><>辣蝐餃<E89D90><E9A483><EFBFBD>
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
if (!allowedTypes.includes(file.type)) {
message.error('隞<>𣈲<EFBFBD>?PDF<44><46>ord<72><64>XT<58><54>xcel <20><>辣');
return false;
}
// <20><>辣憭批<E686AD><E689B9><EFBFBD>嚗?0MB嚗?
if (file.size > 20 * 1024 * 1024) {
message.error('<27><>辣憭批<E686AD>銝滩<E98A9D><EFBFBD><E9A09E> 20MB');
return false;
}
const attachment = await uploadFile(conversationId, file);
setAttachments([...attachments, attachment]);
onUploadComplete(attachment);
return false; // <20>餅迫暺䁅恕銝𠹺<E98A9D>
};
return (
<div className={styles.uploadContainer}>
<Upload
beforeUpload={handleUpload}
showUploadList={false}
accept=".pdf,.docx,.txt,.xlsx"
multiple
>
<Button icon={<PaperClipOutlined />} type="text">
瘛餃<E7989B><E9A483><EFBFBD>
</Button>
</Upload>
{/* 撌脖<E6928C>隡𣳇<E99AA1>隞嗅<E99A9E>銵?*/}
<div className={styles.attachmentList}>
{attachments.map(att => (
<AttachmentCard
key={att.id}
attachment={att}
onRemove={() => {
setAttachments(attachments.filter(a => a.id !== att.id));
}}
/>
))}
</div>
{/* 銝𠹺<E98A9D>餈𥕦漲 */}
{uploading && (
<Progress percent={progress} size="small" />
)}
</div>
);
};
```
---
### 5. SlashCommands嚗<73><EFBFBD><EFBFBD>隞歹<E99A9E>
```tsx
// components/SlashCommands/index.tsx
interface SlashCommandsProps {
onSelect: (command: SlashCommand) => void;
onClose: () => void;
}
const commands: SlashCommand[] = [
{ key: 'polish', icon: '<27>?, label: '西𠧧', description: '<EFBFBD><EFBFBD><EFBFBD>𧋦' },
{ key: 'expand', icon: '<EFBFBD><EFBFBD>', label: '<EFBFBD><EFBFBD>', description: '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>' },
{ key: 'translate', icon: '<EFBFBD><EFBFBD>', label: '<EFBFBD>', description: '<EFBFBD>' },
{ key: 'export', icon: '<EFBFBD><EFBFBD>', label: 'Word', description: '?Word <EFBFBD><EFBFBD>' },
];
export const SlashCommands: React.FC<SlashCommandsProps> = ({
onSelect,
onClose,
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
// <20><EFBFBD>撖潸⏛
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(i => Math.max(0, i - 1));
break;
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(i => Math.min(commands.length - 1, i + 1));
break;
case 'Enter':
e.preventDefault();
onSelect(commands[selectedIndex]);
break;
case 'Escape':
onClose();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedIndex]);
return (
<div className={styles.commandMenu}>
{commands.map((cmd, index) => (
<div
key={cmd.key}
className={`${styles.commandItem} ${index === selectedIndex ? styles.selected : ''}`}
onClick={() => onSelect(cmd)}
onMouseEnter={() => setSelectedIndex(index)}
>
<span className={styles.icon}>{cmd.icon}</span>
<div className={styles.content}>
<div className={styles.label}>{cmd.label}</div>
<div className={styles.description}>{cmd.description}</div>
</div>
</div>
))}
</div>
);
};
```
---
### 6. ActionBar嚗<72><E59A97><EFBFBD>𨀣<EFBFBD>雿𨀣<E99BBF>嚗?
```tsx
// components/ActionBar/index.tsx
interface ActionBarProps {
message: Message;
onCopy: () => void;
onRegenerate: () => void;
onExport: () => void;
}
export const ActionBar: React.FC<ActionBarProps> = ({
message,
onCopy,
onRegenerate,
onExport,
}) => {
return (
<div className={styles.actionBar}>
<Tooltip title="憭滚<E686AD>">
<Button
type="text"
icon={<CopyOutlined />}
onClick={onCopy}
/>
</Tooltip>
<Tooltip title="<22>齿鰵<E9BDBF><E9B0B5><EFBFBD>">
<Button
type="text"
icon={<ReloadOutlined />}
onClick={onRegenerate}
/>
</Tooltip>
<Tooltip title="撖澆枂 Word">
<Button
type="text"
icon={<FileWordOutlined />}
onClick={onExport}
/>
</Tooltip>
</div>
);
};
```
---
## <20>𦹄 Hooks 霈曇恣
### useConversation
```typescript
// hooks/useConversation.ts
interface UseConversationReturn {
conversation: Conversation | null;
messages: Message[];
isLoading: boolean;
sendMessage: (content: string, attachmentIds?: string[]) => Promise<void>;
regenerate: (messageId: string) => Promise<void>;
deleteConversation: () => Promise<void>;
}
export function useConversation(conversationId?: string): UseConversationReturn {
const [conversation, setConversation] = useState<Conversation | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
// <20>㰘蝸撖寡<E69296>
useEffect(() => {
if (conversationId) {
api.getConversation(conversationId).then(data => {
setConversation(data);
setMessages(data.messages);
});
}
}, [conversationId]);
// <20><EFBFBD><E785BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E7989A>嚗?
const sendMessage = async (content: string, attachmentIds?: string[]) => {
setIsLoading(true);
// 瘛餃<E7989B><E9A483><EFBFBD><EFBFBD><E798A8>
const userMessage: Message = {
id: `temp-${Date.now()}`,
role: 'user',
content,
attachments: [],
createdAt: new Date().toISOString(),
};
setMessages(prev => [...prev, userMessage]);
// <20><EFBFBD><E598A5>?AI 瘨<><E798A8>
const aiMessage: Message = {
id: `temp-ai-${Date.now()}`,
role: 'assistant',
content: '',
thinkingContent: '',
createdAt: new Date().toISOString(),
};
setMessages(prev => [...prev, aiMessage]);
// 瘚<><E7989A><EFBFBD>交𤣰
await api.sendMessageStream(conversationId!, content, attachmentIds, {
onThinkingDelta: (delta) => {
setMessages(prev => {
const last = prev[prev.length - 1];
return [...prev.slice(0, -1), {
...last,
thinkingContent: (last.thinkingContent || '') + delta,
}];
});
},
onDelta: (delta) => {
setMessages(prev => {
const last = prev[prev.length - 1];
return [...prev.slice(0, -1), {
...last,
content: last.content + delta,
}];
});
},
onComplete: (finalMessage) => {
setMessages(prev => {
return [...prev.slice(0, -1), finalMessage];
});
setIsLoading(false);
},
onError: (error) => {
message.error(error.message);
setIsLoading(false);
},
});
};
return {
conversation,
messages,
isLoading,
sendMessage,
regenerate: async () => {},
deleteConversation: async () => {},
};
}
```
### useStreamMessage
```typescript
// hooks/useStreamMessage.ts
interface StreamCallbacks {
onThinkingStart?: () => void;
onThinkingDelta?: (content: string) => void;
onThinkingEnd?: (duration: number) => void;
onMessageStart?: (id: string) => void;
onDelta?: (content: string) => void;
onMessageEnd?: (message: Message) => void;
onComplete?: (message: Message) => void;
onError?: (error: Error) => void;
}
export function useStreamMessage() {
const streamMessage = async (
url: string,
body: object,
callbacks: StreamCallbacks
) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(body),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
const events = parseSSE(chunk);
for (const event of events) {
switch (event.type) {
case 'thinking_start':
callbacks.onThinkingStart?.();
break;
case 'thinking_delta':
callbacks.onThinkingDelta?.(event.data.content);
break;
case 'thinking_end':
callbacks.onThinkingEnd?.(event.data.duration);
break;
case 'message_start':
callbacks.onMessageStart?.(event.data.id);
break;
case 'delta':
callbacks.onDelta?.(event.data.content);
break;
case 'message_end':
callbacks.onMessageEnd?.(event.data);
break;
case 'done':
callbacks.onComplete?.(event.data);
break;
case 'error':
callbacks.onError?.(new Error(event.data.message));
break;
}
}
}
};
return { streamMessage };
}
```
---
## <20>𢲡 <20><EFBFBD>撘讛挽霈?
### <20><EFBFBD>蝑𣇉裦
```typescript
// <20><EFBFBD>摰帋<E691B0>
const breakpoints = {
sm: 640, // <20>𧢲㦤
md: 768, // 撟單踎嚗<E8B88E>蜓閬<E89C93><EFBFBD><EFBFBD>
lg: 1024, // 獢屸𢒰
xl: 1280, // 憭批<E686AD>
};
```
### Dashboard <20><EFBFBD>撘?
| <20><EFBFBD> | 撣<><E692A3> |
|------|------|
| `< 768px` | 瘚<>偌蝥輻熊<E8BCBB><EFBFBD><E78390><EFBFBD><E58981><EFBFBD><E288A0><EFBFBD> |
| `<60>?768px` | 瘚<>偌蝥踵赤<E8B8B5>?5 <20>?|
### Workspace <20><EFBFBD>撘?
| <20><EFBFBD> | 撣<><E692A3> |
|------|------|
| `< 768px` | 靘扯器<E689AF><EFBFBD><E8AAAF>𧶏<EFBFBD><F0A7B68F><EFBFBD>皛穃枂嚗㚁<E59A97>颲枏<E9A2B2><EFBFBD><EFBFBD><EFBFBD><E3979B><EFBFBD> |
| `<60>?768px` | 靘扯器<E689AF>誩𤐄摰𡁏遬蝷?240px |
---
## <20><> 蝐餃<E89D90>摰帋<E691B0>
```typescript
// types/index.ts
export interface Agent {
id: string;
name: string;
description: string;
icon: string;
stage: 'design' | 'data' | 'analysis' | 'write' | 'publish';
color: string;
knowledgeBaseId?: string;
isTool?: boolean;
targetModule?: string;
welcomeMessage?: string;
suggestedQuestions?: string[];
}
export interface Conversation {
id: string;
title: string;
agentId: string;
agent?: Agent;
projectId?: string;
messageCount: number;
lastMessage?: string;
createdAt: string;
updatedAt: string;
}
export interface Message {
id: string;
conversationId?: string;
role: 'user' | 'assistant';
content: string;
thinkingContent?: string;
attachments?: Attachment[];
model?: string;
tokens?: number;
isPinned?: boolean;
createdAt: string;
}
export interface Attachment {
id: string;
filename: string;
mimeType: string;
size: number;
ossUrl: string;
textExtracted: boolean;
tokenCount: number;
truncated: boolean;
createdAt: string;
}
export interface IntentSuggestion {
agentId: string;
agentName: string;
confidence: number;
prefillPrompt: string;
}
export interface SlashCommand {
key: string;
icon: string;
label: string;
description: string;
}
```
---
## <20><> <20>湔鰵<E6B994><EFBFBD>
| <20><EFBFBD> | <20><>𧋦 | <20><>捆 |
|------|------|------|
| 2026-01-11 | V1.0 | <20>𥕦遣<F0A595A6>滨垢蝏<E59EA2>辣霈曇恣<E69B87><E681A3>﹝ |