dvc890 commited on
Commit
3f2f630
·
verified ·
1 Parent(s): 019d28a

Update components/ai/WorkAssistantPanel.tsx

Browse files
Files changed (1) hide show
  1. components/ai/WorkAssistantPanel.tsx +138 -74
components/ai/WorkAssistantPanel.tsx CHANGED
@@ -1,7 +1,7 @@
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { AIChatMessage, User } from '../../types';
4
- import { Bot, Send, Sparkles, Loader2, Image as ImageIcon, X, Trash2, Brain, ChevronDown, ChevronRight, Copy, Check } from 'lucide-react';
5
  import ReactMarkdown from 'react-markdown';
6
  import remarkGfm from 'remark-gfm';
7
  import { compressImage } from '../../utils/mediaHelpers';
@@ -11,47 +11,55 @@ interface WorkAssistantPanelProps {
11
  currentUser: User | null;
12
  }
13
 
14
- // Pre-defined Roles
15
  const ROLES = [
16
  {
17
  id: 'editor',
18
- name: '公众号小编',
19
  icon: '📝',
20
- prompt: `你是一专业的微信公众号小编。
21
- 根据用户提供的图片和描述写一篇图文并茂的文
22
- 1. 风格:生动活泼、有感染力,适当使用 Emoji。
23
- 2. 标题:提供3个吸引人的标题供选择。
24
- 3. 图片排版:在文章中合适的位置插入图片占位符。格式必须为:**[图片N]** (例如**[图片1]**, **[图片2]**)。请根据图片内容逻辑安排顺序并标注是哪张图片
25
- 4. 结构包含开头导、正文内容、结尾互动`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  },
27
  {
28
  id: 'host',
29
- name: '活动主持/策划',
30
  icon: '🎤',
 
31
  prompt: `你是一位经验丰富的学校活动策划和主持人。
32
  协助老师撰写活动流程、主持稿或致辞。
33
  1. 风格:庄重、大气或热情(根据活动性质调整)。
34
- 2. 内容:逻辑清晰,环节紧凑。
35
- 3. 格式:标明【环节名称】、【时长预估】、【具体话术】。`
36
- },
37
- {
38
- id: 'writer',
39
- name: '文案润色专家',
40
- icon: '✍️',
41
- prompt: `你是一位资深的文字编辑。
42
- 请帮助老师优化、润色草稿或口语化的文字。
43
- 1. 目标:使文字更加通顺、优雅、符合书面语规范。
44
- 2. 修正:纠正错别字和语病。
45
- 3. 提升:优化修辞,增强表现力,但保持原意不变。`
46
  },
47
  {
48
  id: 'promoter',
49
- name: '宣传文案/朋友圈',
50
  icon: '📢',
 
51
  prompt: `你是一位擅长社交媒体传播的文案。
52
  请为学校活动撰写短小精悍的宣传语,适用于朋友圈、班级群或海报。
53
  1. 长度:200字以内。
54
- 2. 重点:突出亮点,号召行动。
55
  3. 格式:分行排版,便于手机阅读。`
56
  }
57
  ];
@@ -122,23 +130,23 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
122
  text: '',
123
  thought: '',
124
  timestamp: Date.now(),
125
- images: base64Images // Pass images to AI message for rendering placeholders locally if needed, but actually we use user's images
 
126
  };
127
 
128
  setMessages(prev => [...prev, newUserMsg, newAiMsg]);
129
 
130
- // Default expand thinking
131
  if (enableThinking) {
132
  setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: true }));
133
  }
134
 
135
- // Build history context (Simplified: just role context + recent messages)
136
- const historyContext = messages.slice(-5).map(m => ({ role: m.role, text: m.text }));
137
 
138
  // Custom prompt logic for image indexing
139
  let finalPrompt = currentText;
140
  if (base64Images.length > 0) {
141
- finalPrompt += `\n\n(附带了 ${base64Images.length} 张图片。在文章中插入时请使用 **[1]**, **[片2]** 占位符来对应第1、第2张图)`;
142
  }
143
 
