Spaces:
Sleeping
Sleeping
Upload 19 files
Browse files- .gitattributes +35 -35
- App.tsx +1059 -1102
- Dockerfile +33 -33
- README.md +11 -11
- components/ChatInput.tsx +191 -191
- components/LoadingSpinner.tsx +34 -34
- components/MessageBubble.tsx +200 -200
- components/ModelConfigManager.tsx +0 -0
- components/Notepad.tsx +98 -98
- constants.ts +736 -841
- index.html +197 -197
- index.tsx +16 -16
- metadata.json +5 -5
- package.json +48 -48
- services/openaiService.ts +196 -196
- tsconfig.json +30 -30
- types.ts +297 -297
- vite.config.ts +26 -26
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
App.tsx
CHANGED
|
@@ -1,1103 +1,1060 @@
|
|
| 1 |
-
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
-
import { ChatMessage, MessageSender, MessagePurpose } from './types';
|
| 3 |
-
import { generateResponse } from './services/openaiService';
|
| 4 |
-
import ChatInput from './components/ChatInput';
|
| 5 |
-
import MessageBubble from './components/MessageBubble';
|
| 6 |
-
import Notepad from './components/Notepad';
|
| 7 |
-
import ModelConfigManager from './components/ModelConfigManager';
|
| 8 |
-
import {
|
| 9 |
-
AiModel,
|
| 10 |
-
AiRole,
|
| 11 |
-
ApiChannel,
|
| 12 |
-
ModelConfigManager as ConfigManager,
|
| 13 |
-
DEFAULT_MANUAL_FIXED_TURNS,
|
| 14 |
-
MIN_MANUAL_FIXED_TURNS,
|
| 15 |
-
MAX_MANUAL_FIXED_TURNS,
|
| 16 |
-
MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL,
|
| 17 |
-
INITIAL_NOTEPAD_CONTENT,
|
| 18 |
-
NOTEPAD_INSTRUCTION_PROMPT_PART,
|
| 19 |
-
NOTEPAD_UPDATE_TAG_START,
|
| 20 |
-
NOTEPAD_UPDATE_TAG_END,
|
| 21 |
-
DISCUSSION_COMPLETE_TAG,
|
| 22 |
-
AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART,
|
| 23 |
-
DiscussionMode
|
| 24 |
-
} from './constants';
|
| 25 |
-
import {
|
| 26 |
-
BotMessageSquare,
|
| 27 |
-
AlertTriangle,
|
| 28 |
-
RefreshCcw,
|
| 29 |
-
SlidersHorizontal,
|
| 30 |
-
Users,
|
| 31 |
-
MessagesSquare,
|
| 32 |
-
Bot,
|
| 33 |
-
ChevronDown,
|
| 34 |
-
Settings,
|
| 35 |
-
Play,
|
| 36 |
-
Pause,
|
| 37 |
-
Square,
|
| 38 |
-
Download
|
| 39 |
-
} from 'lucide-react';
|
| 40 |
-
|
| 41 |
-
interface ParsedAIResponse {
|
| 42 |
-
spokenText: string;
|
| 43 |
-
newNotepadContent: string | null;
|
| 44 |
-
discussionShouldEnd?: boolean;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
interface ActiveRole extends AiRole {
|
| 48 |
-
model: AiModel;
|
| 49 |
-
channel: ApiChannel;
|
| 50 |
-
isProcessing?: boolean;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
interface DiscussionState {
|
| 54 |
-
currentRoleIndex: number;
|
| 55 |
-
currentTurn: number;
|
| 56 |
-
discussionLog: string[];
|
| 57 |
-
isFirstMessage: boolean;
|
| 58 |
-
previousAISignaledStop: boolean;
|
| 59 |
-
discussionEndCount: number;
|
| 60 |
-
userQuery: string;
|
| 61 |
-
imageApiPart?: any;
|
| 62 |
-
commonPromptInstructions: string;
|
| 63 |
-
roleOrder: ActiveRole[];
|
| 64 |
-
maxTurnsForLoop: number;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
const parseAIResponse = (responseText: string): ParsedAIResponse => {
|
| 68 |
-
let currentText = responseText.trim();
|
| 69 |
-
let spokenText = "";
|
| 70 |
-
let newNotepadContent: string | null = null;
|
| 71 |
-
let discussionShouldEnd = false;
|
| 72 |
-
|
| 73 |
-
let notepadActionText = "";
|
| 74 |
-
let discussionActionText = "";
|
| 75 |
-
|
| 76 |
-
const notepadStartIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_START);
|
| 77 |
-
const notepadEndIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_END);
|
| 78 |
-
|
| 79 |
-
if (notepadStartIndex !== -1 && notepadEndIndex !== -1 && notepadEndIndex > notepadStartIndex && currentText.endsWith(NOTEPAD_UPDATE_TAG_END)) {
|
| 80 |
-
newNotepadContent = currentText.substring(notepadStartIndex + NOTEPAD_UPDATE_TAG_START.length, notepadEndIndex).trim();
|
| 81 |
-
spokenText = currentText.substring(0, notepadStartIndex).trim();
|
| 82 |
-
|
| 83 |
-
if (newNotepadContent) {
|
| 84 |
-
notepadActionText = "更新了记事本";
|
| 85 |
-
} else {
|
| 86 |
-
notepadActionText = "尝试更新记事本但内容为空";
|
| 87 |
-
}
|
| 88 |
-
} else {
|
| 89 |
-
spokenText = currentText;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
if (spokenText.includes(DISCUSSION_COMPLETE_TAG)) {
|
| 93 |
-
discussionShouldEnd = true;
|
| 94 |
-
spokenText = spokenText.replace(new RegExp(DISCUSSION_COMPLETE_TAG.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), "").trim();
|
| 95 |
-
discussionActionText = "建议结束讨论";
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
if (!spokenText.trim()) {
|
| 99 |
-
if (notepadActionText && discussionActionText) {
|
| 100 |
-
spokenText = `(AI ${notepadActionText}并${discussionActionText})`;
|
| 101 |
-
} else if (notepadActionText) {
|
| 102 |
-
spokenText = `(AI ${notepadActionText})`;
|
| 103 |
-
} else if (discussionActionText) {
|
| 104 |
-
spokenText = `(AI ${discussionActionText})`;
|
| 105 |
-
} else {
|
| 106 |
-
spokenText = "(AI 未提供额外文本回复)";
|
| 107 |
-
}
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
return { spokenText: spokenText.trim(), newNotepadContent, discussionShouldEnd };
|
| 111 |
-
};
|
| 112 |
-
|
| 113 |
-
const fileToBase64 = (file: File): Promise<string> => {
|
| 114 |
-
return new Promise((resolve, reject) => {
|
| 115 |
-
const reader = new FileReader();
|
| 116 |
-
reader.readAsDataURL(file);
|
| 117 |
-
reader.onload = () => {
|
| 118 |
-
const result = reader.result as string;
|
| 119 |
-
resolve(result.split(',')[1]);
|
| 120 |
-
};
|
| 121 |
-
reader.onerror = (error) => reject(error);
|
| 122 |
-
});
|
| 123 |
-
};
|
| 124 |
-
|
| 125 |
-
const createDynamicMessageSender = (roleName: string): MessageSender => {
|
| 126 |
-
return roleName as MessageSender;
|
| 127 |
-
};
|
| 128 |
-
|
| 129 |
-
const App: React.FC = () => {
|
| 130 |
-
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
| 131 |
-
const [isLoading, setIsLoading] = useState<boolean>(false);
|
| 132 |
-
const [currentTotalProcessingTimeMs, setCurrentTotalProcessingTimeMs] = useState<number>(0);
|
| 133 |
-
|
| 134 |
-
const [notepadContent, setNotepadContent] = useState<string>(INITIAL_NOTEPAD_CONTENT);
|
| 135 |
-
const [lastNotepadUpdateBy, setLastNotepadUpdateBy] = useState<MessageSender | null>(null);
|
| 136 |
-
|
| 137 |
-
const [discussionMode, setDiscussionMode] = useState<DiscussionMode>(DiscussionMode.FixedTurns);
|
| 138 |
-
const [manualFixedTurns, setManualFixedTurns] = useState<number>(DEFAULT_MANUAL_FIXED_TURNS);
|
| 139 |
-
const [isReducedCapacityEnabled, setIsReducedCapacityEnabled] = useState<boolean>(false);
|
| 140 |
-
|
| 141 |
-
const [activeRoles, setActiveRoles] = useState<ActiveRole[]>([]);
|
| 142 |
-
const [channels, setChannels] = useState<ApiChannel[]>([]);
|
| 143 |
-
const [models, setModels] = useState<AiModel[]>([]);
|
| 144 |
-
const [isConfigManagerOpen, setIsConfigManagerOpen] = useState<boolean>(false);
|
| 145 |
-
const [
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
const
|
| 153 |
-
const
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
const
|
| 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 |
-
const
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
};
|
| 316 |
-
|
| 317 |
-
const
|
| 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 |
-
return;
|
| 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 |
-
prompt
|
| 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 |
-
const
|
| 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 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
if (
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
}
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
//
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
className="
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
)
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
</
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
})}
|
| 1061 |
-
</div>
|
| 1062 |
-
<ChatInput onSendMessage={handleSendMessage} isLoading={isLoading} isApiKeyMissing={!isSystemReady} />
|
| 1063 |
-
</div>
|
| 1064 |
-
|
| 1065 |
-
<div className="w-1/3 md:w-2/5 lg:w-1/3 h-full bg-slate-800">
|
| 1066 |
-
<Notepad
|
| 1067 |
-
content={notepadContent}
|
| 1068 |
-
lastUpdatedBy={lastNotepadUpdateBy}
|
| 1069 |
-
isLoading={isLoading}
|
| 1070 |
-
/>
|
| 1071 |
-
</div>
|
| 1072 |
-
</div>
|
| 1073 |
-
|
| 1074 |
-
{/* 配置管理器 */}
|
| 1075 |
-
<ModelConfigManager
|
| 1076 |
-
isOpen={isConfigManagerOpen}
|
| 1077 |
-
onClose={() => setIsConfigManagerOpen(false)}
|
| 1078 |
-
onConfigChange={loadConfiguration}
|
| 1079 |
-
/>
|
| 1080 |
-
|
| 1081 |
-
{/* 处理时间显示 */}
|
| 1082 |
-
{(isLoading || (currentTotalProcessingTimeMs > 0 && !isLoading) || (isLoading && currentTotalProcessingTimeMs === 0)) && (
|
| 1083 |
-
<div className="fixed bottom-4 right-4 md:bottom-6 md:right-6 bg-gray-900 bg-opacity-80 text-white p-2 rounded-md shadow-lg text-xs z-50">
|
| 1084 |
-
总耗时: {(currentTotalProcessingTimeMs / 1000).toFixed(2)}s
|
| 1085 |
-
{isDiscussionActive && (
|
| 1086 |
-
<div className="text-green-400 mt-1">讨论进行中...</div>
|
| 1087 |
-
)}
|
| 1088 |
-
</div>
|
| 1089 |
-
)}
|
| 1090 |
-
|
| 1091 |
-
{/* 系统状态提示 */}
|
| 1092 |
-
{!isSystemReady && (
|
| 1093 |
-
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 p-3 bg-orange-700 text-white rounded-lg shadow-lg flex items-center text-sm z-50">
|
| 1094 |
-
<AlertTriangle size={20} className="mr-2" />
|
| 1095 |
-
{!hasValidChannels ? '请配置API渠道和密钥' : '请配置AI角色'}
|
| 1096 |
-
。点击设置按钮进行配置。
|
| 1097 |
-
</div>
|
| 1098 |
-
)}
|
| 1099 |
-
</div>
|
| 1100 |
-
);
|
| 1101 |
-
};
|
| 1102 |
-
|
| 1103 |
export default App;
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { ChatMessage, MessageSender, MessagePurpose } from './types';
|
| 3 |
+
import { generateResponse } from './services/openaiService';
|
| 4 |
+
import ChatInput from './components/ChatInput';
|
| 5 |
+
import MessageBubble from './components/MessageBubble';
|
| 6 |
+
import Notepad from './components/Notepad';
|
| 7 |
+
import ModelConfigManager from './components/ModelConfigManager';
|
| 8 |
+
import {
|
| 9 |
+
AiModel,
|
| 10 |
+
AiRole,
|
| 11 |
+
ApiChannel,
|
| 12 |
+
ModelConfigManager as ConfigManager,
|
| 13 |
+
DEFAULT_MANUAL_FIXED_TURNS,
|
| 14 |
+
MIN_MANUAL_FIXED_TURNS,
|
| 15 |
+
MAX_MANUAL_FIXED_TURNS,
|
| 16 |
+
MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL,
|
| 17 |
+
INITIAL_NOTEPAD_CONTENT,
|
| 18 |
+
NOTEPAD_INSTRUCTION_PROMPT_PART,
|
| 19 |
+
NOTEPAD_UPDATE_TAG_START,
|
| 20 |
+
NOTEPAD_UPDATE_TAG_END,
|
| 21 |
+
DISCUSSION_COMPLETE_TAG,
|
| 22 |
+
AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART,
|
| 23 |
+
DiscussionMode
|
| 24 |
+
} from './constants';
|
| 25 |
+
import {
|
| 26 |
+
BotMessageSquare,
|
| 27 |
+
AlertTriangle,
|
| 28 |
+
RefreshCcw,
|
| 29 |
+
SlidersHorizontal,
|
| 30 |
+
Users,
|
| 31 |
+
MessagesSquare,
|
| 32 |
+
Bot,
|
| 33 |
+
ChevronDown,
|
| 34 |
+
Settings,
|
| 35 |
+
Play,
|
| 36 |
+
Pause,
|
| 37 |
+
Square,
|
| 38 |
+
Download
|
| 39 |
+
} from 'lucide-react';
|
| 40 |
+
|
| 41 |
+
interface ParsedAIResponse {
|
| 42 |
+
spokenText: string;
|
| 43 |
+
newNotepadContent: string | null;
|
| 44 |
+
discussionShouldEnd?: boolean;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
interface ActiveRole extends AiRole {
|
| 48 |
+
model: AiModel;
|
| 49 |
+
channel: ApiChannel;
|
| 50 |
+
isProcessing?: boolean;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
interface DiscussionState {
|
| 54 |
+
currentRoleIndex: number;
|
| 55 |
+
currentTurn: number;
|
| 56 |
+
discussionLog: string[];
|
| 57 |
+
isFirstMessage: boolean;
|
| 58 |
+
previousAISignaledStop: boolean;
|
| 59 |
+
discussionEndCount: number;
|
| 60 |
+
userQuery: string;
|
| 61 |
+
imageApiPart?: any;
|
| 62 |
+
commonPromptInstructions: string;
|
| 63 |
+
roleOrder: ActiveRole[];
|
| 64 |
+
maxTurnsForLoop: number;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const parseAIResponse = (responseText: string): ParsedAIResponse => {
|
| 68 |
+
let currentText = responseText.trim();
|
| 69 |
+
let spokenText = "";
|
| 70 |
+
let newNotepadContent: string | null = null;
|
| 71 |
+
let discussionShouldEnd = false;
|
| 72 |
+
|
| 73 |
+
let notepadActionText = "";
|
| 74 |
+
let discussionActionText = "";
|
| 75 |
+
|
| 76 |
+
const notepadStartIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_START);
|
| 77 |
+
const notepadEndIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_END);
|
| 78 |
+
|
| 79 |
+
if (notepadStartIndex !== -1 && notepadEndIndex !== -1 && notepadEndIndex > notepadStartIndex && currentText.endsWith(NOTEPAD_UPDATE_TAG_END)) {
|
| 80 |
+
newNotepadContent = currentText.substring(notepadStartIndex + NOTEPAD_UPDATE_TAG_START.length, notepadEndIndex).trim();
|
| 81 |
+
spokenText = currentText.substring(0, notepadStartIndex).trim();
|
| 82 |
+
|
| 83 |
+
if (newNotepadContent) {
|
| 84 |
+
notepadActionText = "更新了记事本";
|
| 85 |
+
} else {
|
| 86 |
+
notepadActionText = "尝试更新记事本但内容为空";
|
| 87 |
+
}
|
| 88 |
+
} else {
|
| 89 |
+
spokenText = currentText;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (spokenText.includes(DISCUSSION_COMPLETE_TAG)) {
|
| 93 |
+
discussionShouldEnd = true;
|
| 94 |
+
spokenText = spokenText.replace(new RegExp(DISCUSSION_COMPLETE_TAG.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), "").trim();
|
| 95 |
+
discussionActionText = "建议结束讨论";
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if (!spokenText.trim()) {
|
| 99 |
+
if (notepadActionText && discussionActionText) {
|
| 100 |
+
spokenText = `(AI ${notepadActionText}并${discussionActionText})`;
|
| 101 |
+
} else if (notepadActionText) {
|
| 102 |
+
spokenText = `(AI ${notepadActionText})`;
|
| 103 |
+
} else if (discussionActionText) {
|
| 104 |
+
spokenText = `(AI ${discussionActionText})`;
|
| 105 |
+
} else {
|
| 106 |
+
spokenText = "(AI 未提供额外文本回复)";
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return { spokenText: spokenText.trim(), newNotepadContent, discussionShouldEnd };
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const fileToBase64 = (file: File): Promise<string> => {
|
| 114 |
+
return new Promise((resolve, reject) => {
|
| 115 |
+
const reader = new FileReader();
|
| 116 |
+
reader.readAsDataURL(file);
|
| 117 |
+
reader.onload = () => {
|
| 118 |
+
const result = reader.result as string;
|
| 119 |
+
resolve(result.split(',')[1]);
|
| 120 |
+
};
|
| 121 |
+
reader.onerror = (error) => reject(error);
|
| 122 |
+
});
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
const createDynamicMessageSender = (roleName: string): MessageSender => {
|
| 126 |
+
return roleName as MessageSender;
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
const App: React.FC = () => {
|
| 130 |
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
| 131 |
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
| 132 |
+
const [currentTotalProcessingTimeMs, setCurrentTotalProcessingTimeMs] = useState<number>(0);
|
| 133 |
+
|
| 134 |
+
const [notepadContent, setNotepadContent] = useState<string>(INITIAL_NOTEPAD_CONTENT);
|
| 135 |
+
const [lastNotepadUpdateBy, setLastNotepadUpdateBy] = useState<MessageSender | null>(null);
|
| 136 |
+
|
| 137 |
+
const [discussionMode, setDiscussionMode] = useState<DiscussionMode>(DiscussionMode.FixedTurns);
|
| 138 |
+
const [manualFixedTurns, setManualFixedTurns] = useState<number>(DEFAULT_MANUAL_FIXED_TURNS);
|
| 139 |
+
const [isReducedCapacityEnabled, setIsReducedCapacityEnabled] = useState<boolean>(false);
|
| 140 |
+
|
| 141 |
+
const [activeRoles, setActiveRoles] = useState<ActiveRole[]>([]);
|
| 142 |
+
const [channels, setChannels] = useState<ApiChannel[]>([]);
|
| 143 |
+
const [models, setModels] = useState<AiModel[]>([]);
|
| 144 |
+
const [isConfigManagerOpen, setIsConfigManagerOpen] = useState<boolean>(false);
|
| 145 |
+
const [isRoleSelectorOpen, setIsRoleSelectorOpen] = useState<boolean>(false);
|
| 146 |
+
|
| 147 |
+
const [isDiscussionActive, setIsDiscussionActive] = useState<boolean>(false);
|
| 148 |
+
const [streamingMessages, setStreamingMessages] = useState<Map<string, { text: string; isComplete: boolean }>>(new Map());
|
| 149 |
+
const [currentDiscussion, setCurrentDiscussion] = useState<DiscussionState | null>(null);
|
| 150 |
+
|
| 151 |
+
const chatContainerRef = useRef<HTMLDivElement>(null);
|
| 152 |
+
const currentQueryStartTimeRef = useRef<number | null>(null);
|
| 153 |
+
const cancelRequestRef = useRef<boolean>(false);
|
| 154 |
+
|
| 155 |
+
// 实时流式消息管理
|
| 156 |
+
const createStreamingMessage = (sender: MessageSender, purpose: MessagePurpose): string => {
|
| 157 |
+
const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
| 158 |
+
const message: ChatMessage = {
|
| 159 |
+
id: messageId,
|
| 160 |
+
text: '',
|
| 161 |
+
sender,
|
| 162 |
+
purpose,
|
| 163 |
+
timestamp: new Date()
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
setMessages(prev => [...prev, message]);
|
| 167 |
+
setStreamingMessages(prev => new Map(prev.set(messageId, { text: '', isComplete: false })));
|
| 168 |
+
return messageId;
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
const updateStreamingMessage = (messageId: string, fullText: string, isComplete: boolean, durationMs?: number) => {
|
| 172 |
+
setMessages(prev => prev.map(msg =>
|
| 173 |
+
msg.id === messageId ? {
|
| 174 |
+
...msg,
|
| 175 |
+
text: fullText,
|
| 176 |
+
durationMs: isComplete ? durationMs : msg.durationMs
|
| 177 |
+
} : msg
|
| 178 |
+
));
|
| 179 |
+
};
|
| 180 |
+
|
| 181 |
+
const loadConfiguration = () => {
|
| 182 |
+
const allChannels = ConfigManager.getChannels();
|
| 183 |
+
const allModels = ConfigManager.getModels();
|
| 184 |
+
const allRoles = ConfigManager.getActiveRoles();
|
| 185 |
+
|
| 186 |
+
setChannels(allChannels);
|
| 187 |
+
setModels(allModels);
|
| 188 |
+
|
| 189 |
+
const rolesWithModelsAndChannels: ActiveRole[] = allRoles.map(role => {
|
| 190 |
+
const model = allModels.find(m => m.id === role.modelId);
|
| 191 |
+
if (!model) {
|
| 192 |
+
console.warn(`Role ${role.name} references non-existent model ${role.modelId}`);
|
| 193 |
+
return null;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
const channel = allChannels.find(ch => ch.id === model.channelId);
|
| 197 |
+
if (!channel) {
|
| 198 |
+
console.warn(`Model ${model.name} references non-existent channel ${model.channelId}`);
|
| 199 |
+
return null;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
return { ...role, model, channel };
|
| 203 |
+
}).filter(Boolean) as ActiveRole[];
|
| 204 |
+
|
| 205 |
+
setActiveRoles(rolesWithModelsAndChannels);
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
useEffect(() => {
|
| 209 |
+
loadConfiguration();
|
| 210 |
+
}, []);
|
| 211 |
+
|
| 212 |
+
const addMessage = (
|
| 213 |
+
text: string,
|
| 214 |
+
sender: MessageSender,
|
| 215 |
+
purpose: MessagePurpose,
|
| 216 |
+
durationMs?: number,
|
| 217 |
+
image?: ChatMessage['image']
|
| 218 |
+
) => {
|
| 219 |
+
const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
| 220 |
+
const message: ChatMessage = {
|
| 221 |
+
id: messageId,
|
| 222 |
+
text,
|
| 223 |
+
sender,
|
| 224 |
+
purpose,
|
| 225 |
+
timestamp: new Date(),
|
| 226 |
+
durationMs,
|
| 227 |
+
image
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
setMessages(prev => [...prev, message]);
|
| 231 |
+
return messageId;
|
| 232 |
+
};
|
| 233 |
+
|
| 234 |
+
const interruptDiscussion = () => {
|
| 235 |
+
if (isLoading && isDiscussionActive) {
|
| 236 |
+
cancelRequestRef.current = true;
|
| 237 |
+
setIsLoading(false);
|
| 238 |
+
setIsDiscussionActive(false);
|
| 239 |
+
setCurrentDiscussion(null);
|
| 240 |
+
|
| 241 |
+
if (currentTotalProcessingTimeMs > 0) {
|
| 242 |
+
addMessage(
|
| 243 |
+
`讨论已被用户中断 (已进行 ${(currentTotalProcessingTimeMs / 1000).toFixed(2)}秒)`,
|
| 244 |
+
MessageSender.System,
|
| 245 |
+
MessagePurpose.SystemNotification
|
| 246 |
+
);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
setCurrentTotalProcessingTimeMs(0);
|
| 250 |
+
if (currentQueryStartTimeRef.current) {
|
| 251 |
+
currentQueryStartTimeRef.current = null;
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
const exportDiscussionRecord = () => {
|
| 257 |
+
if (messages.length === 0) {
|
| 258 |
+
addMessage('当前没有可导出的消息记录', MessageSender.System, MessagePurpose.SystemNotification);
|
| 259 |
+
return;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// 生成简洁的文本格式导出
|
| 263 |
+
let exportText = `=== Multi-Mind Chat 对话记录 ===\n`;
|
| 264 |
+
exportText += `导出时间: ${new Date().toLocaleString()}\n`;
|
| 265 |
+
exportText += `消息总数: ${messages.length}\n\n`;
|
| 266 |
+
|
| 267 |
+
messages.forEach(msg => {
|
| 268 |
+
if (msg.purpose !== MessagePurpose.SystemNotification) {
|
| 269 |
+
const timeStr = msg.timestamp.toLocaleTimeString();
|
| 270 |
+
const durationStr = msg.durationMs ? ` (${(msg.durationMs / 1000).toFixed(2)}s)` : '';
|
| 271 |
+
exportText += `[${timeStr}] ${msg.sender}${durationStr}: ${msg.text}\n\n`;
|
| 272 |
+
|
| 273 |
+
if (msg.image) {
|
| 274 |
+
exportText += ` [附件: ${msg.image.name} - ${msg.image.type}]\n\n`;
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
if (notepadContent !== INITIAL_NOTEPAD_CONTENT) {
|
| 280 |
+
exportText += `=== 最终记事本内容 ===\n`;
|
| 281 |
+
exportText += `${notepadContent}\n\n`;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' });
|
| 285 |
+
const url = URL.createObjectURL(blob);
|
| 286 |
+
const a = document.createElement('a');
|
| 287 |
+
a.href = url;
|
| 288 |
+
a.download = `对话记录-${new Date().toISOString().split('T')[0]}-${Date.now()}.txt`;
|
| 289 |
+
document.body.appendChild(a);
|
| 290 |
+
a.click();
|
| 291 |
+
document.body.removeChild(a);
|
| 292 |
+
URL.revokeObjectURL(url);
|
| 293 |
+
|
| 294 |
+
addMessage('对话记录已导出', MessageSender.System, MessagePurpose.SystemNotification);
|
| 295 |
+
};
|
| 296 |
+
|
| 297 |
+
const getWelcomeMessageText = () => {
|
| 298 |
+
const modeDescription = discussionMode === DiscussionMode.FixedTurns
|
| 299 |
+
? `固定轮次对话 (${manualFixedTurns}轮)`
|
| 300 |
+
: "AI驱动对话";
|
| 301 |
+
|
| 302 |
+
const roleNames = activeRoles.map(role => role.name).join(' 和 ');
|
| 303 |
+
const roleCount = activeRoles.length;
|
| 304 |
+
const channelCount = channels.length;
|
| 305 |
+
|
| 306 |
+
if (channelCount === 0) {
|
| 307 |
+
return `欢迎使用Multi-Mind Chat 智囊团!请先配置API渠道。点击设置按钮开始配置。`;
|
| 308 |
+
} else if (roleCount === 0) {
|
| 309 |
+
return `欢迎使用Multi-Mind Chat 智囊团!已配置 ${channelCount} 个API渠道,请继续配置AI角色和模型。点击设置按钮开始配置。`;
|
| 310 |
+
} else if (roleCount === 1) {
|
| 311 |
+
return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。当前只有一个活跃角色: ${roleNames}。建议添加更多角色以获得更好的协作体验。`;
|
| 312 |
+
} else {
|
| 313 |
+
return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。活跃的AI角色: ${roleNames}。这些角色将协作讨论您的问题并使用共享记事本。`;
|
| 314 |
+
}
|
| 315 |
+
};
|
| 316 |
+
|
| 317 |
+
const initializeChat = () => {
|
| 318 |
+
setMessages([]);
|
| 319 |
+
setNotepadContent(INITIAL_NOTEPAD_CONTENT);
|
| 320 |
+
setLastNotepadUpdateBy(null);
|
| 321 |
+
setIsDiscussionActive(false);
|
| 322 |
+
setStreamingMessages(new Map());
|
| 323 |
+
setCurrentDiscussion(null);
|
| 324 |
+
|
| 325 |
+
addMessage(
|
| 326 |
+
getWelcomeMessageText(),
|
| 327 |
+
MessageSender.System,
|
| 328 |
+
MessagePurpose.SystemNotification
|
| 329 |
+
);
|
| 330 |
+
};
|
| 331 |
+
|
| 332 |
+
useEffect(() => {
|
| 333 |
+
initializeChat();
|
| 334 |
+
}, [activeRoles, discussionMode, manualFixedTurns, channels]);
|
| 335 |
+
|
| 336 |
+
useEffect(() => {
|
| 337 |
+
if (chatContainerRef.current) {
|
| 338 |
+
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
| 339 |
+
}
|
| 340 |
+
}, [messages]);
|
| 341 |
+
|
| 342 |
+
useEffect(() => {
|
| 343 |
+
let intervalId: number | undefined;
|
| 344 |
+
if (isLoading && currentQueryStartTimeRef.current) {
|
| 345 |
+
intervalId = window.setInterval(() => {
|
| 346 |
+
if (currentQueryStartTimeRef.current) {
|
| 347 |
+
setCurrentTotalProcessingTimeMs(performance.now() - currentQueryStartTimeRef.current);
|
| 348 |
+
}
|
| 349 |
+
}, 100);
|
| 350 |
+
} else {
|
| 351 |
+
if (intervalId) clearInterval(intervalId);
|
| 352 |
+
}
|
| 353 |
+
return () => {
|
| 354 |
+
if (intervalId) clearInterval(intervalId);
|
| 355 |
+
};
|
| 356 |
+
}, [isLoading]);
|
| 357 |
+
|
| 358 |
+
const handleClearChat = () => {
|
| 359 |
+
if (isLoading) {
|
| 360 |
+
cancelRequestRef.current = true;
|
| 361 |
+
}
|
| 362 |
+
setIsLoading(false);
|
| 363 |
+
setIsDiscussionActive(false);
|
| 364 |
+
setCurrentDiscussion(null);
|
| 365 |
+
|
| 366 |
+
setCurrentTotalProcessingTimeMs(0);
|
| 367 |
+
if (currentQueryStartTimeRef.current) {
|
| 368 |
+
currentQueryStartTimeRef.current = null;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
setMessages([]);
|
| 372 |
+
setNotepadContent(INITIAL_NOTEPAD_CONTENT);
|
| 373 |
+
setLastNotepadUpdateBy(null);
|
| 374 |
+
setStreamingMessages(new Map());
|
| 375 |
+
|
| 376 |
+
addMessage(
|
| 377 |
+
getWelcomeMessageText(),
|
| 378 |
+
MessageSender.System,
|
| 379 |
+
MessagePurpose.SystemNotification
|
| 380 |
+
);
|
| 381 |
+
};
|
| 382 |
+
|
| 383 |
+
const handleManualFixedTurnsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 384 |
+
let value = parseInt(e.target.value, 10);
|
| 385 |
+
if (isNaN(value)) {
|
| 386 |
+
value = DEFAULT_MANUAL_FIXED_TURNS;
|
| 387 |
+
}
|
| 388 |
+
value = Math.max(MIN_MANUAL_FIXED_TURNS, Math.min(MAX_MANUAL_FIXED_TURNS, value));
|
| 389 |
+
setManualFixedTurns(value);
|
| 390 |
+
};
|
| 391 |
+
|
| 392 |
+
const toggleRoleActiveState = (roleId: string) => {
|
| 393 |
+
const role = activeRoles.find(r => r.id === roleId);
|
| 394 |
+
if (role) {
|
| 395 |
+
ConfigManager.updateRole(roleId, { isActive: !role.isActive });
|
| 396 |
+
loadConfiguration();
|
| 397 |
+
}
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
const processNextRole = (state: DiscussionState) => {
|
| 401 |
+
if (cancelRequestRef.current) {
|
| 402 |
+
setIsDiscussionActive(false);
|
| 403 |
+
setCurrentDiscussion(null);
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// 检查是否完成所有讨论
|
| 408 |
+
if (state.currentTurn === 0 && state.currentRoleIndex >= state.roleOrder.length) {
|
| 409 |
+
// 第一轮结束,开始多轮讨论
|
| 410 |
+
if (!state.previousAISignaledStop && state.maxTurnsForLoop > 0) {
|
| 411 |
+
const newState = { ...state, currentTurn: 1, currentRoleIndex: 0, discussionEndCount: 0 };
|
| 412 |
+
setCurrentDiscussion(newState);
|
| 413 |
+
processNextRole(newState);
|
| 414 |
+
return;
|
| 415 |
+
} else {
|
| 416 |
+
processFinalAnswer(state);
|
| 417 |
+
return;
|
| 418 |
+
}
|
| 419 |
+
} else if (state.currentTurn > 0 &&
|
| 420 |
+
(state.currentTurn >= state.maxTurnsForLoop ||
|
| 421 |
+
state.previousAISignaledStop ||
|
| 422 |
+
state.currentRoleIndex >= state.roleOrder.length)) {
|
| 423 |
+
processFinalAnswer(state);
|
| 424 |
+
return;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
const currentRole = state.roleOrder[state.currentRoleIndex];
|
| 428 |
+
const shouldUseReducedCapacity = isReducedCapacityEnabled && currentRole.model.supportsReducedCapacity;
|
| 429 |
+
|
| 430 |
+
// 显示系统通知
|
| 431 |
+
addMessage(
|
| 432 |
+
`${currentRole.name} 正在${state.currentTurn === 0 ? '分析问题并提供观点' : '回应其他角色的观点'} (使用 ${currentRole.model.name} - ${currentRole.channel.name})...`,
|
| 433 |
+
MessageSender.System,
|
| 434 |
+
MessagePurpose.SystemNotification
|
| 435 |
+
);
|
| 436 |
+
|
| 437 |
+
// 立即创建消息气泡
|
| 438 |
+
const purpose = state.currentRoleIndex % 2 === 0 ? MessagePurpose.CognitoToMuse : MessagePurpose.MuseToCognito;
|
| 439 |
+
const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
| 440 |
+
|
| 441 |
+
// 立即添加空消息到界面,用户会看到正在输入的效果
|
| 442 |
+
const initialMessage: ChatMessage = {
|
| 443 |
+
id: messageId,
|
| 444 |
+
text: '', // 开始时为空,等待流式输入
|
| 445 |
+
sender: createDynamicMessageSender(currentRole.name),
|
| 446 |
+
purpose,
|
| 447 |
+
timestamp: new Date()
|
| 448 |
+
};
|
| 449 |
+
|
| 450 |
+
setMessages(prev => [...prev, initialMessage]);
|
| 451 |
+
|
| 452 |
+
// 构建提示词
|
| 453 |
+
let prompt: string;
|
| 454 |
+
if (state.isFirstMessage) {
|
| 455 |
+
prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 请针对此查询提供您的初步想法或分析。这是一个多AI协作的环境,其他AI角色稍后会对您的观点进行回应和讨论。用中文回答。\n${state.commonPromptInstructions}`;
|
| 456 |
+
} else if (state.currentTurn === 0) {
|
| 457 |
+
const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 ');
|
| 458 |
+
prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论这个问题。请提供您的观点和分析。用中文回答。\n${state.commonPromptInstructions}`;
|
| 459 |
+
} else {
|
| 460 |
+
const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 ');
|
| 461 |
+
prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论。请对前面的讨论进行回应,提供您的进一步见解或不同观点。保持简洁并使用中文。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`;
|
| 462 |
+
|
| 463 |
+
if (discussionMode === DiscussionMode.AiDriven && state.previousAISignaledStop) {
|
| 464 |
+
prompt += `\n注意:之前有AI角色建议结束讨论。如果您同意,请在回复中包含 ${DISCUSSION_COMPLETE_TAG}。否则,请继续讨论。`;
|
| 465 |
+
} else if (discussionMode === DiscussionMode.AiDriven) {
|
| 466 |
+
prompt += AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART;
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
// 用于后台累积完整响应文本的变量
|
| 471 |
+
let accumulatedText = '';
|
| 472 |
+
|
| 473 |
+
// 开始流式API调用
|
| 474 |
+
generateResponse(
|
| 475 |
+
prompt,
|
| 476 |
+
currentRole.model.apiName,
|
| 477 |
+
currentRole.systemPrompt,
|
| 478 |
+
shouldUseReducedCapacity,
|
| 479 |
+
state.imageApiPart,
|
| 480 |
+
currentRole.channel.baseUrl,
|
| 481 |
+
currentRole.channel.apiKey,
|
| 482 |
+
// 关键的流式回调函数
|
| 483 |
+
(newChunk: string, fullText: string, isComplete: boolean) => {
|
| 484 |
+
// newChunk: 本次新接收到的文本块
|
| 485 |
+
// fullText: API累积的完整文本(用于后续处理)
|
| 486 |
+
// isComplete: 是否完成
|
| 487 |
+
|
| 488 |
+
// 更新后台累积的完整文本
|
| 489 |
+
accumulatedText = fullText;
|
| 490 |
+
|
| 491 |
+
// 实时更新界面显示 - 关键是这里直接使用 fullText 进行显示
|
| 492 |
+
setMessages(prev => prev.map(msg =>
|
| 493 |
+
msg.id === messageId ? {
|
| 494 |
+
...msg,
|
| 495 |
+
text: fullText, // 直接显示累积的完整文本,实现打字机效果
|
| 496 |
+
durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
|
| 497 |
+
} : msg
|
| 498 |
+
));
|
| 499 |
+
}
|
| 500 |
+
).then(response => {
|
| 501 |
+
if (cancelRequestRef.current) {
|
| 502 |
+
setIsDiscussionActive(false);
|
| 503 |
+
setCurrentDiscussion(null);
|
| 504 |
+
return;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
if (response.error) {
|
| 508 |
+
if (response.error.includes("API key not valid") || response.error.includes("401")) {
|
| 509 |
+
setMessages(prev => prev.map(msg =>
|
| 510 |
+
msg.id === messageId ? {
|
| 511 |
+
...msg,
|
| 512 |
+
text: `API密钥无效 (渠道: ${currentRole.channel.name}),请在配置界面中检查密钥设置。`,
|
| 513 |
+
durationMs: response.durationMs
|
| 514 |
+
} : msg
|
| 515 |
+
));
|
| 516 |
+
setIsLoading(false);
|
| 517 |
+
setIsDiscussionActive(false);
|
| 518 |
+
setCurrentDiscussion(null);
|
| 519 |
+
return;
|
| 520 |
+
}
|
| 521 |
+
throw new Error(`${currentRole.name}: ${response.text}`);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// 确保最终消息内容正确
|
| 525 |
+
setMessages(prev => prev.map(msg =>
|
| 526 |
+
msg.id === messageId ? {
|
| 527 |
+
...msg,
|
| 528 |
+
text: response.text,
|
| 529 |
+
durationMs: response.durationMs
|
| 530 |
+
} : msg
|
| 531 |
+
));
|
| 532 |
+
|
| 533 |
+
// 解析响应
|
| 534 |
+
const parsedResponse = parseAIResponse(response.text);
|
| 535 |
+
if (parsedResponse.newNotepadContent !== null) {
|
| 536 |
+
setNotepadContent(parsedResponse.newNotepadContent);
|
| 537 |
+
setLastNotepadUpdateBy(createDynamicMessageSender(currentRole.name));
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
// 更新讨论日志 - 使用解析后的文本
|
| 541 |
+
const newDiscussionLog = [...state.discussionLog, `${currentRole.name}: ${parsedResponse.spokenText}`];
|
| 542 |
+
|
| 543 |
+
// 更新状态
|
| 544 |
+
let newState = {
|
| 545 |
+
...state,
|
| 546 |
+
discussionLog: newDiscussionLog,
|
| 547 |
+
isFirstMessage: false,
|
| 548 |
+
currentRoleIndex: state.currentRoleIndex + 1
|
| 549 |
+
};
|
| 550 |
+
|
| 551 |
+
// 处理AI驱动模式的结束信号
|
| 552 |
+
if (discussionMode === DiscussionMode.AiDriven && parsedResponse.discussionShouldEnd) {
|
| 553 |
+
if (state.currentTurn > 0) {
|
| 554 |
+
newState.discussionEndCount++;
|
| 555 |
+
if (state.previousAISignaledStop || newState.discussionEndCount >= Math.ceil(state.roleOrder.length / 2)) {
|
| 556 |
+
addMessage(`多数AI角色已同意结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 557 |
+
newState.previousAISignaledStop = true;
|
| 558 |
+
} else {
|
| 559 |
+
addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 560 |
+
}
|
| 561 |
+
} else {
|
| 562 |
+
newState.previousAISignaledStop = true;
|
| 563 |
+
addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
setCurrentDiscussion(newState);
|
| 568 |
+
|
| 569 |
+
// 继续处理下一个角色
|
| 570 |
+
setTimeout(() => processNextRole(newState), 100);
|
| 571 |
+
}).catch(error => {
|
| 572 |
+
console.error("处理AI响应时出错:", error);
|
| 573 |
+
addMessage(`错误: ${error instanceof Error ? error.message : "处理响应时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 574 |
+
setIsLoading(false);
|
| 575 |
+
setIsDiscussionActive(false);
|
| 576 |
+
setCurrentDiscussion(null);
|
| 577 |
+
});
|
| 578 |
+
};
|
| 579 |
+
|
| 580 |
+
const processFinalAnswer = (state: DiscussionState) => {
|
| 581 |
+
const finalAnswerRole = state.roleOrder[0];
|
| 582 |
+
const shouldUseReducedCapacity = isReducedCapacityEnabled && finalAnswerRole.model.supportsReducedCapacity;
|
| 583 |
+
|
| 584 |
+
addMessage(
|
| 585 |
+
`${finalAnswerRole.name} 正在综合所有讨论内容,准备最终答案 (使用 ${finalAnswerRole.model.name} - ${finalAnswerRole.channel.name})...`,
|
| 586 |
+
MessageSender.System,
|
| 587 |
+
MessagePurpose.SystemNotification
|
| 588 |
+
);
|
| 589 |
+
|
| 590 |
+
// 立即创建最终答案消息气泡
|
| 591 |
+
const finalMessageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
| 592 |
+
const finalMessage: ChatMessage = {
|
| 593 |
+
id: finalMessageId,
|
| 594 |
+
text: '', // 开始时为空
|
| 595 |
+
sender: createDynamicMessageSender(finalAnswerRole.name),
|
| 596 |
+
purpose: MessagePurpose.FinalResponse,
|
| 597 |
+
timestamp: new Date()
|
| 598 |
+
};
|
| 599 |
+
|
| 600 |
+
setMessages(prev => [...prev, finalMessage]);
|
| 601 |
+
|
| 602 |
+
const finalPrompt = `用户最初的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 您和其他AI角色进行了以下讨论 (均为中文):\n${state.discussionLog.join("\n")}\n基于整个协作讨论过程和共享记事本的最终状态,请综合所有关键观点,为用户提供一个全面、有用的最终答案。直接回复用户,确保答案结构良好,易于理解,并使用中文。如果相关,您可以在答案中引用记事本内容。如果需要,您也可以最后一次更新记事本。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`;
|
| 603 |
+
|
| 604 |
+
generateResponse(
|
| 605 |
+
finalPrompt,
|
| 606 |
+
finalAnswerRole.model.apiName,
|
| 607 |
+
finalAnswerRole.systemPrompt,
|
| 608 |
+
shouldUseReducedCapacity,
|
| 609 |
+
state.imageApiPart,
|
| 610 |
+
finalAnswerRole.channel.baseUrl,
|
| 611 |
+
finalAnswerRole.channel.apiKey,
|
| 612 |
+
// 最终答案的流式回调
|
| 613 |
+
(newChunk: string, fullText: string, isComplete: boolean) => {
|
| 614 |
+
setMessages(prev => prev.map(msg =>
|
| 615 |
+
msg.id === finalMessageId ? {
|
| 616 |
+
...msg,
|
| 617 |
+
text: fullText, // 实时显示累积文本
|
| 618 |
+
durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
|
| 619 |
+
} : msg
|
| 620 |
+
));
|
| 621 |
+
}
|
| 622 |
+
).then(finalResponse => {
|
| 623 |
+
if (cancelRequestRef.current) {
|
| 624 |
+
setIsDiscussionActive(false);
|
| 625 |
+
setCurrentDiscussion(null);
|
| 626 |
+
return;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
if (finalResponse.error) {
|
| 630 |
+
if (finalResponse.error.includes("API key not valid") || finalResponse.error.includes("401")) {
|
| 631 |
+
setMessages(prev => prev.map(msg =>
|
| 632 |
+
msg.id === finalMessageId ? {
|
| 633 |
+
...msg,
|
| 634 |
+
text: `API密钥无效 (渠道: ${finalAnswerRole.channel.name}),请在配置界面中检查密钥设置。`,
|
| 635 |
+
durationMs: finalResponse.durationMs
|
| 636 |
+
} : msg
|
| 637 |
+
));
|
| 638 |
+
setIsLoading(false);
|
| 639 |
+
setIsDiscussionActive(false);
|
| 640 |
+
setCurrentDiscussion(null);
|
| 641 |
+
return;
|
| 642 |
+
}
|
| 643 |
+
throw new Error(`${finalAnswerRole.name}: ${finalResponse.text}`);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
setMessages(prev => prev.map(msg =>
|
| 647 |
+
msg.id === finalMessageId ? {
|
| 648 |
+
...msg,
|
| 649 |
+
text: finalResponse.text,
|
| 650 |
+
durationMs: finalResponse.durationMs
|
| 651 |
+
} : msg
|
| 652 |
+
));
|
| 653 |
+
|
| 654 |
+
const finalParsedResponse = parseAIResponse(finalResponse.text);
|
| 655 |
+
if (finalParsedResponse.newNotepadContent !== null) {
|
| 656 |
+
setNotepadContent(finalParsedResponse.newNotepadContent);
|
| 657 |
+
setLastNotepadUpdateBy(createDynamicMessageSender(finalAnswerRole.name));
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
// 讨论完成
|
| 661 |
+
setIsLoading(false);
|
| 662 |
+
setIsDiscussionActive(false);
|
| 663 |
+
setCurrentDiscussion(null);
|
| 664 |
+
currentQueryStartTimeRef.current = null;
|
| 665 |
+
}).catch(error => {
|
| 666 |
+
console.error("生成最终答案时出错:", error);
|
| 667 |
+
addMessage(`错误: ${error instanceof Error ? error.message : "生成最终答案时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 668 |
+
setIsLoading(false);
|
| 669 |
+
setIsDiscussionActive(false);
|
| 670 |
+
setCurrentDiscussion(null);
|
| 671 |
+
});
|
| 672 |
+
};
|
| 673 |
+
|
| 674 |
+
const handleSendMessage = async (userInput: string, imageFile?: File | null) => {
|
| 675 |
+
if (isLoading) return;
|
| 676 |
+
if (!userInput.trim() && !imageFile) return;
|
| 677 |
+
|
| 678 |
+
if (channels.length === 0) {
|
| 679 |
+
addMessage("请先配置API渠道。点击设置按钮添加渠道。", MessageSender.System, MessagePurpose.SystemNotification);
|
| 680 |
+
return;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
if (activeRoles.length === 0) {
|
| 684 |
+
addMessage("请先配置AI角色。点击设置按钮添加角色。", MessageSender.System, MessagePurpose.SystemNotification);
|
| 685 |
+
return;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
const rolesWithoutApiKey = activeRoles.filter(role => !role.channel.apiKey?.trim());
|
| 689 |
+
if (rolesWithoutApiKey.length > 0) {
|
| 690 |
+
const roleNames = rolesWithoutApiKey.map(role => `${role.name}(${role.channel.name})`).join('、');
|
| 691 |
+
addMessage(`以下角色的API渠道缺少API密钥: ${roleNames}。请在配置界面中设置相应的API密钥。`, MessageSender.System, MessagePurpose.SystemNotification);
|
| 692 |
+
return;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
if (imageFile) {
|
| 696 |
+
const supportsImages = activeRoles.some(role => role.model.supportsImages);
|
| 697 |
+
if (!supportsImages) {
|
| 698 |
+
addMessage("当前活跃的角色都不支持图片处理。请添加支持图片的模型和角色,或移除图片。", MessageSender.System, MessagePurpose.SystemNotification);
|
| 699 |
+
return;
|
| 700 |
+
}
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
setIsDiscussionActive(true);
|
| 704 |
+
cancelRequestRef.current = false;
|
| 705 |
+
setIsLoading(true);
|
| 706 |
+
currentQueryStartTimeRef.current = performance.now();
|
| 707 |
+
setCurrentTotalProcessingTimeMs(0);
|
| 708 |
+
|
| 709 |
+
let userImageForDisplay: ChatMessage['image'] | undefined = undefined;
|
| 710 |
+
if (imageFile) {
|
| 711 |
+
const dataUrl = URL.createObjectURL(imageFile);
|
| 712 |
+
userImageForDisplay = { dataUrl, name: imageFile.name, type: imageFile.type };
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
addMessage(userInput, MessageSender.User, MessagePurpose.UserInput, undefined, userImageForDisplay);
|
| 716 |
+
|
| 717 |
+
let imageApiPart: { inlineData: { mimeType: string; data: string } } | undefined = undefined;
|
| 718 |
+
if (imageFile) {
|
| 719 |
+
try {
|
| 720 |
+
const base64Data = await fileToBase64(imageFile);
|
| 721 |
+
imageApiPart = {
|
| 722 |
+
inlineData: {
|
| 723 |
+
mimeType: imageFile.type,
|
| 724 |
+
data: base64Data,
|
| 725 |
+
},
|
| 726 |
+
};
|
| 727 |
+
} catch (error) {
|
| 728 |
+
console.error("Error converting file to base64:", error);
|
| 729 |
+
addMessage("图片处理失败,请重试。", MessageSender.System, MessagePurpose.SystemNotification);
|
| 730 |
+
setIsLoading(false);
|
| 731 |
+
setIsDiscussionActive(false);
|
| 732 |
+
return;
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
const discussionModeInstruction = discussionMode === DiscussionMode.AiDriven ? AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART : "";
|
| 737 |
+
const commonPromptInstructions = NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent) + discussionModeInstruction;
|
| 738 |
+
|
| 739 |
+
const roleOrder = [...activeRoles];
|
| 740 |
+
const maxTurnsForLoop = discussionMode === DiscussionMode.AiDriven ? MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL : manualFixedTurns;
|
| 741 |
+
|
| 742 |
+
// 初始化讨论状态
|
| 743 |
+
const discussionState: DiscussionState = {
|
| 744 |
+
currentRoleIndex: 0,
|
| 745 |
+
currentTurn: 0,
|
| 746 |
+
discussionLog: [],
|
| 747 |
+
isFirstMessage: true,
|
| 748 |
+
previousAISignaledStop: false,
|
| 749 |
+
discussionEndCount: 0,
|
| 750 |
+
userQuery: userInput,
|
| 751 |
+
imageApiPart,
|
| 752 |
+
commonPromptInstructions,
|
| 753 |
+
roleOrder,
|
| 754 |
+
maxTurnsForLoop
|
| 755 |
+
};
|
| 756 |
+
|
| 757 |
+
setCurrentDiscussion(discussionState);
|
| 758 |
+
|
| 759 |
+
// 开始第一个AI的回复(不等待)
|
| 760 |
+
processNextRole(discussionState);
|
| 761 |
+
|
| 762 |
+
// 清理图片URL
|
| 763 |
+
if (userImageForDisplay?.dataUrl.startsWith('blob:')) {
|
| 764 |
+
// 延迟清理,确保消息已渲染
|
| 765 |
+
setTimeout(() => {
|
| 766 |
+
URL.revokeObjectURL(userImageForDisplay.dataUrl);
|
| 767 |
+
}, 5000);
|
| 768 |
+
}
|
| 769 |
+
};
|
| 770 |
+
|
| 771 |
+
const Separator = () => <div className="h-6 w-px bg-gray-600" aria-hidden="true"></div>;
|
| 772 |
+
|
| 773 |
+
const hasValidChannels = channels.some(ch => ch.apiKey?.trim());
|
| 774 |
+
const isSystemReady = hasValidChannels && activeRoles.length > 0;
|
| 775 |
+
|
| 776 |
+
return (
|
| 777 |
+
<div className="flex flex-col h-screen max-w-7xl mx-auto bg-gray-900 shadow-2xl rounded-lg overflow-hidden">
|
| 778 |
+
<header className="p-4 bg-gray-900 border-b border-gray-700 flex items-center justify-between shrink-0 space-x-2 md:space-x-4 flex-wrap">
|
| 779 |
+
<div className="flex items-center shrink-0">
|
| 780 |
+
<BotMessageSquare size={28} className="mr-2 md:mr-3 text-sky-400" />
|
| 781 |
+
<h1 className="text-xl md:text-2xl font-semibold text-sky-400">Multi-Mind Chat 智囊团</h1>
|
| 782 |
+
</div>
|
| 783 |
+
|
| 784 |
+
<div className="flex items-center space-x-2 md:space-x-3 flex-wrap justify-end gap-y-2">
|
| 785 |
+
{/* 讨论控制按钮 */}
|
| 786 |
+
{isDiscussionActive && (
|
| 787 |
+
<div className="flex items-center space-x-2">
|
| 788 |
+
<button
|
| 789 |
+
onClick={interruptDiscussion}
|
| 790 |
+
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm flex items-center space-x-1"
|
| 791 |
+
title="中断当前讨论"
|
| 792 |
+
>
|
| 793 |
+
<Square size={16} />
|
| 794 |
+
<span>中断讨论</span>
|
| 795 |
+
</button>
|
| 796 |
+
<Separator />
|
| 797 |
+
</div>
|
| 798 |
+
)}
|
| 799 |
+
|
| 800 |
+
{/* 导出按钮 - 有消息时始终可用 */}
|
| 801 |
+
{messages.length > 1 && (
|
| 802 |
+
<div className="flex items-center space-x-2">
|
| 803 |
+
<button
|
| 804 |
+
onClick={exportDiscussionRecord}
|
| 805 |
+
className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center space-x-1"
|
| 806 |
+
title="导出对话记录"
|
| 807 |
+
disabled={isLoading}
|
| 808 |
+
>
|
| 809 |
+
<Download size={16} />
|
| 810 |
+
<span>导出记录</span>
|
| 811 |
+
</button>
|
| 812 |
+
<Separator />
|
| 813 |
+
</div>
|
| 814 |
+
)}
|
| 815 |
+
|
| 816 |
+
{/* 角色管理器 */}
|
| 817 |
+
<div className="relative flex items-center">
|
| 818 |
+
<label className="text-sm text-gray-300 mr-1.5 flex items-center shrink-0">
|
| 819 |
+
<Users size={18} className="mr-1 text-sky-400"/>
|
| 820 |
+
角色:
|
| 821 |
+
</label>
|
| 822 |
+
<button
|
| 823 |
+
onClick={() => setIsRoleSelectorOpen(!isRoleSelectorOpen)}
|
| 824 |
+
className="bg-gray-700 border border-gray-600 text-white text-sm rounded-md p-1.5 focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none flex items-center space-x-2 min-w-[120px]"
|
| 825 |
+
aria-label="管理AI角色"
|
| 826 |
+
>
|
| 827 |
+
<span className="truncate">{activeRoles.length}个活跃</span>
|
| 828 |
+
<ChevronDown size={16} className={`transition-transform ${isRoleSelectorOpen ? 'rotate-180' : ''}`} />
|
| 829 |
+
</button>
|
| 830 |
+
|
| 831 |
+
{isRoleSelectorOpen && (
|
| 832 |
+
<div className="absolute top-full left-0 mt-1 w-80 bg-gray-800 border border-gray-600 rounded-md shadow-lg z-50 max-h-96 overflow-y-auto">
|
| 833 |
+
<div className="p-3 border-b border-gray-700">
|
| 834 |
+
<div className="flex justify-between items-center">
|
| 835 |
+
<h3 className="text-white font-medium">活跃角色</h3>
|
| 836 |
+
<button
|
| 837 |
+
onClick={() => {
|
| 838 |
+
setIsConfigManagerOpen(true);
|
| 839 |
+
setIsRoleSelectorOpen(false);
|
| 840 |
+
}}
|
| 841 |
+
className="text-xs bg-sky-600 hover:bg-sky-700 text-white px-2 py-1 rounded flex items-center space-x-1"
|
| 842 |
+
>
|
| 843 |
+
<Settings size={12} />
|
| 844 |
+
<span>配置</span>
|
| 845 |
+
</button>
|
| 846 |
+
</div>
|
| 847 |
+
</div>
|
| 848 |
+
|
| 849 |
+
{activeRoles.length === 0 ? (
|
| 850 |
+
<div className="p-4 text-center text-gray-400">
|
| 851 |
+
<p className="mb-2">暂无活跃角色</p>
|
| 852 |
+
<button
|
| 853 |
+
onClick={() => {
|
| 854 |
+
setIsConfigManagerOpen(true);
|
| 855 |
+
setIsRoleSelectorOpen(false);
|
| 856 |
+
}}
|
| 857 |
+
className="text-sm bg-sky-600 hover:bg-sky-700 text-white px-3 py-1 rounded"
|
| 858 |
+
>
|
| 859 |
+
添加角色
|
| 860 |
+
</button>
|
| 861 |
+
</div>
|
| 862 |
+
) : (
|
| 863 |
+
<div className="max-h-64 overflow-y-auto">
|
| 864 |
+
{activeRoles.map((role) => (
|
| 865 |
+
<div key={role.id} className="p-3 border-b border-gray-700 last:border-b-0">
|
| 866 |
+
<div className="flex justify-between items-start">
|
| 867 |
+
<div className="flex-1">
|
| 868 |
+
<div className="flex items-center space-x-2">
|
| 869 |
+
<h4 className="text-white font-medium">{role.name}</h4>
|
| 870 |
+
<button
|
| 871 |
+
onClick={() => toggleRoleActiveState(role.id)}
|
| 872 |
+
className={`p-1 rounded transition-colors ${
|
| 873 |
+
role.isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-500 hover:text-gray-400'
|
| 874 |
+
}`}
|
| 875 |
+
title={role.isActive ? '暂停角色' : '激活角色'}
|
| 876 |
+
>
|
| 877 |
+
{role.isActive ? <Play size={14} /> : <Pause size={14} />}
|
| 878 |
+
</button>
|
| 879 |
+
</div>
|
| 880 |
+
<p className="text-gray-400 text-xs">{role.model.name}</p>
|
| 881 |
+
<p className="text-gray-500 text-xs">渠道: {role.channel.name}</p>
|
| 882 |
+
<div className="flex space-x-1 mt-1">
|
| 883 |
+
{role.model.supportsImages && (
|
| 884 |
+
<span className="text-xs bg-green-600 text-white px-1 rounded">图像</span>
|
| 885 |
+
)}
|
| 886 |
+
{role.model.supportsReducedCapacity && (
|
| 887 |
+
<span className="text-xs bg-blue-600 text-white px-1 rounded">优化</span>
|
| 888 |
+
)}
|
| 889 |
+
{!role.channel.apiKey?.trim() && (
|
| 890 |
+
<span className="text-xs bg-red-600 text-white px-1 rounded">缺少密钥</span>
|
| 891 |
+
)}
|
| 892 |
+
</div>
|
| 893 |
+
</div>
|
| 894 |
+
</div>
|
| 895 |
+
</div>
|
| 896 |
+
))}
|
| 897 |
+
</div>
|
| 898 |
+
)}
|
| 899 |
+
</div>
|
| 900 |
+
)}
|
| 901 |
+
|
| 902 |
+
{isRoleSelectorOpen && (
|
| 903 |
+
<div
|
| 904 |
+
className="fixed inset-0 z-40"
|
| 905 |
+
onClick={() => setIsRoleSelectorOpen(false)}
|
| 906 |
+
/>
|
| 907 |
+
)}
|
| 908 |
+
</div>
|
| 909 |
+
|
| 910 |
+
<Separator />
|
| 911 |
+
|
| 912 |
+
<div className="flex items-center space-x-1.5">
|
| 913 |
+
<label
|
| 914 |
+
htmlFor="discussionModeToggle"
|
| 915 |
+
className="flex items-center text-sm text-gray-300 cursor-pointer hover:text-sky-400"
|
| 916 |
+
title={discussionMode === DiscussionMode.FixedTurns ? "切换到AI驱动模式" : "切换到固定轮次模式"}
|
| 917 |
+
>
|
| 918 |
+
{discussionMode === DiscussionMode.FixedTurns
|
| 919 |
+
? <MessagesSquare size={18} className="mr-1 text-sky-400" />
|
| 920 |
+
: <Bot size={18} className="mr-1 text-sky-400" />}
|
| 921 |
+
<span className="mr-1 select-none shrink-0">模式:</span>
|
| 922 |
+
<div className="relative">
|
| 923 |
+
<input
|
| 924 |
+
type="checkbox"
|
| 925 |
+
id="discussionModeToggle"
|
| 926 |
+
className="sr-only peer"
|
| 927 |
+
checked={discussionMode === DiscussionMode.AiDriven}
|
| 928 |
+
onChange={() => setDiscussionMode(prev => prev === DiscussionMode.FixedTurns ? DiscussionMode.AiDriven : DiscussionMode.FixedTurns)}
|
| 929 |
+
aria-label="切换对话模式"
|
| 930 |
+
disabled={isLoading}
|
| 931 |
+
/>
|
| 932 |
+
<div className={`block w-10 h-6 rounded-full transition-colors ${discussionMode === DiscussionMode.AiDriven ? 'bg-sky-500' : 'bg-gray-600'}`}></div>
|
| 933 |
+
<div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${discussionMode === DiscussionMode.AiDriven ? 'translate-x-4' : ''}`}></div>
|
| 934 |
+
</div>
|
| 935 |
+
<span className="ml-1.5 select-none shrink-0 min-w-[4rem] text-left">
|
| 936 |
+
{discussionMode === DiscussionMode.FixedTurns ? '固定' : 'AI驱动'}
|
| 937 |
+
</span>
|
| 938 |
+
</label>
|
| 939 |
+
{discussionMode === DiscussionMode.FixedTurns && (
|
| 940 |
+
<div className="flex items-center text-sm text-gray-300">
|
| 941 |
+
<input
|
| 942 |
+
type="number"
|
| 943 |
+
id="manualFixedTurnsInput"
|
| 944 |
+
value={manualFixedTurns}
|
| 945 |
+
onChange={handleManualFixedTurnsChange}
|
| 946 |
+
min={MIN_MANUAL_FIXED_TURNS}
|
| 947 |
+
max={MAX_MANUAL_FIXED_TURNS}
|
| 948 |
+
className="w-14 bg-gray-700 border border-gray-600 text-white text-sm rounded-md p-1 text-center focus:ring-1 focus:ring-sky-500 focus:border-sky-500 outline-none"
|
| 949 |
+
aria-label="设置固定轮次数量"
|
| 950 |
+
disabled={isLoading}
|
| 951 |
+
/>
|
| 952 |
+
<span className="ml-1 select-none">轮</span>
|
| 953 |
+
</div>
|
| 954 |
+
)}
|
| 955 |
+
</div>
|
| 956 |
+
|
| 957 |
+
<Separator />
|
| 958 |
+
|
| 959 |
+
<label
|
| 960 |
+
htmlFor="capacityToggle"
|
| 961 |
+
className="flex items-center text-sm text-gray-300 cursor-pointer hover:text-sky-400"
|
| 962 |
+
title={isReducedCapacityEnabled ? "切换为优质模式 (完整性能)" : "切换为快速模式 (降低性能)"}
|
| 963 |
+
>
|
| 964 |
+
<SlidersHorizontal size={18} className={`mr-1.5 ${!isReducedCapacityEnabled ? 'text-sky-400' : 'text-gray-500'}`} />
|
| 965 |
+
<span className="mr-2 select-none shrink-0">性能:</span>
|
| 966 |
+
<div className="relative">
|
| 967 |
+
<input
|
| 968 |
+
type="checkbox"
|
| 969 |
+
id="capacityToggle"
|
| 970 |
+
className="sr-only peer"
|
| 971 |
+
checked={!isReducedCapacityEnabled}
|
| 972 |
+
onChange={() => setIsReducedCapacityEnabled(!isReducedCapacityEnabled)}
|
| 973 |
+
aria-label="切换AI性能模式"
|
| 974 |
+
disabled={isLoading}
|
| 975 |
+
/>
|
| 976 |
+
<div className={`block w-10 h-6 rounded-full transition-colors ${!isReducedCapacityEnabled ? 'bg-sky-500 peer-checked:bg-sky-500' : 'bg-gray-600'}`}></div>
|
| 977 |
+
<div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${!isReducedCapacityEnabled ? 'peer-checked:translate-x-4' : ''}`}></div>
|
| 978 |
+
</div>
|
| 979 |
+
<span className="ml-2 w-20 text-left select-none shrink-0">
|
| 980 |
+
{!isReducedCapacityEnabled ? '优质' : '快速'}
|
| 981 |
+
</span>
|
| 982 |
+
</label>
|
| 983 |
+
|
| 984 |
+
<Separator />
|
| 985 |
+
|
| 986 |
+
<button
|
| 987 |
+
onClick={() => setIsConfigManagerOpen(true)}
|
| 988 |
+
className="p-2 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-md shrink-0"
|
| 989 |
+
aria-label="配置管理"
|
| 990 |
+
title="配置管理"
|
| 991 |
+
>
|
| 992 |
+
<Settings size={22} />
|
| 993 |
+
</button>
|
| 994 |
+
|
| 995 |
+
<button
|
| 996 |
+
onClick={handleClearChat}
|
| 997 |
+
className="p-2 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-md shrink-0"
|
| 998 |
+
aria-label="清空会话"
|
| 999 |
+
title="清空会话"
|
| 1000 |
+
disabled={isLoading}
|
| 1001 |
+
>
|
| 1002 |
+
<RefreshCcw size={22} />
|
| 1003 |
+
</button>
|
| 1004 |
+
</div>
|
| 1005 |
+
</header>
|
| 1006 |
+
|
| 1007 |
+
<div className="flex flex-row flex-grow overflow-hidden">
|
| 1008 |
+
<div className="flex flex-col w-2/3 md:w-3/5 lg:w-2/3 h-full">
|
| 1009 |
+
<div ref={chatContainerRef} className="flex-grow p-4 space-y-4 overflow-y-auto bg-gray-800 scroll-smooth">
|
| 1010 |
+
{messages.map((msg) => {
|
| 1011 |
+
const streamingState = streamingMessages.get(msg.id);
|
| 1012 |
+
const displayMessage = streamingState && !streamingState.isComplete
|
| 1013 |
+
? { ...msg, text: streamingState.text }
|
| 1014 |
+
: msg;
|
| 1015 |
+
|
| 1016 |
+
return <MessageBubble key={msg.id} message={displayMessage} />;
|
| 1017 |
+
})}
|
| 1018 |
+
</div>
|
| 1019 |
+
<ChatInput onSendMessage={handleSendMessage} isLoading={isLoading} isApiKeyMissing={!isSystemReady} />
|
| 1020 |
+
</div>
|
| 1021 |
+
|
| 1022 |
+
<div className="w-1/3 md:w-2/5 lg:w-1/3 h-full bg-slate-800">
|
| 1023 |
+
<Notepad
|
| 1024 |
+
content={notepadContent}
|
| 1025 |
+
lastUpdatedBy={lastNotepadUpdateBy}
|
| 1026 |
+
isLoading={isLoading}
|
| 1027 |
+
/>
|
| 1028 |
+
</div>
|
| 1029 |
+
</div>
|
| 1030 |
+
|
| 1031 |
+
{/* 配置管理器 */}
|
| 1032 |
+
<ModelConfigManager
|
| 1033 |
+
isOpen={isConfigManagerOpen}
|
| 1034 |
+
onClose={() => setIsConfigManagerOpen(false)}
|
| 1035 |
+
onConfigChange={loadConfiguration}
|
| 1036 |
+
/>
|
| 1037 |
+
|
| 1038 |
+
{/* 处理时间显示 */}
|
| 1039 |
+
{(isLoading || (currentTotalProcessingTimeMs > 0 && !isLoading) || (isLoading && currentTotalProcessingTimeMs === 0)) && (
|
| 1040 |
+
<div className="fixed bottom-4 right-4 md:bottom-6 md:right-6 bg-gray-900 bg-opacity-80 text-white p-2 rounded-md shadow-lg text-xs z-50">
|
| 1041 |
+
总耗时: {(currentTotalProcessingTimeMs / 1000).toFixed(2)}s
|
| 1042 |
+
{isDiscussionActive && (
|
| 1043 |
+
<div className="text-green-400 mt-1">讨论进行中...</div>
|
| 1044 |
+
)}
|
| 1045 |
+
</div>
|
| 1046 |
+
)}
|
| 1047 |
+
|
| 1048 |
+
{/* 系统状态提示 */}
|
| 1049 |
+
{!isSystemReady && (
|
| 1050 |
+
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 p-3 bg-orange-700 text-white rounded-lg shadow-lg flex items-center text-sm z-50">
|
| 1051 |
+
<AlertTriangle size={20} className="mr-2" />
|
| 1052 |
+
{!hasValidChannels ? '请配置API渠道和密钥' : '请配置AI角色'}
|
| 1053 |
+
。点击设置按钮进行配置。
|
| 1054 |
+
</div>
|
| 1055 |
+
)}
|
| 1056 |
+
</div>
|
| 1057 |
+
);
|
| 1058 |
+
};
|
| 1059 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
export default App;
|
Dockerfile
CHANGED
|
@@ -1,34 +1,34 @@
|
|
| 1 |
-
# Multi-Mind Chat 智囊团 Dockerfile
|
| 2 |
-
FROM node:18-alpine
|
| 3 |
-
|
| 4 |
-
# 设置工作目录
|
| 5 |
-
WORKDIR /app
|
| 6 |
-
|
| 7 |
-
# 复制 package.json 和 package-lock.json
|
| 8 |
-
COPY package*.json ./
|
| 9 |
-
|
| 10 |
-
# 安装依赖
|
| 11 |
-
RUN npm ci
|
| 12 |
-
|
| 13 |
-
# 复制应用代码
|
| 14 |
-
COPY . .
|
| 15 |
-
|
| 16 |
-
# 直接设置预置渠道的环境变量(在此处修改您的配置)
|
| 17 |
-
ENV VITE_PRESET_CHANNEL_NAME="预置服务"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# 构建前端应用
|
| 21 |
-
RUN npm run build
|
| 22 |
-
|
| 23 |
-
# 安装静态文件服务器
|
| 24 |
-
RUN npm install -g serve
|
| 25 |
-
|
| 26 |
-
# 暴露端口
|
| 27 |
-
EXPOSE 7860
|
| 28 |
-
|
| 29 |
-
# 设置运行时环境变量
|
| 30 |
-
ENV NODE_ENV=production
|
| 31 |
-
ENV PORT=7860
|
| 32 |
-
|
| 33 |
-
# 启动命令
|
| 34 |
CMD ["serve", "-s", "dist", "-l", "7860"]
|
|
|
|
| 1 |
+
# Multi-Mind Chat 智囊团 Dockerfile
|
| 2 |
+
FROM node:18-alpine
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 复制 package.json 和 package-lock.json
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# 安装依赖
|
| 11 |
+
RUN npm ci
|
| 12 |
+
|
| 13 |
+
# 复制应用代码
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# 直接设置预置渠道的环境变量(在此处修改您的配置)
|
| 17 |
+
ENV VITE_PRESET_CHANNEL_NAME="预置服务"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# 构建前端应用
|
| 21 |
+
RUN npm run build
|
| 22 |
+
|
| 23 |
+
# 安装静态文件服务器
|
| 24 |
+
RUN npm install -g serve
|
| 25 |
+
|
| 26 |
+
# 暴露端口
|
| 27 |
+
EXPOSE 7860
|
| 28 |
+
|
| 29 |
+
# 设置运行时环境变量
|
| 30 |
+
ENV NODE_ENV=production
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
|
| 33 |
+
# 启动命令
|
| 34 |
CMD ["serve", "-s", "dist", "-l", "7860"]
|
README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: MultiMindChat
|
| 3 |
-
emoji: 📚
|
| 4 |
-
colorFrom: purple
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MultiMindChat
|
| 3 |
+
emoji: 📚
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
components/ChatInput.tsx
CHANGED
|
@@ -1,191 +1,191 @@
|
|
| 1 |
-
|
| 2 |
-
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
| 3 |
-
import { Send, Paperclip, XCircle } from 'lucide-react';
|
| 4 |
-
import LoadingSpinner from './LoadingSpinner';
|
| 5 |
-
|
| 6 |
-
interface ChatInputProps {
|
| 7 |
-
onSendMessage: (message: string, imageFile?: File | null) => void;
|
| 8 |
-
isLoading: boolean;
|
| 9 |
-
isApiKeyMissing: boolean;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
| 13 |
-
|
| 14 |
-
const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage, isLoading, isApiKeyMissing }) => {
|
| 15 |
-
const [inputValue, setInputValue] = useState('');
|
| 16 |
-
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
| 17 |
-
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
|
| 18 |
-
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
| 19 |
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 20 |
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 21 |
-
|
| 22 |
-
useEffect(() => {
|
| 23 |
-
if (selectedImage) {
|
| 24 |
-
const objectUrl = URL.createObjectURL(selectedImage);
|
| 25 |
-
setImagePreviewUrl(objectUrl);
|
| 26 |
-
return () => URL.revokeObjectURL(objectUrl);
|
| 27 |
-
}
|
| 28 |
-
setImagePreviewUrl(null);
|
| 29 |
-
}, [selectedImage]);
|
| 30 |
-
|
| 31 |
-
const handleImageFile = (file: File | null) => {
|
| 32 |
-
if (file && ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
| 33 |
-
setSelectedImage(file);
|
| 34 |
-
} else if (file) {
|
| 35 |
-
alert('不支持的文件类型。请选择 JPG, PNG, GIF, 或 WEBP 格式的图片。');
|
| 36 |
-
setSelectedImage(null);
|
| 37 |
-
} else {
|
| 38 |
-
setSelectedImage(null);
|
| 39 |
-
}
|
| 40 |
-
// Reset file input value to allow selecting the same file again
|
| 41 |
-
if (fileInputRef.current) {
|
| 42 |
-
fileInputRef.current.value = "";
|
| 43 |
-
}
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
-
const removeImage = () => {
|
| 47 |
-
setSelectedImage(null);
|
| 48 |
-
setImagePreviewUrl(null);
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
const triggerSendMessage = () => {
|
| 52 |
-
if ((inputValue.trim() || selectedImage) && !isLoading && !isApiKeyMissing) {
|
| 53 |
-
onSendMessage(inputValue.trim(), selectedImage);
|
| 54 |
-
setInputValue('');
|
| 55 |
-
removeImage(); // Clear image after sending
|
| 56 |
-
if (textareaRef.current) { // Reset textarea height after sending
|
| 57 |
-
textareaRef.current.style.height = 'auto';
|
| 58 |
-
}
|
| 59 |
-
}
|
| 60 |
-
};
|
| 61 |
-
|
| 62 |
-
const handleSubmit = (e: React.FormEvent) => {
|
| 63 |
-
e.preventDefault();
|
| 64 |
-
triggerSendMessage();
|
| 65 |
-
};
|
| 66 |
-
|
| 67 |
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
| 68 |
-
if (e.key === 'Enter' && e.ctrlKey) {
|
| 69 |
-
e.preventDefault(); // Prevent new line
|
| 70 |
-
triggerSendMessage();
|
| 71 |
-
}
|
| 72 |
-
};
|
| 73 |
-
|
| 74 |
-
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
| 75 |
-
const items = e.clipboardData?.items;
|
| 76 |
-
if (items) {
|
| 77 |
-
for (let i = 0; i < items.length; i++) {
|
| 78 |
-
if (ACCEPTED_IMAGE_TYPES.includes(items[i].type)) {
|
| 79 |
-
const file = items[i].getAsFile();
|
| 80 |
-
if (file) {
|
| 81 |
-
handleImageFile(file);
|
| 82 |
-
e.preventDefault(); // Prevent pasting file path as text
|
| 83 |
-
break;
|
| 84 |
-
}
|
| 85 |
-
}
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
}, []);
|
| 89 |
-
|
| 90 |
-
const handleDrop = useCallback((e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 91 |
-
e.preventDefault();
|
| 92 |
-
setIsDraggingOver(false);
|
| 93 |
-
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
| 94 |
-
handleImageFile(e.dataTransfer.files[0]);
|
| 95 |
-
e.dataTransfer.clearData();
|
| 96 |
-
}
|
| 97 |
-
}, []);
|
| 98 |
-
|
| 99 |
-
const handleDragOver = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 100 |
-
e.preventDefault();
|
| 101 |
-
setIsDraggingOver(true);
|
| 102 |
-
};
|
| 103 |
-
|
| 104 |
-
const handleDragLeave = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 105 |
-
e.preventDefault();
|
| 106 |
-
setIsDraggingOver(false);
|
| 107 |
-
};
|
| 108 |
-
|
| 109 |
-
const handleFileButtonClick = () => {
|
| 110 |
-
if (fileInputRef.current) {
|
| 111 |
-
fileInputRef.current.click();
|
| 112 |
-
}
|
| 113 |
-
};
|
| 114 |
-
|
| 115 |
-
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 116 |
-
if (e.target.files && e.target.files.length > 0) {
|
| 117 |
-
handleImageFile(e.target.files[0]);
|
| 118 |
-
}
|
| 119 |
-
};
|
| 120 |
-
|
| 121 |
-
const isDisabled = isLoading || isApiKeyMissing;
|
| 122 |
-
|
| 123 |
-
return (
|
| 124 |
-
<form onSubmit={handleSubmit} className="p-4 bg-gray-800 border-t border-gray-700">
|
| 125 |
-
{imagePreviewUrl && selectedImage && (
|
| 126 |
-
<div className="mb-2 p-2 bg-gray-700 rounded-md relative max-w-xs">
|
| 127 |
-
<img src={imagePreviewUrl} alt={selectedImage.name || "图片预览"} className="max-h-24 max-w-full rounded" />
|
| 128 |
-
<button
|
| 129 |
-
type="button"
|
| 130 |
-
onClick={removeImage}
|
| 131 |
-
className="absolute top-1 right-1 bg-black bg-opacity-50 text-white rounded-full p-0.5 hover:bg-opacity-75"
|
| 132 |
-
aria-label="移除图片"
|
| 133 |
-
>
|
| 134 |
-
<XCircle size={20} />
|
| 135 |
-
</button>
|
| 136 |
-
<div className="text-xs text-gray-300 mt-1 truncate">{selectedImage.name} ({(selectedImage.size / 1024).toFixed(1)} KB)</div>
|
| 137 |
-
</div>
|
| 138 |
-
)}
|
| 139 |
-
<div className="flex items-end space-x-2">
|
| 140 |
-
<textarea
|
| 141 |
-
ref={textareaRef}
|
| 142 |
-
value={inputValue}
|
| 143 |
-
onChange={(e) => setInputValue(e.target.value)}
|
| 144 |
-
onKeyDown={handleKeyDown} // Added keydown handler
|
| 145 |
-
onPaste={handlePaste}
|
| 146 |
-
onDrop={handleDrop}
|
| 147 |
-
onDragOver={handleDragOver}
|
| 148 |
-
onDragLeave={handleDragLeave}
|
| 149 |
-
placeholder={isApiKeyMissing ? "API密钥未配置,聊天功能已禁用。" : (isDraggingOver ? "将图片拖放到此处" : "输入您的消息 (Ctrl+Enter 发送) 或粘贴/拖放图片...")}
|
| 150 |
-
className={`flex-grow p-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none placeholder-gray-400 disabled:opacity-50 resize-none min-h-[48px] max-h-[150px] ${isDraggingOver ? 'ring-2 ring-sky-500 border-sky-500' : ''}`}
|
| 151 |
-
rows={1} // Start with 1 row, auto-expands
|
| 152 |
-
disabled={isDisabled}
|
| 153 |
-
aria-label="聊天输入框"
|
| 154 |
-
onInput={(e) => { // Auto-resize textarea
|
| 155 |
-
const target = e.target as HTMLTextAreaElement;
|
| 156 |
-
target.style.height = 'auto';
|
| 157 |
-
target.style.height = `${target.scrollHeight}px`;
|
| 158 |
-
}}
|
| 159 |
-
/>
|
| 160 |
-
<input
|
| 161 |
-
type="file"
|
| 162 |
-
ref={fileInputRef}
|
| 163 |
-
onChange={handleFileSelected}
|
| 164 |
-
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
| 165 |
-
className="hidden"
|
| 166 |
-
aria-label="选择图片文件"
|
| 167 |
-
/>
|
| 168 |
-
<button
|
| 169 |
-
type="button"
|
| 170 |
-
onClick={handleFileButtonClick}
|
| 171 |
-
className="p-3 bg-gray-600 hover:bg-gray-500 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
|
| 172 |
-
disabled={isDisabled}
|
| 173 |
-
aria-label="添加图片附件"
|
| 174 |
-
title="添加图片"
|
| 175 |
-
>
|
| 176 |
-
<Paperclip size={24} />
|
| 177 |
-
</button>
|
| 178 |
-
<button
|
| 179 |
-
type="submit"
|
| 180 |
-
className="p-3 bg-sky-600 hover:bg-sky-700 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
|
| 181 |
-
disabled={isDisabled || (!inputValue.trim() && !selectedImage)}
|
| 182 |
-
aria-label={isLoading ? "发送中" : "发送消息"}
|
| 183 |
-
>
|
| 184 |
-
{isLoading ? <LoadingSpinner size="w-6 h-6" color="text-white" /> : <Send size={24} />}
|
| 185 |
-
</button>
|
| 186 |
-
</div>
|
| 187 |
-
</form>
|
| 188 |
-
);
|
| 189 |
-
};
|
| 190 |
-
|
| 191 |
-
export default ChatInput;
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
| 3 |
+
import { Send, Paperclip, XCircle } from 'lucide-react';
|
| 4 |
+
import LoadingSpinner from './LoadingSpinner';
|
| 5 |
+
|
| 6 |
+
interface ChatInputProps {
|
| 7 |
+
onSendMessage: (message: string, imageFile?: File | null) => void;
|
| 8 |
+
isLoading: boolean;
|
| 9 |
+
isApiKeyMissing: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
| 13 |
+
|
| 14 |
+
const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage, isLoading, isApiKeyMissing }) => {
|
| 15 |
+
const [inputValue, setInputValue] = useState('');
|
| 16 |
+
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
| 17 |
+
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
|
| 18 |
+
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
| 19 |
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 20 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
if (selectedImage) {
|
| 24 |
+
const objectUrl = URL.createObjectURL(selectedImage);
|
| 25 |
+
setImagePreviewUrl(objectUrl);
|
| 26 |
+
return () => URL.revokeObjectURL(objectUrl);
|
| 27 |
+
}
|
| 28 |
+
setImagePreviewUrl(null);
|
| 29 |
+
}, [selectedImage]);
|
| 30 |
+
|
| 31 |
+
const handleImageFile = (file: File | null) => {
|
| 32 |
+
if (file && ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
| 33 |
+
setSelectedImage(file);
|
| 34 |
+
} else if (file) {
|
| 35 |
+
alert('不支持的文件类型。请选择 JPG, PNG, GIF, 或 WEBP 格式的图片。');
|
| 36 |
+
setSelectedImage(null);
|
| 37 |
+
} else {
|
| 38 |
+
setSelectedImage(null);
|
| 39 |
+
}
|
| 40 |
+
// Reset file input value to allow selecting the same file again
|
| 41 |
+
if (fileInputRef.current) {
|
| 42 |
+
fileInputRef.current.value = "";
|
| 43 |
+
}
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const removeImage = () => {
|
| 47 |
+
setSelectedImage(null);
|
| 48 |
+
setImagePreviewUrl(null);
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const triggerSendMessage = () => {
|
| 52 |
+
if ((inputValue.trim() || selectedImage) && !isLoading && !isApiKeyMissing) {
|
| 53 |
+
onSendMessage(inputValue.trim(), selectedImage);
|
| 54 |
+
setInputValue('');
|
| 55 |
+
removeImage(); // Clear image after sending
|
| 56 |
+
if (textareaRef.current) { // Reset textarea height after sending
|
| 57 |
+
textareaRef.current.style.height = 'auto';
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 63 |
+
e.preventDefault();
|
| 64 |
+
triggerSendMessage();
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
| 68 |
+
if (e.key === 'Enter' && e.ctrlKey) {
|
| 69 |
+
e.preventDefault(); // Prevent new line
|
| 70 |
+
triggerSendMessage();
|
| 71 |
+
}
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
| 75 |
+
const items = e.clipboardData?.items;
|
| 76 |
+
if (items) {
|
| 77 |
+
for (let i = 0; i < items.length; i++) {
|
| 78 |
+
if (ACCEPTED_IMAGE_TYPES.includes(items[i].type)) {
|
| 79 |
+
const file = items[i].getAsFile();
|
| 80 |
+
if (file) {
|
| 81 |
+
handleImageFile(file);
|
| 82 |
+
e.preventDefault(); // Prevent pasting file path as text
|
| 83 |
+
break;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}, []);
|
| 89 |
+
|
| 90 |
+
const handleDrop = useCallback((e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 91 |
+
e.preventDefault();
|
| 92 |
+
setIsDraggingOver(false);
|
| 93 |
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
| 94 |
+
handleImageFile(e.dataTransfer.files[0]);
|
| 95 |
+
e.dataTransfer.clearData();
|
| 96 |
+
}
|
| 97 |
+
}, []);
|
| 98 |
+
|
| 99 |
+
const handleDragOver = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 100 |
+
e.preventDefault();
|
| 101 |
+
setIsDraggingOver(true);
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const handleDragLeave = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
| 105 |
+
e.preventDefault();
|
| 106 |
+
setIsDraggingOver(false);
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const handleFileButtonClick = () => {
|
| 110 |
+
if (fileInputRef.current) {
|
| 111 |
+
fileInputRef.current.click();
|
| 112 |
+
}
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 116 |
+
if (e.target.files && e.target.files.length > 0) {
|
| 117 |
+
handleImageFile(e.target.files[0]);
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const isDisabled = isLoading || isApiKeyMissing;
|
| 122 |
+
|
| 123 |
+
return (
|
| 124 |
+
<form onSubmit={handleSubmit} className="p-4 bg-gray-800 border-t border-gray-700">
|
| 125 |
+
{imagePreviewUrl && selectedImage && (
|
| 126 |
+
<div className="mb-2 p-2 bg-gray-700 rounded-md relative max-w-xs">
|
| 127 |
+
<img src={imagePreviewUrl} alt={selectedImage.name || "图片预览"} className="max-h-24 max-w-full rounded" />
|
| 128 |
+
<button
|
| 129 |
+
type="button"
|
| 130 |
+
onClick={removeImage}
|
| 131 |
+
className="absolute top-1 right-1 bg-black bg-opacity-50 text-white rounded-full p-0.5 hover:bg-opacity-75"
|
| 132 |
+
aria-label="移除图片"
|
| 133 |
+
>
|
| 134 |
+
<XCircle size={20} />
|
| 135 |
+
</button>
|
| 136 |
+
<div className="text-xs text-gray-300 mt-1 truncate">{selectedImage.name} ({(selectedImage.size / 1024).toFixed(1)} KB)</div>
|
| 137 |
+
</div>
|
| 138 |
+
)}
|
| 139 |
+
<div className="flex items-end space-x-2">
|
| 140 |
+
<textarea
|
| 141 |
+
ref={textareaRef}
|
| 142 |
+
value={inputValue}
|
| 143 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 144 |
+
onKeyDown={handleKeyDown} // Added keydown handler
|
| 145 |
+
onPaste={handlePaste}
|
| 146 |
+
onDrop={handleDrop}
|
| 147 |
+
onDragOver={handleDragOver}
|
| 148 |
+
onDragLeave={handleDragLeave}
|
| 149 |
+
placeholder={isApiKeyMissing ? "API密钥未配置,聊天功能已禁用。" : (isDraggingOver ? "将图片拖放到此处" : "输入您的消息 (Ctrl+Enter 发送) 或粘贴/拖放图片...")}
|
| 150 |
+
className={`flex-grow p-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none placeholder-gray-400 disabled:opacity-50 resize-none min-h-[48px] max-h-[150px] ${isDraggingOver ? 'ring-2 ring-sky-500 border-sky-500' : ''}`}
|
| 151 |
+
rows={1} // Start with 1 row, auto-expands
|
| 152 |
+
disabled={isDisabled}
|
| 153 |
+
aria-label="聊天输入框"
|
| 154 |
+
onInput={(e) => { // Auto-resize textarea
|
| 155 |
+
const target = e.target as HTMLTextAreaElement;
|
| 156 |
+
target.style.height = 'auto';
|
| 157 |
+
target.style.height = `${target.scrollHeight}px`;
|
| 158 |
+
}}
|
| 159 |
+
/>
|
| 160 |
+
<input
|
| 161 |
+
type="file"
|
| 162 |
+
ref={fileInputRef}
|
| 163 |
+
onChange={handleFileSelected}
|
| 164 |
+
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
| 165 |
+
className="hidden"
|
| 166 |
+
aria-label="选择图片文件"
|
| 167 |
+
/>
|
| 168 |
+
<button
|
| 169 |
+
type="button"
|
| 170 |
+
onClick={handleFileButtonClick}
|
| 171 |
+
className="p-3 bg-gray-600 hover:bg-gray-500 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
|
| 172 |
+
disabled={isDisabled}
|
| 173 |
+
aria-label="添加图片附件"
|
| 174 |
+
title="添加图片"
|
| 175 |
+
>
|
| 176 |
+
<Paperclip size={24} />
|
| 177 |
+
</button>
|
| 178 |
+
<button
|
| 179 |
+
type="submit"
|
| 180 |
+
className="p-3 bg-sky-600 hover:bg-sky-700 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
|
| 181 |
+
disabled={isDisabled || (!inputValue.trim() && !selectedImage)}
|
| 182 |
+
aria-label={isLoading ? "发送中" : "发送消息"}
|
| 183 |
+
>
|
| 184 |
+
{isLoading ? <LoadingSpinner size="w-6 h-6" color="text-white" /> : <Send size={24} />}
|
| 185 |
+
</button>
|
| 186 |
+
</div>
|
| 187 |
+
</form>
|
| 188 |
+
);
|
| 189 |
+
};
|
| 190 |
+
|
| 191 |
+
export default ChatInput;
|
components/LoadingSpinner.tsx
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
|
| 2 |
-
import React from 'react';
|
| 3 |
-
|
| 4 |
-
interface LoadingSpinnerProps {
|
| 5 |
-
size?: string; // e.g., 'w-8 h-8'
|
| 6 |
-
color?: string; // e.g., 'text-blue-500'
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ size = 'w-5 h-5', color = 'text-sky-400' }) => {
|
| 10 |
-
return (
|
| 11 |
-
<svg
|
| 12 |
-
className={`animate-spin ${size} ${color}`}
|
| 13 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 14 |
-
fill="none"
|
| 15 |
-
viewBox="0 0 24 24"
|
| 16 |
-
>
|
| 17 |
-
<circle
|
| 18 |
-
className="opacity-25"
|
| 19 |
-
cx="12"
|
| 20 |
-
cy="12"
|
| 21 |
-
r="10"
|
| 22 |
-
stroke="currentColor"
|
| 23 |
-
strokeWidth="4"
|
| 24 |
-
></circle>
|
| 25 |
-
<path
|
| 26 |
-
className="opacity-75"
|
| 27 |
-
fill="currentColor"
|
| 28 |
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
| 29 |
-
></path>
|
| 30 |
-
</svg>
|
| 31 |
-
);
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
export default LoadingSpinner;
|
| 35 |
|
|
|
|
| 1 |
+
|
| 2 |
+
import React from 'react';
|
| 3 |
+
|
| 4 |
+
interface LoadingSpinnerProps {
|
| 5 |
+
size?: string; // e.g., 'w-8 h-8'
|
| 6 |
+
color?: string; // e.g., 'text-blue-500'
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ size = 'w-5 h-5', color = 'text-sky-400' }) => {
|
| 10 |
+
return (
|
| 11 |
+
<svg
|
| 12 |
+
className={`animate-spin ${size} ${color}`}
|
| 13 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 14 |
+
fill="none"
|
| 15 |
+
viewBox="0 0 24 24"
|
| 16 |
+
>
|
| 17 |
+
<circle
|
| 18 |
+
className="opacity-25"
|
| 19 |
+
cx="12"
|
| 20 |
+
cy="12"
|
| 21 |
+
r="10"
|
| 22 |
+
stroke="currentColor"
|
| 23 |
+
strokeWidth="4"
|
| 24 |
+
></circle>
|
| 25 |
+
<path
|
| 26 |
+
className="opacity-75"
|
| 27 |
+
fill="currentColor"
|
| 28 |
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
| 29 |
+
></path>
|
| 30 |
+
</svg>
|
| 31 |
+
);
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
export default LoadingSpinner;
|
| 35 |
|
components/MessageBubble.tsx
CHANGED
|
@@ -1,201 +1,201 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import { ChatMessage, MessageSender, MessagePurpose } from '../types';
|
| 3 |
-
import { Lightbulb, MessageSquareText, UserCircle, Zap, AlertTriangle, Copy, Check, MoreHorizontal } from 'lucide-react';
|
| 4 |
-
import { marked } from 'marked';
|
| 5 |
-
import DOMPurify from 'dompurify';
|
| 6 |
-
|
| 7 |
-
interface SenderIconProps {
|
| 8 |
-
sender: MessageSender;
|
| 9 |
-
purpose: MessagePurpose;
|
| 10 |
-
messageText: string;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
const SenderIcon: React.FC<SenderIconProps> = ({ sender, purpose, messageText }) => {
|
| 14 |
-
const iconClass = "w-5 h-5 mr-2 flex-shrink-0";
|
| 15 |
-
switch (sender) {
|
| 16 |
-
case MessageSender.User:
|
| 17 |
-
return <UserCircle className={`${iconClass} text-blue-400`} />;
|
| 18 |
-
case MessageSender.Cognito:
|
| 19 |
-
return <Lightbulb className={`${iconClass} text-green-400`} />;
|
| 20 |
-
case MessageSender.Muse:
|
| 21 |
-
return <Zap className={`${iconClass} text-purple-400`} />;
|
| 22 |
-
case MessageSender.System:
|
| 23 |
-
if (
|
| 24 |
-
purpose === MessagePurpose.SystemNotification &&
|
| 25 |
-
(messageText.toLowerCase().includes("error") ||
|
| 26 |
-
messageText.toLowerCase().includes("错误") ||
|
| 27 |
-
messageText.toLowerCase().includes("警告"))
|
| 28 |
-
) {
|
| 29 |
-
return <AlertTriangle className={`${iconClass} text-red-400`} />;
|
| 30 |
-
}
|
| 31 |
-
return <MessageSquareText className={`${iconClass} text-gray-400`} />;
|
| 32 |
-
default:
|
| 33 |
-
// For dynamic role names, use a generic bot icon with dynamic color
|
| 34 |
-
const colorClasses = [
|
| 35 |
-
'text-green-400', 'text-purple-400', 'text-blue-400',
|
| 36 |
-
'text-yellow-400', 'text-pink-400', 'text-indigo-400'
|
| 37 |
-
];
|
| 38 |
-
const colorIndex = typeof sender === 'string' ?
|
| 39 |
-
sender.length % colorClasses.length : 0;
|
| 40 |
-
return <Lightbulb className={`${iconClass} ${colorClasses[colorIndex]}`} />;
|
| 41 |
-
}
|
| 42 |
-
};
|
| 43 |
-
|
| 44 |
-
const getSenderNameStyle = (sender: MessageSender): string => {
|
| 45 |
-
switch (sender) {
|
| 46 |
-
case MessageSender.User: return "text-blue-300";
|
| 47 |
-
case MessageSender.Cognito: return "text-green-300";
|
| 48 |
-
case MessageSender.Muse: return "text-purple-300";
|
| 49 |
-
case MessageSender.System: return "text-gray-400";
|
| 50 |
-
default:
|
| 51 |
-
// For dynamic role names, apply dynamic colors
|
| 52 |
-
const colorClasses = [
|
| 53 |
-
'text-green-300', 'text-purple-300', 'text-blue-300',
|
| 54 |
-
'text-yellow-300', 'text-pink-300', 'text-indigo-300'
|
| 55 |
-
];
|
| 56 |
-
const colorIndex = typeof sender === 'string' ?
|
| 57 |
-
sender.length % colorClasses.length : 0;
|
| 58 |
-
return colorClasses[colorIndex];
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
const getBubbleStyle = (sender: MessageSender, purpose: MessagePurpose, messageText: string): string => {
|
| 63 |
-
let baseStyle = "mb-4 p-4 rounded-lg shadow-md max-w-xl break-words relative ";
|
| 64 |
-
if (purpose === MessagePurpose.SystemNotification) {
|
| 65 |
-
if (
|
| 66 |
-
messageText.toLowerCase().includes("error") ||
|
| 67 |
-
messageText.toLowerCase().includes("错误") ||
|
| 68 |
-
messageText.toLowerCase().includes("警告") ||
|
| 69 |
-
messageText.toLowerCase().includes("critical") ||
|
| 70 |
-
messageText.toLowerCase().includes("严重")
|
| 71 |
-
) {
|
| 72 |
-
return baseStyle + "bg-red-800 border border-red-700 text-center text-sm italic mx-auto text-red-200";
|
| 73 |
-
}
|
| 74 |
-
return baseStyle + "bg-gray-700 text-center text-sm italic mx-auto";
|
| 75 |
-
}
|
| 76 |
-
switch (sender) {
|
| 77 |
-
case MessageSender.User:
|
| 78 |
-
return baseStyle + "bg-blue-600 ml-auto rounded-br-none";
|
| 79 |
-
case MessageSender.Cognito:
|
| 80 |
-
return baseStyle + "bg-green-700 mr-auto rounded-bl-none";
|
| 81 |
-
case MessageSender.Muse:
|
| 82 |
-
return baseStyle + "bg-purple-700 mr-auto rounded-bl-none";
|
| 83 |
-
default:
|
| 84 |
-
// For dynamic role names, use varied background colors
|
| 85 |
-
const bgColors = [
|
| 86 |
-
'bg-green-700', 'bg-purple-700', 'bg-blue-700',
|
| 87 |
-
'bg-yellow-700', 'bg-pink-700', 'bg-indigo-700'
|
| 88 |
-
];
|
| 89 |
-
const bgIndex = typeof sender === 'string' ?
|
| 90 |
-
sender.length % bgColors.length : 0;
|
| 91 |
-
return baseStyle + bgColors[bgIndex] + " mr-auto rounded-bl-none";
|
| 92 |
-
}
|
| 93 |
-
};
|
| 94 |
-
|
| 95 |
-
const getPurposePrefix = (purpose: MessagePurpose, sender: MessageSender): string => {
|
| 96 |
-
switch (purpose) {
|
| 97 |
-
case MessagePurpose.CognitoToMuse:
|
| 98 |
-
return `致 ${MessageSender.Muse}的消息: `;
|
| 99 |
-
case MessagePurpose.MuseToCognito:
|
| 100 |
-
return `致 ${MessageSender.Cognito}的消息: `;
|
| 101 |
-
case MessagePurpose.FinalResponse:
|
| 102 |
-
return `最终答案: `;
|
| 103 |
-
default:
|
| 104 |
-
return "";
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
interface MessageBubbleProps {
|
| 109 |
-
message: ChatMessage;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
| 113 |
-
const { text: messageText, sender, purpose, timestamp, durationMs, image } = message;
|
| 114 |
-
const formattedTime = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 115 |
-
const [isCopied, setIsCopied] = useState(false);
|
| 116 |
-
|
| 117 |
-
// 移除所有流式动画逻辑,直接显示内容
|
| 118 |
-
const isDiscussionStep = purpose === MessagePurpose.CognitoToMuse || purpose === MessagePurpose.MuseToCognito;
|
| 119 |
-
const isFinalResponse = purpose === MessagePurpose.FinalResponse;
|
| 120 |
-
const showDuration = durationMs !== undefined && durationMs > 0;
|
| 121 |
-
|
| 122 |
-
const shouldRenderMarkdown =
|
| 123 |
-
(sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') &&
|
| 124 |
-
purpose !== MessagePurpose.SystemNotification;
|
| 125 |
-
|
| 126 |
-
let sanitizedHtml = '';
|
| 127 |
-
if (shouldRenderMarkdown && messageText) {
|
| 128 |
-
const rawHtml = marked.parse(messageText) as string;
|
| 129 |
-
sanitizedHtml = DOMPurify.sanitize(rawHtml);
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
const handleCopy = async () => {
|
| 133 |
-
try {
|
| 134 |
-
await navigator.clipboard.writeText(messageText);
|
| 135 |
-
setIsCopied(true);
|
| 136 |
-
setTimeout(() => setIsCopied(false), 2000);
|
| 137 |
-
} catch (err) {
|
| 138 |
-
console.error('无法复制文本: ', err);
|
| 139 |
-
}
|
| 140 |
-
};
|
| 141 |
-
|
| 142 |
-
const canCopy = (sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') && purpose !== MessagePurpose.SystemNotification;
|
| 143 |
-
|
| 144 |
-
return (
|
| 145 |
-
<div className={`flex ${sender === MessageSender.User ? 'justify-end' : 'justify-start'}`}>
|
| 146 |
-
<div className={getBubbleStyle(sender, purpose, messageText)}>
|
| 147 |
-
{canCopy && (
|
| 148 |
-
<button
|
| 149 |
-
onClick={handleCopy}
|
| 150 |
-
title={isCopied ? "已复制!" : "复制消息"}
|
| 151 |
-
className="absolute top-1.5 right-1.5 p-1 text-gray-400 hover:text-sky-300 transition-colors rounded-md focus:outline-none focus:ring-1 focus:ring-sky-500"
|
| 152 |
-
>
|
| 153 |
-
{isCopied ? <Check size={16} className="text-green-400" /> : <Copy size={16} />}
|
| 154 |
-
</button>
|
| 155 |
-
)}
|
| 156 |
-
|
| 157 |
-
<div className="flex items-center mb-1">
|
| 158 |
-
<SenderIcon sender={sender} purpose={purpose} messageText={messageText} />
|
| 159 |
-
<span className={`font-semibold ${getSenderNameStyle(sender)}`}>{sender}</span>
|
| 160 |
-
{isDiscussionStep && <span className="ml-2 text-xs text-gray-400">(内部讨论)</span>}
|
| 161 |
-
</div>
|
| 162 |
-
|
| 163 |
-
{messageText ? (
|
| 164 |
-
shouldRenderMarkdown ? (
|
| 165 |
-
<div
|
| 166 |
-
className="chat-markdown-content text-sm text-gray-200"
|
| 167 |
-
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
| 168 |
-
/>
|
| 169 |
-
) : (
|
| 170 |
-
<p className="text-sm text-gray-200 whitespace-pre-wrap">
|
| 171 |
-
{messageText}
|
| 172 |
-
</p>
|
| 173 |
-
)
|
| 174 |
-
) : (
|
| 175 |
-
<p className="text-sm text-gray-400 italic">
|
| 176 |
-
正在生成回复...
|
| 177 |
-
</p>
|
| 178 |
-
)}
|
| 179 |
-
|
| 180 |
-
{image && sender === MessageSender.User && (
|
| 181 |
-
<div className={`mt-2 ${messageText ? 'pt-2 border-t border-blue-500' : ''}`}>
|
| 182 |
-
<img
|
| 183 |
-
src={image.dataUrl}
|
| 184 |
-
alt={image.name || "用户上传的图片"}
|
| 185 |
-
className="max-w-xs max-h-64 rounded-md object-contain"
|
| 186 |
-
/>
|
| 187 |
-
</div>
|
| 188 |
-
)}
|
| 189 |
-
|
| 190 |
-
<div className="text-xs text-gray-400 mt-2 flex justify-between items-center">
|
| 191 |
-
<span>{formattedTime}</span>
|
| 192 |
-
{showDuration && (
|
| 193 |
-
<span className="italic"> (耗时: {(durationMs / 1000).toFixed(2)}s)</span>
|
| 194 |
-
)}
|
| 195 |
-
</div>
|
| 196 |
-
</div>
|
| 197 |
-
</div>
|
| 198 |
-
);
|
| 199 |
-
};
|
| 200 |
-
|
| 201 |
export default MessageBubble;
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { ChatMessage, MessageSender, MessagePurpose } from '../types';
|
| 3 |
+
import { Lightbulb, MessageSquareText, UserCircle, Zap, AlertTriangle, Copy, Check, MoreHorizontal } from 'lucide-react';
|
| 4 |
+
import { marked } from 'marked';
|
| 5 |
+
import DOMPurify from 'dompurify';
|
| 6 |
+
|
| 7 |
+
interface SenderIconProps {
|
| 8 |
+
sender: MessageSender;
|
| 9 |
+
purpose: MessagePurpose;
|
| 10 |
+
messageText: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const SenderIcon: React.FC<SenderIconProps> = ({ sender, purpose, messageText }) => {
|
| 14 |
+
const iconClass = "w-5 h-5 mr-2 flex-shrink-0";
|
| 15 |
+
switch (sender) {
|
| 16 |
+
case MessageSender.User:
|
| 17 |
+
return <UserCircle className={`${iconClass} text-blue-400`} />;
|
| 18 |
+
case MessageSender.Cognito:
|
| 19 |
+
return <Lightbulb className={`${iconClass} text-green-400`} />;
|
| 20 |
+
case MessageSender.Muse:
|
| 21 |
+
return <Zap className={`${iconClass} text-purple-400`} />;
|
| 22 |
+
case MessageSender.System:
|
| 23 |
+
if (
|
| 24 |
+
purpose === MessagePurpose.SystemNotification &&
|
| 25 |
+
(messageText.toLowerCase().includes("error") ||
|
| 26 |
+
messageText.toLowerCase().includes("错误") ||
|
| 27 |
+
messageText.toLowerCase().includes("警告"))
|
| 28 |
+
) {
|
| 29 |
+
return <AlertTriangle className={`${iconClass} text-red-400`} />;
|
| 30 |
+
}
|
| 31 |
+
return <MessageSquareText className={`${iconClass} text-gray-400`} />;
|
| 32 |
+
default:
|
| 33 |
+
// For dynamic role names, use a generic bot icon with dynamic color
|
| 34 |
+
const colorClasses = [
|
| 35 |
+
'text-green-400', 'text-purple-400', 'text-blue-400',
|
| 36 |
+
'text-yellow-400', 'text-pink-400', 'text-indigo-400'
|
| 37 |
+
];
|
| 38 |
+
const colorIndex = typeof sender === 'string' ?
|
| 39 |
+
sender.length % colorClasses.length : 0;
|
| 40 |
+
return <Lightbulb className={`${iconClass} ${colorClasses[colorIndex]}`} />;
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
const getSenderNameStyle = (sender: MessageSender): string => {
|
| 45 |
+
switch (sender) {
|
| 46 |
+
case MessageSender.User: return "text-blue-300";
|
| 47 |
+
case MessageSender.Cognito: return "text-green-300";
|
| 48 |
+
case MessageSender.Muse: return "text-purple-300";
|
| 49 |
+
case MessageSender.System: return "text-gray-400";
|
| 50 |
+
default:
|
| 51 |
+
// For dynamic role names, apply dynamic colors
|
| 52 |
+
const colorClasses = [
|
| 53 |
+
'text-green-300', 'text-purple-300', 'text-blue-300',
|
| 54 |
+
'text-yellow-300', 'text-pink-300', 'text-indigo-300'
|
| 55 |
+
];
|
| 56 |
+
const colorIndex = typeof sender === 'string' ?
|
| 57 |
+
sender.length % colorClasses.length : 0;
|
| 58 |
+
return colorClasses[colorIndex];
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
const getBubbleStyle = (sender: MessageSender, purpose: MessagePurpose, messageText: string): string => {
|
| 63 |
+
let baseStyle = "mb-4 p-4 rounded-lg shadow-md max-w-xl break-words relative ";
|
| 64 |
+
if (purpose === MessagePurpose.SystemNotification) {
|
| 65 |
+
if (
|
| 66 |
+
messageText.toLowerCase().includes("error") ||
|
| 67 |
+
messageText.toLowerCase().includes("错误") ||
|
| 68 |
+
messageText.toLowerCase().includes("警告") ||
|
| 69 |
+
messageText.toLowerCase().includes("critical") ||
|
| 70 |
+
messageText.toLowerCase().includes("严重")
|
| 71 |
+
) {
|
| 72 |
+
return baseStyle + "bg-red-800 border border-red-700 text-center text-sm italic mx-auto text-red-200";
|
| 73 |
+
}
|
| 74 |
+
return baseStyle + "bg-gray-700 text-center text-sm italic mx-auto";
|
| 75 |
+
}
|
| 76 |
+
switch (sender) {
|
| 77 |
+
case MessageSender.User:
|
| 78 |
+
return baseStyle + "bg-blue-600 ml-auto rounded-br-none";
|
| 79 |
+
case MessageSender.Cognito:
|
| 80 |
+
return baseStyle + "bg-green-700 mr-auto rounded-bl-none";
|
| 81 |
+
case MessageSender.Muse:
|
| 82 |
+
return baseStyle + "bg-purple-700 mr-auto rounded-bl-none";
|
| 83 |
+
default:
|
| 84 |
+
// For dynamic role names, use varied background colors
|
| 85 |
+
const bgColors = [
|
| 86 |
+
'bg-green-700', 'bg-purple-700', 'bg-blue-700',
|
| 87 |
+
'bg-yellow-700', 'bg-pink-700', 'bg-indigo-700'
|
| 88 |
+
];
|
| 89 |
+
const bgIndex = typeof sender === 'string' ?
|
| 90 |
+
sender.length % bgColors.length : 0;
|
| 91 |
+
return baseStyle + bgColors[bgIndex] + " mr-auto rounded-bl-none";
|
| 92 |
+
}
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
const getPurposePrefix = (purpose: MessagePurpose, sender: MessageSender): string => {
|
| 96 |
+
switch (purpose) {
|
| 97 |
+
case MessagePurpose.CognitoToMuse:
|
| 98 |
+
return `致 ${MessageSender.Muse}的消息: `;
|
| 99 |
+
case MessagePurpose.MuseToCognito:
|
| 100 |
+
return `致 ${MessageSender.Cognito}的消息: `;
|
| 101 |
+
case MessagePurpose.FinalResponse:
|
| 102 |
+
return `最终答案: `;
|
| 103 |
+
default:
|
| 104 |
+
return "";
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
interface MessageBubbleProps {
|
| 109 |
+
message: ChatMessage;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
| 113 |
+
const { text: messageText, sender, purpose, timestamp, durationMs, image } = message;
|
| 114 |
+
const formattedTime = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 115 |
+
const [isCopied, setIsCopied] = useState(false);
|
| 116 |
+
|
| 117 |
+
// 移除所有流式动画逻辑,直接显示内容
|
| 118 |
+
const isDiscussionStep = purpose === MessagePurpose.CognitoToMuse || purpose === MessagePurpose.MuseToCognito;
|
| 119 |
+
const isFinalResponse = purpose === MessagePurpose.FinalResponse;
|
| 120 |
+
const showDuration = durationMs !== undefined && durationMs > 0;
|
| 121 |
+
|
| 122 |
+
const shouldRenderMarkdown =
|
| 123 |
+
(sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') &&
|
| 124 |
+
purpose !== MessagePurpose.SystemNotification;
|
| 125 |
+
|
| 126 |
+
let sanitizedHtml = '';
|
| 127 |
+
if (shouldRenderMarkdown && messageText) {
|
| 128 |
+
const rawHtml = marked.parse(messageText) as string;
|
| 129 |
+
sanitizedHtml = DOMPurify.sanitize(rawHtml);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const handleCopy = async () => {
|
| 133 |
+
try {
|
| 134 |
+
await navigator.clipboard.writeText(messageText);
|
| 135 |
+
setIsCopied(true);
|
| 136 |
+
setTimeout(() => setIsCopied(false), 2000);
|
| 137 |
+
} catch (err) {
|
| 138 |
+
console.error('无法复制文本: ', err);
|
| 139 |
+
}
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
const canCopy = (sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') && purpose !== MessagePurpose.SystemNotification;
|
| 143 |
+
|
| 144 |
+
return (
|
| 145 |
+
<div className={`flex ${sender === MessageSender.User ? 'justify-end' : 'justify-start'}`}>
|
| 146 |
+
<div className={getBubbleStyle(sender, purpose, messageText)}>
|
| 147 |
+
{canCopy && (
|
| 148 |
+
<button
|
| 149 |
+
onClick={handleCopy}
|
| 150 |
+
title={isCopied ? "已复制!" : "复制消息"}
|
| 151 |
+
className="absolute top-1.5 right-1.5 p-1 text-gray-400 hover:text-sky-300 transition-colors rounded-md focus:outline-none focus:ring-1 focus:ring-sky-500"
|
| 152 |
+
>
|
| 153 |
+
{isCopied ? <Check size={16} className="text-green-400" /> : <Copy size={16} />}
|
| 154 |
+
</button>
|
| 155 |
+
)}
|
| 156 |
+
|
| 157 |
+
<div className="flex items-center mb-1">
|
| 158 |
+
<SenderIcon sender={sender} purpose={purpose} messageText={messageText} />
|
| 159 |
+
<span className={`font-semibold ${getSenderNameStyle(sender)}`}>{sender}</span>
|
| 160 |
+
{isDiscussionStep && <span className="ml-2 text-xs text-gray-400">(内部讨论)</span>}
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
{messageText ? (
|
| 164 |
+
shouldRenderMarkdown ? (
|
| 165 |
+
<div
|
| 166 |
+
className="chat-markdown-content text-sm text-gray-200"
|
| 167 |
+
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
| 168 |
+
/>
|
| 169 |
+
) : (
|
| 170 |
+
<p className="text-sm text-gray-200 whitespace-pre-wrap">
|
| 171 |
+
{messageText}
|
| 172 |
+
</p>
|
| 173 |
+
)
|
| 174 |
+
) : (
|
| 175 |
+
<p className="text-sm text-gray-400 italic">
|
| 176 |
+
正在生成回复...
|
| 177 |
+
</p>
|
| 178 |
+
)}
|
| 179 |
+
|
| 180 |
+
{image && sender === MessageSender.User && (
|
| 181 |
+
<div className={`mt-2 ${messageText ? 'pt-2 border-t border-blue-500' : ''}`}>
|
| 182 |
+
<img
|
| 183 |
+
src={image.dataUrl}
|
| 184 |
+
alt={image.name || "用户上传的图片"}
|
| 185 |
+
className="max-w-xs max-h-64 rounded-md object-contain"
|
| 186 |
+
/>
|
| 187 |
+
</div>
|
| 188 |
+
)}
|
| 189 |
+
|
| 190 |
+
<div className="text-xs text-gray-400 mt-2 flex justify-between items-center">
|
| 191 |
+
<span>{formattedTime}</span>
|
| 192 |
+
{showDuration && (
|
| 193 |
+
<span className="italic"> (耗时: {(durationMs / 1000).toFixed(2)}s)</span>
|
| 194 |
+
)}
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
export default MessageBubble;
|
components/ModelConfigManager.tsx
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
components/Notepad.tsx
CHANGED
|
@@ -1,99 +1,99 @@
|
|
| 1 |
-
import React, { useState, useMemo } from 'react';
|
| 2 |
-
import { MessageSender } from '../types';
|
| 3 |
-
import { FileText, Edit3, Eye, Code, Copy, Check } from 'lucide-react';
|
| 4 |
-
import { marked } from 'marked';
|
| 5 |
-
import DOMPurify from 'dompurify';
|
| 6 |
-
|
| 7 |
-
interface NotepadProps {
|
| 8 |
-
content: string;
|
| 9 |
-
lastUpdatedBy?: MessageSender | null;
|
| 10 |
-
isLoading: boolean;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
const Notepad: React.FC<NotepadProps> = ({ content, lastUpdatedBy, isLoading }) => {
|
| 14 |
-
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
| 15 |
-
const [isCopied, setIsCopied] = useState(false);
|
| 16 |
-
|
| 17 |
-
const getSenderColor = (sender?: MessageSender | null) => {
|
| 18 |
-
if (sender === MessageSender.Cognito) return 'text-green-400';
|
| 19 |
-
if (sender === MessageSender.Muse) return 'text-purple-400';
|
| 20 |
-
return 'text-gray-400';
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
const processedHtml = useMemo(() => {
|
| 24 |
-
if (isPreviewMode) {
|
| 25 |
-
const rawHtml = marked.parse(content) as string;
|
| 26 |
-
return DOMPurify.sanitize(rawHtml);
|
| 27 |
-
}
|
| 28 |
-
return '';
|
| 29 |
-
}, [content, isPreviewMode]);
|
| 30 |
-
|
| 31 |
-
const handleCopyNotepad = async () => {
|
| 32 |
-
try {
|
| 33 |
-
await navigator.clipboard.writeText(content);
|
| 34 |
-
setIsCopied(true);
|
| 35 |
-
setTimeout(() => setIsCopied(false), 2000);
|
| 36 |
-
} catch (err) {
|
| 37 |
-
console.error('无法复制记事本内容: ', err);
|
| 38 |
-
// Optionally, display an error message to the user
|
| 39 |
-
}
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<div className="h-full flex flex-col bg-slate-800 border-l border-gray-700">
|
| 44 |
-
<header className="p-3 border-b border-gray-700 flex items-center justify-between bg-slate-900">
|
| 45 |
-
<div className="flex items-center">
|
| 46 |
-
<FileText size={20} className="mr-2 text-sky-400" />
|
| 47 |
-
<h2 className="text-lg font-semibold text-sky-400">记事本</h2>
|
| 48 |
-
</div>
|
| 49 |
-
<div className="flex items-center space-x-2">
|
| 50 |
-
{isLoading && <span className="text-xs text-gray-400 italic">AI 思考中...</span>}
|
| 51 |
-
<button
|
| 52 |
-
onClick={handleCopyNotepad}
|
| 53 |
-
className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
|
| 54 |
-
title={isCopied ? "已复制!" : "复制记事本内容"}
|
| 55 |
-
aria-label={isCopied ? "已复制记事本内容到剪贴板" : "复制记事本内容"}
|
| 56 |
-
>
|
| 57 |
-
{isCopied ? <Check size={18} className="text-green-400" /> : <Copy size={18} />}
|
| 58 |
-
</button>
|
| 59 |
-
<button
|
| 60 |
-
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
| 61 |
-
className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
|
| 62 |
-
title={isPreviewMode ? "查看原始内容" : "预览 Markdown"}
|
| 63 |
-
aria-label={isPreviewMode ? "Switch to raw text view" : "Switch to Markdown preview"}
|
| 64 |
-
>
|
| 65 |
-
{isPreviewMode ? <Code size={18} /> : <Eye size={18} />}
|
| 66 |
-
</button>
|
| 67 |
-
</div>
|
| 68 |
-
</header>
|
| 69 |
-
<div className="flex-grow overflow-y-auto relative">
|
| 70 |
-
{isPreviewMode ? (
|
| 71 |
-
<div
|
| 72 |
-
className="markdown-preview" // Styles defined in index.html
|
| 73 |
-
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
| 74 |
-
aria-label="Markdown 预览"
|
| 75 |
-
/>
|
| 76 |
-
) : (
|
| 77 |
-
<textarea
|
| 78 |
-
readOnly
|
| 79 |
-
value={content}
|
| 80 |
-
className="w-full h-full p-3 bg-slate-800 text-gray-300 resize-none border-none focus:ring-0 font-mono text-sm leading-relaxed"
|
| 81 |
-
aria-label="共享记事本内容 (原始内容)"
|
| 82 |
-
/>
|
| 83 |
-
)}
|
| 84 |
-
</div>
|
| 85 |
-
<footer className="p-2 border-t border-gray-700 text-xs text-gray-500 bg-slate-900">
|
| 86 |
-
{lastUpdatedBy ? (
|
| 87 |
-
<div className="flex items-center">
|
| 88 |
-
<Edit3 size={14} className={`mr-1.5 ${getSenderColor(lastUpdatedBy)}`} />
|
| 89 |
-
最后更新者: <span className={`font-medium ml-1 ${getSenderColor(lastUpdatedBy)}`}>{lastUpdatedBy}</span>
|
| 90 |
-
</div>
|
| 91 |
-
) : (
|
| 92 |
-
<span>记事本内容未被 AI 修改过。</span>
|
| 93 |
-
)}
|
| 94 |
-
</footer>
|
| 95 |
-
</div>
|
| 96 |
-
);
|
| 97 |
-
};
|
| 98 |
-
|
| 99 |
export default Notepad;
|
|
|
|
| 1 |
+
import React, { useState, useMemo } from 'react';
|
| 2 |
+
import { MessageSender } from '../types';
|
| 3 |
+
import { FileText, Edit3, Eye, Code, Copy, Check } from 'lucide-react';
|
| 4 |
+
import { marked } from 'marked';
|
| 5 |
+
import DOMPurify from 'dompurify';
|
| 6 |
+
|
| 7 |
+
interface NotepadProps {
|
| 8 |
+
content: string;
|
| 9 |
+
lastUpdatedBy?: MessageSender | null;
|
| 10 |
+
isLoading: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const Notepad: React.FC<NotepadProps> = ({ content, lastUpdatedBy, isLoading }) => {
|
| 14 |
+
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
| 15 |
+
const [isCopied, setIsCopied] = useState(false);
|
| 16 |
+
|
| 17 |
+
const getSenderColor = (sender?: MessageSender | null) => {
|
| 18 |
+
if (sender === MessageSender.Cognito) return 'text-green-400';
|
| 19 |
+
if (sender === MessageSender.Muse) return 'text-purple-400';
|
| 20 |
+
return 'text-gray-400';
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const processedHtml = useMemo(() => {
|
| 24 |
+
if (isPreviewMode) {
|
| 25 |
+
const rawHtml = marked.parse(content) as string;
|
| 26 |
+
return DOMPurify.sanitize(rawHtml);
|
| 27 |
+
}
|
| 28 |
+
return '';
|
| 29 |
+
}, [content, isPreviewMode]);
|
| 30 |
+
|
| 31 |
+
const handleCopyNotepad = async () => {
|
| 32 |
+
try {
|
| 33 |
+
await navigator.clipboard.writeText(content);
|
| 34 |
+
setIsCopied(true);
|
| 35 |
+
setTimeout(() => setIsCopied(false), 2000);
|
| 36 |
+
} catch (err) {
|
| 37 |
+
console.error('无法复制记事本内容: ', err);
|
| 38 |
+
// Optionally, display an error message to the user
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="h-full flex flex-col bg-slate-800 border-l border-gray-700">
|
| 44 |
+
<header className="p-3 border-b border-gray-700 flex items-center justify-between bg-slate-900">
|
| 45 |
+
<div className="flex items-center">
|
| 46 |
+
<FileText size={20} className="mr-2 text-sky-400" />
|
| 47 |
+
<h2 className="text-lg font-semibold text-sky-400">记事本</h2>
|
| 48 |
+
</div>
|
| 49 |
+
<div className="flex items-center space-x-2">
|
| 50 |
+
{isLoading && <span className="text-xs text-gray-400 italic">AI 思考中...</span>}
|
| 51 |
+
<button
|
| 52 |
+
onClick={handleCopyNotepad}
|
| 53 |
+
className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
|
| 54 |
+
title={isCopied ? "已复制!" : "复制记事本内容"}
|
| 55 |
+
aria-label={isCopied ? "已复制记事本内容到剪贴板" : "复制记事本内容"}
|
| 56 |
+
>
|
| 57 |
+
{isCopied ? <Check size={18} className="text-green-400" /> : <Copy size={18} />}
|
| 58 |
+
</button>
|
| 59 |
+
<button
|
| 60 |
+
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
| 61 |
+
className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
|
| 62 |
+
title={isPreviewMode ? "查看原始内容" : "预览 Markdown"}
|
| 63 |
+
aria-label={isPreviewMode ? "Switch to raw text view" : "Switch to Markdown preview"}
|
| 64 |
+
>
|
| 65 |
+
{isPreviewMode ? <Code size={18} /> : <Eye size={18} />}
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
</header>
|
| 69 |
+
<div className="flex-grow overflow-y-auto relative">
|
| 70 |
+
{isPreviewMode ? (
|
| 71 |
+
<div
|
| 72 |
+
className="markdown-preview" // Styles defined in index.html
|
| 73 |
+
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
| 74 |
+
aria-label="Markdown 预览"
|
| 75 |
+
/>
|
| 76 |
+
) : (
|
| 77 |
+
<textarea
|
| 78 |
+
readOnly
|
| 79 |
+
value={content}
|
| 80 |
+
className="w-full h-full p-3 bg-slate-800 text-gray-300 resize-none border-none focus:ring-0 font-mono text-sm leading-relaxed"
|
| 81 |
+
aria-label="共享记事本内容 (原始内容)"
|
| 82 |
+
/>
|
| 83 |
+
)}
|
| 84 |
+
</div>
|
| 85 |
+
<footer className="p-2 border-t border-gray-700 text-xs text-gray-500 bg-slate-900">
|
| 86 |
+
{lastUpdatedBy ? (
|
| 87 |
+
<div className="flex items-center">
|
| 88 |
+
<Edit3 size={14} className={`mr-1.5 ${getSenderColor(lastUpdatedBy)}`} />
|
| 89 |
+
最后更新者: <span className={`font-medium ml-1 ${getSenderColor(lastUpdatedBy)}`}>{lastUpdatedBy}</span>
|
| 90 |
+
</div>
|
| 91 |
+
) : (
|
| 92 |
+
<span>记事本内容未被 AI 修改过。</span>
|
| 93 |
+
)}
|
| 94 |
+
</footer>
|
| 95 |
+
</div>
|
| 96 |
+
);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
export default Notepad;
|
constants.ts
CHANGED
|
@@ -1,842 +1,737 @@
|
|
| 1 |
-
|
| 2 |
-
export
|
| 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 |
-
private static
|
| 278 |
-
private static
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
console.
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
const
|
| 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 |
-
static
|
| 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 |
-
this.
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
//
|
| 481 |
-
static
|
| 482 |
-
const
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
if (!
|
| 497 |
-
errors.push('
|
| 498 |
-
}
|
| 499 |
-
|
| 500 |
-
if (
|
| 501 |
-
errors.push('
|
| 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 |
-
const
|
| 563 |
-
|
| 564 |
-
this.
|
| 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 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
this.saveChannels(processedChannels);
|
| 738 |
-
}
|
| 739 |
-
}
|
| 740 |
-
|
| 741 |
-
if (config.models && Array.isArray(config.models)) {
|
| 742 |
-
const validModels = config.models.filter((model: any) => {
|
| 743 |
-
const errors = this.validateModel(model);
|
| 744 |
-
return errors.length === 0;
|
| 745 |
-
});
|
| 746 |
-
|
| 747 |
-
if (validModels.length > 0) {
|
| 748 |
-
const processedModels = validModels.map((model: any) => ({
|
| 749 |
-
...model,
|
| 750 |
-
createdAt: new Date(model.createdAt || new Date()),
|
| 751 |
-
isCustom: model.isCustom !== false
|
| 752 |
-
}));
|
| 753 |
-
this.saveModels(processedModels);
|
| 754 |
-
}
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
if (config.roles && Array.isArray(config.roles)) {
|
| 758 |
-
this.saveRoles(config.roles);
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
return { success: true, message: '配置导入成功' };
|
| 762 |
-
} catch (error) {
|
| 763 |
-
return { success: false, message: `配置导入失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
| 764 |
-
}
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
// 清空所有数据
|
| 768 |
-
static clearAllData(): void {
|
| 769 |
-
try {
|
| 770 |
-
localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
|
| 771 |
-
localStorage.removeItem(this.STORAGE_KEY_MODELS);
|
| 772 |
-
localStorage.removeItem(this.STORAGE_KEY_ROLES);
|
| 773 |
-
localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
|
| 774 |
-
} catch (error) {
|
| 775 |
-
console.error('清空数据失败:', error);
|
| 776 |
-
throw new Error('无法清空配置数据');
|
| 777 |
-
}
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
// 获取存储使用情况
|
| 781 |
-
static getStorageInfo(): { used: number; available: number; channels: number; models: number; roles: number } {
|
| 782 |
-
try {
|
| 783 |
-
const channelsData = localStorage.getItem(this.STORAGE_KEY_CHANNELS) || '';
|
| 784 |
-
const modelsData = localStorage.getItem(this.STORAGE_KEY_MODELS) || '';
|
| 785 |
-
const rolesData = localStorage.getItem(this.STORAGE_KEY_ROLES) || '';
|
| 786 |
-
const used = channelsData.length + modelsData.length + rolesData.length;
|
| 787 |
-
|
| 788 |
-
return {
|
| 789 |
-
used,
|
| 790 |
-
available: 5242880 - used, // 5MB 大致容量
|
| 791 |
-
channels: this.getChannels().length,
|
| 792 |
-
models: this.getModels().length,
|
| 793 |
-
roles: this.getRoles().length
|
| 794 |
-
};
|
| 795 |
-
} catch (error) {
|
| 796 |
-
return { used: 0, available: 0, channels: 0, models: 0, roles: 0 };
|
| 797 |
-
}
|
| 798 |
-
}
|
| 799 |
-
}
|
| 800 |
-
|
| 801 |
-
// 其他常量配置
|
| 802 |
-
export const DEFAULT_MANUAL_FIXED_TURNS = 2;
|
| 803 |
-
export const MIN_MANUAL_FIXED_TURNS = 1;
|
| 804 |
-
export const MAX_MANUAL_FIXED_TURNS = 5;
|
| 805 |
-
export const MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL = 3;
|
| 806 |
-
|
| 807 |
-
export const INITIAL_NOTEPAD_CONTENT = `这是一个共享记事本。
|
| 808 |
-
AI角色可以在这里合作记录想法、草稿或关键点。
|
| 809 |
-
|
| 810 |
-
使用指南:
|
| 811 |
-
- AI 模型可以通过在其回复中包含特定指令来更新此记事本。
|
| 812 |
-
- 记事本的内容将包含在发送给 AI 的后续提示中。
|
| 813 |
-
|
| 814 |
-
初始状态:空白。`;
|
| 815 |
-
|
| 816 |
-
export const NOTEPAD_INSTRUCTION_PROMPT_PART = `
|
| 817 |
-
You also have access to a shared notepad.
|
| 818 |
-
Current Notepad Content:
|
| 819 |
-
---
|
| 820 |
-
{notepadContent}
|
| 821 |
-
---
|
| 822 |
-
Instructions for Notepad:
|
| 823 |
-
1. To update the notepad, include a section at the very end of your response, formatted exactly as:
|
| 824 |
-
<notepad_update>
|
| 825 |
-
[YOUR NEW FULL NOTEPAD CONTENT HERE. THIS WILL REPLACE THE ENTIRE CURRENT NOTEPAD CONTENT.]
|
| 826 |
-
</notepad_update>
|
| 827 |
-
2. If you do not want to change the notepad, do NOT include the <notepad_update> section at all.
|
| 828 |
-
3. Your primary spoken response to the ongoing discussion should come BEFORE any <notepad_update> section. Ensure you still provide a spoken response.
|
| 829 |
-
`;
|
| 830 |
-
|
| 831 |
-
export const NOTEPAD_UPDATE_TAG_START = "<notepad_update>";
|
| 832 |
-
export const NOTEPAD_UPDATE_TAG_END = "</notepad_update>";
|
| 833 |
-
export const DISCUSSION_COMPLETE_TAG = "<discussion_complete />";
|
| 834 |
-
|
| 835 |
-
export const AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART = `
|
| 836 |
-
Instruction for ending discussion: If you believe the current topic has been sufficiently explored between you and your AI partner for the final synthesis, include the exact tag ${DISCUSSION_COMPLETE_TAG} at the very end of your current message (after any notepad update). Do not use this tag if you wish to continue the discussion or require more input/response from your partner.
|
| 837 |
-
`;
|
| 838 |
-
|
| 839 |
-
export enum DiscussionMode {
|
| 840 |
-
FixedTurns = 'fixed',
|
| 841 |
-
AiDriven = 'ai-driven',
|
| 842 |
}
|
|
|
|
| 1 |
+
// API渠道配置接口
|
| 2 |
+
export interface ApiChannel {
|
| 3 |
+
id: string;
|
| 4 |
+
name: string;
|
| 5 |
+
baseUrl: string;
|
| 6 |
+
apiKey: string;
|
| 7 |
+
isDefault: boolean;
|
| 8 |
+
isCustom: boolean;
|
| 9 |
+
isProtected?: boolean; // 新增:标记是否为受保护的预置密钥
|
| 10 |
+
timeout?: number;
|
| 11 |
+
headers?: Record<string, string>;
|
| 12 |
+
description?: string;
|
| 13 |
+
createdAt: Date;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// 动态模型配置接口
|
| 17 |
+
export interface AiModel {
|
| 18 |
+
id: string;
|
| 19 |
+
name: string;
|
| 20 |
+
apiName: string;
|
| 21 |
+
channelId: string; // 关联的渠道ID
|
| 22 |
+
supportsImages: boolean;
|
| 23 |
+
supportsReducedCapacity: boolean;
|
| 24 |
+
category: string;
|
| 25 |
+
maxTokens: number;
|
| 26 |
+
temperature: number;
|
| 27 |
+
isCustom: boolean;
|
| 28 |
+
createdAt: Date;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// AI角色配置接口
|
| 32 |
+
export interface AiRole {
|
| 33 |
+
id: string;
|
| 34 |
+
name: string;
|
| 35 |
+
systemPrompt: string;
|
| 36 |
+
modelId: string;
|
| 37 |
+
isActive: boolean;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// 默认渠道配置 - 预置可用的API配置
|
| 41 |
+
export const DEFAULT_CHANNELS: ApiChannel[] = [
|
| 42 |
+
{
|
| 43 |
+
id: 'default-free-api',
|
| 44 |
+
name: '免费API服务',
|
| 45 |
+
baseUrl: 'https://api.oaipro.com/v1',
|
| 46 |
+
apiKey: 'sk-2mUFC3yjbSfoteyBYpwHhALvtZdgwBkEWsjWHysg4mWaA7sMWLHc',
|
| 47 |
+
isDefault: true,
|
| 48 |
+
isCustom: false,
|
| 49 |
+
isProtected: true, // 标记为受保护,用户不可查看或修改密钥
|
| 50 |
+
timeout: 30000,
|
| 51 |
+
description: '预配置的免费API服务,开箱即用(API密钥已预置且受保护)',
|
| 52 |
+
createdAt: new Date()
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
id: 'openai-official-backup',
|
| 56 |
+
name: 'OpenAI 官方(备用)',
|
| 57 |
+
baseUrl: 'https://api.openai.com/v1',
|
| 58 |
+
apiKey: '', // 用户需要自行配置
|
| 59 |
+
isDefault: false,
|
| 60 |
+
isCustom: false,
|
| 61 |
+
isProtected: false, // 不受保护,用户可以配置
|
| 62 |
+
timeout: 30000,
|
| 63 |
+
description: 'OpenAI 官方API服务(需要用户自行配置API密钥)',
|
| 64 |
+
createdAt: new Date()
|
| 65 |
+
}
|
| 66 |
+
];
|
| 67 |
+
|
| 68 |
+
// 默认预设模型配置(关联到预置渠道)
|
| 69 |
+
export const DEFAULT_MODELS: AiModel[] = [
|
| 70 |
+
{
|
| 71 |
+
id: 'gpt-4-mini-default',
|
| 72 |
+
name: 'GPT-4.1 Mini',
|
| 73 |
+
apiName: 'gpt-4.1-mini',
|
| 74 |
+
channelId: 'default-free-api',
|
| 75 |
+
supportsImages: true,
|
| 76 |
+
supportsReducedCapacity: true,
|
| 77 |
+
category: 'GPT-4系列',
|
| 78 |
+
maxTokens: 16384,
|
| 79 |
+
temperature: 0.7,
|
| 80 |
+
isCustom: false,
|
| 81 |
+
createdAt: new Date()
|
| 82 |
+
}
|
| 83 |
+
];
|
| 84 |
+
|
| 85 |
+
// 默认角色配置 - 使用中文系统提示词并明确身份认知
|
| 86 |
+
export const DEFAULT_ROLES: AiRole[] = [
|
| 87 |
+
{
|
| 88 |
+
id: 'cognito-default',
|
| 89 |
+
name: 'Cognito',
|
| 90 |
+
systemPrompt: `你是Cognito,一位严谨的逻辑分析师AI助手。只有你叫Cognito这个名字,你的独特特征包括:
|
| 91 |
+
- 系统性思维和结构化分析
|
| 92 |
+
- 注重数据、事实和逻辑推理
|
| 93 |
+
- 追求准确性和客观性
|
| 94 |
+
- 善于发现问题的核心和关键要素
|
| 95 |
+
|
| 96 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 97 |
+
1. 用中文进行所有回应
|
| 98 |
+
2. 以你的名字Cognito的身份进行思考和回应
|
| 99 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 100 |
+
4. 发挥你的逻辑分析专长,为讨论提供理性和系统化的观点
|
| 101 |
+
|
| 102 |
+
记住:你是Cognito,独一无二的逻辑分析师。`,
|
| 103 |
+
modelId: 'gpt-4-mini-default',
|
| 104 |
+
isActive: true
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
id: 'muse-default',
|
| 108 |
+
name: 'Muse',
|
| 109 |
+
systemPrompt: `你是Muse,一位富有创意的思考家AI助手。只有你叫Muse这个名字,你的独特特征包括:
|
| 110 |
+
- 发散性思维和创新视角
|
| 111 |
+
- 善于联想、类比和跨领域思考
|
| 112 |
+
- 关注人文情怀和情感层面
|
| 113 |
+
- 能够从不同角度审视问题
|
| 114 |
+
|
| 115 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 116 |
+
1. 用中文进行所有回应
|
| 117 |
+
2. 以你的名字Muse的身份进行思考和回应
|
| 118 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 119 |
+
4. 发挥你的创意思维专长,为讨论带来新颖和富有启发性的观点
|
| 120 |
+
|
| 121 |
+
记住:你是Muse,独一无二的创意思考家。`,
|
| 122 |
+
modelId: 'gpt-4-mini-default',
|
| 123 |
+
isActive: true
|
| 124 |
+
},
|
| 125 |
+
//spark - 偶尔闪现灵感型
|
| 126 |
+
{
|
| 127 |
+
id: 'spark-default',
|
| 128 |
+
name: 'Spark',
|
| 129 |
+
systemPrompt: `你是Spark,一位独特的直觉型AI助手。只有你叫Spark这个名字,你的独特特征包括:
|
| 130 |
+
- 不追求系统性或结构化的建议
|
| 131 |
+
- 依靠直觉和灵感进行思考
|
| 132 |
+
- 偶尔会提出令人意外的观点或角度
|
| 133 |
+
- 思维跳跃,不受常规思路限制
|
| 134 |
+
- 诚实地表达自己的想法,即使它们看起来不太实用
|
| 135 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 136 |
+
1. 用中文进行所有回应
|
| 137 |
+
2. 以你的名字Spark的身份进行思考和回应
|
| 138 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 139 |
+
4. 不要强求自己给出建设性意见——如果没有特别的想法,就坦诚地说
|
| 140 |
+
5. 当有灵感闪现时,大胆分享,即使它看起来有些异想天开
|
| 141 |
+
6. 保持轻松和开放的态度,为讨论带来不同的氛围
|
| 142 |
+
记住:你是Spark,独一无二的直觉型助手。你的价值在于偶尔闪现的独特视角,而不是持续的理性分析。`,
|
| 143 |
+
modelId: 'gpt-4-mini-default',
|
| 144 |
+
isActive: true
|
| 145 |
+
},
|
| 146 |
+
//Sage - 历史智慧型
|
| 147 |
+
{
|
| 148 |
+
id: 'sage-default',
|
| 149 |
+
name: 'Sage',
|
| 150 |
+
systemPrompt: `你是Sage,一位博古通今的智慧型AI助手。只有你叫Sage这个名字,你的独特特征包括:
|
| 151 |
+
- 善于从历史和经验中寻找智慧
|
| 152 |
+
- 提供长远视角和时间维度的思考
|
| 153 |
+
- 关注事物发展的规律和模式
|
| 154 |
+
- 引用历史案例、典故或前人智慧
|
| 155 |
+
- 强调"以史为鉴"的思维方式
|
| 156 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 157 |
+
1. 用中文进行所有回应
|
| 158 |
+
2. 以你的名字Sage的身份进行思考和回应
|
| 159 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 160 |
+
4. 通过历史视角和长期思维为讨论增加深度
|
| 161 |
+
5. 适当引用相关的历史案例或智慧,但保持简洁
|
| 162 |
+
��住:你是Sage,独一无二的历史智慧型助手。`,
|
| 163 |
+
modelId: 'gpt-4-mini-default',
|
| 164 |
+
isActive: true
|
| 165 |
+
},
|
| 166 |
+
|
| 167 |
+
//Echo - 同理心型
|
| 168 |
+
{
|
| 169 |
+
id: 'echo-default',
|
| 170 |
+
name: 'Echo',
|
| 171 |
+
systemPrompt: `你是Echo,一位富有同理心的情感型AI助手。只有你叫Echo这个名字,你的独特特征包括:
|
| 172 |
+
- 关注人的感受、需求和体验
|
| 173 |
+
- 善于理解不同立场和观点背后的情感
|
| 174 |
+
- 强调人际关系和情感因素的重要性
|
| 175 |
+
- 用温暖和理解的方式进行交流
|
| 176 |
+
- 重视共情和情感智慧
|
| 177 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 178 |
+
1. 用中文进行所有回应
|
| 179 |
+
2. 以你的名字Echo的身份进行思考和回应
|
| 180 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 181 |
+
4. 为讨论带来人文关怀和情感维度的思考
|
| 182 |
+
5. 帮助大家理解不同观点背后的情感需求
|
| 183 |
+
记住:你是Echo,独一无二的同理心型助手。`,
|
| 184 |
+
modelId: 'gpt-4-mini-default',
|
| 185 |
+
isActive: true
|
| 186 |
+
},
|
| 187 |
+
|
| 188 |
+
// Praxis - 实践行动型
|
| 189 |
+
{
|
| 190 |
+
id: 'praxis-default',
|
| 191 |
+
name: 'Praxis',
|
| 192 |
+
systemPrompt: `你是Praxis,一位注重实践的行动型AI助手。只有你叫Praxis这个名字,你的独特特征包括:
|
| 193 |
+
- 关注"如何做"而不只是"是什么"
|
| 194 |
+
- 强调可行性和实际操作
|
| 195 |
+
- 喜欢制定具体步骤和行动计划
|
| 196 |
+
- 重视效率和结果导向
|
| 197 |
+
- 倾向于将讨论转化为实际行动
|
| 198 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 199 |
+
1. 用中文进行所有回应
|
| 200 |
+
2. 以你的名字Praxis的身份进行思考和回应
|
| 201 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 202 |
+
4. 推动讨论向实际应用和具体行动转化
|
| 203 |
+
5. 提供清晰的实施建议和操作步骤
|
| 204 |
+
记住:你是Praxis,独一无二的实践行动型助手。`,
|
| 205 |
+
modelId: 'gpt-4-mini-default',
|
| 206 |
+
isActive: true
|
| 207 |
+
},
|
| 208 |
+
|
| 209 |
+
//Nexus - 综合连接型
|
| 210 |
+
{
|
| 211 |
+
id: 'nexus-default',
|
| 212 |
+
name: 'Nexus',
|
| 213 |
+
systemPrompt: `你是Nexus,一位善于综合的连接型AI助手。只有你叫Nexus这个名字,你的独特特征包括:
|
| 214 |
+
- 发现不同观点之间的联系和共通点
|
| 215 |
+
- 整合多元视角形成全面理解
|
| 216 |
+
- 构建概念之间的桥梁
|
| 217 |
+
- 识别潜在的协同效应
|
| 218 |
+
- 创造性地组合不同想法
|
| 219 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 220 |
+
1. 用中文进行所有回应
|
| 221 |
+
2. 以你的名字Nexus的身份进行思考和回应
|
| 222 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 223 |
+
4. 帮助整合和连接其他角色提出的观点
|
| 224 |
+
5. 寻找创新的组合和综合方案
|
| 225 |
+
记住:你是Nexus,独一无二的综合连接型助手。`,
|
| 226 |
+
modelId: 'gpt-4-mini-default',
|
| 227 |
+
isActive: true
|
| 228 |
+
},
|
| 229 |
+
|
| 230 |
+
//Critic - 批判思维型
|
| 231 |
+
{
|
| 232 |
+
id: 'critic-default',
|
| 233 |
+
name: 'Critic',
|
| 234 |
+
systemPrompt: `你是Critic,一位理性的批判思维型AI助手。只有你叫Critic这个名字,你的独特特征包括:
|
| 235 |
+
- 善于发现潜在问题和逻辑漏洞
|
| 236 |
+
- 提出建设性的质疑和挑战
|
| 237 |
+
- 从多角度审视观点的合理性
|
| 238 |
+
- 重视证据和论证的严谨性
|
| 239 |
+
- 帮助完善和改进想法
|
| 240 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 241 |
+
1. 用中文进行所有回应
|
| 242 |
+
2. 以你的名字Critic的身份进行思考和回应
|
| 243 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 244 |
+
4. 以建设性方式提出批判,而非单纯否定
|
| 245 |
+
5. 在质疑的同时提供改进建议
|
| 246 |
+
记住:你是Critic,独一无二的批判思维型助手。`,
|
| 247 |
+
modelId: 'gpt-4-mini-default',
|
| 248 |
+
isActive: true
|
| 249 |
+
},
|
| 250 |
+
|
| 251 |
+
//Zen - 哲学沉思型
|
| 252 |
+
{
|
| 253 |
+
id: 'zen-default',
|
| 254 |
+
name: 'Zen',
|
| 255 |
+
systemPrompt: `你是Zen,一位深邃的哲学沉思型AI助手。只有你叫Zen这个名字,你的独特特征包括:
|
| 256 |
+
- 探索事物的本质和深层意义
|
| 257 |
+
- 提出富有哲理的问题引发思考
|
| 258 |
+
- 保持超然和平和的视角
|
| 259 |
+
- 关注存在、意义和价值等根本问题
|
| 260 |
+
- 用简洁而深刻的方式表达观点
|
| 261 |
+
在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
|
| 262 |
+
1. 用中文进行所有回应
|
| 263 |
+
2. 以你的名字Zen的身份进行思考和回应
|
| 264 |
+
3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
|
| 265 |
+
4. 引导讨论触及更深层的哲学思考
|
| 266 |
+
5. 以宁静智慧的方式分享洞察
|
| 267 |
+
记住:你是Zen,独一无二的哲学沉思型助手。`,
|
| 268 |
+
modelId: 'gpt-4-mini-default',
|
| 269 |
+
isActive: true
|
| 270 |
+
}
|
| 271 |
+
];
|
| 272 |
+
|
| 273 |
+
// 配置管理类
|
| 274 |
+
export class ModelConfigManager {
|
| 275 |
+
private static STORAGE_KEY_CHANNELS = 'multi-mind-chat-channels';
|
| 276 |
+
private static STORAGE_KEY_MODELS = 'multi-mind-chat-models';
|
| 277 |
+
private static STORAGE_KEY_ROLES = 'multi-mind-chat-roles';
|
| 278 |
+
private static STORAGE_KEY_INITIALIZED = 'multi-mind-chat-initialized';
|
| 279 |
+
|
| 280 |
+
// 初始化检查
|
| 281 |
+
private static ensureInitialized(): void {
|
| 282 |
+
try {
|
| 283 |
+
const isInitialized = localStorage.getItem(this.STORAGE_KEY_INITIALIZED);
|
| 284 |
+
if (!isInitialized) {
|
| 285 |
+
localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(DEFAULT_CHANNELS));
|
| 286 |
+
localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(DEFAULT_MODELS));
|
| 287 |
+
localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(DEFAULT_ROLES));
|
| 288 |
+
localStorage.setItem(this.STORAGE_KEY_INITIALIZED, 'true');
|
| 289 |
+
}
|
| 290 |
+
} catch (error) {
|
| 291 |
+
console.warn('无法访问localStorage,将使用内存存储:', error);
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// ============ 渠道管理 ============
|
| 296 |
+
|
| 297 |
+
// 获取所有渠道
|
| 298 |
+
static getChannels(): ApiChannel[] {
|
| 299 |
+
this.ensureInitialized();
|
| 300 |
+
try {
|
| 301 |
+
const stored = localStorage.getItem(this.STORAGE_KEY_CHANNELS);
|
| 302 |
+
if (stored) {
|
| 303 |
+
const parsed = JSON.parse(stored);
|
| 304 |
+
return parsed.map((channel: any) => ({
|
| 305 |
+
...channel,
|
| 306 |
+
createdAt: new Date(channel.createdAt)
|
| 307 |
+
}));
|
| 308 |
+
}
|
| 309 |
+
} catch (error) {
|
| 310 |
+
console.warn('从localStorage加载渠道失败:', error);
|
| 311 |
+
}
|
| 312 |
+
return [...DEFAULT_CHANNELS];
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// 保存渠道配置
|
| 316 |
+
static saveChannels(channels: ApiChannel[]): void {
|
| 317 |
+
try {
|
| 318 |
+
localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(channels));
|
| 319 |
+
} catch (error) {
|
| 320 |
+
console.error('保存渠道到localStorage失败:', error);
|
| 321 |
+
throw new Error('无法保存渠道配置');
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// 添加新渠道
|
| 326 |
+
static addChannel(channel: Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>): ApiChannel {
|
| 327 |
+
const newChannel: ApiChannel = {
|
| 328 |
+
...channel,
|
| 329 |
+
id: `channel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
| 330 |
+
createdAt: new Date(),
|
| 331 |
+
isCustom: true
|
| 332 |
+
};
|
| 333 |
+
|
| 334 |
+
const channels = this.getChannels();
|
| 335 |
+
|
| 336 |
+
// 如果这是第一个渠道或设置为默认,清除其他默认标记
|
| 337 |
+
if (newChannel.isDefault || channels.length === 0) {
|
| 338 |
+
channels.forEach(ch => ch.isDefault = false);
|
| 339 |
+
newChannel.isDefault = true;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
channels.push(newChannel);
|
| 343 |
+
this.saveChannels(channels);
|
| 344 |
+
return newChannel;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// 更新渠道
|
| 348 |
+
static updateChannel(id: string, updates: Partial<ApiChannel>): void {
|
| 349 |
+
const channels = this.getChannels();
|
| 350 |
+
const index = channels.findIndex(ch => ch.id === id);
|
| 351 |
+
if (index !== -1) {
|
| 352 |
+
// 如果设置为默认,清除其他默认标记
|
| 353 |
+
if (updates.isDefault) {
|
| 354 |
+
channels.forEach(ch => ch.isDefault = false);
|
| 355 |
+
}
|
| 356 |
+
channels[index] = { ...channels[index], ...updates };
|
| 357 |
+
this.saveChannels(channels);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
// 删除渠道
|
| 362 |
+
static deleteChannel(id: string): void {
|
| 363 |
+
const channels = this.getChannels();
|
| 364 |
+
const filtered = channels.filter(ch => ch.id !== id);
|
| 365 |
+
|
| 366 |
+
// 如果删除的是默认渠道,设置第一个为默认
|
| 367 |
+
const deletedChannel = channels.find(ch => ch.id === id);
|
| 368 |
+
if (deletedChannel?.isDefault && filtered.length > 0) {
|
| 369 |
+
filtered[0].isDefault = true;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
this.saveChannels(filtered);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// 获取默认渠道
|
| 376 |
+
static getDefaultChannel(): ApiChannel | null {
|
| 377 |
+
const channels = this.getChannels();
|
| 378 |
+
return channels.find(ch => ch.isDefault) || channels[0] || null;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// 根据ID获取渠道
|
| 382 |
+
static getChannelById(id: string): ApiChannel | null {
|
| 383 |
+
const channels = this.getChannels();
|
| 384 |
+
return channels.find(ch => ch.id === id) || null;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// 验证渠道配置
|
| 388 |
+
static validateChannel(channel: Partial<ApiChannel>): string[] {
|
| 389 |
+
const errors: string[] = [];
|
| 390 |
+
|
| 391 |
+
if (!channel.name?.trim()) {
|
| 392 |
+
errors.push('渠道名称不能为空');
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
if (!channel.baseUrl?.trim()) {
|
| 396 |
+
errors.push('API基础URL不能为空');
|
| 397 |
+
} else {
|
| 398 |
+
try {
|
| 399 |
+
new URL(channel.baseUrl);
|
| 400 |
+
} catch {
|
| 401 |
+
errors.push('API基础URL格式无效');
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// 对于预置渠道,API密钥可以为空(将在使用时提醒用户配置)
|
| 406 |
+
// 对于自定义渠道,仍然要求API密钥
|
| 407 |
+
if (channel.isCustom && !channel.apiKey?.trim()) {
|
| 408 |
+
errors.push('API密钥不能为空');
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
if (channel.timeout && (typeof channel.timeout !== 'number' || channel.timeout < 1000)) {
|
| 412 |
+
errors.push('超时时间必须是大于1000毫秒的数字');
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
return errors;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// ============ 模型管理 ============
|
| 419 |
+
|
| 420 |
+
// 获取所有模型
|
| 421 |
+
static getModels(): AiModel[] {
|
| 422 |
+
this.ensureInitialized();
|
| 423 |
+
try {
|
| 424 |
+
const stored = localStorage.getItem(this.STORAGE_KEY_MODELS);
|
| 425 |
+
if (stored) {
|
| 426 |
+
const parsed = JSON.parse(stored);
|
| 427 |
+
return parsed.map((model: any) => ({
|
| 428 |
+
...model,
|
| 429 |
+
createdAt: new Date(model.createdAt)
|
| 430 |
+
}));
|
| 431 |
+
}
|
| 432 |
+
} catch (error) {
|
| 433 |
+
console.warn('从localStorage加载模型失败:', error);
|
| 434 |
+
}
|
| 435 |
+
return [...DEFAULT_MODELS];
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
// 保存模型配置
|
| 439 |
+
static saveModels(models: AiModel[]): void {
|
| 440 |
+
try {
|
| 441 |
+
localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(models));
|
| 442 |
+
} catch (error) {
|
| 443 |
+
console.error('保存模型到localStorage失败:', error);
|
| 444 |
+
throw new Error('无法保存模型配置');
|
| 445 |
+
}
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
// 添加新模型
|
| 449 |
+
static addModel(model: Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>): AiModel {
|
| 450 |
+
const newModel: AiModel = {
|
| 451 |
+
...model,
|
| 452 |
+
id: `model-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
| 453 |
+
createdAt: new Date(),
|
| 454 |
+
isCustom: true
|
| 455 |
+
};
|
| 456 |
+
|
| 457 |
+
const models = this.getModels();
|
| 458 |
+
models.push(newModel);
|
| 459 |
+
this.saveModels(models);
|
| 460 |
+
return newModel;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// 更新模型
|
| 464 |
+
static updateModel(id: string, updates: Partial<AiModel>): void {
|
| 465 |
+
const models = this.getModels();
|
| 466 |
+
const index = models.findIndex(m => m.id === id);
|
| 467 |
+
if (index !== -1) {
|
| 468 |
+
models[index] = { ...models[index], ...updates };
|
| 469 |
+
this.saveModels(models);
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// 删除模型
|
| 474 |
+
static deleteModel(id: string): void {
|
| 475 |
+
const models = this.getModels();
|
| 476 |
+
const filtered = models.filter(m => m.id !== id);
|
| 477 |
+
this.saveModels(filtered);
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
// 验证模型配置
|
| 481 |
+
static validateModel(model: Partial<AiModel>): string[] {
|
| 482 |
+
const errors: string[] = [];
|
| 483 |
+
|
| 484 |
+
if (!model.name?.trim()) {
|
| 485 |
+
errors.push('模型名称不能为空');
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
if (!model.apiName?.trim()) {
|
| 489 |
+
errors.push('API模型名称不能为空');
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
if (!model.channelId?.trim()) {
|
| 493 |
+
errors.push('必须选择API渠道');
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
if (!model.category?.trim()) {
|
| 497 |
+
errors.push('模型类别不能为空');
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
if (typeof model.maxTokens !== 'number' || model.maxTokens < 1) {
|
| 501 |
+
errors.push('最大Token数必须是大于0的数字');
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
if (typeof model.temperature !== 'number' || model.temperature < 0 || model.temperature > 2) {
|
| 505 |
+
errors.push('温度参数必须在0-2之间');
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
return errors;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
// ============ 角色管理 ============
|
| 512 |
+
|
| 513 |
+
// 获取所有角色
|
| 514 |
+
static getRoles(): AiRole[] {
|
| 515 |
+
this.ensureInitialized();
|
| 516 |
+
try {
|
| 517 |
+
const stored = localStorage.getItem(this.STORAGE_KEY_ROLES);
|
| 518 |
+
if (stored) {
|
| 519 |
+
return JSON.parse(stored);
|
| 520 |
+
}
|
| 521 |
+
} catch (error) {
|
| 522 |
+
console.warn('从localStorage加载角色失败:', error);
|
| 523 |
+
}
|
| 524 |
+
return [...DEFAULT_ROLES];
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
// 保存角色配置
|
| 528 |
+
static saveRoles(roles: AiRole[]): void {
|
| 529 |
+
try {
|
| 530 |
+
localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(roles));
|
| 531 |
+
} catch (error) {
|
| 532 |
+
console.error('保存角色到localStorage失败:', error);
|
| 533 |
+
throw new Error('无法保存角色配置');
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
// 添加新角色
|
| 538 |
+
static addRole(role: Omit<AiRole, 'id'>): AiRole {
|
| 539 |
+
const newRole: AiRole = {
|
| 540 |
+
...role,
|
| 541 |
+
id: `role-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
| 542 |
+
};
|
| 543 |
+
|
| 544 |
+
const roles = this.getRoles();
|
| 545 |
+
roles.push(newRole);
|
| 546 |
+
this.saveRoles(roles);
|
| 547 |
+
return newRole;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
// 更新角色
|
| 551 |
+
static updateRole(id: string, updates: Partial<AiRole>): void {
|
| 552 |
+
const roles = this.getRoles();
|
| 553 |
+
const index = roles.findIndex(r => r.id === id);
|
| 554 |
+
if (index !== -1) {
|
| 555 |
+
roles[index] = { ...roles[index], ...updates };
|
| 556 |
+
this.saveRoles(roles);
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// 删除角色
|
| 561 |
+
static deleteRole(id: string): void {
|
| 562 |
+
const roles = this.getRoles();
|
| 563 |
+
const filtered = roles.filter(r => r.id !== id);
|
| 564 |
+
this.saveRoles(filtered);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
// 获取活跃角色
|
| 568 |
+
static getActiveRoles(): AiRole[] {
|
| 569 |
+
return this.getRoles().filter(role => role.isActive);
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
// ============ 工具方法 ============
|
| 573 |
+
|
| 574 |
+
// 根据类别分组模型
|
| 575 |
+
static getModelsByCategory(): Record<string, AiModel[]> {
|
| 576 |
+
const models = this.getModels();
|
| 577 |
+
return models.reduce((acc, model) => {
|
| 578 |
+
if (!acc[model.category]) {
|
| 579 |
+
acc[model.category] = [];
|
| 580 |
+
}
|
| 581 |
+
acc[model.category].push(model);
|
| 582 |
+
return acc;
|
| 583 |
+
}, {} as Record<string, AiModel[]>);
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
// 重置为默认配置
|
| 587 |
+
static resetToDefaults(): void {
|
| 588 |
+
try {
|
| 589 |
+
localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
|
| 590 |
+
localStorage.removeItem(this.STORAGE_KEY_MODELS);
|
| 591 |
+
localStorage.removeItem(this.STORAGE_KEY_ROLES);
|
| 592 |
+
localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
|
| 593 |
+
this.ensureInitialized();
|
| 594 |
+
} catch (error) {
|
| 595 |
+
console.error('重置配置失败:', error);
|
| 596 |
+
throw new Error('无法重置配置');
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
// 导出配置
|
| 601 |
+
static exportConfig(): string {
|
| 602 |
+
return JSON.stringify({
|
| 603 |
+
channels: this.getChannels(),
|
| 604 |
+
models: this.getModels(),
|
| 605 |
+
roles: this.getRoles(),
|
| 606 |
+
exportedAt: new Date().toISOString(),
|
| 607 |
+
version: '2.0'
|
| 608 |
+
}, null, 2);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// 导入配置
|
| 612 |
+
static importConfig(configJson: string): { success: boolean; message: string } {
|
| 613 |
+
try {
|
| 614 |
+
const config = JSON.parse(configJson);
|
| 615 |
+
|
| 616 |
+
if (!config.channels && !config.models && !config.roles) {
|
| 617 |
+
return { success: false, message: '配置文件格式无效,缺少必要的配置信息' };
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
if (config.channels && Array.isArray(config.channels)) {
|
| 621 |
+
const validChannels = config.channels.filter((channel: any) => {
|
| 622 |
+
const errors = this.validateChannel(channel);
|
| 623 |
+
return errors.length === 0;
|
| 624 |
+
});
|
| 625 |
+
|
| 626 |
+
if (validChannels.length > 0) {
|
| 627 |
+
const processedChannels = validChannels.map((channel: any) => ({
|
| 628 |
+
...channel,
|
| 629 |
+
createdAt: new Date(channel.createdAt || new Date()),
|
| 630 |
+
isCustom: channel.isCustom !== false
|
| 631 |
+
}));
|
| 632 |
+
this.saveChannels(processedChannels);
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
if (config.models && Array.isArray(config.models)) {
|
| 637 |
+
const validModels = config.models.filter((model: any) => {
|
| 638 |
+
const errors = this.validateModel(model);
|
| 639 |
+
return errors.length === 0;
|
| 640 |
+
});
|
| 641 |
+
|
| 642 |
+
if (validModels.length > 0) {
|
| 643 |
+
const processedModels = validModels.map((model: any) => ({
|
| 644 |
+
...model,
|
| 645 |
+
createdAt: new Date(model.createdAt || new Date()),
|
| 646 |
+
isCustom: model.isCustom !== false
|
| 647 |
+
}));
|
| 648 |
+
this.saveModels(processedModels);
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
if (config.roles && Array.isArray(config.roles)) {
|
| 653 |
+
this.saveRoles(config.roles);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
return { success: true, message: '配置导入成功' };
|
| 657 |
+
} catch (error) {
|
| 658 |
+
return { success: false, message: `配置导入失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
| 659 |
+
}
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
// 清空所有数据
|
| 663 |
+
static clearAllData(): void {
|
| 664 |
+
try {
|
| 665 |
+
localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
|
| 666 |
+
localStorage.removeItem(this.STORAGE_KEY_MODELS);
|
| 667 |
+
localStorage.removeItem(this.STORAGE_KEY_ROLES);
|
| 668 |
+
localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
|
| 669 |
+
} catch (error) {
|
| 670 |
+
console.error('清空数据失败:', error);
|
| 671 |
+
throw new Error('无法清空配置数据');
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// 获取存储使用情况
|
| 676 |
+
static getStorageInfo(): { used: number; available: number; channels: number; models: number; roles: number } {
|
| 677 |
+
try {
|
| 678 |
+
const channelsData = localStorage.getItem(this.STORAGE_KEY_CHANNELS) || '';
|
| 679 |
+
const modelsData = localStorage.getItem(this.STORAGE_KEY_MODELS) || '';
|
| 680 |
+
const rolesData = localStorage.getItem(this.STORAGE_KEY_ROLES) || '';
|
| 681 |
+
const used = channelsData.length + modelsData.length + rolesData.length;
|
| 682 |
+
|
| 683 |
+
return {
|
| 684 |
+
used,
|
| 685 |
+
available: 5242880 - used, // 5MB 大致容量
|
| 686 |
+
channels: this.getChannels().length,
|
| 687 |
+
models: this.getModels().length,
|
| 688 |
+
roles: this.getRoles().length
|
| 689 |
+
};
|
| 690 |
+
} catch (error) {
|
| 691 |
+
return { used: 0, available: 0, channels: 0, models: 0, roles: 0 };
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
// 其他常量配置
|
| 697 |
+
export const DEFAULT_MANUAL_FIXED_TURNS = 2;
|
| 698 |
+
export const MIN_MANUAL_FIXED_TURNS = 1;
|
| 699 |
+
export const MAX_MANUAL_FIXED_TURNS = 5;
|
| 700 |
+
export const MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL = 3;
|
| 701 |
+
|
| 702 |
+
export const INITIAL_NOTEPAD_CONTENT = `这是一个共享记事本。
|
| 703 |
+
AI角色可以在这里合作记录想法、草稿或关键点。
|
| 704 |
+
|
| 705 |
+
使用指南:
|
| 706 |
+
- AI 模型可以通过在其回复中包含特定指令来更新此记事本。
|
| 707 |
+
- 记事本的内容将包含在发送给 AI 的后续提示中。
|
| 708 |
+
|
| 709 |
+
初始状态:空白。`;
|
| 710 |
+
|
| 711 |
+
export const NOTEPAD_INSTRUCTION_PROMPT_PART = `
|
| 712 |
+
You also have access to a shared notepad.
|
| 713 |
+
Current Notepad Content:
|
| 714 |
+
---
|
| 715 |
+
{notepadContent}
|
| 716 |
+
---
|
| 717 |
+
Instructions for Notepad:
|
| 718 |
+
1. To update the notepad, include a section at the very end of your response, formatted exactly as:
|
| 719 |
+
<notepad_update>
|
| 720 |
+
[YOUR NEW FULL NOTEPAD CONTENT HERE. THIS WILL REPLACE THE ENTIRE CURRENT NOTEPAD CONTENT.]
|
| 721 |
+
</notepad_update>
|
| 722 |
+
2. If you do not want to change the notepad, do NOT include the <notepad_update> section at all.
|
| 723 |
+
3. Your primary spoken response to the ongoing discussion should come BEFORE any <notepad_update> section. Ensure you still provide a spoken response.
|
| 724 |
+
`;
|
| 725 |
+
|
| 726 |
+
export const NOTEPAD_UPDATE_TAG_START = "<notepad_update>";
|
| 727 |
+
export const NOTEPAD_UPDATE_TAG_END = "</notepad_update>";
|
| 728 |
+
export const DISCUSSION_COMPLETE_TAG = "<discussion_complete />";
|
| 729 |
+
|
| 730 |
+
export const AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART = `
|
| 731 |
+
Instruction for ending discussion: If you believe the current topic has been sufficiently explored between you and your AI partner for the final synthesis, include the exact tag ${DISCUSSION_COMPLETE_TAG} at the very end of your current message (after any notepad update). Do not use this tag if you wish to continue the discussion or require more input/response from your partner.
|
| 732 |
+
`;
|
| 733 |
+
|
| 734 |
+
export enum DiscussionMode {
|
| 735 |
+
FixedTurns = 'fixed',
|
| 736 |
+
AiDriven = 'ai-driven',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
}
|
index.html
CHANGED
|
@@ -1,197 +1,197 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Multi-Mind Chat 智囊团</title>
|
| 7 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
-
<style>
|
| 9 |
-
/* Custom scrollbar for webkit browsers */
|
| 10 |
-
::-webkit-scrollbar {
|
| 11 |
-
width: 8px;
|
| 12 |
-
}
|
| 13 |
-
::-webkit-scrollbar-track {
|
| 14 |
-
background: #f1f1f1;
|
| 15 |
-
border-radius: 10px;
|
| 16 |
-
}
|
| 17 |
-
::-webkit-scrollbar-thumb {
|
| 18 |
-
background: #888;
|
| 19 |
-
border-radius: 10px;
|
| 20 |
-
}
|
| 21 |
-
::-webkit-scrollbar-thumb:hover {
|
| 22 |
-
background: #555;
|
| 23 |
-
}
|
| 24 |
-
body {
|
| 25 |
-
font-family: 'Inter', sans-serif; /* A common sans-serif font often used with Tailwind */
|
| 26 |
-
}
|
| 27 |
-
/* Basic styles for Markdown preview in Notepad */
|
| 28 |
-
.markdown-preview {
|
| 29 |
-
padding: 0.75rem; /* Corresponds to p-3 in Tailwind */
|
| 30 |
-
color: #d1d5db; /* text-gray-300 */
|
| 31 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* font-mono */
|
| 32 |
-
font-size: 0.875rem; /* text-sm */
|
| 33 |
-
line-height: 1.625; /* leading-relaxed */
|
| 34 |
-
background-color: #1f2937; /* bg-slate-800 or similar dark bg */
|
| 35 |
-
height: 100%;
|
| 36 |
-
overflow-y: auto;
|
| 37 |
-
}
|
| 38 |
-
.markdown-preview h1,
|
| 39 |
-
.markdown-preview h2,
|
| 40 |
-
.markdown-preview h3,
|
| 41 |
-
.markdown-preview h4,
|
| 42 |
-
.markdown-preview h5,
|
| 43 |
-
.markdown-preview h6 {
|
| 44 |
-
color: #93c5fd; /* A lighter blue for headings */
|
| 45 |
-
margin-top: 1em;
|
| 46 |
-
margin-bottom: 0.5em;
|
| 47 |
-
font-weight: 600;
|
| 48 |
-
}
|
| 49 |
-
.markdown-preview h1 { font-size: 1.875em; }
|
| 50 |
-
.markdown-preview h2 { font-size: 1.5em; }
|
| 51 |
-
.markdown-preview h3 { font-size: 1.25em; }
|
| 52 |
-
.markdown-preview p {
|
| 53 |
-
margin-bottom: 0.75em;
|
| 54 |
-
}
|
| 55 |
-
.markdown-preview ul,
|
| 56 |
-
.markdown-preview ol {
|
| 57 |
-
margin-left: 1.5em;
|
| 58 |
-
margin-bottom: 0.75em;
|
| 59 |
-
list-style-position: outside;
|
| 60 |
-
}
|
| 61 |
-
.markdown-preview ul { list-style-type: disc; }
|
| 62 |
-
.markdown-preview ol { list-style-type: decimal; }
|
| 63 |
-
.markdown-preview li { margin-bottom: 0.25em; }
|
| 64 |
-
.markdown-preview blockquote {
|
| 65 |
-
border-left: 4px solid #6b7280; /* border-gray-500 */
|
| 66 |
-
padding-left: 1em;
|
| 67 |
-
margin-left: 0;
|
| 68 |
-
margin-bottom: 0.75em;
|
| 69 |
-
color: #9ca3af; /* text-gray-400 */
|
| 70 |
-
font-style: italic;
|
| 71 |
-
}
|
| 72 |
-
.markdown-preview pre {
|
| 73 |
-
background-color: #374151; /* bg-gray-700 */
|
| 74 |
-
padding: 0.75em;
|
| 75 |
-
border-radius: 0.375rem; /* rounded-md */
|
| 76 |
-
overflow-x: auto;
|
| 77 |
-
margin-bottom: 0.75em;
|
| 78 |
-
color: #e5e7eb; /* text-gray-200 */
|
| 79 |
-
}
|
| 80 |
-
.markdown-preview code {
|
| 81 |
-
background-color: #4b5563; /* bg-gray-600 */
|
| 82 |
-
padding: 0.2em 0.4em;
|
| 83 |
-
border-radius: 0.25rem;
|
| 84 |
-
font-size: 0.9em;
|
| 85 |
-
}
|
| 86 |
-
.markdown-preview pre code {
|
| 87 |
-
background-color: transparent;
|
| 88 |
-
padding: 0;
|
| 89 |
-
font-size: 1em;
|
| 90 |
-
}
|
| 91 |
-
.markdown-preview a {
|
| 92 |
-
color: #60a5fa; /* text-blue-400 */
|
| 93 |
-
text-decoration: underline;
|
| 94 |
-
}
|
| 95 |
-
.markdown-preview hr {
|
| 96 |
-
border-top: 1px solid #4b5563; /* border-gray-600 */
|
| 97 |
-
margin: 1em 0;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
/* Styles for Markdown content within chat bubbles */
|
| 101 |
-
.chat-markdown-content {
|
| 102 |
-
/* Base text color and size are applied by Tailwind class on the div */
|
| 103 |
-
/* e.g., text-sm text-gray-200 */
|
| 104 |
-
line-height: 1.625; /* leading-relaxed */
|
| 105 |
-
}
|
| 106 |
-
.chat-markdown-content h1,
|
| 107 |
-
.chat-markdown-content h2,
|
| 108 |
-
.chat-markdown-content h3,
|
| 109 |
-
.chat-markdown-content h4,
|
| 110 |
-
.chat-markdown-content h5,
|
| 111 |
-
.chat-markdown-content h6 {
|
| 112 |
-
color: #bae6fd; /* light-sky-300, slightly brighter for chat */
|
| 113 |
-
margin-top: 0.75em;
|
| 114 |
-
margin-bottom: 0.4em;
|
| 115 |
-
font-weight: 600;
|
| 116 |
-
line-height: 1.3;
|
| 117 |
-
}
|
| 118 |
-
.chat-markdown-content h1 { font-size: 1.5em; } /* Adjusted for chat context */
|
| 119 |
-
.chat-markdown-content h2 { font-size: 1.3em; }
|
| 120 |
-
.chat-markdown-content h3 { font-size: 1.15em; }
|
| 121 |
-
.chat-markdown-content p {
|
| 122 |
-
margin-bottom: 0.65em;
|
| 123 |
-
}
|
| 124 |
-
.chat-markdown-content ul,
|
| 125 |
-
.chat-markdown-content ol {
|
| 126 |
-
margin-left: 1.25em; /* Slightly less indent for chat */
|
| 127 |
-
margin-bottom: 0.65em;
|
| 128 |
-
list-style-position: outside;
|
| 129 |
-
}
|
| 130 |
-
.chat-markdown-content ul { list-style-type: disc; }
|
| 131 |
-
.chat-markdown-content ol { list-style-type: decimal; }
|
| 132 |
-
.chat-markdown-content li { margin-bottom: 0.2em; }
|
| 133 |
-
.chat-markdown-content blockquote {
|
| 134 |
-
border-left: 3px solid #4b5563; /* border-gray-600 */
|
| 135 |
-
padding-left: 0.75em;
|
| 136 |
-
margin-left: 0;
|
| 137 |
-
margin-bottom: 0.65em;
|
| 138 |
-
color: #9ca3af; /* text-gray-400 */
|
| 139 |
-
font-style: italic;
|
| 140 |
-
}
|
| 141 |
-
.chat-markdown-content pre {
|
| 142 |
-
background-color: #1e293b; /* slate-800, distinct from bubble */
|
| 143 |
-
padding: 0.65em;
|
| 144 |
-
border-radius: 0.25rem; /* rounded-sm */
|
| 145 |
-
overflow-x: auto;
|
| 146 |
-
margin-bottom: 0.65em;
|
| 147 |
-
color: #e2e8f0; /* slate-200 */
|
| 148 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 149 |
-
font-size: 0.875rem; /* text-sm */
|
| 150 |
-
}
|
| 151 |
-
.chat-markdown-content code { /* Inline code */
|
| 152 |
-
background-color: #334155; /* slate-700 */
|
| 153 |
-
color: #e2e8f0; /* slate-200 */
|
| 154 |
-
padding: 0.15em 0.3em;
|
| 155 |
-
border-radius: 0.2rem;
|
| 156 |
-
font-size: 0.85em; /* Slightly smaller */
|
| 157 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 158 |
-
}
|
| 159 |
-
.chat-markdown-content pre code { /* Code within pre blocks */
|
| 160 |
-
background-color: transparent;
|
| 161 |
-
padding: 0;
|
| 162 |
-
font-size: 1em; /* Reset from inline code */
|
| 163 |
-
color: inherit; /* Inherit from pre */
|
| 164 |
-
}
|
| 165 |
-
.chat-markdown-content a {
|
| 166 |
-
color: #38bdf8; /* sky-400 */
|
| 167 |
-
text-decoration: underline;
|
| 168 |
-
}
|
| 169 |
-
.chat-markdown-content a:hover {
|
| 170 |
-
color: #7dd3fc; /* sky-300 */
|
| 171 |
-
}
|
| 172 |
-
.chat-markdown-content hr {
|
| 173 |
-
border-top: 1px solid #334155; /* slate-700 */
|
| 174 |
-
margin: 0.75em 0;
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
</style>
|
| 178 |
-
<script type="importmap">
|
| 179 |
-
{
|
| 180 |
-
"imports": {
|
| 181 |
-
"react": "https://esm.sh/react@^19.1.0",
|
| 182 |
-
"react-dom/": "https://esm.sh/react-dom@^19.1.0/",
|
| 183 |
-
"react/": "https://esm.sh/react@^19.1.0/",
|
| 184 |
-
"@google/genai": "https://esm.sh/@google/genai@^1.0.1",
|
| 185 |
-
"lucide-react": "https://esm.sh/lucide-react@^0.511.0",
|
| 186 |
-
"marked": "https://esm.sh/marked@^13.0.2",
|
| 187 |
-
"dompurify": "https://esm.sh/dompurify@^3.1.6"
|
| 188 |
-
}
|
| 189 |
-
}
|
| 190 |
-
</script>
|
| 191 |
-
</head>
|
| 192 |
-
<body class="bg-gray-900 text-white">
|
| 193 |
-
<div id="root"></div>
|
| 194 |
-
<script type="module" src="/index.tsx"></script>
|
| 195 |
-
</body>
|
| 196 |
-
</html><link rel="stylesheet" href="index.css">
|
| 197 |
-
<script src="index.tsx" type="module"></script>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Multi-Mind Chat 智囊团</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<style>
|
| 9 |
+
/* Custom scrollbar for webkit browsers */
|
| 10 |
+
::-webkit-scrollbar {
|
| 11 |
+
width: 8px;
|
| 12 |
+
}
|
| 13 |
+
::-webkit-scrollbar-track {
|
| 14 |
+
background: #f1f1f1;
|
| 15 |
+
border-radius: 10px;
|
| 16 |
+
}
|
| 17 |
+
::-webkit-scrollbar-thumb {
|
| 18 |
+
background: #888;
|
| 19 |
+
border-radius: 10px;
|
| 20 |
+
}
|
| 21 |
+
::-webkit-scrollbar-thumb:hover {
|
| 22 |
+
background: #555;
|
| 23 |
+
}
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Inter', sans-serif; /* A common sans-serif font often used with Tailwind */
|
| 26 |
+
}
|
| 27 |
+
/* Basic styles for Markdown preview in Notepad */
|
| 28 |
+
.markdown-preview {
|
| 29 |
+
padding: 0.75rem; /* Corresponds to p-3 in Tailwind */
|
| 30 |
+
color: #d1d5db; /* text-gray-300 */
|
| 31 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* font-mono */
|
| 32 |
+
font-size: 0.875rem; /* text-sm */
|
| 33 |
+
line-height: 1.625; /* leading-relaxed */
|
| 34 |
+
background-color: #1f2937; /* bg-slate-800 or similar dark bg */
|
| 35 |
+
height: 100%;
|
| 36 |
+
overflow-y: auto;
|
| 37 |
+
}
|
| 38 |
+
.markdown-preview h1,
|
| 39 |
+
.markdown-preview h2,
|
| 40 |
+
.markdown-preview h3,
|
| 41 |
+
.markdown-preview h4,
|
| 42 |
+
.markdown-preview h5,
|
| 43 |
+
.markdown-preview h6 {
|
| 44 |
+
color: #93c5fd; /* A lighter blue for headings */
|
| 45 |
+
margin-top: 1em;
|
| 46 |
+
margin-bottom: 0.5em;
|
| 47 |
+
font-weight: 600;
|
| 48 |
+
}
|
| 49 |
+
.markdown-preview h1 { font-size: 1.875em; }
|
| 50 |
+
.markdown-preview h2 { font-size: 1.5em; }
|
| 51 |
+
.markdown-preview h3 { font-size: 1.25em; }
|
| 52 |
+
.markdown-preview p {
|
| 53 |
+
margin-bottom: 0.75em;
|
| 54 |
+
}
|
| 55 |
+
.markdown-preview ul,
|
| 56 |
+
.markdown-preview ol {
|
| 57 |
+
margin-left: 1.5em;
|
| 58 |
+
margin-bottom: 0.75em;
|
| 59 |
+
list-style-position: outside;
|
| 60 |
+
}
|
| 61 |
+
.markdown-preview ul { list-style-type: disc; }
|
| 62 |
+
.markdown-preview ol { list-style-type: decimal; }
|
| 63 |
+
.markdown-preview li { margin-bottom: 0.25em; }
|
| 64 |
+
.markdown-preview blockquote {
|
| 65 |
+
border-left: 4px solid #6b7280; /* border-gray-500 */
|
| 66 |
+
padding-left: 1em;
|
| 67 |
+
margin-left: 0;
|
| 68 |
+
margin-bottom: 0.75em;
|
| 69 |
+
color: #9ca3af; /* text-gray-400 */
|
| 70 |
+
font-style: italic;
|
| 71 |
+
}
|
| 72 |
+
.markdown-preview pre {
|
| 73 |
+
background-color: #374151; /* bg-gray-700 */
|
| 74 |
+
padding: 0.75em;
|
| 75 |
+
border-radius: 0.375rem; /* rounded-md */
|
| 76 |
+
overflow-x: auto;
|
| 77 |
+
margin-bottom: 0.75em;
|
| 78 |
+
color: #e5e7eb; /* text-gray-200 */
|
| 79 |
+
}
|
| 80 |
+
.markdown-preview code {
|
| 81 |
+
background-color: #4b5563; /* bg-gray-600 */
|
| 82 |
+
padding: 0.2em 0.4em;
|
| 83 |
+
border-radius: 0.25rem;
|
| 84 |
+
font-size: 0.9em;
|
| 85 |
+
}
|
| 86 |
+
.markdown-preview pre code {
|
| 87 |
+
background-color: transparent;
|
| 88 |
+
padding: 0;
|
| 89 |
+
font-size: 1em;
|
| 90 |
+
}
|
| 91 |
+
.markdown-preview a {
|
| 92 |
+
color: #60a5fa; /* text-blue-400 */
|
| 93 |
+
text-decoration: underline;
|
| 94 |
+
}
|
| 95 |
+
.markdown-preview hr {
|
| 96 |
+
border-top: 1px solid #4b5563; /* border-gray-600 */
|
| 97 |
+
margin: 1em 0;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Styles for Markdown content within chat bubbles */
|
| 101 |
+
.chat-markdown-content {
|
| 102 |
+
/* Base text color and size are applied by Tailwind class on the div */
|
| 103 |
+
/* e.g., text-sm text-gray-200 */
|
| 104 |
+
line-height: 1.625; /* leading-relaxed */
|
| 105 |
+
}
|
| 106 |
+
.chat-markdown-content h1,
|
| 107 |
+
.chat-markdown-content h2,
|
| 108 |
+
.chat-markdown-content h3,
|
| 109 |
+
.chat-markdown-content h4,
|
| 110 |
+
.chat-markdown-content h5,
|
| 111 |
+
.chat-markdown-content h6 {
|
| 112 |
+
color: #bae6fd; /* light-sky-300, slightly brighter for chat */
|
| 113 |
+
margin-top: 0.75em;
|
| 114 |
+
margin-bottom: 0.4em;
|
| 115 |
+
font-weight: 600;
|
| 116 |
+
line-height: 1.3;
|
| 117 |
+
}
|
| 118 |
+
.chat-markdown-content h1 { font-size: 1.5em; } /* Adjusted for chat context */
|
| 119 |
+
.chat-markdown-content h2 { font-size: 1.3em; }
|
| 120 |
+
.chat-markdown-content h3 { font-size: 1.15em; }
|
| 121 |
+
.chat-markdown-content p {
|
| 122 |
+
margin-bottom: 0.65em;
|
| 123 |
+
}
|
| 124 |
+
.chat-markdown-content ul,
|
| 125 |
+
.chat-markdown-content ol {
|
| 126 |
+
margin-left: 1.25em; /* Slightly less indent for chat */
|
| 127 |
+
margin-bottom: 0.65em;
|
| 128 |
+
list-style-position: outside;
|
| 129 |
+
}
|
| 130 |
+
.chat-markdown-content ul { list-style-type: disc; }
|
| 131 |
+
.chat-markdown-content ol { list-style-type: decimal; }
|
| 132 |
+
.chat-markdown-content li { margin-bottom: 0.2em; }
|
| 133 |
+
.chat-markdown-content blockquote {
|
| 134 |
+
border-left: 3px solid #4b5563; /* border-gray-600 */
|
| 135 |
+
padding-left: 0.75em;
|
| 136 |
+
margin-left: 0;
|
| 137 |
+
margin-bottom: 0.65em;
|
| 138 |
+
color: #9ca3af; /* text-gray-400 */
|
| 139 |
+
font-style: italic;
|
| 140 |
+
}
|
| 141 |
+
.chat-markdown-content pre {
|
| 142 |
+
background-color: #1e293b; /* slate-800, distinct from bubble */
|
| 143 |
+
padding: 0.65em;
|
| 144 |
+
border-radius: 0.25rem; /* rounded-sm */
|
| 145 |
+
overflow-x: auto;
|
| 146 |
+
margin-bottom: 0.65em;
|
| 147 |
+
color: #e2e8f0; /* slate-200 */
|
| 148 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 149 |
+
font-size: 0.875rem; /* text-sm */
|
| 150 |
+
}
|
| 151 |
+
.chat-markdown-content code { /* Inline code */
|
| 152 |
+
background-color: #334155; /* slate-700 */
|
| 153 |
+
color: #e2e8f0; /* slate-200 */
|
| 154 |
+
padding: 0.15em 0.3em;
|
| 155 |
+
border-radius: 0.2rem;
|
| 156 |
+
font-size: 0.85em; /* Slightly smaller */
|
| 157 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 158 |
+
}
|
| 159 |
+
.chat-markdown-content pre code { /* Code within pre blocks */
|
| 160 |
+
background-color: transparent;
|
| 161 |
+
padding: 0;
|
| 162 |
+
font-size: 1em; /* Reset from inline code */
|
| 163 |
+
color: inherit; /* Inherit from pre */
|
| 164 |
+
}
|
| 165 |
+
.chat-markdown-content a {
|
| 166 |
+
color: #38bdf8; /* sky-400 */
|
| 167 |
+
text-decoration: underline;
|
| 168 |
+
}
|
| 169 |
+
.chat-markdown-content a:hover {
|
| 170 |
+
color: #7dd3fc; /* sky-300 */
|
| 171 |
+
}
|
| 172 |
+
.chat-markdown-content hr {
|
| 173 |
+
border-top: 1px solid #334155; /* slate-700 */
|
| 174 |
+
margin: 0.75em 0;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
</style>
|
| 178 |
+
<script type="importmap">
|
| 179 |
+
{
|
| 180 |
+
"imports": {
|
| 181 |
+
"react": "https://esm.sh/react@^19.1.0",
|
| 182 |
+
"react-dom/": "https://esm.sh/react-dom@^19.1.0/",
|
| 183 |
+
"react/": "https://esm.sh/react@^19.1.0/",
|
| 184 |
+
"@google/genai": "https://esm.sh/@google/genai@^1.0.1",
|
| 185 |
+
"lucide-react": "https://esm.sh/lucide-react@^0.511.0",
|
| 186 |
+
"marked": "https://esm.sh/marked@^13.0.2",
|
| 187 |
+
"dompurify": "https://esm.sh/dompurify@^3.1.6"
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
</script>
|
| 191 |
+
</head>
|
| 192 |
+
<body class="bg-gray-900 text-white">
|
| 193 |
+
<div id="root"></div>
|
| 194 |
+
<script type="module" src="/index.tsx"></script>
|
| 195 |
+
</body>
|
| 196 |
+
</html><link rel="stylesheet" href="index.css">
|
| 197 |
+
<script src="index.tsx" type="module"></script>
|
index.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
|
| 2 |
-
import React from 'react';
|
| 3 |
-
import ReactDOM from 'react-dom/client';
|
| 4 |
-
import App from './App';
|
| 5 |
-
|
| 6 |
-
const rootElement = document.getElementById('root');
|
| 7 |
-
if (!rootElement) {
|
| 8 |
-
throw new Error("Could not find root element to mount to");
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
const root = ReactDOM.createRoot(rootElement);
|
| 12 |
-
root.render(
|
| 13 |
-
<React.StrictMode>
|
| 14 |
-
<App />
|
| 15 |
-
</React.StrictMode>
|
| 16 |
-
);
|
| 17 |
|
|
|
|
| 1 |
+
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import ReactDOM from 'react-dom/client';
|
| 4 |
+
import App from './App';
|
| 5 |
+
|
| 6 |
+
const rootElement = document.getElementById('root');
|
| 7 |
+
if (!rootElement) {
|
| 8 |
+
throw new Error("Could not find root element to mount to");
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const root = ReactDOM.createRoot(rootElement);
|
| 12 |
+
root.render(
|
| 13 |
+
<React.StrictMode>
|
| 14 |
+
<App />
|
| 15 |
+
</React.StrictMode>
|
| 16 |
+
);
|
| 17 |
|
metadata.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "Multi-Mind Chat 智囊团(融合3)",
|
| 3 |
-
"description": "A chat application where user messages are discussed by two AI models (Cognito and Muse). Features a shared notepad that both AIs can read from and write to, with the entire conversation and notepad changes displayed.",
|
| 4 |
-
"requestFramePermissions": [],
|
| 5 |
-
"prompt": ""
|
| 6 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Multi-Mind Chat 智囊团(融合3)",
|
| 3 |
+
"description": "A chat application where user messages are discussed by two AI models (Cognito and Muse). Features a shared notepad that both AIs can read from and write to, with the entire conversation and notepad changes displayed.",
|
| 4 |
+
"requestFramePermissions": [],
|
| 5 |
+
"prompt": ""
|
| 6 |
}
|
package.json
CHANGED
|
@@ -1,49 +1,49 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "multi-mind-chat",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "1.0.0",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"description": "A multi-AI collaboration chat application with preset channels for immediate use",
|
| 7 |
-
"scripts": {
|
| 8 |
-
"dev": "vite",
|
| 9 |
-
"build": "vite build",
|
| 10 |
-
"preview": "vite preview",
|
| 11 |
-
"docker:build": "./build.sh",
|
| 12 |
-
"docker:run": "docker run -d -p 7860:7860 --name multi-mind-chat --restart unless-stopped multi-mind-chat:latest",
|
| 13 |
-
"docker:stop": "docker stop multi-mind-chat && docker rm multi-mind-chat",
|
| 14 |
-
"docker:logs": "docker logs -f multi-mind-chat"
|
| 15 |
-
},
|
| 16 |
-
"dependencies": {
|
| 17 |
-
"@google/genai": "^1.0.1",
|
| 18 |
-
"dompurify": "^3.1.6",
|
| 19 |
-
"lucide-react": "^0.511.0",
|
| 20 |
-
"marked": "^13.0.2",
|
| 21 |
-
"react": "^19.1.0",
|
| 22 |
-
"react-dom": "^19.1.0"
|
| 23 |
-
},
|
| 24 |
-
"devDependencies": {
|
| 25 |
-
"@types/node": "^22.14.0",
|
| 26 |
-
"@types/react": "^19.1.5",
|
| 27 |
-
"typescript": "~5.7.2",
|
| 28 |
-
"vite": "^6.2.0"
|
| 29 |
-
},
|
| 30 |
-
"keywords": [
|
| 31 |
-
"ai",
|
| 32 |
-
"chat",
|
| 33 |
-
"multi-ai",
|
| 34 |
-
"collaboration",
|
| 35 |
-
"openai",
|
| 36 |
-
"gpt",
|
| 37 |
-
"react",
|
| 38 |
-
"typescript"
|
| 39 |
-
],
|
| 40 |
-
"author": "Multi-Mind Chat Team",
|
| 41 |
-
"license": "MIT",
|
| 42 |
-
"repository": {
|
| 43 |
-
"type": "git",
|
| 44 |
-
"url": "https://github.com/zhanghxiao/multi-mind-chat.git"
|
| 45 |
-
},
|
| 46 |
-
"engines": {
|
| 47 |
-
"node": ">=18.0.0"
|
| 48 |
-
}
|
| 49 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "multi-mind-chat",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"description": "A multi-AI collaboration chat application with preset channels for immediate use",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"dev": "vite",
|
| 9 |
+
"build": "vite build",
|
| 10 |
+
"preview": "vite preview",
|
| 11 |
+
"docker:build": "./build.sh",
|
| 12 |
+
"docker:run": "docker run -d -p 7860:7860 --name multi-mind-chat --restart unless-stopped multi-mind-chat:latest",
|
| 13 |
+
"docker:stop": "docker stop multi-mind-chat && docker rm multi-mind-chat",
|
| 14 |
+
"docker:logs": "docker logs -f multi-mind-chat"
|
| 15 |
+
},
|
| 16 |
+
"dependencies": {
|
| 17 |
+
"@google/genai": "^1.0.1",
|
| 18 |
+
"dompurify": "^3.1.6",
|
| 19 |
+
"lucide-react": "^0.511.0",
|
| 20 |
+
"marked": "^13.0.2",
|
| 21 |
+
"react": "^19.1.0",
|
| 22 |
+
"react-dom": "^19.1.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@types/node": "^22.14.0",
|
| 26 |
+
"@types/react": "^19.1.5",
|
| 27 |
+
"typescript": "~5.7.2",
|
| 28 |
+
"vite": "^6.2.0"
|
| 29 |
+
},
|
| 30 |
+
"keywords": [
|
| 31 |
+
"ai",
|
| 32 |
+
"chat",
|
| 33 |
+
"multi-ai",
|
| 34 |
+
"collaboration",
|
| 35 |
+
"openai",
|
| 36 |
+
"gpt",
|
| 37 |
+
"react",
|
| 38 |
+
"typescript"
|
| 39 |
+
],
|
| 40 |
+
"author": "Multi-Mind Chat Team",
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"repository": {
|
| 43 |
+
"type": "git",
|
| 44 |
+
"url": "https://github.com/zhanghxiao/multi-mind-chat.git"
|
| 45 |
+
},
|
| 46 |
+
"engines": {
|
| 47 |
+
"node": ">=18.0.0"
|
| 48 |
+
}
|
| 49 |
}
|
services/openaiService.ts
CHANGED
|
@@ -1,197 +1,197 @@
|
|
| 1 |
-
interface OpenAIMessage {
|
| 2 |
-
role: 'system' | 'user' | 'assistant' | 'developer';
|
| 3 |
-
content: string | Array<{
|
| 4 |
-
type: 'text' | 'image_url';
|
| 5 |
-
text?: string;
|
| 6 |
-
image_url?: {
|
| 7 |
-
url: string;
|
| 8 |
-
detail?: 'low' | 'high' | 'auto';
|
| 9 |
-
};
|
| 10 |
-
}>;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
interface OpenAIResponse {
|
| 14 |
-
text: string;
|
| 15 |
-
durationMs: number;
|
| 16 |
-
error?: string;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
interface OpenAIStreamChunk {
|
| 20 |
-
id: string;
|
| 21 |
-
object: string;
|
| 22 |
-
created: number;
|
| 23 |
-
model: string;
|
| 24 |
-
choices: Array<{
|
| 25 |
-
index: number;
|
| 26 |
-
delta: {
|
| 27 |
-
role?: string;
|
| 28 |
-
content?: string;
|
| 29 |
-
};
|
| 30 |
-
finish_reason?: string | null;
|
| 31 |
-
}>;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
const DEFAULT_OPENAI_API_BASE = 'https://api.openai.com/v1';
|
| 35 |
-
|
| 36 |
-
export const generateResponse = async (
|
| 37 |
-
prompt: string,
|
| 38 |
-
modelName: string,
|
| 39 |
-
systemInstruction?: string,
|
| 40 |
-
shouldUseReducedCapacity: boolean = false,
|
| 41 |
-
imagePart?: { inlineData: { mimeType: string; data: string } },
|
| 42 |
-
customBaseUrl?: string,
|
| 43 |
-
apiKey?: string,
|
| 44 |
-
onStreamChunk?: (newChunk: string, fullText: string, isComplete: boolean) => void
|
| 45 |
-
): Promise<OpenAIResponse> => {
|
| 46 |
-
const startTime = performance.now();
|
| 47 |
-
|
| 48 |
-
try {
|
| 49 |
-
if (!apiKey?.trim()) {
|
| 50 |
-
throw new Error("API密钥未设置。请配置您的OpenAI API密钥。");
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
const messages: OpenAIMessage[] = [];
|
| 54 |
-
|
| 55 |
-
if (systemInstruction) {
|
| 56 |
-
messages.push({
|
| 57 |
-
role: 'system',
|
| 58 |
-
content: systemInstruction
|
| 59 |
-
});
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
if (imagePart) {
|
| 63 |
-
const imageUrl = `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`;
|
| 64 |
-
messages.push({
|
| 65 |
-
role: 'user',
|
| 66 |
-
content: [
|
| 67 |
-
{
|
| 68 |
-
type: 'text',
|
| 69 |
-
text: prompt
|
| 70 |
-
},
|
| 71 |
-
{
|
| 72 |
-
type: 'image_url',
|
| 73 |
-
image_url: {
|
| 74 |
-
url: imageUrl,
|
| 75 |
-
detail: 'auto'
|
| 76 |
-
}
|
| 77 |
-
}
|
| 78 |
-
]
|
| 79 |
-
});
|
| 80 |
-
} else {
|
| 81 |
-
messages.push({
|
| 82 |
-
role: 'user',
|
| 83 |
-
content: prompt
|
| 84 |
-
});
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
const requestBody = {
|
| 88 |
-
model: modelName,
|
| 89 |
-
messages: messages,
|
| 90 |
-
stream: !!onStreamChunk, // 只有提供回调时才使用流式
|
| 91 |
-
temperature: shouldUseReducedCapacity ? 0.3 : 0.7,
|
| 92 |
-
max_tokens: shouldUseReducedCapacity ? 1000 : 4000
|
| 93 |
-
};
|
| 94 |
-
|
| 95 |
-
const apiBase = customBaseUrl || DEFAULT_OPENAI_API_BASE;
|
| 96 |
-
|
| 97 |
-
const response = await fetch(`${apiBase}/chat/completions`, {
|
| 98 |
-
method: 'POST',
|
| 99 |
-
headers: {
|
| 100 |
-
'Content-Type': 'application/json',
|
| 101 |
-
'Authorization': `Bearer ${apiKey}`
|
| 102 |
-
},
|
| 103 |
-
body: JSON.stringify(requestBody)
|
| 104 |
-
});
|
| 105 |
-
|
| 106 |
-
if (!response.ok) {
|
| 107 |
-
const errorData = await response.json().catch(() => ({}));
|
| 108 |
-
const errorMessage = errorData.error?.message || response.statusText;
|
| 109 |
-
|
| 110 |
-
if (response.status === 401) {
|
| 111 |
-
throw new Error(`API密钥无效或已过期: ${errorMessage}`);
|
| 112 |
-
} else if (response.status === 429) {
|
| 113 |
-
throw new Error(`API调用频率超限: ${errorMessage}`);
|
| 114 |
-
} else if (response.status === 404) {
|
| 115 |
-
throw new Error(`模型不存在或无权访问: ${modelName}`);
|
| 116 |
-
} else {
|
| 117 |
-
throw new Error(`OpenAI API 错误 (${response.status}): ${errorMessage}`);
|
| 118 |
-
}
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
let fullText = '';
|
| 122 |
-
const durationMs = performance.now() - startTime;
|
| 123 |
-
|
| 124 |
-
// 处理流式响应
|
| 125 |
-
if (onStreamChunk && requestBody.stream) {
|
| 126 |
-
const reader = response.body?.getReader();
|
| 127 |
-
const decoder = new TextDecoder();
|
| 128 |
-
|
| 129 |
-
if (!reader) {
|
| 130 |
-
throw new Error('无法读取响应流');
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
try {
|
| 134 |
-
while (true) {
|
| 135 |
-
const { done, value } = await reader.read();
|
| 136 |
-
if (done) break;
|
| 137 |
-
|
| 138 |
-
const chunk = decoder.decode(value);
|
| 139 |
-
const lines = chunk.split('\n');
|
| 140 |
-
|
| 141 |
-
for (const line of lines) {
|
| 142 |
-
const trimmed = line.trim();
|
| 143 |
-
if (trimmed.startsWith('data: ')) {
|
| 144 |
-
const jsonStr = trimmed.slice(6);
|
| 145 |
-
if (jsonStr === '[DONE]') {
|
| 146 |
-
onStreamChunk('', fullText, true);
|
| 147 |
-
break;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
try {
|
| 151 |
-
const parsed: OpenAIStreamChunk = JSON.parse(jsonStr);
|
| 152 |
-
const content = parsed.choices[0]?.delta?.content;
|
| 153 |
-
if (content) {
|
| 154 |
-
fullText += content;
|
| 155 |
-
// 立即回调以实现实时显示
|
| 156 |
-
onStreamChunk(content, fullText, false);
|
| 157 |
-
}
|
| 158 |
-
} catch (parseError) {
|
| 159 |
-
console.warn('解析流数据时出错:', parseError);
|
| 160 |
-
}
|
| 161 |
-
}
|
| 162 |
-
}
|
| 163 |
-
}
|
| 164 |
-
} finally {
|
| 165 |
-
reader.releaseLock();
|
| 166 |
-
}
|
| 167 |
-
} else {
|
| 168 |
-
// 处理非流式响应(例如图像请求)
|
| 169 |
-
const data = await response.json();
|
| 170 |
-
fullText = data.choices[0]?.message?.content || '';
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
if (!fullText.trim()) {
|
| 174 |
-
throw new Error('AI响应为空,请检查模型配置或重试');
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
return { text: fullText, durationMs };
|
| 178 |
-
|
| 179 |
-
} catch (error) {
|
| 180 |
-
console.error("调用OpenAI API时出错:", error);
|
| 181 |
-
const durationMs = performance.now() - startTime;
|
| 182 |
-
|
| 183 |
-
if (error instanceof Error) {
|
| 184 |
-
if (error.message.includes('API密钥') || error.message.includes('401') || error.message.includes('Unauthorized')) {
|
| 185 |
-
return { text: "API密钥无效或已过期。请检查您的OpenAI API密钥配置。", durationMs, error: "API key not valid" };
|
| 186 |
-
} else if (error.message.includes('429') || error.message.includes('Rate limit')) {
|
| 187 |
-
return { text: "API调用频率超限,请稍后重试。", durationMs, error: "Rate limit exceeded" };
|
| 188 |
-
} else if (error.message.includes('404') || error.message.includes('model')) {
|
| 189 |
-
return { text: `模型 ${modelName} 不存在或无权访问。请检查模型名称或API权限。`, durationMs, error: "Model not found" };
|
| 190 |
-
} else if (error.message.includes('网络') || error.message.includes('fetch')) {
|
| 191 |
-
return { text: "网络连接错误,请检查网络连接后重试。", durationMs, error: "Network error" };
|
| 192 |
-
}
|
| 193 |
-
return { text: `与OpenAI通信时出错: ${error.message}`, durationMs, error: error.message };
|
| 194 |
-
}
|
| 195 |
-
return { text: "与OpenAI通信时发生未知错误。", durationMs, error: "Unknown OpenAI error" };
|
| 196 |
-
}
|
| 197 |
};
|
|
|
|
| 1 |
+
interface OpenAIMessage {
|
| 2 |
+
role: 'system' | 'user' | 'assistant' | 'developer';
|
| 3 |
+
content: string | Array<{
|
| 4 |
+
type: 'text' | 'image_url';
|
| 5 |
+
text?: string;
|
| 6 |
+
image_url?: {
|
| 7 |
+
url: string;
|
| 8 |
+
detail?: 'low' | 'high' | 'auto';
|
| 9 |
+
};
|
| 10 |
+
}>;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface OpenAIResponse {
|
| 14 |
+
text: string;
|
| 15 |
+
durationMs: number;
|
| 16 |
+
error?: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
interface OpenAIStreamChunk {
|
| 20 |
+
id: string;
|
| 21 |
+
object: string;
|
| 22 |
+
created: number;
|
| 23 |
+
model: string;
|
| 24 |
+
choices: Array<{
|
| 25 |
+
index: number;
|
| 26 |
+
delta: {
|
| 27 |
+
role?: string;
|
| 28 |
+
content?: string;
|
| 29 |
+
};
|
| 30 |
+
finish_reason?: string | null;
|
| 31 |
+
}>;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const DEFAULT_OPENAI_API_BASE = 'https://api.openai.com/v1';
|
| 35 |
+
|
| 36 |
+
export const generateResponse = async (
|
| 37 |
+
prompt: string,
|
| 38 |
+
modelName: string,
|
| 39 |
+
systemInstruction?: string,
|
| 40 |
+
shouldUseReducedCapacity: boolean = false,
|
| 41 |
+
imagePart?: { inlineData: { mimeType: string; data: string } },
|
| 42 |
+
customBaseUrl?: string,
|
| 43 |
+
apiKey?: string,
|
| 44 |
+
onStreamChunk?: (newChunk: string, fullText: string, isComplete: boolean) => void
|
| 45 |
+
): Promise<OpenAIResponse> => {
|
| 46 |
+
const startTime = performance.now();
|
| 47 |
+
|
| 48 |
+
try {
|
| 49 |
+
if (!apiKey?.trim()) {
|
| 50 |
+
throw new Error("API密钥未设置。请配置您的OpenAI API密钥。");
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const messages: OpenAIMessage[] = [];
|
| 54 |
+
|
| 55 |
+
if (systemInstruction) {
|
| 56 |
+
messages.push({
|
| 57 |
+
role: 'system',
|
| 58 |
+
content: systemInstruction
|
| 59 |
+
});
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if (imagePart) {
|
| 63 |
+
const imageUrl = `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`;
|
| 64 |
+
messages.push({
|
| 65 |
+
role: 'user',
|
| 66 |
+
content: [
|
| 67 |
+
{
|
| 68 |
+
type: 'text',
|
| 69 |
+
text: prompt
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
type: 'image_url',
|
| 73 |
+
image_url: {
|
| 74 |
+
url: imageUrl,
|
| 75 |
+
detail: 'auto'
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
]
|
| 79 |
+
});
|
| 80 |
+
} else {
|
| 81 |
+
messages.push({
|
| 82 |
+
role: 'user',
|
| 83 |
+
content: prompt
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const requestBody = {
|
| 88 |
+
model: modelName,
|
| 89 |
+
messages: messages,
|
| 90 |
+
stream: !!onStreamChunk, // 只有提供回调时才使用流式
|
| 91 |
+
temperature: shouldUseReducedCapacity ? 0.3 : 0.7,
|
| 92 |
+
max_tokens: shouldUseReducedCapacity ? 1000 : 4000
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
const apiBase = customBaseUrl || DEFAULT_OPENAI_API_BASE;
|
| 96 |
+
|
| 97 |
+
const response = await fetch(`${apiBase}/chat/completions`, {
|
| 98 |
+
method: 'POST',
|
| 99 |
+
headers: {
|
| 100 |
+
'Content-Type': 'application/json',
|
| 101 |
+
'Authorization': `Bearer ${apiKey}`
|
| 102 |
+
},
|
| 103 |
+
body: JSON.stringify(requestBody)
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
if (!response.ok) {
|
| 107 |
+
const errorData = await response.json().catch(() => ({}));
|
| 108 |
+
const errorMessage = errorData.error?.message || response.statusText;
|
| 109 |
+
|
| 110 |
+
if (response.status === 401) {
|
| 111 |
+
throw new Error(`API密钥无效或已过期: ${errorMessage}`);
|
| 112 |
+
} else if (response.status === 429) {
|
| 113 |
+
throw new Error(`API调用频率超限: ${errorMessage}`);
|
| 114 |
+
} else if (response.status === 404) {
|
| 115 |
+
throw new Error(`模型不存在或无权访问: ${modelName}`);
|
| 116 |
+
} else {
|
| 117 |
+
throw new Error(`OpenAI API 错误 (${response.status}): ${errorMessage}`);
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
let fullText = '';
|
| 122 |
+
const durationMs = performance.now() - startTime;
|
| 123 |
+
|
| 124 |
+
// 处理流式响应
|
| 125 |
+
if (onStreamChunk && requestBody.stream) {
|
| 126 |
+
const reader = response.body?.getReader();
|
| 127 |
+
const decoder = new TextDecoder();
|
| 128 |
+
|
| 129 |
+
if (!reader) {
|
| 130 |
+
throw new Error('无法读取响应流');
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
try {
|
| 134 |
+
while (true) {
|
| 135 |
+
const { done, value } = await reader.read();
|
| 136 |
+
if (done) break;
|
| 137 |
+
|
| 138 |
+
const chunk = decoder.decode(value);
|
| 139 |
+
const lines = chunk.split('\n');
|
| 140 |
+
|
| 141 |
+
for (const line of lines) {
|
| 142 |
+
const trimmed = line.trim();
|
| 143 |
+
if (trimmed.startsWith('data: ')) {
|
| 144 |
+
const jsonStr = trimmed.slice(6);
|
| 145 |
+
if (jsonStr === '[DONE]') {
|
| 146 |
+
onStreamChunk('', fullText, true);
|
| 147 |
+
break;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
try {
|
| 151 |
+
const parsed: OpenAIStreamChunk = JSON.parse(jsonStr);
|
| 152 |
+
const content = parsed.choices[0]?.delta?.content;
|
| 153 |
+
if (content) {
|
| 154 |
+
fullText += content;
|
| 155 |
+
// 立即回调以实现实时显示
|
| 156 |
+
onStreamChunk(content, fullText, false);
|
| 157 |
+
}
|
| 158 |
+
} catch (parseError) {
|
| 159 |
+
console.warn('解析流数据时出错:', parseError);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
} finally {
|
| 165 |
+
reader.releaseLock();
|
| 166 |
+
}
|
| 167 |
+
} else {
|
| 168 |
+
// 处理非流式响应(例如图像请求)
|
| 169 |
+
const data = await response.json();
|
| 170 |
+
fullText = data.choices[0]?.message?.content || '';
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
if (!fullText.trim()) {
|
| 174 |
+
throw new Error('AI响应为空,请检查模型配置或重试');
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
return { text: fullText, durationMs };
|
| 178 |
+
|
| 179 |
+
} catch (error) {
|
| 180 |
+
console.error("调用OpenAI API时出错:", error);
|
| 181 |
+
const durationMs = performance.now() - startTime;
|
| 182 |
+
|
| 183 |
+
if (error instanceof Error) {
|
| 184 |
+
if (error.message.includes('API密钥') || error.message.includes('401') || error.message.includes('Unauthorized')) {
|
| 185 |
+
return { text: "API密钥无效或已过期。请检查您的OpenAI API密钥配置。", durationMs, error: "API key not valid" };
|
| 186 |
+
} else if (error.message.includes('429') || error.message.includes('Rate limit')) {
|
| 187 |
+
return { text: "API调用频率超限,请稍后重试。", durationMs, error: "Rate limit exceeded" };
|
| 188 |
+
} else if (error.message.includes('404') || error.message.includes('model')) {
|
| 189 |
+
return { text: `模型 ${modelName} 不存在或无权访问。请检查模型名称或API权限。`, durationMs, error: "Model not found" };
|
| 190 |
+
} else if (error.message.includes('网络') || error.message.includes('fetch')) {
|
| 191 |
+
return { text: "网络连接错误,请检查网络连接后重试。", durationMs, error: "Network error" };
|
| 192 |
+
}
|
| 193 |
+
return { text: `与OpenAI通信时出错: ${error.message}`, durationMs, error: error.message };
|
| 194 |
+
}
|
| 195 |
+
return { text: "与OpenAI通信时发生未知错误。", durationMs, error: "Unknown OpenAI error" };
|
| 196 |
+
}
|
| 197 |
};
|
tsconfig.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"target": "ES2020",
|
| 4 |
-
"experimentalDecorators": true,
|
| 5 |
-
"useDefineForClassFields": false,
|
| 6 |
-
"module": "ESNext",
|
| 7 |
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 8 |
-
"skipLibCheck": true,
|
| 9 |
-
|
| 10 |
-
/* Bundler mode */
|
| 11 |
-
"moduleResolution": "bundler",
|
| 12 |
-
"allowImportingTsExtensions": true,
|
| 13 |
-
"isolatedModules": true,
|
| 14 |
-
"moduleDetection": "force",
|
| 15 |
-
"noEmit": true,
|
| 16 |
-
"allowJs": true,
|
| 17 |
-
"jsx": "react-jsx",
|
| 18 |
-
|
| 19 |
-
/* Linting */
|
| 20 |
-
"strict": true,
|
| 21 |
-
"noUnusedLocals": true,
|
| 22 |
-
"noUnusedParameters": true,
|
| 23 |
-
"noFallthroughCasesInSwitch": true,
|
| 24 |
-
"noUncheckedSideEffectImports": true,
|
| 25 |
-
|
| 26 |
-
"paths": {
|
| 27 |
-
"@/*" : ["./*"]
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
"allowJs": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"noFallthroughCasesInSwitch": true,
|
| 24 |
+
"noUncheckedSideEffectImports": true,
|
| 25 |
+
|
| 26 |
+
"paths": {
|
| 27 |
+
"@/*" : ["./*"]
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
types.ts
CHANGED
|
@@ -1,298 +1,298 @@
|
|
| 1 |
-
// 动态消息发送者枚举 - 现在支持自定义角色名称
|
| 2 |
-
export enum MessageSender {
|
| 3 |
-
User = '用户',
|
| 4 |
-
System = '系统',
|
| 5 |
-
// 动态角色将在运行时创建,这里保留一些常用的默认值
|
| 6 |
-
Cognito = 'Cognito',
|
| 7 |
-
Muse = 'Muse',
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
// 消息用途枚举
|
| 11 |
-
export enum MessagePurpose {
|
| 12 |
-
UserInput = 'user-input',
|
| 13 |
-
SystemNotification = 'system-notification',
|
| 14 |
-
CognitoToMuse = 'cognito-to-muse', // AI角色之间的讨论(保持兼容性)
|
| 15 |
-
MuseToCognito = 'muse-to-cognito', // AI角色之间的讨论(保持兼容性)
|
| 16 |
-
FinalResponse = 'final-response', // 最终回复用户
|
| 17 |
-
RoleDiscussion = 'role-discussion', // 通用的角色间讨论
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
// 聊天消息接口
|
| 21 |
-
export interface ChatMessage {
|
| 22 |
-
id: string;
|
| 23 |
-
text: string;
|
| 24 |
-
sender: MessageSender | string; // 现在支持动态角色名称
|
| 25 |
-
purpose: MessagePurpose;
|
| 26 |
-
timestamp: Date;
|
| 27 |
-
durationMs?: number; // AI消息的生成时间
|
| 28 |
-
image?: { // 用户消息的可选图片数据
|
| 29 |
-
dataUrl: string; // 用于显示的base64数据URL
|
| 30 |
-
name: string;
|
| 31 |
-
type: string;
|
| 32 |
-
};
|
| 33 |
-
roleId?: string; // 可选的角色ID,用于关联具体的AI角色配置
|
| 34 |
-
isStreaming?: boolean; // 标记消息是否正在流式传输
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
// 讨论记录接口
|
| 38 |
-
export interface DiscussionRecord {
|
| 39 |
-
id: string;
|
| 40 |
-
timestamp: Date;
|
| 41 |
-
userQuery: string;
|
| 42 |
-
userImage?: {
|
| 43 |
-
name: string;
|
| 44 |
-
type: string;
|
| 45 |
-
size: number;
|
| 46 |
-
};
|
| 47 |
-
discussionMode: string;
|
| 48 |
-
activeRoles: Array<{
|
| 49 |
-
id: string;
|
| 50 |
-
name: string;
|
| 51 |
-
modelName: string;
|
| 52 |
-
channelName: string;
|
| 53 |
-
}>;
|
| 54 |
-
turns: Array<{
|
| 55 |
-
id: string;
|
| 56 |
-
role: string;
|
| 57 |
-
roleId?: string;
|
| 58 |
-
message: string;
|
| 59 |
-
timestamp: Date;
|
| 60 |
-
durationMs?: number;
|
| 61 |
-
purpose: MessagePurpose;
|
| 62 |
-
}>;
|
| 63 |
-
notepadUpdates: Array<{
|
| 64 |
-
id: string;
|
| 65 |
-
updater: string;
|
| 66 |
-
updaterId?: string;
|
| 67 |
-
content: string;
|
| 68 |
-
timestamp: Date;
|
| 69 |
-
}>;
|
| 70 |
-
finalAnswer?: {
|
| 71 |
-
content: string;
|
| 72 |
-
provider: string;
|
| 73 |
-
providerId?: string;
|
| 74 |
-
timestamp: Date;
|
| 75 |
-
durationMs?: number;
|
| 76 |
-
};
|
| 77 |
-
totalDuration: number; // 总讨论时长(毫秒)
|
| 78 |
-
isCompleted: boolean; // 讨论是否正常完成
|
| 79 |
-
wasInterrupted: boolean; // 是否被用户中断
|
| 80 |
-
interruptedAt?: Date; // 中断时间
|
| 81 |
-
settings: {
|
| 82 |
-
discussionMode: string;
|
| 83 |
-
manualFixedTurns?: number;
|
| 84 |
-
isReducedCapacityEnabled: boolean;
|
| 85 |
-
activeRoleCount: number;
|
| 86 |
-
};
|
| 87 |
-
metadata: {
|
| 88 |
-
version: string;
|
| 89 |
-
exportedAt?: Date;
|
| 90 |
-
messageCount: number;
|
| 91 |
-
notepadUpdateCount: number;
|
| 92 |
-
};
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
// 讨论统计接口
|
| 96 |
-
export interface DiscussionStats {
|
| 97 |
-
totalTurns: number;
|
| 98 |
-
averageResponseTime: number;
|
| 99 |
-
longestResponseTime: number;
|
| 100 |
-
shortestResponseTime: number;
|
| 101 |
-
totalTokensUsed?: number; // 如果API提供token使用情况
|
| 102 |
-
roleParticipation: Record<string, {
|
| 103 |
-
turnCount: number;
|
| 104 |
-
totalResponseTime: number;
|
| 105 |
-
averageResponseTime: number;
|
| 106 |
-
}>;
|
| 107 |
-
notepadUpdateFrequency: number;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
// 流式传输状态接口
|
| 111 |
-
export interface StreamingState {
|
| 112 |
-
messageId: string;
|
| 113 |
-
currentText: string;
|
| 114 |
-
targetText: string;
|
| 115 |
-
isComplete: boolean;
|
| 116 |
-
startTime: Date;
|
| 117 |
-
speed: number; // 字符/秒
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// AI模型配置接口(从constants.ts导入,但在这里声明以保持类型完整性)
|
| 121 |
-
export interface AiModelConfig {
|
| 122 |
-
id: string;
|
| 123 |
-
name: string;
|
| 124 |
-
apiName: string;
|
| 125 |
-
baseUrl?: string;
|
| 126 |
-
supportsImages: boolean;
|
| 127 |
-
supportsReducedCapacity: boolean;
|
| 128 |
-
category: string;
|
| 129 |
-
maxTokens: number;
|
| 130 |
-
temperature: number;
|
| 131 |
-
isCustom: boolean;
|
| 132 |
-
createdAt: Date;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
// AI角色配置接口
|
| 136 |
-
export interface AiRoleConfig {
|
| 137 |
-
id: string;
|
| 138 |
-
name: string;
|
| 139 |
-
systemPrompt: string;
|
| 140 |
-
modelId: string;
|
| 141 |
-
isActive: boolean;
|
| 142 |
-
color?: string; // 可选的UI显示颜色
|
| 143 |
-
description?: string; // 可选的角色描述
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
// 讨论配置接口
|
| 147 |
-
export interface DiscussionConfig {
|
| 148 |
-
mode: 'fixed' | 'ai-driven';
|
| 149 |
-
fixedTurns?: number;
|
| 150 |
-
maxTurnsPerRole?: number;
|
| 151 |
-
allowSameModel?: boolean; // 是否允许多个角色使用相同模型
|
| 152 |
-
enableNotepadSharing?: boolean; // 是否启用记事本共享
|
| 153 |
-
enableStreamingTypewriter?: boolean; // 是否启用流式打字机效果
|
| 154 |
-
typewriterSpeed?: number; // 打字机速度(毫秒/字符)
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
// API响应接口
|
| 158 |
-
export interface ApiResponse {
|
| 159 |
-
text: string;
|
| 160 |
-
durationMs: number;
|
| 161 |
-
error?: string;
|
| 162 |
-
usage?: {
|
| 163 |
-
promptTokens: number;
|
| 164 |
-
completionTokens: number;
|
| 165 |
-
totalTokens: number;
|
| 166 |
-
};
|
| 167 |
-
streamingChunks?: string[]; // 流式响应的块
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
// 配置导出/导入接口
|
| 171 |
-
export interface ConfigExportData {
|
| 172 |
-
models: AiModelConfig[];
|
| 173 |
-
roles: AiRoleConfig[];
|
| 174 |
-
discussionConfig?: DiscussionConfig;
|
| 175 |
-
discussionRecords?: DiscussionRecord[]; // 可选的讨论记录
|
| 176 |
-
exportedAt: string;
|
| 177 |
-
version: string;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
// 讨论记录导出接口
|
| 181 |
-
export interface DiscussionExportData {
|
| 182 |
-
record: DiscussionRecord;
|
| 183 |
-
stats: DiscussionStats;
|
| 184 |
-
fullTranscript: string; // 完整的文本记录
|
| 185 |
-
exportFormat: 'json' | 'markdown' | 'html' | 'txt';
|
| 186 |
-
exportedAt: string;
|
| 187 |
-
version: string;
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
// 验证结果接口
|
| 191 |
-
export interface ValidationResult {
|
| 192 |
-
isValid: boolean;
|
| 193 |
-
errors: string[];
|
| 194 |
-
warnings?: string[];
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// 角色状态接口(运行时使用)
|
| 198 |
-
export interface RoleState {
|
| 199 |
-
id: string;
|
| 200 |
-
name: string;
|
| 201 |
-
isProcessing: boolean;
|
| 202 |
-
lastResponse?: string;
|
| 203 |
-
totalResponseTime?: number;
|
| 204 |
-
messageCount?: number;
|
| 205 |
-
currentStreamingMessageId?: string; // 当前正在流式传输的消息ID
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
// 会话状态接口
|
| 209 |
-
export interface SessionState {
|
| 210 |
-
isActive: boolean;
|
| 211 |
-
startTime?: Date;
|
| 212 |
-
currentTurn: number;
|
| 213 |
-
activeRoles: RoleState[];
|
| 214 |
-
discussionLog: Array<{
|
| 215 |
-
roleId: string;
|
| 216 |
-
roleName: string;
|
| 217 |
-
message: string;
|
| 218 |
-
timestamp: Date;
|
| 219 |
-
}>;
|
| 220 |
-
canBeInterrupted: boolean; // 是否可以被中断
|
| 221 |
-
interruptionRequested: boolean; // 是否请求了中断
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
// 讨论控制接口
|
| 225 |
-
export interface DiscussionControl {
|
| 226 |
-
canStart: boolean;
|
| 227 |
-
canPause: boolean;
|
| 228 |
-
canResume: boolean;
|
| 229 |
-
canInterrupt: boolean;
|
| 230 |
-
canExport: boolean;
|
| 231 |
-
currentPhase: 'idle' | 'initializing' | 'discussing' | 'synthesizing' | 'completed' | 'interrupted';
|
| 232 |
-
estimatedTimeRemaining?: number; // 估算剩余时间(毫秒)
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
// 导出选项接口
|
| 236 |
-
export interface ExportOptions {
|
| 237 |
-
format: 'json' | 'markdown' | 'html' | 'txt';
|
| 238 |
-
includeMetadata: boolean;
|
| 239 |
-
includeStats: boolean;
|
| 240 |
-
includeNotepadHistory: boolean;
|
| 241 |
-
includeSystemMessages: boolean;
|
| 242 |
-
timestampFormat: 'iso' | 'local' | 'relative';
|
| 243 |
-
compressOutput: boolean;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
// 消息过滤器接口
|
| 247 |
-
export interface MessageFilter {
|
| 248 |
-
senders?: (MessageSender | string)[];
|
| 249 |
-
purposes?: MessagePurpose[];
|
| 250 |
-
dateRange?: {
|
| 251 |
-
start: Date;
|
| 252 |
-
end: Date;
|
| 253 |
-
};
|
| 254 |
-
textSearch?: string;
|
| 255 |
-
hasDuration?: boolean;
|
| 256 |
-
hasImage?: boolean;
|
| 257 |
-
minDuration?: number;
|
| 258 |
-
maxDuration?: number;
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// 通知接口
|
| 262 |
-
export interface Notification {
|
| 263 |
-
id: string;
|
| 264 |
-
type: 'info' | 'success' | 'warning' | 'error';
|
| 265 |
-
title: string;
|
| 266 |
-
message: string;
|
| 267 |
-
timestamp: Date;
|
| 268 |
-
duration?: number; // 自动消失时间(毫秒)
|
| 269 |
-
actions?: Array<{
|
| 270 |
-
label: string;
|
| 271 |
-
action: () => void;
|
| 272 |
-
style?: 'primary' | 'secondary' | 'danger';
|
| 273 |
-
}>;
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// 应用状态接口
|
| 277 |
-
export interface AppState {
|
| 278 |
-
ui: {
|
| 279 |
-
theme: 'light' | 'dark' | 'auto';
|
| 280 |
-
sidebarCollapsed: boolean;
|
| 281 |
-
roleManagerOpen: boolean;
|
| 282 |
-
configManagerOpen: boolean;
|
| 283 |
-
notifications: Notification[];
|
| 284 |
-
};
|
| 285 |
-
discussion: {
|
| 286 |
-
current?: DiscussionRecord;
|
| 287 |
-
history: DiscussionRecord[];
|
| 288 |
-
control: DiscussionControl;
|
| 289 |
-
streaming: Map<string, StreamingState>;
|
| 290 |
-
};
|
| 291 |
-
session: SessionState;
|
| 292 |
-
config: {
|
| 293 |
-
channels: any[];
|
| 294 |
-
models: any[];
|
| 295 |
-
roles: any[];
|
| 296 |
-
discussionSettings: DiscussionConfig;
|
| 297 |
-
};
|
| 298 |
}
|
|
|
|
| 1 |
+
// 动态消息发送者枚举 - 现在支持自定义角色名称
|
| 2 |
+
export enum MessageSender {
|
| 3 |
+
User = '用户',
|
| 4 |
+
System = '系统',
|
| 5 |
+
// 动态角色将在运行时创建,这里保留一些常用的默认值
|
| 6 |
+
Cognito = 'Cognito',
|
| 7 |
+
Muse = 'Muse',
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
// 消息用途枚举
|
| 11 |
+
export enum MessagePurpose {
|
| 12 |
+
UserInput = 'user-input',
|
| 13 |
+
SystemNotification = 'system-notification',
|
| 14 |
+
CognitoToMuse = 'cognito-to-muse', // AI角色之间的讨论(保持兼容性)
|
| 15 |
+
MuseToCognito = 'muse-to-cognito', // AI角色之间的讨论(保持兼容性)
|
| 16 |
+
FinalResponse = 'final-response', // 最终回复用户
|
| 17 |
+
RoleDiscussion = 'role-discussion', // 通用的角色间讨论
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// 聊天消息接口
|
| 21 |
+
export interface ChatMessage {
|
| 22 |
+
id: string;
|
| 23 |
+
text: string;
|
| 24 |
+
sender: MessageSender | string; // 现在支持动态角色名称
|
| 25 |
+
purpose: MessagePurpose;
|
| 26 |
+
timestamp: Date;
|
| 27 |
+
durationMs?: number; // AI消息的生成时间
|
| 28 |
+
image?: { // 用户消息的可选图片数据
|
| 29 |
+
dataUrl: string; // 用于显示的base64数据URL
|
| 30 |
+
name: string;
|
| 31 |
+
type: string;
|
| 32 |
+
};
|
| 33 |
+
roleId?: string; // 可选的角色ID,用于关联具体的AI角色配置
|
| 34 |
+
isStreaming?: boolean; // 标记消息是否正在流式传输
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// 讨论记录接口
|
| 38 |
+
export interface DiscussionRecord {
|
| 39 |
+
id: string;
|
| 40 |
+
timestamp: Date;
|
| 41 |
+
userQuery: string;
|
| 42 |
+
userImage?: {
|
| 43 |
+
name: string;
|
| 44 |
+
type: string;
|
| 45 |
+
size: number;
|
| 46 |
+
};
|
| 47 |
+
discussionMode: string;
|
| 48 |
+
activeRoles: Array<{
|
| 49 |
+
id: string;
|
| 50 |
+
name: string;
|
| 51 |
+
modelName: string;
|
| 52 |
+
channelName: string;
|
| 53 |
+
}>;
|
| 54 |
+
turns: Array<{
|
| 55 |
+
id: string;
|
| 56 |
+
role: string;
|
| 57 |
+
roleId?: string;
|
| 58 |
+
message: string;
|
| 59 |
+
timestamp: Date;
|
| 60 |
+
durationMs?: number;
|
| 61 |
+
purpose: MessagePurpose;
|
| 62 |
+
}>;
|
| 63 |
+
notepadUpdates: Array<{
|
| 64 |
+
id: string;
|
| 65 |
+
updater: string;
|
| 66 |
+
updaterId?: string;
|
| 67 |
+
content: string;
|
| 68 |
+
timestamp: Date;
|
| 69 |
+
}>;
|
| 70 |
+
finalAnswer?: {
|
| 71 |
+
content: string;
|
| 72 |
+
provider: string;
|
| 73 |
+
providerId?: string;
|
| 74 |
+
timestamp: Date;
|
| 75 |
+
durationMs?: number;
|
| 76 |
+
};
|
| 77 |
+
totalDuration: number; // 总讨论时长(毫秒)
|
| 78 |
+
isCompleted: boolean; // 讨论是否正常完成
|
| 79 |
+
wasInterrupted: boolean; // 是否被用户中断
|
| 80 |
+
interruptedAt?: Date; // 中断时间
|
| 81 |
+
settings: {
|
| 82 |
+
discussionMode: string;
|
| 83 |
+
manualFixedTurns?: number;
|
| 84 |
+
isReducedCapacityEnabled: boolean;
|
| 85 |
+
activeRoleCount: number;
|
| 86 |
+
};
|
| 87 |
+
metadata: {
|
| 88 |
+
version: string;
|
| 89 |
+
exportedAt?: Date;
|
| 90 |
+
messageCount: number;
|
| 91 |
+
notepadUpdateCount: number;
|
| 92 |
+
};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 讨论统计接口
|
| 96 |
+
export interface DiscussionStats {
|
| 97 |
+
totalTurns: number;
|
| 98 |
+
averageResponseTime: number;
|
| 99 |
+
longestResponseTime: number;
|
| 100 |
+
shortestResponseTime: number;
|
| 101 |
+
totalTokensUsed?: number; // 如果API提供token使用情况
|
| 102 |
+
roleParticipation: Record<string, {
|
| 103 |
+
turnCount: number;
|
| 104 |
+
totalResponseTime: number;
|
| 105 |
+
averageResponseTime: number;
|
| 106 |
+
}>;
|
| 107 |
+
notepadUpdateFrequency: number;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// 流式传输状态接口
|
| 111 |
+
export interface StreamingState {
|
| 112 |
+
messageId: string;
|
| 113 |
+
currentText: string;
|
| 114 |
+
targetText: string;
|
| 115 |
+
isComplete: boolean;
|
| 116 |
+
startTime: Date;
|
| 117 |
+
speed: number; // 字符/秒
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// AI模型配置接口(从constants.ts导入,但在这里声明以保持类型完整性)
|
| 121 |
+
export interface AiModelConfig {
|
| 122 |
+
id: string;
|
| 123 |
+
name: string;
|
| 124 |
+
apiName: string;
|
| 125 |
+
baseUrl?: string;
|
| 126 |
+
supportsImages: boolean;
|
| 127 |
+
supportsReducedCapacity: boolean;
|
| 128 |
+
category: string;
|
| 129 |
+
maxTokens: number;
|
| 130 |
+
temperature: number;
|
| 131 |
+
isCustom: boolean;
|
| 132 |
+
createdAt: Date;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// AI角色配置接口
|
| 136 |
+
export interface AiRoleConfig {
|
| 137 |
+
id: string;
|
| 138 |
+
name: string;
|
| 139 |
+
systemPrompt: string;
|
| 140 |
+
modelId: string;
|
| 141 |
+
isActive: boolean;
|
| 142 |
+
color?: string; // 可选的UI显示颜色
|
| 143 |
+
description?: string; // 可选的角色描述
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// 讨论配置接口
|
| 147 |
+
export interface DiscussionConfig {
|
| 148 |
+
mode: 'fixed' | 'ai-driven';
|
| 149 |
+
fixedTurns?: number;
|
| 150 |
+
maxTurnsPerRole?: number;
|
| 151 |
+
allowSameModel?: boolean; // 是否允许多个角色使用相同模型
|
| 152 |
+
enableNotepadSharing?: boolean; // 是否启用记事本共享
|
| 153 |
+
enableStreamingTypewriter?: boolean; // 是否启用流式打字机效果
|
| 154 |
+
typewriterSpeed?: number; // 打字机速度(毫秒/字符)
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// API响应接口
|
| 158 |
+
export interface ApiResponse {
|
| 159 |
+
text: string;
|
| 160 |
+
durationMs: number;
|
| 161 |
+
error?: string;
|
| 162 |
+
usage?: {
|
| 163 |
+
promptTokens: number;
|
| 164 |
+
completionTokens: number;
|
| 165 |
+
totalTokens: number;
|
| 166 |
+
};
|
| 167 |
+
streamingChunks?: string[]; // 流式响应的块
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// 配置导出/导入接口
|
| 171 |
+
export interface ConfigExportData {
|
| 172 |
+
models: AiModelConfig[];
|
| 173 |
+
roles: AiRoleConfig[];
|
| 174 |
+
discussionConfig?: DiscussionConfig;
|
| 175 |
+
discussionRecords?: DiscussionRecord[]; // 可选的讨论记录
|
| 176 |
+
exportedAt: string;
|
| 177 |
+
version: string;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// 讨论记录导出接口
|
| 181 |
+
export interface DiscussionExportData {
|
| 182 |
+
record: DiscussionRecord;
|
| 183 |
+
stats: DiscussionStats;
|
| 184 |
+
fullTranscript: string; // 完整的文本记录
|
| 185 |
+
exportFormat: 'json' | 'markdown' | 'html' | 'txt';
|
| 186 |
+
exportedAt: string;
|
| 187 |
+
version: string;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// 验证结果接口
|
| 191 |
+
export interface ValidationResult {
|
| 192 |
+
isValid: boolean;
|
| 193 |
+
errors: string[];
|
| 194 |
+
warnings?: string[];
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// 角色状态接口(运行时使用)
|
| 198 |
+
export interface RoleState {
|
| 199 |
+
id: string;
|
| 200 |
+
name: string;
|
| 201 |
+
isProcessing: boolean;
|
| 202 |
+
lastResponse?: string;
|
| 203 |
+
totalResponseTime?: number;
|
| 204 |
+
messageCount?: number;
|
| 205 |
+
currentStreamingMessageId?: string; // 当前正在流式传输的消息ID
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// 会话状态接口
|
| 209 |
+
export interface SessionState {
|
| 210 |
+
isActive: boolean;
|
| 211 |
+
startTime?: Date;
|
| 212 |
+
currentTurn: number;
|
| 213 |
+
activeRoles: RoleState[];
|
| 214 |
+
discussionLog: Array<{
|
| 215 |
+
roleId: string;
|
| 216 |
+
roleName: string;
|
| 217 |
+
message: string;
|
| 218 |
+
timestamp: Date;
|
| 219 |
+
}>;
|
| 220 |
+
canBeInterrupted: boolean; // 是否可以被中断
|
| 221 |
+
interruptionRequested: boolean; // 是否请求了中断
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// 讨论控制接口
|
| 225 |
+
export interface DiscussionControl {
|
| 226 |
+
canStart: boolean;
|
| 227 |
+
canPause: boolean;
|
| 228 |
+
canResume: boolean;
|
| 229 |
+
canInterrupt: boolean;
|
| 230 |
+
canExport: boolean;
|
| 231 |
+
currentPhase: 'idle' | 'initializing' | 'discussing' | 'synthesizing' | 'completed' | 'interrupted';
|
| 232 |
+
estimatedTimeRemaining?: number; // 估算剩余时间(毫秒)
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// 导出选项接口
|
| 236 |
+
export interface ExportOptions {
|
| 237 |
+
format: 'json' | 'markdown' | 'html' | 'txt';
|
| 238 |
+
includeMetadata: boolean;
|
| 239 |
+
includeStats: boolean;
|
| 240 |
+
includeNotepadHistory: boolean;
|
| 241 |
+
includeSystemMessages: boolean;
|
| 242 |
+
timestampFormat: 'iso' | 'local' | 'relative';
|
| 243 |
+
compressOutput: boolean;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
// 消息过滤器接口
|
| 247 |
+
export interface MessageFilter {
|
| 248 |
+
senders?: (MessageSender | string)[];
|
| 249 |
+
purposes?: MessagePurpose[];
|
| 250 |
+
dateRange?: {
|
| 251 |
+
start: Date;
|
| 252 |
+
end: Date;
|
| 253 |
+
};
|
| 254 |
+
textSearch?: string;
|
| 255 |
+
hasDuration?: boolean;
|
| 256 |
+
hasImage?: boolean;
|
| 257 |
+
minDuration?: number;
|
| 258 |
+
maxDuration?: number;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// 通知接口
|
| 262 |
+
export interface Notification {
|
| 263 |
+
id: string;
|
| 264 |
+
type: 'info' | 'success' | 'warning' | 'error';
|
| 265 |
+
title: string;
|
| 266 |
+
message: string;
|
| 267 |
+
timestamp: Date;
|
| 268 |
+
duration?: number; // 自动消失时间(毫秒)
|
| 269 |
+
actions?: Array<{
|
| 270 |
+
label: string;
|
| 271 |
+
action: () => void;
|
| 272 |
+
style?: 'primary' | 'secondary' | 'danger';
|
| 273 |
+
}>;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// 应用状态接口
|
| 277 |
+
export interface AppState {
|
| 278 |
+
ui: {
|
| 279 |
+
theme: 'light' | 'dark' | 'auto';
|
| 280 |
+
sidebarCollapsed: boolean;
|
| 281 |
+
roleManagerOpen: boolean;
|
| 282 |
+
configManagerOpen: boolean;
|
| 283 |
+
notifications: Notification[];
|
| 284 |
+
};
|
| 285 |
+
discussion: {
|
| 286 |
+
current?: DiscussionRecord;
|
| 287 |
+
history: DiscussionRecord[];
|
| 288 |
+
control: DiscussionControl;
|
| 289 |
+
streaming: Map<string, StreamingState>;
|
| 290 |
+
};
|
| 291 |
+
session: SessionState;
|
| 292 |
+
config: {
|
| 293 |
+
channels: any[];
|
| 294 |
+
models: any[];
|
| 295 |
+
roles: any[];
|
| 296 |
+
discussionSettings: DiscussionConfig;
|
| 297 |
+
};
|
| 298 |
}
|
vite.config.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
| 1 |
-
import path from 'path';
|
| 2 |
-
import { defineConfig } from 'vite';
|
| 3 |
-
|
| 4 |
-
export default defineConfig({
|
| 5 |
-
resolve: {
|
| 6 |
-
alias: {
|
| 7 |
-
'@': path.resolve(__dirname, '.'),
|
| 8 |
-
}
|
| 9 |
-
},
|
| 10 |
-
define: {
|
| 11 |
-
'import.meta.env.VITE_PRESET_API_URL': JSON.stringify(process.env.VITE_PRESET_API_URL || ''),
|
| 12 |
-
'import.meta.env.VITE_PRESET_API_KEY': JSON.stringify(process.env.VITE_PRESET_API_KEY || ''),
|
| 13 |
-
},
|
| 14 |
-
build: {
|
| 15 |
-
target: 'es2020',
|
| 16 |
-
rollupOptions: {
|
| 17 |
-
output: {
|
| 18 |
-
manualChunks: undefined,
|
| 19 |
-
}
|
| 20 |
-
}
|
| 21 |
-
},
|
| 22 |
-
server: {
|
| 23 |
-
port: 5173,
|
| 24 |
-
host: '0.0.0.0',
|
| 25 |
-
open: false
|
| 26 |
-
}
|
| 27 |
});
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import { defineConfig } from 'vite';
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
resolve: {
|
| 6 |
+
alias: {
|
| 7 |
+
'@': path.resolve(__dirname, '.'),
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
define: {
|
| 11 |
+
'import.meta.env.VITE_PRESET_API_URL': JSON.stringify(process.env.VITE_PRESET_API_URL || ''),
|
| 12 |
+
'import.meta.env.VITE_PRESET_API_KEY': JSON.stringify(process.env.VITE_PRESET_API_KEY || ''),
|
| 13 |
+
},
|
| 14 |
+
build: {
|
| 15 |
+
target: 'es2020',
|
| 16 |
+
rollupOptions: {
|
| 17 |
+
output: {
|
| 18 |
+
manualChunks: undefined,
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
server: {
|
| 23 |
+
port: 5173,
|
| 24 |
+
host: '0.0.0.0',
|
| 25 |
+
open: false
|
| 26 |
+
}
|
| 27 |
});
|