Skip to content

Commit

Permalink
1.Fix issue in chatgpt web where clicking the "Stop" button did not t…
Browse files Browse the repository at this point in the history
…rigger an abort request.

2.Modify the "@mention" functionality to search all GPT models available in opengpts.
3.Add a welcome message to the chat panel.
  • Loading branch information
hzeyuan committed Jan 31, 2024
1 parent 02b2cd5 commit 2c23102
Show file tree
Hide file tree
Showing 13 changed files with 631 additions and 456 deletions.
10 changes: 0 additions & 10 deletions apps/extension/assets/@.svg

This file was deleted.

772 changes: 422 additions & 350 deletions apps/extension/src/components/Chat/Chat.tsx

Large diffs are not rendered by default.

40 changes: 32 additions & 8 deletions apps/extension/src/components/GPTs/GPTsSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SelectProps<Gizmo | Gizmo[]>, 'options' | 'children'> { }

function GPTsSearch({ ...props }: GPTsSearchProps) {
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState<Gizmo[]>([]);
const [options, setOptions] = useState<Partial<Gizmo>[]>([]);
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<Gizmo>[] = 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(() => {
Expand All @@ -34,7 +58,7 @@ function GPTsSearch({ ...props }: GPTsSearchProps) {
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size="small" /> : 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 }))}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/components/Markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function PreCode(props: { children: any }) {



const Markdown: FC<{ children: string }> = ({ children }) => {
const Markdown= ({ children }) => {
return (
<ReactMarkdown
remarkPlugins={[remarkMath, remarkBreaks, remarkGfm]}
Expand Down
3 changes: 2 additions & 1 deletion apps/extension/src/components/Message/AIMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const AIMessage = ({ message, chatId }: { chatId: string; message: OMessa
>
<div className={`leading-relaxed break-words break-all `}>
{message.content ? (
<Markdown>{message.content}</Markdown>
message.content instanceof String ? <Markdown>{message.content}</Markdown> : message.content

) : (
<div className="flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/components/Tiptap/MentionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const MentionList = forwardRef<MentionListRef, MentionListProps>((props, ref) =>
<GPTsSearch
getPopupContainer={() => document.getElementById('opengpts-mentionsList')!}
showSearch={true}
placeholder="Search GPTs"
placeholder="Search GPTs in OpenGPTs"
style={{ width: '100%' }}
onSelect={(value, options) => {
const selectedMention: Mention = {
Expand Down
28 changes: 28 additions & 0 deletions apps/extension/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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"
}
}
};
Expand Down
29 changes: 21 additions & 8 deletions packages/core/@react/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -248,18 +248,20 @@ export function useChat({
generateId = nanoid,
initMode = 'api',
initialWebConfig = {}
}: Omit<UseChatOptions, 'api' | 'onFinish'> & {
}: Omit<UseChatOptions, 'api' | 'onFinish' | 'onError'> & {
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<OMessage>) => void) => void;
} = {}): UseChatHelpers & {
mode: "api" | "web";
webConfig: any;
setMode: React.Dispatch<React.SetStateAction<"api" | "web">>;
setWebConfig: React.Dispatch<React.SetStateAction<string>>;


} {
// Generate a unique id for the chat if not provided.
const hookId = useId();
Expand Down Expand Up @@ -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<OMessage>) => {
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);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/constant.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { ChatConfig, ModelKey } from '@opengpts/types'


const WEBSITE_URL = 'https://open-gpts.vercel.app'


const MODELS_DICT: Record<ModelKey, { value: string, desc: string }> = {
chatgpt35API: { value: 'gpt-3.5-turbo-16k', desc: 'ChatGPT (API)' },
chatgptFree35: { value: 'text-davinci-002-render-sha', desc: 'ChatGPT (Web)' },
Expand All @@ -26,5 +30,6 @@ const chatgptWebModelKeys = [
export {
chatgptWebModelKeys,
MODELS_DICT,
DEFAULT_CONFIG
DEFAULT_CONFIG,
WEBSITE_URL
}
103 changes: 70 additions & 33 deletions packages/core/lib/fetch-sse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading

0 comments on commit 2c23102

Please sign in to comment.