mirror of
https://github.com/coaidev/coai.git
synced 2025-05-22 22:40:14 +09:00
chore: update admin actions
This commit is contained in:
parent
c0568bbbf1
commit
8dfb1185b8
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## 🔨 模型 | Models
|
## 🔨 模型 | Models
|
||||||
- [x] OpenAI ChatGPT (GPT-3.5, GPT-4, Instruct, DALL-E 2, DALL-E 3, Text-Davincci, ...)
|
- [x] OpenAI ChatGPT (GPT-3.5, GPT-4, Instruct, DALL-E 2, DALL-E 3, ...)
|
||||||
- [x] Azure OpenAI
|
- [x] Azure OpenAI
|
||||||
- [x] Anthropic Claude (claude-2, claude-instant)
|
- [x] Anthropic Claude (claude-2, claude-instant)
|
||||||
- [x] Slack Claude (deprecated)
|
- [x] Slack Claude (deprecated)
|
||||||
@ -79,9 +79,7 @@
|
|||||||
- [x] Baichuan AI
|
- [x] Baichuan AI
|
||||||
- [x] Douyin Skylark (lite, plus, pro, chat)
|
- [x] Douyin Skylark (lite, plus, pro, chat)
|
||||||
- [x] 360 GPT
|
- [x] 360 GPT
|
||||||
- [x] LLaMa 2 (70b, 13b, 7b)
|
- [x] LocalAI (RWKV, LLaMa 2, Baichuan 7b, Mixtral, ...) _*requires local deployment_
|
||||||
- [x] Code LLaMa (34b, 13b, 7b)
|
|
||||||
- [ ] RWKV
|
|
||||||
|
|
||||||
|
|
||||||
## 📦 部署 | Deploy
|
## 📦 部署 | Deploy
|
||||||
|
@ -154,7 +154,7 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
|
|||||||
format: "<secret>",
|
format: "<secret>",
|
||||||
models: ["bing-creative", "bing-balanced", "bing-precise"],
|
models: ["bing-creative", "bing-balanced", "bing-precise"],
|
||||||
description:
|
description:
|
||||||
"> Bing 服务需要自行搭建,详情请参考 [chatnio-bing-service](https://github.com/Deeptrain-Community/chatnio-bing-service) (如为 bing2api 可直接使用 OpenAI 格式映射)",
|
"> Bing 服务需要自行搭建,详情请参考 [chatnio-bing-service](https://github.com/Deeptrain-Community/chatnio-bing-service) \n > bing2api (如 [bingo](https://github.com/weaigc/bingo)) 可直接使用 OpenAI 格式",
|
||||||
},
|
},
|
||||||
palm: {
|
palm: {
|
||||||
endpoint: "https://generativelanguage.googleapis.com",
|
endpoint: "https://generativelanguage.googleapis.com",
|
||||||
|
@ -27,6 +27,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-editor {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.channel-loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.channel-wrapper {
|
.channel-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -22,7 +22,7 @@ import { Button } from "@/components/ui/button.tsx";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemo, useReducer, useState } from "react";
|
import { useMemo, useReducer, useState } from "react";
|
||||||
import Required from "@/components/Require.tsx";
|
import Required from "@/components/Require.tsx";
|
||||||
import { Plus, Search, X } from "lucide-react";
|
import { Loader2, Plus, Search, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -209,6 +209,8 @@ function ChannelEditor({ display, id, setEnabled }: ChannelEditorProps) {
|
|||||||
}, [edit.models]);
|
}, [edit.models]);
|
||||||
const enabled = useMemo(() => validator(edit), [edit]);
|
const enabled = useMemo(() => validator(edit), [edit]);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const unusedGroups = useMemo(() => {
|
const unusedGroups = useMemo(() => {
|
||||||
if (!edit.group) return channelGroups;
|
if (!edit.group) return channelGroups;
|
||||||
return channelGroups.filter(
|
return channelGroups.filter(
|
||||||
@ -237,7 +239,9 @@ function ChannelEditor({ display, id, setEnabled }: ChannelEditorProps) {
|
|||||||
useEffectAsync(async () => {
|
useEffectAsync(async () => {
|
||||||
if (id === -1) dispatch({ type: "clear" });
|
if (id === -1) dispatch({ type: "clear" });
|
||||||
else {
|
else {
|
||||||
|
setLoading(true);
|
||||||
const resp = await getChannel(id);
|
const resp = await getChannel(id);
|
||||||
|
setLoading(false);
|
||||||
toastState(toast, t, resp as CommonResponse);
|
toastState(toast, t, resp as CommonResponse);
|
||||||
if (resp.data) dispatch({ type: "set", value: resp.data });
|
if (resp.data) dispatch({ type: "set", value: resp.data });
|
||||||
}
|
}
|
||||||
@ -246,6 +250,9 @@ function ChannelEditor({ display, id, setEnabled }: ChannelEditorProps) {
|
|||||||
return (
|
return (
|
||||||
display && (
|
display && (
|
||||||
<div className={`channel-editor`}>
|
<div className={`channel-editor`}>
|
||||||
|
{loading && (
|
||||||
|
<Loader2 className={`channel-loader h-4 w-4 animate-spin`} />
|
||||||
|
)}
|
||||||
<div className={`channel-wrapper w-full h-max`}>
|
<div className={`channel-wrapper w-full h-max`}>
|
||||||
<div className={`channel-row`}>
|
<div className={`channel-row`}>
|
||||||
<div className={`channel-content`}>
|
<div className={`channel-content`}>
|
||||||
|
@ -18,11 +18,14 @@ import { initChatModels } 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 } from "@/store/globals.ts";
|
import { dispatchSubscriptionData } from "@/store/globals.ts";
|
||||||
|
import { marketEvent } from "@/events/market.ts";
|
||||||
|
|
||||||
function AppProvider() {
|
function AppProvider() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffectAsync(async () => {
|
useEffectAsync(async () => {
|
||||||
|
marketEvent.emit(false);
|
||||||
|
|
||||||
const market = await getApiMarket();
|
const market = await getApiMarket();
|
||||||
const charge = await getApiCharge();
|
const charge = await getApiCharge();
|
||||||
|
|
||||||
@ -48,6 +51,8 @@ function AppProvider() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dispatchSubscriptionData(dispatch, await getApiPlans());
|
dispatchSubscriptionData(dispatch, await getApiPlans());
|
||||||
|
|
||||||
|
marketEvent.emit(true);
|
||||||
}, [allModels]);
|
}, [allModels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
5
app/src/events/market.ts
Normal file
5
app/src/events/market.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { EventCommitter } from "@/events/struct.ts";
|
||||||
|
|
||||||
|
export const marketEvent = new EventCommitter<boolean>({
|
||||||
|
name: "market",
|
||||||
|
});
|
@ -403,6 +403,7 @@
|
|||||||
"market": {
|
"market": {
|
||||||
"title": "模型市场",
|
"title": "模型市场",
|
||||||
"model-name": "模型名称",
|
"model-name": "模型名称",
|
||||||
|
"new-model": "新建模型",
|
||||||
"model-name-placeholder": "请输入模型昵称 (如:GPT-4)",
|
"model-name-placeholder": "请输入模型昵称 (如:GPT-4)",
|
||||||
"model-id": "模型 ID",
|
"model-id": "模型 ID",
|
||||||
"model-id-placeholder": "请输入模型 ID (如:gpt-4-0613)",
|
"model-id-placeholder": "请输入模型 ID (如:gpt-4-0613)",
|
||||||
@ -417,6 +418,7 @@
|
|||||||
"custom-image": "自定义图片",
|
"custom-image": "自定义图片",
|
||||||
"custom-image-placeholder": "请输入图片链接",
|
"custom-image-placeholder": "请输入图片链接",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
|
"migrate": "提交",
|
||||||
"update-success": "更新成功",
|
"update-success": "更新成功",
|
||||||
"update-success-prompt": "模型市场设置已成功提交更新至服务器。",
|
"update-success-prompt": "模型市场设置已成功提交更新至服务器。",
|
||||||
"update-failed": "更新失败",
|
"update-failed": "更新失败",
|
||||||
|
@ -465,7 +465,9 @@
|
|||||||
"model-image": "Model Picture",
|
"model-image": "Model Picture",
|
||||||
"custom-image": "Custom Image",
|
"custom-image": "Custom Image",
|
||||||
"custom-image-placeholder": "Please enter an image link",
|
"custom-image-placeholder": "Please enter an image link",
|
||||||
"update": "Update"
|
"update": "Update",
|
||||||
|
"new-model": "Create a new model",
|
||||||
|
"migrate": "Submit"
|
||||||
},
|
},
|
||||||
"model-chart-tip": "Token usage",
|
"model-chart-tip": "Token usage",
|
||||||
"subscription": "Subscription Management",
|
"subscription": "Subscription Management",
|
||||||
|
@ -465,7 +465,9 @@
|
|||||||
"model-image": "モデル写真",
|
"model-image": "モデル写真",
|
||||||
"custom-image": "カスタム画像",
|
"custom-image": "カスタム画像",
|
||||||
"custom-image-placeholder": "画像リンクを入力してください",
|
"custom-image-placeholder": "画像リンクを入力してください",
|
||||||
"update": "更新"
|
"update": "更新",
|
||||||
|
"new-model": "新しいモデル",
|
||||||
|
"migrate": "提出"
|
||||||
},
|
},
|
||||||
"model-chart-tip": "トークンの使用状況",
|
"model-chart-tip": "トークンの使用状況",
|
||||||
"subscription": "サブスクリプション管理",
|
"subscription": "サブスクリプション管理",
|
||||||
|
@ -465,7 +465,9 @@
|
|||||||
"model-image": "Изображение модели",
|
"model-image": "Изображение модели",
|
||||||
"custom-image": "Пользовательское изображение",
|
"custom-image": "Пользовательское изображение",
|
||||||
"custom-image-placeholder": "Введите ссылку на изображение",
|
"custom-image-placeholder": "Введите ссылку на изображение",
|
||||||
"update": "Обновить"
|
"update": "Обновить",
|
||||||
|
"new-model": "Новая модель",
|
||||||
|
"migrate": "передавать"
|
||||||
},
|
},
|
||||||
"model-chart-tip": "Использование токенов",
|
"model-chart-tip": "Использование токенов",
|
||||||
"subscription": "Управление подписками",
|
"subscription": "Управление подписками",
|
||||||
|
@ -22,6 +22,7 @@ import Paragraph from "@/components/Paragraph.tsx";
|
|||||||
import { Label } from "@/components/ui/label.tsx";
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import { cn } from "@/components/ui/lib/utils.ts";
|
||||||
|
|
||||||
type LoggerItemProps = Logger & {
|
type LoggerItemProps = Logger & {
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
@ -104,7 +105,7 @@ function LoggerConsole() {
|
|||||||
/>
|
/>
|
||||||
<div className={`grow`} />
|
<div className={`grow`} />
|
||||||
<Button onClick={sync} variant={`outline`} size={`icon`}>
|
<Button onClick={sync} variant={`outline`} size={`icon`}>
|
||||||
<RotateCcw className={`w-4 h-4`} />
|
<RotateCcw className={cn("w-4 h-4", loading && "animate-spin")} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`logger-console`}>
|
<div className={`logger-console`}>
|
||||||
|
@ -5,19 +5,12 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card.tsx";
|
} from "@/components/ui/card.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Dispatch, useMemo, useReducer, useState } from "react";
|
||||||
Dispatch,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useReducer,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Model as RawModel } from "@/api/types.ts";
|
import { Model as RawModel } from "@/api/types.ts";
|
||||||
import { supportModels } from "@/conf";
|
import { supportModels } from "@/conf";
|
||||||
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { GripVertical, HelpCircle, Plus, Trash2 } from "lucide-react";
|
import { GripVertical, HelpCircle, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
import { generateRandomChar, isUrl } 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";
|
||||||
@ -32,6 +25,7 @@ import { updateMarket } from "@/admin/api/market.ts";
|
|||||||
import { Combobox } from "@/components/ui/combo-box.tsx";
|
import { Combobox } from "@/components/ui/combo-box.tsx";
|
||||||
import { channelModels } from "@/admin/channel.ts";
|
import { channelModels } from "@/admin/channel.ts";
|
||||||
import { cn } from "@/components/ui/lib/utils.ts";
|
import { cn } from "@/components/ui/lib/utils.ts";
|
||||||
|
import { marketEvent } from "@/events/market.ts";
|
||||||
|
|
||||||
type Model = RawModel & {
|
type Model = RawModel & {
|
||||||
seed?: string;
|
seed?: string;
|
||||||
@ -41,21 +35,6 @@ type MarketForm = Model[];
|
|||||||
|
|
||||||
const generateSeed = () => generateRandomChar(8);
|
const generateSeed = () => generateRandomChar(8);
|
||||||
|
|
||||||
const initialState: MarketForm = [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
name: "",
|
|
||||||
free: false,
|
|
||||||
auth: false,
|
|
||||||
description: "",
|
|
||||||
high_context: false,
|
|
||||||
default: false,
|
|
||||||
tag: [],
|
|
||||||
avatar: modelImages[0],
|
|
||||||
seed: generateSeed(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function reducer(state: MarketForm, action: any): MarketForm {
|
function reducer(state: MarketForm, action: any): MarketForm {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "set":
|
case "set":
|
||||||
@ -324,9 +303,8 @@ function MarketImage({ image, idx, dispatch }: MarketImageProps) {
|
|||||||
|
|
||||||
function Market() {
|
function Market() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form, dispatch] = useReducer(reducer, initialState);
|
const [form, dispatch] = useReducer(reducer, supportModels);
|
||||||
const timer = useRef<number | null>(null);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const sync = useRef<boolean>(false);
|
|
||||||
|
|
||||||
const update = async (): Promise<void> => {
|
const update = async (): Promise<void> => {
|
||||||
const preflight = form.filter(
|
const preflight = form.filter(
|
||||||
@ -348,32 +326,10 @@ function Market() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
marketEvent.addEventListener((state: boolean) => {
|
||||||
if (supportModels.length > 0 && !sync.current) {
|
setLoading(!state);
|
||||||
dispatch({ type: "set", payload: [...supportModels] });
|
!state && dispatch({ type: "set", payload: [...supportModels] });
|
||||||
sync.current = true;
|
});
|
||||||
}
|
|
||||||
}, [supportModels]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timer.current) {
|
|
||||||
clearTimeout(timer.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
timer.current = Number(
|
|
||||||
setTimeout(async () => {
|
|
||||||
if (sync.current) {
|
|
||||||
sync.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(
|
|
||||||
`[market] model market migrated, sync to server (models: ${form.length})`,
|
|
||||||
);
|
|
||||||
await update();
|
|
||||||
}, 2000),
|
|
||||||
);
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
const checked = (index: number) => {
|
const checked = (index: number) => {
|
||||||
return useMemo((): boolean => {
|
return useMemo((): boolean => {
|
||||||
@ -387,7 +343,10 @@ function Market() {
|
|||||||
<div className={`market`}>
|
<div className={`market`}>
|
||||||
<Card className={`admin-card market-card`}>
|
<Card className={`admin-card market-card`}>
|
||||||
<CardHeader className={`flex flex-row items-center select-none`}>
|
<CardHeader className={`flex flex-row items-center select-none`}>
|
||||||
<CardTitle>{t("admin.market.title")}</CardTitle>
|
<CardTitle>
|
||||||
|
{t("admin.market.title")}
|
||||||
|
{loading && <Loader2 className={`h-4 w-4 ml-2 animate-spin`} />}
|
||||||
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
loading={true}
|
loading={true}
|
||||||
className={`ml-auto mt-0 whitespace-nowrap`}
|
className={`ml-auto mt-0 whitespace-nowrap`}
|
||||||
@ -395,7 +354,7 @@ function Market() {
|
|||||||
style={{ marginTop: 0 }}
|
style={{ marginTop: 0 }}
|
||||||
onClick={update}
|
onClick={update}
|
||||||
>
|
>
|
||||||
{t("admin.market.update")}
|
{t("admin.market.migrate")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -592,6 +551,21 @@ function Market() {
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
<div className={`market-footer flex flex-row items-center mt-4`}>
|
||||||
|
<div className={`grow`} />
|
||||||
|
<Button
|
||||||
|
size={`sm`}
|
||||||
|
variant={`outline`}
|
||||||
|
className={`mr-2`}
|
||||||
|
onClick={() => dispatch({ type: "new" })}
|
||||||
|
>
|
||||||
|
<Plus className={`h-4 w-4 mr-2`} />
|
||||||
|
{t("admin.market.new-model")}
|
||||||
|
</Button>
|
||||||
|
<Button size={`sm`} onClick={update} loading={true}>
|
||||||
|
{t("admin.market.migrate")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
} from "@/components/ui/dialog.tsx";
|
} from "@/components/ui/dialog.tsx";
|
||||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||||
import Require from "@/components/Require.tsx";
|
import Require from "@/components/Require.tsx";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
type CompProps<T> = {
|
type CompProps<T> = {
|
||||||
data: T;
|
data: T;
|
||||||
@ -422,6 +423,8 @@ function System() {
|
|||||||
initialSystemState,
|
initialSystemState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const doSaving = async (doToast?: boolean) => {
|
const doSaving = async (doToast?: boolean) => {
|
||||||
const res = await setConfig(data);
|
const res = await setConfig(data);
|
||||||
|
|
||||||
@ -429,7 +432,9 @@ function System() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffectAsync(async () => {
|
useEffectAsync(async () => {
|
||||||
|
setLoading(true);
|
||||||
const res = await getConfig();
|
const res = await getConfig();
|
||||||
|
setLoading(false);
|
||||||
toastState(toast, t, res);
|
toastState(toast, t, res);
|
||||||
if (res.status) {
|
if (res.status) {
|
||||||
setData({ type: "set", value: res.data });
|
setData({ type: "set", value: res.data });
|
||||||
@ -440,7 +445,10 @@ function System() {
|
|||||||
<div className={`system`}>
|
<div className={`system`}>
|
||||||
<Card className={`admin-card system-card`}>
|
<Card className={`admin-card system-card`}>
|
||||||
<CardHeader className={`select-none`}>
|
<CardHeader className={`select-none`}>
|
||||||
<CardTitle>{t("admin.settings")}</CardTitle>
|
<CardTitle>
|
||||||
|
{t("admin.settings")}
|
||||||
|
{loading && <Loader2 className={`h-4 w-4 ml-2 inline-block`} />}
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className={`flex flex-col gap-1`}>
|
<CardContent className={`flex flex-col gap-1`}>
|
||||||
<General data={data.general} dispatch={setData} onChange={doSaving} />
|
<General data={data.general} dispatch={setData} onChange={doSaving} />
|
||||||
|
Loading…
Reference in New Issue
Block a user