diff --git a/.env.local.demo b/.env.local.demo index af7b315..b02a16a 100644 --- a/.env.local.demo +++ b/.env.local.demo @@ -1,12 +1,12 @@ -# OpenAI Key. eg: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# OpenAI API Key. eg: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_OPENAI_API_KEY= -# Your own openai api proxy url. If empty here, default proxy will be https://api.openai.com +# Your own OpenAI API proxy address. If left empty, https://api.openai.com will be used by default. NEXT_PUBLIC_OPENAI_API_PROXY= -# Azure OpenAI Key. eg: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Azure OpenAI API Key. eg: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_AZURE_OPENAI_API_KEY= -# your own Azure OpenAI api proxy url. If it is empty, the Azure OpenAI Service will not function properly. -NEXT_PUBLIC_AZURE_OPENAI_API_PROXY= +# Resource name for Azure OpenAI Service. If empty, Azure OpenAI Service will not be able to be used. +NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME= -# set your own sentry dsn. if empty here, it will not report error to sentry +# Set your own sentry dsn. if empty, it will not report error to sentry NEXT_PUBLIC_SENTRY_DSN= \ No newline at end of file diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index e065514..c187377 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -1,5 +1,22 @@ # L-GPT Change Log +## v0.2.0 + +> 2023-05-07 + +### Fixed + +- Fixed the issue of incorrect input box position caused by the vertical scroll bar appearing on some mobile browsers + +### Add + +- Introduce prompt words and prompt word templates + +### Changed + +- Refactor some pages and interaction logic +- Improve the error message content for front-end fetch requests + ## v0.1.3 > 2023-04-28 diff --git a/README.md b/README.md index b8e162a..ec2185f 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,33 @@ English / [简体中文](./README_CN.md) -L-GPT is an open-source project that imitates the OpenAI ChatGPT. [Demo](https://gpt.ltops.cn) +L-GPT is an open-source project that helps you improve your learning, work, and life efficiency by providing various AI models. [Demo](https://gpt.ltopx.com) [QQ 群](./public/screenshots/qq.jpeg) - +## Preview + + + + ## Features - Deploy for free on Vercel -- Responsive design, dark mode and PWA +- Responsive design and dark mode - Safe, all data based on local - Support i18n -- Support Azure OpenAI Service +- Support [Azure OpenAI Service](./azure.md) +- Support configuration and use of custom prompt ## Next - [x] Support Azure OpenAI -- [ ] Introduce prompt words and prompt word templates -- [ ] Support GPT-4 and Claude -- [ ] Desktop version development? +- [x] Introduce prompt words and prompt word templates - [ ] Chat record import and export +- [ ] Support GPT-4 and Claude - [ ] Compress context to save chat tokens +- [ ] Desktop version development ## Deploy on Vercel @@ -34,8 +39,8 @@ Get your own website. # Prefer using user-configured key. # If user hasn't configured, then use this key. -# If neither are configured, it is not possible to use. -# sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# If neither are configured, it is not possible to use OpenAI API. +# eg: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_OPENAI_API_KEY= # Prefer using user-configured proxy address. @@ -43,17 +48,17 @@ NEXT_PUBLIC_OPENAI_API_KEY= # If none of these are being used, then connect directly to the Open AI official address: https://api.openai.com. NEXT_PUBLIC_OPENAI_API_PROXY= -# Set Your Azure OpenAI key. +# Set Your Azure OpenAI API key. NEXT_PUBLIC_AZURE_OPENAI_API_KEY= -# Set Your Azure OpenAI proxy. -NEXT_PUBLIC_AZURE_OPENAI_API_PROXY= +# Set Your Azure OpenAI API resource name. +NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME= # set your own sentry dsn. if empty here, it will not report error to sentry NEXT_PUBLIC_SENTRY_DSN= ``` -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Peek-A-Booo/L-GPT&env=NEXT_PUBLIC_OPENAI_API_KEY&env=NEXT_PUBLIC_OPENAI_API_PROXY&env=NEXT_PUBLIC_AZURE_OPENAI_API_KEY&env=NEXT_PUBLIC_AZURE_OPENAI_API_PROXY&env=NEXT_PUBLIC_SENTRY_DSN) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Peek-A-Booo/L-GPT&env=NEXT_PUBLIC_OPENAI_API_KEY&env=NEXT_PUBLIC_OPENAI_API_PROXY&env=NEXT_PUBLIC_AZURE_OPENAI_API_KEY&env=NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME&env=NEXT_PUBLIC_SENTRY_DSN) ## Running Local @@ -86,10 +91,10 @@ Rename .evn.local.demo to .env.local and configure it. NEXT_PUBLIC_OPENAI_API_KEY= # your own OpenAI Api proxy url. NEXT_PUBLIC_OPENAI_API_PROXY= -# Azure OpenAI Key. eg: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Azure OpenAI API Key. eg: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_AZURE_OPENAI_API_KEY= -# your own Azure OpenAI api proxy url. If it is empty, the Azure OpenAI Service will not function properly. -NEXT_PUBLIC_AZURE_OPENAI_API_PROXY= +# Azure OpenAI resource name. +NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME= # set your own sentry dsn. if empty here, it will not report error to sentry NEXT_PUBLIC_SENTRY_DSN= ``` @@ -110,10 +115,14 @@ pnpm build && pnpm start You can configure the following environment variables. -| Environment Variable | Desc | Required | Default | -| ------------------------------------ | ------------------------------------------------------------- | -------- | ------------------------ | -| `NEXT_PUBLIC_OPENAI_API_KEY` | your OpenAI API Key | false | | -| `NEXT_PUBLIC_OPENAI_API_PROXY` | your OpenAI API proxy server | false | `https://api.openai.com` | -| `NEXT_PUBLIC_AZURE_OPENAI_API_KEY` | your Azure OpenAI API Key | false | | -| `NEXT_PUBLIC_AZURE_OPENAI_API_PROXY` | your Azure OpenAI API proxy server | false | | -| `NEXT_PUBLIC_SENTRY_DSN` | your sentry dsn. If empty, it will not report error to sentry | false | | +| Environment Variable | Desc | Required | Default | +| ---------------------------------------- | --------------------------------------------------------------- | -------- | ------------------------ | +| `NEXT_PUBLIC_OPENAI_API_KEY` | your OpenAI API Key | false | | +| `NEXT_PUBLIC_OPENAI_API_PROXY` | your OpenAI API proxy server | false | `https://api.openai.com` | +| `NEXT_PUBLIC_AZURE_OPENAI_API_KEY` | your Azure OpenAI API Key. [View Example](./azure.md) | false | | +| `NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME` | your Azure OpenAI API resource name. [View Example](./azure.md) | false | | +| `NEXT_PUBLIC_SENTRY_DSN` | your sentry dsn. If empty, it will not report error to sentry | false | | + +## Contact + +Any questions, please feel free to join our QQ group or contact us on [Twitter](https://twitter.com/peekbomb). diff --git a/README_CN.md b/README_CN.md index eb6399e..1857431 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,10 +1,14 @@ # L-GPT -L-GPT 是一项开源项目,借助 OpenAI Api 模仿了 ChatGPT 的功能。 [Demo](https://gpt.ltops.cn) +L-GPT 是一款开源项目,通过提供不同的 AI 模型来帮助你提高学习、工作、生活的效率。 [Demo](https://gpt.ltopx.com) [QQ 群](./public/screenshots/qq.jpeg) - +## 预览 + + + + ## 特性 @@ -12,16 +16,17 @@ L-GPT 是一项开源项目,借助 OpenAI Api 模仿了 ChatGPT 的功能。 [ - 支持响应式,暗黑模式和 PWA - 安全,所有数据均基于本地存储 - 支持 i18n -- 支持 Azure OpenAI Service +- 支持 [Azure OpenAI Service](./azure_CN.md) +- 支持配置和使用自定义 prompt ## 下一步计划 - [x] 支持 Azure OpenAI -- [ ] 引入提示词以及提示词模板 -- [ ] 支持 GPT-4 和 Claude -- [ ] 桌面版本开发? +- [x] 引入提示词以及提示词模板 - [ ] 聊天记录导入导出 +- [ ] 支持 GPT-4 和 Claude - [ ] 压缩上下文,节省聊天 token +- [ ] 桌面版本开发 ## 发布到 Vercel @@ -32,8 +37,8 @@ L-GPT 是一项开源项目,借助 OpenAI Api 模仿了 ChatGPT 的功能。 [ # 优先使用用户配置key # 用户没有配置则使用此key -# 都没配置则无法使用 -# sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# 都没配置则无法使用OpenAI API服务 +# 示例:sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_OPENAI_API_KEY= # 优先使用用户配置的代理地址 @@ -41,17 +46,17 @@ NEXT_PUBLIC_OPENAI_API_KEY= # 都没有使用则直连Open AI 官方地址:https://api.openai.com NEXT_PUBLIC_OPENAI_API_PROXY= -# 配置你的 Azure OpenAI key. +# 配置你的 Azure OpenAI API key. NEXT_PUBLIC_AZURE_OPENAI_API_KEY= -# 配置你的 Azure OpenAI proxy. -NEXT_PUBLIC_AZURE_OPENAI_API_PROXY= +# 配置你的 Azure OpenAI 资源名称. +NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME= # 配置你的 sentry dsn地址。如果为空, 将不会将错误报告到 sentry NEXT_PUBLIC_SENTRY_DSN= ``` -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Peek-A-Booo/L-GPT&env=NEXT_PUBLIC_OPENAI_API_KEY&env=NEXT_PUBLIC_OPENAI_API_PROXY&env=NEXT_PUBLIC_AZURE_OPENAI_API_KEY&env=NEXT_PUBLIC_AZURE_OPENAI_API_PROXY&env=NEXT_PUBLIC_SENTRY_DSN) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Peek-A-Booo/L-GPT&env=NEXT_PUBLIC_OPENAI_API_KEY&env=NEXT_PUBLIC_OPENAI_API_PROXY&env=NEXT_PUBLIC_AZURE_OPENAI_API_KEY&env=NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME&env=NEXT_PUBLIC_SENTRY_DSN) ## 本地运行 @@ -86,8 +91,8 @@ NEXT_PUBLIC_OPENAI_API_KEY= NEXT_PUBLIC_OPENAI_API_PROXY= # Azure OpenAI Key: eg: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_AZURE_OPENAI_API_KEY= -# 配置你的Azure OpenAI代理地址。 If it is empty, the Azure OpenAI Service will not function properly. -NEXT_PUBLIC_AZURE_OPENAI_API_PROXY= +# Azure OpenAI 资源名称 +NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME= # 配置你的 sentry dsn地址。如果为空, 将不会将错误报告到 sentry NEXT_PUBLIC_SENTRY_DSN= ``` @@ -108,10 +113,14 @@ pnpm build && pnpm start 你可以配置以下环境变量。 -| 环境变量 | 描述 | 是否必须配置 | 默认值 | -| ------------------------------------ | --------------------------------------------------------- | ------------ | ------------------------ | -| `NEXT_PUBLIC_OPENAI_API_KEY` | 你个人的 OpenAI API key | 否 | | -| `NEXT_PUBLIC_OPENAI_API_PROXY` | 你个人的 OpenAI API 代理地址 | 否 | `https://api.openai.com` | -| `NEXT_PUBLIC_AZURE_OPENAI_API_KEY` | 你个人的 Azure OpenAI API key | 否 | | -| `NEXT_PUBLIC_AZURE_OPENAI_API_PROXY` | 你个人的 Azure OpenAI API 代理地址 | 否 | | -| `NEXT_PUBLIC_SENTRY_DSN` | 你的 sentry dsn 地址。如果为空, 将不会将错误报告到 sentry | 否 | | +| 环境变量 | 描述 | 是否必须配置 | 默认值 | +| ---------------------------------------- | ----------------------------------------------------------------- | ------------ | ------------------------ | +| `NEXT_PUBLIC_OPENAI_API_KEY` | 你个人的 OpenAI API key | 否 | | +| `NEXT_PUBLIC_OPENAI_API_PROXY` | 你个人的 OpenAI API 代理地址 | 否 | `https://api.openai.com` | +| `NEXT_PUBLIC_AZURE_OPENAI_API_KEY` | 你个人的 Azure OpenAI API key。[查看示例](./azure_CN.md) | 否 | | +| `NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME` | 你个人的 Azure OpenAI API 服务资源名称。[查看示例](./azure_CN.md) | 否 | | +| `NEXT_PUBLIC_SENTRY_DSN` | 你的 sentry dsn 地址。如果为空, 将不会将错误报告到 sentry | 否 | | + +## 联系方式 + +有任何疑问欢迎加入 QQ 群或联系 [Twitter](https://twitter.com/peekbomb). diff --git a/a.js b/a.js deleted file mode 100644 index 7b6c8b9..0000000 --- a/a.js +++ /dev/null @@ -1,194 +0,0 @@ -addEventListener("fetch", (event) => { - event.respondWith(handleRequest(event.request)); -}); - -const config = { - openai: { - originUrl: "https://api.openai.com", - }, - azure: { - apiVersion: "2023-03-15-preview", - // The name of your Azure OpenAI Resource. - resourceName: "lgpt-azure-openai", - // The deployment name you chose when you deployed the model. - model: { - "gpt-3.5-turbo-0301": "lgpt-35-turbo", - }, - }, -}; - -async function handleRequest(request) { - if ( - !request.url.includes("openai-proxy") && - !request.url.includes("azure-proxy") - ) { - return new Response("404 Not Found", { status: 404 }); - } - - if (request.method === "OPTIONS") return handleOPTIONS(request); - - if (request.url.includes("openai-proxy")) { - return handleOpenAI(request); - } else if (request.url.includes("azure-proxy")) { - return handleAzure(request); - } -} - -async function handleOpenAI(request) { - const url = new URL(request.url); - - url.host = config.openai.originUrl.replace(/^https?:\/\//, ""); - - const modifiedRequest = new Request(url.toString(), { - headers: request.headers, - method: request.method, - body: request.body, - redirect: "follow", - }); - - const response = await fetch(modifiedRequest); - const modifiedResponse = new Response(response.body, response); - - // 添加允许跨域访问的响应头 - modifiedResponse.headers.set("Access-Control-Allow-Origin", "*"); - - return modifiedResponse; -} - -async function handleAzure(request) { - const url = new URL(request.url); - - if (url.pathname === "/v1/chat/completions") { - var path = "chat/completions"; - } else if (url.pathname === "/v1/completions") { - var path = "completions"; - } else if (url.pathname === "/v1/models") { - return handleModels(request); - } else { - return new Response("404 Not Found", { status: 404 }); - } - - let body; - if (request.method === "POST") body = await request.json(); - - const modelName = body?.model; - const deployName = config.azure.model[modelName]; - - if (deployName === "") return new Response("Missing model", { status: 403 }); - - const fetchAPI = `https://${config.azure.resourceName}.openai.azure.com/openai/deployments/${deployName}/${path}?api-version=${config.azure.apiVersion}`; - - const authKey = request.headers.get("Authorization"); - - if (!authKey) return new Response("Not allowed", { status: 403 }); - - const payload = { - method: request.method, - headers: { - "Content-Type": "application/json", - "api-key": authKey.replace("Bearer ", ""), - }, - body: typeof body === "object" ? JSON.stringify(body) : "{}", - }; - - const response = await fetch(fetchAPI, payload); - - if (body?.stream != true) { - return response; - } - - const { readable, writable } = new TransformStream(); - stream(response.body, writable); - return new Response(readable, response); -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// support printer mode and add newline -async function stream(readable, writable) { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - // const decoder = new TextDecoder(); - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - // let decodedValue = decoder.decode(value); - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let buffer = ""; - while (true) { - let { value, done } = await reader.read(); - if (done) { - break; - } - buffer += decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line - let lines = buffer.split(delimiter); - - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(30); - } - - buffer = lines[lines.length - 1]; - } - - if (buffer) { - await writer.write(encoder.encode(buffer)); - } - await writer.write(encodedNewline); - await writer.close(); -} - -async function handleModels() { - const data = { - object: "list", - data: [], - }; - - for (let key in config.azure.model) { - data.data.push({ - id: key, - object: "model", - created: 1677610602, - owned_by: "openai", - permission: [ - { - id: "modelperm-M56FXnG1AsIr3SXq8BYPvXJA", - object: "model_permission", - created: 1679602088, - allow_create_engine: false, - allow_sampling: true, - allow_logprobs: true, - allow_search_indices: false, - allow_view: true, - allow_fine_tuning: false, - organization: "*", - group: null, - is_blocking: false, - }, - ], - root: key, - parent: null, - }); - } - - const json = JSON.stringify(data, null, 2); - return new Response(json, { - headers: { "Content-Type": "application/json" }, - }); -} - -async function handleOPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", - "Access-Control-Allow-Headers": "*", - }, - }); -} diff --git a/a1.js b/a1.js deleted file mode 100644 index e956c19..0000000 --- a/a1.js +++ /dev/null @@ -1,25 +0,0 @@ -const TELEGRAPH_URL = "https://api.openai.com"; - -addEventListener("fetch", (event) => { - event.respondWith(handleRequest(event.request)); -}); - -async function handleRequest(request) { - const url = new URL(request.url); - url.host = TELEGRAPH_URL.replace(/^https?:\/\//, ""); - - const modifiedRequest = new Request(url.toString(), { - headers: request.headers, - method: request.method, - body: request.body, - redirect: "follow", - }); - - const response = await fetch(modifiedRequest); - const modifiedResponse = new Response(response.body, response); - - // 添加允许跨域访问的响应头 - modifiedResponse.headers.set("Access-Control-Allow-Origin", "*"); - - return modifiedResponse; -} diff --git a/azure.md b/azure.md new file mode 100644 index 0000000..17292c1 --- /dev/null +++ b/azure.md @@ -0,0 +1,19 @@ +# Azure OpenAI Serive + +## Request Access + +[Click to apply](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu&culture=en-us&country=us) + +## API Pricing + +[Check](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/) + +## Configure + +- NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME + + + +- NEXT_PUBLIC_AZURE_OPENAI_API_KEY + + diff --git a/azure_CN.md b/azure_CN.md new file mode 100644 index 0000000..24441ff --- /dev/null +++ b/azure_CN.md @@ -0,0 +1,19 @@ +# Azure OpenAI Serive + +## 申请 + +[点击申请](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu&culture=en-us&country=us) + +## API 价格 + +[查看](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/) + +## 配置 + +- NEXT_PUBLIC_AZURE_OPENAI_RESOURCE_NAME + + + +- NEXT_PUBLIC_AZURE_OPENAI_API_KEY + + diff --git a/components/ChatSection/ChatFooter/index.tsx b/components/ChatSection/ChatFooter/index.tsx index c234c90..0bfd419 100644 --- a/components/ChatSection/ChatFooter/index.tsx +++ b/components/ChatSection/ChatFooter/index.tsx @@ -8,14 +8,14 @@ import { AiOutlineRedo, AiOutlineClear } from "react-icons/ai"; import { BsStop } from "react-icons/bs"; import { useDebounceFn } from "ahooks"; import toast from "react-hot-toast"; -import { useChannel, useOpenAI, useProxy, useStreamDecoder } from "@/hooks"; +import { useChannel, useOpenAI, useStreamDecoder } from "@/hooks"; import { useScrollToBottom, Confirm, Button, Textarea } from "@/components"; import { isMobile } from "@/utils"; +import { PROMPT_BASE } from "@/prompt"; const ChatFooter: React.FC = () => { // data - const [openai] = useOpenAI(); - const [proxy] = useProxy(); + const [newOpenAI] = useOpenAI(); const [channel, setChannel] = useChannel(); const [inputValue, setInputValue] = React.useState(""); const setLoadingStart = useChatLoading((state) => state.updateStart); @@ -101,51 +101,67 @@ const ChatFooter: React.FC = () => { const sendToGPT = React.useCallback( (chat_list: ChatItem[]) => { + const modelType: any = findChannel?.channel_model.type; + const modelConfig = (newOpenAI as any)[modelType]; + const prompt = findChannel?.channel_prompt || PROMPT_BASE; + if (!findChannel?.channel_prompt) { + setChannel((channel) => { + const { list, activeId } = channel; + const findCh = list.find((item) => item.channel_id === activeId); + if (!findCh) return channel; + findCh.channel_prompt = PROMPT_BASE; + return channel; + }); + } + setLoadingStart(true); setLoadingFinish(true); const controller = new AbortController(); setChatAbort(controller); - const { openAIKey, azureOpenAIKey, model, temperature, max_tokens } = - openai; + const fetchUrl = `/api/${modelType}`; - let fetchUrl = ""; - let proxyUrl = ""; - let Authorization = ""; - if (model.startsWith("openai")) { - fetchUrl = "/api/openai"; - proxyUrl = proxy.openai; - Authorization = openAIKey; - } else if (model.startsWith("azure")) { - fetchUrl = "/api/azure"; - proxyUrl = proxy.azure; - Authorization = azureOpenAIKey; + let params: any = {}; + if (modelType === "openai") { + params = { + model: findChannel?.channel_model.name, + temperature: modelConfig.temperature, + max_tokens: modelConfig.max_tokens, + prompt, + proxy: modelConfig.proxy, + }; + } else if (modelType === "azure") { + params = { + model: findChannel?.channel_model.name, + temperature: modelConfig.temperature, + max_tokens: modelConfig.max_tokens, + prompt, + resourceName: modelConfig.resourceName, + }; } + params.chat_list = chat_list.map((item) => ({ + role: item.role, + content: item.content, + })); + fetch(fetchUrl, { method: "post", headers: { "Content-Type": "application/json", - Authorization, + Authorization: modelConfig.apiKey, }, signal: controller.signal, - body: JSON.stringify({ - proxyUrl, - model, - temperature, - max_tokens, - chat_list: chat_list.map((item) => ({ - role: item.role, - content: item.content, - })), - }), + body: JSON.stringify(params), }) .then(async (response) => { setLoadingStart(false); + if (!response.ok || !response.body) { setLoadingFinish(false); - if (!response.ok) toast.error(response.statusText); + if (!response.ok) + toast.error(response.statusText || tCommon("service-error")); return; } let channel_id = ""; @@ -156,8 +172,9 @@ const ChatFooter: React.FC = () => { response.body.getReader(), (content: string) => { setChannel((channel) => { - const findChannel = channel.list.find( - (item) => item.channel_id === channel.activeId + const { list, activeId } = channel; + const findChannel = list.find( + (item) => item.channel_id === activeId ); if (!findChannel) return channel; const lastItem = findChannel.chat_list.at(-1); @@ -188,7 +205,7 @@ const ChatFooter: React.FC = () => { // get gpt title if (!channel_name) getChannelNameByGPT(channel_id, channel_chat_list); }) - .catch((err) => { + .catch(() => { setLoadingStart(false); setLoadingFinish(false); }); @@ -206,32 +223,37 @@ const ChatFooter: React.FC = () => { content: tPrompt("get-title"), }); - const { openAIKey, azureOpenAIKey, model } = openai; + const modelType: any = findChannel?.channel_model.type; + const modelConfig = (newOpenAI as any)[modelType]; + + const fetchUrl = `/api/${modelType}`; - let fetchUrl = ""; - let proxyUrl = ""; - let Authorization = ""; - if (model.startsWith("openai")) { - fetchUrl = "/api/openai"; - proxyUrl = proxy.openai; - Authorization = openAIKey; - } else if (model.startsWith("azure")) { - fetchUrl = "/api/azure"; - proxyUrl = proxy.azure; - Authorization = azureOpenAIKey; + let params: any = {}; + if (modelType === "openai") { + params = { + model: findChannel?.channel_model.name, + temperature: modelConfig.temperature, + max_tokens: modelConfig.max_tokens, + proxy: modelConfig.proxy, + }; + } else if (modelType === "azure") { + params = { + model: findChannel?.channel_model.name, + temperature: modelConfig.temperature, + max_tokens: modelConfig.max_tokens, + resourceName: modelConfig.resourceName, + }; } + params.chat_list = chat_list; + fetch(fetchUrl, { method: "post", headers: { "Content-Type": "application/json", - Authorization, + Authorization: modelConfig.apiKey, }, - body: JSON.stringify({ - model, - proxyUrl, - chat_list, - }), + body: JSON.stringify(params), }).then(async (response) => { if (!response.ok || !response.body) return; decoder( @@ -256,8 +278,10 @@ const ChatFooter: React.FC = () => { const { activeId, list } = channel; const findChannel = list.find((item) => item.channel_id === activeId); if (!findChannel) return channel; + findChannel.channel_icon = "RiChatSmile2Line"; findChannel.chat_list = []; findChannel.channel_name = ""; + findChannel.channel_prompt = ""; return channel; }); }; diff --git a/components/ChatSection/ChatList/configure.tsx b/components/ChatSection/ChatList/configure.tsx new file mode 100644 index 0000000..f03289c --- /dev/null +++ b/components/ChatSection/ChatList/configure.tsx @@ -0,0 +1,195 @@ +import * as React from "react"; +import clsx from "clsx"; +import { motion } from "framer-motion"; +import { useTranslation } from "next-i18next"; +import { useChannel } from "@/hooks"; +import { Select } from "@/components"; +import { AI_MODELS } from "@/utils/models"; +import { PROMPT_DEFAULT } from "@/prompt"; +import type { Prompt } from "@/prompt"; + +const renderLabel = (item: any) => { + return ( +
+ {item.ico} + {item.label} +
+ ); +}; + +const Configure: React.FC = () => { + const [channel, setChannel] = useChannel(); + const [isShow, setIsShow] = React.useState(true); + + const { findChannel, options } = React.useMemo(() => { + const findChannel = channel.list.find( + (item) => item.channel_id === channel.activeId + ); + const options = + AI_MODELS.find((item) => item.value === findChannel?.channel_model.type) + ?.models || []; + + return { findChannel, options }; + }, [channel]); + + const { t } = useTranslation("prompt"); + + const onChangeType = (value: string) => { + setChannel((channel) => { + const { list, activeId } = channel; + const nowChannel = list.find((item) => item.channel_id === activeId); + if (!nowChannel) return channel; + nowChannel.channel_model.type = value; + nowChannel.channel_model.name = + AI_MODELS.find((val) => val.value === value)?.models[0].value || ""; + return channel; + }); + }; + + const onChangeModel = (value: string) => { + setChannel((channel) => { + const { list, activeId } = channel; + const nowChannel = list.find((item) => item.channel_id === activeId); + if (!nowChannel) return channel; + nowChannel.channel_model.name = value; + return channel; + }); + }; + + const handlePrompt = (item: Prompt) => { + setChannel((channel) => { + const { list, activeId } = channel; + const findCh = list.find((item) => item.channel_id === activeId); + if (!findCh) return channel; + findCh.channel_icon = item.icon; + findCh.channel_name = item.title; + findCh.channel_prompt = item.content; + return channel; + }); + }; + + React.useEffect(() => { + setIsShow(false); + setTimeout(() => { + setIsShow(true); + }, 100); + }, [channel.activeId]); + + return ( +
+ {isShow && ( + +
+
+ +
+
+
+ +
+
+ {PROMPT_DEFAULT.slice(0, 2).map((item) => ( + handlePrompt(item)} + > + {item.label} + + ))} +
+
+ {PROMPT_DEFAULT.slice(2, 4).map((item) => ( + handlePrompt(item)} + > + {item.label} + + ))} +
+
+ + )} + + {/* {isShow && ( + +
asfasf
+
asfasf
+
asfasf
+
asfasf
+
asfasf
+
+ )} */} + + ); +}; + +export default Configure; diff --git a/components/ChatSection/ChatList/index.tsx b/components/ChatSection/ChatList/index.tsx index 79f26ce..9be35ca 100644 --- a/components/ChatSection/ChatList/index.tsx +++ b/components/ChatSection/ChatList/index.tsx @@ -18,6 +18,7 @@ import { FaUserAlt } from "react-icons/fa"; import { useChannel, useRevoke } from "@/hooks"; import type { ChatItem } from "@/hooks"; import { useChatLoading } from "@/state"; +import Configure from "./configure"; import GPTSvg from "@/assets/gpt.svg"; const ChatList: React.FC = () => { @@ -41,9 +42,11 @@ const ChatList: React.FC = () => { }, ]; - const chatList = - channel.list.find((item) => item.channel_id === channel.activeId) - ?.chat_list || []; + const findChannel = channel.list.find( + (item) => item.channel_id === channel.activeId + ); + + const chatList = findChannel?.chat_list || []; const { set } = useRevoke({ revoke: (value) => onRevoke(value), @@ -98,7 +101,8 @@ const ChatList: React.FC = () => { }, [channel.activeId]); return ( -
+ <> + {!chatList.length && !findChannel?.channel_prompt && }
{chatList.map((item, index) => (
{ )}
-
+ ); }; diff --git a/components/ChatSection/index.tsx b/components/ChatSection/index.tsx index d8ee9f7..e8e0041 100644 --- a/components/ChatSection/index.tsx +++ b/components/ChatSection/index.tsx @@ -7,12 +7,17 @@ import ChatFooter from "./ChatFooter"; const ChatSection: React.FC = () => { const [openai] = useOpenAI(); - // only the OpenAI Key or env OpenAI Key Configuration is not empty, show the chat section - if (!openai.openAIKey && !openai.envOpenAIKey) return null; + if ( + !openai.openai.apiKey && + !openai.azure.apiKey && + !openai.env.OPENAI_API_KEY && + !openai.env.AZURE_API_KEY + ) + return null; return ( { const { t } = useTranslation("menu"); + const { theme, setTheme } = useTheme(); + const { format } = useDateFormat(); + const setOpen = useMobileMenuOpen((state) => state.update); + const [nowTheme, setNowTheme] = React.useState(""); + const [channel, setChannel] = useChannel(); const open = useMobileMenuOpen((state) => state.open); - const setOpen = useMobileMenuOpen((state) => state.update); const setSettingOpen = useSettingOpen((state) => state.update); - const { format } = useDateFormat(); - const onClose = () => setOpen(false); const stopPropagation = (e: any) => e.stopPropagation(); @@ -32,7 +38,13 @@ const MobileMenu: React.FC = () => { setChannel((channel) => { channel.list.push({ channel_id, + channel_icon: "RiChatSmile2Line", channel_name: "", + channel_model: { + type: AI_MODELS[0].value, + name: AI_MODELS[0].models[0].value, + }, + channel_prompt: "", chat_list: [], }); channel.activeId = channel_id; @@ -76,18 +88,32 @@ const MobileMenu: React.FC = () => { }); }; + const onToggleTheme = () => setTheme(nowTheme === "light" ? "dark" : "light"); + + const onOpenPrompt = () => alert("Prompt Manage ToDo..."); + const onSettingOpen = () => setSettingOpen(true); + React.useEffect(() => { + setNowTheme(theme === "dark" ? "dark" : "light"); + }, [theme]); + return ( + + L - GPT + +
+ } width="78%" open={open} onClose={onClose} > -
+
-
+
{channel.list.map((item) => (
{ "dark:text-white/90" )} > - + {renderIcon(item.channel_icon)} {item.channel_name || t("new-conversation")}
{
{item.chat_list.length} {t("messages")} @@ -174,7 +202,7 @@ const MobileMenu: React.FC = () => {
))}
-
+
{ } onOk={onClearChannel} /> - - Github - -
- {t("setting")} +
+
+
+ {nowTheme === "light" ? ( + + ) : ( + + )} +
+
+
+ + + +
+
+
+ +
+
+
+
+ +
+
diff --git a/components/Menu/index.tsx b/components/Menu/index.tsx index 907b6e3..5eaa5e6 100644 --- a/components/Menu/index.tsx +++ b/components/Menu/index.tsx @@ -2,13 +2,15 @@ import * as React from "react"; import clsx from "clsx"; import { twMerge } from "tailwind-merge"; import { useTranslation } from "next-i18next"; +import { useTheme } from "next-themes"; import { AiOutlineDelete, AiFillGithub, AiOutlineVerticalAlignTop, AiOutlineSetting, } from "react-icons/ai"; -import { BsChatSquareText } from "react-icons/bs"; +import { MdOutlineLightMode, MdDarkMode } from "react-icons/md"; +import { HiLightBulb } from "react-icons/hi"; import { useDateFormat } from "l-hooks"; import { v4 as uuidv4 } from "uuid"; import { useChannel, initChannelList } from "@/hooks"; @@ -16,11 +18,15 @@ import type { ChannelListItem } from "@/hooks"; import { Button, Confirm, ContextMenu } from "@/components"; import type { ContextMenuOption } from "@/components"; import { useSettingOpen } from "@/state"; +import { AI_MODELS } from "@/utils/models"; +import renderIcon from "./renderIcon"; const Menu: React.FC = () => { const { t } = useTranslation("menu"); + const { theme, setTheme } = useTheme(); const { format } = useDateFormat(); const setOpen = useSettingOpen((state) => state.update); + const [nowTheme, setNowTheme] = React.useState(""); const [channel, setChannel] = useChannel(); const menuOptions: ContextMenuOption[] = [ @@ -43,7 +49,13 @@ const Menu: React.FC = () => { setChannel((channel) => { channel.list.push({ channel_id, + channel_icon: "RiChatSmile2Line", channel_name: "", + channel_model: { + type: AI_MODELS[0].value, + name: AI_MODELS[0].models[0].value, + }, + channel_prompt: "", chat_list: [], }); channel.activeId = channel_id; @@ -101,8 +113,16 @@ const Menu: React.FC = () => { } }; + const onToggleTheme = () => setTheme(nowTheme === "light" ? "dark" : "light"); + + const onOpenPrompt = () => alert("Prompt Manage ToDo..."); + const onOpenSetting = () => setOpen(true); + React.useEffect(() => { + setNowTheme(theme === "dark" ? "dark" : "light"); + }, [theme]); + return (
{ "dark:bg-slate-800" )} > +
+ + L - GPT + +