refactor(web): render the chat column outside App
The workbench shell still owns session and modal state. Message stream, filters, and the composer now live in ChatColumn so App stops assembling that column inline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-162
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||||
import { AppShell } from './components/layout/AppShell';
|
import { AppShell } from './components/layout/AppShell';
|
||||||
import { DocumentPane } from './components/doc/DocumentPane';
|
import { DocumentPane } from './components/doc/DocumentPane';
|
||||||
import { MessageStream } from './components/chat/MessageStream';
|
import { ChatColumn } from './components/chat/ChatColumn';
|
||||||
import { InputArea } from './components/chat/InputArea';
|
|
||||||
import { fetchFreeformDoc } from './api/freeformDoc';
|
import { fetchFreeformDoc } from './api/freeformDoc';
|
||||||
import type { DiffResult, OverlayMode } from './features/documentRevision/types';
|
import type { DiffResult, OverlayMode } from './features/documentRevision/types';
|
||||||
import { fetchProjectLineage, getProject } from './api/projectWorkflow';
|
import { fetchProjectLineage, getProject } from './api/projectWorkflow';
|
||||||
@@ -35,7 +34,6 @@ import {
|
|||||||
import { DocumentLineage } from './types/projectWorkflow';
|
import { DocumentLineage } from './types/projectWorkflow';
|
||||||
import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice';
|
import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice';
|
||||||
import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner';
|
import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner';
|
||||||
import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard';
|
|
||||||
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
|
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
|
||||||
import { UserSettingsModal } from './components/user/UserSettingsModal';
|
import { UserSettingsModal } from './components/user/UserSettingsModal';
|
||||||
import { OnDemand } from './components/lazy/OnDemand';
|
import { OnDemand } from './components/lazy/OnDemand';
|
||||||
@@ -114,13 +112,8 @@ import { RequestEpoch, shareInflight } from './data/initFanout';
|
|||||||
import {
|
import {
|
||||||
resolveSessionProjectId,
|
resolveSessionProjectId,
|
||||||
} from './lib/sessionAdapter';
|
} from './lib/sessionAdapter';
|
||||||
import { CategoryFilterBar } from './components/history/CategoryFilterBar';
|
import { navigateToAnchor, parseAnchorHash } from './engine/anchorNavigator';
|
||||||
import { HistoryCalendar } from './components/history/HistoryCalendar';
|
|
||||||
import { ChatSearchModal } from './components/history/ChatSearchModal';
|
|
||||||
import { DigestTimelineModal } from './components/history/DigestTimelineModal';
|
|
||||||
import { navigateToAnchor, parseAnchorHash, resolveTurnAnchorId } from './engine/anchorNavigator';
|
|
||||||
import { useUnreadMarks } from './hooks/useUnreadMarks';
|
import { useUnreadMarks } from './hooks/useUnreadMarks';
|
||||||
import { CategoryDock } from './components/chat/CategoryDock';
|
|
||||||
import { fetchChatCategories, fetchChatDates } from './api/chatWindow';
|
import { fetchChatCategories, fetchChatDates } from './api/chatWindow';
|
||||||
import { ChatCategoryStat, ChatDateStat, QuotedMessage } from './types/workbench';
|
import { ChatCategoryStat, ChatDateStat, QuotedMessage } from './types/workbench';
|
||||||
import { saveFile } from './data/fileStore';
|
import { saveFile } from './data/fileStore';
|
||||||
@@ -134,9 +127,6 @@ export {
|
|||||||
|
|
||||||
const loadAdminConsoleView = () =>
|
const loadAdminConsoleView = () =>
|
||||||
import('./components/admin/console/AdminConsoleView').then((m) => ({ default: m.AdminConsoleView }));
|
import('./components/admin/console/AdminConsoleView').then((m) => ({ default: m.AdminConsoleView }));
|
||||||
const loadChatExportModal = () =>
|
|
||||||
import('./components/history/ChatExportModal').then((m) => ({ default: m.ChatExportModal }));
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// Wave 2:启动时幂等剔除 localStorage 中的 demo seed 残留(须先于 load*)
|
// Wave 2:启动时幂等剔除 localStorage 中的 demo seed 残留(须先于 load*)
|
||||||
purgeDemoSeedsFromLocalStorage();
|
purgeDemoSeedsFromLocalStorage();
|
||||||
@@ -1553,156 +1543,42 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
workflowBanner={workflowBanner}
|
workflowBanner={workflowBanner}
|
||||||
chatContent={
|
chatContent={
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
<ChatColumn
|
||||||
{/* P1, P2, P3: 顶部分类统计过滤栏、历史日历、三模搜索、智能摘要、导出与未读角标 */}
|
activeSessionId={activeSessionId}
|
||||||
<CategoryFilterBar
|
categoryStats={categoryStats}
|
||||||
categories={categoryStats}
|
chatWindow={chatWindow}
|
||||||
selectedCategory={chatWindow.selectedCategory}
|
isCalendarOpen={isCalendarOpen}
|
||||||
onSelectCategory={chatWindow.setSelectedCategory}
|
setIsCalendarOpen={setIsCalendarOpen}
|
||||||
onNavigateToCategory={(_cat, lastMessageId) => {
|
isSearchOpen={isSearchOpen}
|
||||||
if (lastMessageId && activeSessionId) {
|
setIsSearchOpen={setIsSearchOpen}
|
||||||
const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
|
isDigestOpen={isDigestOpen}
|
||||||
navigateToAnchor(
|
setIsDigestOpen={setIsDigestOpen}
|
||||||
{ sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
|
isExportOpen={isExportOpen}
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
setIsExportOpen={setIsExportOpen}
|
||||||
);
|
unreadTotal={unreadTotal}
|
||||||
}
|
dateStats={dateStats}
|
||||||
}}
|
unreadMarks={unreadMarks}
|
||||||
onToggleCalendar={() => setIsCalendarOpen((prev) => !prev)}
|
unreadMessageIds={unreadMessageIds}
|
||||||
isCalendarOpen={isCalendarOpen}
|
isAllReadCompleted={isAllReadCompleted}
|
||||||
onOpenSearch={() => setIsSearchOpen(true)}
|
consumeNextUnread={consumeNextUnread}
|
||||||
onOpenDigest={() => setIsDigestOpen(true)}
|
handleSelectDoc={handleSelectDoc}
|
||||||
onOpenExport={() => setIsExportOpen(true)}
|
handleCardAction={handleCardAction}
|
||||||
unreadTotal={unreadTotal}
|
handleErrorAction={handleErrorAction}
|
||||||
/>
|
handleQuoteMessage={handleQuoteMessage}
|
||||||
{isCalendarOpen && (
|
handleSaveMessageToPrivate={handleSaveMessageToPrivate}
|
||||||
<HistoryCalendar
|
handleExportReplyDocx={handleExportReplyDocx}
|
||||||
dateStats={dateStats}
|
pendingProposal={pendingProposal}
|
||||||
unreadMarks={unreadMarks}
|
proposalError={proposalError}
|
||||||
onSelectDate={(date) => {
|
handleAcceptProposal={handleAcceptProposal}
|
||||||
chatWindow.jumpToDate(date);
|
handleRejectProposal={handleRejectProposal}
|
||||||
setIsCalendarOpen(false);
|
handleSendMessage={handleSendMessage}
|
||||||
}}
|
isThinking={isThinking}
|
||||||
onClose={() => setIsCalendarOpen(false)}
|
isStreamingActive={isStreamingActive}
|
||||||
/>
|
handleStopStreaming={handleStopStreaming}
|
||||||
)}
|
activeQuotes={activeQuotes}
|
||||||
{isSearchOpen && (
|
handleRemoveQuote={handleRemoveQuote}
|
||||||
<ChatSearchModal
|
isCurrentProjectExited={isCurrentProjectExited}
|
||||||
sessionId={activeSessionId}
|
/>
|
||||||
onSelectHit={(hit) => {
|
|
||||||
navigateToAnchor(
|
|
||||||
{ sessionId: activeSessionId, messageId: hit.message_id },
|
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
|
||||||
);
|
|
||||||
setIsSearchOpen(false);
|
|
||||||
}}
|
|
||||||
onClose={() => setIsSearchOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isDigestOpen && (
|
|
||||||
<DigestTimelineModal
|
|
||||||
sessionId={activeSessionId}
|
|
||||||
onJumpToMessage={(messageId) => {
|
|
||||||
navigateToAnchor(
|
|
||||||
{ sessionId: activeSessionId, messageId },
|
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
|
||||||
);
|
|
||||||
setIsDigestOpen(false);
|
|
||||||
}}
|
|
||||||
onClose={() => setIsDigestOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isExportOpen && (
|
|
||||||
<OnDemand
|
|
||||||
label="聊天导出"
|
|
||||||
load={loadChatExportModal}
|
|
||||||
componentProps={{
|
|
||||||
isOpen: isExportOpen,
|
|
||||||
onClose: () => setIsExportOpen(false),
|
|
||||||
sessionId: activeSessionId,
|
|
||||||
dayStack: chatWindow.displayDayStack,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div style={{ position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<MessageStream
|
|
||||||
sessionId={activeSessionId}
|
|
||||||
dayStack={chatWindow.displayDayStack}
|
|
||||||
isLoadingOlder={chatWindow.phase === 'loading_older'}
|
|
||||||
hasMoreBefore={chatWindow.hasMoreBefore}
|
|
||||||
onLoadOlder={chatWindow.loadOlder}
|
|
||||||
onLoadFullDay={chatWindow.loadFullDay}
|
|
||||||
onOpenCase={handleSelectDoc}
|
|
||||||
onCardAction={handleCardAction}
|
|
||||||
onErrorAction={handleErrorAction}
|
|
||||||
onQuote={handleQuoteMessage}
|
|
||||||
onNavigateToAnchor={(targetAnchor) => {
|
|
||||||
navigateToAnchor(
|
|
||||||
{ sessionId: activeSessionId, messageId: targetAnchor },
|
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
selectedCategory={chatWindow.selectedCategory}
|
|
||||||
unreadMessageIds={unreadMessageIds}
|
|
||||||
onSaveToPrivate={handleSaveMessageToPrivate}
|
|
||||||
onExportDocx={handleExportReplyDocx}
|
|
||||||
onOpenDoc={handleSelectDoc}
|
|
||||||
/>
|
|
||||||
<CategoryDock
|
|
||||||
categories={categoryStats}
|
|
||||||
activeCategory={chatWindow.selectedCategory}
|
|
||||||
messages={chatWindow.allMessages}
|
|
||||||
unreadTotal={unreadTotal}
|
|
||||||
unreadMessageIds={unreadMessageIds}
|
|
||||||
isAllReadCompleted={isAllReadCompleted}
|
|
||||||
onConsumeNextUnread={async () => {
|
|
||||||
const targetId = await consumeNextUnread();
|
|
||||||
if (targetId && activeSessionId) {
|
|
||||||
navigateToAnchor(
|
|
||||||
{ sessionId: activeSessionId, messageId: targetId, highlightMs: 2000 },
|
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onNavigateToCategory={(_cat, lastMessageId) => {
|
|
||||||
if (lastMessageId && activeSessionId) {
|
|
||||||
const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
|
|
||||||
navigateToAnchor(
|
|
||||||
{ sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
|
|
||||||
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSelectCategory={chatWindow.setSelectedCategory}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{pendingProposal && (
|
|
||||||
<div style={{ padding: '0 16px' }}>
|
|
||||||
{proposalError && (
|
|
||||||
<div style={{ color: 'var(--danger, #b91c1c)', fontSize: 12, marginBottom: 8 }}>
|
|
||||||
{proposalError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ProposalConfirmCard
|
|
||||||
proposedChanges={pendingProposal.changes}
|
|
||||||
impactAnalysis={pendingProposal.impact}
|
|
||||||
potentialRisk={pendingProposal.risk}
|
|
||||||
onAccept={handleAcceptProposal}
|
|
||||||
onReject={handleRejectProposal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<InputArea
|
|
||||||
onSendMessage={handleSendMessage}
|
|
||||||
isThinking={isThinking}
|
|
||||||
isStreaming={isStreamingActive}
|
|
||||||
onStopStreaming={handleStopStreaming}
|
|
||||||
quotes={activeQuotes}
|
|
||||||
onRemoveQuote={handleRemoveQuote}
|
|
||||||
disabled={isCurrentProjectExited}
|
|
||||||
disabledMessage="您已退出该研发项目。依据受控体系审计要求,会话已转为只读留痕模式,无法继续发送消息。"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
docContent={
|
docContent={
|
||||||
<DocumentPane
|
<DocumentPane
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
test('chat column lives outside the workbench shell', () => {
|
||||||
|
const app = readFileSync(new URL('../../App.tsx', import.meta.url), 'utf8');
|
||||||
|
const column = readFileSync(new URL('./ChatColumn.tsx', import.meta.url), 'utf8');
|
||||||
|
assert.equal((app.match(/<ChatColumn\b/g) || []).length, 1);
|
||||||
|
assert.equal(app.includes('<MessageStream'), false);
|
||||||
|
assert.equal(app.includes('<InputArea'), false);
|
||||||
|
assert.equal(column.includes('<MessageStream'), true);
|
||||||
|
assert.equal(column.includes('<InputArea'), true);
|
||||||
|
assert.equal(column.includes('ProposalConfirmCard'), true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { OnDemand } from '../lazy/OnDemand';
|
||||||
|
import { CategoryFilterBar } from '../history/CategoryFilterBar';
|
||||||
|
import { HistoryCalendar } from '../history/HistoryCalendar';
|
||||||
|
import { ChatSearchModal } from '../history/ChatSearchModal';
|
||||||
|
import { DigestTimelineModal } from '../history/DigestTimelineModal';
|
||||||
|
import { navigateToAnchor, resolveTurnAnchorId } from '../../engine/anchorNavigator';
|
||||||
|
import { CategoryDock } from './CategoryDock';
|
||||||
|
import { MessageStream } from './MessageStream';
|
||||||
|
import { InputArea } from './InputArea';
|
||||||
|
import { ProposalConfirmCard } from '../doc/ProposalConfirmCard';
|
||||||
|
import type { UseChatWindowReturn } from '../../hooks/useChatWindow';
|
||||||
|
import type { ChatCategoryStat, ChatDateStat, ChatMessage, QuotedMessage, UnreadMark } from '../../types/workbench';
|
||||||
|
|
||||||
|
const loadChatExportModal = () =>
|
||||||
|
import('../history/ChatExportModal').then((m) => ({ default: m.ChatExportModal }));
|
||||||
|
|
||||||
|
export interface ChatColumnProps {
|
||||||
|
activeSessionId: string;
|
||||||
|
categoryStats: ChatCategoryStat[];
|
||||||
|
chatWindow: UseChatWindowReturn;
|
||||||
|
isCalendarOpen: boolean;
|
||||||
|
setIsCalendarOpen: (next: boolean | ((prev: boolean) => boolean)) => void;
|
||||||
|
isSearchOpen: boolean;
|
||||||
|
setIsSearchOpen: (open: boolean) => void;
|
||||||
|
isDigestOpen: boolean;
|
||||||
|
setIsDigestOpen: (open: boolean) => void;
|
||||||
|
isExportOpen: boolean;
|
||||||
|
setIsExportOpen: (open: boolean) => void;
|
||||||
|
unreadTotal: number;
|
||||||
|
dateStats: ChatDateStat[];
|
||||||
|
unreadMarks: UnreadMark[];
|
||||||
|
unreadMessageIds: number[];
|
||||||
|
isAllReadCompleted: boolean;
|
||||||
|
consumeNextUnread: () => Promise<number | null | undefined>;
|
||||||
|
handleSelectDoc: (id: string) => void;
|
||||||
|
handleCardAction: (cardId: string, actionId: string, payload?: unknown) => void;
|
||||||
|
handleErrorAction: (messageId: string, action: 'retry' | 'continue' | 'restart') => void;
|
||||||
|
handleQuoteMessage: (message: ChatMessage) => void;
|
||||||
|
handleSaveMessageToPrivate: (message: ChatMessage) => void;
|
||||||
|
handleExportReplyDocx: (message: ChatMessage) => void | Promise<void>;
|
||||||
|
pendingProposal: { changes: string; impact: string; risk: string } | null;
|
||||||
|
proposalError: string | null;
|
||||||
|
handleAcceptProposal: () => void;
|
||||||
|
handleRejectProposal: () => void;
|
||||||
|
handleSendMessage: React.ComponentProps<typeof InputArea>['onSendMessage'];
|
||||||
|
isThinking: boolean;
|
||||||
|
isStreamingActive: boolean;
|
||||||
|
handleStopStreaming: () => void;
|
||||||
|
activeQuotes: QuotedMessage[];
|
||||||
|
handleRemoveQuote: (index: number) => void;
|
||||||
|
isCurrentProjectExited: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatColumn({
|
||||||
|
activeSessionId,
|
||||||
|
categoryStats,
|
||||||
|
chatWindow,
|
||||||
|
isCalendarOpen,
|
||||||
|
setIsCalendarOpen,
|
||||||
|
isSearchOpen,
|
||||||
|
setIsSearchOpen,
|
||||||
|
isDigestOpen,
|
||||||
|
setIsDigestOpen,
|
||||||
|
isExportOpen,
|
||||||
|
setIsExportOpen,
|
||||||
|
unreadTotal,
|
||||||
|
dateStats,
|
||||||
|
unreadMarks,
|
||||||
|
unreadMessageIds,
|
||||||
|
isAllReadCompleted,
|
||||||
|
consumeNextUnread,
|
||||||
|
handleSelectDoc,
|
||||||
|
handleCardAction,
|
||||||
|
handleErrorAction,
|
||||||
|
handleQuoteMessage,
|
||||||
|
handleSaveMessageToPrivate,
|
||||||
|
handleExportReplyDocx,
|
||||||
|
pendingProposal,
|
||||||
|
proposalError,
|
||||||
|
handleAcceptProposal,
|
||||||
|
handleRejectProposal,
|
||||||
|
handleSendMessage,
|
||||||
|
isThinking,
|
||||||
|
isStreamingActive,
|
||||||
|
handleStopStreaming,
|
||||||
|
activeQuotes,
|
||||||
|
handleRemoveQuote,
|
||||||
|
isCurrentProjectExited,
|
||||||
|
}: ChatColumnProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
|
<CategoryFilterBar
|
||||||
|
categories={categoryStats}
|
||||||
|
selectedCategory={chatWindow.selectedCategory}
|
||||||
|
onSelectCategory={chatWindow.setSelectedCategory}
|
||||||
|
onNavigateToCategory={(_cat, lastMessageId) => {
|
||||||
|
if (lastMessageId && activeSessionId) {
|
||||||
|
const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onToggleCalendar={() => setIsCalendarOpen((prev) => !prev)}
|
||||||
|
isCalendarOpen={isCalendarOpen}
|
||||||
|
onOpenSearch={() => setIsSearchOpen(true)}
|
||||||
|
onOpenDigest={() => setIsDigestOpen(true)}
|
||||||
|
onOpenExport={() => setIsExportOpen(true)}
|
||||||
|
unreadTotal={unreadTotal}
|
||||||
|
/>
|
||||||
|
{isCalendarOpen && (
|
||||||
|
<HistoryCalendar
|
||||||
|
dateStats={dateStats}
|
||||||
|
unreadMarks={unreadMarks}
|
||||||
|
onSelectDate={(date) => {
|
||||||
|
chatWindow.jumpToDate(date);
|
||||||
|
setIsCalendarOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => setIsCalendarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isSearchOpen && (
|
||||||
|
<ChatSearchModal
|
||||||
|
sessionId={activeSessionId}
|
||||||
|
onSelectHit={(hit) => {
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId: hit.message_id },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => setIsSearchOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isDigestOpen && (
|
||||||
|
<DigestTimelineModal
|
||||||
|
sessionId={activeSessionId}
|
||||||
|
onJumpToMessage={(messageId) => {
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
setIsDigestOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => setIsDigestOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isExportOpen && (
|
||||||
|
<OnDemand
|
||||||
|
label="聊天导出"
|
||||||
|
load={loadChatExportModal}
|
||||||
|
componentProps={{
|
||||||
|
isOpen: isExportOpen,
|
||||||
|
onClose: () => setIsExportOpen(false),
|
||||||
|
sessionId: activeSessionId,
|
||||||
|
dayStack: chatWindow.displayDayStack,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{ position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<MessageStream
|
||||||
|
sessionId={activeSessionId}
|
||||||
|
dayStack={chatWindow.displayDayStack}
|
||||||
|
isLoadingOlder={chatWindow.phase === 'loading_older'}
|
||||||
|
hasMoreBefore={chatWindow.hasMoreBefore}
|
||||||
|
onLoadOlder={chatWindow.loadOlder}
|
||||||
|
onLoadFullDay={chatWindow.loadFullDay}
|
||||||
|
onOpenCase={handleSelectDoc}
|
||||||
|
onCardAction={handleCardAction}
|
||||||
|
onErrorAction={handleErrorAction}
|
||||||
|
onQuote={handleQuoteMessage}
|
||||||
|
onNavigateToAnchor={(targetAnchor) => {
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId: targetAnchor },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
selectedCategory={chatWindow.selectedCategory}
|
||||||
|
unreadMessageIds={unreadMessageIds}
|
||||||
|
onSaveToPrivate={handleSaveMessageToPrivate}
|
||||||
|
onExportDocx={handleExportReplyDocx}
|
||||||
|
onOpenDoc={handleSelectDoc}
|
||||||
|
/>
|
||||||
|
<CategoryDock
|
||||||
|
categories={categoryStats}
|
||||||
|
activeCategory={chatWindow.selectedCategory}
|
||||||
|
messages={chatWindow.allMessages}
|
||||||
|
unreadTotal={unreadTotal}
|
||||||
|
unreadMessageIds={unreadMessageIds}
|
||||||
|
isAllReadCompleted={isAllReadCompleted}
|
||||||
|
onConsumeNextUnread={async () => {
|
||||||
|
const targetId = await consumeNextUnread();
|
||||||
|
if (targetId && activeSessionId) {
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId: targetId, highlightMs: 2000 },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onNavigateToCategory={(_cat, lastMessageId) => {
|
||||||
|
if (lastMessageId && activeSessionId) {
|
||||||
|
const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
|
||||||
|
navigateToAnchor(
|
||||||
|
{ sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
|
||||||
|
{ sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSelectCategory={chatWindow.setSelectedCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{pendingProposal && (
|
||||||
|
<div style={{ padding: '0 16px' }}>
|
||||||
|
{proposalError && (
|
||||||
|
<div style={{ color: 'var(--danger, #b91c1c)', fontSize: 12, marginBottom: 8 }}>
|
||||||
|
{proposalError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ProposalConfirmCard
|
||||||
|
proposedChanges={pendingProposal.changes}
|
||||||
|
impactAnalysis={pendingProposal.impact}
|
||||||
|
potentialRisk={pendingProposal.risk}
|
||||||
|
onAccept={handleAcceptProposal}
|
||||||
|
onReject={handleRejectProposal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<InputArea
|
||||||
|
onSendMessage={handleSendMessage}
|
||||||
|
isThinking={isThinking}
|
||||||
|
isStreaming={isStreamingActive}
|
||||||
|
onStopStreaming={handleStopStreaming}
|
||||||
|
quotes={activeQuotes}
|
||||||
|
onRemoveQuote={handleRemoveQuote}
|
||||||
|
disabled={isCurrentProjectExited}
|
||||||
|
disabledMessage="您已退出该研发项目。依据受控体系审计要求,会话已转为只读留痕模式,无法继续发送消息。"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user