Merge branch 'Yidadaa:main' into main
This commit is contained in:
commit
5bc94d401d
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals"
|
"extends": "next/core-web-vitals",
|
||||||
|
"plugins": ["prettier"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,3 +35,5 @@ yarn-error.log*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
dev
|
dev
|
||||||
|
|
||||||
|
public/prompts.json
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,json,html,css,scss,md}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 80,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: 'all',
|
||||||
|
bracketSpacing: true,
|
||||||
|
arrowParens: 'always',
|
||||||
|
};
|
|
@ -22,6 +22,7 @@ One-Click to deploy your own ChatGPT web UI.
|
||||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||||
- 精心设计的 UI,响应式设计,支持深色模式
|
- 精心设计的 UI,响应式设计,支持深色模式
|
||||||
- 极快的首屏加载速度(~85kb)
|
- 极快的首屏加载速度(~85kb)
|
||||||
|
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||||
- 一键导出聊天记录,完整的 Markdown 支持
|
- 一键导出聊天记录,完整的 Markdown 支持
|
||||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||||
|
@ -31,6 +32,7 @@ One-Click to deploy your own ChatGPT web UI.
|
||||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||||
- Responsive design, and dark mode
|
- Responsive design, and dark mode
|
||||||
- Fast first screen loading speed (~85kb)
|
- Fast first screen loading speed (~85kb)
|
||||||
|
- 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
|
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||||
- One-click export all chat history with full Markdown support
|
- One-click export all chat history with full Markdown support
|
||||||
|
|
||||||
|
@ -152,13 +154,10 @@ If you would like to contribute your API key, you can email it to the author and
|
||||||
[@mushan0x0](https://github.com/mushan0x0)
|
[@mushan0x0](https://github.com/mushan0x0)
|
||||||
[@ClarenceDan](https://github.com/ClarenceDan)
|
[@ClarenceDan](https://github.com/ClarenceDan)
|
||||||
[@zhangjia](https://github.com/zhangjia)
|
[@zhangjia](https://github.com/zhangjia)
|
||||||
|
[@hoochanlon](https://github.com/hoochanlon)
|
||||||
|
|
||||||
### 贡献者 Contributor
|
### 贡献者 Contributor
|
||||||
|
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||||
[@AprilNEA](https://github.com/AprilNEA)
|
|
||||||
[@iSource](https://github.com/iSource)
|
|
||||||
[@iFwu](https://github.com/iFwu)
|
|
||||||
[@xiaotianxt](https://github.com/xiaotianxt)
|
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,4 @@ export function getAccessCodes(): Set<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACCESS_CODES = getAccessCodes();
|
export const ACCESS_CODES = getAccessCodes();
|
||||||
|
export const IS_IN_DOCKER = process.env.DOCKER;
|
||||||
|
|
|
@ -333,11 +333,65 @@
|
||||||
|
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin single-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-hints {
|
||||||
|
min-height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
|
||||||
|
background-color: var(--white);
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
|
||||||
|
.prompt-hint {
|
||||||
|
color: var(--black);
|
||||||
|
padding: 6px 10px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
border: transparent 1px solid;
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
|
@include single-line();
|
||||||
|
}
|
||||||
|
.hint-content {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
@include single-line();
|
||||||
|
}
|
||||||
|
|
||||||
|
&-selected,
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-panel-inner {
|
.chat-input-panel-inner {
|
||||||
|
@ -375,7 +429,7 @@
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
bottom: 10px;
|
bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-content {
|
.export-content {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
@ -28,6 +29,7 @@ import Locale from "../locales";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { REPO_URL } from "../constant";
|
import { REPO_URL } from "../constant";
|
||||||
import { ControllerPool } from "../requests";
|
import { ControllerPool } from "../requests";
|
||||||
|
import { Prompt, usePromptStore } from "../store/prompt";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
|
@ -146,24 +148,77 @@ function useSubmitHandler() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PromptHints(props: {
|
||||||
|
prompts: Prompt[];
|
||||||
|
onPromptSelect: (prompt: Prompt) => void;
|
||||||
|
}) {
|
||||||
|
if (props.prompts.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["prompt-hints"]}>
|
||||||
|
{props.prompts.map((prompt, i) => (
|
||||||
|
<div
|
||||||
|
className={styles["prompt-hint"]}
|
||||||
|
key={prompt.title + i.toString()}
|
||||||
|
onClick={() => props.onPromptSelect(prompt)}
|
||||||
|
>
|
||||||
|
<div className={styles["hint-title"]}>{prompt.title}</div>
|
||||||
|
<div className={styles["hint-content"]}>{prompt.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Chat(props: { showSideBar?: () => void }) {
|
export function Chat(props: { showSideBar?: () => void }) {
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
const [session, sessionIndex] = useChatStore((state) => [
|
const [session, sessionIndex] = useChatStore((state) => [
|
||||||
state.currentSession(),
|
state.currentSession(),
|
||||||
state.currentSessionIndex,
|
state.currentSessionIndex,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
|
|
||||||
const onUserInput = useChatStore((state) => state.onUserInput);
|
// prompt hints
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||||
|
const onSearch = useDebouncedCallback(
|
||||||
|
(text: string) => {
|
||||||
|
if (chatStore.config.disablePromptHint) return;
|
||||||
|
setPromptHints(promptStore.search(text));
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
{ leading: true, trailing: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPromptSelect = (prompt: Prompt) => {
|
||||||
|
setUserInput(prompt.content);
|
||||||
|
setPromptHints([]);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// only search prompts when user input is short
|
||||||
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
|
const onInput = (text: string) => {
|
||||||
|
setUserInput(text);
|
||||||
|
const n = text.trim().length;
|
||||||
|
if (n === 0 || n > SEARCH_TEXT_LIMIT) {
|
||||||
|
setPromptHints([]);
|
||||||
|
} else {
|
||||||
|
onSearch(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// submit user input
|
// submit user input
|
||||||
const onUserSubmit = () => {
|
const onUserSubmit = () => {
|
||||||
if (userInput.length <= 0) return;
|
if (userInput.length <= 0) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
onUserInput(userInput).then(() => setIsLoading(false));
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
@ -198,7 +253,9 @@ export function Chat(props: { showSideBar?: () => void }) {
|
||||||
for (let i = botIndex; i >= 0; i -= 1) {
|
for (let i = botIndex; i >= 0; i -= 1) {
|
||||||
if (messages[i].role === "user") {
|
if (messages[i].role === "user") {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
onUserInput(messages[i].content).then(() => setIsLoading(false));
|
chatStore
|
||||||
|
.onUserInput(messages[i].content)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,7 +263,6 @@ export function Chat(props: { showSideBar?: () => void }) {
|
||||||
|
|
||||||
// for auto-scroll
|
// for auto-scroll
|
||||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
// wont scroll while hovering messages
|
// wont scroll while hovering messages
|
||||||
const [autoScroll, setAutoScroll] = useState(false);
|
const [autoScroll, setAutoScroll] = useState(false);
|
||||||
|
@ -373,17 +429,21 @@ export function Chat(props: { showSideBar?: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles["chat-input-panel"]}>
|
<div className={styles["chat-input-panel"]}>
|
||||||
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
<div className={styles["chat-input-panel-inner"]}>
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={styles["chat-input"]}
|
className={styles["chat-input"]}
|
||||||
placeholder={Locale.Chat.Input(submitKey)}
|
placeholder={Locale.Chat.Input(submitKey)}
|
||||||
rows={3}
|
rows={4}
|
||||||
onInput={(e) => setUserInput(e.currentTarget.value)}
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onKeyDown={(e) => onInputKeyDown(e as any)}
|
onKeyDown={(e) => onInputKeyDown(e as any)}
|
||||||
onFocus={() => setAutoScroll(true)}
|
onFocus={() => setAutoScroll(true)}
|
||||||
onBlur={() => setAutoScroll(false)}
|
onBlur={() => {
|
||||||
|
setAutoScroll(false);
|
||||||
|
setTimeout(() => setPromptHints([]), 100);
|
||||||
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -411,9 +471,11 @@ function useSwitchTheme() {
|
||||||
document.body.classList.add("light");
|
document.body.classList.add("light");
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeColor = getComputedStyle(document.body).getPropertyValue("--theme-color").trim();
|
const themeColor = getComputedStyle(document.body)
|
||||||
|
.getPropertyValue("--theme-color")
|
||||||
|
.trim();
|
||||||
const metaDescription = document.querySelector('meta[name="theme-color"]');
|
const metaDescription = document.querySelector('meta[name="theme-color"]');
|
||||||
metaDescription?.setAttribute('content', themeColor);
|
metaDescription?.setAttribute("content", themeColor);
|
||||||
}, [config.theme]);
|
}, [config.theme]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ import styles from "./settings.module.scss";
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import ClearIcon from "../icons/clear.svg";
|
import ClearIcon from "../icons/clear.svg";
|
||||||
|
import EditIcon from "../icons/edit.svg";
|
||||||
|
|
||||||
import { List, ListItem, Popover } from "./ui-lib";
|
import { List, ListItem, Popover, showToast } from "./ui-lib";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import {
|
import {
|
||||||
|
@ -19,12 +20,13 @@ import {
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { Avatar } from "./home";
|
import { Avatar, PromptHints } from "./home";
|
||||||
|
|
||||||
import Locale, { changeLang, getLang } from "../locales";
|
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||||
import { getCurrentCommitId } from "../utils";
|
import { getCurrentCommitId } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UPDATE_URL } from "../constant";
|
import { UPDATE_URL } from "../constant";
|
||||||
|
import { SearchService, usePromptStore } from "../store/prompt";
|
||||||
|
|
||||||
function SettingItem(props: {
|
function SettingItem(props: {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -78,6 +80,10 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const builtinCount = SearchService.count.builtin;
|
||||||
|
const customCount = promptStore.prompts.size ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles["window-header"]}>
|
<div className={styles["window-header"]}>
|
||||||
|
@ -206,22 +212,18 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem title={Locale.Settings.Lang.Name}>
|
<SettingItem title={Locale.Settings.Lang.Name}>
|
||||||
<div className="">
|
|
||||||
<select
|
<select
|
||||||
value={getLang()}
|
value={getLang()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
changeLang(e.target.value as any);
|
changeLang(e.target.value as any);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="en" key="en">
|
{AllLangs.map((lang) => (
|
||||||
{Locale.Settings.Lang.Options.en}
|
<option value={lang} key={lang}>
|
||||||
</option>
|
{Locale.Settings.Lang.Options[lang]}
|
||||||
|
|
||||||
<option value="cn" key="cn">
|
|
||||||
{Locale.Settings.Lang.Options.cn}
|
|
||||||
</option>
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<div className="no-mobile">
|
<div className="no-mobile">
|
||||||
|
@ -238,6 +240,37 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</div>
|
</div>
|
||||||
</List>
|
</List>
|
||||||
|
<List>
|
||||||
|
<SettingItem
|
||||||
|
title={Locale.Settings.Prompt.Disable.Title}
|
||||||
|
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.disablePromptHint}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.disablePromptHint = e.currentTarget.checked)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
title={Locale.Settings.Prompt.List}
|
||||||
|
subTitle={Locale.Settings.Prompt.ListCount(
|
||||||
|
builtinCount,
|
||||||
|
customCount
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon={<EditIcon />}
|
||||||
|
text={Locale.Settings.Prompt.Edit}
|
||||||
|
onClick={() => showToast(Locale.WIP)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</List>
|
||||||
<List>
|
<List>
|
||||||
{enabledAccessControl ? (
|
{enabledAccessControl ? (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function ListItem(props: { children: JSX.Element[] }) {
|
||||||
return <div className={styles["list-item"]}>{props.children}</div>;
|
return <div className={styles["list-item"]}>{props.children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function List(props: { children: JSX.Element[] }) {
|
export function List(props: { children: JSX.Element[] | JSX.Element }) {
|
||||||
return <div className={styles.list}>{props.children}</div>;
|
return <div className={styles.list}>{props.children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(0 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(10.5 11) rotate(0 1.4166666666666665 1.8333333333333333)" d="M2.83,0L2.83,3C2.83,3.37 2.53,3.67 2.17,3.67L0,3.67 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2.6666666666666665 1.3333333333333333) rotate(0 5.333333333333333 6.666666666666666)" d="M10.67,4L10.67,0.67C10.67,0.3 10.37,0 10,0L0.67,0C0.3,0 0,0.3 0,0.67L0,12.67C0,13.03 0.3,13.33 0.67,13.33L2.67,13.33 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 5.333333333333333) rotate(0 2.333333333333333 0)" d="M0,0L4.67,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7.666666666666666 7.666666666666666) rotate(0 2.833333333333333 3.5)" d="M0,7L5.67,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 8) rotate(0 1.3333333333333333 0)" d="M0,0L2.67,0 " /></g></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -3,7 +3,7 @@ import "./styles/globals.scss";
|
||||||
import "./styles/markdown.scss";
|
import "./styles/markdown.scss";
|
||||||
import "./styles/prism.scss";
|
import "./styles/prism.scss";
|
||||||
import process from "child_process";
|
import process from "child_process";
|
||||||
import { ACCESS_CODES } from "./api/access";
|
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
||||||
|
|
||||||
let COMMIT_ID: string | undefined;
|
let COMMIT_ID: string | undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -28,7 +28,7 @@ export const metadata = {
|
||||||
function Meta() {
|
function Meta() {
|
||||||
const metas = {
|
const metas = {
|
||||||
version: COMMIT_ID ?? "unknown",
|
version: COMMIT_ID ?? "unknown",
|
||||||
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
|
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -45,8 +45,9 @@ const cn = {
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "Language",
|
Name: "Language",
|
||||||
Options: {
|
Options: {
|
||||||
cn: "中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "头像",
|
Avatar: "头像",
|
||||||
|
@ -61,6 +62,16 @@ const cn = {
|
||||||
SendKey: "发送键",
|
SendKey: "发送键",
|
||||||
Theme: "主题",
|
Theme: "主题",
|
||||||
TightBorder: "紧凑边框",
|
TightBorder: "紧凑边框",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "禁用提示词自动补全",
|
||||||
|
SubTitle: "禁用后将无法自动根据输入补全",
|
||||||
|
},
|
||||||
|
List: "自定义提示词列表",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`内置 ${builtin} 条,用户定义 ${custom} 条`,
|
||||||
|
Edit: "编辑",
|
||||||
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "附带历史消息数",
|
Title: "附带历史消息数",
|
||||||
SubTitle: "每次请求携带的历史消息数",
|
SubTitle: "每次请求携带的历史消息数",
|
||||||
|
|
|
@ -49,8 +49,9 @@ const en: LocaleType = {
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "语言",
|
Name: "语言",
|
||||||
Options: {
|
Options: {
|
||||||
cn: "中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
|
@ -65,6 +66,16 @@ const en: LocaleType = {
|
||||||
SendKey: "Send Key",
|
SendKey: "Send Key",
|
||||||
Theme: "Theme",
|
Theme: "Theme",
|
||||||
TightBorder: "Tight Border",
|
TightBorder: "Tight Border",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Disable auto-completion",
|
||||||
|
SubTitle: "After disabling, auto-completion will not be available",
|
||||||
|
},
|
||||||
|
List: "Prompt List",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} built-in, ${custom} user-defined`,
|
||||||
|
Edit: "Edit",
|
||||||
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "Attached Messages Count",
|
Title: "Attached Messages Count",
|
||||||
SubTitle: "Number of sent messages attached per request",
|
SubTitle: "Number of sent messages attached per request",
|
||||||
|
|
|
@ -1,53 +1,57 @@
|
||||||
import CN from './cn'
|
import CN from "./cn";
|
||||||
import EN from './en'
|
import EN from "./en";
|
||||||
|
import TW from "./tw";
|
||||||
|
|
||||||
export type { LocaleType } from './cn'
|
export type { LocaleType } from "./cn";
|
||||||
|
|
||||||
type Lang = 'en' | 'cn'
|
export const AllLangs = ["en", "cn", "tw"] as const;
|
||||||
|
type Lang = (typeof AllLangs)[number];
|
||||||
|
|
||||||
const LANG_KEY = 'lang'
|
const LANG_KEY = "lang";
|
||||||
|
|
||||||
function getItem(key: string) {
|
function getItem(key: string) {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(key)
|
return localStorage.getItem(key);
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setItem(key: string, value: string) {
|
function setItem(key: string, value: string) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, value)
|
localStorage.setItem(key, value);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLanguage() {
|
function getLanguage() {
|
||||||
try {
|
try {
|
||||||
return navigator.language.toLowerCase()
|
return navigator.language.toLowerCase();
|
||||||
} catch {
|
} catch {
|
||||||
return 'cn'
|
return "cn";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLang(): Lang {
|
export function getLang(): Lang {
|
||||||
const savedLang = getItem(LANG_KEY)
|
const savedLang = getItem(LANG_KEY);
|
||||||
|
|
||||||
if (['en', 'cn'].includes(savedLang ?? '')) {
|
if (AllLangs.includes((savedLang ?? "") as Lang)) {
|
||||||
return savedLang as Lang
|
return savedLang as Lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = getLanguage()
|
const lang = getLanguage();
|
||||||
|
|
||||||
if (lang.includes('zh') || lang.includes('cn')) {
|
if (lang.includes("zh") || lang.includes("cn")) {
|
||||||
return 'cn'
|
return "cn";
|
||||||
|
} else if (lang.includes("tw")) {
|
||||||
|
return "tw";
|
||||||
} else {
|
} else {
|
||||||
return 'en'
|
return "en";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeLang(lang: Lang) {
|
export function changeLang(lang: Lang) {
|
||||||
setItem(LANG_KEY, lang)
|
setItem(LANG_KEY, lang);
|
||||||
location.reload()
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { en: EN, cn: CN }[getLang()]
|
export default { en: EN, cn: CN, tw: TW }[getLang()];
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type { LocaleType } from "./index";
|
||||||
|
|
||||||
|
const tw: LocaleType = {
|
||||||
|
WIP: "該功能仍在開發中……",
|
||||||
|
Error: {
|
||||||
|
Unauthorized: "目前您的狀態是未授權,請前往設定頁面填寫授權碼。",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} 條對話`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: "查看消息列表",
|
||||||
|
CompressedHistory: "查看壓縮後的歷史 Prompt",
|
||||||
|
Export: "匯出聊天紀錄",
|
||||||
|
Copy: "複製",
|
||||||
|
Stop: "停止",
|
||||||
|
Retry: "重試",
|
||||||
|
},
|
||||||
|
Typing: "正在輸入…",
|
||||||
|
Input: (submitKey: string) => `輸入訊息後,按下 ${submitKey} 鍵即可發送`,
|
||||||
|
Send: "發送",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "匯出聊天記錄為 Markdown",
|
||||||
|
Copy: "複製全部",
|
||||||
|
Download: "下載檔案",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "上下文記憶 Prompt",
|
||||||
|
EmptyContent: "尚未記憶",
|
||||||
|
Copy: "複製全部",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "新的對話",
|
||||||
|
DeleteChat: "確定要刪除選取的對話嗎?",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "設定",
|
||||||
|
SubTitle: "設定選項",
|
||||||
|
Actions: {
|
||||||
|
ClearAll: "清除所有數據",
|
||||||
|
ResetAll: "重置所有設定",
|
||||||
|
Close: "關閉",
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "語言",
|
||||||
|
Options: {
|
||||||
|
cn: "简体中文",
|
||||||
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: "大頭貼",
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `當前版本:${x}`,
|
||||||
|
IsLatest: "已是最新版本",
|
||||||
|
CheckUpdate: "檢查更新",
|
||||||
|
IsChecking: "正在檢查更新...",
|
||||||
|
FoundUpdate: (x: string) => `發現新版本:${x}`,
|
||||||
|
GoToUpdate: "前往更新",
|
||||||
|
},
|
||||||
|
SendKey: "發送鍵",
|
||||||
|
Theme: "主題",
|
||||||
|
TightBorder: "緊湊邊框",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "停用提示詞自動補全",
|
||||||
|
SubTitle: "若停用後,將無法自動根據輸入進行補全",
|
||||||
|
},
|
||||||
|
List: "自定義提示詞列表",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
|
||||||
|
Edit: "編輯",
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "附帶歷史訊息數",
|
||||||
|
SubTitle: "每次請求附帶的歷史訊息數",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "歷史訊息長度壓縮閾值",
|
||||||
|
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
|
||||||
|
},
|
||||||
|
Token: {
|
||||||
|
Title: "API Key",
|
||||||
|
SubTitle: "使用自己的 Key 可規避受控訪問限制",
|
||||||
|
Placeholder: "OpenAI API Key",
|
||||||
|
},
|
||||||
|
AccessCode: {
|
||||||
|
Title: "訪問碼",
|
||||||
|
SubTitle: "現在是受控訪問狀態",
|
||||||
|
Placeholder: "請輸入訪問碼",
|
||||||
|
},
|
||||||
|
Model: "模型 (model)",
|
||||||
|
Temperature: {
|
||||||
|
Title: "隨機性 (temperature)",
|
||||||
|
SubTitle: "值越大,回復越隨機",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "單次回復限制 (max_tokens)",
|
||||||
|
SubTitle: "單次交互所用的最大 Token 數",
|
||||||
|
},
|
||||||
|
PresencePenlty: {
|
||||||
|
Title: "話題新穎度 (presence_penalty)",
|
||||||
|
SubTitle: "值越大,越有可能擴展到新話題",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "新的對話",
|
||||||
|
BotHello: "請問需要我的協助嗎?",
|
||||||
|
Error: "出錯了,請稍後再嘗試",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
||||||
|
Topic:
|
||||||
|
"直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
||||||
|
Summarize:
|
||||||
|
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內",
|
||||||
|
},
|
||||||
|
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "已複製到剪貼簿中",
|
||||||
|
Failed: "複製失敗,請賦予剪貼簿權限",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tw;
|
|
@ -40,6 +40,8 @@ export interface ChatConfig {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
tightBorder: boolean;
|
tightBorder: boolean;
|
||||||
|
|
||||||
|
disablePromptHint: boolean;
|
||||||
|
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: string;
|
model: string;
|
||||||
temperature: number;
|
temperature: number;
|
||||||
|
@ -124,6 +126,8 @@ const DEFAULT_CONFIG: ChatConfig = {
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: false,
|
tightBorder: false,
|
||||||
|
|
||||||
|
disablePromptHint: false,
|
||||||
|
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
temperature: 1,
|
temperature: 1,
|
||||||
|
@ -190,7 +194,7 @@ interface ChatStore {
|
||||||
updateMessage: (
|
updateMessage: (
|
||||||
sessionIndex: number,
|
sessionIndex: number,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
updater: (message?: Message) => void
|
updater: (message?: Message) => void,
|
||||||
) => void;
|
) => void;
|
||||||
getMessagesWithMemory: () => Message[];
|
getMessagesWithMemory: () => Message[];
|
||||||
getMemoryPrompt: () => Message;
|
getMemoryPrompt: () => Message;
|
||||||
|
@ -338,7 +342,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
ControllerPool.addController(
|
ControllerPool.addController(
|
||||||
sessionIndex,
|
sessionIndex,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
controller
|
controller,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
filterBot: !get().config.sendBotMessages,
|
filterBot: !get().config.sendBotMessages,
|
||||||
|
@ -361,7 +365,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
const config = get().config;
|
const config = get().config;
|
||||||
const n = session.messages.length;
|
const n = session.messages.length;
|
||||||
const recentMessages = session.messages.slice(
|
const recentMessages = session.messages.slice(
|
||||||
n - config.historyMessageCount
|
n - config.historyMessageCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoryPrompt = get().getMemoryPrompt();
|
const memoryPrompt = get().getMemoryPrompt();
|
||||||
|
@ -376,7 +380,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
updateMessage(
|
updateMessage(
|
||||||
sessionIndex: number,
|
sessionIndex: number,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
updater: (message?: Message) => void
|
updater: (message?: Message) => void,
|
||||||
) {
|
) {
|
||||||
const sessions = get().sessions;
|
const sessions = get().sessions;
|
||||||
const session = sessions.at(sessionIndex);
|
const session = sessions.at(sessionIndex);
|
||||||
|
@ -393,24 +397,24 @@ export const useChatStore = create<ChatStore>()(
|
||||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
get().updateCurrentSession(
|
get().updateCurrentSession(
|
||||||
(session) => (session.topic = trimTopic(res))
|
(session) => (session.topic = trimTopic(res)),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = get().config;
|
const config = get().config;
|
||||||
let toBeSummarizedMsgs = session.messages.slice(
|
let toBeSummarizedMsgs = session.messages.slice(
|
||||||
session.lastSummarizeIndex
|
session.lastSummarizeIndex,
|
||||||
);
|
);
|
||||||
const historyMsgLength = toBeSummarizedMsgs.reduce(
|
const historyMsgLength = toBeSummarizedMsgs.reduce(
|
||||||
(pre, cur) => pre + cur.content.length,
|
(pre, cur) => pre + cur.content.length,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (historyMsgLength > 4000) {
|
if (historyMsgLength > 4000) {
|
||||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||||
-config.historyMessageCount
|
-config.historyMessageCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,7 +427,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
"[Chat History] ",
|
"[Chat History] ",
|
||||||
toBeSummarizedMsgs,
|
toBeSummarizedMsgs,
|
||||||
historyMsgLength,
|
historyMsgLength,
|
||||||
config.compressMessageLengthThreshold
|
config.compressMessageLengthThreshold,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (historyMsgLength > config.compressMessageLengthThreshold) {
|
if (historyMsgLength > config.compressMessageLengthThreshold) {
|
||||||
|
@ -445,7 +449,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("[Summarize] ", error);
|
console.error("[Summarize] ", error);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -474,6 +478,6 @@ export const useChatStore = create<ChatStore>()(
|
||||||
{
|
{
|
||||||
name: LOCAL_KEY,
|
name: LOCAL_KEY,
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
|
export interface Prompt {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptStore {
|
||||||
|
latestId: number;
|
||||||
|
prompts: Map<number, Prompt>;
|
||||||
|
|
||||||
|
add: (prompt: Prompt) => number;
|
||||||
|
remove: (id: number) => void;
|
||||||
|
search: (text: string) => Prompt[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROMPT_KEY = "prompt-store";
|
||||||
|
|
||||||
|
export const SearchService = {
|
||||||
|
ready: false,
|
||||||
|
engine: new Fuse<Prompt>([], { keys: ["title"] }),
|
||||||
|
count: {
|
||||||
|
builtin: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
init(prompts: Prompt[]) {
|
||||||
|
if (this.ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.engine.setCollection(prompts);
|
||||||
|
this.ready = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
this.engine.remove((doc) => doc.id === id);
|
||||||
|
},
|
||||||
|
|
||||||
|
add(prompt: Prompt) {
|
||||||
|
this.engine.add(prompt);
|
||||||
|
},
|
||||||
|
|
||||||
|
search(text: string) {
|
||||||
|
const results = this.engine.search(text);
|
||||||
|
return results.map((v) => v.item);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePromptStore = create<PromptStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
latestId: 0,
|
||||||
|
prompts: new Map(),
|
||||||
|
|
||||||
|
add(prompt) {
|
||||||
|
const prompts = get().prompts;
|
||||||
|
prompt.id = get().latestId + 1;
|
||||||
|
prompts.set(prompt.id, prompt);
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
latestId: prompt.id!,
|
||||||
|
prompts: prompts,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return prompt.id!;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(id) {
|
||||||
|
const prompts = get().prompts;
|
||||||
|
prompts.delete(id);
|
||||||
|
SearchService.remove(id);
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
prompts,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
search(text) {
|
||||||
|
return SearchService.search(text) as Prompt[];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: PROMPT_KEY,
|
||||||
|
version: 1,
|
||||||
|
onRehydrateStorage(state) {
|
||||||
|
const PROMPT_URL = "./prompts.json";
|
||||||
|
|
||||||
|
type PromptList = Array<[string, string]>;
|
||||||
|
|
||||||
|
fetch(PROMPT_URL)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
const builtinPrompts = [res.en, res.cn]
|
||||||
|
.map((promptList: PromptList) => {
|
||||||
|
return promptList.map(
|
||||||
|
([title, content]) =>
|
||||||
|
({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
} as Prompt)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.concat([...(state?.prompts?.values() ?? [])]);
|
||||||
|
|
||||||
|
const allPromptsForSearch = builtinPrompts.reduce(
|
||||||
|
(pre, cur) => pre.concat(cur),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||||
|
SearchService.init(allPromptsForSearch);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
36
package.json
36
package.json
|
@ -4,25 +4,21 @@
|
||||||
"private": false,
|
"private": false,
|
||||||
"license": "Anti 996",
|
"license": "Anti 996",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "yarn fetch && next dev",
|
||||||
"build": "next build",
|
"build": "yarn fetch && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"fetch": "node ./scripts/fetch-prompts.mjs",
|
||||||
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@types/node": "^18.14.6",
|
|
||||||
"@types/react": "^18.0.28",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"@types/react-katex": "^3.0.0",
|
|
||||||
"@types/spark-md5": "^3.0.2",
|
|
||||||
"@vercel/analytics": "^0.1.11",
|
"@vercel/analytics": "^0.1.11",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"emoji-picker-react": "^4.4.7",
|
"emoji-picker-react": "^4.4.7",
|
||||||
"eslint": "8.35.0",
|
|
||||||
"eslint-config-next": "13.2.3",
|
|
||||||
"eventsource-parser": "^0.1.0",
|
"eventsource-parser": "^0.1.0",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"next": "^13.2.3",
|
"next": "^13.2.3",
|
||||||
|
"node-fetch": "^3.3.1",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -33,7 +29,23 @@
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"typescript": "4.9.5",
|
"use-debounce": "^9.0.3",
|
||||||
"zustand": "^4.3.6"
|
"zustand": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.14.6",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/react-katex": "^3.0.0",
|
||||||
|
"@types/spark-md5": "^3.0.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.36.0",
|
||||||
|
"eslint-config-next": "13.2.3",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"husky": "^8.0.0",
|
||||||
|
"lint-staged": "^13.2.0",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"typescript": "4.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
const CN_URL =
|
||||||
|
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
|
||||||
|
const EN_URL =
|
||||||
|
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
|
||||||
|
const FILE = "./public/prompts.json";
|
||||||
|
|
||||||
|
async function fetchCN() {
|
||||||
|
console.log("[Fetch] fetching cn prompts...");
|
||||||
|
try {
|
||||||
|
const raw = await (await fetch(CN_URL)).json();
|
||||||
|
return raw.map((v) => [v.act, v.prompt]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Fetch] failed to fetch cn prompts", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchEN() {
|
||||||
|
console.log("[Fetch] fetching en prompts...");
|
||||||
|
try {
|
||||||
|
const raw = await (await fetch(EN_URL)).text();
|
||||||
|
return raw
|
||||||
|
.split("\n")
|
||||||
|
.slice(1)
|
||||||
|
.map((v) => v.split('","').map((v) => v.replace('"', "")));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Fetch] failed to fetch cn prompts", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
Promise.all([fetchCN(), fetchEN()])
|
||||||
|
.then(([cn, en]) => {
|
||||||
|
fs.writeFile(FILE, JSON.stringify({ cn, en }));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[Fetch] failed to fetch prompts");
|
||||||
|
fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] }));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
console.log("[Fetch] saved to " + FILE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
Loading…
Reference in New Issue