From 57d45005f473de7cd4c1722801d83eb6577c91b9 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Wed, 28 Feb 2024 23:27:03 +0800 Subject: [PATCH] feat: support import model from admin market and market editor stacked feature --- README.md | 6 +- adapter/chatgpt/chat.go | 4 + app/src-tauri/tauri.conf.json | 2 +- app/src/admin/channel.ts | 4 - app/src/admin/hook.tsx | 62 ++- app/src/assets/admin/market.less | 29 ++ app/src/components/admin/ChargeWidget.tsx | 18 +- .../admin/assemblies/ChannelEditor.tsx | 5 +- app/src/components/app/AppProvider.tsx | 27 +- app/src/conf/index.ts | 3 +- app/src/events/market.ts | 5 - app/src/resources/i18n/cn.json | 6 +- app/src/resources/i18n/en.json | 6 +- app/src/resources/i18n/ja.json | 6 +- app/src/resources/i18n/ru.json | 6 +- app/src/routes/admin/Market.tsx | 433 +++++++++++++----- app/src/routes/admin/Subscription.tsx | 10 +- app/src/routes/admin/System.tsx | 4 +- 18 files changed, 427 insertions(+), 209 deletions(-) delete mode 100644 app/src/events/market.ts diff --git a/README.md b/README.md index 086cbe3..9464a2c 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ _🚀 **Next Generation AI One-Stop Solution**_ 13. **支持偏好设置**, i18n 多语言支持, 自定义最大携带会话数, 最大回复 tokens 数, 模型参数自定义, 重置设置等 ![偏好设置](/screenshot/settings.png) 14. **附加功能** _(可通过后台系统设置设置附加功能的用户分组权限来开启和关闭)_ - - 🍎 **AI 项目生成器功能**, 支持生成过程查看, 支持 TAR / ZIP 格式下载 *(原理为预设实现, 可能不稳定)* - - 📂 **批量文章生成功能**, 支持生成进度条, 一键生成 DOCX 文档的 TAR / ZIP 格式下载 *(需要生成数量高于上游该模型的最高并发数)* - - 🥪 **AI 卡片功能** (已废弃), AI 的问题和答案以卡片形式展现, 可直接以图片 url 形式嵌入。*(原理为动态生成 SVG, 模型的相应时间可能较长而导致 TIMEOUT, 当前已放弃支持)* + - *[停止支持]* 🍎 **AI 项目生成器功能**, 支持生成过程查看, 支持 TAR / ZIP 格式下载 *(原理为预设实现, 可能不稳定)* + - *[停止支持]* 📂 **批量文章生成功能**, 支持生成进度条, 一键生成 DOCX 文档的 TAR / ZIP 格式下载 *(需要生成数量高于上游该模型的最高并发数)* + - *[已弃用]* 🥪 **AI 卡片功能** (已废弃), AI 的问题和答案以卡片形式展现, 可直接以图片 url 形式嵌入。*(原理为动态生成 SVG)* - 🔔 丰富用户管理和计费体系 1. **丰富且美观的仪表盘**, 包含本日和当月入账信息, 订阅人数, 模型使用统计折线图, 饼状图分析, 收入统计, 用户类型统计, 模型使用统计, 请求次数和模型错误数量统计图表等 ![仪表盘](/screenshot/admin.png) diff --git a/adapter/chatgpt/chat.go b/adapter/chatgpt/chat.go index 823c6c7..0469501 100644 --- a/adapter/chatgpt/chat.go +++ b/adapter/chatgpt/chat.go @@ -136,6 +136,10 @@ func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, callback global if err != nil { if form := processChatErrorResponse(err.Body); form != nil { + if form.Error.Type == "" { + form.Error.Type = "unknown" + } + msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type) return errors.New(hideRequestId(msg)) } diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 3185925..643b80d 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "chatnio", - "version": "3.9.2" + "version": "3.10.0" }, "tauri": { "allowlist": { diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 16dc1b9..e47eef3 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -213,10 +213,6 @@ export const defaultChannelModels: string[] = getUniqueList( Object.values(ChannelInfos).flatMap((info) => info.models), ); -export const channelModels: string[] = getUniqueList( - Object.values(ChannelInfos).flatMap((info) => info.models), -); - export const channelGroups: string[] = [ AnonymousType, NormalType, diff --git a/app/src/admin/hook.tsx b/app/src/admin/hook.tsx index 557ca60..afc396a 100644 --- a/app/src/admin/hook.tsx +++ b/app/src/admin/hook.tsx @@ -1,15 +1,55 @@ import { useMemo, useState } from "react"; import { getUniqueList } from "@/utils/base.ts"; import { defaultChannelModels } from "@/admin/channel.ts"; -import { getApiModels } from "@/api/v1.ts"; +import { getApiMarket, getApiModels } from "@/api/v1.ts"; import { useEffectAsync } from "@/utils/hook.ts"; +import { Model } from "@/api/types.ts"; -export const useSupportModels = () => { - const [supportModels, setSupportModels] = useState([]); +export type onStateChange = (state: boolean, data?: T) => void; + +export const useAllModels = (onStateChange?: onStateChange) => { + const [allModels, setAllModels] = useState([]); const update = async () => { + onStateChange?.(false, allModels); const models = await getApiModels(); - setSupportModels(models.data); + onStateChange?.(true, models.data); + + setAllModels(models.data); + }; + + useEffectAsync(update, []); + + return { + allModels, + update, + }; +}; + +export const useChannelModels = (onStateChange?: onStateChange) => { + const { allModels, update } = useAllModels(onStateChange); + + const channelModels = useMemo( + () => getUniqueList([...allModels, ...defaultChannelModels]), + [allModels], + ); + + return { + channelModels, + allModels, + update, + }; +}; + +export const useSupportModels = (onStateChange?: onStateChange) => { + const [supportModels, setSupportModels] = useState([]); + + const update = async () => { + onStateChange?.(false, supportModels); + const market = await getApiMarket(); + onStateChange?.(true, market); + + setSupportModels(market); }; useEffectAsync(update, []); @@ -19,17 +59,3 @@ export const useSupportModels = () => { update, }; }; - -export const useChannelModels = () => { - const { supportModels, update } = useSupportModels(); - - const channelModels = useMemo( - () => getUniqueList([...supportModels, ...defaultChannelModels]), - [supportModels], - ); - - return { - channelModels, - update, - }; -}; diff --git a/app/src/assets/admin/market.less b/app/src/assets/admin/market.less index c1c6f70..9d85efc 100644 --- a/app/src/assets/admin/market.less +++ b/app/src/assets/admin/market.less @@ -31,6 +31,22 @@ } } +.market-alert { + display: flex; + flex-direction: column; + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + margin-bottom: 1rem; + padding: 0.75rem; + + .market-alert-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; + } +} + .market-list { display: flex; flex-direction: column; @@ -68,6 +84,19 @@ border-color: hsl(var(--error)); } + &.stacked { + border-color: transparent !important; + padding: 0.25rem 0.5rem; + margin-bottom: 0.25rem; + + .market-row { + width: max-content; + flex-wrap: nowrap; + flex-shrink: 0; + gap: 0.5rem; + } + } + .market-tags { display: flex; flex-direction: row; diff --git a/app/src/components/admin/ChargeWidget.tsx b/app/src/components/admin/ChargeWidget.tsx index c46b6da..8c9d3b5 100644 --- a/app/src/components/admin/ChargeWidget.tsx +++ b/app/src/components/admin/ChargeWidget.tsx @@ -77,7 +77,7 @@ import { import { getUniqueList, parseNumber } from "@/utils/base.ts"; import { defaultChannelModels } from "@/admin/channel.ts"; import { getPricing } from "@/admin/datasets/charge.ts"; -import { useSupportModels } from "@/admin/hook.tsx"; +import { useAllModels } from "@/admin/hook.tsx"; const initialState: ChargeProps = { id: -1, @@ -370,7 +370,7 @@ type ChargeEditorProps = { dispatch: (action: any) => void; onRefresh: () => void; usedModels: string[]; - supportModels: string[]; + allModels: string[]; }; function ChargeEditor({ @@ -378,7 +378,7 @@ function ChargeEditor({ dispatch, onRefresh, usedModels, - supportModels, + allModels, }: ChargeEditorProps) { const { t } = useTranslation(); const { toast } = useToast(); @@ -386,8 +386,8 @@ function ChargeEditor({ const [model, setModel] = useState(""); const channelModels = useMemo( - () => getUniqueList([...supportModels, ...defaultChannelModels]), - [supportModels], + () => getUniqueList([...allModels, ...defaultChannelModels]), + [allModels], ); const unusedModels = useMemo(() => { @@ -719,7 +719,7 @@ function ChargeWidget() { const [form, dispatch] = useReducer(reducer, initialState); const [loading, setLoading] = useState(false); - const { supportModels, update } = useSupportModels(); + const { allModels, update } = useAllModels(); const currentModels = useMemo(() => { return data.flatMap((charge) => charge.models); @@ -731,10 +731,10 @@ function ChargeWidget() { const unusedModels = useMemo(() => { if (loading) return []; - return supportModels.filter( + return allModels.filter( (model) => !usedModels.includes(model) && model.trim() !== "", ); - }, [loading, supportModels, usedModels]); + }, [loading, allModels, usedModels]); async function refresh(ignoreUpdate?: boolean) { setLoading(true); @@ -763,7 +763,7 @@ function ChargeWidget() { onRefresh={refresh} form={form} dispatch={dispatch} - supportModels={supportModels} + allModels={allModels} usedModels={usedModels} /> diff --git a/app/src/components/admin/assemblies/ChannelEditor.tsx b/app/src/components/admin/assemblies/ChannelEditor.tsx index f680788..8d1571b 100644 --- a/app/src/components/admin/assemblies/ChannelEditor.tsx +++ b/app/src/components/admin/assemblies/ChannelEditor.tsx @@ -11,7 +11,6 @@ import { import { Channel, channelGroups, - channelModels, ChannelTypes, getChannelInfo, } from "@/admin/channel.ts"; @@ -47,6 +46,7 @@ import Paragraph, { ParagraphItem, } from "@/components/Paragraph.tsx"; import { MultiCombobox } from "@/components/ui/multi-combobox.tsx"; +import { useChannelModels } from "@/admin/hook.tsx"; type CustomActionProps = { onPost: (model: string) => void; @@ -136,11 +136,12 @@ function ChannelEditor({ }: ChannelEditorProps) { const { t } = useTranslation(); const info = useMemo(() => getChannelInfo(edit.type), [edit.type]); + const { channelModels } = useChannelModels(); const unusedModels = useMemo(() => { return channelModels.filter( (model) => !edit.models.includes(model) && model !== "", ); - }, [edit.models]); + }, [channelModels, edit.models]); const enabled = useMemo(() => validator(edit), [edit]); const [loading, setLoading] = useState(false); diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index f3fabf2..cafdca7 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -3,14 +3,8 @@ import { ThemeProvider } from "@/components/ThemeProvider.tsx"; import DialogManager from "@/dialogs"; import Broadcast from "@/components/Broadcast.tsx"; import { useEffectAsync } from "@/utils/hook.ts"; -import { allModels, supportModels } from "@/conf"; -import { channelModels } from "@/admin/channel.ts"; -import { - getApiCharge, - getApiMarket, - getApiModels, - getApiPlans, -} from "@/api/v1.ts"; +import { supportModels } from "@/conf"; +import { getApiCharge, getApiMarket, getApiPlans } from "@/api/v1.ts"; import { loadPreferenceModels } from "@/conf/storage.ts"; import { resetJsArray } from "@/utils/base.ts"; import { useDispatch } from "react-redux"; @@ -18,7 +12,6 @@ import { initChatModels, updateMasks } from "@/store/chat.ts"; import { Model } from "@/api/types.ts"; import { ChargeProps, nonBilling } from "@/admin/charge.ts"; import { dispatchSubscriptionData, setTheme } from "@/store/globals.ts"; -import { marketEvent } from "@/events/market.ts"; import { infoEvent } from "@/events/info.ts"; import { setForm } from "@/store/info.ts"; import { themeEvent } from "@/events/theme.ts"; @@ -34,8 +27,6 @@ function AppProvider() { }, []); useEffectAsync(async () => { - marketEvent.emit(false); - const market = await getApiMarket(); const charge = await getApiCharge(); @@ -51,22 +42,10 @@ function AppProvider() { }); resetJsArray(supportModels, loadPreferenceModels(market)); - resetJsArray( - allModels, - supportModels.map((model) => model.id), - ); initChatModels(dispatch); - const models = await getApiModels(); - models.data.forEach((model: string) => { - if (!allModels.includes(model)) allModels.push(model); - if (!channelModels.includes(model)) channelModels.push(model); - }); - dispatchSubscriptionData(dispatch, await getApiPlans()); - - marketEvent.emit(true); - }, [allModels]); + }, []); return ( <> diff --git a/app/src/conf/index.ts b/app/src/conf/index.ts index 15f04b0..f075289 100644 --- a/app/src/conf/index.ts +++ b/app/src/conf/index.ts @@ -9,7 +9,7 @@ import { syncSiteInfo } from "@/admin/api/info.ts"; import { getOfflineModels, loadPreferenceModels } from "@/conf/storage.ts"; import { setAxiosConfig } from "@/conf/api.ts"; -export const version = "3.9.2"; // version of the current build +export const version = "3.10.0"; // version of the current build export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin) export const deploy: boolean = true; // is production environment (for api endpoint) export const tokenField = getTokenField(deploy); // token field name for storing token @@ -18,7 +18,6 @@ export let apiEndpoint: string = getRestApi(deploy); // api endpoint for rest ap export let websocketEndpoint: string = getWebsocketApi(deploy); // api endpoint for websocket calls export let supportModels: Model[] = loadPreferenceModels(getOfflineModels()); // support models in model market of the current site -export let allModels: string[] = supportModels.map((model) => model.id); // all support model id list of the current site setAxiosConfig({ endpoint: apiEndpoint, diff --git a/app/src/events/market.ts b/app/src/events/market.ts deleted file mode 100644 index 9b2ecaa..0000000 --- a/app/src/events/market.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EventCommitter } from "@/events/struct.ts"; - -export const marketEvent = new EventCommitter({ - name: "market", -}); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index 0d36727..bab7d36 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -494,6 +494,8 @@ "market": { "title": "模型市场", "model-name": "模型名称", + "not-use": "部分模型未使用", + "import-all": "导入全部", "new-model": "新建模型", "model-name-placeholder": "请输入模型昵称 (如:GPT-4)", "model-id": "模型 ID", @@ -518,12 +520,12 @@ "sync-failed": "同步失败", "sync-failed-prompt": "地址无法请求或者模型市场模型为空\n(端点:{{endpoint}})", "sync-success": "同步成功", - "sync-success-prompt": "已从上游同步,添加 {{length}} 个模型,请检查后点击更新以保存。", + "sync-success-prompt": "已从上游同步,添加 {{length}} 个模型,请检查后点击提交方可生效,否则将不会保存", "sync-items": "共发现 {{length}} 个模型,已有模型 {{exist}} 个(不会覆盖),新增模型 {{new}} 个(同步全部),本站点渠道已支持模型 {{support}} 个(同步已支持模型)", "sync-all": "同步全部 ({{length}} 个)", "sync-self": "同步已支持模型 ({{length}} 个)", "update-success": "更新成功", - "update-success-prompt": "模型市场设置已成功提交更新至服务器。", + "update-success-prompt": "模型市场已成功更新(刷新浏览器即可立即应用)", "update-failed": "更新失败", "update-failed-prompt": "更新请求失败,原因:{{reason}}" }, diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 1eec052..d0311bb 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -579,7 +579,7 @@ "model-is-default-tip": "Whether the model is added to the default model list (models not added to the default model list will not appear in the home model list by default)", "model-tag": "Model label", "update-success": "Upgrade successful", - "update-success-prompt": "Model Marketplace settings were successfully submitted for update to the server.", + "update-success-prompt": "Model Marketplace updated successfully (refresh your browser to apply now)", "update-failed": "Update failed", "update-failed-prompt": "Update request failed for {{reason}}", "model-image": "Model Picture", @@ -599,7 +599,9 @@ "sync-failed-prompt": "Address could not be requested or model market model is empty\n(Endpoint: {{endpoint}})", "sync-items": "A total of {{length}} models have been found, {{exist}} models have been found (will not be overwritten), {{new}} models have been added (all synchronized), {{support}} models have been supported by this site channel (synchronized supported models)", "sync-success": "Sync successfully.", - "sync-success-prompt": "Synced from upstream, added {{length}} models, please check and click Update to save." + "sync-success-prompt": "Synced from upstream, added {{length}} models, please check and click submit to take effect, otherwise it will not be saved", + "not-use": "Some models are not used", + "import-all": "Import Full" }, "model-chart-tip": "Token usage", "subscription": "Subscription Management", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index 7fe7fe5..6b5264f 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -579,7 +579,7 @@ "model-is-default-tip": "モデルがデフォルトモデルリストに追加されるかどうか(デフォルトでは、デフォルトモデルリストに追加されていないモデルはホームモデルリストに表示されません)", "model-tag": "モデルラベル", "update-success": "正常に更新されました", - "update-success-prompt": "モデルマーケットプレイスの設定は、サーバーへの更新のために正常に送信されました。", + "update-success-prompt": "モデルマーケットプレイスが正常に更新されました(今すぐ適用するにはブラウザを更新してください)", "update-failed": "更新に失敗", "update-failed-prompt": "{{reason}}の更新リクエストが失敗しました", "model-image": "モデル写真", @@ -599,7 +599,9 @@ "sync-failed-prompt": "住所をリクエストできなかったか、モデルマーケットモデルが空です\n(エンドポイント:{{ endpoint }})", "sync-items": "合計{{length}}個のモデルが見つかりました。{{exist}}個のモデルが見つかりました(上書きされません)。{{new}}個のモデルが追加されました(すべて同期済み)。{{support}}個のモデルがこのサイトチャネルでサポートされています(同期対応モデル)", "sync-success": "同期成功", - "sync-success-prompt": "アップストリームから同期され、{{length}}モデルが追加されました。確認して[更新]をクリックして保存してください。" + "sync-success-prompt": "アップストリームから同期され、{{length}}モデルが追加されました。確認して送信をクリックして有効にしてください。有効にしないと保存されません", + "not-use": "一部のモデルは使用されていません", + "import-all": "すべてインポート..." }, "model-chart-tip": "トークンの使用状況", "subscription": "サブスクリプション管理", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 06bc737..1d70343 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -579,7 +579,7 @@ "model-is-default-tip": "Добавлена ли модель в список моделей по умолчанию (модели, не добавленные в список моделей по умолчанию, не будут отображаться в списке моделей дома по умолчанию)", "model-tag": "Этикетка модели", "update-success": "Успешно обновлено", - "update-success-prompt": "Настройки Model Marketplace успешно отправлены на обновление на сервер.", + "update-success-prompt": "Модель Marketplace успешно обновлена (обновите браузер, чтобы подать заявку сейчас)", "update-failed": "Ошибка обновления", "update-failed-prompt": "Запрос на обновление не выполнен по {{reason}}", "model-image": "Изображение модели", @@ -599,7 +599,9 @@ "sync-failed-prompt": "Адрес не может быть запрошен или модель рынка пуста\n(Конечная точка: {{endpoint}})", "sync-items": "Всего найдено {{length}} моделей, {{exist}} моделей найдено (не будет перезаписано), {{new}} моделей добавлено (все синхронизировано), {{support}} моделей поддерживается этим каналом сайта (синхронизированные поддерживаемые модели)", "sync-success": "Успешная синхронизация", - "sync-success-prompt": "Синхронизировано с вышестоящими, добавлено {{length}} моделей, проверьте и нажмите «Обновить», чтобы сохранить." + "sync-success-prompt": "Синхронизировано с вышестоящими, добавлено {{length}} моделей, проверьте и нажмите «Отправить», чтобы вступить в силу, иначе оно не будет сохранено", + "not-use": "Некоторые модели не используются", + "import-all": "Импортировать все..." }, "model-chart-tip": "Использование токенов", "subscription": "Управление подписками", diff --git a/app/src/routes/admin/Market.tsx b/app/src/routes/admin/Market.tsx index c8ae31f..0200fbc 100644 --- a/app/src/routes/admin/Market.tsx +++ b/app/src/routes/admin/Market.tsx @@ -7,17 +7,22 @@ import { import { useTranslation } from "react-i18next"; import { Dispatch, useMemo, useReducer, useState } from "react"; import { Model as RawModel } from "@/api/types.ts"; -import { allModels, supportModels } from "@/conf"; import { Input } from "@/components/ui/input.tsx"; import { + Activity, + AlertCircle, ChevronDown, ChevronUp, HelpCircle, - Loader2, + Import, + Maximize, + Minimize, Plus, + RotateCw, + Save, Trash2, } from "lucide-react"; -import { generateRandomChar, isUrl, resetJsArray } from "@/utils/base.ts"; +import { generateRandomChar, isUrl } from "@/utils/base.ts"; import Require from "@/components/Require.tsx"; import { Textarea } from "@/components/ui/textarea.tsx"; import Tips from "@/components/Tips.tsx"; @@ -27,7 +32,6 @@ import { marketEditableTags, modelImages } from "@/admin/market.ts"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Combobox } from "@/components/ui/combo-box.tsx"; -import { channelModels } from "@/admin/channel.ts"; import { cn } from "@/components/ui/lib/utils.ts"; import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx"; import { useToast } from "@/components/ui/use-toast.ts"; @@ -39,19 +43,11 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog.tsx"; -import { - getApiCharge, - getApiMarket, - getApiModels, - getV1Path, -} from "@/api/v1.ts"; -import { ChargeProps, nonBilling } from "@/admin/charge.ts"; -import { useDispatch, useSelector } from "react-redux"; -import { selectModel, setModel, setModelList } from "@/store/chat.ts"; -import { loadPreferenceModels } from "@/conf/storage.ts"; +import { getApiMarket, getV1Path } from "@/api/v1.ts"; import { updateMarket } from "@/admin/api/market.ts"; -import { marketEvent } from "@/events/market.ts"; import { toast } from "sonner"; +import { useChannelModels, useSupportModels } from "@/admin/hook.tsx"; +import Icon from "@/components/utils/Icon.tsx"; type Model = RawModel & { seed?: string; @@ -110,6 +106,38 @@ function reducer(state: MarketForm, action: any): MarketForm { seed: generateSeed(), }, ]; + case "new-template": + return [ + ...state, + { + id: action.payload.id, + name: action.payload.name, + free: false, + auth: false, + description: "", + high_context: false, + default: false, + tag: [], + avatar: modelImages[0], + seed: generateSeed(), + }, + ]; + case "batch-new-template": + return [ + ...state, + ...action.payload.map((model: { id: string; name: string }) => ({ + id: model.id, + name: model.name, + free: false, + auth: false, + description: "", + high_context: false, + default: false, + tag: [], + avatar: modelImages[0], + seed: generateSeed(), + })), + ]; case "remove": let { idx } = action.payload; return [...state.slice(0, idx), ...state.slice(idx + 1)]; @@ -377,9 +405,18 @@ type MarketItemProps = { form: MarketForm; dispatch: Dispatch; index: number; + stacked: boolean; + channelModels: string[]; }; -function MarketItem({ model, form, dispatch, index }: MarketItemProps) { +function MarketItem({ + model, + form, + stacked, + dispatch, + index, + channelModels, +}: MarketItemProps) { const { t } = useTranslation(); const checked = useMemo( @@ -387,7 +424,63 @@ function MarketItem({ model, form, dispatch, index }: MarketItemProps) { [model], ); - return ( + const Actions = () => ( +
+ {!stacked &&
} + + + + + +
+ ); + + return !stacked ? (
@@ -489,61 +582,27 @@ function MarketItem({ model, form, dispatch, index }: MarketItemProps) { {t("admin.market.model-image")}
-
-
- - - - - -
+
+ ) : ( +
+ { + dispatch({ + type: "update-name", + payload: { + idx: index, + name: e.target.value, + }, + }); + }} + /> + +
); } @@ -551,9 +610,17 @@ type SyncDialogProps = { open: boolean; setOpen: (state: boolean) => void; onConfirm: (form: MarketForm) => Promise; + allModels: string[]; + supportModels: Model[]; }; -function SyncDialog({ open, setOpen, onConfirm }: SyncDialogProps) { +function SyncDialog({ + open, + setOpen, + allModels, + supportModels, + onConfirm, +}: SyncDialogProps) { const { t } = useTranslation(); const [form, setForm] = useState([]); const { toast } = useToast(); @@ -666,48 +733,118 @@ function SyncDialog({ open, setOpen, onConfirm }: SyncDialogProps) { ); } +type MarketAlertProps = { + open: boolean; + models: string[]; + onImport: (model: string) => void; + onImportAll: () => void; +}; + +function MarketAlert({ + open, + models, + onImport, + onImportAll, +}: MarketAlertProps) { + const { t } = useTranslation(); + + return ( + open && + models.length > 0 && ( +
+
+ + {t("admin.market.not-use")} + +
+
+ {models.map((model, index) => ( + + ))} +
+
+ ) + ); +} + +function getModelName(id: string): string { + // replace all `-` to ` ` except first `-` keep it + let begin = true; + + return id + .replace(/-/g, (l) => { + if (begin) { + begin = false; + return l; + } + return " "; + }) + .replace(/\b\w/g, (l) => l.toUpperCase()) + .replace(/Gpt/g, "GPT") + .replace(/Tts/g, "TTS") + .replace(/Dall-E/g, "DALL-E") + .replace(/Dalle/g, "DALLE") + .replace(/Glm/g, "GLM") + .trim(); +} + function Market() { const { t } = useTranslation(); - const [form, dispatch] = useReducer(reducer, supportModels); - const [loading, setLoading] = useState(false); - const model = useSelector(selectModel); + const [stepSupport, setStepSupport] = useState(false); + const [stepAll, setStepAll] = useState(false); - const globalDispatch = useDispatch(); + const [stacked, setStacked] = useState(false); - const sync = async (): Promise => { - const market = await getApiMarket(); - const charge = await getApiCharge(); + const [form, dispatch] = useReducer(reducer, []); + const [open, setOpen] = useState(false); - market.forEach((item: Model) => { - const instance = charge.find((i: ChargeProps) => - i.models.includes(item.id), - ); - if (!instance) return; + const { supportModels, update: updateSuppportModels } = useSupportModels( + (state, data) => { + setStepSupport(!state); + state && dispatch({ type: "set", payload: data }); + }, + ); - item.free = instance.type === nonBilling; - item.auth = !item.free || !instance.anonymous; - item.price = { ...instance }; - }); + const { + allModels, + channelModels, + update: updateAllModels, + } = useChannelModels((state) => setStepAll(!state)); - resetJsArray(supportModels, loadPreferenceModels(market)); - resetJsArray( - allModels, - supportModels.map((model) => model.id), - ); - globalDispatch(setModelList(supportModels)); - allModels.length > 0 && - !allModels.includes(model) && - globalDispatch(setModel(allModels[0])); + const unusedModels = useMemo( + (): string[] => + allModels.filter((model) => !form.map((m) => m.id).includes(model)), + [form, allModels], + ); - const models = await getApiModels(); - models.data.forEach((model: string) => { - if (!allModels.includes(model)) allModels.push(model); - if (!channelModels.includes(model)) channelModels.push(model); - }); + const loading = stepSupport || stepAll; + const update = async () => { + await updateSuppportModels(); + await updateAllModels(); }; - const update = async (): Promise => { + const sync = async (): Promise => {}; + + const submit = async (): Promise => { const preflight = form.filter( (model) => model.id.trim().length > 0 && model.name.trim().length > 0, ); @@ -733,18 +870,13 @@ function Market() { dispatch({ type: "add-multiple", payload: [...data] }); }; - marketEvent.addEventListener((state: boolean) => { - setLoading(!state); - !state && dispatch({ type: "set", payload: [...supportModels] }); - }); - - const [open, setOpen] = useState(false); - return (
{ await migrate(data); toast(t("admin.market.sync-success"), { @@ -757,25 +889,70 @@ function Market() { }} /> - - - {t("admin.market.title")} - {loading && ( - - )} - - + + {t("admin.market.title")} +
+ +
+ + + +
+ { + dispatch({ + type: "new-template", + payload: { + id: model, + name: getModelName(model), + }, + }); + }} + onImportAll={() => { + dispatch({ + type: "batch-new-template", + payload: unusedModels.map((model) => ({ + id: model, + name: getModelName(model), + })), + }); + }} + />
{form.length > 0 ? ( form.map((model, index) => ( @@ -783,8 +960,10 @@ function Market() { key={index} model={model} form={form} + stacked={stacked} dispatch={dispatch} index={index} + channelModels={channelModels} /> )) ) : ( @@ -802,7 +981,7 @@ function Market() { {t("admin.market.new-model")} -
diff --git a/app/src/routes/admin/Subscription.tsx b/app/src/routes/admin/Subscription.tsx index aa1ce2b..742b698 100644 --- a/app/src/routes/admin/Subscription.tsx +++ b/app/src/routes/admin/Subscription.tsx @@ -442,6 +442,11 @@ function PlanConfig() { }} />
+ +
- -