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

22 KiB
Raw Blame History

AIA V2.1 <20>滨垢蝏<E59EA2>辣霈曇恣

<EFBFBD><EFBFBD>𧋦嚗间2.1
**<2A>𥕦遣<F0A595A6><EFBFBD>**嚗?026-01-11
**<2A><><EFBFBD><EFBFBD>**嚗鑹eact 19 + TypeScript 5 + Ant Design 6 + Ant Design X 2.1


<EFBFBD><EFBFBD><E79285>蝏𤘪<E89D8F>

frontend-v2/src/modules/aia/
<0A><EFBFBD><E98EBF><EFBFBD> pages/
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> Dashboard.tsx              # <20><EFBFBD>雿枏之<E69E8F><E4B98B><EFBFBD>擐㚚△嚗?
<0A>?  <20><EFBFBD><E5A999><EFBFBD> Workspace.tsx              # 撖寡<E69296>撌乩<E6928C><E4B9A9>?
<0A><EFBFBD><E98EBF><EFBFBD> components/
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> AgentPipeline/
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> index.tsx              # 5<>嗆挾瘚<E68CBE>偌蝥踹捆<E8B8B9>?
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> StageColumn.tsx        # <20>閖𧫴畾萄<E795BE>
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> AgentCard.tsx          # <20><EFBFBD>雿枏㨃<E69E8F>?
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> IntentSearch/
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> index.tsx              # <20>誩㦛<E8AAA9>𦦵揣獢?
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> SuggestionDropdown.tsx # 撱箄悅銝𧢲<E98A9D>獢?
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> ConversationList/
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> index.tsx              # <20><>蟮隡朞<E99AA1><E69C9E>𡑒”
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> ConversationItem.tsx   # 隡朞<E99AA1>憿?
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> MessageList/
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> index.tsx              # 瘨<><E798A8><EFBFBD>𡑒”
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> UserMessage.tsx        # <20><EFBFBD><EFBFBD><E798A8>
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> AssistantMessage.tsx   # AI<41>𧼮<EFBFBD>
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> ThinkingBlock.tsx      # 瘛勗漲<E58B97><EFBFBD><E882BD><EFBFBD><EFBFBD><EFBFBD>
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> Attachment/
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> AttachmentUpload.tsx   # <20><>辣銝𠹺<E98A9D>
<0A>?  <20>?  <20><EFBFBD><E98EBF><EFBFBD> AttachmentCard.tsx     # <20><><EFBFBD><EFBFBD>
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> AttachmentPreview.tsx  # <20><>辣憸<E8BEA3><E686B8>
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> SlashCommands/
<0A>?  <20>?  <20><EFBFBD><E5A999><EFBFBD> index.tsx              # 敹急㭘<E680A5><E3AD98><EFBFBD>𨅯<EFBFBD>
<0A>?  <20><EFBFBD><E5A999><EFBFBD> ActionBar/
<0A>?      <20><EFBFBD><E5A999><EFBFBD> index.tsx              # 蝏𤘪<E89D8F><F0A498AA><EFBFBD><E6BBA2>?
<0A><EFBFBD><E98EBF><EFBFBD> hooks/
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> useConversation.ts         # 撖寡<E69296>蝞∠<E89D9E>
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> useAgents.ts               # <20><EFBFBD>雿𤘪㺭<F0A498AA>?
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> useIntentRouter.ts         # <20>誩㦛頝舐眏
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> useStreamMessage.ts        # 瘚<><E7989A><EFBFBD><E798A8>
<0A>?  <20><EFBFBD><E5A999><EFBFBD> useAttachment.ts           # <20><>辣銝𠹺<E98A9D>
<0A><EFBFBD><E98EBF><EFBFBD> api/
<0A>?  <20><EFBFBD><E5A999><EFBFBD> index.ts                   # API 撠<><E692A0>
<0A><EFBFBD><E98EBF><EFBFBD> types/
<0A>?  <20><EFBFBD><E5A999><EFBFBD> index.ts                   # TypeScript 蝐餃<E89D90>
<0A><EFBFBD><E98EBF><EFBFBD> styles/
<0A>?  <20><EFBFBD><E98EBF><EFBFBD> dashboard.module.css       # Dashboard <20><EFBFBD>
<0A>?  <20><EFBFBD><E5A999><EFBFBD> workspace.module.css       # Workspace <20><EFBFBD>
<0A><EFBFBD><E5A999><EFBFBD> index.tsx                      # 璅<E79285><E288AA>亙藁 + 頝舐眏

