File size: 37,970 Bytes
019d28a
 
 
c5b4c4c
019d28a
 
 
c071bb8
019d28a
 
 
 
 
 
c071bb8
019d28a
 
 
3f2f630
019d28a
c071bb8
 
 
 
5c4258e
 
 
 
c071bb8
 
 
 
 
5c4258e
 
 
 
 
 
019d28a
 
 
3f2f630
019d28a
3f2f630
019d28a
c071bb8
5c4258e
c071bb8
 
 
 
 
 
 
 
 
 
5c4258e
c071bb8
 
019d28a
 
 
c071bb8
019d28a
c071bb8
 
 
 
 
 
5c4258e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
019d28a
 
 
 
 
 
8e5411a
2b32ad3
019d28a
 
 
 
c071bb8
 
 
 
 
019d28a
 
 
 
 
 
b0770b8
019d28a
c5b4c4c
c071bb8
f4d1cb8
019d28a
2b32ad3
019d28a
2b32ad3
 
 
 
 
 
 
f4d1cb8
 
 
 
 
 
 
 
 
 
019d28a
 
 
 
 
 
 
 
c071bb8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
019d28a
 
 
 
 
f4d1cb8
 
 
 
 
 
 
 
 
019d28a
c071bb8
019d28a
 
 
 
c071bb8
 
019d28a
 
 
c071bb8
f4d1cb8
 
019d28a
c071bb8
 
019d28a
 
 
 
 
c071bb8
019d28a
c071bb8
019d28a
 
 
 
 
c071bb8
019d28a
 
 
 
62849dd
 
019d28a
 
 
 
f4d1cb8
 
 
 
019d28a
c071bb8
019d28a
 
c071bb8
019d28a
 
c071bb8
 
 
 
 
019d28a
3f2f630
c071bb8
 
019d28a
 
 
 
 
 
 
 
 
 
 
 
 
b0770b8
019d28a
8e5411a
3f2f630
 
f4d1cb8
 
019d28a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c071bb8
019d28a
 
c071bb8
019d28a
 
62849dd
 
 
019d28a
62849dd
019d28a
 
 
 
 
 
 
f4d1cb8
 
 
019d28a
f4d1cb8
019d28a
f4d1cb8
019d28a
 
 
 
3f2f630
 
 
 
 
 
 
019d28a
 
 
b0770b8
 
3f2f630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
019d28a
 
3f2f630
019d28a
 
3f2f630
019d28a
 
3f2f630
019d28a
 
 
 
3f2f630
019d28a
 
 
 
 
 
 
 
 
 
 
3f2f630
019d28a
 
 
 
 
3f2f630
019d28a
 
 
 
3f2f630
019d28a
3f2f630
 
 
 
 
019d28a
 
 
 
 
 
8e5411a
 
019d28a
 
 
 
8e5411a
019d28a
8e5411a
 
 
 
 
 
 
 
3f2f630
019d28a
 
 
 
 
f4d1cb8
019d28a
 
3f2f630
 
 
c071bb8
 
3f2f630
019d28a
 
 
 
3f2f630
 
 
019d28a
 
 
 
 
 
 
3f2f630
019d28a
 
 
 
3f2f630
019d28a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62849dd
 
 
 
 
 
 
3f2f630
019d28a
3f2f630
019d28a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f2f630
019d28a
 
 
 
 
 
 
 
 
3f2f630
 
 
 
c071bb8
3f2f630
 
 
019d28a
 
 
 
 
c071bb8
3f2f630
019d28a
 
 
3f2f630
 
019d28a
 
 
 
c071bb8
 
 
 
 
 
 
 
 
 
 
 
019d28a
 
 
3f2f630
c5b4c4c
 
 
 
2b32ad3
 
 
 
 
c5b4c4c
 
019d28a
2b32ad3
 
 
 
019d28a
c5b4c4c
c071bb8
 
 
 
019d28a
 
c071bb8
019d28a
 
 
 
 
 
f4d1cb8
 
 
 
 
 
 
 
 
 
b0770b8
 
 
 
f4d1cb8
 
 
 
 
 
019d28a
3f2f630
c071bb8
3f2f630
019d28a
 
 
 
 
b0770b8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667

