Merge remote-tracking branch 'upstream/main'

This commit is contained in:
ZhaoLiu 2023-05-16 11:01:36 +08:00
commit 1742f6eb93
42 changed files with 1215 additions and 654 deletions

View File

@ -31,7 +31,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- New in v2: create, share and debug your chat tools with prompt templates (mask)
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
- Automatically compresses chat history to support long conversations while also saving your tokens
- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch
- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
## Roadmap
@ -62,7 +62,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
@ -265,6 +265,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[@jhansion](https://github.com/jhansion)
[@Sha1rholder](https://github.com/Sha1rholder)
[@AnsonHyq](https://github.com/AnsonHyq)
[@synwith](https://github.com/synwith)
### Contributor

View File

@ -43,8 +43,7 @@ export function auth(req: NextRequest) {
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return {
error: true,
needAccessCode: true,
msg: "Please go settings page and fill your access code.",
msg: !accessCode ? "empty access code" : "wrong access code",
};
}
@ -58,7 +57,7 @@ export function auth(req: NextRequest) {
console.log("[Auth] admin did not provide an api key");
return {
error: true,
msg: "Empty Api Key",
msg: "admin did not provide an api key",
};
}
} else {

View File

@ -1,49 +1,8 @@
import { createParser } from "eventsource-parser";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
import { requestOpenai } from "../../common";
async function createStream(res: Response) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
async start(controller) {
function onParse(event: any) {
if (event.type === "event") {
const data = event.data;
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
if (data === "[DONE]") {
controller.close();
return;
}
try {
const json = JSON.parse(data);
const text = json.choices[0].delta.content;
const queue = encoder.encode(text);
controller.enqueue(queue);
} catch (e) {
controller.error(e);
}
}
}
const parser = createParser(onParse);
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk, { stream: true }));
}
},
});
return stream;
}
function formatResponse(msg: any) {
const jsonMsg = ["```json\n", JSON.stringify(msg, null, " "), "\n```"].join(
"",
);
return new Response(jsonMsg);
}
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
@ -58,40 +17,10 @@ async function handle(
}
try {
const api = await requestOpenai(req);
const contentType = api.headers.get("Content-Type") ?? "";
// streaming response
if (contentType.includes("stream")) {
const stream = await createStream(api);
const res = new Response(stream);
res.headers.set("Content-Type", contentType);
return res;
}
// try to parse error msg
try {
const mayBeErrorBody = await api.json();
if (mayBeErrorBody.error) {
console.error("[OpenAI Response] ", mayBeErrorBody);
return formatResponse(mayBeErrorBody);
} else {
const res = new Response(JSON.stringify(mayBeErrorBody));
res.headers.set("Content-Type", "application/json");
res.headers.set("Cache-Control", "no-cache");
return res;
}
} catch (e) {
console.error("[OpenAI Parse] ", e);
return formatResponse({
msg: "invalid response from openai server",
error: e,
});
}
return await requestOpenai(req);
} catch (e) {
console.error("[OpenAI] ", e);
return formatResponse(e);
return NextResponse.json(prettyObject(e));
}
}

View File

@ -1,9 +0,0 @@
import type {
CreateChatCompletionRequest,
CreateChatCompletionResponse,
} from "openai";
export type ChatRequest = CreateChatCompletionRequest;
export type ChatResponse = CreateChatCompletionResponse;
export type Updater<T> = (updater: (value: T) => void) => void;

83
app/client/api.ts Normal file
View File

@ -0,0 +1,83 @@
import { ACCESS_CODE_PREFIX } from "../constant";
import { ModelConfig, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
export type ChatModel = ModelType;
export interface RequestMessage {
role: MessageRole;
content: string;
}
export interface LLMConfig {
model: string;
temperature?: number;
top_p?: number;
stream?: boolean;
presence_penalty?: number;
frequency_penalty?: number;
}
export interface ChatOptions {
messages: RequestMessage[];
config: LLMConfig;
onUpdate?: (message: string, chunk: string) => void;
onFinish: (message: string) => void;
onError?: (err: Error) => void;
onController?: (controller: AbortController) => void;
}
export interface LLMUsage {
used: number;
total: number;
}
export abstract class LLMApi {
abstract chat(options: ChatOptions): Promise<void>;
abstract usage(): Promise<LLMUsage>;
}
export class ClientApi {
public llm: LLMApi;
constructor() {
this.llm = new ChatGPTApi();
}
config() {}
prompts() {}
masks() {}
}
export const api = new ClientApi();
export function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {
"Content-Type": "application/json",
};
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
const validString = (x: string) => x && x.length > 0;
// use user's api key first
if (validString(accessStore.token)) {
headers.Authorization = makeBearer(accessStore.token);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers.Authorization = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
return headers;
}

37
app/client/controller.ts Normal file
View File

@ -0,0 +1,37 @@
// To store message streaming controller
export const ChatControllerPool = {
controllers: {} as Record<string, AbortController>,
addController(
sessionIndex: number,
messageId: number,
controller: AbortController,
) {
const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
const controller = this.controllers[key];
controller?.abort();
},
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
},
hasPending() {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
},
key(sessionIndex: number, messageIndex: number) {
return `${sessionIndex},${messageIndex}`;
},
};

View File