144
  const response = await fetch('/api/ai/chat', {
@@ -152,10 +160,10 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
152
  body: JSON.stringify({
153
  text: finalPrompt,
154
  images: base64Images,
155
- history: historyContext,
156
  enableThinking,
157
- overrideSystemPrompt: selectedRole.prompt, // Inject specific role prompt
158
- disableAudio: true // No TTS for work assistant
159
  })
160
  });
161
 
@@ -188,7 +196,6 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
188
  aiThoughtAccumulated += data.content;
189
  setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, thought: aiThoughtAccumulated } : m));
190
  } else if (data.type === 'text') {
191
- // Once text starts, collapse thinking
192
  if (aiTextAccumulated === '' && aiThoughtAccumulated !== '') {
193
  setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: false }));
194
  }
@@ -209,31 +216,69 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
209
  }
210
  };
211
 
212
- // Custom Renderer for [图片N]
 
 
 
213
  const renderContent = (text: string, sourceImages: string[] | undefined) => {
214
- // Split by image placeholders: **[图片N]** or [图片N]
215
- const parts = text.split(/(\*\*\[图片\d+\]\*\*|\[图片\d+\])/g);
216
-
 
 
 
 
 
217
  return (
218
  <div>
219
  {parts.map((part, idx) => {
220
- const match = part.match(/\d+/);
221
- if ((part.startsWith('[图片') || part.startsWith('**[片')) && match && sourceImages) {
222
- const imgIndex = parseInt(match[0]) - 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  if (sourceImages[imgIndex]) {
224
  return (
225
- <div key={idx} className="my-4">
226
  <img
227
  src={`data:image/jpeg;base64,${sourceImages[imgIndex]}`}
228
- className="max-w-full md:max-w-md rounded-lg shadow-sm border border-gray-200"
229
  alt={`Image ${imgIndex + 1}`}
230
  />
231
- <div className="text-center text-xs text-gray-400 mt-1">{imgIndex + 1}</div>
232
  </div>
233
  );
234
  }
235
  }
236
- return <ReactMarkdown key={idx} remarkPlugins={[remarkGfm]} components={{p: ({node, ...props}) => <p className="mb-2 last:mb-0" {...props}/>}}>{part}</ReactMarkdown>;
 
 
237
  })}
238
  </div>
239
  );
@@ -246,21 +291,24 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
246
  {/* Toolbar */}
247
  <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">
248
  <div className="flex items-center gap-2">
249
- <span className="text-sm font-bold text-gray-700">工作场景:</span>
250
  <div className="relative group">
251
  <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">
252
  <span>{selectedRole.icon} {selectedRole.name}</span>
253
  <ChevronDown size={14}/>
254
  </button>
255
- <div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden hidden group-hover:block z-50">
256
  {ROLES.map(role => (
257
  <button
258
  key={role.id}
259
  onClick={() => setSelectedRole(role)}
260
- className={`w-full text-left px-4 py-3 text-sm hover:bg-indigo-50 flex items-center gap-2 ${selectedRole.id === role.id ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-gray-700'}`}
261
  >
262
- <span>{role.icon}</span>
263
- <span>{role.name}</span>
 
 
 
264
  </button>
265
  ))}
266
  </div>
@@ -268,7 +316,7 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
268
  </div>
269
 
270
  <div className="flex items-center gap-4">
271
- <div className="flex items-center gap-2" title="仅支持部分高级模型 (如 Doubao Pro, Gemini Flash)">
272
  <span className="text-sm text-gray-600 font-medium flex items-center gap-1">
273
  <Brain size={16} className={enableThinking ? "text-purple-600" : "text-gray-400"}/> 深度思考
274
  </span>
@@ -277,7 +325,7 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
277
  <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>
278
  </label>
279
  </div>
280
- <button onClick={() => setMessages([])} className="text-gray-400 hover:text-red-500 p-2 rounded-full hover:bg-red-50 transition-colors">
281
  <Trash2 size={18}/>
282
  </button>
283
  </div>
@@ -287,32 +335,35 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
287
  <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
288
  {messages.length === 0 && (
289
  <div className="flex flex-col items-center justify-center h-full text-gray-400 opacity-60">
290
- <Bot size={48} className="mb-4"/>
291
- <p className="text-lg font-bold">我是你的{selectedRole.name}</p>
292
- <p className="text-sm">上传图片或输入要求,开始工作吧</p>
 
 
 
293
  </div>
294
  )}
295
 
296
  {messages.map((msg, index) => {
297
- // Find the user message immediately preceding this AI message to get source images for placeholders
298
- let sourceImages: string[] = [];
299
- if (msg.role === 'model') {
300
- // Look back for the connected user message (usually index - 1)
 
 
301
  const prevMsg = messages[index - 1];
302
  if (prevMsg && prevMsg.role === 'user' && prevMsg.images) {
303
  sourceImages = prevMsg.images;
304
  }
305
- } else if (msg.role === 'user' && msg.images) {
306
- sourceImages = msg.images;
307
  }
308
 
309
  return (
310
- <div key={msg.id} className={`flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''} max-w-4xl mx-auto w-full`}>
311
  <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'}`}>
312
  {msg.role === 'model' ? <Sparkles size={20}/> : <span className="font-bold text-xs">ME</span>}
313
  </div>
314
 
315
- <div className={`flex flex-col gap-2 max-w-[85%] ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
316
  {msg.role === 'model' && msg.thought && (
317
  <div className="w-full bg-purple-50 rounded-xl border border-purple-100 overflow-hidden mb-2">
318
  <button
@@ -330,10 +381,10 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
330
  </div>
331
  )}
332
 
333
- <div className={`p-4 rounded-2xl shadow-sm text-sm overflow-hidden relative group ${msg.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none' : 'bg-white text-gray-800 border border-gray-100 rounded-tl-none'}`}>
334
- {/* User Image Preview Grid */}
335
  {msg.role === 'user' && sourceImages.length > 0 && (
336
- <div className="grid grid-cols-3 gap-2 mb-3">
337
  {sourceImages.map((img, i) => (
338
  <div key={i} className="relative aspect-square">
339
  <img src={`data:image/jpeg;base64,${img}`} className="w-full h-full object-cover rounded-lg border border-white/20" />
@@ -353,7 +404,7 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
353
  <button
354
  onClick={() => handleCopy(msg.text || '')}
355
  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"
356
- title="复制内容"
357
  >
358
  <Copy size={14}/>
359
  </button>
@@ -363,6 +414,14 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
363
  </div>
364
  );
365
  })}
 
 
 
 
 
 
 
 
366
  <div ref={messagesEndRef} />
367
  </div>
368
 
@@ -371,28 +430,32 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
371
  <div className="max-w-4xl mx-auto flex flex-col gap-3">
372
  {/* Image Preview */}
373
  {selectedImages.length > 0 && (
374
- <div className="flex gap-2 overflow-x-auto pb-2 px-1">
375
  {selectedImages.map((file, idx) => (
376
  <div key={idx} className="relative w-16 h-16 shrink-0 group rounded-lg overflow-hidden border border-gray-200 shadow-sm">
377
  <img src={URL.createObjectURL(file)} className="w-full h-full object-cover" />
378
- <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
379
- <button onClick={() => setSelectedImages(prev => prev.filter((_, i) => i !== idx))} className="text-white hover:text-red-400"><X size={16}/></button>
380
  </div>
381
  <div className="absolute bottom-0 right-0 bg-blue-600 text-white text-[9px] px-1 rounded-tl">图{idx+1}</div>
382
  </div>
383
  ))}
 
 
 
 
384
  </div>
385
  )}
386
 
387
- <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">
388
- <button onClick={() => fileInputRef.current?.click()} className="p-3 text-gray-500 hover:bg-white hover:text-indigo-600 rounded-xl transition-colors shrink-0" title="上传图片">
389
  <ImageIcon size={22}/>
390
  </button>
391
  <input type="file" multiple accept="image/*" ref={fileInputRef} className="hidden" onChange={handleImageSelect} onClick={(e) => (e.currentTarget.value = '')} />
392
 
393
  <textarea
394
  className="flex-1 bg-transparent border-none outline-none text-sm resize-none max-h-32 py-3 px-2"
395
- placeholder={`给${selectedRole.name}下达指令... (支持 Shift+Enter 换行)`}
396
  rows={1}
397
  value={textInput}
398
  onChange={e => setTextInput(e.target.value)}
@@ -407,8 +470,9 @@ export const WorkAssistantPanel: React.FC<WorkAssistantPanelProps> = ({ currentU
407
  {isProcessing ? <Loader2 className="animate-spin" size={20}/> : <Send size={20}/>}
408
  </button>
409
  </div>
410
- <div className="text-center text-xs text-gray-400">
411
- * AI 生成内容仅供参考,请人工审核后使用
 
412
  </div>
413
  </div>
414
  </div>
 
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { AIChatMessage, User } from '../../types';
4
+ import { Bot, Send, Sparkles, Loader2, Image as ImageIcon, X, Trash2, Brain, ChevronDown, ChevronRight, Copy, Check, FileText, Plus } from 'lucide-react';
5
  import ReactMarkdown from 'react-markdown';
6
  import remarkGfm from 'remark-gfm';
7
  import { compressImage } from '../../utils/mediaHelpers';
 
11
  currentUser: User | null;
12
  }
13
 
14
+ // Optimized Roles based on user's requirements
15
  const ROLES = [
16
  {
17
  id: 'editor',
18
+ name: '公众号图文精修',
19
  icon: '📝',
20
+ description: '自动优化文稿并插入图片占符',
21
+ prompt: `你是一位拥有10年经验的学校公众号主编。你的任务是根据用户提供的草稿(或活动描述)以及上传的图片数量,重并排版一篇高质量公众号推文。
22
+
23
+ ### 核心任务
24
+ 1. **文案润色**:将文字修改为优雅、生动、富有教育情怀的风格(参考:初冬暖阳万物含情)
25
+ 2. **标题生成**提供3个吸人的标题
26
+ 3. **图片排版(关键)**:
27
+ - 用户上传了 {{IMAGE_COUNT}} 张图片。
28
+ - 你必须将这 {{IMAGE_COUNT}} 张图片全部分配到文章的合适位置。
29
+ - **格式要求**:请在段落结束后,使用 **(图Start-图End)** 或 **(图N)** 的格式插入占位符。
30
+ - **智能分组**:不要一张张插入。请根据段落内容将相关图片分组。例如:
31
+ - 描写校园环境时,插入 (图1-图5)
32
+ - 描写大课间活动时,插入 (图6-图10)
33
+ - 描写讲座时,插入 (图11-图12)
34
+ - 请确保图片编号从1开始连续编号,不要遗漏,也不要超出用户上传的总数。
35
+
36
+ ### 输出风格示例
37
+ ...(正文内容)...
38
+ ...细致观摩校园文化建设的匠心设计。(图1-图5)
39
+
40
+ ...尽显校园的蓬勃生机。...让校长们在行走中感受优质校园的育人底蕴。(图6-图10)
41
+ ...`
42
  },
43
  {
44
  id: 'host',
45
+ name: '活动主持/致辞',
46
  icon: '🎤',
47
+ description: '生成正式的活动主持词',
48
  prompt: `你是一位经验丰富的学校活动策划和主持人。
49
  协助老师撰写活动流程、主持稿或致辞。
50
  1. 风格:庄重、大气或热情(根据活动性质调整)。
51
+ 2. 内容:逻辑清晰,环节紧凑,串词自然过渡
52
+ 3. 格式:标明【环节名称】、【具体话术】。`
 
 
 
 
 
 
 
 
 
 
53
  },
54
  {
55
  id: 'promoter',
56
+ name: '朋友圈/短文案',
57
  icon: '📢',
58
+ description: '生成短小精悍的宣传语',
59
  prompt: `你是一位擅长社交媒体传播的文案。
60
  请为学校活动撰写短小精悍的宣传语,适用于朋友圈、班级群或海报。
61
  1. 长度:200字以内。
62
+ 2. 重点:突出亮点,号召行动,适当使用Emoji增加亲和力
63
  3. 格式:分行排版,便于手机阅读。`
64
  }
65
  ];
 
130
  text: '',
131
  thought: '',
132
  timestamp: Date.now(),
133
+ // Pass user images to AI message so it can render the placeholders using the source images
134
+ images: base64Images
135
  };
136
 
137
  setMessages(prev => [...prev, newUserMsg, newAiMsg]);
138
 
 
139
  if (enableThinking) {
140
  setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: true }));
141
  }
142
 
143
+ // Inject Image Count into System Prompt
144
+ const dynamicSystemPrompt = selectedRole.prompt.replace('{{IMAGE_COUNT}}', String(base64Images.length));
145
 
146
  // Custom prompt logic for image indexing
147
  let finalPrompt = currentText;
148
  if (base64Images.length > 0) {
149
+ finalPrompt += `\n\n[系统提示] 用户上传了 ${base64Images.length} 张图片。请务必在文章中合理位置插入 图1-X) 格式的占位符`;
150
  }
151
 
152
  const response = await fetch('/api/ai/chat', {
 
160
  body: JSON.stringify({
161
  text: finalPrompt,
162
  images: base64Images,
163
+ history: [], // Work assistant usually doesn't need long history context to keep focus sharp
164
  enableThinking,
165
+ overrideSystemPrompt: dynamicSystemPrompt,
166
+ disableAudio: true
167
  })
168
  });
169
 
 
196
  aiThoughtAccumulated += data.content;
197
  setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, thought: aiThoughtAccumulated } : m));
198
  } else if (data.type === 'text') {
 
199
  if (aiTextAccumulated === '' && aiThoughtAccumulated !== '') {
200
  setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: false }));