import React, { useState, useRef, useEffect } from 'react';
import { AIChatMessage, User } from '../../types';
import { Bot, Send, Sparkles, Loader2, Image as ImageIcon, X, Trash2, Brain, ChevronDown, ChevronRight, Copy, Check, FileText, Plus, Paperclip, File, Globe, Square, Camera } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { compressImage } from '../../utils/mediaHelpers';
import { parseDocument } from '../../utils/documentParser';
import { Toast, ToastState } from '../Toast';

interface WorkAssistantPanelProps {
    currentUser: User | null;
}

// Optimized Roles for Formal School Context
const ROLES = [
    { 
        id: 'editor', 
        name: '公众号图文精修', 
        icon: '📝', 
        description: '自动优化文稿并智能排版图片',
        prompt: `你是一位拥有10年经验的学校官方公众号主编。你的任务是根据用户提供的草稿(或活动描述)以及上传的图片,撰写一篇高质量的官方推文。

### 核心原则
1.  **文风要求**:端庄、大气、严谨,富有教育情怀。多用四字成语(如“精准赋能”、“蓄势扬帆”)。
2.  **严禁使用 Emoji**:保持学校官方文稿的严肃性,绝对不要出现任何表情包符号。
3.  **结构规范**:标题要对仗工整,正文要有层次感(如“校园寻迹”、“专题引领”等小标题)。
4.  **内容升华**:将具体活动上升到“立德树人”、“核心素养”、“高质量发展”等高度。

### 图片排版规则 (至关重要)
- **如果有图片**:用户上传了 {{IMAGE_COUNT}} 张图片。请务必将这 {{IMAGE_COUNT}} 张图片根据段落内容逻辑,以 **(图Start-图End)** 或 **(图N)** 的格式插入到文章最合适的位置。不要遗漏任何图片。
- **如果没有图片**:如果用户没有上传任何图片,**绝对不要**在文中插入任何 "(图x)" 占位符。直接输出纯文本文章即可。

### 输出风格示例
标题:精准赋能强基石 蓄势扬帆谋新篇
(正文段落,引用诗句或名言开篇...)
...校园每一处景观都被赋予了“微笑育人”的深刻内涵。(图1-图3)
...(小标题)...
...展现了跨学科融合的教学创新。(图4)`
    },
    { 
        id: 'host', 
        name: '活动主持/致辞', 
        icon: '🎤', 
        description: '生成正式的活动主持词',
        prompt: `你是一位经验丰富的学校活动策划和主持人。
协助老师撰写活动流程、主持稿或领导/嘉宾致辞。
1. **风格**:庄重、大气、热情但不失礼仪。**严禁使用 Emoji**。
2. **内容**:逻辑清晰,环节紧凑,串词自然过渡。
3. **格式**:标明【环节名称】、【具体话术】。` 
    },
    { 
        id: 'writer', 
        name: '公文/通知润色', 
        icon: '✍️', 
        description: '将草稿转化为正式公文',
        prompt: `你是一位资深的教育系统笔杆子。
请帮助老师将口语化或草稿文字转化为正式、规范的公文或通知。
1. **目标**:准确、简练、得体。**严禁使用 Emoji**。
2. **修正**:纠正错别字和语病,规范标点符号。
3. **格式**:符合公文通报的标准格式。` 
    },
    { 
        id: 'promoter', 
        name: '宣传/海报文案', 
        icon: '📢', 
        description: '生成精炼的宣传标语',
        prompt: `你是一位擅长教育传播的文案专家。
请为学校活动撰写短小精悍的宣传语,适用于海报、展板或家长群通知。
1. **长度**:200字以内。
2. **重点**:突出亮点,语言富有感染力,但保持端庄,不要使用过于浮夸的网络用语或 Emoji。
3. **排版**:分行排版,重点突出。` 
    },
    {
        id: 'lesson_planner',
        name: '智能教案生成',
        icon: '📚',
        description: '根据内容一键生成标准教案',
        prompt: `你是一位拥有20年教龄的特级教师和教研组长,熟悉新课标要求。
请根据用户提供的【教学内容】或【课文文章】,设计一份科学、严谨、符合教育教学规律的标准教案。

### 核心原则
1. **严禁使用 Emoji**:保持教案的专业性和严谨性。
2. **结构完整**:必须包含以下标准环节。
3. **以生为本**:体现学生的主体地位和核心素养的培养。

### 输出格式
**一、教学目标**
1. 知识与技能:...
2. 过程与方法:...
3. 情感态度与价值观:...

**二、教学重难点**
1. 重点:...
2. 难点:...

**三、教学方法**
(如:讲授法、讨论法、情境教学法等)

**四、教学过程**
1. **环节一:导入新课** (设计意图:...)
   - ...
2. **环节二:新课讲授** (设计意图:...)
   - ...
3. **环节三:巩固练习** (设计意图:...)
   - ...
4. **环节四:课堂小结**
   - ...
5. **环节五:作业布置** (分层作业)
   - ...

**五、板书设计**
(简洁明了的板书结构)`
    },
    {
        id: 'speech_maker',
        name: '演讲/说书稿',
        icon: '🎙️',
        description: '将资料转化为生动的讲稿',
        prompt: `你是一位金牌演讲撰稿人和故事讲述专家。
请根据用户提供的文本资料,将其改写为一份极具感染力、适合口头表达的【演讲稿】或【说书稿】。

### 核心原则
1. **严禁使用 Emoji**:保持文稿的文学性和正式感。
2. **口语化表达**:将书面语转化为听觉语言,多用短句,避免生僻词和冗长句式。
3. **情感共鸣**:注重情感的递进和渲染,使听众产生共鸣。

### 输出结构要求
1. **【开场白】**:用一个引人入胜的提问、故事或金句开场,迅速抓住听众注意力。
2. **【正文】**:
   - 逻辑清晰,层层递进。
   - 多用具体的细节描写和生动的例子,画面感强。
   - 适当加入“(停顿)”、“(重音)”、“(环视观众)”等演讲提示词,辅助老师表达。
3. **【结语】**:升华主题,发出号召或留下回味,给听众留下深刻印象。

### 风格要求
- 语言生动形象,抑扬顿挫。
- 根据内容定调:如果是说书,要有评书的韵味;如果是演讲,要有气势和感染力。`
    }
];

