diff --git a/apps/extension/assets/@.svg b/apps/extension/assets/@.svg deleted file mode 100644 index 6444a3d..0000000 --- a/apps/extension/assets/@.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/extension/src/components/Chat/Chat.tsx b/apps/extension/src/components/Chat/Chat.tsx index 2ea98c8..b0d8b1e 100644 --- a/apps/extension/src/components/Chat/Chat.tsx +++ b/apps/extension/src/components/Chat/Chat.tsx @@ -1,173 +1,227 @@ -import { useChat } from "@opengpts/core/@react" -import React, { useEffect, useImperativeHandle, useMemo, useRef, useState, type RefObject, forwardRef } from "react" -import { ChatInputArea } from "./ChatInputArea" -import { nanoid } from '@opengpts/core/shared/utils' -import { motion } from "framer-motion" -import useUserSelection from "~src/hooks/useUserSelection" -import _ from "lodash" +import { useChat } from "@opengpts/core/@react"; +import React, { useEffect, useImperativeHandle, useMemo, useRef, useState, type RefObject, forwardRef } from "react"; +import { ChatInputArea } from "./ChatInputArea"; +import logoIcon from "data-base64:~assets/icon.png" +import { nanoid } from "@opengpts/core/shared/utils"; +import { motion } from "framer-motion"; +import useUserSelection from "~src/hooks/useUserSelection"; +import _ from "lodash"; // import InteractivePanel from "../InteractivePanel" -import { webSearch, type SearchRequest } from "~src/contents/web-search" -import { PauseCircleOutlined } from "@ant-design/icons" -import { Button } from "antd" -import type { ChatRequest, FunctionCallHandler } from 'ai'; -import type { OMessage, Session } from "@opengpts/types" -import { MessagesList } from "../Message/MessageList" -import type { MessagesListMethods } from "../Message/MessageList" -import { useChatStore } from "~src/store/useChatStore" -import { ofetch } from "ofetch" -import useChatQuoteStore from "~src/store/useChatQuoteStore" -import { useChatPanelContext } from "../Panel/ChatPanel" -import useScreenCapture from "~src/store/useScreenCapture" -import { useTranslation } from "react-i18next" -import { useStorage } from "@plasmohq/storage/hook" -import { Storage } from "@plasmohq/storage" -import { MODELS_DICT } from "~src/constant" -import { OpenAI } from "@opengpts/core" -import { useDebouncedCallback } from "use-debounce" +import { webSearch, type SearchRequest } from "~src/contents/web-search"; +import { PauseCircleOutlined } from "@ant-design/icons"; +import { Button } from "antd"; +import type { ChatRequest, FunctionCallHandler } from "ai"; +import type { OMessage, Session } from "@opengpts/types"; +import { MessagesList } from "../Message/MessageList"; +import type { MessagesListMethods } from "../Message/MessageList"; +import { useChatStore } from "~src/store/useChatStore"; +import { ofetch } from "ofetch"; +import useChatQuoteStore from "~src/store/useChatQuoteStore"; +import { useChatPanelContext } from "../Panel/ChatPanel"; +import useScreenCapture from "~src/store/useScreenCapture"; +import { useTranslation } from "react-i18next"; +import { useStorage } from "@plasmohq/storage/hook"; +import { Storage } from "@plasmohq/storage"; +import { MODELS_DICT } from "~src/constant"; +import { OpenAI } from "@opengpts/core"; +import { useDebouncedCallback } from "use-debounce"; export type ChatProps = { - ref: RefObject - uiMessages?: any[], - systemMessage?: string, - children?: any, - className?: string, + ref: RefObject; + uiMessages?: any[]; + systemMessage?: string; + children?: any; + className?: string; }; -export type ChatRef = { - -} - +export type ChatRef = {}; const apiMapping = { - "get_current_weather": { + get_current_weather: { url: "http://localhost:1337/api/fn-api/get_current_weather", }, - "dalle3": { + dalle3: { url: "http://localhost:1337/api/fn-api/dalle3", - } + }, }; - -export const Chat = forwardRef(({ uiMessages = [], systemMessage = "你好有什么我可以帮助你的么?", children = '', className = '' }, ref) => { - const [content, setContent] = useState("") - const { mention, setMention, command, setCommand, setModel, chatId, setChatId, model, webAccess, setFileList } = useChatPanelContext() - const [hideInputArea, setHideInputArea] = useState(false) - const messagesListRef = useRef(null); - const inputRef = useRef(null); - - const { selection } = useUserSelection() - const { resetCapture } = useScreenCapture() - const [setCloseSelectionTextPanel] = useState(true) - const { t } = useTranslation() - const checkChatExist = useChatStore(state => state.checkChatExist) - const getChatMessages = useChatStore(state => state.getChatMessages) - const addChatMessage = useChatStore(state => state.addChatMessage) - const getQuoteMessage = useChatQuoteStore(state => state.getQuote) - const addChatIfNotExist = useChatStore(state => state.addChatIfNotExist) - const [chatgptConfig] = useStorage({ - key: "chatgpt-config", - instance: new Storage({ - area: "local" - }) - }) - const functionCallHandler: FunctionCallHandler = async ( - chatMessages, - functionCall - ) => { - console.log( - "正在调用插件", - functionCall.name, - "参数为:", - functionCall.arguments - ) - console.log('chatMessages', chatMessages) - // according to functionCall.name to call api - // TODO:fetch apiMapping from server - const apiInfo = apiMapping[functionCall.name as string]; - let message: OMessage; - const functionCallMessage = chatMessages.find((message) => { - const fnCall = message.function_call - return typeof fnCall === 'object' && fnCall && fnCall.name! === functionCall.name - }) - - if (!apiInfo) { - return { - messages: [ - ...chatMessages, - { - id: functionCallMessage?.id || nanoid(), - name: functionCall.name, - role: "function" as const, - isError: true, - content: JSON.stringify({ - error: '未找到该函数' - }) - } - ] +export const Chat = forwardRef( + ({ uiMessages = [], systemMessage = "你好有什么我可以帮助你的么?", children = "", className = "" }, ref) => { + const [content, setContent] = useState(""); + const { mention, setMention, command, setCommand, setModel, chatId, setChatId, model, webAccess, setFileList } = + useChatPanelContext(); + const [hideInputArea, setHideInputArea] = useState(false); + const messagesListRef = useRef(null); + const inputRef = useRef(null); + + const { selection } = useUserSelection(); + const { resetCapture } = useScreenCapture(); + const [setCloseSelectionTextPanel] = useState(true); + const { t } = useTranslation(); + const checkChatExist = useChatStore((state) => state.checkChatExist); + const getChatMessages = useChatStore((state) => state.getChatMessages); + const addChatMessage = useChatStore((state) => state.addChatMessage); + const getQuoteMessage = useChatQuoteStore((state) => state.getQuote); + const addChatIfNotExist = useChatStore((state) => state.addChatIfNotExist); + const [chatgptConfig] = useStorage({ + key: "chatgpt-config", + instance: new Storage({ + area: "local", + }), + }); + const functionCallHandler: FunctionCallHandler = async (chatMessages, functionCall) => { + console.log("正在调用插件", functionCall.name, "参数为:", functionCall.arguments); + console.log("chatMessages", chatMessages); + // according to functionCall.name to call api + // TODO:fetch apiMapping from server + const apiInfo = apiMapping[functionCall.name as string]; + let message: OMessage; + const functionCallMessage = chatMessages.find((message) => { + const fnCall = message.function_call; + return typeof fnCall === "object" && fnCall && fnCall.name! === functionCall.name; + }); + + if (!apiInfo) { + return { + messages: [ + ...chatMessages, + { + id: functionCallMessage?.id || nanoid(), + name: functionCall.name, + role: "function" as const, + isError: true, + content: JSON.stringify({ + error: "未找到该函数", + }), + }, + ], + }; } - } - try { - const content = await ofetch(apiInfo.url, { - method: "POST", - body: JSON.stringify({ args: functionCall.arguments }), - timeout: 30000 - }) - console.log('content', content) - message = { - id: functionCallMessage?.id || nanoid(), - name: functionCall.name, - role: "function" as const, - content: content - } - - } catch (error) { - console.error("Error processing API request:", error); - // return handleApiFunctionError(functionCall, error); - message = { - id: functionCallMessage?.id || nanoid(), - name: functionCall.name, - role: "function" as const, - isError: true, - content: JSON.stringify({ - error: error.message - }) + try { + const content = await ofetch(apiInfo.url, { + method: "POST", + body: JSON.stringify({ args: functionCall.arguments }), + timeout: 30000, + }); + console.log("content", content); + message = { + id: functionCallMessage?.id || nanoid(), + name: functionCall.name, + role: "function" as const, + content: content, + }; + } catch (error) { + console.error("Error processing API request:", error); + // return handleApiFunctionError(functionCall, error); + message = { + id: functionCallMessage?.id || nanoid(), + name: functionCall.name, + role: "function" as const, + isError: true, + content: JSON.stringify({ + error: error.message, + }), + }; } - } - const functionResponse: ChatRequest = { - messages: [ - ...chatMessages, - message - ] - } - console.log('functionResponse', functionResponse) - return functionResponse - } - - - const { webConfig, setWebConfig, mode, setMode, input, isLoading, stop, append, messages, setMessages } = - useChat({ - initMode: 'web', + const functionResponse: ChatRequest = { + messages: [...chatMessages, message], + }; + console.log("functionResponse", functionResponse); + return functionResponse; + }; + + const { webConfig, setWebConfig, mode, setMode, input, isLoading, stop, append, messages, setMessages } = useChat({ + initMode: "web", api: "http://127.0.0.1:1337/api/chat", experimental_onFunctionCall: functionCallHandler, credentials: "omit", initialMessages: [], initialInput: "", - onError: (error) => { - console.log(error) + onError: (error, updateMessage) => { + if (error.message === "Request timed out") { + updateMessage({ + content: ( + + + {t("NetworkApplication")} + + ({t("NetworkUnstableRequestFailed")}) + + + {t("DueToOpenAILimitation")} + + ), + }); + } else if (error.message === "noChatGPTPlusArkoseToken") { + updateMessage({ + content: ( + <> + + + {t("NetworkApplication")} + + ({t("GPT4AndGPTsCallFailed")}) + + + {t("VisitChatOpenAIPage")} + + > + ), + }); + } else if (error.message == "chatGPT403") { + updateMessage({ + content: ( + + + {t("NetworkApplication")} + + ( {t("ChatGPTPlusUsersCanTry")}) + + + {t("DueToOpenAILimitation")} + + ), + }); + } else if (error.message == "chatGPT429") { + updateMessage({ + content: ( + + + {t("NetworkApplication")} + + ({t("OpenAIRestrictedYourRequestFrequency")}) + + + {t("CheckLimitOverage")} + + ), + }); + } else if (error.message == "chatGPT400") { + updateMessage({ + content: ( + + + {t("NetworkApplication")} + + ({t("OpenAIRequestFailed")}) + + + {t("GPTsUnavailableOrDeleted")} + + ), + }); + } + console.log("useChat", error); // setError(error.message) // alert(error.message) }, onResponse: (response) => { - handleScrollToBottom() + handleScrollToBottom(); }, - onFinish: async ( - message: OMessage, - session?: Session, - conversation?: OpenAI['conversation'] - ) => { + onFinish: async (message: OMessage, session?: Session, conversation?: OpenAI["conversation"]) => { // clear mentions - setMention(undefined) + setMention(undefined); if (!session) return; - const chatMessages = getChatMessages(chatId) + const chatMessages = getChatMessages(chatId); const chat = { chatId: session.conversationId, title: chatMessages[0].content, @@ -178,27 +232,27 @@ export const Chat = forwardRef(({ uiMessages = [], systemMes message: { // id: session.messageId, ...message, - } + }, }, - fileList: [] - } - if (mode === 'web') { - const newChatId = session.conversationId! + fileList: [], + }; + if (mode === "web") { + const newChatId = session.conversationId!; if (!checkChatExist(chatId)) { - const initialTitle = chatMessages[0].content.slice(0, 30) - chat['chatId'] = newChatId + if (typeof chatMessages[0].content !== "string") return; + const initialTitle = chatMessages[0].content.slice(0, 30); + chat["chatId"] = newChatId; setChatId(newChatId); - addChatIfNotExist(chat) - addChatMessage(newChatId, chatMessages[0]) - addChatMessage(newChatId, message) - if (conversation) conversation.updateTitle(newChatId, initialTitle) - } - else { - addChatMessage(newChatId, message) + addChatIfNotExist(chat); + addChatMessage(newChatId, chatMessages[0]); + addChatMessage(newChatId, message); + if (conversation) conversation.updateTitle(newChatId, initialTitle); + } else { + addChatMessage(newChatId, message); } } else { - addChatIfNotExist(chat) - addChatMessage(chatId, message) + addChatIfNotExist(chat); + addChatMessage(chatId, message); } }, body: { @@ -206,219 +260,237 @@ export const Chat = forwardRef(({ uiMessages = [], systemMes }, sendExtraMessageFields: true, initialWebConfig: { - ...chatgptConfig - } - }) - - const handleSubmit = async ({ content }) => { - if (!content) return; - let webSearchPrompt = '' - //TODO: use model that was mentioned last, if not exist, use default model, temporarily - const mentionType = _.get(mention, 'type', '') - const modelKey = mentionType === 'GPTs' ? 'gpt-4-gizmo' : mention?.key ?? model.key - const quoteMessage = getQuoteMessage(chatId) - const capturedImage = useScreenCapture.getState().capturedImage - - const message: OMessage = { - id: nanoid(), - content: `${selection} - ${content}`, - role: "user", - display: { - name: 'Me', - icon: '', + ...chatgptConfig, }, - quoteMessage: quoteMessage, - images: capturedImage ? [capturedImage] : [], - command, - ui: quoteMessage?.content, - } - setMessages([...messages, message]) - if (webAccess) { - try { - const searchRequest: SearchRequest = { - query: content, - timerange: '', - region: '', - } - const webSearchResults = await webSearch(searchRequest, 3) - - //merge webSearchResults - console.log('webSearchResults', webSearchResults) - webSearchPrompt = '# 搜索到一些相关的内容:' + webSearchResults.map((result) => { - return ` + }); + + const handleSubmit = async ({ content }) => { + if (!content) return; + let webSearchPrompt = ""; + //TODO: use model that was mentioned last, if not exist, use default model, temporarily + const mentionType = _.get(mention, "type", ""); + const modelKey = mentionType === "GPTs" ? "gpt-4-gizmo" : mention?.key ?? model.key; + const quoteMessage = getQuoteMessage(chatId); + const capturedImage = useScreenCapture.getState().capturedImage; + + const message: OMessage = { + id: nanoid(), + content: `${selection} + ${content}`, + role: "user", + display: { + name: "Me", + icon: "", + }, + quoteMessage: quoteMessage, + images: capturedImage ? [capturedImage] : [], + command, + ui: quoteMessage?.content, + }; + setMessages([...messages, message]); + if (webAccess) { + try { + const searchRequest: SearchRequest = { + query: content, + timerange: "", + region: "", + }; + const webSearchResults = await webSearch(searchRequest, 3); + + //merge webSearchResults + console.log("webSearchResults", webSearchResults); + webSearchPrompt = + "# 搜索到一些相关的内容:" + + webSearchResults.map((result) => { + return ` ### ${result.title} > ${result.body} [查看更多](${result.url}) - ` - }); - } catch (error) { - console.error(`webSearch error:${error}`) + `; + }); + } catch (error) { + console.error(`webSearch error:${error}`); + } } - } - - addChatMessage(chatId, message) - setFileList([]) - - const options = {} - - function handleGPTsOptions(mention) { - const gizmoId = _.get(mention, 'key', ''); - if (!gizmoId) throw new Error('gizmo_id is required'); - return { gizmoId, ...webConfig }; - } - - // Helper function to handle web mode options - function handleWebModeOptions(chatId) { - return checkChatExist(chatId) ? { conversationId: chatId } : {}; - } - // Helper function to handle default options - function handleDefaultOptions(modelKey, capturedImage) { - const modelName = MODELS_DICT[modelKey].value; - return { model: modelName, imgUrl: capturedImage }; - } - let bodyOptions = {}; + addChatMessage(chatId, message); + setFileList([]); - if (mention && mentionType === 'GPTs') { - bodyOptions = handleGPTsOptions(mention); - } else { - bodyOptions = model.mode === 'web' ? - handleWebModeOptions(chatId) : - handleDefaultOptions(model.key, useScreenCapture.getState().capturedImage); - } + const options = {}; - options['body'] = { modelName: modelKey, ...bodyOptions }; - - append({ - ...message, - content: `${selection} - ${webSearchPrompt} - ${content}` - }, { - options, - }, { - mention: mention ?? { - name: model.name, - icon: model.icon, + function handleGPTsOptions(mention) { + const gizmoId = _.get(mention, "key", ""); + if (!gizmoId) throw new Error("gizmo_id is required"); + return { gizmoId, ...webConfig }; } - }) - - setContent('') - setMention(undefined) - setCommand(undefined) - handleScrollToBottom() - resetCapture() - } - - const handleScrollToBottom = useDebouncedCallback(() => { - if (messagesListRef.current) { - messagesListRef.current.scrollToBottom(); - } - }, 300) + // Helper function to handle web mode options + function handleWebModeOptions(chatId) { + return checkChatExist(chatId) ? { conversationId: chatId } : {}; + } + // Helper function to handle default options + function handleDefaultOptions(modelKey, capturedImage) { + const modelName = MODELS_DICT[modelKey].value; + return { model: modelName, imgUrl: capturedImage }; + } + let bodyOptions = {}; + + if (mention && mentionType === "GPTs") { + bodyOptions = handleGPTsOptions(mention); + } else { + bodyOptions = + model.mode === "web" + ? handleWebModeOptions(chatId) + : handleDefaultOptions(model.key, useScreenCapture.getState().capturedImage); + } - const onInputChange = (v: string) => { - inputRef.current.setContent(v) - setContent(v) - } - - const handleHideInputArea = () => { - - } + options["body"] = { modelName: modelKey, ...bodyOptions }; - useImperativeHandle(ref, () => { - return { - handleSubmit: () => inputRef.current.submit(), - handleScrollToBottom: handleScrollToBottom, - onInputChange: onInputChange, - setHideInputArea: setHideInputArea, - } - }) + append( + { + ...message, + content: `${selection} + ${webSearchPrompt} + ${content}`, + }, + { + options, + }, + { + mention: mention ?? { + name: model.name, + icon: model.icon, + }, + } + ); - useEffect(() => { - console.log('selection', selection) - if (selection.length > 0) { - // setCloseSelectionTextPanel(false) - } - }, [selection]) - useEffect(() => { - if (isLoading) { + setContent(""); + setMention(undefined); + setCommand(undefined); handleScrollToBottom(); - } - }, [messages]) + resetCapture(); + }; - useEffect(() => { - // const model = MODELS.find(({ name }) => name === model.key) - console.log('设置模型为', model?.key) - if (model) { - setMode(model.mode) - } - }, [model]) + const handleScrollToBottom = useDebouncedCallback(() => { + if (messagesListRef.current) { + messagesListRef.current.scrollToBottom(); + } + }, 300); - useEffect(() => { - const messages = getChatMessages(chatId) - console.log('messages', messages) - setMessages(messages) - }, [chatId]) + const onInputChange = (v: string) => { + inputRef.current.setContent(v); + setContent(v); + }; - useEffect(() => { - console.log('chatgptConfig', chatgptConfig) - setWebConfig({ ...chatgptConfig }) - }, [chatgptConfig]) + const handleHideInputArea = () => { }; + useImperativeHandle(ref, () => { + return { + handleSubmit: () => inputRef.current.submit(), + handleScrollToBottom: handleScrollToBottom, + onInputChange: onInputChange, + setHideInputArea: setHideInputArea, + }; + }); + + useEffect(() => { + console.log("selection", selection); + if (selection.length > 0) { + // setCloseSelectionTextPanel(false) + } + }, [selection]); + useEffect(() => { + if (isLoading) { + handleScrollToBottom(); + } + }, [messages]); - return ( - <> - - - {/* { + // const model = MODELS.find(({ name }) => name === model.key) + console.log("设置模型为", model?.key); + if (model) { + setMode(model.mode); + } + }, [model]); + + useEffect(() => { + const messages = getChatMessages(chatId); + console.log("messages", messages); + setMessages(messages); + }, [chatId]); + + useEffect(() => { + console.log("chatgptConfig", chatgptConfig); + setWebConfig({ ...chatgptConfig }); + }, [chatgptConfig]); + + return ( + <> + + + {/* setCloseSelectionTextPanel(true)} title={t('Select Text')} description={selection} className='overflow-scroll max-h-64'> */} - - - { - isLoading && 0 ? ( + + ) : ( + + + + + + + 我今天能帮你什么? + + + + )} + + {isLoading && ( + + + + {t("Stop")} + + + )} + + - - - {t('Stop')} - + - } - - - - - - > - ) -}) - - - - + + > + ); + } +); diff --git a/apps/extension/src/components/GPTs/GPTsSearch.tsx b/apps/extension/src/components/GPTs/GPTsSearch.tsx index 7649b6a..51918d9 100644 --- a/apps/extension/src/components/GPTs/GPTsSearch.tsx +++ b/apps/extension/src/components/GPTs/GPTsSearch.tsx @@ -3,24 +3,48 @@ import { Select, Spin } from 'antd'; import type { SelectProps } from 'antd'; import debounce from 'lodash/debounce'; import useGPTStore from '~src/store/useGPTsStore'; -import type { Gizmo } from '@opengpts/types'; +import type { Gizmo, Gpts } from '@opengpts/types'; +import { WEBSITE_URL } from '@opengpts/core/constant' export interface GPTsSearchProps extends Omit, 'options' | 'children'> { } function GPTsSearch({ ...props }: GPTsSearchProps) { const [fetching, setFetching] = useState(false); - const [options, setOptions] = useState([]); + const [options, setOptions] = useState[]>([]); const gptsList = useGPTStore(state => state.gptsList) - const fetchGPTs = (search: string) => { + const fetchGPTs = async (searchValue: string) => { + if (!searchValue.trim()) return setOptions([]); setFetching(true); - const filteredGPTs = gptsList.filter(gpt => - gpt.display.name.toLowerCase().includes(search.toLowerCase()) - ); - setOptions(filteredGPTs); + const res = await fetch(`${WEBSITE_URL}/api/gpts/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + question: searchValue, + }) + }) + if (res.ok) { + const resp = await res.json(); + console.log('resp', resp) + const gpts: Gpts[] = resp.data; + const filteredGPTs: Partial[] = gpts.map(gpt => { + return { + id: gpt.uuid, + display: { + name: gpt.name, + profile_picture_url: gpt.avatar_url, + description: gpt.description, + } + } + }) + setOptions(filteredGPTs); + } setFetching(false); + }; const debounceFetcher = useMemo(() => { @@ -34,7 +58,7 @@ function GPTsSearch({ ...props }: GPTsSearchProps) { onSearch={debounceFetcher} notFoundContent={fetching ? : null} {...props} - options={options.map(gpt => ({ label: gpt.display.name, value: gpt.id, ...gpt }))} + options={options.map(gpt => ({ label: gpt?.display?.name, value: gpt.id, ...gpt }))} /> ); } diff --git a/apps/extension/src/components/Markdown/index.tsx b/apps/extension/src/components/Markdown/index.tsx index 9b175a8..a56d22b 100644 --- a/apps/extension/src/components/Markdown/index.tsx +++ b/apps/extension/src/components/Markdown/index.tsx @@ -52,7 +52,7 @@ export function PreCode(props: { children: any }) { -const Markdown: FC<{ children: string }> = ({ children }) => { +const Markdown= ({ children }) => { return ( {message.content ? ( - {message.content} + message.content instanceof String ? {message.content} : message.content + ) : ( diff --git a/apps/extension/src/components/Tiptap/MentionList.tsx b/apps/extension/src/components/Tiptap/MentionList.tsx index 967249c..11f87fd 100644 --- a/apps/extension/src/components/Tiptap/MentionList.tsx +++ b/apps/extension/src/components/Tiptap/MentionList.tsx @@ -110,7 +110,7 @@ const MentionList = forwardRef((props, ref) => document.getElementById('opengpts-mentionsList')!} showSearch={true} - placeholder="Search GPTs" + placeholder="Search GPTs in OpenGPTs" style={{ width: '100%' }} onSelect={(value, options) => { const selectedMention: Mention = { diff --git a/apps/extension/src/i18n.js b/apps/extension/src/i18n.js index 38f5cbf..0e5946d 100644 --- a/apps/extension/src/i18n.js +++ b/apps/extension/src/i18n.js @@ -70,6 +70,20 @@ const resources = { 'publishGPTDescription': 'Are you ready to make this GPT public and available to others?', 'AsyncGPTsFromChatGPT.tooltip': 'One-Click Async GPTs From ChatGPT', 'promptBuilder.tooltip': 'If there is an error, please {{gpts}} on any chat to automatically generate the logo', + + // gpt request error + 'NetworkApplication': 'ChatGPT Network Application', + 'NetworkUnstableRequestFailed': 'Network unstable, request failed', + 'DueToOpenAILimitation': 'Due to OpenAI limitations, you must keep your ChatGPT account logged in. Stability issues may require frequent refreshes.', + 'GPT4AndGPTsCallFailed': 'GPT4 and GPTs call failed', + 'VisitChatOpenAIPage': 'Please visit https://chat.openai.com/, start a ChatGPT4 conversation, then try again', + 'ChatGPTPlusUsersCanTry': 'ChatGPT Plus users can try', + 'OpenAIRestrictedYourRequestFrequency': 'OpenAI restricted your request frequency', + 'CheckLimitOverage': 'Check if you have exceeded the limit of 40 messages in 3 hours for GPT4. If not, refresh the https://chat.openai.com/ page and try again', + 'LogError': 'Log error', + 'Error': 'Error', + 'AlertErrorMessage': 'Alert error message', + "GPTsUnavailableOrDeleted": "This GPTs may have been converted to private or deleted, making it unavailable for use. You can try switching to another GPTs." } }, zh: { @@ -130,6 +144,20 @@ const resources = { 'publishGPTDescription': '您准备好将这个 GPT 公开并向其他人提供了吗?', 'AsyncGPTsFromChatGPT.tooltip': '一键从ChatGPT同步GPTs', 'promptBuilder.tooltip': '如果出现错误,请{{gpts}} 上任意对话,才能自动生成Logo', + + // gpt request error + 'NetworkApplication': 'ChatGPT 网络应用', + 'NetworkUnstableRequestFailed': '网络不稳定,请求失败', + 'DueToOpenAILimitation': '由于 OpenAI 的限制,需时刻保持您的 ChatGPT 账户登录状态。稳定性问题可能需要频繁刷新。', + 'GPT4AndGPTsCallFailed': 'GPT4 和 GPTs 调用失败', + 'VisitChatOpenAIPage': '请访问 https://chat.openai.com/,开始一次 ChatGPT4 对话,然后重试', + 'ChatGPTPlusUsersCanTry': 'ChatGPT Plus 用户可尝试', + 'OpenAIRestrictedYourRequestFrequency': 'OpenAI 限制了您的请求频率', + 'CheckLimitOverage': '检查是否超过了 GPT4 的 3 小时内 40 条信息的限制。如果没有,请刷新 https://chat.openai.com/ 页面后重试', + 'LogError': '记录错误', + 'Error': '错误', + 'AlertErrorMessage': '警告错误信息', + "GPTsUnavailableOrDeleted": "这个GPTs可能已经被转为私有或者被删除了,导致无法使用,你可以尝试切换到其他的GPTs" } } }; diff --git a/packages/core/@react/hooks/use-chat.ts b/packages/core/@react/hooks/use-chat.ts index 0bfcda8..1a805a2 100644 --- a/packages/core/@react/hooks/use-chat.ts +++ b/packages/core/@react/hooks/use-chat.ts @@ -160,7 +160,7 @@ const getStreamedResponse = async ( if (webConfig && !webConfig['token']) { - throw new Error('if you use web mode, you must provide a token') + throw new Error('chatGPT403') } const openai = new OpenAI({ token: webConfig!.token }); @@ -200,7 +200,7 @@ const getStreamedResponse = async ( generateId, messageConfig, }) : callChatWeb({ - callMethod: openai.gpt.call.bind(openai.gpt), + callLLm: openai.gpt.call.bind(openai.gpt), messages: constructedMessagesPayload, body: { data: chatRequest.data, @@ -248,18 +248,20 @@ export function useChat({ generateId = nanoid, initMode = 'api', initialWebConfig = {} -}: Omit & { +}: Omit & { api?: string | StreamingReactResponseAction; key?: string; initMode?: 'api' | 'web'; initialWebConfig?: any; onFinish?: (message: OMessage, session?: any, conversation?: OpenAI['conversation']) => void; + onError?: (error: Error, updateMessage: (messageInfo: Partial) => void) => void; } = {}): UseChatHelpers & { mode: "api" | "web"; webConfig: any; setMode: React.Dispatch>; setWebConfig: React.Dispatch>; + } { // Generate a unique id for the chat if not provided. const hookId = useId(); @@ -357,27 +359,38 @@ export function useChat({ abortControllerRef.current = null; } catch (error) { + // Ignore abort errors as they are expected. if ((error as any).name === 'AbortError') { abortControllerRef.current = null; return null; } - if (onError && error instanceof Error) { - onError(error); - } if (error instanceof Error) { - const newMessages = [...messagesRef.current, { + let newMessage = { id: generateId(), createdAt: new Date(), content: error.message, role: 'assistant', isError: true, - } as OMessage]; + } as OMessage + + const updateMessage = (messageInfo: Partial) => { + newMessage = { + ...newMessage, + ...messageInfo + } + } + onError && onError(error, updateMessage); + + const newMessages = [...messagesRef.current, newMessage]; mutate(newMessages, false); console.error(`[useChat] ${error.message}`) setError(error as Error); } + + + } finally { mutateLoading(false); } diff --git a/packages/core/constant.ts b/packages/core/constant.ts index 7b39c36..2c3625b 100644 --- a/packages/core/constant.ts +++ b/packages/core/constant.ts @@ -1,5 +1,9 @@ import type { ChatConfig, ModelKey } from '@opengpts/types' + +const WEBSITE_URL = 'https://open-gpts.vercel.app' + + const MODELS_DICT: Record = { chatgpt35API: { value: 'gpt-3.5-turbo-16k', desc: 'ChatGPT (API)' }, chatgptFree35: { value: 'text-davinci-002-render-sha', desc: 'ChatGPT (Web)' }, @@ -26,5 +30,6 @@ const chatgptWebModelKeys = [ export { chatgptWebModelKeys, MODELS_DICT, - DEFAULT_CONFIG + DEFAULT_CONFIG, + WEBSITE_URL } \ No newline at end of file diff --git a/packages/core/lib/fetch-sse.mjs b/packages/core/lib/fetch-sse.mjs index 5b48710..006a4b2 100644 --- a/packages/core/lib/fetch-sse.mjs +++ b/packages/core/lib/fetch-sse.mjs @@ -136,43 +136,80 @@ function hasBom(buffer) { export async function fetchSSE(resource, options) { - const { onMessage, onStart, onFinish, onError, ...fetchOptions } = options - const resp = await fetch(resource, fetchOptions).catch(async (err) => { - await onError(err) - }) - if (!resp) return - if (!resp.ok) { - await onError(resp) - return - } - const parser = createParser((event) => { - if (event.type === 'event') { - onMessage(event.data) + let timeoutId; + const { onMessage, onStart, onFinish, onError, timeout = 30000, ...fetchOptions } = options + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Request timed out")); + }, timeout); + }); + + try { + const resp = await Promise.race([ + fetch(resource, fetchOptions), + timeoutPromise + ]).catch(async (err) => { + // The user aborted a request. + // Request timed out + onError && await onError(err); + }); + if (!resp) return + if (!resp.ok) { + await onError(resp) + return } - }) - let hasStarted = false - const reader = resp.body.getReader() - let result - while (!(result = await reader.read()).done) { - const chunk = result.value - if (!hasStarted) { - const str = new TextDecoder().decode(chunk) - hasStarted = true - await onStart(str) + const parser = createParser((event) => { + if (event.type === 'event') { + if (timeoutId) { + clearTimeout(timeoutId); // 当收到消息时,清除超时定时器 + timeoutId = null; // 重置定时器ID + } + onMessage(event.data); + } + }) + let hasStarted = false + const reader = resp.body.getReader() - let fakeSseData - try { - const commonResponse = JSON.parse(str) - fakeSseData = 'data: ' + JSON.stringify(commonResponse) + '\n\ndata: [DONE]\n\n' - } catch (error) { - console.debug('not common response', error) + try { + let result + while (!(result = await reader.read()).done) { + const chunk = result.value + if (!hasStarted) { + const str = new TextDecoder().decode(chunk) + hasStarted = true + await onStart(str) + + let fakeSseData + try { + const commonResponse = JSON.parse(str) + fakeSseData = 'data: ' + JSON.stringify(commonResponse) + '\n\ndata: [DONE]\n\n' + } catch (error) { + console.debug('not common response', error) + } + if (fakeSseData) { + parser.feed(new TextEncoder().encode(fakeSseData)) + break + } + } + parser.feed(chunk) } - if (fakeSseData) { - parser.feed(new TextEncoder().encode(fakeSseData)) - break + + } catch (err) { + if (err.name === 'AbortError') { + console.log('Fetch was aborted'); + } else { + throw err; // 抛出其他类型的错误 } } - parser.feed(chunk) + onFinish && await onFinish() + } catch (error) { + console.log("报错3", error) + onError && await onError(error); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); // 当收到消息时,清除超时定时器 + timeoutId = null; // 重置定时器ID + } } - onFinish && await onFinish() } \ No newline at end of file diff --git a/packages/core/shared/call-chat-web.ts b/packages/core/shared/call-chat-web.ts index 1a3c56a..3273db5 100644 --- a/packages/core/shared/call-chat-web.ts +++ b/packages/core/shared/call-chat-web.ts @@ -3,7 +3,7 @@ import { OpenAI, StreamEvent } from '../web/openai'; import type { ChatConfig, OMessage, Session } from '@opengpts/types'; export async function callChatWeb({ - callMethod, + callLLm, messages, body, abortController, @@ -16,7 +16,7 @@ export async function callChatWeb({ webConfig, messageConfig }: { - callMethod: OpenAI['gpt']['call']; // The provided call function + callLLm: OpenAI['gpt']['call']; // The provided call function messages: Message[]; body: Record; abortController?: () => AbortController | null; @@ -75,18 +75,20 @@ export async function callChatWeb({ responseMessage['id'] = session.messageId! onFinish && onFinish(responseMessage, session, conversation); }, - onError: (resp: Response | Error) => { }, + onError: (error: Error) => { + // restoreMessagesOnFailure() + }, onAbort: () => { } }; - try { - await callMethod(session, event, webConfig); - - - return responseMessage; - } catch (error) { + await callLLm(session, event, webConfig, { + controller: abortController?.(), + }).catch(err => { + if(err.message === 'noChatGPTPlusArkoseToken') return; restoreMessagesOnFailure(); - throw error; - } + throw err; + }) + return responseMessage; + } diff --git a/packages/core/web/openai.ts b/packages/core/web/openai.ts index eccb348..f52e221 100644 --- a/packages/core/web/openai.ts +++ b/packages/core/web/openai.ts @@ -19,7 +19,7 @@ interface StreamEvent { onFinish?: ({ conversation }: { conversation: Conversation }) => void - onError?: (resp: Response | Error) => void + onError?: (error: Error) => void onAbort?: () => void } @@ -429,7 +429,9 @@ class GPT { } - public async call(session: Session, event?: StreamEvent, config: ChatConfig = DEFAULT_CONFIG): Promise<{ + public async call(session: Session, event?: StreamEvent, config?: ChatConfig, options?: { + controller?: AbortController | null + }): Promise<{ done: boolean, text?: string; imagePointers?: string[]; @@ -444,23 +446,20 @@ class GPT { if (!question) return { done: false, session, - error: `ques - tion is empty` + error: `question is empty` } - const controller = new AbortController(); + // const controller = new AbortController(); // Define the abort event handler const onAbortHandler = () => { - if (event?.onAbort) { - event.onAbort(); - } - controller.signal.removeEventListener('abort', onAbortHandler); + event?.onAbort && event.onAbort(); + options?.controller?.signal.removeEventListener('abort', onAbortHandler); if (session.autoClean && session.conversationId) { this.conversation?.delete(session.conversationId); } }; - controller.signal.addEventListener('abort', onAbortHandler); + options?.controller && options?.controller.signal.addEventListener('abort', onAbortHandler); const modelName = session?.modelName || 'chatgptFree35' let usedModel = MODELS_DICT[modelName]?.value @@ -480,11 +479,7 @@ class GPT { // console.log('cookie', cookie) const needArkoseToken = modelName !== 'chatgptFree35' if (needArkoseToken) { - const errorMsg = 'Please ensure you are logged in at https://chat.openai.com. ' + - '\n' + - 'After logging in, engage in any conversation at https://chat.openai.com/g/g-LJcAplYdM-opengptsz and then retry. ' + - '\n' + - 'If you encounter any further issues or have questions, feel free to seek assistance at https://chat.openai.com/g/g-LJcAplYdM-opengptsz or contact us for more help.' + const errorMsg = 'noChatGPTPlusArkoseToken' if (!config?.chatgptArkoseReqUrl) { throw new Error(errorMsg); } @@ -507,10 +502,9 @@ class GPT { const conversationInstance = this.conversation let text = '', imagePointers: string[] = []; const response = await new Promise((resolve, reject) => { - console.log('this.token', this?.token, this) - return fetchSSE(`${config.customChatGptWebApiUrl}${config.customChatGptWebApiPath}`, { + return fetchSSE(`${config?.customChatGptWebApiUrl}${config?.customChatGptWebApiPath}`, { method: 'POST', - signal: controller.signal, + signal: options?.controller?.signal, credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -544,7 +538,7 @@ class GPT { model: usedModel, parent_message_id: session.parentMessageId, timezone_offset_min: new Date().getTimezoneOffset(), - history_and_training_disabled: config.disableWebModeHistory, + history_and_training_disabled: config?.disableWebModeHistory || false, arkose_token: arkoseToken, }), onMessage(message: string) { @@ -565,7 +559,7 @@ class GPT { if (data.error) { if (data.error.includes('unusual activity')) throw new Error( - "Please keep https://chat.openai.com open and try again. If it still doesn't work, type some characters in the input box of chatgpt web page and try again.", + "Please keep https://chat.openai.com open and try again.", ) else throw new Error(data.error) } @@ -601,25 +595,34 @@ class GPT { }) }, async onError(resp: Response | Error) { - event?.onError && event.onError(resp); - // port.onMessage.removeListener(messageListener) - // port.onDisconnect.removeListener(disconnectListener) - controller.signal.removeEventListener('abort', onAbortHandler) - if (resp instanceof Error) throw resp + options?.controller && options?.controller.signal.removeEventListener('abort', onAbortHandler) - console.debug('resp.status', resp.status) + if (resp instanceof Error) { + reject(resp) + return + } + console.debug('resp.status', resp.status, resp.ok) + if (resp.status === 404) { + reject(new Error('chatGPT404')) + return; + } if (resp.status === 403) { - reject(new Error('Authorization failed, please open or login https://chat.openai.com/ try again')) + reject(new Error('chatGPT403')) return; } if (resp.status === 429) { - reject(new Error('Maybe You\'ve reached the current usage cap for GPT-4,')) + reject(new Error('chatGPT429')) return; + } if (resp.status === 404) { + } const error = await resp.json().catch(() => ({})) - reject(new Error(!_.isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)) - }, + reject(new Error(!_.isEmpty(error) ? JSON.stringify(resp) : `${resp.status} ${resp.statusText}`)) + } }) + }).catch(error => { + console.log('fetch-sse', error) + throw error }); if (session?.autoClean && session?.conversationId) this.conversation?.delete(session?.conversationId) return response as any; diff --git a/packages/types/gizmo.d.ts b/packages/types/gizmo.d.ts index ab35b54..782e3f6 100644 --- a/packages/types/gizmo.d.ts +++ b/packages/types/gizmo.d.ts @@ -14,8 +14,8 @@ interface Voice { interface Display { name: string; description: string; - welcome_message: string; - prompt_starters: string[]; + welcome_message?: string; + prompt_starters?: string[]; profile_picture_url?: string; profile_pic_id?: string; categories?: string[];