<EFBFBD>綫 霈曇恣閫<E681A3><E996AB>

<EFBFBD>脣蔗蝟餌<EFBFBD>

: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;
}

<EFBFBD><EFBFBD>蝟餌<EFBFBD>

/* <20>萄儐 8px 蝵烐聢 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;

<EFBFBD><EFBFBD>

/* 蝘餃𢆡隡睃<E99AA1> */
--breakpoint-sm: 640px;   /* <20>𧢲㦤 */
--breakpoint-md: 768px;   /* 撟單踎 */
--breakpoint-lg: 1024px;  /* 獢屸𢒰 */
--breakpoint-xl: 1280px;  /* 憭批<E686AD> */

<EFBFBD><EFBFBD> 憿菟𢒰霈曇恣

1. Dashboard嚗<64><EFBFBD><EFBFBD>憭批<E686AD>嚗?

// 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>**嚗?

  • 憿園<EFBFBD><EFBFBD>葉憭扳<EFBFBD><EFBFBD>
  • 5<EFBFBD>嗆挾瘚<EFBFBD>偌蝥踵赤<EFBFBD>穃像<EFBFBD><EFBFBD>獢屸𢒰嚗? 蝥萄<E89DA5>皛𡁜𢆡嚗<F0A286A1><EFBFBD><EFBFBD>
  • Gemini 憌擧聢憭抒<E686AD><E68A92>?

2. Workspace嚗<65>笆霂嘥極雿𨅯蝱嚗?

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

<EFBFBD>妝 蝏<>辣霂衣<E99C82>霈曇恣

1. AgentPipeline嚗?<3F>嗆挾瘚<E68CBE>偌蝥選<E89DA5>

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

**<2A><EFBFBD><E79195><EFBFBD>**嚗?

.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>嚗?

// 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>嚗?

// 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>?..' : `撌脫楛摨行<E691A8><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>
  );
};

**<2A><EFBFBD>**嚗?

.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>

// 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(`<60><>憭帋<E686AD>隡?${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>

// 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>嚗?

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

<EFBFBD>𦹄 Hooks 霈曇恣

useConversation

// 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

// 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 };
}

<EFBFBD>𢲡 <20><EFBFBD>撘讛挽霈?

<EFBFBD><EFBFBD>蝑𣇉裦

// <20><EFBFBD>摰帋<E691B0>
const breakpoints = {
  sm: 640,   // <20>𧢲㦤
  md: 768,   // 撟單踎嚗<E8B88E>蜓閬<E89C93><EFBFBD><EFBFBD>
  lg: 1024,  // 獢屸𢒰
  xl: 1280,  // 憭批<E686AD>
};

Dashboard <20><EFBFBD>撘?

<EFBFBD><EFBFBD> <EFBFBD><EFBFBD>
< 768px <EFBFBD>偌蝥輻熊<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>?768px <EFBFBD>偌蝥踵赤<EFBFBD>?5 <20>?

Workspace <20><EFBFBD>撘?

<EFBFBD><EFBFBD> <EFBFBD><EFBFBD>
< 768px 靘扯器<EFBFBD><EFBFBD><EFBFBD>𧶏<EFBFBD><EFBFBD><EFBFBD>皛穃枂嚗㚁<EFBFBD>颲枏<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>?768px 靘扯器<EFBFBD>誩𤐄摰𡁏遬蝷?240px

<EFBFBD><EFBFBD> 蝐餃<E89D90>摰帋<E691B0>

// 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;
}

<EFBFBD><EFBFBD> <20>湔鰵<E6B994><EFBFBD>

<EFBFBD><EFBFBD> <EFBFBD><EFBFBD>𧋦 <EFBFBD><EFBFBD>
2026-01-11 V1.0 <EFBFBD>𥕦遣<EFBFBD>滨垢蝏<EFBFBD>辣霈曇恣<EFBFBD><EFBFBD>