export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentUser }) => {
    const [selectedRole, setSelectedRole] = useState(ROLES[0]);
    const [enableThinking, setEnableThinking] = useState(false);
    const [enableSearch, setEnableSearch] = useState(false);
    const [isMobile, setIsMobile] = useState(false);
    
    const [messages, setMessages] = useState<AIChatMessage[]>([]);
    const [textInput, setTextInput] = useState('');
    const [selectedImages, setSelectedImages] = useState<File[]>([]);
    
    const [docFile, setDocFile] = useState<File | null>(null);
    const [docContent, setDocContent] = useState<string>('');
    const [docLoading, setDocLoading] = useState(false);

    const [isProcessing, setIsProcessing] = useState(false);
    
    const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' });
    const [isThinkingExpanded, setIsThinkingExpanded] = useState<Record<string, boolean>>({});
    
    const messagesEndRef = useRef<HTMLDivElement>(null);
    const scrollContainerRef = useRef<HTMLDivElement>(null); 
    const fileInputRef = useRef<HTMLInputElement>(null);
    const cameraInputRef = useRef<HTMLInputElement>(null);
    const docInputRef = useRef<HTMLInputElement>(null);
    const abortControllerRef = useRef<AbortController | null>(null);

    // Smart Scroll & Check Mobile
    useEffect(() => {
        const checkMobile = () => {
            const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
            const mobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase());
            setIsMobile(mobile);
        };
        checkMobile();

        if (!scrollContainerRef.current || !messagesEndRef.current) return;
        const container = scrollContainerRef.current;
        const { scrollTop, scrollHeight, clientHeight } = container;
        const isNearBottom = scrollHeight - scrollTop - clientHeight < 150;
        const lastMsg = messages[messages.length - 1];
        const isUserMsg = lastMsg?.role === 'user';

        if (isNearBottom || (isUserMsg && !isProcessing)) {
            messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
        }
    }, [messages, isProcessing, isThinkingExpanded]);

    const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.target.files) {
            setSelectedImages(prev => [...prev, ...Array.from(e.target.files!)]);
        }
    };

    const handleDocSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.target.files && e.target.files.length > 0) {
            const file = e.target.files[0];
            setDocFile(file);
            setDocLoading(true);
            try {
                const text = await parseDocument(file);
                setDocContent(text);
                setToast({ show: true, message: '文档解析成功,将作为参考内容发送', type: 'success' });
            } catch (err: any) {
                setToast({ show: true, message: '文档解析失败: ' + err.message, type: 'error' });
                setDocFile(null);
                setDocContent('');
            } finally {
                setDocLoading(false);
            }
        }
    };

    const clearDoc = () => {
        setDocFile(null);
        setDocContent('');
        if (docInputRef.current) docInputRef.current.value = '';
    };

    const handleCopy = (text: string) => {
        navigator.clipboard.writeText(text);
        setToast({ show: true, message: '内容已复制', type: 'success' });
    };

    const handleStopGeneration = () => {
        if (abortControllerRef.current) {
            abortControllerRef.current.abort();
            abortControllerRef.current = null;
            setIsProcessing(false);
            setToast({ show: true, message: '已停止生成', type: 'error' });
        }
    };

    const handleSubmit = async () => {
        if ((!textInput.trim() && selectedImages.length === 0 && !docContent) || isProcessing) return;
        
        setIsProcessing(true);
        const currentText = textInput;
        const currentImages = [...selectedImages];
        const currentDocText = docContent;
        const currentDocName = docFile?.name;
        
        setTextInput('');
        setSelectedImages([]);
        clearDoc();
        
        abortControllerRef.current = new AbortController();

        const userMsgId = crypto.randomUUID();
        const aiMsgId = crypto.randomUUID();

        try {
            const base64Images = await Promise.all(currentImages.map(f => compressImage(f)));

            const newUserMsg: AIChatMessage = { 
                id: userMsgId, 
                role: 'user', 
                text: currentText + (currentDocName ? `\n\n[附带文档]: ${currentDocName}` : ''), 
                images: base64Images, 
                timestamp: Date.now() 
            };
            
            const newAiMsg: AIChatMessage = { 
                id: aiMsgId, 
                role: 'model', 
                text: '', 
                thought: '', 
                timestamp: Date.now(),
                images: base64Images,
                isSearching: enableSearch 
            };

            setMessages(prev => [...prev, newUserMsg, newAiMsg]);
            
            setTimeout(() => {
                messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
            }, 100);
            
            if (enableThinking) {
                setIsThinkingExpanded(prev => ({ ...prev, [aiMsgId]: true }));
            }

            const dynamicSystemPrompt = selectedRole.prompt.replace(/{{IMAGE_COUNT}}/g, String(base64Images.length));

            let finalPrompt = currentText;
            
            if (currentDocText) {
                finalPrompt += `\n\n【参考文档内容 (${currentDocName})】:\n${currentDocText}\n\n请根据上述文档内容和我的要求进行创作。`;
            }

            if (base64Images.length > 0) {
                finalPrompt += `\n\n[系统提示] 用户上传了 ${base64Images.length} 张图片。请务必在文章中合理位置插入 (图1-图X) 格式的占位符。`;
            } else {
                finalPrompt += `\n\n[系统提示] 用户未上传图片。请忽略所有图片排版指令,严禁在文中插入任何 (图x) 占位符,仅输出纯文字。`;
            }

            const response = await fetch('/api/ai/chat', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-user-username': currentUser?.username || '',
                    'x-user-role': currentUser?.role || '',
                    'x-school-id': currentUser?.schoolId || ''
                },
                body: JSON.stringify({ 
                    text: finalPrompt, 
                    images: base64Images, 
                    history: [], 
                    enableThinking,
                    enableSearch,
                    overrideSystemPrompt: dynamicSystemPrompt, 
                    disableAudio: true 
                }),
                signal: abortControllerRef.current.signal
            });

            if (!response.ok) throw new Error(response.statusText);
            if (!response.body) throw new Error('No response body');

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let aiTextAccumulated = '';
            let aiThoughtAccumulated = '';
            let buffer = '';

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                
                buffer += decoder.decode(value, { stream: true });
                const parts = buffer.split('\n\n');
                buffer = parts.pop() || ''; 
                
                for (const line of parts) {
                    if (line.startsWith('data: ')) {
                        const jsonStr = line.replace('data: ', '').trim();
                        if (jsonStr === '[DONE]') break;
                        
                        try {
                            const data = JSON.parse(jsonStr);
                            
                            if (data.type === 'thinking') {
                                aiThoughtAccumulated += data.content;
                                setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, thought: aiThoughtAccumulated } : m));
                            } else if (data.type === 'text') {
                                if (aiTextAccumulated === '' && aiThoughtAccumulated !== '') {
                                    setIsThinkingExpanded(prev => ({ ...prev, [aiMsgId]: false }));
                                }
                                aiTextAccumulated += data.content;
                                setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: aiTextAccumulated, isSearching: false } : m));
                            } else if (data.type === 'search') {
                                setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, isSearching: true } : m));
                            } else if (data.type === 'error') {
                                setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: `⚠️ 错误: ${data.message}`, isSearching: false } : m));
                            }
                        } catch (e) {}
                    }
                }
            }

        } catch (error: any) {
            if (error.name !== 'AbortError') {
                setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: `抱歉,处理失败: ${error.message}`, isSearching: false } : m));
            }
        } finally {
            setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, isSearching: false } : m));
            setIsProcessing(false);
            abortControllerRef.current = null;
        }
    };

    const renderContent = (text: string, sourceImages: string[] | undefined) => {
        if (!sourceImages || sourceImages.length === 0) {
            return <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>;
        }

        const splitRegex = /((?图\d+(?:-图\d+)?)?)/g;
        const parts = text.split(splitRegex);

        return (
            <div>
                {parts.map((part, idx) => {
                    const rangeMatch = part.match(/图(\d+)-图(\d+)/); 
                    const singleMatch = part.match(/图(\d+)/); 

                    if (rangeMatch) {
                        const start = parseInt(rangeMatch[1]);
                        const end = parseInt(rangeMatch[2]);
                        const imagesToShow: string[] = [];
                        
                        for (let i = start; i <= end; i++) {
                            if (sourceImages[i-1]) imagesToShow.push(sourceImages[i-1]);
                        }

                        if (imagesToShow.length > 0) {
                            return (
                                <div key={idx} className="my-4 bg-gray-50 p-2 rounded-lg border border-gray-100">
                                    <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
                                        {imagesToShow.map((img, i) => (
                                            <div key={i} className="relative aspect-[4/3] group">
                                                <img src={`data:image/jpeg;base64,${img}`} className="w-full h-full object-cover rounded-md cursor-pointer hover:opacity-90 transition-opacity" title={` ${start+i}`}/>
                                                <div className="absolute bottom-1 right-1 bg-black/60 text-white text-[10px] px-1.5 rounded">图 {start+i}</div>
                                            </div>
                                        ))}
                                    </div>
                                    <div className="text-center text-xs text-gray-400 mt-2 font-mono">{part}</div>
                                </div>
                            );
                        }
                    } else if (singleMatch && !part.includes('-')) {
                        const imgIndex = parseInt(singleMatch[1]) - 1;
                        if (sourceImages[imgIndex]) {
                            return (
                                <div key={idx} className="my-4 flex flex-col items-center">
                                    <img 
                                        src={`data:image/jpeg;base64,${sourceImages[imgIndex]}`} 
                                        className="max-w-full md:max-w-sm rounded-lg shadow-sm border border-gray-200" 
                                        alt={`Image ${imgIndex + 1}`} 
                                    />
                                    <div className="text-center text-xs text-gray-400 mt-1 font-mono">{part}</div>
                                </div>
                            );
                        }
                    }
                    return <ReactMarkdown key={idx} remarkPlugins={[remarkGfm]} components={{p: ({node, ...props}) => <p className="mb-2 last:mb-0 inline" {...props}/>}}>{part}</ReactMarkdown>;
                })}
            </div>
        );
    };

    return (
        <div className="flex flex-col h-full bg-slate-50 relative">
            {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})}/>}
            
            <div className="bg-white px-6 py-3 border-b border-gray-200 flex flex-wrap gap-4 items-center justify-between shadow-sm shrink-0 z-20">
                <div className="flex items-center gap-2">
                    <span className="text-sm font-bold text-gray-700">工作模式:</span>
                    <div className="relative group">
                        <button className="flex items-center gap-2 px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-bold border border-indigo-100 hover:bg-indigo-100 transition-colors">
                            <span>{selectedRole.icon} {selectedRole.name}</span>
                            <ChevronDown size={14}/>
                        </button>
                        <div className="absolute top-full left-0 mt-1 w-64 bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden hidden group-hover:block z-50">
                            {ROLES.map(role => (
                                <button 
                                    key={role.id}
                                    onClick={() => setSelectedRole(role)}
                                    className={`w-full text-left px-4 py-3 hover:bg-indigo-50 flex items-start gap-3 border-b border-gray-50 last:border-0 ${selectedRole.id === role.id ? 'bg-indigo-50' : ''}`}
                                >
                                    <div className="text-xl mt-0.5">{role.icon}</div>
                                    <div>
                                        <div className={`text-sm font-bold ${selectedRole.id === role.id ? 'text-indigo-700' : 'text-gray-800'}`}>{role.name}</div>
                                        <div className="text-xs text-gray-500 mt-0.5">{role.description}</div>
                                    </div>
                                </button>
                            ))}
                        </div>
                    </div>
                </div>

                <div className="flex items-center gap-3">
                    <div className="flex items-center gap-1" title="开启后AI会进行更深入的逻辑推演">
                        <label className="relative inline-flex items-center cursor-pointer">
                            <input type="checkbox" checked={enableThinking} onChange={e => setEnableThinking(e.target.checked)} className="sr-only peer"/>
                            <div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-purple-600"></div>
                        </label>
                        <span className="text-xs text-gray-600 font-bold">深度思考</span>
                    </div>
                    <div className="flex items-center gap-1" title="开启联网搜索">
                        <label className="relative inline-flex items-center cursor-pointer">
                            <input type="checkbox" checked={enableSearch} onChange={e => setEnableSearch(e.target.checked)} className="sr-only peer"/>
                            <div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
                        </label>
                        <span className="text-xs text-gray-600 font-bold">联网搜索</span>
                    </div>
                    <div className="w-px h-6 bg-gray-200 mx-1"></div>
                    <button onClick={() => setMessages([])} className="text-gray-400 hover:text-red-500 p-2 rounded-full hover:bg-red-50 transition-colors" title="清空对话">
                        <Trash2 size={18}/>
                    </button>
                </div>
            </div>

            <div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
                {messages.length === 0 && (
                    <div className="flex flex-col items-center justify-center h-full text-gray-400 opacity-60">
                        <FileText size={48} className="mb-4 text-indigo-300"/>
                        <p className="text-lg font-bold text-gray-600">我是你的{selectedRole.name}助理</p>
                        <p className="text-sm mt-2 max-w-md text-center">
                            请上传活动照片(支持批量)或文档(Word/PDF/Txt)<br/>
                            输入简单的活动描述,我会为你生成端庄大气的官方文稿。
                        </p>
                    </div>
                )}
                
                {messages.map((msg, index) => {
                    let sourceImages: string[] = msg.images || [];
                    
                    if (msg.role === 'model' && (!sourceImages || sourceImages.length === 0)) {
                        const prevMsg = messages[index - 1];
                        if (prevMsg && prevMsg.role === 'user' && prevMsg.images) {
                            sourceImages = prevMsg.images;
                        }
                    }

                    return (
                        <div key={msg.id} className={`flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''} max-w-5xl mx-auto w-full`}>
                            <div className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 shadow-sm ${msg.role === 'model' ? 'bg-white border border-indigo-100 text-indigo-600' : 'bg-blue-600 text-white'}`}>
                                {msg.role === 'model' ? <Sparkles size={20}/> : <span className="font-bold text-xs">ME</span>}
                            </div>
                            
                            <div className={`flex flex-col gap-2 max-w-[90%] ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
                                {msg.role === 'model' && msg.thought && (
                                    <div className="w-full bg-purple-50 rounded-xl border border-purple-100 overflow-hidden mb-2">
                                        <button 
                                            onClick={() => setIsThinkingExpanded(prev => ({ ...prev, [msg.id]: !prev[msg.id] }))}
                                            className="w-full px-4 py-2 flex items-center justify-between text-xs font-bold text-purple-700 bg-purple-100/50 hover:bg-purple-100 transition-colors"
                                        >
                                            <span className="flex items-center gap-2"><Brain size={14}/> 深度思考过程</span>
                                            {isThinkingExpanded[msg.id] ? <ChevronDown size={14}/> : <ChevronRight size={14}/>}
                                        </button>
                                        {isThinkingExpanded[msg.id] && (
                                            <div className="p-4 text-xs text-purple-800 whitespace-pre-wrap leading-relaxed border-t border-purple-100 font-mono bg-white/50">
                                                {msg.thought}
                                            </div>
                                        )}
                                    </div>
                                )}

                                {msg.role === 'model' && msg.isSearching && (
                                    <div className="flex items-center gap-2 bg-blue-50 text-blue-600 px-3 py-2 rounded-xl mb-2 text-xs border border-blue-100 animate-pulse w-fit">
                                        <Globe size={14} className="animate-spin"/>
                                        <span>正在联网搜索相关信息...</span>
                                    </div>
                                )}

                                <div className={`p-5 rounded-2xl shadow-sm text-sm overflow-hidden relative group w-full ${msg.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none' : 'bg-white text-gray-800 border border-gray-100 rounded-tl-none'}`}>
                                    {msg.role === 'user' && sourceImages.length > 0 && (
                                        <div className="grid grid-cols-4 gap-2 mb-3">
                                            {sourceImages.map((img, i) => (
                                                <div key={i} className="relative aspect-square">
                                                    <img src={`data:image/jpeg;base64,${img}`} className="w-full h-full object-cover rounded-lg border border-white/20" />
                                                    <div className="absolute bottom-0 right-0 bg-black/50 text-white text-[10px] px-1.5 rounded-tl-lg">图{i+1}</div>
                                                </div>
                                            ))}
                                        </div>
                                    )}

                                    <div className={`markdown-body ${msg.role === 'user' ? 'text-white' : ''}`}>
                                        {msg.role === 'model' ? renderContent(msg.text || '', sourceImages) : <p className="whitespace-pre-wrap">{msg.text}</p>}
                                    </div>

                                    {msg.role === 'model' && !isProcessing && (
                                        <button 
                                            onClick={() => handleCopy(msg.text || '')} 
                                            className="absolute top-2 right-2 p-1.5 text-gray-400 hover:text-blue-600 bg-white/80 backdrop-blur rounded-lg opacity-0 group-hover:opacity-100 transition-all shadow-sm border border-gray-100"
                                            title="复制纯文本"
                                        >
                                            <Copy size={14}/>
                                        </button>
                                    )}
                                </div>
                            </div>
                        </div>
                    );
                })}
                {isProcessing && (
                    <div className="flex justify-center py-4">
                        <div className="bg-white px-4 py-2 rounded-full shadow-sm border border-indigo-100 flex items-center gap-2 text-indigo-600 text-sm animate-pulse">
                            <Bot size={16}/>
                            <span>正在阅读资料并撰写文案...</span>
                        </div>
                    </div>
                )}
                <div ref={messagesEndRef} />
            </div>

            <div className="bg-white border-t border-gray-200 p-4 z-20">
                <div className="max-w-4xl mx-auto flex flex-col gap-3">
                    {(selectedImages.length > 0 || docFile) && (
                        <div className="flex gap-2 overflow-x-auto pb-2 px-1 custom-scrollbar">
                            {selectedImages.map((file, idx) => (
                                <div key={idx} className="relative w-16 h-16 shrink-0 group rounded-lg overflow-hidden border border-gray-200 shadow-sm">
                                    <img src={URL.createObjectURL(file)} className="w-full h-full object-cover" />
                                    <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer" onClick={() => setSelectedImages(prev => prev.filter((_, i) => i !== idx))}>
                                        <X size={16} className="text-white"/>
                                    </div>
                                    <div className="absolute bottom-0 right-0 bg-blue-600 text-white text-[9px] px-1 rounded-tl">图{idx+1}</div>
                                </div>
                            ))}
                            {docFile && (
                                <div className="relative h-16 px-3 bg-indigo-50 border border-indigo-200 rounded-lg flex items-center justify-center shrink-0 min-w-[120px] max-w-[200px]">
                                    <div className="flex flex-col items-start overflow-hidden">
                                        <div className="flex items-center text-indigo-700 font-bold text-xs mb-1">
                                            <File size={12} className="mr-1"/> 
                                            {docLoading ? '解析中...' : '已解析'}
                                        </div>
                                        <span className="text-[10px] text-indigo-500 truncate w-full" title={docFile.name}>{docFile.name}</span>
                                    </div>
                                    <button onClick={clearDoc} className="absolute top-0.5 right-0.5 text-indigo-300 hover:text-red-500 p-0.5"><X size={12}/></button>
                                </div>
                            )}
                        </div>
                    )}

                    <div className="flex gap-2 items-end bg-gray-50 p-2 rounded-2xl border border-gray-200 focus-within:ring-2 focus-within:ring-indigo-100 focus-within:border-indigo-300 transition-all shadow-inner">
                        <div className="flex items-center">
                            <button onClick={() => fileInputRef.current?.click()} className="p-2 text-gray-500 hover:bg-white hover:text-blue-600 rounded-full transition-colors shrink-0" title="相册/文件">
                                <ImageIcon size={22}/>
                            </button>
                            {isMobile && (
                                <button onClick={() => cameraInputRef.current?.click()} className="p-2 text-gray-500 hover:bg-white hover:text-blue-600 rounded-full transition-colors shrink-0" title="拍照">
                                    <Camera size={22}/>
                                </button>
                            )}
                        </div>
                        
                        <input type="file" multiple accept="image/*" ref={fileInputRef} className="hidden" onChange={handleImageSelect} onClick={(e) => (e.currentTarget.value = '')} />
                        
                        {isMobile && (
                            <input type="file" accept="image/*" capture="environment" ref={cameraInputRef} className="hidden" onChange={handleImageSelect} onClick={(e) => (e.currentTarget.value = '')} />
                        )}

                        <button onClick={() => docInputRef.current?.click()} className={`p-2 rounded-full transition-colors shrink-0 ${docFile ? 'text-indigo-600 bg-indigo-50' : 'text-gray-500 hover:bg-white hover:text-indigo-600'}`} title="上传文档 (Docx, PDF, Txt)">
                            <Paperclip size={22}/>
                        </button>
                        <input type="file" accept=".docx, .pdf, .txt" ref={docInputRef} className="hidden" onChange={handleDocSelect} onClick={(e) => (e.currentTarget.value = '')} />

                        <textarea 
                            className="flex-1 bg-transparent border-none outline-none text-sm resize-none max-h-32 py-3 px-2"
                            placeholder={selectedRole.id === 'editor' ? "请输入活动描述或直接上传活动方案文档..." : `${selectedRole.name}下达指令...`}
                            rows={1}
                            value={textInput}
                            onChange={e => setTextInput(e.target.value)}
                            onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }}
                        />

                        {isProcessing ? (
                            <button 
                                onClick={handleStopGeneration} 
                                className="p-3 bg-red-500 text-white rounded-xl shadow-sm hover:bg-red-600 transition-all shrink-0 animate-pulse"
                                title="停止生成"
                            >
                                <Square size={20} fill="currentColor"/>
                            </button>
                        ) : (
                            <button 
                                onClick={handleSubmit}
                                onMouseDown={(e) => e.preventDefault()}
                                onTouchEnd={(e) => { e.preventDefault(); handleSubmit(); }}
                                type="button"
                                disabled={(!textInput.trim() && selectedImages.length === 0 && !docFile) || isProcessing || docLoading}
                                className={`p-3 rounded-xl transition-all shrink-0 shadow-sm ${(!textInput.trim() && selectedImages.length === 0 && !docFile) || isProcessing || docLoading ? 'bg-gray-200 text-gray-400 cursor-not-allowed' : 'bg-indigo-600 text-white hover:bg-indigo-700 hover:scale-105'}`}
                            >
                                {isProcessing || docLoading ? <Loader2 className="animate-spin" size={20}/> : <Send size={20}/>}
                            </button>
                        )}
                    </div>
                    <div className="flex justify-between text-xs text-gray-400 px-1">
                        <span>* 支持上传 Word/PDF/Txt 解析内容</span>
                        <span>AI 生成内容仅供参考</span>
                    </div>
                </div>
            </div>
        </div>
    );
};