push
This commit is contained in:
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user