This commit is contained in:
GH Action - Upstream Sync 2023-06-13 01:41:18 +00:00
commit 65f7152ca3
7 changed files with 173 additions and 50 deletions

View File

@ -1,14 +1,32 @@
import { OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "../../auth";
import { requestOpenai } from "../../common"; import { requestOpenai } from "../../common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
async function handle( async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[OpenAI Route] params ", params); console.log("[OpenAI Route] params ", params);
const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + subpath,
},
{
status: 403,
},
);
}
const authResult = auth(req); const authResult = auth(req);
if (authResult.error) { if (authResult.error) {
return NextResponse.json(authResult, { return NextResponse.json(authResult, {

View File

@ -1,4 +1,4 @@
import { REQUEST_TIMEOUT_MS } from "@/app/constant"; import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
@ -10,10 +10,6 @@ import {
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
export class ChatGPTApi implements LLMApi { export class ChatGPTApi implements LLMApi {
public ChatPath = "v1/chat/completions";
public UsagePath = "dashboard/billing/usage";
public SubsPath = "dashboard/billing/subscription";
path(path: string): string { path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl; let openaiUrl = useAccessStore.getState().openaiUrl;
if (openaiUrl.endsWith("/")) { if (openaiUrl.endsWith("/")) {
@ -55,7 +51,7 @@ export class ChatGPTApi implements LLMApi {
options.onController?.(controller); options.onController?.(controller);
try { try {
const chatPath = this.path(this.ChatPath); const chatPath = this.path(OpenaiPath.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: "POST",
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
@ -177,14 +173,14 @@ export class ChatGPTApi implements LLMApi {
const [used, subs] = await Promise.all([ const [used, subs] = await Promise.all([
fetch( fetch(
this.path( this.path(
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`, `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
), ),
{ {
method: "GET", method: "GET",
headers: getHeaders(), headers: getHeaders(),
}, },
), ),
fetch(this.path(this.SubsPath), { fetch(this.path(OpenaiPath.SubsPath), {
method: "GET", method: "GET",
headers: getHeaders(), headers: getHeaders(),
}), }),
@ -228,3 +224,4 @@ export class ChatGPTApi implements LLMApi {
} as LLMUsage; } as LLMUsage;
} }
} }
export { OpenaiPath };

View File

@ -17,10 +17,38 @@
transition: all ease 0.3s; transition: all ease 0.3s;
margin-bottom: 10px; margin-bottom: 10px;
align-items: center; align-items: center;
height: 16px;
width: var(--icon-width);
&:not(:last-child) { &:not(:last-child) {
margin-right: 5px; margin-right: 5px;
} }
.text {
white-space: nowrap;
padding-left: 5px;
opacity: 0;
transform: translateX(-5px);
transition: all ease 0.3s;
transition-delay: 0.1s;
pointer-events: none;
}
&:hover {
width: var(--full-width);
.text {
opacity: 1;
transform: translate(0);
}
}
.text,
.icon {
display: flex;
align-items: center;
justify-content: center;
}
} }
} }

View File

@ -1,5 +1,5 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { useState, useRef, useEffect, useLayoutEffect } from "react"; import React, { useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
@ -279,6 +279,57 @@ function ClearContextDivider() {
); );
} }
function ChatAction(props: {
text: string;
icon: JSX.Element;
onClick: () => void;
}) {
const iconRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState({
full: 20,
icon: 20,
});
function updateWidth() {
if (!iconRef.current || !textRef.current) return;
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
const textWidth = getWidth(textRef.current);
const iconWidth = getWidth(iconRef.current);
setWidth({
full: textWidth + iconWidth,
icon: iconWidth,
});
}
useEffect(() => {
updateWidth();
}, []);
return (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={() => {
props.onClick();
setTimeout(updateWidth, 1);
}}
style={
{
"--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`,
} as React.CSSProperties
}
>
<div ref={iconRef} className={chatStyle["icon"]}>
{props.icon}
</div>
<div className={chatStyle["text"]} ref={textRef}>
{props.text}
</div>
</div>
);
}
function useScrollToBottom() { function useScrollToBottom() {
// for auto-scroll // for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
@ -330,34 +381,32 @@ export function ChatActions(props: {
return ( return (
<div className={chatStyle["chat-input-actions"]}> <div className={chatStyle["chat-input-actions"]}>
{couldStop && ( {couldStop && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={stopAll} onClick={stopAll}
> text={Locale.Chat.InputActions.Stop}
<StopIcon /> icon={<StopIcon />}
</div> />
)} )}
{!props.hitBottom && ( {!props.hitBottom && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.scrollToBottom} onClick={props.scrollToBottom}
> text={Locale.Chat.InputActions.ToBottom}
<BottomIcon /> icon={<BottomIcon />}
</div> />
)} )}
{props.hitBottom && ( {props.hitBottom && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptModal} onClick={props.showPromptModal}
> text={Locale.Chat.InputActions.Settings}
<SettingsIcon /> icon={<SettingsIcon />}
</div> />
)} )}
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={nextTheme} onClick={nextTheme}
> text={Locale.Chat.InputActions.Theme[theme]}
icon={
<>
{theme === Theme.Auto ? ( {theme === Theme.Auto ? (
<AutoIcon /> <AutoIcon />
) : theme === Theme.Light ? ( ) : theme === Theme.Light ? (
@ -365,26 +414,27 @@ export function ChatActions(props: {
) : theme === Theme.Dark ? ( ) : theme === Theme.Dark ? (
<DarkIcon /> <DarkIcon />
) : null} ) : null}
</div> </>
}
/>
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptHints} onClick={props.showPromptHints}
> text={Locale.Chat.InputActions.Prompt}
<PromptIcon /> icon={<PromptIcon />}
</div> />
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={() => { onClick={() => {
navigate(Path.Masks); navigate(Path.Masks);
}} }}
> text={Locale.Chat.InputActions.Masks}
<MaskIcon /> icon={<MaskIcon />}
</div> />
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`} text={Locale.Chat.InputActions.Clear}
icon={<BreakIcon />}
onClick={() => { onClick={() => {
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
if (session.clearContextIndex === session.messages.length) { if (session.clearContextIndex === session.messages.length) {
@ -395,9 +445,7 @@ export function ChatActions(props: {
} }
}); });
}} }}
> />
<BreakIcon />
</div>
</div> </div>
); );
} }

View File

@ -45,3 +45,9 @@ export const LAST_INPUT_KEY = "last-input";
export const REQUEST_TIMEOUT_MS = 60000; export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
};

View File

@ -27,6 +27,19 @@ const cn = {
Retry: "重试", Retry: "重试",
Delete: "删除", Delete: "删除",
}, },
InputActions: {
Stop: "停止响应",
ToBottom: "滚到最新",
Theme: {
auto: "自动主题",
light: "亮色模式",
dark: "深色模式",
},
Prompt: "快捷指令",
Masks: "所有面具",
Clear: "清除聊天",
Settings: "对话设置",
},
Rename: "重命名对话", Rename: "重命名对话",
Typing: "正在输入…", Typing: "正在输入…",
Input: (submitKey: string) => { Input: (submitKey: string) => {

View File

@ -28,6 +28,19 @@ const en: RequiredLocaleType = {
Retry: "Retry", Retry: "Retry",
Delete: "Delete", Delete: "Delete",
}, },
InputActions: {
Stop: "Stop",
ToBottom: "To Latest",
Theme: {
auto: "Auto",
light: "Light Theme",
dark: "Dark Theme",
},
Prompt: "Prompts",
Masks: "Masks",
Clear: "Clear Context",
Settings: "Settings",
},
Rename: "Rename Chat", Rename: "Rename Chat",
Typing: "Typing…", Typing: "Typing…",
Input: (submitKey: string) => { Input: (submitKey: string) => {