mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-03 00:26:40 +08:00
Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web
This commit is contained in:
commit
65f7152ca3
@ -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, {
|
||||||
|
@ -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 };
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,61 +381,60 @@ 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]}
|
||||||
{theme === Theme.Auto ? (
|
icon={
|
||||||
<AutoIcon />
|
<>
|
||||||
) : theme === Theme.Light ? (
|
{theme === Theme.Auto ? (
|
||||||
<LightIcon />
|
<AutoIcon />
|
||||||
) : theme === Theme.Dark ? (
|
) : theme === Theme.Light ? (
|
||||||
<DarkIcon />
|
<LightIcon />
|
||||||
) : null}
|
) : theme === Theme.Dark ? (
|
||||||
</div>
|
<DarkIcon />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
};
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user