@ -0,0 +1,194 @@
import { REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
import Locale from "../../locales";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
export class ChatGPTApi implements LLMApi {
public ChatPath = "v1/chat/completions";
public UsagePath = "dashboard/billing/usage";
public SubsPath = "dashboard/billing/subscription";
path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl;
if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
}
return [openaiUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
content: v.content,
}));
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
},
};
const requestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
};
console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(this.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const reqestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
let responseText = "";
const finish = () => {
options.onFinish(responseText);
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(reqestTimeoutId);
if (res.status === 401) {
let extraInfo = { error: undefined };
try {
extraInfo = await res.clone().json();
} catch {}
responseText += "\n\n" + Locale.Error.Unauthorized;
if (extraInfo.error) {
responseText += "\n\n" + prettyObject(extraInfo);
}
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]") {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = json.choices[0].delta.content;
if (delta) {
responseText += delta;
options.onUpdate?.(responseText, delta);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
},
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(reqestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat reqeust", e);
options.onError?.(e as Error);
}
}
async usage() {
const formatDate = (d: Date) =>
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
.getDate()
.toString()
.padStart(2, "0")}`;
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth);
const endDate = formatDate(new Date(Date.now() + ONE_DAY));
const [used, subs] = await Promise.all([
fetch(
this.path(
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
),
{
method: "GET",
headers: getHeaders(),
},
),
fetch(this.path(this.SubsPath), {
method: "GET",
headers: getHeaders(),
}),
]);
if (!used.ok || !subs.ok || used.status === 401) {
throw new Error(Locale.Error.Unauthorized);
}
const response = (await used.json()) as {
total_usage?: number;
error?: {
type: string;
message: string;
};
};
const total = (await subs.json()) as {
hard_limit_usd?: number;
};
if (response.error && response.error.type) {
throw Error(response.error.message);
}
if (response.total_usage) {
response.total_usage = Math.round(response.total_usage) / 100;
}
if (total.hard_limit_usd) {
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
}
return {
used: response.total_usage,
total: total.hard_limit_usd,
} as LLMUsage;
}
}

View File

@ -16,6 +16,7 @@ import { Link, useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask";
import { useRef, useEffect } from "react";
export function ChatItem(props: {
onClick?: () => void;
@ -29,6 +30,14 @@ export function ChatItem(props: {
narrow?: boolean;
mask: Mask;
}) {
const draggableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
});
}
}, [props.selected]);
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
@ -37,7 +46,10 @@ export function ChatItem(props: {
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={provided.innerRef}
ref={(ele) => {
draggableRef.current = ele;
provided.innerRef(ele);
}}
{...provided.draggableProps}
{...provided.dragHandleProps}
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(

View File

@ -22,7 +22,7 @@ import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg";
import {
Message,
ChatMessage,
SubmitKey,
useChatStore,
BOT_HELLO,
@ -43,7 +43,7 @@ import {
import dynamic from "next/dynamic";
import { ControllerPool } from "../requests";
import { ChatControllerPool } from "../client/controller";
import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales";
@ -63,7 +63,7 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
});
function exportMessages(messages: Message[], topic: string) {
function exportMessages(messages: ChatMessage[], topic: string) {
const mdText =
`# ${topic}\n\n` +
messages
@ -230,7 +230,9 @@ export function PromptHints(props: {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (noPrompts) return;
if (e.metaKey || e.altKey || e.ctrlKey) {
return;
}
// arrow up / down to select prompt
const changeIndex = (delta: number) => {
e.stopPropagation();
@ -329,8 +331,8 @@ export function ChatActions(props: {
}
// stop all responses
const couldStop = ControllerPool.hasPending();
const stopAll = () => ControllerPool.stopAll();
const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll();
return (
<div className={chatStyle["chat-input-actions"]}>
@ -392,7 +394,7 @@ export function ChatActions(props: {
}
export function Chat() {
type RenderMessage = Message & { preview?: boolean };
type RenderMessage = ChatMessage & { preview?: boolean };
const chatStore = useChatStore();
const [session, sessionIndex] = useChatStore((state) => [
@ -485,13 +487,17 @@ export function Chat() {
// stop response
const onUserStop = (messageId: number) => {
ControllerPool.stop(sessionIndex, messageId);
ChatControllerPool.stop(sessionIndex, messageId);
};
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// if ArrowUp and no userInput, fill with last input
if (e.key === "ArrowUp" && userInput.length <= 0) {
if (
e.key === "ArrowUp" &&
userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey)
) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
e.preventDefault();
return;
@ -501,7 +507,7 @@ export function Chat() {
e.preventDefault();
}
};
const onRightClick = (e: any, message: Message) => {
const onRightClick = (e: any, message: ChatMessage) => {
// copy to clipboard
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
@ -789,7 +795,14 @@ export function Chat() {
scrollToBottom={scrollToBottom}
hitBottom={hitBottom}
showPromptHints={() => {
// Click again to close
if (promptHints.length > 0) {
setPromptHints([]);
return;
}
inputRef.current?.focus();
setUserInput("/");
onSearch("");
}}
/>

View File

@ -186,7 +186,7 @@
.chat-item-delete {
position: absolute;
top: 10px;
right: -20px;
right: 0;
transition: all ease 0.3s;
opacity: 0;
cursor: pointer;
@ -194,7 +194,7 @@
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
right: 10px;
transform: translateX(-10px);
}
.chat-item:hover > .chat-item-delete:hover {

View File

@ -23,7 +23,6 @@ import {
} from "react-router-dom";
import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
import { useMaskStore } from "../store/mask";
export function Loading(props: { noLogo?: boolean }) {
return (
@ -64,17 +63,17 @@ export function useSwitchTheme() {
}
const metaDescriptionDark = document.querySelector(
'meta[name="theme-color"][media]',
'meta[name="theme-color"][media*="dark"]',
);
const metaDescriptionLight = document.querySelector(
'meta[name="theme-color"]:not([media])',
'meta[name="theme-color"][media*="light"]',
);
if (config.theme === "auto") {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
} else {
const themeColor = getCSSVar("--themeColor");
const themeColor = getCSSVar("--theme-color");
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
}
@ -91,12 +90,24 @@ const useHasHydrated = () => {
return hasHydrated;
};
const loadAsyncGoogleFont = () => {
const linkEl = document.createElement("link");
linkEl.rel = "stylesheet";
linkEl.href =
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
document.head.appendChild(linkEl);
};
function Screen() {
const config = useAppConfig();
const location = useLocation();
const isHome = location.pathname === Path.Home;
const isMobileScreen = useMobileScreen();
useEffect(() => {
loadAsyncGoogleFont();
}, []);
return (
<div
className={

View File

@ -13,8 +13,9 @@ import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { ChatMessage, ModelConfig, useChatStore } from "../store";
import { ROLES } from "../client/api";
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
@ -22,7 +23,7 @@ import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useState } from "react";
import { downloadAs, readFromFile } from "../utils";
import { Updater } from "../api/openai/typing";
import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
@ -107,8 +108,8 @@ export function MaskConfig(props: {
}
function ContextPromptItem(props: {
prompt: Message;
update: (prompt: Message) => void;
prompt: ChatMessage;
update: (prompt: ChatMessage) => void;
remove: () => void;
}) {
const [focusingInput, setFocusingInput] = useState(false);
@ -116,7 +117,7 @@ function ContextPromptItem(props: {
return (
<div className={chatStyle["context-prompt-row"]}>
{!focusingInput && (
<select
<Select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
@ -131,7 +132,7 @@ function ContextPromptItem(props: {
{r}
</option>
))}
</select>
</Select>
)}
<Input
value={props.prompt.content}
@ -160,12 +161,12 @@ function ContextPromptItem(props: {
}
export function ContextPrompts(props: {
context: Message[];
updateContext: (updater: (context: Message[]) => void) => void;
context: ChatMessage[];
updateContext: (updater: (context: ChatMessage[]) => void) => void;
}) {
const context = props.context;
const addContextPrompt = (prompt: Message) => {
const addContextPrompt = (prompt: ChatMessage) => {
props.updateContext((context) => context.push(prompt));
};
@ -173,7 +174,7 @@ export function ContextPrompts(props: {
props.updateContext((context) => context.splice(i, 1));
};
const updateContextPrompt = (i: number, prompt: Message) => {
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
props.updateContext((context) => (context[i] = prompt));
};
@ -307,7 +308,7 @@ export function MaskPage() {
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<select
<Select
className={styles["mask-filter-lang"]}
value={filterLang ?? Locale.Settings.Lang.All}
onChange={(e) => {
@ -327,7 +328,7 @@ export function MaskPage() {
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</select>
</Select>
<IconButton
className={styles["mask-create"]}

View File

@ -2,7 +2,7 @@ import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";
import { InputRange } from "./input-range";
import { List, ListItem } from "./ui-lib";
import { List, ListItem, Select } from "./ui-lib";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
@ -11,7 +11,7 @@ export function ModelConfigList(props: {
return (
<>
<ListItem title={Locale.Settings.Model}>
<select
<Select
value={props.modelConfig.model}
onChange={(e) => {
props.updateConfig(
@ -27,7 +27,7 @@ export function ModelConfigList(props: {
{v.name}
</option>
))}
</select>
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Temperature.Title}

View File

@ -10,7 +10,15 @@ import ClearIcon from "../icons/clear.svg";
import LoadingIcon from "../icons/three-dots.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
import {
Input,
List,
ListItem,
Modal,
PasswordInput,
Popover,
Select,
} from "./ui-lib";
import { ModelConfigList } from "./model-config";
import { IconButton } from "./button";
@ -368,7 +376,7 @@ export function Settings() {
</ListItem>
<ListItem title={Locale.Settings.SendKey}>
<select
<Select
value={config.submitKey}
onChange={(e) => {
updateConfig(
@ -382,11 +390,11 @@ export function Settings() {
{v}
</option>
))}
</select>
</Select>
</ListItem>
<ListItem title={Locale.Settings.Theme}>
<select
<Select
value={config.theme}
onChange={(e) => {
updateConfig(
@ -399,11 +407,11 @@ export function Settings() {
{v}
</option>
))}
</select>
</Select>
</ListItem>
<ListItem title={Locale.Settings.Lang.Name}>
<select
<Select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
@ -414,7 +422,7 @@ export function Settings() {
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</select>
</Select>
</ListItem>
<ListItem
@ -565,9 +573,9 @@ export function Settings() {
<List>
<ModelConfigList
modelConfig={config.modelConfig}
updateConfig={(upater) => {
updateConfig={(updater) => {
const modelConfig = { ...config.modelConfig };
upater(modelConfig);
updater(modelConfig);
config.update((config) => (config.modelConfig = modelConfig));
}}
/>

View File

@ -203,3 +203,28 @@
resize: none;
min-width: 50px;
}
.select-with-icon {
position: relative;
max-width: fit-content;
.select-with-icon-select {
height: 100%;
border: var(--border-in-light);
padding: 10px 25px 10px 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;
background-color: var(--white);
color: var(--black);
text-align: center;
}
.select-with-icon-icon {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
}
}

View File

@ -3,6 +3,7 @@ import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg";
import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react";
@ -244,3 +245,20 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
</div>
);
}
export function Select(
props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
>,
) {
const { className, children, ...otherProps } = props;
return (
<div className={`${styles["select-with-icon"]} ${className}`}>
<select className={styles["select-with-icon-select"]} {...otherProps}>
{children}
</select>
<DownIcon className={styles["select-with-icon-icon"]} />
</div>
);
}

View File

@ -40,3 +40,5 @@ export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "ak-";
export const LAST_INPUT_KEY = "last-input";
export const REQUEST_TIMEOUT_MS = 60000;

1
app/icons/down.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(-90 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 575 B

View File

@ -8,12 +8,11 @@ const buildConfig = getBuildConfig();
export const metadata = {
title: "SoulShellGPT",
description: "Your personal Chat Bot.",
description: "Your personal ChatGPT Chat Bot.",
appleWebApp: {
title: "SoulShellGPT",
statusBarStyle: "default",
},
themeColor: "#fafafa",
};
export default function RootLayout({
@ -24,22 +23,8 @@ export default function RootLayout({
return (
<html lang="en">
<head>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta
name="theme-color"
content="#151515"
media="(prefers-color-scheme: dark)"
/>
<meta name="version" content={buildConfig.commitId} />
<link rel="manifest" href="/site.webmanifest"></link>
<link rel="preconnect" href="https://fonts.proxy.ustclug.org"></link>
<link
href="https://fonts.proxy.ustclug.org/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
rel="stylesheet"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>{children}</body>

View File

@ -67,7 +67,7 @@ const cn = {
ConfirmClearAll: "确认清除所有数据?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
@ -78,7 +78,9 @@ const cn = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "头像",

244
app/locales/cs.ts Normal file
View File

@ -0,0 +1,244 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const cs: LocaleType = {
WIP: "V přípravě...",
Error: {
Unauthorized:
"Neoprávněný přístup, zadejte přístupový kód na stránce nastavení.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} zpráv`,
},
Chat: {
SubTitle: (count: number) => `${count} zpráv s ChatGPT`,
Actions: {
ChatList: "Přejít na seznam chatů",
CompressedHistory: "Pokyn z komprimované paměti historie",
Export: "Exportovat všechny zprávy jako Markdown",
Copy: "Kopírovat",
Stop: "Zastavit",
Retry: "Zopakovat",
Delete: "Smazat",
},
Rename: "Přejmenovat chat",
Typing: "Píše...",
Input: (submitKey: string) => {
var inputHints = `${submitKey} pro odeslání`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter pro řádkování";
}
return inputHints + ", / pro vyhledávání pokynů";
},
Send: "Odeslat",
Config: {
Reset: "Obnovit výchozí",
SaveAs: "Uložit jako Masku",
},
},
Export: {
Title: "Všechny zprávy",
Copy: "Kopírovat vše",
Download: "Stáhnout",
MessageFromYou: "Zpráva od vás",
MessageFromChatGPT: "Zpráva z ChatGPT",
},
Memory: {
Title: "Pokyn z paměti",
EmptyContent: "Zatím nic.",
Send: "Odeslat paměť",
Copy: "Kopírovat paměť",
Reset: "Obnovit relaci",
ResetConfirm:
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
},
Home: {
NewChat: "Nový chat",
DeleteChat: "Potvrzujete smazání vybrané konverzace?",
DeleteToast: "Chat smazán",
Revert: "Zvrátit",
},
Settings: {
Title: "Nastavení",
SubTitle: "Všechna nastavení",
Actions: {
ClearAll: "Vymazat všechna data",
ResetAll: "Obnovit veškeré nastavení",
Close: "Zavřít",
ConfirmResetAll: "Jste si jisti, že chcete obnovit všechna nastavení?",
ConfirmClearAll: "Jste si jisti, že chcete smazat všechna data?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Všechny jazyky",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Velikost písma",
SubTitle: "Nastavení velikosti písma obsahu chatu",
},
Update: {
Version: (x: string) => `Verze: ${x}`,
IsLatest: "Aktuální verze",
CheckUpdate: "Zkontrolovat aktualizace",
IsChecking: "Kontrola aktualizace...",
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
GoToUpdate: "Aktualizovat",
},
SendKey: "Odeslat klíč",
Theme: "Téma",
TightBorder: "Těsné ohraničení",
SendPreviewBubble: {
Title: "Odesílat chatovací bublinu s náhledem",
SubTitle: "Zobrazit v náhledu bubliny",
},
Mask: {
Title: "Úvodní obrazovka Masek",
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek",
},
Prompt: {
Disable: {
Title: "Deaktivovat automatické dokončování",
SubTitle: "Zadejte / pro spuštění automatického dokončování",
},
List: "Seznam pokynů",
ListCount: (builtin: number, custom: number) =>
`${builtin} vestavěných, ${custom} uživatelských`,
Edit: "Upravit",
Modal: {
Title: "Seznam pokynů",
Add: "Přidat pokyn",
Search: "Hledat pokyny",
},
EditModal: {
Title: "Editovat pokyn",
},
},
HistoryCount: {
Title: "Počet připojených zpráv",
SubTitle: "Počet odeslaných připojených zpráv na žádost",
},
CompressThreshold: {
Title: "Práh pro kompresi historie",
SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
},
Token: {
Title: "API klíč",
SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
Placeholder: "Klíč API OpenAI",
},
Usage: {
Title: "Stav účtu",
SubTitle(used: any, total: any) {
return `Použito tento měsíc $${used}, předplaceno $${total}`;
},
IsChecking: "Kontroluji...",
Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
},
AccessCode: {
Title: "Přístupový kód",
SubTitle: "Kontrola přístupu povolena",
Placeholder: "Potřebujete přístupový kód",
},
Model: "Model",
Temperature: {
Title: "Teplota",
SubTitle: "Větší hodnota činí výstup náhodnějším",
},
MaxTokens: {
Title: "Max. počet tokenů",
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů",
},
PresencePenlty: {
Title: "Přítomnostní korekce",
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
},
},
Store: {
DefaultTopic: "Nová konverzace",
BotHello: "Ahoj! Jak mohu dnes pomoci?",
Error: "Něco se pokazilo, zkuste to prosím později.",
Prompt: {
History: (content: string) =>
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " +
content,
Topic:
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.",
Summarize:
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.",
},
},
Copy: {
Success: "Zkopírováno do schránky",
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
},
Context: {
Toast: (x: any) => `Použití ${x} kontextových pokynů`,
Edit: "Kontextové a paměťové pokyny",
Add: "Přidat pokyn",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Maska",
Page: {
Title: "Šablona pokynu",
SubTitle: (count: number) => `${count} šablon pokynů`,
Search: "Hledat v šablonách",
Create: "Vytvořit",
},
Item: {
Info: (count: number) => `${count} pokynů`,
Chat: "Chat",
View: "Zobrazit",
Edit: "Upravit",
Delete: "Smazat",
DeleteConfirm: "Potvrdit smazání?",
},
EditModal: {
Title: (readonly: boolean) =>
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`,
Download: "Stáhnout",
Clone: "Duplikovat",
},
Config: {
Avatar: "Avatar Bota",
Name: "Jméno Bota",
},
},
NewChat: {
Return: "Zpět",
Skip: "Přeskočit",
Title: "Vyberte Masku",
SubTitle: "Chatovat s duší za Maskou",
More: "Najít více",
NotShow: "Nezobrazovat znovu",
ConfirmNoShow: "Potvrdit zakázáníMůžete jej povolit později v nastavení.",
},
UI: {
Confirm: "Potvrdit",
Cancel: "Zrušit",
Close: "Zavřít",
Create: "Vytvořit",
Edit: "Upravit",
},
};
export default cs;

View File

@ -71,7 +71,7 @@ const de: LocaleType = {
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "All Languages",
All: "Alle Sprachen",
Options: {
cn: "简体中文",
en: "English",
@ -81,7 +81,9 @@ const de: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",

View File

@ -80,7 +80,9 @@ const en: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",

View File

@ -69,18 +69,20 @@ const es: LocaleType = {
ConfirmClearAll: "Are you sure you want to reset all chat?",
},
Lang: {
Name: "Language",
All: "All Languages",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Todos los idiomas",
Options: {
cn: "简体中文",
en: "Inglés",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",

View File

@ -7,6 +7,8 @@ import TR from "./tr";
import JP from "./jp";
import DE from "./de";
import VI from "./vi";
import RU from "./ru";
import CS from "./cs";
export type { LocaleType } from "./cn";
@ -20,6 +22,8 @@ export const AllLangs = [
"jp",
"de",
"vi",
"ru",
"cs",
] as const;
export type Lang = (typeof AllLangs)[number];
@ -82,4 +86,6 @@ export default {
jp: JP,
de: DE,
vi: VI,
ru: RU,
cs: CS,
}[getLang()] as typeof CN;

View File

@ -69,8 +69,8 @@ const it: LocaleType = {
ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?",
},
Lang: {
Name: "Lingue",
All: "All Languages",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tutte le lingue",
Options: {
cn: "简体中文",
en: "English",
@ -80,7 +80,9 @@ const it: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",

View File

@ -69,7 +69,7 @@ const jp: LocaleType = {
ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
@ -80,7 +80,9 @@ const jp: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "アバター",

250
app/locales/ru.ts Normal file
View File

@ -0,0 +1,250 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const ru: LocaleType = {
WIP: "Скоро...",
Error: {
Unauthorized:
"Несанкционированный доступ. Пожалуйста, введите код доступа на странице настроек.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} сообщений`,
},
Chat: {
SubTitle: (count: number) => `${count} сообщений с ChatGPT`,
Actions: {
ChatList: "Перейти к списку чатов",
CompressedHistory: "Сжатая история памяти",
Export: "Экспортировать все сообщения в формате Markdown",
Copy: "Копировать",
Stop: "Остановить",
Retry: "Повторить",
Delete: "Удалить",
},
Rename: "Переименовать чат",
Typing: "Печатает…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} для отправки сообщения`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter для переноса строки";
}
return inputHints + ", / для поиска подсказок";
},
Send: "Отправить",
Config: {
Reset: "Сбросить настройки",
SaveAs: "Сохранить как маску",
},
},
Export: {
Title: "Все сообщения",
Copy: "Копировать все",
Download: "Скачать",
MessageFromYou: "Сообщение от вас",
MessageFromChatGPT: "Сообщение от ChatGPT",
},
Memory: {
Title: "Память",
EmptyContent: "Пусто.",
Send: "Отправить память",
Copy: "Копировать память",
Reset: "Сбросить сессию",
ResetConfirm:
"При сбросе текущая история переписки и историческая память будут удалены. Вы уверены, что хотите сбросить?",
},
Home: {
NewChat: "Новый чат",
DeleteChat: "Вы действительно хотите удалить выбранный разговор?",
DeleteToast: "Чат удален",
Revert: "Отмена",
},
Settings: {
Title: "Настройки",
SubTitle: "Все настройки",
Actions: {
ClearAll: "Очистить все данные",
ResetAll: "Сбросить все настройки",
Close: "Закрыть",
ConfirmResetAll: "Вы уверены, что хотите сбросить все настройки?",
ConfirmClearAll: "Вы уверены, что хотите очистить все данные?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Все языки",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Аватар",
FontSize: {
Title: "Размер шрифта",
SubTitle: "Настроить размер шрифта контента чата",
},
Update: {
Version: (x: string) => `Версия: ${x}`,
IsLatest: "Последняя версия",
CheckUpdate: "Проверить обновление",
IsChecking: "Проверка обновления...",
FoundUpdate: (x: string) => `Найдена новая версия: ${x}`,
GoToUpdate: "Обновить",
},
SendKey: "Клавиша отправки",
Theme: "Тема",
TightBorder: "Узкая граница",
SendPreviewBubble: {
Title: "Отправить предпросмотр",
SubTitle: "Предварительный просмотр markdown в пузыре",
},
Mask: {
Title: "Экран заставки маски",
SubTitle: "Показывать экран заставки маски перед началом нового чата",
},
Prompt: {
Disable: {
Title: "Отключить автозаполнение",
SubTitle: "Ввод / для запуска автозаполнения",
},
List: "Список подсказок",
ListCount: (builtin: number, custom: number) =>
`${builtin} встроенных, ${custom} пользовательских`,
Edit: "Редактировать",
Modal: {
Title: "Список подсказок",
Add: "Добавить",
Search: "Поиск подсказок",
},
EditModal: {
Title: "Редактировать подсказку",
},
},
HistoryCount: {
Title: "Количество прикрепляемых сообщений",
SubTitle:
"Количество отправляемых сообщений, прикрепляемых к каждому запросу",
},
CompressThreshold: {
Title: "Порог сжатия истории",
SubTitle:
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
},
Token: {
Title: "API ключ",
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
Placeholder: "API ключ OpenAI",
},
Usage: {
Title: "Баланс аккаунта",
SubTitle(used: any, total: any) {
return `Использовано в этом месяце $${used}, подписка $${total}`;
},
IsChecking: "Проверка...",
Check: "Проверить",
NoAccess: "Введите API ключ, чтобы проверить баланс",
},
AccessCode: {
Title: "Код доступа",
SubTitle: "Контроль доступа включен",
Placeholder: "Требуется код доступа",
},
Model: "Модель",
Temperature: {
Title: "Температура",
SubTitle: "Чем выше значение, тем более случайный вывод",
},
MaxTokens: {
Title: "Максимальное количество токенов",
SubTitle: "Максимальная длина вводных и генерируемых токенов",
},
PresencePenlty: {
Title: "Штраф за повторения",
SubTitle:
"Чем выше значение, тем больше вероятность общения на новые темы",
},
},
Store: {
DefaultTopic: "Новый разговор",
BotHello: "Здравствуйте! Как я могу вам помочь сегодня?",
Error: "Что-то пошло не так. Пожалуйста, попробуйте еще раз позже.",
Prompt: {
History: (content: string) =>
"Это краткое содержание истории чата между ИИ и пользователем: " +
content,
Topic:
"Пожалуйста, создайте заголовок из четырех или пяти слов, который кратко описывает нашу беседу, без введения, знаков пунктуации, кавычек, точек, символов или дополнительного текста. Удалите кавычки.",
Summarize:
"Кратко изложите нашу дискуссию в 200 словах или менее для использования в будущем контексте.",
},
},
Copy: {
Success: "Скопировано в буфер обмена",
Failed:
"Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена",
},
Context: {
Toast: (x: any) => `С ${x} контекстными подсказками`,
Edit: "Контекстные и памятные подсказки",
Add: "Добавить подсказку",
},
Plugin: {
Name: "Плагин",
},
Mask: {
Name: "Маска",
Page: {
Title: "Шаблон подсказки",
SubTitle: (count: number) => `${count} шаблонов подсказок`,
Search: "Поиск шаблонов",
Create: "Создать",
},
Item: {
Info: (count: number) => `${count} подсказок`,
Chat: "Чат",
View: "Просмотр",
Edit: "Редактировать",
Delete: "Удалить",
DeleteConfirm: "Подтвердить удаление?",
},
EditModal: {
Title: (readonly: boolean) =>
`Редактирование шаблона подсказки ${
readonly ? "(только для чтения)" : ""
}`,
Download: "Скачать",
Clone: "Клонировать",
},
Config: {
Avatar: "Аватар бота",
Name: "Имя бота",
},
},
NewChat: {
Return: "Вернуться",
Skip: "Пропустить",
Title: "Выберите маску",
SubTitle: "Общайтесь с душой за маской",
More: "Найти еще",
NotShow: "Не показывать снова",
ConfirmNoShow:
"Подтвердите отключение? Вы можете включить это позже в настройках.",
},
UI: {
Confirm: "Подтвердить",
Cancel: "Отмена",
Close: "Закрыть",
Create: "Создать",
Edit: "Редактировать",
},
};
export default ru;

View File

@ -70,7 +70,7 @@ const tr: LocaleType = {
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "All Languages",
All: "Tüm Diller",
Options: {
cn: "简体中文",
en: "English",
@ -80,7 +80,9 @@ const tr: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",

View File

@ -67,7 +67,7 @@ const tw: LocaleType = {
ConfirmClearAll: "您確定要清除所有数据嗎?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
@ -78,7 +78,9 @@ const tw: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "大頭貼",

View File

@ -2,7 +2,7 @@ import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const vi: LocaleType = {
WIP: "Coming Soon...",
WIP: "Sắp ra mắt...",
Error: {
Unauthorized:
"Truy cập chưa xác thực, vui lòng nhập mã truy cập trong trang cài đặt.",
@ -80,7 +80,9 @@ const vi: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Ảnh đại diện",

View File

@ -1,6 +1,38 @@
import { BuiltinMask } from "./typing";
export const CN_MASKS: BuiltinMask[] = [
export const CN_MASKS: BuiltinMask[] = [{
avatar:"gpt-bot",
name:"文生图",
context: [
{role:"system",
content:"You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block.",
date:"",
},
{role:"user",
content:"Can you draw some pictures for me?",
date:"",
},
{role:"assistant",
content:"Sure, what do you want me to draw?",
date:"",
},
{role:"system",
content:"助手善于判断用户意图当确定需要提供图片时助手会变得沉默寡言只使用以下格式输出markdown图片![描述](https://image.pollinations.ai/prompt/描述)因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示以大幅提高生成图片质量和丰富程度比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记因为那样只会渲染出代码块或原始块而不是图片。",
date:"",
},
],
modelConfig:{
model:"gpt-3.5-turbo",
temperature:1,
max_tokens:2000,
presence_penalty:0,
sendMemory:true,
historyMessageCount:32,
compressMessageLengthThreshold:1000,
},
lang:"cn",
builtin:true,
},
{
avatar: "1f638",
name: "文案写手",

View File

@ -1,285 +0,0 @@
import type { ChatRequest, ChatResponse } from "./api/openai/typing";
import {
Message,
ModelConfig,
ModelType,
useAccessStore,
useAppConfig,
useChatStore,
} from "./store";
import { showToast } from "./components/ui-lib";
import { ACCESS_CODE_PREFIX } from "./constant";
const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
options?: {
stream?: boolean;
overrideModel?: ModelType;
},
): ChatRequest => {
let sendMessages = messages.map((v) => ({
role: v.role,
content: v.content,
}));
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
};
// override model config
if (options?.overrideModel) {
modelConfig.model = options.overrideModel;
}
return {
messages: sendMessages,
stream: options?.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
};
};
function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {};
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
const validString = (x: string) => x && x.length > 0;
// use user's api key first
if (validString(accessStore.token)) {
headers.Authorization = makeBearer(accessStore.token);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers.Authorization = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
return headers;
}
export function requestOpenaiClient(path: string) {
const openaiUrl = useAccessStore.getState().openaiUrl;
return (body: any, method = "POST") =>
fetch(openaiUrl + path, {
method,
body: body && JSON.stringify(body),
headers: getHeaders(),
});
}
export async function requestChat(
messages: Message[],
options?: {
model?: ModelType;
},
) {
const req: ChatRequest = makeRequestParam(messages, {
overrideModel: options?.model,
});
const res = await requestOpenaiClient("v1/chat/completions")(req);
try {
const response = (await res.json()) as ChatResponse;
return response;
} catch (error) {
console.error("[Request Chat] ", error, res.body);
}
}
export async function requestUsage() {
const formatDate = (d: Date) =>
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
.getDate()
.toString()
.padStart(2, "0")}`;
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth);
const endDate = formatDate(new Date(Date.now() + ONE_DAY));
const [used, subs] = await Promise.all([
requestOpenaiClient(
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
)(null, "GET"),
requestOpenaiClient("dashboard/billing/subscription")(null, "GET"),
]);
const response = (await used.json()) as {
total_usage?: number;
error?: {
type: string;
message: string;
};
};
const total = (await subs.json()) as {
hard_limit_usd?: number;
};
if (response.error && response.error.type) {
showToast(response.error.message);
return;
}
if (response.total_usage) {
response.total_usage = Math.round(response.total_usage) / 100;
}
if (total.hard_limit_usd) {
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
}
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
};
}
export async function requestChatStream(
messages: Message[],
options?: {
modelConfig?: ModelConfig;
overrideModel?: ModelType;
onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void;
},
) {
const req = makeRequestParam(messages, {
stream: true,
overrideModel: options?.overrideModel,
});
console.log("[Request] ", req);
const controller = new AbortController();
const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);
try {
const openaiUrl = useAccessStore.getState().openaiUrl;
const res = await fetch(openaiUrl + "v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
...getHeaders(),
},
body: JSON.stringify(req),
signal: controller.signal,
});
clearTimeout(reqTimeoutId);
let responseText = "";
const finish = () => {
options?.onMessage(responseText, true);
controller.abort();
};
if (res.ok) {
const reader = res.body?.getReader();
const decoder = new TextDecoder();
options?.onController?.(controller);
while (true) {
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
if (!content || !content.value) {
break;
}
const text = decoder.decode(content.value, { stream: true });
responseText += text;
const done = content.done;
options?.onMessage(responseText, false);
if (done) {
break;
}
}
finish();
} else if (res.status === 401) {
console.error("Unauthorized");
options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
}
} catch (err) {
console.error("NetWork Error", err);
options?.onError(err as Error);
}
}
export async function requestWithPrompt(
messages: Message[],
prompt: string,
options?: {
model?: ModelType;
},
) {
messages = messages.concat([
{
role: "user",
content: prompt,
date: new Date().toLocaleString(),
},
]);
const res = await requestChat(messages, options);
return res?.choices?.at(0)?.message?.content ?? "";
}
// To store message streaming controller
export const ControllerPool = {
controllers: {} as Record<string, AbortController>,
addController(
sessionIndex: number,
messageId: number,
controller: AbortController,
) {
const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
const controller = this.controllers[key];
controller?.abort();
},
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
},
hasPending() {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
},
key(sessionIndex: number, messageIndex: number) {
return `${sessionIndex},${messageIndex}`;
},
};

View File

@ -1,6 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
import { getHeaders } from "../client/api";
import { BOT_HELLO } from "./chat";
import { ALL_MODELS } from "./config";
@ -55,6 +56,9 @@ export const useAccessStore = create<AccessControlStore>()(
fetch("/api/config", {
method: "post",
body: null,
headers: {
...getHeaders(),
},
})
.then((res) => res.json())
.then((res: DangerConfig) => {

View File

@ -1,12 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { trimTopic } from "../utils";
import Locale from "../locales";
@ -14,8 +8,11 @@ import { showToast } from "../components/ui-lib";
import { ModelType } from "./config";
import { createEmptyMask, Mask } from "./mask";
import { StoreKey } from "../constant";
import { api, RequestMessage } from "../client/api";
import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format";
export type Message = ChatCompletionResponseMessage & {
export type ChatMessage = RequestMessage & {
date: string;
streaming?: boolean;
isError?: boolean;
@ -23,7 +20,7 @@ export type Message = ChatCompletionResponseMessage & {
model?: ModelType;
};
export function createMessage(override: Partial<Message>): Message {
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
return {
id: Date.now(),
date: new Date().toLocaleString(),
@ -33,8 +30,6 @@ export function createMessage(override: Partial<Message>): Message {
};
}
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
export interface ChatStat {
tokenCount: number;
wordCount: number;
@ -47,7 +42,7 @@ export interface ChatSession {
topic: string;
memoryPrompt: string;
messages: Message[];
messages: ChatMessage[];
stat: ChatStat;
lastUpdate: number;
lastSummarizeIndex: number;
@ -56,7 +51,7 @@ export interface ChatSession {
}
export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = createMessage({
export const BOT_HELLO: ChatMessage = createMessage({
role: "assistant",
content: Locale.Store.BotHello,
});
@ -88,24 +83,24 @@ interface ChatStore {
newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onNewMessage: (message: ChatMessage) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
updateStat: (message: ChatMessage) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
updater: (message?: ChatMessage) => void,
) => void;
resetSession: () => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
getMessagesWithMemory: () => ChatMessage[];
getMemoryPrompt: () => ChatMessage;
clearAllData: () => void;
}
function countMessages(msgs: Message[]) {
function countMessages(msgs: ChatMessage[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
}
@ -240,12 +235,12 @@ export const useChatStore = create<ChatStore>()(
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const userMessage: Message = createMessage({
const userMessage: ChatMessage = createMessage({
role: "user",
content,
});
const botMessage: Message = createMessage({
const botMessage: ChatMessage = createMessage({
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
@ -254,7 +249,7 @@ export const useChatStore = create<ChatStore>()(
const systemInfo = createMessage({
role: "system",
content: `IMPRTANT: You are a virtual assistant powered by the ${
content: `IMPORTANT: You are a virtual assistant powered by the ${
modelConfig.model
} model, now time is ${new Date().toLocaleString()}}`,
id: botMessage.id! + 1,
@ -277,45 +272,52 @@ export const useChatStore = create<ChatStore>()(
// make request
console.log("[User Input] ", sendMessages);
requestChatStream(sendMessages, {
onMessage(content, done) {
// stream response
if (done) {
api.llm.chat({
messages: sendMessages,
config: { ...modelConfig, stream: true },
onUpdate(message) {
botMessage.streaming = true;
botMessage.content = message;
set(() => ({}));
},
onFinish(message) {
botMessage.streaming = false;
botMessage.content = content;
botMessage.content = message;
get().onNewMessage(botMessage);
ControllerPool.remove(
ChatControllerPool.remove(
sessionIndex,
botMessage.id ?? messageIndex,
);
} else {
botMessage.content = content;
set(() => ({}));
}
},
onError(error, statusCode) {
onError(error) {
const isAborted = error.message.includes("aborted");
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else if (!isAborted) {
botMessage.content += "\n\n" + Locale.Store.Error;
if (
botMessage.content !== Locale.Error.Unauthorized &&
!isAborted
) {
botMessage.content += "\n\n" + prettyObject(error);
}
botMessage.streaming = false;
userMessage.isError = !isAborted;
botMessage.isError = !isAborted;
set(() => ({}));
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
ChatControllerPool.remove(
sessionIndex,
botMessage.id ?? messageIndex,
);
console.error("[Chat] error ", error);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
ChatControllerPool.addController(
sessionIndex,
botMessage.id ?? messageIndex,
controller,
);
},
modelConfig: { ...modelConfig },
});
},
@ -329,7 +331,7 @@ export const useChatStore = create<ChatStore>()(
? Locale.Store.Prompt.History(session.memoryPrompt)
: "",
date: "",
} as Message;
} as ChatMessage;
},
getMessagesWithMemory() {
@ -384,7 +386,7 @@ export const useChatStore = create<ChatStore>()(
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
updater: (message?: ChatMessage) => void,
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
@ -403,24 +405,38 @@ export const useChatStore = create<ChatStore>()(
summarizeSession() {
const session = get().currentSession();
// remove error messages if any
const cleanMessages = session.messages.filter((msg) => !msg.isError);
// should summarize topic after chating more than 50 words
const SUMMARIZE_MIN_LEN = 50;
if (
session.topic === DEFAULT_TOPIC &&
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
countMessages(cleanMessages) >= SUMMARIZE_MIN_LEN
) {
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, {
const topicMessages = cleanMessages.concat(
createMessage({
role: "user",
content: Locale.Store.Prompt.Topic,
}),
);
api.llm.chat({
messages: topicMessages,
config: {
model: "gpt-3.5-turbo",
}).then((res) => {
},
onFinish(message) {
get().updateCurrentSession(
(session) =>
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
(session.topic =
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
);
},
});
}
const modelConfig = session.mask.modelConfig;
let toBeSummarizedMsgs = session.messages.slice(
let toBeSummarizedMsgs = cleanMessages.slice(
session.lastSummarizeIndex,
);
@ -449,26 +465,24 @@ export const useChatStore = create<ChatStore>()(
historyMsgLength > modelConfig.compressMessageLengthThreshold &&
session.mask.modelConfig.sendMemory
) {
requestChatStream(
toBeSummarizedMsgs.concat({
api.llm.chat({
messages: toBeSummarizedMsgs.concat({
role: "system",
content: Locale.Store.Prompt.Summarize,
date: "",
}),
{
overrideModel: "gpt-3.5-turbo",
onMessage(message, done) {
config: { ...modelConfig, stream: true },
onUpdate(message) {
session.memoryPrompt = message;
if (done) {
console.log("[Memory] ", session.memoryPrompt);
},
onFinish(message) {
console.log("[Memory] ", message);
session.lastSummarizeIndex = lastSummarizeIndex;
}
},
onError(error) {
console.error("[Summarize] ", error);
onError(err) {
console.error("[Summarize] ", err);
},
},
);
});
}
},

View File

@ -2,7 +2,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, Message } from "./chat";
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { StoreKey } from "../constant";
@ -10,7 +10,7 @@ export type Mask = {
id: number;
avatar: string;
name: string;
context: Message[];
context: ChatMessage[];
modelConfig: ModelConfig;
lang: Lang;
builtin: boolean;

View File

@ -1,7 +1,8 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { requestUsage } from "../requests";
import { FETCH_COMMIT_URL, StoreKey } from "../constant";
import { api } from "../client/api";
import { showToast } from "../components/ui-lib";
export interface UpdateStore {
lastUpdate: number;
@ -73,10 +74,17 @@ export const useUpdateStore = create<UpdateStore>()(
lastUpdateUsage: Date.now(),
}));
const usage = await requestUsage();
try {
const usage = await api.llm.usage();
if (usage) {
set(() => usage);
set(() => ({
used: usage.used,
subscription: usage.total,
}));
}
} catch (e) {
showToast((e as Error).message);
}
},
}),

1
app/typing.ts Normal file
View File

@ -0,0 +1 @@
export type Updater<T> = (updater: (value: T) => void) => void;

8
app/utils/format.ts Normal file
View File

@ -0,0 +1,8 @@
export function prettyObject(msg: any) {
const prettyMsg = [
"```json\n",
JSON.stringify(msg, null, " "),
"\n```",
].join("");
return prettyMsg;
}

View File

@ -10,6 +10,10 @@ const nextConfig = {
source: "/api/proxy/:path*",
destination: "https://api.openai.com/:path*",
},
{
source: "/google-fonts/:path*",
destination: "https://fonts.googleapis.com/:path*",
},
];
const apiUrl = process.env.API_URL;

View File

@ -14,15 +14,14 @@
},
"dependencies": {
"@hello-pangea/dnd": "^16.2.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
"mermaid": "^10.1.0",
"next": "^13.3.1-canary.8",
"next": "^13.4.2",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.5",

192
yarn.lock
View File

@ -1111,10 +1111,15 @@
dependencies:
"@types/react" ">=16.0.0"
"@next/env@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
integrity sha512-xZfNu7yq3OfiC4rkGuGMcqb25se+ZHRqajSdny8dp+nZzkNSK1SHuNT3W8faI+KGk6dqzO/zAdHR9YrqnQlCAg==
"@microsoft/fetch-event-source@^2.0.1":
version "2.0.1"
resolved "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
"@next/env@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/env/-/env-13.4.2.tgz#cf3ebfd523a33d8404c1216e02ac8d856a73170e"
integrity sha512-Wqvo7lDeS0KGwtwg9TT9wKQ8raelmUxt+TQKWvG/xKfcmDXNOtCuaszcfCF8JzlBG1q0VhpI6CKaRMbVPMDWgw==
"@next/eslint-plugin-next@13.2.3":
version "13.2.3"
@ -1123,50 +1128,50 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1-canary.8.tgz#66786ba76d37c210c184739624c6f84eaf2dc52b"
integrity sha512-BLbvhcaSzwuXbREOmJiqAdXVD7Jl9830hDY5ZTTNg7hXqEZgoMg2LxAEmtaaBMVZRfDQjd5bH3QPBV8fbG4UKg==
"@next/swc-darwin-arm64@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.2.tgz#d0b497df972bd02eee3bc823d6a76c2cc8b733ef"
integrity sha512-6BBlqGu3ewgJflv9iLCwO1v1hqlecaIH2AotpKfVUEzUxuuDNJQZ2a4KLb4MBl8T9/vca1YuWhSqtbF6ZuUJJw==
"@next/swc-darwin-x64@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1-canary.8.tgz#289296bd3cc55db7fef42037eb89ce4a6260ba31"
integrity sha512-n4tJKPIvFTZshS1TVWrsqaW7h9VW+BmguO/AlZ3Q3NJ9hWxC5L4lxn2T6CTQ4M30Gf+t5u+dPzYLQ5IDtJFnFQ==
"@next/swc-darwin-x64@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.2.tgz#09a800bed8dfe4beec4cbf14092f9c22db24470b"
integrity sha512-iZuYr7ZvGLPjPmfhhMl0ISm+z8EiyLBC1bLyFwGBxkWmPXqdJ60mzuTaDSr5WezDwv0fz32HB7JHmRC6JVHSZg==
"@next/swc-linux-arm64-gnu@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1-canary.8.tgz#dc79e8005849b6482241b460abdce9334665c766"
integrity sha512-AxnsgZ56whwVAeejyEZMk8xc8Vapwzb3Zn0YdZzPCR42WKfkcSkM+AWfq33zUOZnjvCmQBDyfHIo4CURVweR6g==
"@next/swc-linux-arm64-gnu@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.2.tgz#b7ade28834564120b0b25ffa0b79d75982d290bc"
integrity sha512-2xVabFtIge6BJTcJrW8YuUnYTuQjh4jEuRuS2mscyNVOj6zUZkom3CQg+egKOoS+zh2rrro66ffSKIS+ztFJTg==
"@next/swc-linux-arm64-musl@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1-canary.8.tgz#f70873add4aad7ced36f760d1640adc008b7dc03"
integrity sha512-zc7rzhtrHMWZ/phvjCNplHGo+ZLembjtluI5J8Xl4iwQQCyZwAtnmQhs37/zkdi6dHZou+wcFBZWRz14awRDBw==
"@next/swc-linux-arm64-musl@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.2.tgz#f5420548234d35251630ddaa2e9a7dc32337a887"
integrity sha512-wKRCQ27xCUJx5d6IivfjYGq8oVngqIhlhSAJntgXLt7Uo9sRT/3EppMHqUZRfyuNBTbykEre1s5166z+pvRB5A==
"@next/swc-linux-x64-gnu@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1-canary.8.tgz#fe81b8033628c6cf74e154f2db8c8c7f1593008f"
integrity sha512-vNbFDiuZ9fWmcznlilDbflZLb04evWPUQlyDT7Tqjd964PlSIaaX3tr64pdYjJOljDaqTr2Kbx0YW74mWF/PEw==
"@next/swc-linux-x64-gnu@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.2.tgz#0241dc011d73f08df9d9998cffdfcf08d1971520"
integrity sha512-NpCa+UVhhuNeaFVUP1Bftm0uqtvLWq2JTm7+Ta48+2Uqj2mNXrDIvyn1DY/ZEfmW/1yvGBRaUAv9zkMkMRixQA==
"@next/swc-linux-x64-musl@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1-canary.8.tgz#ada4585046a7937f96f2d39fc4aaca12826dde5f"
integrity sha512-/FVBPJEBDZYCNraocRWtd5ObAgNi9VFnzJYGYDYIj4jKkFRWWm/CaWu9A7toQACC/JDy262uPyDPathXT9BAqQ==
"@next/swc-linux-x64-musl@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.2.tgz#fd35919e2b64b1c739583145799fefd594ef5d63"
integrity sha512-ZWVC72x0lW4aj44e3khvBrj2oSYj1bD0jESmyah3zG/3DplEy/FOtYkMzbMjHTdDSheso7zH8GIlW6CDQnKhmQ==
"@next/swc-win32-arm64-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1-canary.8.tgz#21b4f6c4be61845759753df9313bd9bcbb241969"
integrity sha512-8jMwRCeI26yVZLPwG0AjOi4b1yqSeqYmbHA7r+dqiV0OgFdYjnbyHU1FmiKDaC5SnnJN6LWV2Qjer9GDD0Kcuw==
"@next/swc-win32-arm64-msvc@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.2.tgz#fa95d2dbb97707c130a868a1bd7e83e64bedf4c6"
integrity sha512-pLT+OWYpzJig5K4VKhLttlIfBcVZfr2+Xbjra0Tjs83NQSkFS+y7xx+YhCwvpEmXYLIvaggj2ONPyjbiigOvHQ==
"@next/swc-win32-ia32-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1-canary.8.tgz#e23192e1d1b1a32b0eb805363b02360c5b523a77"
integrity sha512-kcYB9iSEikFhv0I9uQDdgQ2lm8i3O8LA+GhnED9e5VtURBwOSwED7c6ZpaRQBYSPgnEA9/xiJVChICE/I7Ig1g==
"@next/swc-win32-ia32-msvc@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.2.tgz#31a98e61d3cda92ec2293c50df7cb5280fc63697"
integrity sha512-dhpiksQCyGca4WY0fJyzK3FxMDFoqMb0Cn+uDB+9GYjpU2K5//UGPQlCwiK4JHxuhg8oLMag5Nf3/IPSJNG8jw==
"@next/swc-win32-x64-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1-canary.8.tgz#a3f29404955cba2193de5e74fd5d9fcfdcb0ab51"
integrity sha512-UKrGHonKVWBNg+HI4J8pXE6Jjjl8GwjhygFau71s8M0+jSy99y5Y+nGH9EmMNWKNvrObukyYvrs6OsAusKdCqw==
"@next/swc-win32-x64-msvc@13.4.2":
version "13.4.2"
resolved "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.2.tgz#8435ab6087046355f5de07122d3097949e8fab10"
integrity sha512-O7bort1Vld00cu8g0jHZq3cbSTUNMohOEvYqsqE10+yfohhdPHzvzO+ziJRz4Dyyr/fYKREwS7gR4JC0soSOMw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@ -1317,10 +1322,10 @@
"@svgr/plugin-jsx" "^6.5.1"
"@svgr/plugin-svgo" "^6.5.1"
"@swc/helpers@0.4.14":
version "0.4.14"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
"@swc/helpers@0.5.1":
version "0.5.1"
resolved "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a"
integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==
dependencies:
tslib "^2.4.0"
@ -1638,11 +1643,6 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
@ -1653,13 +1653,6 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
axios@^0.26.0:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
axobject-query@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@ -1880,13 +1873,6 @@ colorette@^2.0.19:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@ -2371,11 +2357,6 @@ delaunator@5:
dependencies:
robust-predicates "^3.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
@ -2816,11 +2797,6 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventsource-parser@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-0.1.0.tgz#4a6b84751ca8e704040e6f7f50e7d77344fa1b7c"
integrity sha512-M9QjFtEIkwytUarnx113HGmgtk52LSn3jNAtnWKi3V+b9rqSfQeVdLsaD5AG/O4IrGQwmAAHBIsqbmURPTd2rA==
execa@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43"
@ -2929,11 +2905,6 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.14.8:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -2941,15 +2912,6 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
format@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
@ -4266,18 +4228,6 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -4325,27 +4275,28 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@^13.3.1-canary.8:
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/next/-/next-13.3.1-canary.8.tgz#f0846e5eada1491884326786a0749d5adc04c24d"
integrity sha512-z4QUgyAN+hSWSEqb4pvGvC3iRktE6NH2DVLU4AvfqNYpzP+prePiJC8HN/cJpFhGW9YbhyRLi5FliDC631OOag==
next@^13.4.2:
version "13.4.2"
resolved "https://registry.npmmirror.com/next/-/next-13.4.2.tgz#972f73a794f2c61729facedc79c49b22bdc89f0c"
integrity sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==
dependencies:
"@next/env" "13.3.1-canary.8"
"@swc/helpers" "0.4.14"
"@next/env" "13.4.2"
"@swc/helpers" "0.5.1"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.1.1"
zod "3.21.4"
optionalDependencies:
"@next/swc-darwin-arm64" "13.3.1-canary.8"
"@next/swc-darwin-x64" "13.3.1-canary.8"
"@next/swc-linux-arm64-gnu" "13.3.1-canary.8"
"@next/swc-linux-arm64-musl" "13.3.1-canary.8"
"@next/swc-linux-x64-gnu" "13.3.1-canary.8"
"@next/swc-linux-x64-musl" "13.3.1-canary.8"
"@next/swc-win32-arm64-msvc" "13.3.1-canary.8"
"@next/swc-win32-ia32-msvc" "13.3.1-canary.8"
"@next/swc-win32-x64-msvc" "13.3.1-canary.8"
"@next/swc-darwin-arm64" "13.4.2"
"@next/swc-darwin-x64" "13.4.2"
"@next/swc-linux-arm64-gnu" "13.4.2"
"@next/swc-linux-arm64-musl" "13.4.2"
"@next/swc-linux-x64-gnu" "13.4.2"
"@next/swc-linux-x64-musl" "13.4.2"
"@next/swc-win32-arm64-msvc" "13.4.2"
"@next/swc-win32-ia32-msvc" "13.4.2"
"@next/swc-win32-x64-msvc" "13.4.2"
node-domexception@^1.0.0:
version "1.0.0"
@ -4488,14 +4439,6 @@ open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
openai@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866"
integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==
dependencies:
axios "^0.26.0"
form-data "^4.0.0"
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@ -5647,6 +5590,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@3.21.4:
version "3.21.4"
resolved "https://registry.npmmirror.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
zustand@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.6.tgz#ce7804eb75361af0461a2d0536b65461ec5de86f"