mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 13:00:14 +09:00
feat: support import model from admin market and market editor stacked feature
This commit is contained in:
parent
7ba52d62d3
commit
57d45005f4
@ -47,9 +47,9 @@ _🚀 **Next Generation AI One-Stop Solution**_
|
||||
13. **支持偏好设置**, i18n 多语言支持, 自定义最大携带会话数, 最大回复 tokens 数, 模型参数自定义, 重置设置等
|
||||

|
||||
14. **附加功能** _(可通过后台系统设置设置附加功能的用户分组权限来开启和关闭)_
|
||||
- 🍎 **AI 项目生成器功能**, 支持生成过程查看, 支持 TAR / ZIP 格式下载 *(原理为预设实现, 可能不稳定)*
|
||||
- 📂 **批量文章生成功能**, 支持生成进度条, 一键生成 DOCX 文档的 TAR / ZIP 格式下载 *(需要生成数量高于上游该模型的最高并发数)*
|
||||
- 🥪 **AI 卡片功能** (已废弃), AI 的问题和答案以卡片形式展现, 可直接以图片 url 形式嵌入。*(原理为动态生成 SVG, 模型的相应时间可能较长而导致 TIMEOUT, 当前已放弃支持)*
|
||||
- *[停止支持]* 🍎 **AI 项目生成器功能**, 支持生成过程查看, 支持 TAR / ZIP 格式下载 *(原理为预设实现, 可能不稳定)*
|
||||
- *[停止支持]* 📂 **批量文章生成功能**, 支持生成进度条, 一键生成 DOCX 文档的 TAR / ZIP 格式下载 *(需要生成数量高于上游该模型的最高并发数)*
|
||||
- *[已弃用]* 🥪 **AI 卡片功能** (已废弃), AI 的问题和答案以卡片形式展现, 可直接以图片 url 形式嵌入。*(原理为动态生成 SVG)*
|
||||
- 🔔 丰富用户管理和计费体系
|
||||
1. **丰富且美观的仪表盘**, 包含本日和当月入账信息, 订阅人数, 模型使用统计折线图, 饼状图分析, 收入统计, 用户类型统计, 模型使用统计, 请求次数和模型错误数量统计图表等
|
||||

