diff --git a/README.md b/README.md index d496d68edfc..30ccd714379 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# attention please + +This project is forked from [ChatGPT-Next-Web](https://github.com/Yidadaa/ChatGPT-Next-Web), and changed somewhere as personal like, thanks to [Yidadaa](https://github.com/Yidadaa) and some other developers' hard working and finally I have my own ChatGPT assistant now. Also, I should thank [Vercel](https://vercel.com/), Vercel is the platform for frontend developers, providing the speed and reliability innovators need to create at the moment of inspiration. Thanks again. +
icon diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index 4264893d93e..4b1d8cb3ae4 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -106,12 +106,9 @@ async function request(req: NextRequest) { console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); - const timeoutId = setTimeout( - () => { - controller.abort(); - }, - 10 * 60 * 1000, - ); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 10 * 60 * 1000); const fetchUrl = `${baseUrl}${path}`; diff --git a/app/api/common.ts b/app/api/common.ts index a75f2de5cfa..2265a9575ec 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -44,12 +44,9 @@ export async function requestOpenai(req: NextRequest) { console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); - const timeoutId = setTimeout( - () => { - controller.abort(); - }, - 10 * 60 * 1000, - ); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 10 * 60 * 1000); if (serverConfig.isAzure) { if (!serverConfig.azureApiVersion) { @@ -112,16 +109,16 @@ export async function requestOpenai(req: NextRequest) { try { const res = await fetch(fetchUrl, fetchOptions); - // Extract the OpenAI-Organization header from the response - const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); + // Extract the OpenAI-Organization header from the response + const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); - // Check if serverConfig.openaiOrgId is defined and not an empty string - if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { - // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present - console.log("[Org ID]", openaiOrganizationHeader); - } else { - console.log("[Org ID] is not set up."); - } + // Check if serverConfig.openaiOrgId is defined and not an empty string + if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { + // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present + console.log("[Org ID]", openaiOrganizationHeader); + } else { + console.log("[Org ID] is not set up."); + } // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); @@ -129,7 +126,6 @@ export async function requestOpenai(req: NextRequest) { // to disable nginx buffering newHeaders.set("X-Accel-Buffering", "no"); - // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV) // Also, this is to prevent the header from being sent to the client if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { @@ -142,7 +138,6 @@ export async function requestOpenai(req: NextRequest) { // The browser will try to decode the response with brotli and fail newHeaders.delete("content-encoding"); - return new Response(res.body, { status: res.status, statusText: res.statusText, diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index ebd19289129..ffaa3dcaba6 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -32,12 +32,9 @@ async function handle( console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); - const timeoutId = setTimeout( - () => { - controller.abort(); - }, - 10 * 60 * 1000, - ); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 10 * 60 * 1000); const authResult = auth(req, ModelProvider.GeminiPro); if (authResult.error) { diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 816c2046b22..5675f425d6b 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -24,8 +24,8 @@ async function handle( // Validate the endpoint to prevent potential SSRF attacks if ( - !mergedAllowedWebDavEndpoints.some( - (allowedEndpoint) => endpoint?.startsWith(allowedEndpoint), + !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => + endpoint?.startsWith(allowedEndpoint), ) ) { return NextResponse.json( diff --git a/app/client/api.ts b/app/client/api.ts index 7bee546b4f6..b1ac27b936f 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -12,7 +12,7 @@ import { ClaudeApi } from "./platforms/anthropic"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; -export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; +export const Models = ["gpt-3.5-turbo"] as const; export type ChatModel = ModelType; export interface MultimodalContent { diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index a786f5275f4..76006fd6729 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -120,7 +120,9 @@ export class GeminiProApi implements LLMApi { if (!baseUrl) { baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/google/" + Google.ChatPath(modelConfig.model) + ? DEFAULT_API_HOST + + "/api/proxy/google/" + + Google.ChatPath(modelConfig.model) : this.path(Google.ChatPath(modelConfig.model)); } @@ -139,7 +141,7 @@ export class GeminiProApi implements LLMApi { () => controller.abort(), REQUEST_TIMEOUT_MS, ); - + if (shouldStream) { let responseText = ""; let remainText = ""; @@ -182,59 +184,58 @@ export class GeminiProApi implements LLMApi { const decoder = new TextDecoder(); let partialData = ""; - return reader?.read().then(function processText({ - done, - value, - }): Promise { - if (done) { - if (response.status !== 200) { - try { - let data = JSON.parse(ensureProperEnding(partialData)); - if (data && data[0].error) { - options.onError?.(new Error(data[0].error.message)); - } else { + return reader + ?.read() + .then(function processText({ done, value }): Promise { + if (done) { + if (response.status !== 200) { + try { + let data = JSON.parse(ensureProperEnding(partialData)); + if (data && data[0].error) { + options.onError?.(new Error(data[0].error.message)); + } else { + options.onError?.(new Error("Request failed")); + } + } catch (_) { options.onError?.(new Error("Request failed")); } - } catch (_) { - options.onError?.(new Error("Request failed")); } - } - console.log("Stream complete"); - // options.onFinish(responseText + remainText); - finished = true; - return Promise.resolve(); - } + console.log("Stream complete"); + // options.onFinish(responseText + remainText); + finished = true; + return Promise.resolve(); + } - partialData += decoder.decode(value, { stream: true }); + partialData += decoder.decode(value, { stream: true }); - try { - let data = JSON.parse(ensureProperEnding(partialData)); + try { + let data = JSON.parse(ensureProperEnding(partialData)); - const textArray = data.reduce( - (acc: string[], item: { candidates: any[] }) => { - const texts = item.candidates.map((candidate) => - candidate.content.parts - .map((part: { text: any }) => part.text) - .join(""), - ); - return acc.concat(texts); - }, - [], - ); + const textArray = data.reduce( + (acc: string[], item: { candidates: any[] }) => { + const texts = item.candidates.map((candidate) => + candidate.content.parts + .map((part: { text: any }) => part.text) + .join(""), + ); + return acc.concat(texts); + }, + [], + ); - if (textArray.length > existingTexts.length) { - const deltaArray = textArray.slice(existingTexts.length); - existingTexts = textArray; - remainText += deltaArray.join(""); + if (textArray.length > existingTexts.length) { + const deltaArray = textArray.slice(existingTexts.length); + existingTexts = textArray; + remainText += deltaArray.join(""); + } + } catch (error) { + // console.log("[Response Animation] error: ", error,partialData); + // skip error message when parsing json } - } catch (error) { - // console.log("[Response Animation] error: ", error,partialData); - // skip error message when parsing json - } - return reader.read().then(processText); - }); + return reader.read().then(processText); + }); }) .catch((error) => { console.error("Error:", error); diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 06119250465..132f216c168 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1461,6 +1461,13 @@ function _Chat() { : message.date.toLocaleString()}
+ {!isUser && !message.preview && ( +
+
+ {message.date.toLocaleString()} +
+
+ )} {shouldShowClearContextDivider && } diff --git a/app/components/home.module.scss b/app/components/home.module.scss index b836d2bec93..145f44ab8fd 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -1,20 +1,20 @@ @mixin container { - background-color: var(--white); border: var(--border-in-light); - border-radius: 20px; + border-radius: 4px; box-shadow: var(--shadow); color: var(--black); - background-color: var(--white); - min-width: 600px; - min-height: 370px; - max-width: 1200px; - + background: #c7edcc; //rgb(199,237,204);//#7c9d42;// var(--white); + min-width: 800px; + min-height: 600px; display: flex; overflow: hidden; box-sizing: border-box; - width: var(--window-width); height: var(--window-height); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; } .container { @@ -26,12 +26,9 @@ --window-width: 100vw; --window-height: var(--full-height); --window-content-width: calc(100% - var(--sidebar-width)); - @include container(); - max-width: 100vw; max-height: var(--full-height); - border-radius: 0; border: 0; } @@ -42,7 +39,7 @@ width: var(--sidebar-width); box-sizing: border-box; padding: 20px; - background-color: var(--second); + background: var(--second); display: flex; flex-direction: column; box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05); @@ -326,6 +323,240 @@ margin-right: 15px; } +.chat { + display: flex; + flex-direction: column; + position: relative; + height: 100%; +} + +.chat-body { + flex: 1; + overflow-x: hidden; + overflow-y: scroll; + padding: 20px; + position: relative; + overscroll-behavior: none; +} + +.chat-body-title { + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.chat-message { + display: flex; + flex-direction: row; + + &:last-child { + animation: slide-in ease 0.3s; + } +} + +.chat-message-user { + display: flex; + flex-direction: row-reverse; + position: relative; +} + +.chat-message-container { + max-width: var(--message-max-width); + display: flex; + flex-direction: column; + align-items: flex-start; + + &:hover { + .chat-message-top-actions { + opacity: 1; + transform: translateX(10px); + pointer-events: all; + } + } +} + +.chat-message-avatar { + margin-top: 20px; +} + +.chat-message-status { + font-size: 12px; + color: #aaa; + line-height: 1.5; + margin-top: 5px; +} + +.chat-message-item { + box-sizing: border-box; + max-width: 100%; + margin-top: 10px; + border-radius: 10px; + background-color: rgba(0, 0, 0, 0.05); + padding: 10px; + font-size: 14px; + user-select: text; + word-break: break-word; + border: var(--border-in-light); + position: relative; +} + +.chat-message-top-actions { + min-width: 120px; + font-size: 12px; + position: absolute; + right: 20px; + top: -26px; + left: 30px; + transition: all ease 0.3s; + opacity: 0; + pointer-events: none; + + display: flex; + flex-direction: row-reverse; + + .chat-message-top-action { + opacity: 0.5; + color: var(--black); + white-space: nowrap; + cursor: pointer; + + &:hover { + opacity: 1; + } + + &:not(:first-child) { + margin-right: 10px; + } + } +} + +.chat-message-user > .chat-message-container > .chat-message-item { + background-color: var(--second); +} + +.chat-message-actions { + display: flex; + flex-direction: row-reverse; + width: 100%; + padding-top: 5px; + box-sizing: border-box; + font-size: 12px; +} + +.chat-message-action-date { + color: #aaa; +} + +.chat-input-panel { + position: relative; + width: 100%; + padding: 20px; + padding-top: 10px; + box-sizing: border-box; + flex-direction: column; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border-top: var(--border-in-light); + box-shadow: var(--card-shadow); +} + +@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 { + display: flex; + flex: 1; +} + +.chat-input { + height: 100%; + width: 100%; + border-radius: 10px; + border: var(--border-in-light); + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03); + background-color: var(--white); + color: var(--black); + font-family: inherit; + padding: 10px 90px 10px 14px; + resize: none; + outline: none; +} + +.chat-input:focus { + border: 1px solid var(--primary); +} + +.chat-input-send { + background-color: var(--primary); + color: white; + + position: absolute; + right: 30px; + bottom: 32px; +} + +@media only screen and (max-width: 600px) { + .chat-input { + font-size: 16px; + } + + .chat-input-send { + bottom: 30px; + } +} + .loading-content { display: flex; flex-direction: column; diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index 54c646f237c..12304465e1b 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -20,11 +20,21 @@ import { BUILTIN_MASK_STORE } from "../masks"; function MaskItem(props: { mask: Mask; onClick?: () => void }) { return (
+ + {/* +
+ {props.mask.name} +
*/} +
{props.mask.name}
+
); } diff --git a/app/constant.ts b/app/constant.ts index 411e481508d..bb9eaf18b30 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -1,5 +1,5 @@ -export const OWNER = "Yidadaa"; -export const REPO = "ChatGPT-Next-Web"; +export const OWNER = "EricYangXD"; +export const REPO = "ChatGPT-Next"; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; export const UPDATE_URL = `${REPO_URL}#keep-updated`; diff --git a/app/global.d.ts b/app/global.d.ts index 31e2b6e8a84..e2daf9ba9f3 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -21,7 +21,7 @@ declare interface Window { writeBinaryFile(path: string, data: Uint8Array): Promise; writeTextFile(path: string, data: string): Promise; }; - notification:{ + notification: { requestPermission(): Promise; isPermissionGranted(): Promise; sendNotification(options: string | Options): void; diff --git a/app/layout.tsx b/app/layout.tsx index 5898b21a1fa..8a10ee2b0a3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,7 +36,10 @@ export default function RootLayout({ - + diff --git a/app/masks/index.ts b/app/masks/index.ts index aa4917e3e3c..c17f925e826 100644 --- a/app/masks/index.ts +++ b/app/masks/index.ts @@ -22,6 +22,8 @@ export const BUILTIN_MASK_STORE = { }, }; -export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...TW_MASKS, ...EN_MASKS].map( - (m) => BUILTIN_MASK_STORE.add(m), -); +export const BUILTIN_MASKS: BuiltinMask[] = [ + ...CN_MASKS, + ...TW_MASKS, + ...EN_MASKS, +].map((m) => BUILTIN_MASK_STORE.add(m)); diff --git a/app/store/config.ts b/app/store/config.ts index 94cfcd8ecaa..ec73943d310 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -7,6 +7,7 @@ import { StoreKey, } from "../constant"; import { createPersistStore } from "../utils/store"; +import { isMacOS } from "../utils"; export type ModelType = (typeof DEFAULT_MODELS)[number]["name"]; @@ -27,9 +28,9 @@ export enum Theme { const config = getClientConfig(); export const DEFAULT_CONFIG = { + // submitKey: SubmitKey.MetaEnter as SubmitKey, lastUpdate: Date.now(), // timestamp, to merge state - - submitKey: SubmitKey.Enter, + submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter, avatar: "1f603", fontSize: 14, theme: Theme.Auto as Theme, diff --git a/app/store/sync.ts b/app/store/sync.ts index d3582e3c935..77f7b9cddf5 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -100,15 +100,17 @@ export const useSyncStore = createPersistStore( const remoteState = await client.get(config.username); if (!remoteState || remoteState === "") { await client.set(config.username, JSON.stringify(localState)); - console.log("[Sync] Remote state is empty, using local state instead."); - return + console.log( + "[Sync] Remote state is empty, using local state instead.", + ); + return; } else { const parsedRemoteState = JSON.parse( await client.get(config.username), ) as AppState; mergeAppState(localState, parsedRemoteState); setLocalAppState(localState); - } + } } catch (e) { console.log("[Sync] failed to get remote state", e); throw e;