diff --git a/web/src/components/table/tokens/index.jsx b/web/src/components/table/tokens/index.jsx index 85229b26..ee1ff75d 100644 --- a/web/src/components/table/tokens/index.jsx +++ b/web/src/components/table/tokens/index.jsx @@ -17,7 +17,9 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { Notification, Button, Space, Toast, Typography, Select } from '@douyinfe/semi-ui'; +import { API, showError, getModelCategories, selectFilter } from '../../../helpers'; import CardPro from '../../common/ui/CardPro'; import TokensTable from './TokensTable.jsx'; import TokensActions from './TokensActions.jsx'; @@ -28,9 +30,243 @@ import { useTokensData } from '../../../hooks/tokens/useTokensData'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { createCardProPagination } from '../../../helpers/utils'; -const TokensPage = () => { - const tokensData = useTokensData(); +function TokensPage() { + // Define the function first, then pass it into the hook to avoid TDZ errors + const openFluentNotificationRef = useRef(null); + const tokensData = useTokensData((key) => openFluentNotificationRef.current?.(key)); const isMobile = useIsMobile(); + const latestRef = useRef({ tokens: [], selectedKeys: [], t: (k) => k, selectedModel: '', prefillKey: '' }); + const [modelOptions, setModelOptions] = useState([]); + const [selectedModel, setSelectedModel] = useState(''); + const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false); + const [prefillKey, setPrefillKey] = useState(''); + + // Keep latest data for handlers inside notifications + useEffect(() => { + latestRef.current = { + tokens: tokensData.tokens, + selectedKeys: tokensData.selectedKeys, + t: tokensData.t, + selectedModel, + prefillKey, + }; + }, [tokensData.tokens, tokensData.selectedKeys, tokensData.t, selectedModel, prefillKey]); + + const loadModels = async () => { + try { + const res = await API.get('/api/user/models'); + const { success, message, data } = res.data || {}; + if (success) { + const categories = getModelCategories(tokensData.t); + const options = (data || []).map((model) => { + let icon = null; + for (const [key, category] of Object.entries(categories)) { + if (key !== 'all' && category.filter({ model_name: model })) { + icon = category.icon; + break; + } + } + return { + label: ( + + {icon} + {model} + + ), + value: model, + }; + }); + setModelOptions(options); + } else { + showError(tokensData.t(message)); + } + } catch (e) { + showError(e.message || 'Failed to load models'); + } + }; + + function openFluentNotification(key) { + const { t } = latestRef.current; + const SUPPRESS_KEY = 'fluent_notify_suppressed'; + if (localStorage.getItem(SUPPRESS_KEY) === '1') return; + const container = document.getElementById('fluent-new-api-container'); + if (!container) { + Toast.warning(t('未检测到 Fluent 容器,请确认扩展已启用')); + return; + } + setPrefillKey(key || ''); + setFluentNoticeOpen(true); + if (modelOptions.length === 0) { + // fire-and-forget; a later effect will refresh the notice content + loadModels() + } + Notification.info({ + id: 'fluent-detected', + title: t('检测到 Fluent(流畅阅读)'), + content: ( +
+
+ {prefillKey + ? t('已检测到 Fluent 扩展,已从操作中指定密钥,将使用该密钥进行填充。请选择模型后继续。') + : t('已检测到 Fluent 扩展,请选择模型后可一键填充当前选中令牌(或本页第一个令牌)。')} +
+
+