update api platform

This commit is contained in:
Zhang Minghan 2023-10-02 12:48:07 +08:00
parent a378a572fb
commit 467e041f85
18 changed files with 271 additions and 53 deletions

View File

@ -5,7 +5,14 @@ import { Button } from "./components/ui/button.tsx";
import router from "./router.ts";
import I18nProvider from "./components/I18nProvider.tsx";
import ProjectLink from "./components/ProjectLink.tsx";
import { BadgeCent, Boxes, CalendarPlus, Cloud, Menu } from "lucide-react";
import {
BadgeCent,
Boxes,
CalendarPlus,
Cloud,
Menu,
Plug,
} from "lucide-react";
import { Provider, useDispatch, useSelector } from "react-redux";
import { toggleMenu } from "./store/menu.ts";
import store from "./store/index.ts";
@ -31,8 +38,10 @@ import Quota from "./routes/Quota.tsx";
import { openDialog as openQuotaDialog, quotaSelector } from "./store/quota.ts";
import { openDialog as openPackageDialog } from "./store/package.ts";
import { openDialog as openSub } from "./store/subscription.ts";
import { openDialog as openApiDialog } from "./store/api.ts";
import Package from "./routes/Package.tsx";
import Subscription from "./routes/Subscription.tsx";
import ApiKey from "./routes/ApiKey.tsx";
function Settings() {
const { t } = useTranslation();
@ -69,6 +78,10 @@ function Settings() {
<Boxes className={`h-4 w-4 mr-1`} />
{t("pkg.title")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openApiDialog())}>
<Plug className={`h-4 w-4 mr-1`} />
{t("api.title")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
@ -133,6 +146,7 @@ function App() {
<RouterProvider router={router} />
<Toaster />
<Quota />
<ApiKey />
<Package />
<Subscription />
</Provider>

27
app/src/assets/api.less Normal file
View File

@ -0,0 +1,27 @@
.api-dialog {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: max-content;
padding: 24px 0 12px;
gap: 24px;
}
.api-wrapper {
display: flex;
flex-direction: row;
gap: 6px;
width: 100%;
input {
text-align: center;
font-size: 16px;
cursor: pointer;
flex-grow: 1;
}
button {
flex-shrink: 0;
}
}

View File

@ -131,13 +131,14 @@
flex-direction: row;
align-items: center;
text-align: center;
font-size: 2rem;
font-size: min(2rem, 7vw);
gap: 12px;
user-select: none;
white-space: nowrap;
img {
width: 3rem;
height: 3rem;
width: min(3rem, 14vw);
height: min(3rem, 14vw);
}
}
}

View File

@ -19,7 +19,7 @@ export const supportModels: string[] = [
"Claude-2",
"Claude-2-100k",
"SparkDesk 讯飞星火",
"Palm2"
"Palm2",
// "Claude-2",
// "Claude-2-100k",
];
@ -32,7 +32,7 @@ export const supportModelConvertor: Record<string, string> = {
"Claude-2": "claude-1",
"Claude-2-100k": "claude-2", // not claude-2-100k
"SparkDesk 讯飞星火": "spark-desk",
"Palm2": "chat-bison-001"
"Palm2": "chat-bison-001",
};
export function login() {

View File

@ -22,6 +22,11 @@ type BuySubscriptionResponse = {
error: string;
};
type ApiKeyResponse = {
status: boolean;
key: string;
};
export async function buyQuota(quota: number): Promise<QuotaResponse> {
try {
const resp = await axios.post(`/buy`, { quota });
@ -77,3 +82,19 @@ export async function buySubscription(
return { status: false, error: "network error" };
}
}
export async function getKey(): Promise<ApiKeyResponse> {
try {
const resp = await axios.get(`/apikey`);
if (resp.data.status === false) {
return { status: false, key: "" };
}
return {
status: resp.data.status,
key: resp.data.key,
};
} catch (e) {
console.debug(e);
return { status: false, key: "" };
}
}

View File

@ -2,8 +2,11 @@ import axios from "axios";
import type { ConversationInstance } from "./types.ts";
import { setHistory } from "../store/chat.ts";
import { manager } from "./manager.ts";
import { AppDispatch } from "../store";
export async function updateConversationList(dispatch: any): Promise<void> {
export async function updateConversationList(
dispatch: AppDispatch,
): Promise<void> {
const resp = await axios.get("/conversation/list");
dispatch(
@ -24,7 +27,7 @@ export async function loadConversation(
}
export async function deleteConversation(
dispatch: any,
dispatch: AppDispatch,
id: number,
): Promise<boolean> {
const resp = await axios.get(`/conversation/delete?id=${id}`);
@ -34,7 +37,7 @@ export async function deleteConversation(
}
export async function toggleConversation(
dispatch: any,
dispatch: AppDispatch,
id: number,
): Promise<void> {
return manager.toggle(dispatch, id);

View File

@ -9,12 +9,13 @@ import {
} from "../store/chat.ts";
import { useShared } from "../utils.ts";
import { ChatProps } from "./connection.ts";
import {supportModelConvertor} from "../conf.ts";
import { supportModelConvertor } from "../conf.ts";
import { AppDispatch } from "../store";
export class Manager {
conversations: Record<number, Conversation>;
current: number;
dispatch: any;
dispatch?: AppDispatch;
public constructor() {
this.conversations = {};
@ -22,7 +23,7 @@ export class Manager {
this.current = -1;
}
public setDispatch(dispatch: any): void {
public setDispatch(dispatch: AppDispatch): void {
this.dispatch = dispatch;
}
@ -30,7 +31,7 @@ export class Manager {
console.debug(
`[manager] conversation receive message (id: ${idx}, length: ${message.length})`,
);
if (idx === this.current) this.dispatch(setMessages(message));
if (idx === this.current) this.dispatch?.(setMessages(message));
}
public getCurrent(): number {
@ -57,24 +58,20 @@ export class Manager {
instance.load(res.message);
}
public async toggle(dispatch: any, id: number): Promise<void> {
public async toggle(dispatch: AppDispatch, id: number): Promise<void> {
if (!this.conversations[id]) await this.add(id);
this.current = id;
dispatch(setCurrent(id));
dispatch(setMessages(this.get(id)!.copyMessages()));
}
public async delete(dispatch: any, id: number): Promise<void> {
public async delete(dispatch: AppDispatch, id: number): Promise<void> {
if (this.getCurrent() === id) await this.toggle(dispatch, -1);
dispatch(removeHistory(id));
if (this.conversations[id]) delete this.conversations[id];
}
public async send(
t: any,
auth: boolean,
props: ChatProps,
): Promise<boolean> {
public async send(t: any, auth: boolean, props: ChatProps): Promise<boolean> {
props.model = supportModelConvertor[props.model.trim()];
const id = this.getCurrent();
if (!this.conversations[id]) return false;
@ -89,7 +86,7 @@ export class Manager {
`[manager] raise new conversation (name: ${props.message})`,
);
const { hook, useHook } = useShared<number>();
this.dispatch(
this.dispatch?.(
addHistory({
message: props.message,
hook,

View File

@ -27,6 +27,7 @@ const resources = {
"There was an error logging you in. Please try again.",
"request-failed":
"Request failed. Please check your network and try again.",
close: "Close",
conversation: {
title: "Conversation",
empty: "Empty",
@ -154,6 +155,12 @@ const resources = {
empty: "generating...",
download: "Download {{name}} format",
},
api: {
title: "API Settings",
copied: "Copied",
"copied-description": "API key has been copied to clipboard",
"learn-more": "Learn more",
},
},
},
cn: {
@ -175,6 +182,7 @@ const resources = {
"server-error": "服务器错误",
"server-error-prompt": "登录出错,请重试。",
"request-failed": "请求失败,请检查您的网络并重试。",
close: "关闭",
conversation: {
title: "会话",
empty: "空空如也",
@ -294,6 +302,12 @@ const resources = {
empty: "生成中...",
download: "下载 {{name}} 格式",
},
api: {
title: "API 设置",
copied: "复制成功",
"copied-description": "API 密钥已复制到剪贴板",
"learn-more": "了解更多",
},
},
},
ru: {
@ -318,6 +332,7 @@ const resources = {
"При входе произошла ошибка. Пожалуйста, попробуйте еще раз.",
"request-failed":
"Ошибка запроса. Пожалуйста, проверьте свою сеть и попробуйте еще раз.",
close: "Закрыть",
conversation: {
title: "Разговор",
empty: "Пусто",
@ -445,6 +460,12 @@ const resources = {
empty: "генерация...",
download: "Загрузить {{name}} формат",
},
api: {
title: "Настройки API",
copied: "Скопировано",
"copied-description": "Ключ API скопирован в буфер обмена",
"learn-more": "Узнать больше",
},
},
},
};

71
app/src/routes/ApiKey.tsx Normal file
View File

@ -0,0 +1,71 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import { Button } from "../components/ui/button.tsx";
import "../assets/api.less";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
closeDialog,
dialogSelector,
setDialog,
keySelector,
} from "../store/api.ts";
import {Input} from "../components/ui/input.tsx";
import {Copy, ExternalLink} from "lucide-react";
import {useToast} from "../components/ui/use-toast.ts";
import {copyClipboard} from "../utils.ts";
function Package() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector(dialogSelector);
const key = useSelector(keySelector);
const { toast } = useToast();
async function copyKey() {
await copyClipboard(key);
toast({
title: t("api.copied"),
description: t("api.copied-description"),
});
}
return (
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("api.title")}</DialogTitle>
<DialogDescription asChild>
<div className={`api-dialog`}>
<div className={`api-wrapper`}>
<Input value={key} />
<Button variant={`default`} size={`icon`} onClick={copyKey}>
<Copy className={`h-4 w-4`} />
</Button>
</div>
<Button variant={`outline`} asChild>
<a href={`https://docs.chatnio.net`} target={`_blank`}>
<ExternalLink className={`h-4 w-4 mr-2`} />
{t("buy.learn-more")}
</a>
</Button>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant={`outline`} onClick={() => dispatch(closeDialog())}>
{t("close")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default Package;

View File

@ -177,25 +177,28 @@ function Generation() {
return (
<div className={`generation-page`}>
<div className={`generation-container`}>
<Button
className={`action`}
variant={`ghost`}
size={`icon`}
onClick={() => router.navigate("/")}
disabled={state}
>
<ChevronLeft className={`h-5 w-5 back`} />
</Button>
<Wrapper
onSend={(prompt: string, model: string) => {
console.debug(
`[generation] create generation request (prompt: ${prompt}, model: ${supportModelConvertor[model]})`,
);
return manager.generateWithBlock(prompt, supportModelConvertor[model]);
}}
/>
</div>
<div className={`generation-container`}>
<Button
className={`action`}
variant={`ghost`}
size={`icon`}
onClick={() => router.navigate("/")}
disabled={state}
>
<ChevronLeft className={`h-5 w-5 back`} />
</Button>
<Wrapper
onSend={(prompt: string, model: string) => {
console.debug(
`[generation] create generation request (prompt: ${prompt}, model: ${supportModelConvertor[model]})`,
);
return manager.generateWithBlock(
prompt,
supportModelConvertor[model],
);
}}
/>
</div>
</div>
);
}

View File

@ -22,7 +22,7 @@ import {
} from "../components/ui/tooltip.tsx";
import { useDispatch, useSelector } from "react-redux";
import type { RootState } from "../store";
import {selectAuthenticated, selectInit} from "../store/auth.ts";
import { selectAuthenticated, selectInit } from "../store/auth.ts";
import { login, supportModels } from "../conf.ts";
import {
deleteConversation,
@ -171,7 +171,9 @@ function SideBar() {
<AlertDialogDescription>
{t("conversation.remove-description")}
<strong className={`conversation-name`}>
{ extractMessage(filterMessage(removeConversation?.name || "")) }
{extractMessage(
filterMessage(removeConversation?.name || ""),
)}
</strong>
{t("end")}
</AlertDialogDescription>
@ -298,15 +300,20 @@ function ChatWrapper() {
clearEvent?.();
}
async function processSend(data: string, auth: boolean, model: string, web: boolean): Promise<boolean> {
async function processSend(
data: string,
auth: boolean,
model: string,
web: boolean,
): Promise<boolean> {
const message: string = formatMessage(file, data);
if (message.length > 0 && data.trim().length > 0) {
if (await manager.send(t, auth, { message, web, model })) {
clearFile();
return true;
}
return false;
}
return false;
}
async function handleSend(auth: boolean, model: string, web: boolean) {

40
app/src/store/api.ts Normal file
View File

@ -0,0 +1,40 @@
import { createSlice } from "@reduxjs/toolkit";
import { getKey } from "../conversation/addition.ts";
import { AppDispatch } from "./index.ts";
export const apiSlice = createSlice({
name: "api",
initialState: {
dialog: false,
key: "",
},
reducers: {
toggleDialog: (state) => {
state.dialog = !state.dialog;
},
setDialog: (state, action) => {
state.dialog = action.payload as boolean;
},
openDialog: (state) => {
state.dialog = true;
},
closeDialog: (state) => {
state.dialog = false;
},
setKey: (state, action) => {
state.key = action.payload as string;
},
},
});
export const { toggleDialog, setDialog, openDialog, closeDialog, setKey } =
apiSlice.actions;
export default apiSlice.reducer;
export const dialogSelector = (state: any): boolean => state.api.dialog;
export const keySelector = (state: any): string => state.api.key;
export const getPackage = async (dispatch: AppDispatch) => {
const response = await getKey();
if (response.status) dispatch(setKey(response.key));
};

View File

@ -1,6 +1,7 @@
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { tokenField } from "../conf.ts";
import { AppDispatch } from "./index.ts";
export const authSlice = createSlice({
name: "auth",
@ -37,7 +38,11 @@ export const authSlice = createSlice({
},
});
export function validateToken(dispatch: any, token: string, hook?: () => any) {
export function validateToken(
dispatch: AppDispatch,
token: string,
hook?: () => any,
) {
token = token.trim();
dispatch(setToken(token));

View File

@ -5,6 +5,7 @@ import chatReducer from "./chat";
import quotaReducer from "./quota";
import packageReducer from "./package";
import subscriptionReducer from "./subscription";
import apiReducer from "./api";
const store = configureStore({
reducer: {
@ -14,6 +15,7 @@ const store = configureStore({
quota: quotaReducer,
package: packageReducer,
subscription: subscriptionReducer,
api: apiReducer,
},
});

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { getPackage } from "../conversation/addition.ts";
import { AppDispatch } from "./index.ts";
export const packageSlice = createSlice({
name: "package",
@ -41,7 +42,7 @@ export const dialogSelector = (state: any): boolean => state.package.dialog;
export const certSelector = (state: any): boolean => state.package.cert;
export const teenagerSelector = (state: any): boolean => state.package.teenager;
const refreshPackage = async (dispatch: any) => {
const refreshPackage = async (dispatch: AppDispatch) => {
const current = new Date().getTime(); //@ts-ignore
if (window.hasOwnProperty("package") && current - window.package < 2500)
return; //@ts-ignore
@ -51,7 +52,7 @@ const refreshPackage = async (dispatch: any) => {
if (response.status) dispatch(refreshState(response));
};
export const refreshPackageTask = (dispatch: any) => {
export const refreshPackageTask = (dispatch: AppDispatch) => {
setInterval(() => refreshPackage(dispatch), 20000);
refreshPackage(dispatch).then();
};

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "./index.ts";
import { AppDispatch, RootState } from "./index.ts";
import axios from "axios";
export const quotaSlice = createSlice({
@ -50,7 +50,7 @@ export const quotaValueSelector = (state: RootState): number =>
export const quotaSelector = (state: RootState): string =>
state.quota.quota.toFixed(2);
const refreshQuota = async (dispatch: any) => {
const refreshQuota = async (dispatch: AppDispatch) => {
const current = new Date().getTime(); //@ts-ignore
if (window.hasOwnProperty("quota") && current - window.quota < 2500) return; //@ts-ignore
window.quota = current;
@ -59,7 +59,7 @@ const refreshQuota = async (dispatch: any) => {
if (response.data.status) dispatch(setQuota(response.data.quota));
};
export const refreshQuotaTask = (dispatch: any) => {
export const refreshQuotaTask = (dispatch: AppDispatch) => {
setInterval(() => refreshQuota(dispatch), 5000);
refreshQuota(dispatch).then();
};

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { getSubscription } from "../conversation/addition.ts";
import { AppDispatch } from "./index.ts";
export const subscriptionSlice = createSlice({
name: "subscription",
@ -44,7 +45,7 @@ export const isSubscribedSelector = (state: any): boolean =>
export const expiredSelector = (state: any): number =>
state.subscription.expired;
export const refreshSubscription = async (dispatch: any) => {
export const refreshSubscription = async (dispatch: AppDispatch) => {
const current = new Date().getTime(); //@ts-ignore
if (
window.hasOwnProperty("subscription") && //@ts-ignore
@ -57,7 +58,7 @@ export const refreshSubscription = async (dispatch: any) => {
if (response.status) dispatch(updateSubscription(response));
};
export const refreshSubscriptionTask = (dispatch: any) => {
export const refreshSubscriptionTask = (dispatch: AppDispatch) => {
setInterval(() => refreshSubscription(dispatch), 20000);
refreshSubscription(dispatch).then();
};

View File

@ -161,7 +161,11 @@ export function filterMessage(message: string): string {
return message.replace(/```file\n\[\[.*]]\n[\s\S]*?\n```\n\n/g, "");
}
export function extractMessage(message: string, length: number = 50, flow: string = "...") {
export function extractMessage(
message: string,
length: number = 50,
flow: string = "...",
) {
return message.length > length ? message.slice(0, length) + flow : message;
}