chore: update admin actions

This commit is contained in:
Zhang Minghan 2024-01-19 11:58:31 +08:00
parent c0568bbbf1
commit 8dfb1185b8
13 changed files with 82 additions and 66 deletions

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -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`}>

View File

@ -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
View File

@ -0,0 +1,5 @@
import { EventCommitter } from "@/events/struct.ts";
export const marketEvent = new EventCommitter<boolean>({
name: "market",
});

View File

@ -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": "更新失败",

View File

@ -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",

View File

@ -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": "サブスクリプション管理",

View File

@ -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": "Управление подписками",

View File

@ -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`}>

View File

@ -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>

View File

@ -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} />