Skip to content

Commit 5b6d2da

Browse files
authored
feat: integrate AI chat panel and enhance document editing experience (#298)
* feat: integrate AI chat panel and enhance document editing experience * feat(chat-panel): add new components for brainstorming and chat states, enhance document reference handling
1 parent 7a5628a commit 5b6d2da

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1580
-4267
lines changed

apps/DocFlow/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"last 2 Edge versions"
3030
],
3131
"scripts": {
32-
"dev": "next dev",
32+
"dev": "next dev --webpack",
3333
"build": "next build --turbo",
3434
"build:webpack": "next build --webpack",
3535
"build:analyze": "next experimental-analyze",
@@ -174,14 +174,14 @@
174174
"react-hot-toast": "^2.5.2",
175175
"react-markdown": "^10.1.0",
176176
"react-photo-view": "^1.2.7",
177-
"react-resizable-panels": "^3.0.2",
178177
"react-use-audio-player": "^4.0.2",
179178
"reactflow": "^11.11.4",
180179
"recharts": "2.15.4",
181180
"remark": "^15.0.1",
182181
"remark-gfm": "^4.0.1",
183182
"remark-parse": "^11.0.0",
184183
"require-in-the-middle": "^8.0.1",
184+
"react-resizable-panels": "^4.6.2",
185185
"snapdom": "^0.1.2",
186186
"socket.io-client": "^4.8.1",
187187
"sonner": "^2.0.6",

apps/DocFlow/src/app/docs/[room]/page.tsx

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IndexeddbPersistence } from 'y-indexeddb';
1010
import { HocuspocusProvider } from '@hocuspocus/provider';
1111
import { Eye } from 'lucide-react';
1212
import dynamic from 'next/dynamic';
13+
import { Group, Panel, Separator } from 'react-resizable-panels';
1314

1415
// 动态导入 CommentPanel,禁用 SSR
1516
const CommentPanel = dynamic(
@@ -20,8 +21,19 @@ const CommentPanel = dynamic(
2021
loading: () => null,
2122
},
2223
);
24+
25+
// 动态导入 ChatPanel,禁用 SSR
26+
const ChatPanel = dynamic(
27+
() => import('@/app/docs/_components/ChatPanel').then((mod) => ({ default: mod.ChatPanel })),
28+
{
29+
ssr: false,
30+
loading: () => null,
31+
},
32+
);
33+
2334
import { ExtensionKit } from '@/extensions/extension-kit';
2435
import { getCursorColorByUserId, getAuthToken } from '@/utils';
36+
import { getSelectionLineRange } from '@/utils/editor';
2537
import DocumentHeader from '@/app/docs/_components/DocumentHeader';
2638
import { FloatingToc } from '@/app/docs/_components/FloatingToc';
2739
import { SearchPanel } from '@/app/docs/_components/SearchPanel';
@@ -39,6 +51,7 @@ import { useCommentStore } from '@/stores/commentStore';
3951
import { useEditorStore } from '@/stores/editorStore';
4052
import { useEditorHistory } from '@/hooks/useEditorHistory';
4153
import { storage, STORAGE_KEYS } from '@/utils/storage/local-storage';
54+
import { useChatStore } from '@/stores/chatStore';
4255

4356
// 类型定义
4457
interface CollaborationUser {
@@ -58,6 +71,7 @@ export default function DocumentPage() {
5871
const forceReadOnly = searchParams?.get('readonly') === 'true';
5972

6073
const { documentGroups } = useFileStore();
74+
const { isOpen: isChatOpen } = useChatStore();
6175

6276
// 防止水合不匹配的强制客户端渲染
6377
const [isMounted, setIsMounted] = useState(false);
@@ -359,7 +373,7 @@ export default function DocumentPage() {
359373
}
360374
}, [editor, isPanelOpen, closePanel]);
361375