|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "chatnio",
|
||||
"version": "3.9.2"
|
||||
"version": "3.10.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -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,
|
||||
|
@ -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<string[]>([]);
|
||||
export type onStateChange<T> = (state: boolean, data?: T) => void;
|
||||
|
||||
export const useAllModels = (onStateChange?: onStateChange<string[]>) => {
|
||||
const [allModels, setAllModels] = useState<string[]>([]);
|
||||
|
||||
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<string[]>) => {
|
||||
const { allModels, update } = useAllModels(onStateChange);
|
||||
|
||||
const channelModels = useMemo(
|
||||
() => getUniqueList([...allModels, ...defaultChannelModels]),
|
||||
[allModels],
|
||||
);
|
||||
|
||||
return {
|
||||
channelModels,
|
||||
allModels,
|
||||
update,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSupportModels = (onStateChange?: onStateChange<Model[]>) => {
|
||||
const [supportModels, setSupportModels] = useState<Model[]>([]);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
<ChargeTable data={data} dispatch={dispatch} onRefresh={refresh} />
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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,
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { EventCommitter } from "@/events/struct.ts";
|
||||
|
||||
export const marketEvent = new EventCommitter<boolean>({
|
||||
name: "market",
|
||||
});
|
@ -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}}"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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": "サブスクリプション管理",
|
||||
|
@ -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": "Управление подписками",
|
||||
|
@ -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<any>;
|
||||
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 = () => (
|
||||
<div className={`market-row`}>
|
||||
{!stacked && <div className={`grow`} />}
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "add-below",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Plus className={`h-4 w-4`} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "upward",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
disabled={index === 0}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "downward",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
disabled={index === form.length - 1}
|
||||
>
|
||||
<ChevronDown className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "remove",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return !stacked ? (
|
||||
<div className={cn("market-item", !checked && "error")}>
|
||||
<div className={`model-wrapper`}>
|
||||
<div className={`market-row`}>
|
||||
@ -489,61 +582,27 @@ function MarketItem({ model, form, dispatch, index }: MarketItemProps) {
|
||||
<span>{t("admin.market.model-image")}</span>
|
||||
<MarketImage image={model.avatar} idx={index} dispatch={dispatch} />
|
||||
</div>
|
||||
<div className={`market-row`}>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "add-below",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Plus className={`h-4 w-4`} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "upward",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
disabled={index === 0}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "downward",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
disabled={index === form.length - 1}
|
||||
>
|
||||
<ChevronDown className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "remove",
|
||||
payload: { idx: index },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
<Actions />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("market-item stacked", !checked && "error")}>
|
||||
<Input
|
||||
value={model.name}
|
||||
placeholder={t("admin.market.model-name-placeholder")}
|
||||
className={`grow mr-2`}
|
||||
onChange={(e) => {
|
||||
dispatch({
|
||||
type: "update-name",
|
||||
payload: {
|
||||
idx: index,
|
||||
name: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Actions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -551,9 +610,17 @@ type SyncDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
onConfirm: (form: MarketForm) => Promise<boolean>;
|
||||
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<MarketForm>([]);
|
||||
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 && (
|
||||
<div className={`market-alert`}>
|
||||
<div
|
||||
className={`flex flex-row items-center mb-2 whitespace-nowrap select-none`}
|
||||
>
|
||||
<AlertCircle className={`h-4 w-4 mr-2 translate-y-[1px]`} />
|
||||
<span>{t("admin.market.not-use")}</span>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
size={`sm`}
|
||||
className={`ml-auto`}
|
||||
onClick={onImportAll}
|
||||
>
|
||||
<Import className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.market.import-all")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`market-alert-wrapper`}>
|
||||
{models.map((model, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={`outline`}
|
||||
size={`sm`}
|
||||
className={`text-sm`}
|
||||
onClick={() => onImport(model)}
|
||||
>
|
||||
{model}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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<boolean>(false);
|
||||
|
||||
const model = useSelector(selectModel);
|
||||
const [stepSupport, setStepSupport] = useState<boolean>(false);
|
||||
const [stepAll, setStepAll] = useState<boolean>(false);
|
||||
|
||||
const globalDispatch = useDispatch();
|
||||
const [stacked, setStacked] = useState<boolean>(false);
|
||||
|
||||
const sync = async (): Promise<void> => {
|
||||
const market = await getApiMarket();
|
||||
const charge = await getApiCharge();
|
||||
const [form, dispatch] = useReducer(reducer, []);
|
||||
const [open, setOpen] = useState<boolean>(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<void> => {
|
||||
const sync = async (): Promise<void> => {};
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
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<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className={`market`}>
|
||||
<SyncDialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
allModels={allModels}
|
||||
supportModels={supportModels}
|
||||
onConfirm={async (data: MarketForm) => {
|
||||
await migrate(data);
|
||||
toast(t("admin.market.sync-success"), {
|
||||
@ -757,25 +889,70 @@ function Market() {
|
||||
}}
|
||||
/>
|
||||
<Card className={`admin-card market-card`}>
|
||||
<CardHeader className={`flex flex-row items-center select-none`}>
|
||||
<CardTitle>
|
||||
{t("admin.market.title")}
|
||||
{loading && (
|
||||
<Loader2
|
||||
className={`inline-block h-4 w-4 ml-2 animate-spin relative top-[-2px]`}
|
||||
/>
|
||||
)}
|
||||
</CardTitle>
|
||||
<Button
|
||||
className={`ml-auto mt-0 whitespace-nowrap`}
|
||||
size={`sm`}
|
||||
style={{ marginTop: 0 }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("admin.market.sync")}
|
||||
</Button>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("admin.market.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`market-actions flex flex-row items-center mb-4`}>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`whitespace-nowrap`}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Activity className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.market.sync")}
|
||||
</Button>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
variant={`outline`}
|
||||
size={`icon`}
|
||||
className={`mr-2`}
|
||||
onClick={() => setStacked(!stacked)}
|
||||
>
|
||||
<Icon
|
||||
icon={stacked ? <Minimize /> : <Maximize />}
|
||||
className={`h-4 w-4`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
className={`mr-2`}
|
||||
onClick={update}
|
||||
>
|
||||
<RotateCw className={cn("h-4 w-4", loading && "animate-spin")} />
|
||||
</Button>
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`mr-2`}
|
||||
loading={true}
|
||||
onClick={submit}
|
||||
>
|
||||
<Save className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
<MarketAlert
|
||||
open={!loading}
|
||||
models={unusedModels}
|
||||
onImport={(model: string) => {
|
||||
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),
|
||||
})),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className={`market-list`}>
|
||||
{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() {
|
||||
<Plus className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.market.new-model")}
|
||||
</Button>
|
||||
<Button size={`sm`} onClick={update} loading={true}>
|
||||
<Button size={`sm`} onClick={submit} loading={true}>
|
||||
{t("admin.market.migrate")}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -442,6 +442,11 @@ function PlanConfig() {
|
||||
}}
|
||||
/>
|
||||
<div className={`plan-config-row pb-2`}>
|
||||
<Button variant={`outline`} onClick={() => setOpen(true)}>
|
||||
<Activity className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.plan.sync")}
|
||||
</Button>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
variant={`outline`}
|
||||
size={`icon`}
|
||||
@ -453,11 +458,6 @@ function PlanConfig() {
|
||||
className={`h-4 w-4`}
|
||||
/>
|
||||
</Button>
|
||||
<Button variant={`outline`} onClick={() => setOpen(true)}>
|
||||
<Activity className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.plan.sync")}
|
||||
</Button>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`mr-2`}
|
||||
|
@ -51,8 +51,8 @@ import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
|
||||
import { allGroups } from "@/utils/groups.ts";
|
||||
import { channelModels } from "@/admin/channel.ts";
|
||||
import { supportModels } from "@/conf";
|
||||
import { useChannelModels } from "@/admin/hook.tsx";
|
||||
|
||||
type CompProps<T> = {
|
||||
data: T;
|
||||
@ -593,6 +593,8 @@ function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
|
||||
function Common({ data, dispatch, onChange }: CompProps<CommonState>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { channelModels } = useChannelModels();
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
title={t("admin.system.common")}
|
||||
|
Loading…
Reference in New Issue
Block a user