♻️ refactor(helpers): standardize file naming conventions and improve code organization
- Rename files to follow camelCase naming convention: • auth-header.js → authUtils.js • other.js → logUtils.js • rehypeSplitWordsIntoSpans.js → textAnimationUtils.js - Update import paths in affected components: • Update exports in helpers/index.js • Fix import in LogsTable.js for logUtils • Fix import in MarkdownRenderer.js for textAnimationUtils - Remove old files after successful migration - Improve file naming clarity: • authUtils.js better describes authentication utilities • logUtils.js clearly indicates log processing functions • textAnimationUtils.js concisely describes text animation functionality This refactoring enhances code maintainability and follows consistent naming patterns throughout the helpers directory.
This commit is contained in:
105
web/src/helpers/apiUtils.js
Normal file
105
web/src/helpers/apiUtils.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { formatMessageForAPI } from './messageUtils';
|
||||
|
||||
// 构建API请求载荷
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages.map(formatMessageForAPI);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: 'system',
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
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;
|
||||
};
|
||||
|
||||
// 处理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;
|
||||
};
|
||||
@@ -7,4 +7,4 @@ export function authHeader() {
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
export * from './history';
|
||||
export * from './auth-header';
|
||||
export * from './authUtils';
|
||||
export * from './utils';
|
||||
export * from './api';
|
||||
export * from './apiUtils';
|
||||
export * from './messageUtils';
|
||||
export * from './textAnimationUtils';
|
||||
export * from './logUtils';
|
||||
|
||||
@@ -4,4 +4,4 @@ export function getLogOther(otherStr) {
|
||||
}
|
||||
let other = JSON.parse(otherStr);
|
||||
return other;
|
||||
}
|
||||
}
|
||||
201
web/src/helpers/messageUtils.js
Normal file
201
web/src/helpers/messageUtils.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
// 生成唯一ID
|
||||
let messageId = 4;
|
||||
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 || '';
|
||||
}
|
||||
return typeof message.content === 'string' ? message.content : '';
|
||||
};
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
const thoughts = [];
|
||||
const 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();
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
reasoningContent: processedReasoningContent
|
||||
};
|
||||
};
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
}
|
||||
|
||||
const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
|
||||
return processThinkTags(content, reasoningContent);
|
||||
};
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
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 || '' },
|
||||
...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,
|
||||
isThinkingComplete: false,
|
||||
hasAutoCollapsed: false,
|
||||
status: 'loading'
|
||||
}
|
||||
);
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
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;
|
||||
};
|
||||
77
web/src/helpers/textAnimationUtils.js
Normal file
77
web/src/helpers/textAnimationUtils.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/**
|
||||
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
||||
* 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
||||
*/
|
||||
export function rehypeSplitWordsIntoSpans(options = {}) {
|
||||
const { previousContentLength = 0 } = options;
|
||||
|
||||
return (tree) => {
|
||||
let currentCharCount = 0; // 当前已处理的字符数
|
||||
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(node.tagName) &&
|
||||
node.children
|
||||
) {
|
||||
const newChildren = [];
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'text') {
|
||||
try {
|
||||
// 使用 Intl.Segmenter 精准拆分中英文及标点
|
||||
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
||||
const segments = segmenter.segment(child.value);
|
||||
|
||||
Array.from(segments)
|
||||
.map((seg) => seg.segment)
|
||||
.filter(Boolean)
|
||||
.forEach((word) => {
|
||||
const wordStartPos = currentCharCount;
|
||||
const wordEndPos = currentCharCount + word.length;
|
||||
|
||||
// 判断这个词是否是新增的(在 previousContentLength 之后)
|
||||
const isNewContent = wordStartPos >= previousContentLength;
|
||||
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: isNewContent ? ['animate-fade-in'] : [],
|
||||
},
|
||||
children: [{ type: 'text', value: word }],
|
||||
});
|
||||
|
||||
currentCharCount = wordEndPos;
|
||||
});
|
||||
} catch (_) {
|
||||
// Fallback:如果浏览器不支持 Segmenter
|
||||
const textStartPos = currentCharCount;
|
||||
const isNewContent = textStartPos >= previousContentLength;
|
||||
|
||||
if (isNewContent) {
|
||||
// 新内容,添加动画
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: ['animate-fade-in'],
|
||||
},
|
||||
children: [{ type: 'text', value: child.value }],
|
||||
});
|
||||
} else {
|
||||
// 旧内容,不添加动画
|
||||
newChildren.push(child);
|
||||
}
|
||||
|
||||
currentCharCount += child.value.length;
|
||||
}
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
});
|
||||
node.children = newChildren;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user