362-
// Ctrl+C 复制选中文本为 JSON 格式
376+
// Ctrl+C 复制选中文本为 JSON 格式,并添加文档引用元数据
363377
useEffect(() => {
364378
if (!editor) return;
365379

@@ -369,15 +383,37 @@ export default function DocumentPage() {
369383
if (!selection || selection.isCollapsed) return;
370384

371385
try {
372-
// 2. 获取数据
386+
// 2. 获取选中的文本内容
387+
const selectedText = selection.toString();
388+
if (!selectedText) return;
389+
390+
// 3. 获取编辑器 JSON 数据
373391
const json = editor.getJSON();
374392
const jsonString = JSON.stringify(json);
375393

376-
// 3. 关键修复:使用 e.clipboardData 而不是 navigator.clipboard
377-
// 这样可以避免异步权限问题和 'clipboard is not defined' 错误
394+
const { from, to } = editor.state.selection;
395+
const { startLine, endLine } = getSelectionLineRange(editor.state.doc, from, to);
396+
397+
// 5. 构建文档引用元数据
398+
const documentName = getCurrentDocumentName() || '未命名文档';
399+
const referenceData = {
400+
type: 'docflow-reference',
401+
fileName: documentName,
402+
startLine: Math.max(1, startLine - 1),
403+
endLine: Math.max(1, endLine - 1),
404+
content: selectedText,
405+
charCount: selectedText.length,
406+
};
407+
408+
// 6. 使用 e.clipboardData 设置多种格式
378409
if (e.clipboardData) {
410+
// 设置纯文本(保持默认复制行为)
411+
e.clipboardData.setData('text/plain', selectedText);
412+
// 设置 JSON 格式(原有功能)
379413
e.clipboardData.setData('text/json', jsonString);
380-
e.preventDefault(); // 只有成功设置数据后才阻止默认行为
414+
// 设置文档引用元数据(新功能)
415+
e.clipboardData.setData('application/docflow-reference', JSON.stringify(referenceData));
416+
e.preventDefault();
381417
}
382418
} catch (error) {
383419
console.error('复制失败:', error);
@@ -525,16 +561,33 @@ export default function DocumentPage() {
525561
doc={doc}
526562
/>
527563

528-
{/* 主内容区域 */}
564+
{/* 主内容区域 - 使用可调整大小的面板布局 */}
529565
<div className="flex flex-1 overflow-hidden">
530-
<div className="flex-1 relative overflow-hidden">
531-
<div
532-
ref={editorContainRef}
533-
className="h-full overflow-y-auto overflow-x-hidden relative w-full"
534-
>
535-
<EditorContent editor={editor} className="prose-container h-full pl-14" />
536-
</div>
537-
</div>
566+
<Group orientation="horizontal" className="flex-1">
567+
{/* 编辑器面板 */}
568+
<Panel defaultSize={isChatOpen ? 65 : 100} minSize={30}>
569+
<div className="h-full relative overflow-hidden">
570+
<div
571+
ref={editorContainRef}
572+
className="h-full overflow-y-auto overflow-x-hidden relative w-full"
573+
>
574+
<EditorContent editor={editor} className="prose-container h-full pl-14" />
575+
</div>
576+
</div>
577+
</Panel>
578+
579+
{/* 聊天面板分隔条 */}
580+
{isChatOpen && (
581+
<>
582+
<Separator className="w-1 bg-gray-200 dark:bg-gray-800 hover:bg-blue-500 dark:hover:bg-blue-500 transition-colors cursor-col-resize" />
583+
<Panel defaultSize={35} minSize={20} maxSize={60}>
584+
<Activity mode={isChatOpen ? 'visible' : 'hidden'}>
585+
<ChatPanel documentId={documentId} />
586+
</Activity>
587+
</Panel>
588+
</>
589+
)}
590+
</Group>
538591
</div>
539592

540593
{/* 右侧悬浮目录和评论面板 - 使用 Activity 优化 Selective Hydration */}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import { Lightbulb } from 'lucide-react';
4+
5+
interface BrainstormEmptyStateProps {
6+
brainstormCount: number;
7+
}
8+
9+
export function BrainstormEmptyState({ brainstormCount }: BrainstormEmptyStateProps) {
10+
return (
11+
<div className="flex flex-col items-center justify-center h-full text-center px-8">
12+
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-400 to-indigo-500 flex items-center justify-center mb-4 shadow-lg shadow-purple-200/40">
13+
<Lightbulb className="w-7 h-7 text-white" />
14+
</div>
15+
<h3 className="text-sm font-semibold text-gray-700 mb-1.5">头脑风暴模式</h3>
16+
<p className="text-xs text-gray-400 max-w-[280px] leading-relaxed">
17+
输入主题,AI 将同时生成 {brainstormCount} 个不同方案
18+
<br />
19+
<span className="text-gray-300">适合创意发散、多角度思考</span>
20+
</p>
21+
</div>
22+
);
23+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use client';
2+
3+
import { Copy, Loader2, Square, Zap } from 'lucide-react';
4+
import ReactMarkdown from 'react-markdown';
5+
import remarkGfm from 'remark-gfm';
6+
import { toast } from 'sonner';
7+
8+
import { compactMarkdownComponents } from '@/components/business/ai/markdown-components';
9+
import { cn } from '@/utils';
10+
11+
export interface BrainstormResponseItem {
12+
content: string;
13+
finished: boolean;
14+
}
15+
16+
interface BrainstormResultsProps {
17+
responses: BrainstormResponseItem[];
18+
count: number;
19+
isBrainstorming: boolean;
20+
onStop: () => void;
21+
onRegenerate: () => void;
22+
}
23+
24+
export function BrainstormResults({
25+
responses,
26+
count,
27+
isBrainstorming,
28+
onStop,
29+
onRegenerate,
30+
}: BrainstormResultsProps) {
31+
return (
32+
<div className="flex-1 p-4 overflow-y-auto">
33+
<div
34+
className={cn(
35+
'grid gap-3',
36+
count === 2 && 'grid-cols-2',
37+
count === 3 && 'grid-cols-3',
38+
count === 4 && 'grid-cols-2',
39+
count === 5 && 'grid-cols-3',
40+
)}
41+
>
42+
{responses.map((response, index) => (
43+
<div
44+
key={index}
45+
className={cn(
46+
'bg-white border-2 border-gray-200 hover:border-purple-300 rounded-xl p-3 relative min-h-[200px] group transition-all shadow-sm hover:shadow-md',
47+
isBrainstorming && !response.finished && 'border-purple-200',
48+
)}
49+
>
50+
<div className="flex items-center justify-between mb-2">
51+
<div className="flex items-center gap-1.5">
52+
<div className="flex items-center justify-center w-5 h-5 rounded-md bg-gradient-to-br from-purple-500 to-indigo-600 text-white text-[11px] font-bold shadow-sm">
53+
{index + 1}
54+
</div>
55+
{!isBrainstorming && response.content && (
56+
<span className="text-[10px] text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded">
57+
{response.content.length}
58+
</span>
59+
)}
60+
</div>
61+
{!isBrainstorming && response.content && (
62+
<button
63+
onClick={() => {
64+
navigator.clipboard.writeText(response.content);
65+
toast.success('已复制');
66+
}}
67+
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-purple-600 rounded hover:bg-purple-50 transition-all"
68+
title="复制"
69+
>
70+
<Copy className="h-3 w-3" />
71+
</button>
72+
)}
73+
</div>
74+
<div className="markdown-content text-[12px] leading-relaxed text-gray-700">
75+
{response.content ? (
76+
<>
77+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={compactMarkdownComponents}>
78+
{response.content}
79+
</ReactMarkdown>
80+
{!response.finished && (
81+
<span className="inline-block w-1 h-3 bg-gradient-to-b from-purple-500 to-indigo-600 ml-0.5 animate-pulse rounded-sm" />
82+
)}
83+
</>
84+
) : (
85+
<div className="flex items-center gap-1.5 text-gray-400 text-xs">
86+
<Loader2 className="h-3 w-3 animate-spin" />
87+
生成中...
88+
</div>
89+
)}
90+
</div>
91+
</div>
92+
))}
93+
</div>
94+
{isBrainstorming && (
95+
<div className="flex justify-center mt-4">
96+
<button
97+
onClick={onStop}
98+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-red-600 hover:text-red-700 rounded-lg border-2 border-red-300 hover:border-red-400 bg-white hover:bg-red-50 transition-all shadow-sm"
99+
>
100+
<Square className="h-3 w-3 fill-current" />
101+
停止生成
102+
</button>
103+
</div>
104+
)}
105+
{!isBrainstorming && responses.some((r) => r.content) && (
106+
<div className="flex justify-center mt-4">
107+
<button
108+
onClick={onRegenerate}
109+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-gray-600 hover:text-gray-800 rounded-lg border border-gray-300 hover:border-gray-400 bg-white hover:bg-gray-50 transition-all shadow-sm"
110+
>
111+
<Zap className="h-3 w-3" />
112+
重新生成
113+
</button>
114+
</div>
115+
)}
116+
</div>
117+
);
118+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import { Bot } from 'lucide-react';
4+
5+
export function ChatEmptyState() {
6+
return (
7+
<div className="flex flex-col items-center justify-center h-full text-center px-8">
8+
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-400 to-indigo-500 flex items-center justify-center mb-4 shadow-lg shadow-blue-200/40">
9+
<Bot className="w-7 h-7 text-white" />
10+
</div>
11+
<h3 className="text-sm font-semibold text-gray-700 mb-1.5">文档 AI 助手</h3>
12+
<p className="text-xs text-gray-400 max-w-[240px] leading-relaxed">
13+
问我任何关于文档的问题,我会尽力帮助你。
14+
<br />
15+
<span className="text-gray-300">Enter 发送 · Shift+Enter 换行</span>
16+
</p>
17+
</div>
18+
);
19+
}

0 commit comments

Comments
 (0)