'.length).trim();
- if (unclosedThought) {
- if (currentReasoningContent) {
- currentReasoningContent += '\n\n---\n\n' + unclosedThought;
- } else {
- currentReasoningContent = unclosedThought;
- }
- }
- currentContent = currentContent.substring(0, lastOpenThinkIndex);
- }
- }
-
- currentContent = currentContent.replace(/<\/?think>/g, '').trim();
-
- return [...prevMessage.slice(0, -1), {
- ...lastMessage,
- status: 'complete',
- reasoningContent: currentReasoningContent || null,
- content: currentContent,
- isReasoningExpanded: false
- }];
- }
- return prevMessage;
- });
- }
- }, [setMessage]);
-
- const toggleReasoningExpansion = (messageId) => {
- setMessage(prevMessages =>
- prevMessages.map(msg =>
- msg.id === messageId && msg.role === 'assistant'
- ? { ...msg, isReasoningExpanded: !msg.isReasoningExpanded }
- : msg
- )
- );
- };
-
- const renderCustomChatContent = useCallback(
- ({ message, className }) => {
- return (
-
- );
- },
- [styleState],
- );
-
- const renderChatBoxAction = useCallback((props) => {
- const { message: currentMessage } = props;
-
- const isAnyMessageGenerating = message.some(msg => msg.status === 'loading' || msg.status === 'incomplete');
-
- return (
-
- );
- }, [handleMessageReset, handleMessageCopy, handleMessageDelete, styleState, message, handleRoleToggle]);
+ useEffect(() => {
+ debouncedSaveConfig();
+ }, [debouncedSaveConfig]);
return (
@@ -1202,10 +471,10 @@ const Playground = () => {
showDebugPanel={showDebugPanel}
roleInfo={roleInfo}
onMessageSend={onMessageSend}
- onMessageCopy={handleMessageCopy}
- onMessageReset={handleMessageReset}
- onMessageDelete={handleMessageDelete}
- onRoleToggle={handleRoleToggle}
+ onMessageCopy={messageActions.handleMessageCopy}
+ onMessageReset={messageActions.handleMessageReset}
+ onMessageDelete={messageActions.handleMessageDelete}
+ onRoleToggle={messageActions.handleRoleToggle}
onStopGenerator={onStopGenerator}
onClearMessages={() => setMessage([])}
onToggleDebugPanel={() => setShowDebugPanel(!showDebugPanel)}
diff --git a/web/src/utils/apiUtils.js b/web/src/utils/apiUtils.js
new file mode 100644
index 00000000..9601d01b
--- /dev/null
+++ b/web/src/utils/apiUtils.js
@@ -0,0 +1,100 @@
+import { formatMessageForAPI } from './messageUtils';
+
+// 构建API请求载荷
+export const buildApiPayload = (messages, systemMessage, inputs, parameterEnabled) => {
+ const formattedMessages = messages.map(formatMessageForAPI);
+
+ if (systemMessage) {
+ formattedMessages.unshift(formatMessageForAPI(systemMessage));
+ }
+
+ const payload = {
+ messages: formattedMessages,
+ stream: inputs.stream,
+ model: inputs.model,
+ group: inputs.group,
+ };
+
+ // 添加可选参数
+ const optionalParams = [
+ 'max_tokens', 'temperature', 'top_p',
+ 'frequency_penalty', 'presence_penalty', 'seed'
+ ];
+
+ optionalParams.forEach(param => {
+ if (parameterEnabled[param]) {
+ if (param === 'max_tokens' && inputs[param] > 0) {
+ payload[param] = parseInt(inputs[param]);
+ } else if (param === 'seed' && inputs[param] !== null && inputs[param] !== '') {
+ payload[param] = parseInt(inputs[param]);
+ } else if (param !== 'max_tokens' && param !== 'seed') {
+ payload[param] = inputs[param];
+ }
+ }
+ });
+
+ return payload;
+};
+
+// 处理API错误响应
+export const handleApiError = (error, response = null) => {
+ const errorInfo = {
+ error: error.message || '未知错误',
+ timestamp: new Date().toISOString(),
+ stack: error.stack
+ };
+
+ if (response) {
+ errorInfo.status = response.status;
+ errorInfo.statusText = response.statusText;
+ }
+
+ if (error.message.includes('HTTP error')) {
+ errorInfo.details = '服务器返回了错误状态码';
+ } else if (error.message.includes('Failed to fetch')) {
+ errorInfo.details = '网络连接失败或服务器无响应';
+ }
+
+ return errorInfo;
+};
+
+// 处理模型数据
+export const processModelsData = (data, currentModel) => {
+ const modelOptions = data.map(model => ({
+ label: model,
+ value: model,
+ }));
+
+ const hasCurrentModel = modelOptions.some(option => option.value === currentModel);
+ const selectedModel = hasCurrentModel && modelOptions.length > 0
+ ? currentModel
+ : modelOptions[0]?.value;
+
+ return { modelOptions, selectedModel };
+};
+
+// 处理分组数据
+export const processGroupsData = (data, userGroup) => {
+ let groupOptions = Object.entries(data).map(([group, info]) => ({
+ label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc,
+ value: group,
+ ratio: info.ratio,
+ fullLabel: info.desc,
+ }));
+
+ if (groupOptions.length === 0) {
+ groupOptions = [{
+ label: '用户分组',
+ value: '',
+ ratio: 1,
+ }];
+ } else if (userGroup) {
+ const userGroupIndex = groupOptions.findIndex(g => g.value === userGroup);
+ if (userGroupIndex > -1) {
+ const userGroupOption = groupOptions.splice(userGroupIndex, 1)[0];
+ groupOptions.unshift(userGroupOption);
+ }
+ }
+
+ return groupOptions;
+};
\ No newline at end of file
diff --git a/web/src/utils/constants.js b/web/src/utils/constants.js
new file mode 100644
index 00000000..8ad9a5d9
--- /dev/null
+++ b/web/src/utils/constants.js
@@ -0,0 +1,78 @@
+// Playground 相关常量
+export const DEFAULT_MESSAGES = [
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: '你好',
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: '你好,请问有什么可以帮助您的吗?',
+ reasoningContent: '',
+ isReasoningExpanded: false,
+ },
+];
+
+export const MESSAGE_STATUS = {
+ LOADING: 'loading',
+ INCOMPLETE: 'incomplete',
+ COMPLETE: 'complete',
+ ERROR: 'error',
+};
+
+export const MESSAGE_ROLES = {
+ USER: 'user',
+ ASSISTANT: 'assistant',
+ SYSTEM: 'system',
+};
+
+export const DEBUG_TABS = {
+ PREVIEW: 'preview',
+ REQUEST: 'request',
+ RESPONSE: 'response',
+};
+
+export const API_ENDPOINTS = {
+ CHAT_COMPLETIONS: '/pg/chat/completions',
+ USER_MODELS: '/api/user/models',
+ USER_GROUPS: '/api/user/self/groups',
+};
+
+export const DEFAULT_CONFIG = {
+ inputs: {
+ model: 'gpt-4',
+ group: '',
+ temperature: 0.7,
+ top_p: 1,
+ max_tokens: 2048,
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ seed: null,
+ stream: true,
+ imageEnabled: false,
+ imageUrls: [''],
+ },
+ parameterEnabled: {
+ temperature: true,
+ top_p: false,
+ max_tokens: false,
+ frequency_penalty: false,
+ presence_penalty: false,
+ seed: false,
+ },
+ systemPrompt: '',
+ showDebugPanel: false,
+};
+
+export const THINK_TAG_REGEX = /([\s\S]*?)<\/think>/g;
+
+export const ERROR_MESSAGES = {
+ NO_TEXT_CONTENT: '此消息没有可复制的文本内容',
+ INVALID_MESSAGE_TYPE: '无法复制此类型的消息内容',
+ COPY_FAILED: '复制失败,请手动选择文本复制',
+ COPY_HTTPS_REQUIRED: '复制功能需要 HTTPS 环境,请手动复制',
+ BROWSER_NOT_SUPPORTED: '浏览器不支持复制功能,请手动复制',
+};
\ No newline at end of file
diff --git a/web/src/utils/messageUtils.js b/web/src/utils/messageUtils.js
new file mode 100644
index 00000000..019116d8
--- /dev/null
+++ b/web/src/utils/messageUtils.js
@@ -0,0 +1,123 @@
+import { THINK_TAG_REGEX, MESSAGE_ROLES } from './constants';
+
+// 生成唯一ID
+let messageId = 4;
+export const generateMessageId = () => `${messageId++}`;
+
+// 提取消息中的文本内容
+export const getTextContent = (message) => {
+ if (Array.isArray(message.content)) {
+ const textContent = message.content.find(item => item.type === 'text');
+ return textContent?.text || '';
+ }
+ return typeof message.content === 'string' ? message.content : '';
+};
+
+// 处理 think 标签
+export const processThinkTags = (content, reasoningContent = '') => {
+ if (!content.includes('')) {
+ return { content, reasoningContent };
+ }
+
+ let thoughts = [];
+ let replyParts = [];
+ let lastIndex = 0;
+ let match;
+
+ THINK_TAG_REGEX.lastIndex = 0;
+ while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
+ replyParts.push(content.substring(lastIndex, match.index));
+ thoughts.push(match[1]);
+ lastIndex = match.index + match[0].length;
+ }
+ replyParts.push(content.substring(lastIndex));
+
+ const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
+
+ let processedReasoningContent = reasoningContent;
+ if (thoughts.length > 0) {
+ const thoughtsStr = thoughts.join('\n\n---\n\n');
+ processedReasoningContent = reasoningContent
+ ? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
+ : thoughtsStr;
+ }
+
+ return {
+ content: processedContent,
+ reasoningContent: processedReasoningContent
+ };
+};
+
+// 处理未完成的 think 标签
+export const processIncompleteThinkTags = (content, reasoningContent = '') => {
+ const lastOpenThinkIndex = content.lastIndexOf('');
+ if (lastOpenThinkIndex === -1) {
+ return processThinkTags(content, reasoningContent);
+ }
+
+ const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
+ if (!fragmentAfterLastOpen.includes('')) {
+ const unclosedThought = fragmentAfterLastOpen.substring(''.length).trim();
+ const cleanContent = content.substring(0, lastOpenThinkIndex);
+
+ let processedReasoningContent = reasoningContent;
+ if (unclosedThought) {
+ processedReasoningContent = reasoningContent
+ ? `${reasoningContent}\n\n---\n\n${unclosedThought}`
+ : unclosedThought;
+ }
+
+ return processThinkTags(cleanContent, processedReasoningContent);
+ }
+
+ return processThinkTags(content, reasoningContent);
+};
+
+// 构建消息内容(包含图片)
+export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
+ const validImageUrls = imageUrls.filter(url => url.trim() !== '');
+
+ if (imageEnabled && validImageUrls.length > 0) {
+ return [
+ { type: 'text', text: textContent },
+ ...validImageUrls.map(url => ({
+ type: 'image_url',
+ image_url: { url: url.trim() }
+ }))
+ ];
+ }
+
+ return textContent;
+};
+
+// 创建新消息
+export const createMessage = (role, content, options = {}) => ({
+ role,
+ content,
+ createAt: Date.now(),
+ id: generateMessageId(),
+ ...options
+});
+
+// 创建加载中的助手消息
+export const createLoadingAssistantMessage = () => createMessage(
+ MESSAGE_ROLES.ASSISTANT,
+ '',
+ {
+ reasoningContent: '',
+ isReasoningExpanded: true,
+ status: 'loading'
+ }
+);
+
+// 检查消息是否包含图片
+export const hasImageContent = (message) => {
+ return Array.isArray(message.content) &&
+ message.content.some(item => item.type === 'image_url');
+};
+
+// 格式化消息用于API请求
+export const formatMessageForAPI = (message) => ({
+ role: message.role,
+ content: message.content
+});
\ No newline at end of file