mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 05:20:15 +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 数, 模型参数自定义, 重置设置等
|
13. **支持偏好设置**, i18n 多语言支持, 自定义最大携带会话数, 最大回复 tokens 数, 模型参数自定义, 重置设置等
|
||||||

|

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

|

|
||||||
|
@ -136,6 +136,10 @@ func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, callback global
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if form := processChatErrorResponse(err.Body); form != 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)
|
msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type)
|
||||||
return errors.New(hideRequestId(msg))
|
return errors.New(hideRequestId(msg))
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "chatnio",
|
"productName": "chatnio",
|
||||||
"version": "3.9.2"
|
"version": "3.10.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -213,10 +213,6 @@ export const defaultChannelModels: string[] = getUniqueList(
|
|||||||
Object.values(ChannelInfos).flatMap((info) => info.models),
|
Object.values(ChannelInfos).flatMap((info) => info.models),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const channelModels: string[] = getUniqueList(
|
|
||||||
Object.values(ChannelInfos).flatMap((info) => info.models),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const channelGroups: string[] = [
|
export const channelGroups: string[] = [
|
||||||
AnonymousType,
|
AnonymousType,
|
||||||
NormalType,
|
NormalType,
|
||||||
|
@ -1,15 +1,55 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { getUniqueList } from "@/utils/base.ts";
|
import { getUniqueList } from "@/utils/base.ts";
|
||||||
import { defaultChannelModels } from "@/admin/channel.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 { useEffectAsync } from "@/utils/hook.ts";
|
||||||
|
import { Model } from "@/api/types.ts";
|
||||||
|
|
||||||
export const useSupportModels = () => {
|
export type onStateChange<T> = (state: boolean, data?: T) => void;
|
||||||
const [supportModels, setSupportModels] = useState<string[]>([]);
|
|
||||||
|
export const useAllModels = (onStateChange?: onStateChange<string[]>) => {
|
||||||
|
const [allModels, setAllModels] = useState<string[]>([]);
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
|
onStateChange?.(false, allModels);
|
||||||
const models = await getApiModels();
|
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, []);
|
useEffectAsync(update, []);
|
||||||
@ -19,17 +59,3 @@ export const useSupportModels = () => {
|
|||||||
update,
|
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 {
|
.market-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -68,6 +84,19 @@
|
|||||||
border-color: hsl(var(--error));
|
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 {
|
.market-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -77,7 +77,7 @@ import {
|
|||||||
import { getUniqueList, parseNumber } from "@/utils/base.ts";
|
import { getUniqueList, parseNumber } from "@/utils/base.ts";
|
||||||
import { defaultChannelModels } from "@/admin/channel.ts";
|
import { defaultChannelModels } from "@/admin/channel.ts";
|
||||||
import { getPricing } from "@/admin/datasets/charge.ts";
|
import { getPricing } from "@/admin/datasets/charge.ts";
|
||||||
import { useSupportModels } from "@/admin/hook.tsx";
|
import { useAllModels } from "@/admin/hook.tsx";
|
||||||
|
|
||||||
const initialState: ChargeProps = {
|
const initialState: ChargeProps = {
|
||||||
id: -1,
|
id: -1,
|
||||||
@ -370,7 +370,7 @@ type ChargeEditorProps = {
|
|||||||
dispatch: (action: any) => void;
|
dispatch: (action: any) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
usedModels: string[];
|
usedModels: string[];
|
||||||
supportModels: string[];
|
allModels: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChargeEditor({
|
function ChargeEditor({
|
||||||
@ -378,7 +378,7 @@ function ChargeEditor({
|
|||||||
dispatch,
|
dispatch,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
usedModels,
|
usedModels,
|
||||||
supportModels,
|
allModels,
|
||||||
}: ChargeEditorProps) {
|
}: ChargeEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -386,8 +386,8 @@ function ChargeEditor({
|
|||||||
const [model, setModel] = useState("");
|
const [model, setModel] = useState("");
|
||||||
|
|
||||||
const channelModels = useMemo(
|
const channelModels = useMemo(
|
||||||
() => getUniqueList([...supportModels, ...defaultChannelModels]),
|
() => getUniqueList([...allModels, ...defaultChannelModels]),
|
||||||
[supportModels],
|
[allModels],
|
||||||
);
|
);
|
||||||
|
|
||||||
const unusedModels = useMemo(() => {
|
const unusedModels = useMemo(() => {
|
||||||
@ -719,7 +719,7 @@ function ChargeWidget() {
|
|||||||
const [form, dispatch] = useReducer(reducer, initialState);
|
const [form, dispatch] = useReducer(reducer, initialState);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { supportModels, update } = useSupportModels();
|
const { allModels, update } = useAllModels();
|
||||||
|
|
||||||
const currentModels = useMemo(() => {
|
const currentModels = useMemo(() => {
|
||||||
return data.flatMap((charge) => charge.models);
|
return data.flatMap((charge) => charge.models);
|
||||||
@ -731,10 +731,10 @@ function ChargeWidget() {
|
|||||||
|
|
||||||
const unusedModels = useMemo(() => {
|
const unusedModels = useMemo(() => {
|
||||||
if (loading) return [];
|
if (loading) return [];
|
||||||
return supportModels.filter(
|
return allModels.filter(
|
||||||
(model) => !usedModels.includes(model) && model.trim() !== "",
|
(model) => !usedModels.includes(model) && model.trim() !== "",
|
||||||
);
|
);
|
||||||
}, [loading, supportModels, usedModels]);
|
}, [loading, allModels, usedModels]);
|
||||||
|
|
||||||
async function refresh(ignoreUpdate?: boolean) {
|
async function refresh(ignoreUpdate?: boolean) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -763,7 +763,7 @@ function ChargeWidget() {
|
|||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
form={form}
|
form={form}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
supportModels={supportModels}
|
allModels={allModels}
|
||||||
usedModels={usedModels}
|
usedModels={usedModels}
|
||||||
/>
|
/>
|
||||||
<ChargeTable data={data} dispatch={dispatch} onRefresh={refresh} />
|
<ChargeTable data={data} dispatch={dispatch} onRefresh={refresh} />
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Channel,
|
Channel,
|
||||||
channelGroups,
|
channelGroups,
|
||||||
channelModels,
|
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
getChannelInfo,
|
getChannelInfo,
|
||||||
} from "@/admin/channel.ts";
|
} from "@/admin/channel.ts";
|
||||||
@ -47,6 +46,7 @@ import Paragraph, {
|
|||||||
ParagraphItem,
|
ParagraphItem,
|
||||||
} from "@/components/Paragraph.tsx";
|
} from "@/components/Paragraph.tsx";
|
||||||
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
|
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
|
||||||
|
import { useChannelModels } from "@/admin/hook.tsx";
|
||||||
|
|
||||||
type CustomActionProps = {
|
type CustomActionProps = {
|
||||||
onPost: (model: string) => void;
|
onPost: (model: string) => void;
|
||||||
@ -136,11 +136,12 @@ function ChannelEditor({
|
|||||||
}: ChannelEditorProps) {
|
}: ChannelEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const info = useMemo(() => getChannelInfo(edit.type), [edit.type]);
|
const info = useMemo(() => getChannelInfo(edit.type), [edit.type]);
|
||||||
|
const { channelModels } = useChannelModels();
|
||||||
const unusedModels = useMemo(() => {
|
const unusedModels = useMemo(() => {
|
||||||
return channelModels.filter(
|
return channelModels.filter(
|
||||||
(model) => !edit.models.includes(model) && model !== "",
|
(model) => !edit.models.includes(model) && model !== "",
|
||||||
);
|
);
|
||||||
}, [edit.models]);
|
}, [channelModels, edit.models]);
|
||||||
const enabled = useMemo(() => validator(edit), [edit]);
|
const enabled = useMemo(() => validator(edit), [edit]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
@ -3,14 +3,8 @@ import { ThemeProvider } from "@/components/ThemeProvider.tsx";
|
|||||||
import DialogManager from "@/dialogs";
|
import DialogManager from "@/dialogs";
|
||||||
import Broadcast from "@/components/Broadcast.tsx";
|
import Broadcast from "@/components/Broadcast.tsx";
|
||||||
import { useEffectAsync } from "@/utils/hook.ts";
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
import { allModels, supportModels } from "@/conf";
|
import { supportModels } from "@/conf";
|
||||||
import { channelModels } from "@/admin/channel.ts";
|
import { getApiCharge, getApiMarket, getApiPlans } from "@/api/v1.ts";
|
||||||
import {
|
|
||||||
getApiCharge,
|
|
||||||
getApiMarket,
|
|
||||||
getApiModels,
|
|
||||||
getApiPlans,
|
|
||||||
} from "@/api/v1.ts";
|
|
||||||
import { loadPreferenceModels } from "@/conf/storage.ts";
|
import { loadPreferenceModels } from "@/conf/storage.ts";
|
||||||
import { resetJsArray } from "@/utils/base.ts";
|
import { resetJsArray } from "@/utils/base.ts";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
@ -18,7 +12,6 @@ import { initChatModels, updateMasks } from "@/store/chat.ts";
|
|||||||
import { Model } from "@/api/types.ts";
|
import { Model } from "@/api/types.ts";
|
||||||
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
|
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
|
||||||
import { dispatchSubscriptionData, setTheme } from "@/store/globals.ts";
|
import { dispatchSubscriptionData, setTheme } from "@/store/globals.ts";
|
||||||
import { marketEvent } from "@/events/market.ts";
|
|
||||||
import { infoEvent } from "@/events/info.ts";
|
import { infoEvent } from "@/events/info.ts";
|
||||||
import { setForm } from "@/store/info.ts";
|
import { setForm } from "@/store/info.ts";
|
||||||
import { themeEvent } from "@/events/theme.ts";
|
import { themeEvent } from "@/events/theme.ts";
|
||||||
@ -34,8 +27,6 @@ function AppProvider() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffectAsync(async () => {
|
useEffectAsync(async () => {
|
||||||
marketEvent.emit(false);
|
|
||||||
|
|
||||||
const market = await getApiMarket();
|
const market = await getApiMarket();
|
||||||
const charge = await getApiCharge();
|
const charge = await getApiCharge();
|
||||||
|
|
||||||
@ -51,22 +42,10 @@ function AppProvider() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
resetJsArray(supportModels, loadPreferenceModels(market));
|
resetJsArray(supportModels, loadPreferenceModels(market));
|
||||||
resetJsArray(
|
|
||||||
allModels,
|
|
||||||
supportModels.map((model) => model.id),
|
|
||||||
);
|
|
||||||
initChatModels(dispatch);
|
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());
|
dispatchSubscriptionData(dispatch, await getApiPlans());
|
||||||
|
}, []);
|
||||||
marketEvent.emit(true);
|
|
||||||
}, [allModels]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -9,7 +9,7 @@ import { syncSiteInfo } from "@/admin/api/info.ts";
|
|||||||
import { getOfflineModels, loadPreferenceModels } from "@/conf/storage.ts";
|
import { getOfflineModels, loadPreferenceModels } from "@/conf/storage.ts";
|
||||||
import { setAxiosConfig } from "@/conf/api.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 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 deploy: boolean = true; // is production environment (for api endpoint)
|
||||||
export const tokenField = getTokenField(deploy); // token field name for storing token
|
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 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 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({
|
setAxiosConfig({
|
||||||
endpoint: apiEndpoint,
|
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": {
|
"market": {
|
||||||
"title": "模型市场",
|
"title": "模型市场",
|
||||||
"model-name": "模型名称",
|
"model-name": "模型名称",
|
||||||
|
"not-use": "部分模型未使用",
|
||||||
|
"import-all": "导入全部",
|
||||||
"new-model": "新建模型",
|
"new-model": "新建模型",
|
||||||
"model-name-placeholder": "请输入模型昵称 (如:GPT-4)",
|
"model-name-placeholder": "请输入模型昵称 (如:GPT-4)",
|
||||||
"model-id": "模型 ID",
|
"model-id": "模型 ID",
|
||||||
@ -518,12 +520,12 @@
|
|||||||
"sync-failed": "同步失败",
|
"sync-failed": "同步失败",
|
||||||
"sync-failed-prompt": "地址无法请求或者模型市场模型为空\n(端点:{{endpoint}})",
|
"sync-failed-prompt": "地址无法请求或者模型市场模型为空\n(端点:{{endpoint}})",
|
||||||
"sync-success": "同步成功",
|
"sync-success": "同步成功",
|
||||||
"sync-success-prompt": "已从上游同步,添加 {{length}} 个模型,请检查后点击更新以保存。",
|
"sync-success-prompt": "已从上游同步,添加 {{length}} 个模型,请检查后点击提交方可生效,否则将不会保存",
|
||||||
"sync-items": "共发现 {{length}} 个模型,已有模型 {{exist}} 个(不会覆盖),新增模型 {{new}} 个(同步全部),本站点渠道已支持模型 {{support}} 个(同步已支持模型)",
|
"sync-items": "共发现 {{length}} 个模型,已有模型 {{exist}} 个(不会覆盖),新增模型 {{new}} 个(同步全部),本站点渠道已支持模型 {{support}} 个(同步已支持模型)",
|
||||||
"sync-all": "同步全部 ({{length}} 个)",
|
"sync-all": "同步全部 ({{length}} 个)",
|
||||||
"sync-self": "同步已支持模型 ({{length}} 个)",
|
"sync-self": "同步已支持模型 ({{length}} 个)",
|
||||||
"update-success": "更新成功",
|
"update-success": "更新成功",
|
||||||
"update-success-prompt": "模型市场设置已成功提交更新至服务器。",
|
"update-success-prompt": "模型市场已成功更新(刷新浏览器即可立即应用)",
|
||||||
"update-failed": "更新失败",
|
"update-failed": "更新失败",
|
||||||
"update-failed-prompt": "更新请求失败,原因:{{reason}}"
|
"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-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",
|
"model-tag": "Model label",
|
||||||
"update-success": "Upgrade successful",
|
"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": "Update failed",
|
||||||
"update-failed-prompt": "Update request failed for {{reason}}",
|
"update-failed-prompt": "Update request failed for {{reason}}",
|
||||||
"model-image": "Model Picture",
|
"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-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-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": "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",
|
"model-chart-tip": "Token usage",
|
||||||
"subscription": "Subscription Management",
|
"subscription": "Subscription Management",
|
||||||
|
@ -579,7 +579,7 @@
|
|||||||
"model-is-default-tip": "モデルがデフォルトモデルリストに追加されるかどうか(デフォルトでは、デフォルトモデルリストに追加されていないモデルはホームモデルリストに表示されません)",
|
"model-is-default-tip": "モデルがデフォルトモデルリストに追加されるかどうか(デフォルトでは、デフォルトモデルリストに追加されていないモデルはホームモデルリストに表示されません)",
|
||||||
"model-tag": "モデルラベル",
|
"model-tag": "モデルラベル",
|
||||||
"update-success": "正常に更新されました",
|
"update-success": "正常に更新されました",
|
||||||
"update-success-prompt": "モデルマーケットプレイスの設定は、サーバーへの更新のために正常に送信されました。",
|
"update-success-prompt": "モデルマーケットプレイスが正常に更新されました(今すぐ適用するにはブラウザを更新してください)",
|
||||||
"update-failed": "更新に失敗",
|
"update-failed": "更新に失敗",
|
||||||
"update-failed-prompt": "{{reason}}の更新リクエストが失敗しました",
|
"update-failed-prompt": "{{reason}}の更新リクエストが失敗しました",
|
||||||
"model-image": "モデル写真",
|
"model-image": "モデル写真",
|
||||||
@ -599,7 +599,9 @@
|
|||||||
"sync-failed-prompt": "住所をリクエストできなかったか、モデルマーケットモデルが空です\n(エンドポイント:{{ endpoint }})",
|
"sync-failed-prompt": "住所をリクエストできなかったか、モデルマーケットモデルが空です\n(エンドポイント:{{ endpoint }})",
|
||||||
"sync-items": "合計{{length}}個のモデルが見つかりました。{{exist}}個のモデルが見つかりました(上書きされません)。{{new}}個のモデルが追加されました(すべて同期済み)。{{support}}個のモデルがこのサイトチャネルでサポートされています(同期対応モデル)",
|
"sync-items": "合計{{length}}個のモデルが見つかりました。{{exist}}個のモデルが見つかりました(上書きされません)。{{new}}個のモデルが追加されました(すべて同期済み)。{{support}}個のモデルがこのサイトチャネルでサポートされています(同期対応モデル)",
|
||||||
"sync-success": "同期成功",
|
"sync-success": "同期成功",
|
||||||
"sync-success-prompt": "アップストリームから同期され、{{length}}モデルが追加されました。確認して[更新]をクリックして保存してください。"
|
"sync-success-prompt": "アップストリームから同期され、{{length}}モデルが追加されました。確認して送信をクリックして有効にしてください。有効にしないと保存されません",
|
||||||
|
"not-use": "一部のモデルは使用されていません",
|
||||||
|
"import-all": "すべてインポート..."
|
||||||
},
|
},
|
||||||
"model-chart-tip": "トークンの使用状況",
|
"model-chart-tip": "トークンの使用状況",
|
||||||
"subscription": "サブスクリプション管理",
|
"subscription": "サブスクリプション管理",
|
||||||
|
@ -579,7 +579,7 @@
|
|||||||
"model-is-default-tip": "Добавлена ли модель в список моделей по умолчанию (модели, не добавленные в список моделей по умолчанию, не будут отображаться в списке моделей дома по умолчанию)",
|
"model-is-default-tip": "Добавлена ли модель в список моделей по умолчанию (модели, не добавленные в список моделей по умолчанию, не будут отображаться в списке моделей дома по умолчанию)",
|
||||||
"model-tag": "Этикетка модели",
|
"model-tag": "Этикетка модели",
|
||||||
"update-success": "Успешно обновлено",
|
"update-success": "Успешно обновлено",
|
||||||
"update-success-prompt": "Настройки Model Marketplace успешно отправлены на обновление на сервер.",
|
"update-success-prompt": "Модель Marketplace успешно обновлена (обновите браузер, чтобы подать заявку сейчас)",
|
||||||
"update-failed": "Ошибка обновления",
|
"update-failed": "Ошибка обновления",
|
||||||
"update-failed-prompt": "Запрос на обновление не выполнен по {{reason}}",
|
"update-failed-prompt": "Запрос на обновление не выполнен по {{reason}}",
|
||||||
"model-image": "Изображение модели",
|
"model-image": "Изображение модели",
|
||||||
@ -599,7 +599,9 @@
|
|||||||
"sync-failed-prompt": "Адрес не может быть запрошен или модель рынка пуста\n(Конечная точка: {{endpoint}})",
|
"sync-failed-prompt": "Адрес не может быть запрошен или модель рынка пуста\n(Конечная точка: {{endpoint}})",
|
||||||
"sync-items": "Всего найдено {{length}} моделей, {{exist}} моделей найдено (не будет перезаписано), {{new}} моделей добавлено (все синхронизировано), {{support}} моделей поддерживается этим каналом сайта (синхронизированные поддерживаемые модели)",
|
"sync-items": "Всего найдено {{length}} моделей, {{exist}} моделей найдено (не будет перезаписано), {{new}} моделей добавлено (все синхронизировано), {{support}} моделей поддерживается этим каналом сайта (синхронизированные поддерживаемые модели)",
|
||||||
"sync-success": "Успешная синхронизация",
|
"sync-success": "Успешная синхронизация",
|
||||||
"sync-success-prompt": "Синхронизировано с вышестоящими, добавлено {{length}} моделей, проверьте и нажмите «Обновить», чтобы сохранить."
|
"sync-success-prompt": "Синхронизировано с вышестоящими, добавлено {{length}} моделей, проверьте и нажмите «Отправить», чтобы вступить в силу, иначе оно не будет сохранено",
|
||||||
|
"not-use": "Некоторые модели не используются",
|
||||||
|
"import-all": "Импортировать все..."
|
||||||
},
|
},
|
||||||
"model-chart-tip": "Использование токенов",
|
"model-chart-tip": "Использование токенов",
|
||||||
"subscription": "Управление подписками",
|
"subscription": "Управление подписками",
|
||||||
|
@ -7,17 +7,22 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Dispatch, useMemo, useReducer, useState } from "react";
|
import { Dispatch, useMemo, useReducer, useState } from "react";
|
||||||
import { Model as RawModel } from "@/api/types.ts";
|
import { Model as RawModel } from "@/api/types.ts";
|
||||||
import { allModels, supportModels } from "@/conf";
|
|
||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import {
|
import {
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Loader2,
|
Import,
|
||||||
|
Maximize,
|
||||||
|
Minimize,
|
||||||
Plus,
|
Plus,
|
||||||
|
RotateCw,
|
||||||
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} 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 Require from "@/components/Require.tsx";
|
||||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||||
import Tips from "@/components/Tips.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 { Checkbox } from "@/components/ui/checkbox.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { Combobox } from "@/components/ui/combo-box.tsx";
|
import { Combobox } from "@/components/ui/combo-box.tsx";
|
||||||
import { channelModels } from "@/admin/channel.ts";
|
|
||||||
import { cn } from "@/components/ui/lib/utils.ts";
|
import { cn } from "@/components/ui/lib/utils.ts";
|
||||||
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
|
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
|
||||||
import { useToast } from "@/components/ui/use-toast.ts";
|
import { useToast } from "@/components/ui/use-toast.ts";
|
||||||
@ -39,19 +43,11 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog.tsx";
|
} from "@/components/ui/dialog.tsx";
|
||||||
import {
|
import { getApiMarket, getV1Path } from "@/api/v1.ts";
|
||||||
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 { updateMarket } from "@/admin/api/market.ts";
|
import { updateMarket } from "@/admin/api/market.ts";
|
||||||
import { marketEvent } from "@/events/market.ts";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useChannelModels, useSupportModels } from "@/admin/hook.tsx";
|
||||||
|
import Icon from "@/components/utils/Icon.tsx";
|
||||||
|
|
||||||
type Model = RawModel & {
|
type Model = RawModel & {
|
||||||
seed?: string;
|
seed?: string;
|
||||||
@ -110,6 +106,38 @@ function reducer(state: MarketForm, action: any): MarketForm {
|
|||||||
seed: generateSeed(),
|
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":
|
case "remove":
|
||||||
let { idx } = action.payload;
|
let { idx } = action.payload;
|
||||||
return [...state.slice(0, idx), ...state.slice(idx + 1)];
|
return [...state.slice(0, idx), ...state.slice(idx + 1)];
|
||||||
@ -377,9 +405,18 @@ type MarketItemProps = {
|
|||||||
form: MarketForm;
|
form: MarketForm;
|
||||||
dispatch: Dispatch<any>;
|
dispatch: Dispatch<any>;
|
||||||
index: number;
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const checked = useMemo(
|
const checked = useMemo(
|
||||||
@ -387,7 +424,63 @@ function MarketItem({ model, form, dispatch, index }: MarketItemProps) {
|
|||||||
[model],
|
[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={cn("market-item", !checked && "error")}>
|
||||||
<div className={`model-wrapper`}>
|
<div className={`model-wrapper`}>
|
||||||
<div className={`market-row`}>
|
<div className={`market-row`}>
|
||||||
@ -489,60 +582,26 @@ function MarketItem({ model, form, dispatch, index }: MarketItemProps) {
|
|||||||
<span>{t("admin.market.model-image")}</span>
|
<span>{t("admin.market.model-image")}</span>
|
||||||
<MarketImage image={model.avatar} idx={index} dispatch={dispatch} />
|
<MarketImage image={model.avatar} idx={index} dispatch={dispatch} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`market-row`}>
|
<Actions />
|
||||||
<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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -551,9 +610,17 @@ type SyncDialogProps = {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (state: boolean) => void;
|
setOpen: (state: boolean) => void;
|
||||||
onConfirm: (form: MarketForm) => Promise<boolean>;
|
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 { t } = useTranslation();
|
||||||
const [form, setForm] = useState<MarketForm>([]);
|
const [form, setForm] = useState<MarketForm>([]);
|
||||||
const { toast } = useToast();
|
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() {
|
function Market() {
|
||||||
const { t } = useTranslation();
|
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 [form, dispatch] = useReducer(reducer, []);
|
||||||
const market = await getApiMarket();
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const charge = await getApiCharge();
|
|
||||||
|
|
||||||
market.forEach((item: Model) => {
|
const { supportModels, update: updateSuppportModels } = useSupportModels(
|
||||||
const instance = charge.find((i: ChargeProps) =>
|
(state, data) => {
|
||||||
i.models.includes(item.id),
|
setStepSupport(!state);
|
||||||
|
state && dispatch({ type: "set", payload: data });
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (!instance) return;
|
|
||||||
|
|
||||||
item.free = instance.type === nonBilling;
|
const {
|
||||||
item.auth = !item.free || !instance.anonymous;
|
|
||||||
item.price = { ...instance };
|
|
||||||
});
|
|
||||||
|
|
||||||
resetJsArray(supportModels, loadPreferenceModels(market));
|
|
||||||
resetJsArray(
|
|
||||||
allModels,
|
allModels,
|
||||||
supportModels.map((model) => model.id),
|
channelModels,
|
||||||
);
|
update: updateAllModels,
|
||||||
globalDispatch(setModelList(supportModels));
|
} = useChannelModels((state) => setStepAll(!state));
|
||||||
allModels.length > 0 &&
|
|
||||||
!allModels.includes(model) &&
|
|
||||||
globalDispatch(setModel(allModels[0]));
|
|
||||||
|
|
||||||
const models = await getApiModels();
|
const unusedModels = useMemo(
|
||||||
models.data.forEach((model: string) => {
|
(): string[] =>
|
||||||
if (!allModels.includes(model)) allModels.push(model);
|
allModels.filter((model) => !form.map((m) => m.id).includes(model)),
|
||||||
if (!channelModels.includes(model)) channelModels.push(model);
|
[form, allModels],
|
||||||
});
|
);
|
||||||
|
|
||||||
|
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(
|
const preflight = form.filter(
|
||||||
(model) => model.id.trim().length > 0 && model.name.trim().length > 0,
|
(model) => model.id.trim().length > 0 && model.name.trim().length > 0,
|
||||||
);
|
);
|
||||||
@ -733,18 +870,13 @@ function Market() {
|
|||||||
dispatch({ type: "add-multiple", payload: [...data] });
|
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 (
|
return (
|
||||||
<div className={`market`}>
|
<div className={`market`}>
|
||||||
<SyncDialog
|
<SyncDialog
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
|
allModels={allModels}
|
||||||
|
supportModels={supportModels}
|
||||||
onConfirm={async (data: MarketForm) => {
|
onConfirm={async (data: MarketForm) => {
|
||||||
await migrate(data);
|
await migrate(data);
|
||||||
toast(t("admin.market.sync-success"), {
|
toast(t("admin.market.sync-success"), {
|
||||||
@ -757,25 +889,70 @@ function Market() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Card className={`admin-card market-card`}>
|
<Card className={`admin-card market-card`}>
|
||||||
<CardHeader className={`flex flex-row items-center select-none`}>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>{t("admin.market.title")}</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<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`}>
|
<div className={`market-list`}>
|
||||||
{form.length > 0 ? (
|
{form.length > 0 ? (
|
||||||
form.map((model, index) => (
|
form.map((model, index) => (
|
||||||
@ -783,8 +960,10 @@ function Market() {
|
|||||||
key={index}
|
key={index}
|
||||||
model={model}
|
model={model}
|
||||||
form={form}
|
form={form}
|
||||||
|
stacked={stacked}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
index={index}
|
index={index}
|
||||||
|
channelModels={channelModels}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@ -802,7 +981,7 @@ function Market() {
|
|||||||
<Plus className={`h-4 w-4 mr-2`} />
|
<Plus className={`h-4 w-4 mr-2`} />
|
||||||
{t("admin.market.new-model")}
|
{t("admin.market.new-model")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size={`sm`} onClick={update} loading={true}>
|
<Button size={`sm`} onClick={submit} loading={true}>
|
||||||
{t("admin.market.migrate")}
|
{t("admin.market.migrate")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -442,6 +442,11 @@ function PlanConfig() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={`plan-config-row pb-2`}>
|
<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
|
<Button
|
||||||
variant={`outline`}
|
variant={`outline`}
|
||||||
size={`icon`}
|
size={`icon`}
|
||||||
@ -453,11 +458,6 @@ function PlanConfig() {
|
|||||||
className={`h-4 w-4`}
|
className={`h-4 w-4`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={`outline`} onClick={() => setOpen(true)}>
|
|
||||||
<Activity className={`h-4 w-4 mr-2`} />
|
|
||||||
{t("admin.plan.sync")}
|
|
||||||
</Button>
|
|
||||||
<div className={`grow`} />
|
|
||||||
<Button
|
<Button
|
||||||
variant={`outline`}
|
variant={`outline`}
|
||||||
className={`mr-2`}
|
className={`mr-2`}
|
||||||
|
@ -51,8 +51,8 @@ import { cn } from "@/components/ui/lib/utils.ts";
|
|||||||
import { Switch } from "@/components/ui/switch.tsx";
|
import { Switch } from "@/components/ui/switch.tsx";
|
||||||
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
|
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
|
||||||
import { allGroups } from "@/utils/groups.ts";
|
import { allGroups } from "@/utils/groups.ts";
|
||||||
import { channelModels } from "@/admin/channel.ts";
|
|
||||||
import { supportModels } from "@/conf";
|
import { supportModels } from "@/conf";
|
||||||
|
import { useChannelModels } from "@/admin/hook.tsx";
|
||||||
|
|
||||||
type CompProps<T> = {
|
type CompProps<T> = {
|
||||||
data: T;
|
data: T;
|
||||||
@ -593,6 +593,8 @@ function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
|
|||||||
function Common({ data, dispatch, onChange }: CompProps<CommonState>) {
|
function Common({ data, dispatch, onChange }: CompProps<CommonState>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { channelModels } = useChannelModels();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
title={t("admin.system.common")}
|
title={t("admin.system.common")}
|
||||||
|
Loading…
Reference in New Issue
Block a user