This commit is contained in:
Mckay Wrigley
2023-04-04 09:39:37 -06:00
parent 6156a2702f
commit d570c8b1ed
19 changed files with 1685 additions and 267 deletions
+130
View File
@@ -0,0 +1,130 @@
import { ChatBody, Message } from '@/types/chat';
import { GoogleSource } from '@/types/google';
import { OPENAI_API_HOST } from '@/utils/app/const';
import { cleanSourceText } from '@/utils/server/google';
import { Readability } from '@mozilla/readability';
import endent from 'endent';
import jsdom, { JSDOM } from 'jsdom';
import { NextApiRequest, NextApiResponse } from 'next';
const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => {
try {
const { messages, key, model } = req.body as ChatBody;
const userMessage = messages[messages.length - 1];
const googleRes = await fetch(
`https://customsearch.googleapis.com/customsearch/v1?key=${
process.env.GOOGLE_API_KEY
}&cx=${process.env.GOOGLE_CSE_ID}&q=${userMessage.content.trim()}&num=5`,
);
const googleData = await googleRes.json();
const sources: GoogleSource[] = googleData.items.map((item: any) => ({
title: item.title,
link: item.link,
displayLink: item.displayLink,
snippet: item.snippet,
image: item.pagemap?.cse_image?.[0]?.src,
text: '',
}));
const sourcesWithText: any = await Promise.all(
sources.map(async (source) => {
try {
const res = await fetch(source.link);
const html = await res.text();
const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.on('error', (error) => {
if (!error.message.includes('Could not parse CSS stylesheet')) {
console.error(error);
}
});
const dom = new JSDOM(html, { virtualConsole });
const doc = dom.window.document;
const parsed = new Readability(doc).parse();
if (parsed) {
let sourceText = cleanSourceText(parsed.textContent);
return {
...source,
// TODO: switch to tokens
text: sourceText.slice(0, 2000),
} as GoogleSource;
}
return null;
} catch (error) {
return null;
}
}),
);
const filteredSources: GoogleSource[] = sourcesWithText.filter(Boolean);
const answerPrompt = endent`
Provide me with the information I requested. Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as a markdown link as you use them at the end of each sentence by number of the source (ex: [[1]](link.com)). Provide an accurate response and then stop. Today's date is ${new Date().toLocaleDateString()}.
Example Input:
What's the weather in San Francisco today?
Example Sources:
[Weather in San Francisco](https://www.google.com/search?q=weather+san+francisco)
Example Response:
It's 70 degrees and sunny in San Francisco today. [[1]](https://www.google.com/search?q=weather+san+francisco)
Input:
${userMessage.content.trim()}
Sources:
${filteredSources.map((source) => {
return endent`
${source.title} (${source.link}):
${source.text}
`;
})}
Response:
`;
const answerMessage: Message = { role: 'user', content: answerPrompt };
const answerRes = await fetch(`${OPENAI_API_HOST}/v1/chat/completions`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`,
...(process.env.OPENAI_ORGANIZATION && {
'OpenAI-Organization': process.env.OPENAI_ORGANIZATION,
}),
},
method: 'POST',
body: JSON.stringify({
model: model.id,
messages: [
{
role: 'system',
content: `Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as [1](link), etc, as you use them.`,
},
answerMessage,
],
max_tokens: 1000,
temperature: 1,
stream: false,
}),
});
const { choices: choices2 } = await answerRes.json();
const answer = choices2[0].message.content;
res.status(200).json({ answer });
} catch (error) {
return new Response('Error', { status: 500 });
}
};
export default handler;
+193 -86
View File
@@ -8,12 +8,14 @@ import { ErrorMessage } from '@/types/error';
import { LatestExportFormat, SupportedExportFormats } from '@/types/export';
import { Folder, FolderType } from '@/types/folder';
import {
fallbackModelID,
OpenAIModel,
OpenAIModelID,
OpenAIModels,
fallbackModelID,
} from '@/types/openai';
import { Plugin, PluginKey } from '@/types/plugin';
import { Prompt } from '@/types/prompt';
import { getEndpoint } from '@/utils/app/api';
import {
cleanConversationHistory,
cleanSelectedConversation,
@@ -33,16 +35,18 @@ import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import { useEffect, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import toast from 'react-hot-toast';
import { v4 as uuidv4 } from 'uuid';
interface HomeProps {
serverSideApiKeyIsSet: boolean;
serverSidePluginKeysSet: boolean;
defaultModelId: OpenAIModelID;
}
const Home: React.FC<HomeProps> = ({
serverSideApiKeyIsSet,
serverSidePluginKeysSet,
defaultModelId,
}) => {
const { t } = useTranslation('chat');
@@ -50,6 +54,7 @@ const Home: React.FC<HomeProps> = ({
// STATE ----------------------------------------------
const [apiKey, setApiKey] = useState<string>('');
const [pluginKeys, setPluginKeys] = useState<PluginKey[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark');
const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false);
@@ -76,7 +81,11 @@ const Home: React.FC<HomeProps> = ({
// FETCH RESPONSE ----------------------------------------------
const handleSend = async (message: Message, deleteCount = 0) => {
const handleSend = async (
message: Message,
deleteCount = 0,
plugin: Plugin | null = null,
) => {
if (selectedConversation) {
let updatedConversation: Conversation;
@@ -108,8 +117,10 @@ const Home: React.FC<HomeProps> = ({
prompt: updatedConversation.prompt,
};
const endpoint = getEndpoint(plugin);
const controller = new AbortController();
const response = await fetch('/api/chat', {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -133,94 +144,130 @@ const Home: React.FC<HomeProps> = ({
return;
}
if (updatedConversation.messages.length === 1) {
const { content } = message;
const customName =
content.length > 30 ? content.substring(0, 30) + '...' : content;
if (!plugin) {
if (updatedConversation.messages.length === 1) {
const { content } = message;
const customName =
content.length > 30 ? content.substring(0, 30) + '...' : content;
updatedConversation = {
...updatedConversation,
name: customName,
};
}
setLoading(false);
const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;
let isFirst = true;
let text = '';
while (!done) {
if (stopConversationRef.current === true) {
controller.abort();
done = true;
break;
}
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
text += chunkValue;
if (isFirst) {
isFirst = false;
const updatedMessages: Message[] = [
...updatedConversation.messages,
{ role: 'assistant', content: chunkValue },
];
updatedConversation = {
...updatedConversation,
messages: updatedMessages,
};
setSelectedConversation(updatedConversation);
} else {
const updatedMessages: Message[] = updatedConversation.messages.map(
(message, index) => {
if (index === updatedConversation.messages.length - 1) {
return {
...message,
content: text,
};
}
return message;
},
);
updatedConversation = {
...updatedConversation,
messages: updatedMessages,
};
setSelectedConversation(updatedConversation);
}
}
saveConversation(updatedConversation);
const updatedConversations: Conversation[] = conversations.map(
(conversation) => {
if (conversation.id === selectedConversation.id) {
return updatedConversation;
}
return conversation;
},
);
if (updatedConversations.length === 0) {
updatedConversations.push(updatedConversation);
}
setConversations(updatedConversations);
saveConversations(updatedConversations);
setMessageIsStreaming(false);
} else {
const { answer } = await response.json();
const updatedMessages: Message[] = [
...updatedConversation.messages,
{ role: 'assistant', content: answer },
];
updatedConversation = {
...updatedConversation,
name: customName,
messages: updatedMessages,
};
}
setLoading(false);
setSelectedConversation(updatedConversation);
saveConversation(updatedConversation);
const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;
let isFirst = true;
let text = '';
const updatedConversations: Conversation[] = conversations.map(
(conversation) => {
if (conversation.id === selectedConversation.id) {
return updatedConversation;
}
while (!done) {
if (stopConversationRef.current === true) {
controller.abort();
done = true;
break;
return conversation;
},
);
if (updatedConversations.length === 0) {
updatedConversations.push(updatedConversation);
}
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
text += chunkValue;
setConversations(updatedConversations);
saveConversations(updatedConversations);
if (isFirst) {
isFirst = false;
const updatedMessages: Message[] = [
...updatedConversation.messages,
{ role: 'assistant', content: chunkValue },
];
updatedConversation = {
...updatedConversation,
messages: updatedMessages,
};
setSelectedConversation(updatedConversation);
} else {
const updatedMessages: Message[] = updatedConversation.messages.map(
(message, index) => {
if (index === updatedConversation.messages.length - 1) {
return {
...message,
content: text,
};
}
return message;
},
);
updatedConversation = {
...updatedConversation,
messages: updatedMessages,
};
setSelectedConversation(updatedConversation);
}
setLoading(false);
setMessageIsStreaming(false);
}
saveConversation(updatedConversation);
const updatedConversations: Conversation[] = conversations.map(
(conversation) => {
if (conversation.id === selectedConversation.id) {
return updatedConversation;
}
return conversation;
},
);
if (updatedConversations.length === 0) {
updatedConversations.push(updatedConversation);
}
setConversations(updatedConversations);
saveConversations(updatedConversations);
setMessageIsStreaming(false);
}
};
@@ -283,6 +330,45 @@ const Home: React.FC<HomeProps> = ({
localStorage.setItem('apiKey', apiKey);
};
const handlePluginKeyChange = (pluginKey: PluginKey) => {
if (pluginKeys.some((key) => key.pluginId === pluginKey.pluginId)) {
const updatedPluginKeys = pluginKeys.map((key) => {
if (key.pluginId === pluginKey.pluginId) {
return pluginKey;
}
return key;
});
setPluginKeys(updatedPluginKeys);
localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys));
} else {
setPluginKeys([...pluginKeys, pluginKey]);
localStorage.setItem(
'pluginKeys',
JSON.stringify([...pluginKeys, pluginKey]),
);
}
};
const handleClearPluginKey = (pluginKey: PluginKey) => {
const updatedPluginKeys = pluginKeys.filter(
(key) => key.pluginId !== pluginKey.pluginId,
);
if (updatedPluginKeys.length === 0) {
setPluginKeys([]);
localStorage.removeItem('pluginKeys');
return;
}
setPluginKeys(updatedPluginKeys);
localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys));
};
const handleToggleChatbar = () => {
setShowSidebar(!showSidebar);
localStorage.setItem('showChatbar', JSON.stringify(!showSidebar));
@@ -496,8 +582,6 @@ const Home: React.FC<HomeProps> = ({
// PROMPT OPERATIONS --------------------------------------------
const handleCreatePrompt = () => {
const lastPrompt = prompts[prompts.length - 1];
const newPrompt: Prompt = {
id: uuidv4(),
name: `Prompt ${prompts.length + 1}`,
@@ -562,11 +646,21 @@ const Home: React.FC<HomeProps> = ({
}
const apiKey = localStorage.getItem('apiKey');
if (apiKey) {
if (serverSideApiKeyIsSet) {
fetchModels('');
setApiKey('');
localStorage.removeItem('apiKey');
} else if (apiKey) {
setApiKey(apiKey);
fetchModels(apiKey);
} else if (serverSideApiKeyIsSet) {
fetchModels('');
}
const pluginKeys = localStorage.getItem('pluginKeys');
if (serverSidePluginKeysSet) {
setPluginKeys([]);
localStorage.removeItem('pluginKeys');
} else if (pluginKeys) {
setPluginKeys(JSON.parse(pluginKeys));
}
if (window.innerWidth < 640) {
@@ -654,6 +748,7 @@ const Home: React.FC<HomeProps> = ({
lightMode={lightMode}
selectedConversation={selectedConversation}
apiKey={apiKey}
pluginKeys={pluginKeys}
folders={folders.filter((folder) => folder.type === 'chat')}
onToggleLightMode={handleLightMode}
onCreateFolder={(name) => handleCreateFolder(name, 'chat')}
@@ -667,6 +762,8 @@ const Home: React.FC<HomeProps> = ({
onClearConversations={handleClearConversations}
onExportConversations={handleExportData}
onImportConversations={handleImportConversations}
onPluginKeyChange={handlePluginKeyChange}
onClearPluginKey={handleClearPluginKey}
/>
<button
@@ -755,10 +852,20 @@ export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
process.env.DEFAULT_MODEL) ||
fallbackModelID;
let serverSidePluginKeysSet = false;
const googleApiKey = process.env.GOOGLE_API_KEY;
const googleCSEId = process.env.GOOGLE_CSE_ID;
if (googleApiKey && googleCSEId) {
serverSidePluginKeysSet = true;
}
return {
props: {
serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY,
defaultModelId,
serverSidePluginKeysSet,
...(await serverSideTranslations(locale ?? 'en', [
'common',
'chat',