✨ feat: Add custom request body editor with persistent message storage
- Add CustomRequestEditor component with JSON validation and real-time formatting - Implement bidirectional sync between chat messages and custom request body - Add persistent local storage for chat messages (separate from config) - Remove redundant System Prompt field in custom mode - Refactor configuration storage to separate messages and settings New Features: • Custom request body mode with JSON editor and syntax highlighting • Real-time bidirectional synchronization between chat UI and custom request body • Persistent message storage that survives page refresh • Enhanced configuration export/import including message data • Improved parameter organization with collapsible sections Technical Changes: • Add loadMessages/saveMessages functions in configStorage • Update usePlaygroundState hook to handle message persistence • Refactor SettingsPanel to remove System Prompt in custom mode • Add STORAGE_KEYS constants for better storage key management • Implement debounced auto-save for both config and messages • Add hash-based change detection to prevent unnecessary updates UI/UX Improvements: • Disabled state styling for parameters in custom mode • Warning banners and visual feedback for mode switching • Mobile-responsive design for custom request editor • Consistent styling with existing design system
This commit is contained in:
@@ -1,37 +1,42 @@
|
||||
import { formatMessageForAPI } from './messageUtils';
|
||||
|
||||
// 构建API请求载荷
|
||||
export const buildApiPayload = (messages, systemMessage, inputs, parameterEnabled) => {
|
||||
const formattedMessages = messages.map(formatMessageForAPI);
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages.map(formatMessageForAPI);
|
||||
|
||||
if (systemMessage) {
|
||||
formattedMessages.unshift(formatMessageForAPI(systemMessage));
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: 'system',
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
messages: formattedMessages,
|
||||
stream: inputs.stream,
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加可选参数
|
||||
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];
|
||||
}
|
||||
}
|
||||
});
|
||||
// 添加启用的参数
|
||||
if (parameterEnabled.temperature && inputs.temperature !== undefined) {
|
||||
payload.temperature = inputs.temperature;
|
||||
}
|
||||
if (parameterEnabled.top_p && inputs.top_p !== undefined) {
|
||||
payload.top_p = inputs.top_p;
|
||||
}
|
||||
if (parameterEnabled.max_tokens && inputs.max_tokens !== undefined) {
|
||||
payload.max_tokens = inputs.max_tokens;
|
||||
}
|
||||
if (parameterEnabled.frequency_penalty && inputs.frequency_penalty !== undefined) {
|
||||
payload.frequency_penalty = inputs.frequency_penalty;
|
||||
}
|
||||
if (parameterEnabled.presence_penalty && inputs.presence_penalty !== undefined) {
|
||||
payload.presence_penalty = inputs.presence_penalty;
|
||||
}
|
||||
if (parameterEnabled.seed && inputs.seed !== undefined && inputs.seed !== null) {
|
||||
payload.seed = inputs.seed;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
// 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',
|
||||
@@ -29,21 +12,42 @@ export const MESSAGE_ROLES = {
|
||||
SYSTEM: 'system',
|
||||
};
|
||||
|
||||
// 默认消息示例
|
||||
export const DEFAULT_MESSAGES = [
|
||||
{
|
||||
role: MESSAGE_ROLES.USER,
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: '你好',
|
||||
},
|
||||
{
|
||||
role: MESSAGE_ROLES.ASSISTANT,
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: '你好,请问有什么可以帮助您的吗?',
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ========== UI 相关常量 ==========
|
||||
export const DEBUG_TABS = {
|
||||
PREVIEW: 'preview',
|
||||
REQUEST: 'request',
|
||||
RESPONSE: 'response',
|
||||
};
|
||||
|
||||
// ========== API 相关常量 ==========
|
||||
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',
|
||||
model: 'gpt-4o',
|
||||
group: '',
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
@@ -65,14 +69,27 @@ export const DEFAULT_CONFIG = {
|
||||
},
|
||||
systemPrompt: '',
|
||||
showDebugPanel: false,
|
||||
customRequestMode: false,
|
||||
customRequestBody: '',
|
||||
};
|
||||
|
||||
// ========== 正则表达式 ==========
|
||||
export const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/g;
|
||||
|
||||
// ========== 错误消息 ==========
|
||||
export const ERROR_MESSAGES = {
|
||||
NO_TEXT_CONTENT: '此消息没有可复制的文本内容',
|
||||
INVALID_MESSAGE_TYPE: '无法复制此类型的消息内容',
|
||||
COPY_FAILED: '复制失败,请手动选择文本复制',
|
||||
COPY_HTTPS_REQUIRED: '复制功能需要 HTTPS 环境,请手动复制',
|
||||
BROWSER_NOT_SUPPORTED: '浏览器不支持复制功能,请手动复制',
|
||||
JSON_PARSE_ERROR: '自定义请求体格式错误,请检查JSON格式',
|
||||
API_REQUEST_ERROR: '请求发生错误',
|
||||
NETWORK_ERROR: '网络连接失败或服务器无响应',
|
||||
};
|
||||
|
||||
// ========== 存储键名 ==========
|
||||
export const STORAGE_KEYS = {
|
||||
CONFIG: 'playground_config',
|
||||
MESSAGES: 'playground_messages',
|
||||
};
|
||||
@@ -6,6 +6,8 @@ export const generateMessageId = () => `${messageId++}`;
|
||||
|
||||
// 提取消息中的文本内容
|
||||
export const getTextContent = (message) => {
|
||||
if (!message || !message.content) return '';
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
return textContent?.text || '';
|
||||
@@ -15,12 +17,12 @@ export const getTextContent = (message) => {
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content.includes('<think>')) {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
let thoughts = [];
|
||||
let replyParts = [];
|
||||
const thoughts = [];
|
||||
const replyParts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
@@ -33,14 +35,10 @@ export const processThinkTags = (content, reasoningContent = '') => {
|
||||
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;
|
||||
}
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
@@ -50,6 +48,8 @@ export const processThinkTags = (content, reasoningContent = '') => {
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
@@ -59,13 +59,9 @@ export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
|
||||
let processedReasoningContent = reasoningContent;
|
||||
if (unclosedThought) {
|
||||
processedReasoningContent = reasoningContent
|
||||
? `${reasoningContent}\n\n---\n\n${unclosedThought}`
|
||||
: unclosedThought;
|
||||
}
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
@@ -75,11 +71,15 @@ export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
const validImageUrls = imageUrls.filter(url => url.trim() !== '');
|
||||
if (!textContent && (!imageUrls || imageUrls.length === 0)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
|
||||
|
||||
if (imageEnabled && validImageUrls.length > 0) {
|
||||
return [
|
||||
{ type: 'text', text: textContent },
|
||||
{ type: 'text', text: textContent || '' },
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url: url.trim() }
|
||||
@@ -87,7 +87,7 @@ export const buildMessageContent = (textContent, imageUrls = [], imageEnabled =
|
||||
];
|
||||
}
|
||||
|
||||
return textContent;
|
||||
return textContent || '';
|
||||
};
|
||||
|
||||
// 创建新消息
|
||||
@@ -114,12 +114,88 @@ export const createLoadingAssistantMessage = () => createMessage(
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return Array.isArray(message.content) &&
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
export const formatMessageForAPI = (message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
});
|
||||
export const formatMessageForAPI = (message) => {
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
};
|
||||
};
|
||||
|
||||
// 验证消息是否有效
|
||||
export const isValidMessage = (message) => {
|
||||
return message &&
|
||||
message.role &&
|
||||
(message.content || message.content === '');
|
||||
};
|
||||
|
||||
// 获取最后一条用户消息
|
||||
export const getLastUserMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取最后一条助手消息
|
||||
export const getLastAssistantMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 构建API请求负载(从apiUtils移动过来)
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages
|
||||
.filter(isValidMessage)
|
||||
.map(formatMessageForAPI)
|
||||
.filter(Boolean);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: MESSAGE_ROLES.SYSTEM,
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
const parameterMappings = {
|
||||
temperature: 'temperature',
|
||||
top_p: 'top_p',
|
||||
max_tokens: 'max_tokens',
|
||||
frequency_penalty: 'frequency_penalty',
|
||||
presence_penalty: 'presence_penalty',
|
||||
seed: 'seed'
|
||||
};
|
||||
|
||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
};
|
||||
Reference in New Issue
Block a user