🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)
- Ran: bun run eslint:fix && bun run lint:fix - Inserted AGPL license header via eslint-plugin-header - Enforced no-multiple-empty-lines and other lint rules - Formatted code using Prettier v3 (@so1ve/prettier-config) - No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
@@ -25,9 +25,13 @@ import {
|
||||
showInfo,
|
||||
showSuccess,
|
||||
loadChannelModels,
|
||||
copy
|
||||
copy,
|
||||
} from '../../helpers';
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants';
|
||||
import {
|
||||
CHANNEL_OPTIONS,
|
||||
ITEMS_PER_PAGE,
|
||||
MODEL_TABLE_PAGE_SIZE,
|
||||
} from '../../constants';
|
||||
import { useIsMobile } from '../common/useIsMobile';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
@@ -64,7 +68,7 @@ export const useChannelsData = () => {
|
||||
|
||||
// Status filter
|
||||
const [statusFilter, setStatusFilter] = useState(
|
||||
localStorage.getItem('channel-status-filter') || 'all'
|
||||
localStorage.getItem('channel-status-filter') || 'all',
|
||||
);
|
||||
|
||||
// Type tabs states
|
||||
@@ -115,9 +119,12 @@ export const useChannelsData = () => {
|
||||
// Initialize from localStorage
|
||||
useEffect(() => {
|
||||
const localIdSort = localStorage.getItem('id-sort') === 'true';
|
||||
const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
|
||||
const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true';
|
||||
const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true';
|
||||
const localPageSize =
|
||||
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
|
||||
const localEnableTagMode =
|
||||
localStorage.getItem('enable-tag-mode') === 'true';
|
||||
const localEnableBatchDelete =
|
||||
localStorage.getItem('enable-batch-delete') === 'true';
|
||||
|
||||
setIdSort(localIdSort);
|
||||
setPageSize(localPageSize);
|
||||
@@ -175,7 +182,10 @@ export const useChannelsData = () => {
|
||||
// Save column preferences
|
||||
useEffect(() => {
|
||||
if (Object.keys(visibleColumns).length > 0) {
|
||||
localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns));
|
||||
localStorage.setItem(
|
||||
'channels-table-columns',
|
||||
JSON.stringify(visibleColumns),
|
||||
);
|
||||
}
|
||||
}, [visibleColumns]);
|
||||
|
||||
@@ -289,14 +299,21 @@ export const useChannelsData = () => {
|
||||
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||
if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') {
|
||||
setLoading(true);
|
||||
await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort);
|
||||
await searchChannels(
|
||||
enableTagMode,
|
||||
typeKey,
|
||||
statusF,
|
||||
page,
|
||||
pageSize,
|
||||
idSort,
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reqId = ++requestCounter.current;
|
||||
setLoading(true);
|
||||
const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
|
||||
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
|
||||
const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
|
||||
const res = await API.get(
|
||||
`/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
|
||||
@@ -310,7 +327,10 @@ export const useChannelsData = () => {
|
||||
if (success) {
|
||||
const { items, total, type_counts } = data;
|
||||
if (type_counts) {
|
||||
const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
|
||||
const sumAll = Object.values(type_counts).reduce(
|
||||
(acc, v) => acc + v,
|
||||
0,
|
||||
);
|
||||
setTypeCounts({ ...type_counts, all: sumAll });
|
||||
}
|
||||
setChannelFormat(items, enableTagMode);
|
||||
@@ -334,11 +354,18 @@ export const useChannelsData = () => {
|
||||
setSearching(true);
|
||||
try {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF);
|
||||
await loadChannels(
|
||||
page,
|
||||
pageSz,
|
||||
sortFlag,
|
||||
enableTagMode,
|
||||
typeKey,
|
||||
statusF,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
|
||||
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
|
||||
const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
|
||||
const res = await API.get(
|
||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`,
|
||||
@@ -346,7 +373,10 @@ export const useChannelsData = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
const { items = [], total = 0, type_counts = {} } = data;
|
||||
const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
|
||||
const sumAll = Object.values(type_counts).reduce(
|
||||
(acc, v) => acc + v,
|
||||
0,
|
||||
);
|
||||
setTypeCounts({ ...type_counts, all: sumAll });
|
||||
setChannelFormat(items, enableTagMode);
|
||||
setChannelCount(total);
|
||||
@@ -365,7 +395,14 @@ export const useChannelsData = () => {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(page, pageSize, idSort, enableTagMode);
|
||||
} else {
|
||||
await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
|
||||
await searchChannels(
|
||||
enableTagMode,
|
||||
activeTypeKey,
|
||||
statusFilter,
|
||||
page,
|
||||
pageSize,
|
||||
idSort,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,9 +488,16 @@ export const useChannelsData = () => {
|
||||
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||
setActivePage(page);
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
loadChannels(page, pageSize, idSort, enableTagMode).then(() => { });
|
||||
loadChannels(page, pageSize, idSort, enableTagMode).then(() => {});
|
||||
} else {
|
||||
searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
|
||||
searchChannels(
|
||||
enableTagMode,
|
||||
activeTypeKey,
|
||||
statusFilter,
|
||||
page,
|
||||
pageSize,
|
||||
idSort,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -469,7 +513,14 @@ export const useChannelsData = () => {
|
||||
showError(reason);
|
||||
});
|
||||
} else {
|
||||
searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort);
|
||||
searchChannels(
|
||||
enableTagMode,
|
||||
activeTypeKey,
|
||||
statusFilter,
|
||||
1,
|
||||
size,
|
||||
idSort,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -500,7 +551,10 @@ export const useChannelsData = () => {
|
||||
showError(res?.data?.message || t('渠道复制失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error));
|
||||
showError(
|
||||
t('渠道复制失败: ') +
|
||||
(error?.response?.data?.message || error?.message || error),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -539,7 +593,11 @@ export const useChannelsData = () => {
|
||||
data.priority = parseInt(data.priority);
|
||||
break;
|
||||
case 'weight':
|
||||
if (data.weight === undefined || data.weight < 0 || data.weight === '') {
|
||||
if (
|
||||
data.weight === undefined ||
|
||||
data.weight < 0 ||
|
||||
data.weight === ''
|
||||
) {
|
||||
showInfo('权重必须是非负整数!');
|
||||
return;
|
||||
}
|
||||
@@ -682,7 +740,11 @@ export const useChannelsData = () => {
|
||||
const res = await API.post(`/api/channel/fix`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails));
|
||||
showSuccess(
|
||||
t('已修复 ${success} 个通道,失败 ${fails} 个通道。')
|
||||
.replace('${success}', data.success)
|
||||
.replace('${fails}', data.fails),
|
||||
);
|
||||
await refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -691,7 +753,7 @@ export const useChannelsData = () => {
|
||||
|
||||
// Test channel
|
||||
const testChannel = async (record, model) => {
|
||||
setTestQueue(prev => [...prev, { channel: record, model }]);
|
||||
setTestQueue((prev) => [...prev, { channel: record, model }]);
|
||||
if (!isProcessingQueue) {
|
||||
setIsProcessingQueue(true);
|
||||
}
|
||||
@@ -710,21 +772,28 @@ export const useChannelsData = () => {
|
||||
} else {
|
||||
const filteredModelsList = currentTestChannel.models
|
||||
.split(',')
|
||||
.filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()));
|
||||
.filter((m) =>
|
||||
m.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
|
||||
);
|
||||
const modelIdx = filteredModelsList.indexOf(model);
|
||||
pageNo = modelIdx !== -1 ? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1 : 1;
|
||||
pageNo =
|
||||
modelIdx !== -1
|
||||
? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1
|
||||
: 1;
|
||||
}
|
||||
setModelTablePage(pageNo);
|
||||
}
|
||||
|
||||
try {
|
||||
setTestingModels(prev => new Set([...prev, model]));
|
||||
const res = await API.get(`/api/channel/test/${channel.id}?model=${model}`);
|
||||
setTestingModels((prev) => new Set([...prev, model]));
|
||||
const res = await API.get(
|
||||
`/api/channel/test/${channel.id}?model=${model}`,
|
||||
);
|
||||
const { success, message, time } = res.data;
|
||||
|
||||
setModelTestResults(prev => ({
|
||||
setModelTestResults((prev) => ({
|
||||
...prev,
|
||||
[`${channel.id}-${model}`]: { success, time }
|
||||
[`${channel.id}-${model}`]: { success, time },
|
||||
}));
|
||||
|
||||
if (success) {
|
||||
@@ -745,14 +814,14 @@ export const useChannelsData = () => {
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
} finally {
|
||||
setTestingModels(prev => {
|
||||
setTestingModels((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(model);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
setTestQueue(prev => prev.slice(1));
|
||||
setTestQueue((prev) => prev.slice(1));
|
||||
};
|
||||
|
||||
// Monitor queue changes
|
||||
@@ -943,4 +1012,4 @@ export const useChannelsData = () => {
|
||||
setCompactMode,
|
||||
setActivePage,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -46,4 +46,4 @@ export function useTokenKeys(id) {
|
||||
}, []);
|
||||
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useContainerWidth = () => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
const { width: newWidth } = entry.contentRect;
|
||||
setWidth(newWidth);
|
||||
|
||||
@@ -109,16 +109,25 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
navigate('/login');
|
||||
}, [navigate, t, userDispatch]);
|
||||
|
||||
const handleLanguageChange = useCallback((lang) => {
|
||||
i18n.changeLanguage(lang);
|
||||
}, [i18n]);
|
||||
const handleLanguageChange = useCallback(
|
||||
(lang) => {
|
||||
i18n.changeLanguage(lang);
|
||||
},
|
||||
[i18n],
|
||||
);
|
||||
|
||||
const handleThemeToggle = useCallback((newTheme) => {
|
||||
if (!newTheme || (newTheme !== 'light' && newTheme !== 'dark' && newTheme !== 'auto')) {
|
||||
return;
|
||||
}
|
||||
setTheme(newTheme);
|
||||
}, [setTheme]);
|
||||
const handleThemeToggle = useCallback(
|
||||
(newTheme) => {
|
||||
if (
|
||||
!newTheme ||
|
||||
(newTheme !== 'light' && newTheme !== 'dark' && newTheme !== 'auto')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setTheme(newTheme);
|
||||
},
|
||||
[setTheme],
|
||||
);
|
||||
|
||||
const handleMobileMenuToggle = useCallback(() => {
|
||||
if (isMobile) {
|
||||
|
||||
@@ -32,4 +32,4 @@ export const useIsMobile = () => {
|
||||
() => window.matchMedia(query).matches,
|
||||
() => false,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -47,4 +47,4 @@ export const useMinimumLoadingTime = (loading, minimumTime = 1000) => {
|
||||
}, [loading, minimumTime]);
|
||||
|
||||
return showSkeleton;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,38 +20,41 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNavigation = (t, docsLink) => {
|
||||
const mainNavLinks = useMemo(() => [
|
||||
{
|
||||
text: t('首页'),
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: t('控制台'),
|
||||
itemKey: 'console',
|
||||
to: '/console',
|
||||
},
|
||||
{
|
||||
text: t('模型广场'),
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
},
|
||||
...(docsLink
|
||||
? [
|
||||
{
|
||||
text: t('文档'),
|
||||
itemKey: 'docs',
|
||||
isExternal: true,
|
||||
externalLink: docsLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: t('关于'),
|
||||
itemKey: 'about',
|
||||
to: '/about',
|
||||
},
|
||||
], [t, docsLink]);
|
||||
const mainNavLinks = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('首页'),
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: t('控制台'),
|
||||
itemKey: 'console',
|
||||
to: '/console',
|
||||
},
|
||||
{
|
||||
text: t('模型广场'),
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
},
|
||||
...(docsLink
|
||||
? [
|
||||
{
|
||||
text: t('文档'),
|
||||
itemKey: 'docs',
|
||||
isExternal: true,
|
||||
externalLink: docsLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: t('关于'),
|
||||
itemKey: 'about',
|
||||
to: '/about',
|
||||
},
|
||||
],
|
||||
[t, docsLink],
|
||||
);
|
||||
|
||||
return {
|
||||
mainNavLinks,
|
||||
|
||||
@@ -26,7 +26,8 @@ export const useNotifications = (statusState) => {
|
||||
const announcements = statusState?.status?.announcements || [];
|
||||
|
||||
// Helper functions
|
||||
const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
|
||||
const getAnnouncementKey = (a) =>
|
||||
`${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
|
||||
|
||||
const calculateUnreadCount = () => {
|
||||
if (!announcements.length) return 0;
|
||||
@@ -37,7 +38,8 @@ export const useNotifications = (statusState) => {
|
||||
readKeys = [];
|
||||
}
|
||||
const readSet = new Set(readKeys);
|
||||
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length;
|
||||
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a)))
|
||||
.length;
|
||||
};
|
||||
|
||||
const getUnreadKeys = () => {
|
||||
@@ -49,7 +51,9 @@ export const useNotifications = (statusState) => {
|
||||
readKeys = [];
|
||||
}
|
||||
const readSet = new Set(readKeys);
|
||||
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey);
|
||||
return announcements
|
||||
.filter((a) => !readSet.has(getAnnouncementKey(a)))
|
||||
.map(getAnnouncementKey);
|
||||
};
|
||||
|
||||
// Effects
|
||||
@@ -71,7 +75,9 @@ export const useNotifications = (statusState) => {
|
||||
} catch (_) {
|
||||
readKeys = [];
|
||||
}
|
||||
const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)]));
|
||||
const mergedKeys = Array.from(
|
||||
new Set([...readKeys, ...announcements.map(getAnnouncementKey)]),
|
||||
);
|
||||
localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys));
|
||||
}
|
||||
setUnreadCount(0);
|
||||
|
||||
@@ -22,10 +22,12 @@ import { useState, useCallback } from 'react';
|
||||
const KEY = 'default_collapse_sidebar';
|
||||
|
||||
export const useSidebarCollapsed = () => {
|
||||
const [collapsed, setCollapsed] = useState(() => localStorage.getItem(KEY) === 'true');
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
() => localStorage.getItem(KEY) === 'true',
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setCollapsed(prev => {
|
||||
setCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
localStorage.setItem(KEY, next.toString());
|
||||
return next;
|
||||
@@ -38,4 +40,4 @@ export const useSidebarCollapsed = () => {
|
||||
}, []);
|
||||
|
||||
return [collapsed, toggle, set];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,27 +27,32 @@ import { TABLE_COMPACT_MODES_KEY } from '../../constants';
|
||||
* 内部使用 localStorage 保存状态,并监听 storage 事件保持多标签页同步。
|
||||
*/
|
||||
export function useTableCompactMode(tableKey = 'global') {
|
||||
const [compactMode, setCompactModeState] = useState(() => getTableCompactMode(tableKey));
|
||||
const [compactMode, setCompactModeState] = useState(() =>
|
||||
getTableCompactMode(tableKey),
|
||||
);
|
||||
|
||||
const setCompactMode = useCallback((value) => {
|
||||
setCompactModeState(value);
|
||||
setTableCompactMode(value, tableKey);
|
||||
}, [tableKey]);
|
||||
const setCompactMode = useCallback(
|
||||
(value) => {
|
||||
setCompactModeState(value);
|
||||
setTableCompactMode(value, tableKey);
|
||||
},
|
||||
[tableKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorage = (e) => {
|
||||
if (e.key === TABLE_COMPACT_MODES_KEY) {
|
||||
try {
|
||||
const modes = JSON.parse(e.newValue || '{}');
|
||||
setCompactModeState(!!modes[tableKey]);
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, [tableKey]);
|
||||
useEffect(() => {
|
||||
const handleStorage = (e) => {
|
||||
if (e.key === TABLE_COMPACT_MODES_KEY) {
|
||||
try {
|
||||
const modes = JSON.parse(e.newValue || '{}');
|
||||
setCompactModeState(!!modes[tableKey]);
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, [tableKey]);
|
||||
|
||||
return [compactMode, setCompactMode];
|
||||
}
|
||||
return [compactMode, setCompactMode];
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
modelToColor,
|
||||
getQuotaWithUnit
|
||||
getQuotaWithUnit,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
processRawData,
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
generateChartTimePoints,
|
||||
updateChartSpec,
|
||||
updateMapValue,
|
||||
initializeMaps
|
||||
initializeMaps,
|
||||
} from '../../helpers/dashboard';
|
||||
|
||||
export const useDashboardCharts = (
|
||||
@@ -45,7 +45,7 @@ export const useDashboardCharts = (
|
||||
setPieData,
|
||||
setLineData,
|
||||
setModelColors,
|
||||
t
|
||||
t,
|
||||
) => {
|
||||
// ========== 图表规格状态 ==========
|
||||
const [spec_pie, setSpecPie] = useState({
|
||||
@@ -271,150 +271,160 @@ export const useDashboardCharts = (
|
||||
return newModelColors;
|
||||
}, []);
|
||||
|
||||
const updateChartData = useCallback((data) => {
|
||||
const processedData = processRawData(
|
||||
data,
|
||||
const updateChartData = useCallback(
|
||||
(data) => {
|
||||
const processedData = processRawData(
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
initializeMaps,
|
||||
updateMapValue,
|
||||
);
|
||||
|
||||
const {
|
||||
totalQuota,
|
||||
totalTimes,
|
||||
totalTokens,
|
||||
uniqueModels,
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap,
|
||||
} = processedData;
|
||||
|
||||
const trendDataResult = calculateTrendData(
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
setTrendData(trendDataResult);
|
||||
|
||||
const newModelColors = generateModelColors(uniqueModels, {});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
const aggregatedData = aggregateDataByTimeAndModel(
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
|
||||
const modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
updateMapValue(modelTotals, value.model, value.count);
|
||||
}
|
||||
|
||||
const newPieData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count,
|
||||
}))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const chartTimePoints = generateChartTimePoints(
|
||||
aggregatedData,
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
|
||||
let newLineData = [];
|
||||
|
||||
chartTimePoints.forEach((time) => {
|
||||
let timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota
|
||||
? getQuotaWithUnit(aggregated.quota, 4)
|
||||
: 0,
|
||||
};
|
||||
});
|
||||
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }));
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
updateChartSpec(
|
||||
setSpecPie,
|
||||
newPieData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'id0',
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecLine,
|
||||
newLineData,
|
||||
`${t('总计')}:${renderQuota(totalQuota, 2)}`,
|
||||
newModelColors,
|
||||
'barData',
|
||||
);
|
||||
|
||||
// ===== 模型调用次数折线图 =====
|
||||
let modelLineData = [];
|
||||
chartTimePoints.forEach((time) => {
|
||||
const timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Count: aggregated?.count || 0,
|
||||
};
|
||||
});
|
||||
modelLineData.push(...timeData);
|
||||
});
|
||||
modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// ===== 模型调用次数排行柱状图 =====
|
||||
const rankData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
Model: model,
|
||||
Count: count,
|
||||
}))
|
||||
.sort((a, b) => b.Count - a.Count);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecModelLine,
|
||||
modelLineData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'lineData',
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecRankBar,
|
||||
rankData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'rankData',
|
||||
);
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
setTimes(totalTimes);
|
||||
setConsumeTokens(totalTokens);
|
||||
},
|
||||
[
|
||||
dataExportDefaultTime,
|
||||
initializeMaps,
|
||||
updateMapValue
|
||||
);
|
||||
|
||||
const {
|
||||
totalQuota,
|
||||
totalTimes,
|
||||
totalTokens,
|
||||
uniqueModels,
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap
|
||||
} = processedData;
|
||||
|
||||
const trendDataResult = calculateTrendData(
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap,
|
||||
dataExportDefaultTime
|
||||
);
|
||||
setTrendData(trendDataResult);
|
||||
|
||||
const newModelColors = generateModelColors(uniqueModels, {});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
const aggregatedData = aggregateDataByTimeAndModel(data, dataExportDefaultTime);
|
||||
|
||||
const modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
updateMapValue(modelTotals, value.model, value.count);
|
||||
}
|
||||
|
||||
const newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count,
|
||||
})).sort((a, b) => b.value - a.value);
|
||||
|
||||
const chartTimePoints = generateChartTimePoints(
|
||||
aggregatedData,
|
||||
data,
|
||||
dataExportDefaultTime
|
||||
);
|
||||
|
||||
let newLineData = [];
|
||||
|
||||
chartTimePoints.forEach((time) => {
|
||||
let timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }));
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
updateChartSpec(
|
||||
setSpecPie,
|
||||
newPieData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'id0'
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecLine,
|
||||
newLineData,
|
||||
`${t('总计')}:${renderQuota(totalQuota, 2)}`,
|
||||
newModelColors,
|
||||
'barData'
|
||||
);
|
||||
|
||||
// ===== 模型调用次数折线图 =====
|
||||
let modelLineData = [];
|
||||
chartTimePoints.forEach((time) => {
|
||||
const timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Count: aggregated?.count || 0,
|
||||
};
|
||||
});
|
||||
modelLineData.push(...timeData);
|
||||
});
|
||||
modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// ===== 模型调用次数排行柱状图 =====
|
||||
const rankData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
Model: model,
|
||||
Count: count,
|
||||
}))
|
||||
.sort((a, b) => b.Count - a.Count);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecModelLine,
|
||||
modelLineData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'lineData'
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecRankBar,
|
||||
rankData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'rankData'
|
||||
);
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
setTimes(totalTimes);
|
||||
setConsumeTokens(totalTokens);
|
||||
}, [
|
||||
dataExportDefaultTime,
|
||||
setTrendData,
|
||||
generateModelColors,
|
||||
setModelColors,
|
||||
setPieData,
|
||||
setLineData,
|
||||
setConsumeQuota,
|
||||
setTimes,
|
||||
setConsumeTokens,
|
||||
t
|
||||
]);
|
||||
setTrendData,
|
||||
generateModelColors,
|
||||
setModelColors,
|
||||
setPieData,
|
||||
setLineData,
|
||||
setConsumeQuota,
|
||||
setTimes,
|
||||
setConsumeTokens,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
// ========== 初始化图表主题 ==========
|
||||
useEffect(() => {
|
||||
@@ -432,6 +442,6 @@ export const useDashboardCharts = (
|
||||
|
||||
// 函数
|
||||
updateChartData,
|
||||
generateModelColors
|
||||
generateModelColors,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,7 +49,8 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
data_export_default_time: '',
|
||||
});
|
||||
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime());
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] =
|
||||
useState(getDefaultTime());
|
||||
|
||||
// ========== 数据状态 ==========
|
||||
const [quotaData, setQuotaData] = useState([]);
|
||||
@@ -72,7 +73,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
consumeQuota: [],
|
||||
tokens: [],
|
||||
rpm: [],
|
||||
tpm: []
|
||||
tpm: [],
|
||||
});
|
||||
|
||||
// ========== Uptime 数据 ==========
|
||||
@@ -86,7 +87,8 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
|
||||
// ========== Panel enable flags ==========
|
||||
const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
|
||||
const announcementsEnabled = statusState?.status?.announcements_enabled ?? true;
|
||||
const announcementsEnabled =
|
||||
statusState?.status?.announcements_enabled ?? true;
|
||||
const faqEnabled = statusState?.status?.faq_enabled ?? true;
|
||||
const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
|
||||
|
||||
@@ -94,16 +96,25 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
|
||||
|
||||
// ========== Memoized Values ==========
|
||||
const timeOptions = useMemo(() => TIME_OPTIONS.map(option => ({
|
||||
...option,
|
||||
label: t(option.label)
|
||||
})), [t]);
|
||||
const timeOptions = useMemo(
|
||||
() =>
|
||||
TIME_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
label: t(option.label),
|
||||
})),
|
||||
[t],
|
||||
);
|
||||
|
||||
const performanceMetrics = useMemo(() => {
|
||||
const { start_timestamp, end_timestamp } = inputs;
|
||||
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
|
||||
const avgRPM = isNaN(times / timeDiff) ? '0' : (times / timeDiff).toFixed(3);
|
||||
const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
|
||||
const timeDiff =
|
||||
(Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
|
||||
const avgRPM = isNaN(times / timeDiff)
|
||||
? '0'
|
||||
: (times / timeDiff).toFixed(3);
|
||||
const avgTPM = isNaN(consumeTokens / timeDiff)
|
||||
? '0'
|
||||
: (consumeTokens / timeDiff).toFixed(3);
|
||||
|
||||
return { avgRPM, avgTPM, timeDiff };
|
||||
}, [times, consumeTokens, inputs.start_timestamp, inputs.end_timestamp]);
|
||||
@@ -218,13 +229,16 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
return data;
|
||||
}, [loadQuotaData, loadUptimeData]);
|
||||
|
||||
const handleSearchConfirm = useCallback(async (updateChartDataCallback) => {
|
||||
const data = await refresh();
|
||||
if (data && data.length > 0 && updateChartDataCallback) {
|
||||
updateChartDataCallback(data);
|
||||
}
|
||||
setSearchModalVisible(false);
|
||||
}, [refresh]);
|
||||
const handleSearchConfirm = useCallback(
|
||||
async (updateChartDataCallback) => {
|
||||
const data = await refresh();
|
||||
if (data && data.length > 0 && updateChartDataCallback) {
|
||||
updateChartDataCallback(data);
|
||||
}
|
||||
setSearchModalVisible(false);
|
||||
},
|
||||
[refresh],
|
||||
);
|
||||
|
||||
// ========== Effects ==========
|
||||
useEffect(() => {
|
||||
@@ -305,6 +319,6 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
// 导航和翻译
|
||||
navigate,
|
||||
t,
|
||||
isMobile
|
||||
isMobile,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
IconPulse,
|
||||
IconStopwatchStroked,
|
||||
IconTypograph,
|
||||
IconSend
|
||||
IconSend,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { renderQuota } from '../../helpers';
|
||||
import { createSectionTitle } from '../../helpers/dashboard';
|
||||
@@ -40,111 +40,114 @@ export const useDashboardStats = (
|
||||
trendData,
|
||||
performanceMetrics,
|
||||
navigate,
|
||||
t
|
||||
t,
|
||||
) => {
|
||||
const groupedStatsData = useMemo(() => [
|
||||
{
|
||||
title: createSectionTitle(Wallet, t('账户数据')),
|
||||
color: 'bg-blue-50',
|
||||
items: [
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked />,
|
||||
avatarColor: 'blue',
|
||||
trendData: [],
|
||||
trendColor: '#3b82f6'
|
||||
},
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram />,
|
||||
avatarColor: 'purple',
|
||||
trendData: [],
|
||||
trendColor: '#8b5cf6'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Activity, t('使用统计')),
|
||||
color: 'bg-green-50',
|
||||
items: [
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconSend />,
|
||||
avatarColor: 'green',
|
||||
trendData: [],
|
||||
trendColor: '#10b981'
|
||||
},
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse />,
|
||||
avatarColor: 'cyan',
|
||||
trendData: trendData.times,
|
||||
trendColor: '#06b6d4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Zap, t('资源消耗')),
|
||||
color: 'bg-yellow-50',
|
||||
items: [
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked />,
|
||||
avatarColor: 'yellow',
|
||||
trendData: trendData.consumeQuota,
|
||||
trendColor: '#f59e0b'
|
||||
},
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||
icon: <IconTextStroked />,
|
||||
avatarColor: 'pink',
|
||||
trendData: trendData.tokens,
|
||||
trendColor: '#ec4899'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Gauge, t('性能指标')),
|
||||
color: 'bg-indigo-50',
|
||||
items: [
|
||||
{
|
||||
title: t('平均RPM'),
|
||||
value: performanceMetrics.avgRPM,
|
||||
icon: <IconStopwatchStroked />,
|
||||
avatarColor: 'indigo',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#6366f1'
|
||||
},
|
||||
{
|
||||
title: t('平均TPM'),
|
||||
value: performanceMetrics.avgTPM,
|
||||
icon: <IconTypograph />,
|
||||
avatarColor: 'orange',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#f97316'
|
||||
}
|
||||
]
|
||||
}
|
||||
], [
|
||||
userState?.user?.quota,
|
||||
userState?.user?.used_quota,
|
||||
userState?.user?.request_count,
|
||||
times,
|
||||
consumeQuota,
|
||||
consumeTokens,
|
||||
trendData,
|
||||
performanceMetrics,
|
||||
navigate,
|
||||
t
|
||||
]);
|
||||
const groupedStatsData = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: createSectionTitle(Wallet, t('账户数据')),
|
||||
color: 'bg-blue-50',
|
||||
items: [
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked />,
|
||||
avatarColor: 'blue',
|
||||
trendData: [],
|
||||
trendColor: '#3b82f6',
|
||||
},
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram />,
|
||||
avatarColor: 'purple',
|
||||
trendData: [],
|
||||
trendColor: '#8b5cf6',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Activity, t('使用统计')),
|
||||
color: 'bg-green-50',
|
||||
items: [
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconSend />,
|
||||
avatarColor: 'green',
|
||||
trendData: [],
|
||||
trendColor: '#10b981',
|
||||
},
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse />,
|
||||
avatarColor: 'cyan',
|
||||
trendData: trendData.times,
|
||||
trendColor: '#06b6d4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Zap, t('资源消耗')),
|
||||
color: 'bg-yellow-50',
|
||||
items: [
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked />,
|
||||
avatarColor: 'yellow',
|
||||
trendData: trendData.consumeQuota,
|
||||
trendColor: '#f59e0b',
|
||||
},
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||
icon: <IconTextStroked />,
|
||||
avatarColor: 'pink',
|
||||
trendData: trendData.tokens,
|
||||
trendColor: '#ec4899',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Gauge, t('性能指标')),
|
||||
color: 'bg-indigo-50',
|
||||
items: [
|
||||
{
|
||||
title: t('平均RPM'),
|
||||
value: performanceMetrics.avgRPM,
|
||||
icon: <IconStopwatchStroked />,
|
||||
avatarColor: 'indigo',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#6366f1',
|
||||
},
|
||||
{
|
||||
title: t('平均TPM'),
|
||||
value: performanceMetrics.avgTPM,
|
||||
icon: <IconTypograph />,
|
||||
avatarColor: 'orange',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#f97316',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
userState?.user?.quota,
|
||||
userState?.user?.used_quota,
|
||||
userState?.user?.request_count,
|
||||
times,
|
||||
consumeQuota,
|
||||
consumeTokens,
|
||||
trendData,
|
||||
performanceMetrics,
|
||||
navigate,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
groupedStatsData
|
||||
groupedStatsData,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
isAdmin,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string
|
||||
timestamp2string,
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -61,7 +61,9 @@ export const useMjLogsData = () => {
|
||||
// User and admin
|
||||
const isAdminUser = isAdmin();
|
||||
// Role-specific storage key to prevent different roles from overwriting each other
|
||||
const STORAGE_KEY = isAdminUser ? 'mj-logs-table-columns-admin' : 'mj-logs-table-columns-user';
|
||||
const STORAGE_KEY = isAdminUser
|
||||
? 'mj-logs-table-columns-admin'
|
||||
: 'mj-logs-table-columns-user';
|
||||
|
||||
// Modal states
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -77,7 +79,7 @@ export const useMjLogsData = () => {
|
||||
mj_id: '',
|
||||
dateRange: [
|
||||
timestamp2string(now.getTime() / 1000 - 2592000),
|
||||
timestamp2string(now.getTime() / 1000 + 3600)
|
||||
timestamp2string(now.getTime() / 1000 + 3600),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -222,7 +224,8 @@ export const useMjLogsData = () => {
|
||||
// Load logs function
|
||||
const loadLogs = async (page = 1, size = pageSize) => {
|
||||
setLoading(true);
|
||||
const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
|
||||
const { channel_id, mj_id, start_timestamp, end_timestamp } =
|
||||
getFormValues();
|
||||
let localStartTimestamp = Date.parse(start_timestamp);
|
||||
let localEndTimestamp = Date.parse(end_timestamp);
|
||||
const url = isAdminUser
|
||||
@@ -275,7 +278,8 @@ export const useMjLogsData = () => {
|
||||
|
||||
// Initialize data
|
||||
useEffect(() => {
|
||||
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
|
||||
const localPageSize =
|
||||
parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
|
||||
setPageSize(localPageSize);
|
||||
loadLogs(1, localPageSize).then();
|
||||
}, []);
|
||||
@@ -331,4 +335,4 @@ export const useMjLogsData = () => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,48 +56,57 @@ export const useModelPricingData = () => {
|
||||
const [userState] = useContext(UserContext);
|
||||
|
||||
// 充值汇率(price)与美元兑人民币汇率(usd_exchange_rate)
|
||||
const priceRate = useMemo(() => statusState?.status?.price ?? 1, [statusState]);
|
||||
const usdExchangeRate = useMemo(() => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate]);
|
||||
const priceRate = useMemo(
|
||||
() => statusState?.status?.price ?? 1,
|
||||
[statusState],
|
||||
);
|
||||
const usdExchangeRate = useMemo(
|
||||
() => statusState?.status?.usd_exchange_rate ?? priceRate,
|
||||
[statusState, priceRate],
|
||||
);
|
||||
|
||||
const filteredModels = useMemo(() => {
|
||||
let result = models;
|
||||
|
||||
// 分组筛选
|
||||
if (filterGroup !== 'all') {
|
||||
result = result.filter(model => model.enable_groups.includes(filterGroup));
|
||||
result = result.filter((model) =>
|
||||
model.enable_groups.includes(filterGroup),
|
||||
);
|
||||
}
|
||||
|
||||
// 计费类型筛选
|
||||
if (filterQuotaType !== 'all') {
|
||||
result = result.filter(model => model.quota_type === filterQuotaType);
|
||||
result = result.filter((model) => model.quota_type === filterQuotaType);
|
||||
}
|
||||
|
||||
// 端点类型筛选
|
||||
if (filterEndpointType !== 'all') {
|
||||
result = result.filter(model =>
|
||||
model.supported_endpoint_types &&
|
||||
model.supported_endpoint_types.includes(filterEndpointType)
|
||||
result = result.filter(
|
||||
(model) =>
|
||||
model.supported_endpoint_types &&
|
||||
model.supported_endpoint_types.includes(filterEndpointType),
|
||||
);
|
||||
}
|
||||
|
||||
// 供应商筛选
|
||||
if (filterVendor !== 'all') {
|
||||
if (filterVendor === 'unknown') {
|
||||
result = result.filter(model => !model.vendor_name);
|
||||
result = result.filter((model) => !model.vendor_name);
|
||||
} else {
|
||||
result = result.filter(model => model.vendor_name === filterVendor);
|
||||
result = result.filter((model) => model.vendor_name === filterVendor);
|
||||
}
|
||||
}
|
||||
|
||||
// 标签筛选
|
||||
if (filterTag !== 'all') {
|
||||
const tagLower = filterTag.toLowerCase();
|
||||
result = result.filter(model => {
|
||||
result = result.filter((model) => {
|
||||
if (!model.tags) return false;
|
||||
const tagsArr = model.tags
|
||||
.toLowerCase()
|
||||
.split(/[,;|\s]+/)
|
||||
.map(tag => tag.trim())
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
return tagsArr.includes(tagLower);
|
||||
});
|
||||
@@ -106,16 +115,28 @@ export const useModelPricingData = () => {
|
||||
// 搜索筛选
|
||||
if (searchValue.length > 0) {
|
||||
const searchTerm = searchValue.toLowerCase();
|
||||
result = result.filter(model =>
|
||||
(model.model_name && model.model_name.toLowerCase().includes(searchTerm)) ||
|
||||
(model.description && model.description.toLowerCase().includes(searchTerm)) ||
|
||||
(model.tags && model.tags.toLowerCase().includes(searchTerm)) ||
|
||||
(model.vendor_name && model.vendor_name.toLowerCase().includes(searchTerm))
|
||||
result = result.filter(
|
||||
(model) =>
|
||||
(model.model_name &&
|
||||
model.model_name.toLowerCase().includes(searchTerm)) ||
|
||||
(model.description &&
|
||||
model.description.toLowerCase().includes(searchTerm)) ||
|
||||
(model.tags && model.tags.toLowerCase().includes(searchTerm)) ||
|
||||
(model.vendor_name &&
|
||||
model.vendor_name.toLowerCase().includes(searchTerm)),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [models, searchValue, filterGroup, filterQuotaType, filterEndpointType, filterVendor, filterTag]);
|
||||
}, [
|
||||
models,
|
||||
searchValue,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
]);
|
||||
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
@@ -130,7 +151,7 @@ export const useModelPricingData = () => {
|
||||
const displayPrice = (usdPrice) => {
|
||||
let priceInUSD = usdPrice;
|
||||
if (showWithRecharge) {
|
||||
priceInUSD = usdPrice * priceRate / usdExchangeRate;
|
||||
priceInUSD = (usdPrice * priceRate) / usdExchangeRate;
|
||||
}
|
||||
|
||||
if (currency === 'CNY') {
|
||||
@@ -176,7 +197,16 @@ export const useModelPricingData = () => {
|
||||
setLoading(true);
|
||||
let url = '/api/pricing';
|
||||
const res = await API.get(url);
|
||||
const { success, message, data, vendors, group_ratio, usable_group, supported_endpoint, auto_groups } = res.data;
|
||||
const {
|
||||
success,
|
||||
message,
|
||||
data,
|
||||
vendors,
|
||||
group_ratio,
|
||||
usable_group,
|
||||
supported_endpoint,
|
||||
auto_groups,
|
||||
} = res.data;
|
||||
if (success) {
|
||||
setGroupRatio(group_ratio);
|
||||
setUsableGroup(usable_group);
|
||||
@@ -184,7 +214,7 @@ export const useModelPricingData = () => {
|
||||
// 构建供应商 Map 方便查找
|
||||
const vendorMap = {};
|
||||
if (Array.isArray(vendors)) {
|
||||
vendors.forEach(v => {
|
||||
vendors.forEach((v) => {
|
||||
vendorMap[v.id] = v;
|
||||
});
|
||||
}
|
||||
@@ -260,7 +290,14 @@ export const useModelPricingData = () => {
|
||||
// 当筛选条件变化时重置到第一页
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [filterGroup, filterQuotaType, filterEndpointType, filterVendor, filterTag, searchValue]);
|
||||
}, [
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
searchValue,
|
||||
]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
@@ -335,4 +372,4 @@ export const useModelPricingData = () => {
|
||||
// 国际化
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,7 +51,8 @@ export const usePricingFilterCounts = ({
|
||||
const matchesFilters = (model, ignore = []) => {
|
||||
// 分组
|
||||
if (!ignore.includes('group') && filterGroup !== 'all') {
|
||||
if (!model.enable_groups || !model.enable_groups.includes(filterGroup)) return false;
|
||||
if (!model.enable_groups || !model.enable_groups.includes(filterGroup))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计费类型
|
||||
@@ -90,7 +91,8 @@ export const usePricingFilterCounts = ({
|
||||
if (
|
||||
!(
|
||||
model.model_name.toLowerCase().includes(term) ||
|
||||
(model.description && model.description.toLowerCase().includes(term)) ||
|
||||
(model.description &&
|
||||
model.description.toLowerCase().includes(term)) ||
|
||||
tags.includes(term) ||
|
||||
(model.vendor_name && model.vendor_name.toLowerCase().includes(term))
|
||||
)
|
||||
@@ -104,27 +106,62 @@ export const usePricingFilterCounts = ({
|
||||
// 生成不同视图所需的模型集合
|
||||
const quotaTypeModels = useMemo(
|
||||
() => allModels.filter((m) => matchesFilters(m, ['quota'])),
|
||||
[allModels, filterGroup, filterEndpointType, filterVendor, filterTag, searchValue]
|
||||
[
|
||||
allModels,
|
||||
filterGroup,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
searchValue,
|
||||
],
|
||||
);
|
||||
|
||||
const endpointTypeModels = useMemo(
|
||||
() => allModels.filter((m) => matchesFilters(m, ['endpoint'])),
|
||||
[allModels, filterGroup, filterQuotaType, filterVendor, filterTag, searchValue]
|
||||
[
|
||||
allModels,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
searchValue,
|
||||
],
|
||||
);
|
||||
|
||||
const vendorModels = useMemo(
|
||||
() => allModels.filter((m) => matchesFilters(m, ['vendor'])),
|
||||
[allModels, filterGroup, filterQuotaType, filterEndpointType, filterTag, searchValue]
|
||||
[
|
||||
allModels,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterTag,
|
||||
searchValue,
|
||||
],
|
||||
);
|
||||
|
||||
const tagModels = useMemo(
|
||||
() => allModels.filter((m) => matchesFilters(m, ['tag'])),
|
||||
[allModels, filterGroup, filterQuotaType, filterEndpointType, filterVendor, searchValue]
|
||||
[
|
||||
allModels,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
searchValue,
|
||||
],
|
||||
);
|
||||
|
||||
const groupCountModels = useMemo(
|
||||
() => allModels.filter((m) => matchesFilters(m, ['group'])),
|
||||
[allModels, filterQuotaType, filterEndpointType, filterVendor, filterTag, searchValue]
|
||||
[
|
||||
allModels,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
searchValue,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -134,4 +171,4 @@ export const usePricingFilterCounts = ({
|
||||
groupCountModels,
|
||||
tagModels,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ export const useModelsData = () => {
|
||||
|
||||
const vendorMap = useMemo(() => {
|
||||
const map = {};
|
||||
vendors.forEach(v => {
|
||||
vendors.forEach((v) => {
|
||||
map[v.id] = v;
|
||||
});
|
||||
return map;
|
||||
@@ -118,7 +118,11 @@ export const useModelsData = () => {
|
||||
};
|
||||
|
||||
// Load models data
|
||||
const loadModels = async (page = 1, size = pageSize, vendorKey = activeVendorKey) => {
|
||||
const loadModels = async (
|
||||
page = 1,
|
||||
size = pageSize,
|
||||
vendorKey = activeVendorKey,
|
||||
) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = `/api/models/?p=${page}&page_size=${size}`;
|
||||
@@ -136,7 +140,10 @@ export const useModelsData = () => {
|
||||
setModelFormat(newPageData);
|
||||
|
||||
if (data.vendor_counts) {
|
||||
const sumAll = Object.values(data.vendor_counts).reduce((acc, v) => acc + v, 0);
|
||||
const sumAll = Object.values(data.vendor_counts).reduce(
|
||||
(acc, v) => acc + v,
|
||||
0,
|
||||
);
|
||||
setVendorCounts({ ...data.vendor_counts, all: sumAll });
|
||||
}
|
||||
} else {
|
||||
@@ -178,7 +185,10 @@ export const useModelsData = () => {
|
||||
setModelCount(data.total || newPageData.length);
|
||||
setModelFormat(newPageData);
|
||||
if (data.vendor_counts) {
|
||||
const sumAll = Object.values(data.vendor_counts).reduce((acc, v) => acc + v, 0);
|
||||
const sumAll = Object.values(data.vendor_counts).reduce(
|
||||
(acc, v) => acc + v,
|
||||
0,
|
||||
);
|
||||
setVendorCounts({ ...data.vendor_counts, all: sumAll });
|
||||
}
|
||||
} else {
|
||||
@@ -217,10 +227,12 @@ export const useModelsData = () => {
|
||||
await refresh();
|
||||
} else {
|
||||
// Update local state for enable/disable
|
||||
setModels(prevModels =>
|
||||
prevModels.map(model =>
|
||||
model.id === id ? { ...model, status: action === 'enable' ? 1 : 0 } : model
|
||||
)
|
||||
setModels((prevModels) =>
|
||||
prevModels.map((model) =>
|
||||
model.id === id
|
||||
? { ...model, status: action === 'enable' ? 1 : 0 }
|
||||
: model,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -228,7 +240,6 @@ export const useModelsData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setActivePage(page);
|
||||
@@ -249,11 +260,14 @@ export const useModelsData = () => {
|
||||
|
||||
// Handle row click and styling
|
||||
const handleRow = (record, index) => {
|
||||
const rowStyle = record.status !== 1 ? {
|
||||
style: {
|
||||
background: 'var(--semi-color-disabled-border)',
|
||||
},
|
||||
} : {};
|
||||
const rowStyle =
|
||||
record.status !== 1
|
||||
? {
|
||||
style: {
|
||||
background: 'var(--semi-color-disabled-border)',
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
...rowStyle,
|
||||
@@ -262,8 +276,10 @@ export const useModelsData = () => {
|
||||
if (event.target.closest('button, .semi-button')) {
|
||||
return;
|
||||
}
|
||||
const newSelectedKeys = selectedKeys.some(item => item.id === record.id)
|
||||
? selectedKeys.filter(item => item.id !== record.id)
|
||||
const newSelectedKeys = selectedKeys.some(
|
||||
(item) => item.id === record.id,
|
||||
)
|
||||
? selectedKeys.filter((item) => item.id !== record.id)
|
||||
: [...selectedKeys, record];
|
||||
setSelectedKeys(newSelectedKeys);
|
||||
},
|
||||
@@ -278,8 +294,8 @@ export const useModelsData = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const deletePromises = selectedKeys.map(model =>
|
||||
API.delete(`/api/models/${model.id}`)
|
||||
const deletePromises = selectedKeys.map((model) =>
|
||||
API.delete(`/api/models/${model.id}`),
|
||||
);
|
||||
|
||||
const results = await Promise.all(deletePromises);
|
||||
@@ -289,7 +305,9 @@ export const useModelsData = () => {
|
||||
if (res.data.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
showError(`删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`);
|
||||
showError(
|
||||
`删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -381,4 +399,4 @@ export const useModelsData = () => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,13 +23,13 @@ import { SSE } from 'sse.js';
|
||||
import {
|
||||
API_ENDPOINTS,
|
||||
MESSAGE_STATUS,
|
||||
DEBUG_TABS
|
||||
DEBUG_TABS,
|
||||
} from '../../constants/playground.constants';
|
||||
import {
|
||||
getUserIdFromLocalStorage,
|
||||
handleApiError,
|
||||
processThinkTags,
|
||||
processIncompleteThinkTags
|
||||
processIncompleteThinkTags,
|
||||
} from '../../helpers';
|
||||
|
||||
export const useApiRequest = (
|
||||
@@ -37,167 +37,233 @@ export const useApiRequest = (
|
||||
setDebugData,
|
||||
setActiveDebugTab,
|
||||
sseSourceRef,
|
||||
saveMessages
|
||||
saveMessages,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 处理消息自动关闭逻辑的公共函数
|
||||
const applyAutoCollapseLogic = useCallback((message, isThinkingComplete = true) => {
|
||||
const shouldAutoCollapse = isThinkingComplete && !message.hasAutoCollapsed;
|
||||
return {
|
||||
isThinkingComplete,
|
||||
hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
|
||||
isReasoningExpanded: shouldAutoCollapse ? false : message.isReasoningExpanded,
|
||||
};
|
||||
}, []);
|
||||
const applyAutoCollapseLogic = useCallback(
|
||||
(message, isThinkingComplete = true) => {
|
||||
const shouldAutoCollapse =
|
||||
isThinkingComplete && !message.hasAutoCollapsed;
|
||||
return {
|
||||
isThinkingComplete,
|
||||
hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
|
||||
isReasoningExpanded: shouldAutoCollapse
|
||||
? false
|
||||
: message.isReasoningExpanded,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 流式消息更新
|
||||
const streamMessageUpdate = useCallback((textChunk, type) => {
|
||||
setMessage(prevMessage => {
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
if (!lastMessage) return prevMessage;
|
||||
if (lastMessage.role !== 'assistant') return prevMessage;
|
||||
if (lastMessage.status === MESSAGE_STATUS.ERROR) {
|
||||
return prevMessage;
|
||||
}
|
||||
const streamMessageUpdate = useCallback(
|
||||
(textChunk, type) => {
|
||||
setMessage((prevMessage) => {
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
if (!lastMessage) return prevMessage;
|
||||
if (lastMessage.role !== 'assistant') return prevMessage;
|
||||
if (lastMessage.status === MESSAGE_STATUS.ERROR) {
|
||||
return prevMessage;
|
||||
}
|
||||
|
||||
if (lastMessage.status === MESSAGE_STATUS.LOADING ||
|
||||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
|
||||
if (
|
||||
lastMessage.status === MESSAGE_STATUS.LOADING ||
|
||||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE
|
||||
) {
|
||||
let newMessage = { ...lastMessage };
|
||||
|
||||
let newMessage = { ...lastMessage };
|
||||
if (type === 'reasoning') {
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
reasoningContent:
|
||||
(lastMessage.reasoningContent || '') + textChunk,
|
||||
status: MESSAGE_STATUS.INCOMPLETE,
|
||||
isThinkingComplete: false,
|
||||
};
|
||||
} else if (type === 'content') {
|
||||
const shouldCollapseReasoning =
|
||||
!lastMessage.content && lastMessage.reasoningContent;
|
||||
const newContent = (lastMessage.content || '') + textChunk;
|
||||
|
||||
if (type === 'reasoning') {
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
reasoningContent: (lastMessage.reasoningContent || '') + textChunk,
|
||||
status: MESSAGE_STATUS.INCOMPLETE,
|
||||
isThinkingComplete: false,
|
||||
};
|
||||
} else if (type === 'content') {
|
||||
const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
|
||||
const newContent = (lastMessage.content || '') + textChunk;
|
||||
let shouldCollapseFromThinkTag = false;
|
||||
let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
|
||||
|
||||
let shouldCollapseFromThinkTag = false;
|
||||
let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
|
||||
|
||||
if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
|
||||
const thinkMatches = newContent.match(/<think>/g);
|
||||
const thinkCloseMatches = newContent.match(/<\/think>/g);
|
||||
if (thinkMatches && thinkCloseMatches &&
|
||||
thinkCloseMatches.length >= thinkMatches.length) {
|
||||
shouldCollapseFromThinkTag = true;
|
||||
thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
|
||||
if (
|
||||
lastMessage.isReasoningExpanded &&
|
||||
newContent.includes('</think>')
|
||||
) {
|
||||
const thinkMatches = newContent.match(/<think>/g);
|
||||
const thinkCloseMatches = newContent.match(/<\/think>/g);
|
||||
if (
|
||||
thinkMatches &&
|
||||
thinkCloseMatches &&
|
||||
thinkCloseMatches.length >= thinkMatches.length
|
||||
) {
|
||||
shouldCollapseFromThinkTag = true;
|
||||
thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
|
||||
}
|
||||
}
|
||||
|
||||
// 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
|
||||
const isThinkingComplete =
|
||||
(lastMessage.reasoningContent &&
|
||||
!lastMessage.isThinkingComplete) ||
|
||||
thinkingCompleteFromTags;
|
||||
|
||||
const autoCollapseState = applyAutoCollapseLogic(
|
||||
lastMessage,
|
||||
isThinkingComplete,
|
||||
);
|
||||
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
content: newContent,
|
||||
status: MESSAGE_STATUS.INCOMPLETE,
|
||||
...autoCollapseState,
|
||||
};
|
||||
}
|
||||
|
||||
// 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
|
||||
const isThinkingComplete = (lastMessage.reasoningContent && !lastMessage.isThinkingComplete) ||
|
||||
thinkingCompleteFromTags;
|
||||
|
||||
const autoCollapseState = applyAutoCollapseLogic(lastMessage, isThinkingComplete);
|
||||
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
content: newContent,
|
||||
status: MESSAGE_STATUS.INCOMPLETE,
|
||||
...autoCollapseState,
|
||||
};
|
||||
return [...prevMessage.slice(0, -1), newMessage];
|
||||
}
|
||||
|
||||
return [...prevMessage.slice(0, -1), newMessage];
|
||||
}
|
||||
|
||||
return prevMessage;
|
||||
});
|
||||
}, [setMessage, applyAutoCollapseLogic]);
|
||||
return prevMessage;
|
||||
});
|
||||
},
|
||||
[setMessage, applyAutoCollapseLogic],
|
||||
);
|
||||
|
||||
// 完成消息
|
||||
const completeMessage = useCallback((status = MESSAGE_STATUS.COMPLETE) => {
|
||||
setMessage(prevMessage => {
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
if (lastMessage.status === MESSAGE_STATUS.COMPLETE ||
|
||||
lastMessage.status === MESSAGE_STATUS.ERROR) {
|
||||
return prevMessage;
|
||||
}
|
||||
|
||||
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
|
||||
|
||||
const updatedMessages = [
|
||||
...prevMessage.slice(0, -1),
|
||||
{
|
||||
...lastMessage,
|
||||
status: status,
|
||||
...autoCollapseState,
|
||||
const completeMessage = useCallback(
|
||||
(status = MESSAGE_STATUS.COMPLETE) => {
|
||||
setMessage((prevMessage) => {
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
if (
|
||||
lastMessage.status === MESSAGE_STATUS.COMPLETE ||
|
||||
lastMessage.status === MESSAGE_STATUS.ERROR
|
||||
) {
|
||||
return prevMessage;
|
||||
}
|
||||
];
|
||||
|
||||
// 在消息完成时保存,传入更新后的消息列表
|
||||
if (status === MESSAGE_STATUS.COMPLETE || status === MESSAGE_STATUS.ERROR) {
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
}
|
||||
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
|
||||
|
||||
return updatedMessages;
|
||||
});
|
||||
}, [setMessage, applyAutoCollapseLogic, saveMessages]);
|
||||
const updatedMessages = [
|
||||
...prevMessage.slice(0, -1),
|
||||
{
|
||||
...lastMessage,
|
||||
status: status,
|
||||
...autoCollapseState,
|
||||
},
|
||||
];
|
||||
|
||||
// 在消息完成时保存,传入更新后的消息列表
|
||||
if (
|
||||
status === MESSAGE_STATUS.COMPLETE ||
|
||||
status === MESSAGE_STATUS.ERROR
|
||||
) {
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
}
|
||||
|
||||
return updatedMessages;
|
||||
});
|
||||
},
|
||||
[setMessage, applyAutoCollapseLogic, saveMessages],
|
||||
);
|
||||
|
||||
// 非流式请求
|
||||
const handleNonStreamRequest = useCallback(async (payload) => {
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
request: payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
response: null
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.REQUEST);
|
||||
const handleNonStreamRequest = useCallback(
|
||||
async (payload) => {
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
request: payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
response: null,
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.REQUEST);
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody = '';
|
||||
try {
|
||||
errorBody = await response.text();
|
||||
} catch (e) {
|
||||
errorBody = '无法读取错误响应体';
|
||||
if (!response.ok) {
|
||||
let errorBody = '';
|
||||
try {
|
||||
errorBody = await response.text();
|
||||
} catch (e) {
|
||||
errorBody = '无法读取错误响应体';
|
||||
}
|
||||
|
||||
const errorInfo = handleApiError(
|
||||
new Error(
|
||||
`HTTP error! status: ${response.status}, body: ${errorBody}`,
|
||||
),
|
||||
response,
|
||||
);
|
||||
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response: JSON.stringify(errorInfo, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status}, body: ${errorBody}`,
|
||||
);
|
||||
}
|
||||
|
||||
const errorInfo = handleApiError(
|
||||
new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`),
|
||||
response
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
setDebugData(prev => ({
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response: JSON.stringify(errorInfo, null, 2)
|
||||
response: JSON.stringify(data, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
|
||||
}
|
||||
if (data.choices?.[0]) {
|
||||
const choice = data.choices[0];
|
||||
let content = choice.message?.content || '';
|
||||
let reasoningContent = choice.message?.reasoning_content || '';
|
||||
|
||||
const data = await response.json();
|
||||
const processed = processThinkTags(content, reasoningContent);
|
||||
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
response: JSON.stringify(data, null, 2)
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
setMessage((prevMessage) => {
|
||||
const newMessages = [...prevMessage];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
|
||||
const autoCollapseState = applyAutoCollapseLogic(
|
||||
lastMessage,
|
||||
true,
|
||||
);
|
||||
|
||||
if (data.choices?.[0]) {
|
||||
const choice = data.choices[0];
|
||||
let content = choice.message?.content || '';
|
||||
let reasoningContent = choice.message?.reasoning_content || '';
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
content: processed.content,
|
||||
reasoningContent: processed.reasoningContent,
|
||||
status: MESSAGE_STATUS.COMPLETE,
|
||||
...autoCollapseState,
|
||||
};
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Non-stream request error:', error);
|
||||
|
||||
const processed = processThinkTags(content, reasoningContent);
|
||||
const errorInfo = handleApiError(error);
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response: JSON.stringify(errorInfo, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
setMessage(prevMessage => {
|
||||
setMessage((prevMessage) => {
|
||||
const newMessages = [...prevMessage];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
|
||||
@@ -205,168 +271,164 @@ export const useApiRequest = (
|
||||
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
content: processed.content,
|
||||
reasoningContent: processed.reasoningContent,
|
||||
status: MESSAGE_STATUS.COMPLETE,
|
||||
content: t('请求发生错误: ') + error.message,
|
||||
status: MESSAGE_STATUS.ERROR,
|
||||
...autoCollapseState,
|
||||
};
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Non-stream request error:', error);
|
||||
|
||||
const errorInfo = handleApiError(error);
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
response: JSON.stringify(errorInfo, null, 2)
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
setMessage(prevMessage => {
|
||||
const newMessages = [...prevMessage];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
|
||||
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
|
||||
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
content: t('请求发生错误: ') + error.message,
|
||||
status: MESSAGE_STATUS.ERROR,
|
||||
...autoCollapseState,
|
||||
};
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
}, [setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic]);
|
||||
},
|
||||
[setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic],
|
||||
);
|
||||
|
||||
// SSE请求
|
||||
const handleSSE = useCallback((payload) => {
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
request: payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
response: null
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.REQUEST);
|
||||
const handleSSE = useCallback(
|
||||
(payload) => {
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
request: payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
response: null,
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.REQUEST);
|
||||
|
||||
const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
method: 'POST',
|
||||
payload: JSON.stringify(payload),
|
||||
});
|
||||
const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
method: 'POST',
|
||||
payload: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
sseSourceRef.current = source;
|
||||
sseSourceRef.current = source;
|
||||
|
||||
let responseData = '';
|
||||
let hasReceivedFirstResponse = false;
|
||||
let isStreamComplete = false; // 添加标志位跟踪流是否正常完成
|
||||
let responseData = '';
|
||||
let hasReceivedFirstResponse = false;
|
||||
let isStreamComplete = false; // 添加标志位跟踪流是否正常完成
|
||||
|
||||
source.addEventListener('message', (e) => {
|
||||
if (e.data === '[DONE]') {
|
||||
isStreamComplete = true; // 标记流正常完成
|
||||
source.close();
|
||||
sseSourceRef.current = null;
|
||||
setDebugData(prev => ({ ...prev, response: responseData }));
|
||||
completeMessage();
|
||||
return;
|
||||
}
|
||||
source.addEventListener('message', (e) => {
|
||||
if (e.data === '[DONE]') {
|
||||
isStreamComplete = true; // 标记流正常完成
|
||||
source.close();
|
||||
sseSourceRef.current = null;
|
||||
setDebugData((prev) => ({ ...prev, response: responseData }));
|
||||
completeMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
responseData += e.data + '\n';
|
||||
|
||||
if (!hasReceivedFirstResponse) {
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
hasReceivedFirstResponse = true;
|
||||
}
|
||||
|
||||
const delta = payload.choices?.[0]?.delta;
|
||||
if (delta) {
|
||||
if (delta.reasoning_content) {
|
||||
streamMessageUpdate(delta.reasoning_content, 'reasoning');
|
||||
}
|
||||
if (delta.content) {
|
||||
streamMessageUpdate(delta.content, 'content');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse SSE message:', error);
|
||||
const errorInfo = `解析错误: ${error.message}`;
|
||||
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response: responseData + `\n\nError: ${errorInfo}`,
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('error', (e) => {
|
||||
// 只有在流没有正常完成且连接状态异常时才处理错误
|
||||
if (!isStreamComplete && source.readyState !== 2) {
|
||||
console.error('SSE Error:', e);
|
||||
const errorMessage = e.data || t('请求发生错误');
|
||||
|
||||
const errorInfo = handleApiError(new Error(errorMessage));
|
||||
errorInfo.readyState = source.readyState;
|
||||
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response:
|
||||
responseData +
|
||||
'\n\nSSE Error:\n' +
|
||||
JSON.stringify(errorInfo, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(errorMessage, 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
sseSourceRef.current = null;
|
||||
source.close();
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('readystatechange', (e) => {
|
||||
// 检查 HTTP 状态错误,但避免与正常关闭重复处理
|
||||
if (
|
||||
e.readyState >= 2 &&
|
||||
source.status !== undefined &&
|
||||
source.status !== 200 &&
|
||||
!isStreamComplete
|
||||
) {
|
||||
const errorInfo = handleApiError(new Error('HTTP状态错误'));
|
||||
errorInfo.status = source.status;
|
||||
errorInfo.readyState = source.readyState;
|
||||
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response:
|
||||
responseData +
|
||||
'\n\nHTTP Error:\n' +
|
||||
JSON.stringify(errorInfo, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
source.close();
|
||||
streamMessageUpdate(t('连接已断开'), 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
responseData += e.data + '\n';
|
||||
|
||||
if (!hasReceivedFirstResponse) {
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
hasReceivedFirstResponse = true;
|
||||
}
|
||||
|
||||
const delta = payload.choices?.[0]?.delta;
|
||||
if (delta) {
|
||||
if (delta.reasoning_content) {
|
||||
streamMessageUpdate(delta.reasoning_content, 'reasoning');
|
||||
}
|
||||
if (delta.content) {
|
||||
streamMessageUpdate(delta.content, 'content');
|
||||
}
|
||||
}
|
||||
source.stream();
|
||||
} catch (error) {
|
||||
console.error('Failed to parse SSE message:', error);
|
||||
const errorInfo = `解析错误: ${error.message}`;
|
||||
console.error('Failed to start SSE stream:', error);
|
||||
const errorInfo = handleApiError(error);
|
||||
|
||||
setDebugData(prev => ({
|
||||
setDebugData((prev) => ({
|
||||
...prev,
|
||||
response: responseData + `\n\nError: ${errorInfo}`
|
||||
response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2),
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
|
||||
streamMessageUpdate(t('建立连接时发生错误'), 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('error', (e) => {
|
||||
// 只有在流没有正常完成且连接状态异常时才处理错误
|
||||
if (!isStreamComplete && source.readyState !== 2) {
|
||||
console.error('SSE Error:', e);
|
||||
const errorMessage = e.data || t('请求发生错误');
|
||||
|
||||
const errorInfo = handleApiError(new Error(errorMessage));
|
||||
errorInfo.readyState = source.readyState;
|
||||
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
response: responseData + '\n\nSSE Error:\n' + JSON.stringify(errorInfo, null, 2)
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(errorMessage, 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
sseSourceRef.current = null;
|
||||
source.close();
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('readystatechange', (e) => {
|
||||
// 检查 HTTP 状态错误,但避免与正常关闭重复处理
|
||||
if (e.readyState >= 2 && source.status !== undefined && source.status !== 200 && !isStreamComplete) {
|
||||
const errorInfo = handleApiError(new Error('HTTP状态错误'));
|
||||
errorInfo.status = source.status;
|
||||
errorInfo.readyState = source.readyState;
|
||||
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2)
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
source.close();
|
||||
streamMessageUpdate(t('连接已断开'), 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
source.stream();
|
||||
} catch (error) {
|
||||
console.error('Failed to start SSE stream:', error);
|
||||
const errorInfo = handleApiError(error);
|
||||
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2)
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(t('建立连接时发生错误'), 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
}
|
||||
}, [setDebugData, setActiveDebugTab, streamMessageUpdate, completeMessage, t, applyAutoCollapseLogic]);
|
||||
},
|
||||
[
|
||||
setDebugData,
|
||||
setActiveDebugTab,
|
||||
streamMessageUpdate,
|
||||
completeMessage,
|
||||
t,
|
||||
applyAutoCollapseLogic,
|
||||
],
|
||||
);
|
||||
|
||||
// 停止生成
|
||||
const onStopGenerator = useCallback(() => {
|
||||
@@ -377,16 +439,17 @@ export const useApiRequest = (
|
||||
}
|
||||
|
||||
// 无论是否存在 SSE 连接,都尝试处理最后一条正在生成的消息
|
||||
setMessage(prevMessage => {
|
||||
setMessage((prevMessage) => {
|
||||
if (prevMessage.length === 0) return prevMessage;
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
|
||||
if (lastMessage.status === MESSAGE_STATUS.LOADING ||
|
||||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
|
||||
|
||||
if (
|
||||
lastMessage.status === MESSAGE_STATUS.LOADING ||
|
||||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE
|
||||
) {
|
||||
const processed = processIncompleteThinkTags(
|
||||
lastMessage.content || '',
|
||||
lastMessage.reasoningContent || ''
|
||||
lastMessage.reasoningContent || '',
|
||||
);
|
||||
|
||||
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
|
||||
@@ -399,7 +462,7 @@ export const useApiRequest = (
|
||||
reasoningContent: processed.reasoningContent || null,
|
||||
content: processed.content,
|
||||
...autoCollapseState,
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// 停止生成时也保存,传入更新后的消息列表
|
||||
@@ -412,13 +475,16 @@ export const useApiRequest = (
|
||||
}, [setMessage, applyAutoCollapseLogic, saveMessages]);
|
||||
|
||||
// 发送请求
|
||||
const sendRequest = useCallback((payload, isStream) => {
|
||||
if (isStream) {
|
||||
handleSSE(payload);
|
||||
} else {
|
||||
handleNonStreamRequest(payload);
|
||||
}
|
||||
}, [handleSSE, handleNonStreamRequest]);
|
||||
const sendRequest = useCallback(
|
||||
(payload, isStream) => {
|
||||
if (isStream) {
|
||||
handleSSE(payload);
|
||||
} else {
|
||||
handleNonStreamRequest(payload);
|
||||
}
|
||||
},
|
||||
[handleSSE, handleNonStreamRequest],
|
||||
);
|
||||
|
||||
return {
|
||||
sendRequest,
|
||||
@@ -426,4 +492,4 @@ export const useApiRequest = (
|
||||
streamMessageUpdate,
|
||||
completeMessage,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useDataLoader = (
|
||||
inputs,
|
||||
handleInputChange,
|
||||
setModels,
|
||||
setGroups
|
||||
setGroups,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -37,7 +37,10 @@ export const useDataLoader = (
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const { modelOptions, selectedModel } = processModelsData(data, inputs.model);
|
||||
const { modelOptions, selectedModel } = processModelsData(
|
||||
data,
|
||||
inputs.model,
|
||||
);
|
||||
setModels(modelOptions);
|
||||
|
||||
if (selectedModel !== inputs.model) {
|
||||
@@ -57,11 +60,15 @@ export const useDataLoader = (
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const userGroup = userState?.user?.group || JSON.parse(localStorage.getItem('user'))?.group;
|
||||
const userGroup =
|
||||
userState?.user?.group ||
|
||||
JSON.parse(localStorage.getItem('user'))?.group;
|
||||
const groupOptions = processGroupsData(data, userGroup);
|
||||
setGroups(groupOptions);
|
||||
|
||||
const hasCurrentGroup = groupOptions.some(option => option.value === inputs.group);
|
||||
const hasCurrentGroup = groupOptions.some(
|
||||
(option) => option.value === inputs.group,
|
||||
);
|
||||
if (!hasCurrentGroup) {
|
||||
handleInputChange('group', groupOptions[0]?.value || '');
|
||||
}
|
||||
@@ -83,6 +90,6 @@ export const useDataLoader = (
|
||||
|
||||
return {
|
||||
loadModels,
|
||||
loadGroups
|
||||
loadGroups,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,43 +23,49 @@ import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent } from '../../helpers';
|
||||
import { ERROR_MESSAGES } from '../../constants/playground.constants';
|
||||
|
||||
export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => {
|
||||
export const useMessageActions = (
|
||||
message,
|
||||
setMessage,
|
||||
onMessageSend,
|
||||
saveMessages,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 复制消息
|
||||
const handleMessageCopy = useCallback((targetMessage) => {
|
||||
const textToCopy = getTextContent(targetMessage);
|
||||
const handleMessageCopy = useCallback(
|
||||
(targetMessage) => {
|
||||
const textToCopy = getTextContent(targetMessage);
|
||||
|
||||
if (!textToCopy) {
|
||||
Toast.warning({
|
||||
content: t(ERROR_MESSAGES.NO_TEXT_CONTENT),
|
||||
duration: 2,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!textToCopy) {
|
||||
Toast.warning({
|
||||
content: t(ERROR_MESSAGES.NO_TEXT_CONTENT),
|
||||
duration: 2,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
Toast.success({
|
||||
content: t('消息已复制到剪贴板'),
|
||||
duration: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Clipboard API 复制失败:', err);
|
||||
const copyToClipboard = async (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
Toast.success({
|
||||
content: t('消息已复制到剪贴板'),
|
||||
duration: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Clipboard API 复制失败:', err);
|
||||
fallbackCopy(text);
|
||||
}
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const fallbackCopy = (text) => {
|
||||
try {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.cssText = `
|
||||
const fallbackCopy = (text) => {
|
||||
try {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.cssText = `
|
||||
position: fixed;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
@@ -67,171 +73,214 @@ export const useMessageActions = (message, setMessage, onMessageSend, saveMessag
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
`;
|
||||
textArea.setAttribute('readonly', '');
|
||||
textArea.setAttribute('readonly', '');
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, text.length);
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, text.length);
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (successful) {
|
||||
Toast.success({
|
||||
content: t('消息已复制到剪贴板'),
|
||||
duration: 2,
|
||||
if (successful) {
|
||||
Toast.success({
|
||||
content: t('消息已复制到剪贴板'),
|
||||
duration: 2,
|
||||
});
|
||||
} else {
|
||||
throw new Error('execCommand copy failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('回退复制方案也失败:', err);
|
||||
|
||||
let errorMessage = t(ERROR_MESSAGES.COPY_FAILED);
|
||||
if (
|
||||
window.location.protocol === 'http:' &&
|
||||
window.location.hostname !== 'localhost'
|
||||
) {
|
||||
errorMessage = t(ERROR_MESSAGES.COPY_HTTPS_REQUIRED);
|
||||
} else if (!navigator.clipboard && !document.execCommand) {
|
||||
errorMessage = t(ERROR_MESSAGES.BROWSER_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
Toast.error({
|
||||
content: errorMessage,
|
||||
duration: 4,
|
||||
});
|
||||
} else {
|
||||
throw new Error('execCommand copy failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('回退复制方案也失败:', err);
|
||||
};
|
||||
|
||||
let errorMessage = t(ERROR_MESSAGES.COPY_FAILED);
|
||||
if (window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||
errorMessage = t(ERROR_MESSAGES.COPY_HTTPS_REQUIRED);
|
||||
} else if (!navigator.clipboard && !document.execCommand) {
|
||||
errorMessage = t(ERROR_MESSAGES.BROWSER_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
Toast.error({
|
||||
content: errorMessage,
|
||||
duration: 4,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboard(textToCopy);
|
||||
}, [t]);
|
||||
copyToClipboard(textToCopy);
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// 重新生成消息
|
||||
const handleMessageReset = useCallback((targetMessage) => {
|
||||
setMessage(prevMessages => {
|
||||
// 使用引用查找索引,防止重复 id 造成误匹配
|
||||
let messageIndex = prevMessages.findIndex(msg => msg === targetMessage);
|
||||
const handleMessageReset = useCallback(
|
||||
(targetMessage) => {
|
||||
setMessage((prevMessages) => {
|
||||
// 使用引用查找索引,防止重复 id 造成误匹配
|
||||
let messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg === targetMessage,
|
||||
);
|
||||
|
||||
// 回退到 id 匹配(兼容不同引用场景)
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id);
|
||||
}
|
||||
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
|
||||
if (targetMessage.role === 'user') {
|
||||
const newMessages = prevMessages.slice(0, messageIndex);
|
||||
const contentToSend = getTextContent(targetMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
onMessageSend(contentToSend);
|
||||
}, 100);
|
||||
|
||||
return newMessages;
|
||||
} else if (targetMessage.role === 'assistant' || targetMessage.role === 'system') {
|
||||
let userMessageIndex = messageIndex - 1;
|
||||
while (userMessageIndex >= 0 && prevMessages[userMessageIndex].role !== 'user') {
|
||||
userMessageIndex--;
|
||||
// 回退到 id 匹配(兼容不同引用场景)
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg.id === targetMessage.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (userMessageIndex >= 0) {
|
||||
const userMessage = prevMessages[userMessageIndex];
|
||||
const newMessages = prevMessages.slice(0, userMessageIndex);
|
||||
const contentToSend = getTextContent(userMessage);
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
|
||||
if (targetMessage.role === 'user') {
|
||||
const newMessages = prevMessages.slice(0, messageIndex);
|
||||
const contentToSend = getTextContent(targetMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
onMessageSend(contentToSend);
|
||||
}, 100);
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
}
|
||||
|
||||
return prevMessages;
|
||||
});
|
||||
}, [setMessage, onMessageSend]);
|
||||
|
||||
// 删除消息
|
||||
const handleMessageDelete = useCallback((targetMessage) => {
|
||||
Modal.confirm({
|
||||
title: t('确认删除'),
|
||||
content: t('确定要删除这条消息吗?'),
|
||||
okText: t('确定'),
|
||||
cancelText: t('取消'),
|
||||
okButtonProps: {
|
||||
type: 'danger',
|
||||
},
|
||||
onOk: () => {
|
||||
setMessage(prevMessages => {
|
||||
// 使用引用查找索引,防止重复 id 造成误匹配
|
||||
let messageIndex = prevMessages.findIndex(msg => msg === targetMessage);
|
||||
|
||||
// 回退到 id 匹配(兼容不同引用场景)
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id);
|
||||
} else if (
|
||||
targetMessage.role === 'assistant' ||
|
||||
targetMessage.role === 'system'
|
||||
) {
|
||||
let userMessageIndex = messageIndex - 1;
|
||||
while (
|
||||
userMessageIndex >= 0 &&
|
||||
prevMessages[userMessageIndex].role !== 'user'
|
||||
) {
|
||||
userMessageIndex--;
|
||||
}
|
||||
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
if (userMessageIndex >= 0) {
|
||||
const userMessage = prevMessages[userMessageIndex];
|
||||
const newMessages = prevMessages.slice(0, userMessageIndex);
|
||||
const contentToSend = getTextContent(userMessage);
|
||||
|
||||
let updatedMessages;
|
||||
if (targetMessage.role === 'user' && messageIndex < prevMessages.length - 1) {
|
||||
const nextMessage = prevMessages[messageIndex + 1];
|
||||
if (nextMessage.role === 'assistant') {
|
||||
Toast.success({
|
||||
content: t('已删除消息及其回复'),
|
||||
duration: 2,
|
||||
});
|
||||
updatedMessages = prevMessages.filter((_, index) =>
|
||||
index !== messageIndex && index !== messageIndex + 1
|
||||
setTimeout(() => {
|
||||
onMessageSend(contentToSend);
|
||||
}, 100);
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
}
|
||||
|
||||
return prevMessages;
|
||||
});
|
||||
},
|
||||
[setMessage, onMessageSend],
|
||||
);
|
||||
|
||||
// 删除消息
|
||||
const handleMessageDelete = useCallback(
|
||||
(targetMessage) => {
|
||||
Modal.confirm({
|
||||
title: t('确认删除'),
|
||||
content: t('确定要删除这条消息吗?'),
|
||||
okText: t('确定'),
|
||||
cancelText: t('取消'),
|
||||
okButtonProps: {
|
||||
type: 'danger',
|
||||
},
|
||||
onOk: () => {
|
||||
setMessage((prevMessages) => {
|
||||
// 使用引用查找索引,防止重复 id 造成误匹配
|
||||
let messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg === targetMessage,
|
||||
);
|
||||
|
||||
// 回退到 id 匹配(兼容不同引用场景)
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg.id === targetMessage.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
|
||||
let updatedMessages;
|
||||
if (
|
||||
targetMessage.role === 'user' &&
|
||||
messageIndex < prevMessages.length - 1
|
||||
) {
|
||||
const nextMessage = prevMessages[messageIndex + 1];
|
||||
if (nextMessage.role === 'assistant') {
|
||||
Toast.success({
|
||||
content: t('已删除消息及其回复'),
|
||||
duration: 2,
|
||||
});
|
||||
updatedMessages = prevMessages.filter(
|
||||
(_, index) =>
|
||||
index !== messageIndex && index !== messageIndex + 1,
|
||||
);
|
||||
} else {
|
||||
Toast.success({
|
||||
content: t('消息已删除'),
|
||||
duration: 2,
|
||||
});
|
||||
updatedMessages = prevMessages.filter(
|
||||
(msg) => msg.id !== targetMessage.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Toast.success({
|
||||
content: t('消息已删除'),
|
||||
duration: 2,
|
||||
});
|
||||
updatedMessages = prevMessages.filter(msg => msg.id !== targetMessage.id);
|
||||
updatedMessages = prevMessages.filter(
|
||||
(msg) => msg.id !== targetMessage.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Toast.success({
|
||||
content: t('消息已删除'),
|
||||
duration: 2,
|
||||
});
|
||||
updatedMessages = prevMessages.filter(msg => msg.id !== targetMessage.id);
|
||||
}
|
||||
|
||||
// 删除消息后保存,传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
return updatedMessages;
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [setMessage, t, saveMessages]);
|
||||
// 删除消息后保存,传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
return updatedMessages;
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[setMessage, t, saveMessages],
|
||||
);
|
||||
|
||||
// 切换角色
|
||||
const handleRoleToggle = useCallback((targetMessage) => {
|
||||
if (!(targetMessage.role === 'assistant' || targetMessage.role === 'system')) {
|
||||
return;
|
||||
}
|
||||
const handleRoleToggle = useCallback(
|
||||
(targetMessage) => {
|
||||
if (
|
||||
!(targetMessage.role === 'assistant' || targetMessage.role === 'system')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRole = targetMessage.role === 'assistant' ? 'system' : 'assistant';
|
||||
const newRole =
|
||||
targetMessage.role === 'assistant' ? 'system' : 'assistant';
|
||||
|
||||
setMessage(prevMessages => {
|
||||
const updatedMessages = prevMessages.map(msg => {
|
||||
if (msg.id === targetMessage.id &&
|
||||
(msg.role === 'assistant' || msg.role === 'system')) {
|
||||
return { ...msg, role: newRole };
|
||||
}
|
||||
return msg;
|
||||
setMessage((prevMessages) => {
|
||||
const updatedMessages = prevMessages.map((msg) => {
|
||||
if (
|
||||
msg.id === targetMessage.id &&
|
||||
(msg.role === 'assistant' || msg.role === 'system')
|
||||
) {
|
||||
return { ...msg, role: newRole };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
|
||||
// 切换角色后保存,传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
return updatedMessages;
|
||||
});
|
||||
|
||||
// 切换角色后保存,传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
return updatedMessages;
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: t(`已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`),
|
||||
duration: 2,
|
||||
});
|
||||
}, [setMessage, t, saveMessages]);
|
||||
Toast.success({
|
||||
content: t(
|
||||
`已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`,
|
||||
),
|
||||
duration: 2,
|
||||
});
|
||||
},
|
||||
[setMessage, t, saveMessages],
|
||||
);
|
||||
|
||||
return {
|
||||
handleMessageCopy,
|
||||
@@ -239,4 +288,4 @@ export const useMessageActions = (message, setMessage, onMessageSend, saveMessag
|
||||
handleMessageDelete,
|
||||
handleRoleToggle,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,7 +20,11 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../../helpers';
|
||||
import {
|
||||
getTextContent,
|
||||
buildApiPayload,
|
||||
createLoadingAssistantMessage,
|
||||
} from '../../helpers';
|
||||
import { MESSAGE_ROLES } from '../../constants/playground.constants';
|
||||
|
||||
export const useMessageEdit = (
|
||||
@@ -28,7 +32,7 @@ export const useMessageEdit = (
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
sendRequest,
|
||||
saveMessages
|
||||
saveMessages,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [editingMessageId, setEditingMessageId] = useState(null);
|
||||
@@ -45,31 +49,36 @@ export const useMessageEdit = (
|
||||
const handleEditSave = useCallback(() => {
|
||||
if (!editingMessageId || !editValue.trim()) return;
|
||||
|
||||
setMessage(prevMessages => {
|
||||
let messageIndex = prevMessages.findIndex(msg => msg === editingMessageRef.current);
|
||||
setMessage((prevMessages) => {
|
||||
let messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg === editingMessageRef.current,
|
||||
);
|
||||
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = prevMessages.findIndex(msg => msg.id === editingMessageId);
|
||||
messageIndex = prevMessages.findIndex(
|
||||
(msg) => msg.id === editingMessageId,
|
||||
);
|
||||
}
|
||||
|
||||
const targetMessage = prevMessages[messageIndex];
|
||||
let newContent;
|
||||
|
||||
if (Array.isArray(targetMessage.content)) {
|
||||
newContent = targetMessage.content.map(item =>
|
||||
item.type === 'text' ? { ...item, text: editValue.trim() } : item
|
||||
newContent = targetMessage.content.map((item) =>
|
||||
item.type === 'text' ? { ...item, text: editValue.trim() } : item,
|
||||
);
|
||||
} else {
|
||||
newContent = editValue.trim();
|
||||
}
|
||||
|
||||
const updatedMessages = prevMessages.map(msg =>
|
||||
msg.id === editingMessageId ? { ...msg, content: newContent } : msg
|
||||
const updatedMessages = prevMessages.map((msg) =>
|
||||
msg.id === editingMessageId ? { ...msg, content: newContent } : msg,
|
||||
);
|
||||
|
||||
// 处理用户消息编辑后的重新生成
|
||||
if (targetMessage.role === MESSAGE_ROLES.USER) {
|
||||
const hasSubsequentAssistantReply = messageIndex < prevMessages.length - 1 &&
|
||||
const hasSubsequentAssistantReply =
|
||||
messageIndex < prevMessages.length - 1 &&
|
||||
prevMessages[messageIndex + 1].role === MESSAGE_ROLES.ASSISTANT;
|
||||
|
||||
if (hasSubsequentAssistantReply) {
|
||||
@@ -79,14 +88,25 @@ export const useMessageEdit = (
|
||||
okText: t('重新生成'),
|
||||
cancelText: t('仅保存'),
|
||||
onOk: () => {
|
||||
const messagesUntilUser = updatedMessages.slice(0, messageIndex + 1);
|
||||
const messagesUntilUser = updatedMessages.slice(
|
||||
0,
|
||||
messageIndex + 1,
|
||||
);
|
||||
setMessage(messagesUntilUser);
|
||||
// 编辑后保存(重新生成的情况),传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(messagesUntilUser), 0);
|
||||
|
||||
setTimeout(() => {
|
||||
const payload = buildApiPayload(messagesUntilUser, null, inputs, parameterEnabled);
|
||||
setMessage(prevMsg => [...prevMsg, createLoadingAssistantMessage()]);
|
||||
const payload = buildApiPayload(
|
||||
messagesUntilUser,
|
||||
null,
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
);
|
||||
setMessage((prevMsg) => [
|
||||
...prevMsg,
|
||||
createLoadingAssistantMessage(),
|
||||
]);
|
||||
sendRequest(payload, inputs.stream);
|
||||
}, 100);
|
||||
},
|
||||
@@ -94,7 +114,7 @@ export const useMessageEdit = (
|
||||
setMessage(updatedMessages);
|
||||
// 编辑后保存(仅保存的情况),传入更新后的消息列表
|
||||
setTimeout(() => saveMessages(updatedMessages), 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
return prevMessages;
|
||||
}
|
||||
@@ -109,7 +129,16 @@ export const useMessageEdit = (
|
||||
editingMessageRef.current = null;
|
||||
setEditValue('');
|
||||
Toast.success({ content: t('消息已更新'), duration: 2 });
|
||||
}, [editingMessageId, editValue, t, inputs, parameterEnabled, sendRequest, setMessage, saveMessages]);
|
||||
}, [
|
||||
editingMessageId,
|
||||
editValue,
|
||||
t,
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
sendRequest,
|
||||
setMessage,
|
||||
saveMessages,
|
||||
]);
|
||||
|
||||
const handleEditCancel = useCallback(() => {
|
||||
setEditingMessageId(null);
|
||||
@@ -123,6 +152,6 @@ export const useMessageEdit = (
|
||||
setEditValue,
|
||||
handleMessageEdit,
|
||||
handleEditSave,
|
||||
handleEditCancel
|
||||
handleEditCancel,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,8 +18,18 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../../constants/playground.constants';
|
||||
import { loadConfig, saveConfig, loadMessages, saveMessages } from '../../components/playground/configStorage';
|
||||
import {
|
||||
DEFAULT_MESSAGES,
|
||||
DEFAULT_CONFIG,
|
||||
DEBUG_TABS,
|
||||
MESSAGE_STATUS,
|
||||
} from '../../constants/playground.constants';
|
||||
import {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
loadMessages,
|
||||
saveMessages,
|
||||
} from '../../components/playground/configStorage';
|
||||
import { processIncompleteThinkTags } from '../../helpers';
|
||||
|
||||
export const usePlaygroundState = () => {
|
||||
@@ -28,18 +38,20 @@ export const usePlaygroundState = () => {
|
||||
const [initialMessages] = useState(() => loadMessages() || DEFAULT_MESSAGES);
|
||||
|
||||
// 基础配置状态
|
||||
const [inputs, setInputs] = useState(savedConfig.inputs || DEFAULT_CONFIG.inputs);
|
||||
const [inputs, setInputs] = useState(
|
||||
savedConfig.inputs || DEFAULT_CONFIG.inputs,
|
||||
);
|
||||
const [parameterEnabled, setParameterEnabled] = useState(
|
||||
savedConfig.parameterEnabled || DEFAULT_CONFIG.parameterEnabled
|
||||
savedConfig.parameterEnabled || DEFAULT_CONFIG.parameterEnabled,
|
||||
);
|
||||
const [showDebugPanel, setShowDebugPanel] = useState(
|
||||
savedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel
|
||||
savedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
||||
);
|
||||
const [customRequestMode, setCustomRequestMode] = useState(
|
||||
savedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode
|
||||
savedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
||||
);
|
||||
const [customRequestBody, setCustomRequestBody] = useState(
|
||||
savedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody
|
||||
savedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
||||
);
|
||||
|
||||
// UI状态
|
||||
@@ -57,7 +69,7 @@ export const usePlaygroundState = () => {
|
||||
response: null,
|
||||
timestamp: null,
|
||||
previewRequest: null,
|
||||
previewTimestamp: null
|
||||
previewTimestamp: null,
|
||||
});
|
||||
const [activeDebugTab, setActiveDebugTab] = useState(DEBUG_TABS.PREVIEW);
|
||||
const [previewPayload, setPreviewPayload] = useState(null);
|
||||
@@ -74,21 +86,24 @@ export const usePlaygroundState = () => {
|
||||
|
||||
// 配置更新函数
|
||||
const handleInputChange = useCallback((name, value) => {
|
||||
setInputs(prev => ({ ...prev, [name]: value }));
|
||||
setInputs((prev) => ({ ...prev, [name]: value }));
|
||||
}, []);
|
||||
|
||||
const handleParameterToggle = useCallback((paramName) => {
|
||||
setParameterEnabled(prev => ({
|
||||
setParameterEnabled((prev) => ({
|
||||
...prev,
|
||||
[paramName]: !prev[paramName]
|
||||
[paramName]: !prev[paramName],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 消息保存函数 - 改为立即保存,可以接受参数
|
||||
const saveMessagesImmediately = useCallback((messagesToSave) => {
|
||||
// 如果提供了参数,使用参数;否则使用当前状态
|
||||
saveMessages(messagesToSave || message);
|
||||
}, [message]);
|
||||
const saveMessagesImmediately = useCallback(
|
||||
(messagesToSave) => {
|
||||
// 如果提供了参数,使用参数;否则使用当前状态
|
||||
saveMessages(messagesToSave || message);
|
||||
},
|
||||
[message],
|
||||
);
|
||||
|
||||
// 配置保存
|
||||
const debouncedSaveConfig = useCallback(() => {
|
||||
@@ -106,15 +121,24 @@ export const usePlaygroundState = () => {
|
||||
};
|
||||
saveConfig(configToSave);
|
||||
}, 1000);
|
||||
}, [inputs, parameterEnabled, showDebugPanel, customRequestMode, customRequestBody]);
|
||||
}, [
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
]);
|
||||
|
||||
// 配置导入/重置
|
||||
const handleConfigImport = useCallback((importedConfig) => {
|
||||
if (importedConfig.inputs) {
|
||||
setInputs(prev => ({ ...prev, ...importedConfig.inputs }));
|
||||
setInputs((prev) => ({ ...prev, ...importedConfig.inputs }));
|
||||
}
|
||||
if (importedConfig.parameterEnabled) {
|
||||
setParameterEnabled(prev => ({ ...prev, ...importedConfig.parameterEnabled }));
|
||||
setParameterEnabled((prev) => ({
|
||||
...prev,
|
||||
...importedConfig.parameterEnabled,
|
||||
}));
|
||||
}
|
||||
if (typeof importedConfig.showDebugPanel === 'boolean') {
|
||||
setShowDebugPanel(importedConfig.showDebugPanel);
|
||||
@@ -163,10 +187,13 @@ export const usePlaygroundState = () => {
|
||||
if (!Array.isArray(message) || message.length === 0) return;
|
||||
|
||||
const lastMsg = message[message.length - 1];
|
||||
if (lastMsg.status === MESSAGE_STATUS.LOADING || lastMsg.status === MESSAGE_STATUS.INCOMPLETE) {
|
||||
if (
|
||||
lastMsg.status === MESSAGE_STATUS.LOADING ||
|
||||
lastMsg.status === MESSAGE_STATUS.INCOMPLETE
|
||||
) {
|
||||
const processed = processIncompleteThinkTags(
|
||||
lastMsg.content || '',
|
||||
lastMsg.reasoningContent || ''
|
||||
lastMsg.reasoningContent || '',
|
||||
);
|
||||
|
||||
const fixedLastMsg = {
|
||||
@@ -241,4 +268,4 @@ export const usePlaygroundState = () => {
|
||||
handleConfigImport,
|
||||
handleConfigReset,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useSyncMessageAndCustomBody = (
|
||||
inputs,
|
||||
setCustomRequestBody,
|
||||
setMessage,
|
||||
debouncedSaveConfig
|
||||
debouncedSaveConfig,
|
||||
) => {
|
||||
const isUpdatingFromMessage = useRef(false);
|
||||
const isUpdatingFromCustomBody = useRef(false);
|
||||
@@ -35,11 +35,13 @@ export const useSyncMessageAndCustomBody = (
|
||||
const lastCustomBodyHash = useRef('');
|
||||
|
||||
const getMessageHash = useCallback((messages) => {
|
||||
return JSON.stringify(messages.map(msg => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})));
|
||||
return JSON.stringify(
|
||||
messages.map((msg) => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getCustomBodyHash = useCallback((customBody) => {
|
||||
@@ -68,13 +70,13 @@ export const useSyncMessageAndCustomBody = (
|
||||
model: inputs.model || 'gpt-4o',
|
||||
messages: [],
|
||||
temperature: inputs.temperature || 0.7,
|
||||
stream: inputs.stream !== false
|
||||
stream: inputs.stream !== false,
|
||||
};
|
||||
}
|
||||
|
||||
customPayload.messages = message.map(msg => ({
|
||||
customPayload.messages = message.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
const newCustomBody = JSON.stringify(customPayload, null, 2);
|
||||
@@ -88,7 +90,18 @@ export const useSyncMessageAndCustomBody = (
|
||||
} finally {
|
||||
isUpdatingFromMessage.current = false;
|
||||
}
|
||||
}, [customRequestMode, customRequestBody, message, inputs.model, inputs.temperature, inputs.stream, getMessageHash, getCustomBodyHash, setCustomRequestBody, debouncedSaveConfig]);
|
||||
}, [
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
message,
|
||||
inputs.model,
|
||||
inputs.temperature,
|
||||
inputs.stream,
|
||||
getMessageHash,
|
||||
getCustomBodyHash,
|
||||
setCustomRequestBody,
|
||||
debouncedSaveConfig,
|
||||
]);
|
||||
|
||||
const syncCustomBodyToMessage = useCallback(() => {
|
||||
if (!customRequestMode || isUpdatingFromMessage.current) return;
|
||||
@@ -108,8 +121,8 @@ export const useSyncMessageAndCustomBody = (
|
||||
createAt: Date.now(),
|
||||
...(msg.role === MESSAGE_ROLES.ASSISTANT && {
|
||||
reasoningContent: msg.reasoningContent || '',
|
||||
isReasoningExpanded: false
|
||||
})
|
||||
isReasoningExpanded: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
setMessage(newMessages);
|
||||
@@ -121,10 +134,16 @@ export const useSyncMessageAndCustomBody = (
|
||||
} finally {
|
||||
isUpdatingFromCustomBody.current = false;
|
||||
}
|
||||
}, [customRequestMode, customRequestBody, getCustomBodyHash, getMessageHash, setMessage]);
|
||||
}, [
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
getCustomBodyHash,
|
||||
getMessageHash,
|
||||
setMessage,
|
||||
]);
|
||||
|
||||
return {
|
||||
syncMessageToCustomBody,
|
||||
syncCustomBodyToMessage
|
||||
syncCustomBodyToMessage,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,7 +20,10 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useState, useEffect } from 'react';
|
||||
import { API, showError, showSuccess, copy } from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { REDEMPTION_ACTIONS, REDEMPTION_STATUS } from '../../constants/redemption.constants';
|
||||
import {
|
||||
REDEMPTION_ACTIONS,
|
||||
REDEMPTION_STATUS,
|
||||
} from '../../constants/redemption.constants';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -193,8 +196,8 @@ export const useRedemptionsData = () => {
|
||||
|
||||
// Row selection configuration
|
||||
const rowSelection = {
|
||||
onSelect: (record, selected) => { },
|
||||
onSelectAll: (selected, selectedRows) => { },
|
||||
onSelect: (record, selected) => {},
|
||||
onSelectAll: (selected, selectedRows) => {},
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
setSelectedKeys(selectedRows);
|
||||
},
|
||||
@@ -204,9 +207,11 @@ export const useRedemptionsData = () => {
|
||||
const handleRow = (record, index) => {
|
||||
// Local isExpired function
|
||||
const isExpired = (rec) => {
|
||||
return rec.status === REDEMPTION_STATUS.UNUSED &&
|
||||
return (
|
||||
rec.status === REDEMPTION_STATUS.UNUSED &&
|
||||
rec.expired_time !== 0 &&
|
||||
rec.expired_time < Math.floor(Date.now() / 1000);
|
||||
rec.expired_time < Math.floor(Date.now() / 1000)
|
||||
);
|
||||
};
|
||||
|
||||
if (record.status !== REDEMPTION_STATUS.UNUSED || isExpired(record)) {
|
||||
@@ -228,7 +233,7 @@ export const useRedemptionsData = () => {
|
||||
Modal.error({
|
||||
title: '无法复制到剪贴板,请手动复制',
|
||||
content: text,
|
||||
size: 'large'
|
||||
size: 'large',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -352,4 +357,4 @@ export const useRedemptionsData = () => {
|
||||
// Translation function
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
isAdmin,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string
|
||||
timestamp2string,
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -59,7 +59,9 @@ export const useTaskLogsData = () => {
|
||||
// User and admin
|
||||
const isAdminUser = isAdmin();
|
||||
// Role-specific storage key to prevent different roles from overwriting each other
|
||||
const STORAGE_KEY = isAdminUser ? 'task-logs-table-columns-admin' : 'task-logs-table-columns-user';
|
||||
const STORAGE_KEY = isAdminUser
|
||||
? 'task-logs-table-columns-admin'
|
||||
: 'task-logs-table-columns-user';
|
||||
|
||||
// Modal state
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -79,7 +81,7 @@ export const useTaskLogsData = () => {
|
||||
task_id: '',
|
||||
dateRange: [
|
||||
timestamp2string(zeroNow.getTime() / 1000),
|
||||
timestamp2string(now.getTime() / 1000 + 3600)
|
||||
timestamp2string(now.getTime() / 1000 + 3600),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -174,7 +176,11 @@ export const useTaskLogsData = () => {
|
||||
let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
|
||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
||||
|
||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
||||
if (
|
||||
formValues.dateRange &&
|
||||
Array.isArray(formValues.dateRange) &&
|
||||
formValues.dateRange.length === 2
|
||||
) {
|
||||
start_timestamp = formValues.dateRange[0];
|
||||
end_timestamp = formValues.dateRange[1];
|
||||
}
|
||||
@@ -208,7 +214,8 @@ export const useTaskLogsData = () => {
|
||||
// Load logs function
|
||||
const loadLogs = async (page = 1, size = pageSize) => {
|
||||
setLoading(true);
|
||||
const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
|
||||
const { channel_id, task_id, start_timestamp, end_timestamp } =
|
||||
getFormValues();
|
||||
let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
|
||||
let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
|
||||
let url = isAdminUser
|
||||
@@ -262,7 +269,8 @@ export const useTaskLogsData = () => {
|
||||
|
||||
// Initialize data
|
||||
useEffect(() => {
|
||||
const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
||||
const localPageSize =
|
||||
parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
||||
setPageSize(localPageSize);
|
||||
loadLogs(1, localPageSize).then();
|
||||
}, []);
|
||||
@@ -319,4 +327,4 @@ export const useTaskLogsData = () => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,12 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../helpers';
|
||||
import { API, copy, showError, showSuccess } from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
|
||||
@@ -139,9 +134,9 @@ export const useTokensData = (openFluentNotification) => {
|
||||
id: 'new-api',
|
||||
baseUrl: serverAddress,
|
||||
apiKey: 'sk-' + record.key,
|
||||
}
|
||||
};
|
||||
let encodedConfig = encodeURIComponent(
|
||||
btoa(JSON.stringify(cherryConfig))
|
||||
btoa(JSON.stringify(cherryConfig)),
|
||||
);
|
||||
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
||||
} else {
|
||||
@@ -235,8 +230,8 @@ export const useTokensData = (openFluentNotification) => {
|
||||
|
||||
// Row selection handlers
|
||||
const rowSelection = {
|
||||
onSelect: (record, selected) => { },
|
||||
onSelectAll: (selected, selectedRows) => { },
|
||||
onSelect: (record, selected) => {},
|
||||
onSelectAll: (selected, selectedRows) => {},
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
setSelectedKeys(selectedRows);
|
||||
},
|
||||
@@ -296,9 +291,9 @@ export const useTokensData = (openFluentNotification) => {
|
||||
icon: null,
|
||||
content: t('请选择你的复制方式'),
|
||||
footer: (
|
||||
<div className="flex gap-2">
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className="px-3 py-1 bg-gray-200 rounded"
|
||||
className='px-3 py-1 bg-gray-200 rounded'
|
||||
onClick={async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
@@ -312,7 +307,7 @@ export const useTokensData = (openFluentNotification) => {
|
||||
{t('名称+密钥')}
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 bg-blue-500 text-white rounded"
|
||||
className='px-3 py-1 bg-blue-500 text-white rounded'
|
||||
onClick={async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
@@ -389,4 +384,4 @@ export const useTokensData = (openFluentNotification) => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
renderLogContent,
|
||||
renderAudioModelPrice,
|
||||
renderClaudeModelPrice,
|
||||
renderModelPrice
|
||||
renderModelPrice,
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -75,7 +75,9 @@ export const useLogsData = () => {
|
||||
// User and admin
|
||||
const isAdminUser = isAdmin();
|
||||
// Role-specific storage key to prevent different roles from overwriting each other
|
||||
const STORAGE_KEY = isAdminUser ? 'logs-table-columns-admin' : 'logs-table-columns-user';
|
||||
const STORAGE_KEY = isAdminUser
|
||||
? 'logs-table-columns-admin'
|
||||
: 'logs-table-columns-user';
|
||||
|
||||
// Statistics state
|
||||
const [stat, setStat] = useState({
|
||||
@@ -352,28 +354,28 @@ export const useLogsData = () => {
|
||||
key: t('日志详情'),
|
||||
value: other?.claude
|
||||
? renderClaudeLogContent(
|
||||
other?.model_ratio,
|
||||
other.completion_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
other?.user_group_ratio,
|
||||
other.cache_ratio || 1.0,
|
||||
other.cache_creation_ratio || 1.0,
|
||||
)
|
||||
other?.model_ratio,
|
||||
other.completion_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
other?.user_group_ratio,
|
||||
other.cache_ratio || 1.0,
|
||||
other.cache_creation_ratio || 1.0,
|
||||
)
|
||||
: renderLogContent(
|
||||
other?.model_ratio,
|
||||
other.completion_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
other?.user_group_ratio,
|
||||
other.cache_ratio || 1.0,
|
||||
false,
|
||||
1.0,
|
||||
other.web_search || false,
|
||||
other.web_search_call_count || 0,
|
||||
other.file_search || false,
|
||||
other.file_search_call_count || 0,
|
||||
),
|
||||
other?.model_ratio,
|
||||
other.completion_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
other?.user_group_ratio,
|
||||
other.cache_ratio || 1.0,
|
||||
false,
|
||||
1.0,
|
||||
other.web_search || false,
|
||||
other.web_search_call_count || 0,
|
||||
other.file_search || false,
|
||||
other.file_search_call_count || 0,
|
||||
),
|
||||
});
|
||||
}
|
||||
if (logs[i].type === 2) {
|
||||
@@ -514,7 +516,7 @@ export const useLogsData = () => {
|
||||
// Page handlers
|
||||
const handlePageChange = (page) => {
|
||||
setActivePage(page);
|
||||
loadLogs(page, pageSize).then((r) => { });
|
||||
loadLogs(page, pageSize).then((r) => {});
|
||||
};
|
||||
|
||||
const handlePageSizeChange = async (size) => {
|
||||
@@ -624,4 +626,4 @@ export const useLogsData = () => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -282,4 +282,4 @@ export const useUsersData = () => {
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user