├── .env.example ├── .gitignore ├── README.md ├── README_zh.md ├── components ├── DropDown.tsx ├── Footer.tsx ├── Header.tsx ├── LoadingDots.tsx ├── Recommend.tsx └── ResizablePanel.tsx ├── messages ├── en.json └── zh.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── generate.ts └── index.tsx ├── postcss.config.js ├── public ├── 1-black.png ├── 2-black.png ├── favicon.ico ├── icon.svg ├── og-image.png ├── screenshot.png ├── screenshot_zh.png ├── vercel.svg └── vercelLogo.png ├── styles ├── globals.css ├── loading-dots.module.css └── markdown.module.css ├── tailwind.config.js ├── test └── utils.test.ts ├── tsconfig.json └── utils ├── OpenAIStream.ts ├── auth.ts ├── fetchWithTimeout.ts └── utils.ts /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | NEXT_PUBLIC_USE_USER_KEY=false 3 | NEXT_PUBLIC_SECRET= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .env 38 | 39 | # idea 40 | .idea 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat Simplifier 2 | 3 | [![discord](https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true)](https://chat.imzbb.cc) 4 | 5 | English | [中文](https://github.com/zhengbangbo/chat-simplifier/blob/main/README_zh.md) 6 | 7 | This project simplify chat content for you using AI. 8 | 9 | [![Chat Simplifier](./public/screenshot.png)](https://chat-simplifier.vercel.app/) 10 | 11 | ## How it works 12 | 13 | This project uses the [Chat GPT API](https://platform.openai.com/docs/api-reference/chat) (gpt-3.5-turbo) and [Vercel Edge functions](https://vercel.com/features/edge-functions) with streaming. It constructs a prompt based on the form and user input, sends it to the GPT-3 API via a Vercel Edge function, then streams the response back to the application. 14 | 15 | ## Running Locally 16 | 17 | After cloning the repository, go to [OpenAI](https://beta.openai.com/account/api-keys) to create an account and refer to the [environment variable instructions](#environment-variable-description) to put your API key into a file named `.env`. 18 | 19 | Then, run the application in the command line and it will be available at `http://localhost:3000`. 20 | 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | ## Environment variable description 26 | 27 | | Environment variable | Description | Required | Optional value | 28 | |---------|------|-----|--| 29 | |OPENAI_API_KEY| OpenAI API Key,separate with `,` when there are multiple | No | ([Get](https://beta.openai.com/account/api-keys)) | 30 | |NEXT_PUBLIC_USE_USER_KEY|Whether to use the API key entered by the user| No, default value is `true` |`true` or `false` | 31 | |NEXT_PUBLIC_SECRET|Secret string for the project. Use for generating signatures for API calls | No |`null`| 32 | 33 | ## One-Click Deploy 34 | 35 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): 36 | 37 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/zhengbangbo/chat-simplifier&env=OPENAI_API_KEY,NEXT_PUBLIC_USE_USER_KEY&envDescription=%E7%82%B9%E5%87%BB%E5%8F%B3%E4%BE%A7%E3%80%8CLearn%20More%E3%80%8D%E6%9F%A5%E7%9C%8B%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AF%B4%E6%98%8E&envLink=https://github.com/zhengbangbo/chat-simplifier/wiki/Deploy&project-name=chat-simplifier&repository-name=chat-simplifier) 38 | 39 | ## Credits 40 | 41 | Inspired by [TwtterBio](https://github.com/Nutlope/twitterbio) and [Jimmy Lv](https://www.bilibili.com/video/BV17M411i7B6). 42 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 聊天简化器 2 | 3 | [![discord](https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true)](https://chat.imzbb.cc) 4 | 5 | [English](https://github.com/zhengbangbo/chat-simplifier/blob/main/README.md) | 中文 6 | 7 | 此项目使用 AI 为您简化聊天内容 8 | 9 | [![Chat Simplifier](./public/screenshot_zh.png)](https://chat-simplifier.vercel.app/) 10 | 11 | ## 它是怎么工作的 12 | 13 | 该项目使用 [Chat GPT API](https://platform.openai.com/docs/api-reference/chat) (gpt-3.5-turbo)和 [Vercel Edge functions](https://vercel.com/features/edge-functions) 流式传输。它根据表单和用户输入构建 Prompts,通过 Vercel Edge 函数发送到 GPT-3 API,然后将响应流回到应用程序。 14 | 15 | ## 本地运行 16 | 17 | 克隆存储库后,前往 [OpenAI](https://beta.openai.com/account/api-keys) 创建帐户,并参考[环境变量说明](#环境变量说明)将 API 密钥放入名为 `.env` 的文件中。 18 | 19 | 然后,在命令行中运行应用程序,它将在 `http://localhost:3000` 处可用。 20 | 21 | > **Note** 22 | > 本地运行时,服务端使用本地网络请求 OpenAI,因此需要配置好网络代理。 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## 环境变量说明 29 | 30 | | 环境变量 | 说明 | 是否必须 |可选值 | 31 | |---------|------|---|----| 32 | |OPENAI_API_KEY| OpenAI API Key,当有多个时用`,`分隔 | 非必需 |([获取](https://beta.openai.com/account/api-keys)) | 33 | |NEXT_PUBLIC_USE_USER_KEY|是否使用用户自己输入的 API 密钥| 必须,默认为 true |`true` or `false` | 34 | |NEXT_PUBLIC_SECRET|项目的秘密字符串。用于生成 API 调用的签名| 非必需 | `123abc` | 35 | 36 | ## 一键部署 37 | 38 | 使用 [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples) 部署示例: 39 | 40 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/zhengbangbo/chat-simplifier&env=OPENAI_API_KEY,NEXT_PUBLIC_USE_USER_KEY&envDescription=%E7%82%B9%E5%87%BB%E5%8F%B3%E4%BE%A7%E3%80%8CLearn%20More%E3%80%8D%E6%9F%A5%E7%9C%8B%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AF%B4%E6%98%8E&envLink=https://github.com/zhengbangbo/chat-simplifier/wiki/Deploy&project-name=chat-simplifier&repository-name=chat-simplifier) 41 | 42 | ## 致谢 43 | 44 | 灵感来自 [TwtterBio](https://github.com/Nutlope/twitterbio) 和 [Jimmy Lv](https://www.bilibili.com/video/BV17M411i7B6)。 45 | -------------------------------------------------------------------------------- /components/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Transition } from "@headlessui/react"; 2 | import { 3 | CheckIcon, 4 | ChevronDownIcon, 5 | ChevronUpIcon, 6 | } from "@heroicons/react/20/solid"; 7 | import { Fragment } from "react"; 8 | import { useTranslations } from 'next-intl' 9 | 10 | function classNames(...classes: string[]) { 11 | return classes.filter(Boolean).join(" "); 12 | } 13 | 14 | export type FormType = "paragraphForm" | "outlineForm"; 15 | 16 | interface DropDownProps { 17 | form: FormType; 18 | setForm: (form: FormType) => void; 19 | } 20 | 21 | let forms: FormType[] = ["paragraphForm", "outlineForm"] 22 | 23 | export default function DropDown({ form, setForm}: DropDownProps) { 24 | const t = useTranslations('Index') 25 | return ( 26 | 27 |
28 | 29 | {t(form)} 30 | 39 |
40 | 41 | 50 | 54 |
55 | {forms.map((formItem) => ( 56 | 57 | {({ active }) => ( 58 | 71 | )} 72 | 73 | ))} 74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Footer() { 4 | return ( 5 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useRouter } from 'next/router' 3 | import { useTranslations } from "next-intl"; 4 | 5 | export default function Header() { 6 | const t = useTranslations('Index') 7 | const { locale, locales, route } = useRouter() 8 | const otherLocale = locales?.find((cur) => cur !== locale) 9 | 10 | return ( 11 |
12 |
13 | { 14 | otherLocale && ( 15 |
17 | 18 | {t('switchLocale', { locale: otherLocale })} 19 | 20 |
21 | ) 22 | } 23 | {" / "} 24 | 30 |

{t('deployWiki')}

31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/loading-dots.module.css"; 2 | 3 | const LoadingDots = ({ 4 | color = "#000", 5 | style = "small", 6 | }: { 7 | color: string; 8 | style: string; 9 | }) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default LoadingDots; 20 | 21 | LoadingDots.defaultProps = { 22 | style: "small", 23 | }; 24 | -------------------------------------------------------------------------------- /components/Recommend.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl' 2 | 3 | export default function Recommend() { 4 | const t = useTranslations('Index') 5 | return ( 6 |
7 |

{t('recommend')}

8 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/ResizablePanel.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import useMeasure from "react-use-measure"; 3 | 4 | export default function ResizablePanel({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | let [ref, { height }] = useMeasure(); 10 | 11 | return ( 12 | 18 |
19 | {children} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": { 3 | "title": "Chat Simplifier", 4 | "switchLocale": "{locale, select,zh {中文} en {English} other {}}", 5 | "deployWiki": "Deploy your own website?", 6 | "slogan": "Too many group messages? It's too long to look!", 7 | "step0": "Paste your OpenAI API Key", 8 | "openaiApiKeyPlaceholder": "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 9 | "step1": "Paste your chat content", 10 | "helpPageLink": "(Click here to view tutorials)", 11 | "placeholder": "Choose more chat content and copy it. It usually contains nicknames and content.", 12 | "step2": "Select the form of the output.", 13 | "simplifierButton": "Simplify the chat content", 14 | "privacyPolicy1": "It is not recommended to upload chat that is too private. For more information, please see", 15 | "privacyPolicy2": "Privacy Statement", 16 | "simplifiedContent": "Simplified content", 17 | "paragraphForm": "Paragraph", 18 | "paragraphFormPrompt": "You will receive a chat record and we hope you can summarize it. Please provide a brief summary in one paragraph.", 19 | "outlineForm": "Outline", 20 | "outlineFormPrompt": "You will receive a chat record and we hope you can summarize it. Please provide a concise outline format that includes a list.", 21 | "pasteButton": "Paste", 22 | "clearButton": "Clear", 23 | "copySuccess": "Chat copied to clipboard", 24 | "copyError": "Copy failed, please use HTTPS", 25 | "emptyChatError": "Please paste the chat content", 26 | "timeoutError": "Request timed out, please try again later", 27 | "internalServerError": "Internal server error, please try again later", 28 | "emptyAPIKeyError": "Please paste the OpenAI API key", 29 | "invalidAPIKeyError": "The OpenAI API key is invalid", 30 | "recommend": "Recommend Other AI Application" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /messages/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": { 3 | "title": "聊天简化器", 4 | "switchLocale": "{locale, select,zh {中文} en {English} other {}}", 5 | "deployWiki": "部署自己的网站?", 6 | "slogan": "群消息太多?太长不看!", 7 | "step0": "粘贴你的 OpenAI API Key", 8 | "openaiApiKeyPlaceholder": "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 9 | "step1": "粘贴你的聊天内容", 10 | "helpPageLink": "(点击这里查看教程)", 11 | "placeholder": "多选聊天内容,复制。通常包含昵称和内容。", 12 | "step2": "选择输出结果的形式。", 13 | "simplifierButton": "简化聊天内容", 14 | "simplifiedContent": "简化后的内容", 15 | "privacyPolicy1": "不建议上传过于隐私的聊天内容,详情查看", 16 | "privacyPolicy2": "《隐私声明》", 17 | "paragraphForm": "段落", 18 | "paragraphFormPrompt": "你将得到一串聊天记录,希望你能够对这些记录进行摘要。要求简明扼要,以一段话的形式输出。", 19 | "outlineForm": "大纲", 20 | "outlineFormPrompt": "你将得到一串聊天记录,希望你能够对这些记录进行摘要。要求简明扼要,以包含列表的大纲形式输出。", 21 | "pasteButton": "粘贴", 22 | "clearButton": "清空", 23 | "copySuccess": "摘要已复制到剪贴板", 24 | "emptyChatError": "聊天内容不能为空", 25 | "copyError": "复制失败,请使用HTTPS", 26 | "timeoutError": "请求超时,请稍后再试", 27 | "internalServerError": "服务器内部错误,请稍后再试", 28 | "emptyAPIKeyError": "OpenAI API Key 不能为空", 29 | "invalidAPIKeyError": "OpenAI API Key 无效", 30 | "recommend": "推荐其他 AI 应用" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | i18n: { 5 | locales: ['en', 'zh'], 6 | defaultLocale: 'en', 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start", 7 | "test": "vitest" 8 | }, 9 | "dependencies": { 10 | "@headlessui/react": "^1.7.14", 11 | "@headlessui/tailwindcss": "^0.1.3", 12 | "@heroicons/react": "^2.0.17", 13 | "@tailwindcss/forms": "^0.5.3", 14 | "@vercel/analytics": "^0.1.11", 15 | "dotenv": "^16.0.3", 16 | "eventsource-parser": "^1.0.0", 17 | "framer-motion": "^10.12.2", 18 | "js-sha256": "^0.9.0", 19 | "marked": "^4.3.0", 20 | "next": "latest", 21 | "next-intl": "^2.13.1", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-hook-form": "^7.43.9", 25 | "react-hot-toast": "^2.4.0", 26 | "react-use-measure": "^2.1.1" 27 | }, 28 | "devDependencies": { 29 | "@types/marked": "^4.0.8", 30 | "@types/node": "18.15.11", 31 | "@types/react": "18.0.35", 32 | "@types/react-dom": "18.0.11", 33 | "autoprefixer": "^10.4.14", 34 | "postcss": "^8.4.22", 35 | "tailwindcss": "^3.3.1", 36 | "typescript": "5.0.4", 37 | "vitest": "^0.30.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | import type { AppProps } from "next/app"; 3 | import { NextIntlProvider } from 'next-intl' 4 | import "../styles/globals.css"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 25 | 29 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default MyDocument; 44 | -------------------------------------------------------------------------------- /pages/api/generate.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, OpenAIStreamPayload } from "../../utils/OpenAIStream"; 2 | import type { ChatGPTMessage } from "../../utils/OpenAIStream"; 3 | import { checkOpenAIKey } from "../../utils/utils"; 4 | import { verifySignature } from "../../utils/auth"; 5 | 6 | export const config = { 7 | runtime: "edge", 8 | unstable_allowDynamic: [ 9 | "/node_modules/js-sha256/src/sha256.js", 10 | ] 11 | }; 12 | 13 | const handler = async (req: Request): Promise => { 14 | 15 | if (!process.env.NEXT_PUBLIC_USE_USER_KEY) { 16 | return new Response("NEXT_PUBLIC_USE_USER_KEY", { 17 | status: 501, 18 | statusText: "No environment variable set: NEXT_PUBLIC_USE_USER_KEY" 19 | }); 20 | }; 21 | 22 | if (process.env.NEXT_PUBLIC_USE_USER_KEY === 'false') { 23 | if (!process.env.OPENAI_API_KEY) { 24 | return new Response("OPENAI_API_KEY", { 25 | status: 501, 26 | statusText: "No environment variable set: OPENAI_API_KEY" 27 | }); 28 | } 29 | } 30 | 31 | 32 | const { prompt, time, sign, api_key } = (await req.json()) as { 33 | prompt: ChatGPTMessage[]; 34 | time: number; 35 | sign: string; 36 | api_key?: string; 37 | }; 38 | 39 | if (!await verifySignature({ 40 | t: time, m: prompt?.[prompt.length - 1]?.content || '' 41 | }, sign)) { 42 | return new Response("Invalid signature", { status: 400 }); 43 | } 44 | 45 | const payload: OpenAIStreamPayload = { 46 | model: "gpt-3.5-turbo", 47 | messages: prompt, 48 | temperature: 0.8, 49 | top_p: 1, 50 | frequency_penalty: 0, 51 | presence_penalty: 0, 52 | max_tokens: 300, 53 | stream: true, 54 | n: 1, 55 | api_key, 56 | } 57 | 58 | const stream = await OpenAIStream(payload); 59 | return new Response(stream); 60 | }; 61 | 62 | export default handler; 63 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import type { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import Image from "next/image"; 5 | import { use, useEffect, useState } from "react"; 6 | import { useTranslations } from 'next-intl' 7 | import { Toaster, toast } from "react-hot-toast"; 8 | import DropDown, { FormType } from "../components/DropDown"; 9 | import Footer from "../components/Footer"; 10 | 11 | import Header from "../components/Header"; 12 | import LoadingDots from "../components/LoadingDots"; 13 | import ResizablePanel from "../components/ResizablePanel"; 14 | import Recommend from "../components/Recommend"; 15 | import { fetchWithTimeout } from '../utils/fetchWithTimeout' 16 | import { generateSignature } from '../utils/auth' 17 | import { checkOpenAIKey } from "../utils/utils"; 18 | import { marked } from "marked"; 19 | import styles from '../styles/markdown.module.css' 20 | 21 | const useUserKey = process.env.NEXT_PUBLIC_USE_USER_KEY === "false" ? false : true; 22 | 23 | const REQUEST_TIMEOUT = 15 * 1000 // 15s timeout 24 | 25 | const Home: NextPage = () => { 26 | const t = useTranslations('Index') 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [chat, setChat] = useState(""); 30 | const [form, setForm] = useState("paragraphForm"); 31 | const [api_key, setAPIKey] = useState("") 32 | const [isSecureContext, setIsSecureContext] = useState(false) 33 | const [generatedChat, setGeneratedChat] = useState(""); 34 | 35 | console.log("Streamed response: ", generatedChat); 36 | 37 | const system_prompt = 38 | form === 'paragraphForm' ? 39 | `${t('paragraphFormPrompt')}` 40 | : `${t('outlineFormPrompt')}`; 41 | 42 | const user_prompt = chat; 43 | const prompt = [ 44 | { 45 | "role": "system", 46 | "content": system_prompt 47 | }, 48 | { 49 | "role": "user", 50 | "content": user_prompt 51 | } 52 | ] 53 | 54 | useEffect(() => { 55 | if (typeof window !== "undefined") { 56 | setIsSecureContext(window.isSecureContext) 57 | } 58 | }) 59 | 60 | const generateChat = async (e: any) => { 61 | e.preventDefault(); 62 | setGeneratedChat(""); 63 | setLoading(true); 64 | 65 | if (!chat) { 66 | toast.error(t('emptyChatError')) 67 | setLoading(false) 68 | return 69 | } 70 | 71 | if (useUserKey) { 72 | if (!api_key) { 73 | toast.error(t('emptyAPIKeyError')) 74 | setLoading(false) 75 | return 76 | } 77 | if (!checkOpenAIKey(api_key)) { 78 | toast.error(t('invalidAPIKeyError')) 79 | setLoading(false) 80 | return 81 | } 82 | } 83 | console.log("Sending request to Edge function."); 84 | 85 | const timestamp = Date.now() 86 | try { 87 | const response = useUserKey ? 88 | await fetchWithTimeout("/api/generate", { 89 | method: "POST", 90 | headers: { 91 | "Content-Type": "application/json", 92 | }, 93 | timeout: REQUEST_TIMEOUT, 94 | body: JSON.stringify({ 95 | prompt, 96 | time: timestamp, 97 | sign: await generateSignature({ 98 | t: timestamp, 99 | m: prompt?.[prompt.length - 1]?.content || "", 100 | }), 101 | api_key, 102 | }), 103 | }) 104 | : 105 | await fetchWithTimeout("/api/generate", { 106 | method: "POST", 107 | headers: { 108 | "Content-Type": "application/json", 109 | }, 110 | timeout: REQUEST_TIMEOUT, 111 | body: JSON.stringify({ 112 | prompt, 113 | time: timestamp, 114 | sign: await generateSignature({ 115 | t: timestamp, 116 | m: prompt?.[prompt.length - 1]?.content || "", 117 | }), 118 | }), 119 | }) 120 | console.log("Edge function returned."); 121 | 122 | if (!response.ok) { 123 | throw new Error(response.statusText); 124 | } 125 | 126 | // This data is a ReadableStream 127 | const data = response.body; 128 | if (!data) { 129 | return; 130 | } 131 | 132 | const reader = data.getReader(); 133 | const decoder = new TextDecoder(); 134 | let done = false; 135 | 136 | while (!done) { 137 | const { value, done: doneReading } = await reader.read(); 138 | done = doneReading; 139 | const chunkValue = decoder.decode(value).replace("<|im_end|>", ""); 140 | setGeneratedChat((prev) => prev + chunkValue); 141 | } 142 | 143 | setLoading(false); 144 | } catch (e: any) { 145 | console.error('[fetch ERROR]', e) 146 | if (e instanceof Error && e?.name === 'AbortError') { 147 | setLoading(false) 148 | toast.error(t('timeoutError')) 149 | } else { 150 | setLoading(false) 151 | toast.error(e?.message || t('unknownError')) 152 | } 153 | } 154 | }; 155 | 156 | return ( 157 |
158 | 159 | {t('title')} 160 | 161 | 162 |
163 |
164 |

165 | {t('title')} 166 |

167 |

{t('slogan')}

168 |
169 | {useUserKey && ( 170 | <> 171 |
172 | 173 |

174 | {t('step0')}{" "} 175 | 176 | {t('helpPageLink')} 181 | 182 |

183 |
184 | setAPIKey(e.target.value)} 187 | className="w-full rounded-md border-2 border-gray-300 shadow-sm focus:border-black focus:ring-black p-2" 188 | placeholder={ 189 | t('openaiApiKeyPlaceholder') 190 | } 191 | /> 192 | ) 193 | } 194 |
195 | 1 icon 201 |

202 | {t('step1')}{" "} 203 | { 204 | !useUserKey && ( 205 | 206 | {t('helpPageLink')} 211 | 212 | ) 213 | } 214 |

215 |
216 |
217 | { 218 | isSecureContext && ( 219 | navigator.clipboard.readText().then((clipText) => setChat(clipText))}> 221 | {t('pasteButton')} 222 | 223 | ) 224 | } 225 | setChat("")}> 227 | {t('clearButton')} 228 | 229 |
230 |