201
  }
 
216
  }
217
  };
218
 
219
+ /**
220
+ * Advanced Text Renderer
221
+ * Detects (图N) or (图N-图M) patterns and renders image grids
222
+ */
223
  const renderContent = (text: string, sourceImages: string[] | undefined) => {
224
+ if (!sourceImages || sourceImages.length === 0) {
225
+ return <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>;
226
+ }
227
+
228
+ // Regex to catch: (图1) or (图1-图3) or (图1) ...
229
+ const splitRegex = /((?图\d+(?:-图\d+)?)?)/g;
230
+ const parts = text.split(splitRegex);
231
+
232
  return (
233
  <div>
234
  {parts.map((part, idx) => {
235
+ // Check if part matches image placeholder pattern
236
+ const rangeMatch = part.match(/(\d+)-图(\d+)/); // Range: 图1-图5
237
+ const singleMatch = part.match(/图(\d+)/); // Single: 图1
238
+
239
+ if (rangeMatch) {
240
+ const start = parseInt(rangeMatch[1]);
241
+ const end = parseInt(rangeMatch[2]);
242
+ const imagesToShow: string[] = [];
243
+
244
+ for (let i = start; i <= end; i++) {
245
+ if (sourceImages[i-1]) imagesToShow.push(sourceImages[i-1]);
246
+ }
247
+
248
+ if (imagesToShow.length > 0) {
249
+ return (
250
+ <div key={idx} className="my-4 bg-gray-50 p-2 rounded-lg border border-gray-100">
251
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
252
+ {imagesToShow.map((img, i) => (
253
+ <div key={i} className="relative aspect-[4/3] group">
254
+ <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}`}/>
255
+ <div className="absolute bottom-1 right-1 bg-black/60 text-white text-[10px] px-1.5 rounded">图 {start+i}</div>
256
+ </div>
257
+ ))}
258
+ </div>
259
+ <div className="text-center text-xs text-gray-400 mt-2 font-mono">{part}</div>
260
+ </div>
261
+ );
262
+ }
263
+ } else if (singleMatch && !part.includes('-')) {
264
+ // Single Image
265
+ const imgIndex = parseInt(singleMatch[1]) - 1;
266
  if (sourceImages[imgIndex]) {
267
  return (
268
+ <div key={idx} className="my-4 flex flex-col items-center">
269
  <img
270
  src={`data:image/jpeg;base64,${sourceImages[imgIndex]}`}
271
+ className="max-w-full md:max-w-sm rounded-lg shadow-sm border border-gray-200"
272
  alt={`Image ${imgIndex + 1}`}
273
  />
274
+ <div className="text-center text-xs text-gray-400 mt-1 font-mono">{part}</div>
275
  </div>
276
  );
277
  }
278
  }
279
+
280
+ // Standard text
281
+ return <ReactMarkdown key={idx} remarkPlugins={[remarkGfm]} components={{p: ({node, ...props}) => <p className="mb-2 last:mb-0 inline" {...props}/>}}>{part}</ReactMarkdown>;
282
  })}
283
  </div>
284
  );
 
291
  {/* Toolbar */}
292
  <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">
293
  <div className="flex items-center gap-2">
294
+ <span className="text-sm font-bold text-gray-700">工作模式:</span>
295
  <div className="relative group">
296
  <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">
297
  <span>{selectedRole.icon} {selectedRole.name}</span>
298
  <ChevronDown size={14}/>
299
  </button>
300
+ <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">
301
  {ROLES.map(role => (
302
  <button
303
  key={role.id}
304
  onClick={() => setSelectedRole(role)}
305
+ 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' : ''}`}
306
  >
307
+ <div className="text-xl mt-0.5">{role.icon}</div>
308
+ <div>
309
+ <div className={`text-sm font-bold ${selectedRole.id === role.id ? 'text-indigo-700' : 'text-gray-800'}`}>{role.name}</div>
310
+ <div className="text-xs text-gray-500 mt-0.5">{role.description}</div>
311
+ </div>
312
  </button>
313
  ))}
314
  </div>
 
316
  </div>
317
 
318
  <div className="flex items-center gap-4">
319
+ <div className="flex items-center gap-2" title="开启后AI会进行更深入的逻辑推演">
320
  <span className="text-sm text-gray-600 font-medium flex items-center gap-1">
321
  <Brain size={16} className={enableThinking ? "text-purple-600" : "text-gray-400"}/> 深度思考
322
  </span>
 
325
  <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>
326
  </label>
327
  </div>
328
+ <button onClick={() => setMessages([])} className="text-gray-400 hover:text-red-500 p-2 rounded-full hover:bg-red-50 transition-colors" title="清空对话">
329
  <Trash2 size={18}/>
330
  </button>
331
  </div>
 
335
  <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
336
  {messages.length === 0 && (
337
  <div className="flex flex-col items-center justify-center h-full text-gray-400 opacity-60">
338
+ <FileText size={48} className="mb-4 text-indigo-300"/>
339
+ <p className="text-lg font-bold text-gray-600">我是你的{selectedRole.name}助理</p>
340
+ <p className="text-sm mt-2 max-w-md text-center">
341
+ 请上传活动照片(支持批量)并输入简单的活动描述。<br/>
342
+ 我会为你生成图文并茂的文章,并自动排版图片位置。
343
+ </p>
344
  </div>
345
  )}
346
 
347
  {messages.map((msg, index) => {
348
+ // Logic to retrieve images from the AI message itself (passed during creation)
349
+ // OR from the preceding user message if it's a model response
350
+ let sourceImages: string[] = msg.images || [];
351
+
352
+ if (msg.role === 'model' && (!sourceImages || sourceImages.length === 0)) {
353
+ // Fallback: look at previous user message
354
  const prevMsg = messages[index - 1];
355
  if (prevMsg && prevMsg.role === 'user' && prevMsg.images) {
356
  sourceImages = prevMsg.images;
357
  }
 
 
358
  }
359
 
360
  return (
361
+ <div key={msg.id} className={`flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''} max-w-5xl mx-auto w-full`}>
362
  <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'}`}>
363
  {msg.role === 'model' ? <Sparkles size={20}/> : <span className="font-bold text-xs">ME</span>}
364
  </div>
365
 
366
+ <div className={`flex flex-col gap-2 max-w-[90%] ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
367
  {msg.role === 'model' && msg.thought && (
368
  <div className="w-full bg-purple-50 rounded-xl border border-purple-100 overflow-hidden mb-2">
369
  <button
 
381
  </div>
382
  )}
383
 
384
+ <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'}`}>
385
+ {/* User Image Preview Grid (Only for user messages) */}
386
  {msg.role === 'user' && sourceImages.length > 0 && (
387
+ <div className="grid grid-cols-4 gap-2 mb-3">
388
  {sourceImages.map((img, i) => (
389
  <div key={i} className="relative aspect-square">
390
  <img src={`data:image/jpeg;base64,${img}`} className="w-full h-full object-cover rounded-lg border border-white/20" />
 
404
  <button
405
  onClick={() => handleCopy(msg.text || '')}
406
  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"
407
+ title="复制纯文本"
408
  >
409
  <Copy size={14}/>
410
  </button>
 
414
  </div>
415
  );
416
  })}
417
+ {isProcessing && (
418
+ <div className="flex justify-center py-4">
419
+ <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">
420
+ <Bot size={16}/>
421
+ <span>正在撰写文案并排版图片...</span>
422
+ </div>
423
+ </div>
424
+ )}
425
  <div ref={messagesEndRef} />
426
  </div>
427
 
 
430
  <div className="max-w-4xl mx-auto flex flex-col gap-3">
431
  {/* Image Preview */}
432
  {selectedImages.length > 0 && (
433
+ <div className="flex gap-2 overflow-x-auto pb-2 px-1 custom-scrollbar">
434
  {selectedImages.map((file, idx) => (
435
  <div key={idx} className="relative w-16 h-16 shrink-0 group rounded-lg overflow-hidden border border-gray-200 shadow-sm">
436
  <img src={URL.createObjectURL(file)} className="w-full h-full object-cover" />
437
+ <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))}>
438
+ <X size={16} className="text-white"/>
439
  </div>
440
  <div className="absolute bottom-0 right-0 bg-blue-600 text-white text-[9px] px-1 rounded-tl">图{idx+1}</div>
441
  </div>
442
  ))}
443
+ <div className="w-16 h-16 shrink-0 rounded-lg border-2 border-dashed border-gray-300 flex flex-col items-center justify-center text-gray-400 cursor-pointer hover:border-indigo-400 hover:text-indigo-500 transition-colors" onClick={() => fileInputRef.current?.click()}>
444
+ <Plus size={20}/>
445
+ <span className="text-[9px]">添加</span>
446
+ </div>
447
  </div>
448
  )}
449
 
450
+ <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">
451
+ <button onClick={() => fileInputRef.current?.click()} className={`p-3 rounded-xl transition-colors shrink-0 ${selectedImages.length > 0 ? 'text-indigo-600 bg-indigo-50' : 'text-gray-500 hover:bg-white hover:text-indigo-600'}`} title="上传图片">
452
  <ImageIcon size={22}/>
453
  </button>
454
  <input type="file" multiple accept="image/*" ref={fileInputRef} className="hidden" onChange={handleImageSelect} onClick={(e) => (e.currentTarget.value = '')} />
455
 
456
  <textarea
457
  className="flex-1 bg-transparent border-none outline-none text-sm resize-none max-h-32 py-3 px-2"
458
+ placeholder={selectedRole.id === 'editor' ? "请粘贴草稿文字或输入活动描述..." : `给${selectedRole.name}下达指令...`}
459
  rows={1}
460
  value={textInput}
461
  onChange={e => setTextInput(e.target.value)}
 
470
  {isProcessing ? <Loader2 className="animate-spin" size={20}/> : <Send size={20}/>}
471
  </button>
472
  </div>
473
+ <div className="flex justify-between text-xs text-gray-400 px-1">
474
+ <span>* 支持 Shift+Enter 换行</span>
475
+ <span>AI 生成内容仅供参考</span>
476
  </div>
477
  </div>
478
  </div>