diff --git a/README.md b/README.md index 69b649926b5..a42cbd1d4ef 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,11 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 ## What's New -- πŸš€ v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). -- πŸš€ v2.7 let's share conversations as image, or share to ShareGPT! -- πŸš€ v2.8 now we have a client that runs across all platforms! +- πŸš€ v2.10.1 support Google Gemini Pro model. - πŸš€ v2.9.11 you can use azure endpoint now. +- πŸš€ v2.8 now we have a client that runs across all platforms! +- πŸš€ v2.7 let's share conversations as image, or share to ShareGPT! +- πŸš€ v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). ## δΈ»θ¦εŠŸθƒ½ diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index c35e93cb396..6eaa3c97177 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -20,6 +20,7 @@ export class GeminiProApi implements LLMApi { ); } async chat(options: ChatOptions): Promise { + const apiClient = this; const messages = options.messages.map((v) => ({ role: v.role.replace("assistant", "model").replace("system", "user"), parts: [{ text: v.content }], @@ -61,8 +62,7 @@ export class GeminiProApi implements LLMApi { console.log("[Request] google payload: ", requestPayload); - // todo: support stream later - const shouldStream = false; + const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); try { @@ -82,13 +82,21 @@ export class GeminiProApi implements LLMApi { if (shouldStream) { let responseText = ""; let remainText = ""; + let streamChatPath = chatPath.replace( + "generateContent", + "streamGenerateContent", + ); let finished = false; + const finish = () => { + finished = true; + options.onFinish(responseText + remainText); + }; // animate response to make it looks smooth function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + finish(); return; } @@ -105,88 +113,41 @@ export class GeminiProApi implements LLMApi { // start animaion animateResponseText(); + fetch(streamChatPath, chatPayload) + .then((response) => { + const reader = response?.body?.getReader(); + const decoder = new TextDecoder(); + let partialData = ""; + + return reader?.read().then(function processText({ + done, + value, + }): Promise { + if (done) { + console.log("Stream complete"); + // options.onFinish(responseText + remainText); + finished = true; + return Promise.resolve(); + } - const finish = () => { - if (!finished) { - finished = true; - options.onFinish(responseText + remainText); - } - }; + partialData += decoder.decode(value, { stream: true }); - controller.signal.onabort = finish; - - fetchEventSource(chatPath, { - ...chatPayload, - async onopen(res) { - clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log( - "[OpenAI] request response content type: ", - contentType, - ); - - if (contentType?.startsWith("text/plain")) { - responseText = await res.clone().text(); - return finish(); - } - - if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 - ) { - const responseTexts = [responseText]; - let extraInfo = await res.clone().text(); try { - const resJson = await res.clone().json(); - extraInfo = prettyObject(resJson); - } catch {} - - if (res.status === 401) { - responseTexts.push(Locale.Error.Unauthorized); + let data = JSON.parse(ensureProperEnding(partialData)); + console.log(data); + let fetchText = apiClient.extractMessage(data[data.length - 1]); + console.log("[Response Animation] fetchText: ", fetchText); + remainText += fetchText; + } catch (error) { + // skip error message when parsing json } - if (extraInfo) { - responseTexts.push(extraInfo); - } - - responseText = responseTexts.join("\n\n"); - - return finish(); - } - }, - onmessage(msg) { - if (msg.data === "[DONE]" || finished) { - return finish(); - } - const text = msg.data; - try { - const json = JSON.parse(text) as { - choices: Array<{ - delta: { - content: string; - }; - }>; - }; - const delta = json.choices[0]?.delta?.content; - if (delta) { - remainText += delta; - } - } catch (e) { - console.error("[Request] parse error", text); - } - }, - onclose() { - finish(); - }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); + return reader.read().then(processText); + }); + }) + .catch((error) => { + console.error("Error:", error); + }); } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); @@ -220,3 +181,10 @@ export class GeminiProApi implements LLMApi { return "/api/google/" + path; } } + +function ensureProperEnding(str: string) { + if (str.startsWith("[") && !str.endsWith("]")) { + return str + "]"; + } + return str; +} diff --git a/app/layout.tsx b/app/layout.tsx index b234051f943..be2162475d8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,10 @@ import "./styles/markdown.scss"; import "./styles/highlight.scss"; import { getClientConfig } from "./config/client"; import { type Metadata } from "next"; +import { SpeedInsights } from "@vercel/speed-insights/next"; +import { getServerSideConfig } from "./config/server"; + +const serverConfig = getServerSideConfig(); export const metadata: Metadata = { title: "NextChat", @@ -35,7 +39,14 @@ export default function RootLayout({ - {children} + + {children} + {serverConfig?.isVercel && ( + <> + + + )} + ); } diff --git a/app/page.tsx b/app/page.tsx index 20b503174d4..b3f169a9b74 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,7 +10,11 @@ export default async function App() { return ( <> - {serverConfig?.isVercel && } + {serverConfig?.isVercel && ( + <> + + + )} ); } diff --git a/app/store/chat.ts b/app/store/chat.ts index 4af5a52acf9..dff6b7bf1c6 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -386,7 +386,9 @@ export const useChatStore = createPersistStore( const contextPrompts = session.mask.context.slice(); // system prompts, to get close to OpenAI Web ChatGPT - const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; + const shouldInjectSystemPrompts = + modelConfig.enableInjectSystemPrompts && + session.mask.modelConfig.model.startsWith("gpt-"); var systemPrompts: ChatMessage[] = []; systemPrompts = shouldInjectSystemPrompts diff --git a/package.json b/package.json index 102461b314f..a014c7bfe14 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@hello-pangea/dnd": "^16.5.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", + "@vercel/speed-insights": "^1.0.2", "emoji-picker-react": "^4.5.15", "fuse.js": "^7.0.0", "html-to-image": "^1.11.11", diff --git a/yarn.lock b/yarn.lock index 5469672db2b..bf07c27eea1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1704,6 +1704,11 @@ resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-0.1.11.tgz#727a0ac655a4a89104cdea3e6925476470299428" integrity sha512-mj5CPR02y0BRs1tN3oZcBNAX9a8NxsIUl9vElDPcqxnMfP0RbRc9fI9Ud7+QDg/1Izvt5uMumsr+6YsmVHcyuw== +"@vercel/speed-insights@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vercel/speed-insights/-/speed-insights-1.0.2.tgz#1bebf3e7c7046b6a911721233b263b69214ddb3e" + integrity sha512-y5HWeB6RmlyVYxJAMrjiDEz8qAIy2cit0fhBq+MD78WaUwQvuBnQlX4+5MuwVTWi46bV3klaRMq83u9zUy1KOg== + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"