chatgpt is taking shape, convo persists, layout mimics original

This commit is contained in:
Daniel Avila 2023-02-05 19:41:24 -05:00
parent 3a199757ae
commit f889f23792
9 changed files with 161 additions and 35 deletions

View file

@ -1,11 +1,23 @@
require('dotenv').config();
const Keyv = require('keyv');
const messageStore = new Keyv(process.env.MONGODB_URI, { namespace: 'chatgpt' });
const ask = async (question) => {
const ask = async (question, progressCallback, convo) => {
const { ChatGPTAPI } = await import('chatgpt');
const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_KEY });
const res = await api.sendMessage(question, {
onProgress: (partialRes) => console.log(partialRes.text)
});
const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_KEY, messageStore });
let options = {
onProgress: (partialRes) => {
if (partialRes.text.length > 0) {
progressCallback(partialRes);
}
}
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await api.sendMessage(question, options);
return res;
};

View file

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="./src/index.js"></script>
</body>
</html>

38
package-lock.json generated
View file

@ -9,11 +9,13 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"chatgpt": "^4.1.1",
"cors": "^2.8.5",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.3",
"eventsource": "^2.0.2",
"keyv": "^4.5.2",
"mongoose": "^6.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -3511,6 +3513,26 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@keyv/mongo": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@keyv/mongo/-/mongo-2.1.8.tgz",
"integrity": "sha512-IOFKS9Y10c42NCaoD/6OKmqz7FMCm/VbMbrip7ma8tBvdWcPhDkkPV3ZpLgGsGw39RePzzKO6FQ89xs0+BFCKg==",
"dependencies": {
"mongodb": "^4.5.0",
"pify": "^5.0.0"
}
},
"node_modules/@keyv/mongo/node_modules/pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@ -14615,6 +14637,22 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@keyv/mongo": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@keyv/mongo/-/mongo-2.1.8.tgz",
"integrity": "sha512-IOFKS9Y10c42NCaoD/6OKmqz7FMCm/VbMbrip7ma8tBvdWcPhDkkPV3ZpLgGsGw39RePzzKO6FQ89xs0+BFCKg==",
"requires": {
"mongodb": "^4.5.0",
"pify": "^5.0.0"
},
"dependencies": {
"pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="
}
}
},
"@leichtgewicht/ip-codec": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",

View file

@ -21,11 +21,13 @@
},
"homepage": "https://github.com/danny-avila/rpp2210-mvp#readme",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"chatgpt": "^4.1.1",
"cors": "^2.8.5",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.3",
"eventsource": "^2.0.2",
"keyv": "^4.5.2",
"mongoose": "^6.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View file

@ -15,22 +15,32 @@ app.get('/', function (req, res) {
res.sendFile(path.join(projectPath, 'public', 'index.html'));
});
app.post('/ask', (req, res) => {
console.log(req.body, 'we in here');
app.post('/ask', async (req, res) => {
console.log(req.body);
const { text, parentMessageId, conversationId } = req.body;
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Access-Control-Allow-Origin':'*',
'X-Accel-Buffering':'no'
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no'
});
res.write('event: message\ndata: This is chunk 1\n\n');
res.write('event: message\ndata: This is chunk 2\n\n');
setTimeout(() => {
res.write('event: message\ndata: This is chunk 3\n\n');
res.end();
}, 3500);
let i = 0;
const progressCallback = (partial) => {
// console.log('partial', partial);
if (i === 0) {
res.write(`event: message\ndata: ${JSON.stringify({ ...partial, initial: true })}\n\n`);
i++;
}
const data = JSON.stringify({...partial, message: true });
res.write(`event: message\ndata: ${data}\n\n`);
};
const gptResponse = await ask(text, progressCallback, { parentMessageId, conversationId });
res.write(`event: message\ndata: ${JSON.stringify(gptResponse)}\n\n`);
res.end();
});
app.listen(port, () => {

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import Messages from './components/Messages';
import TextChat from './components/TextChat';
const App = () => {
@ -8,9 +9,8 @@ const App = () => {
<div className="flex h-screen">
<div className="w-80 bg-slate-800"></div>
<div className="flex h-full w-full flex-col bg-gray-50 ">
<div className="flex-1 overflow-y-auto"></div>
{/* <textarea className="m-10 h-16 p-4" onChange={(e) => console.log(e.target.value)}/> */}
<TextChat />
<Messages messages={messages} />
<TextChat messages={messages} setMessages={setMessages}/>
</div>
</div>
);

View file

@ -0,0 +1,12 @@
import React from 'react';
export default function Message({ sender, text }) {
return (
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<strong className="relative flex w-[30px] flex-col items-end">{sender}:</strong>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
{text}
</div>
</div>
);
}

View file

@ -0,0 +1,28 @@
import React, { useEffect, useRef } from 'react';
import Message from './Message';
export default function Messages({ messages }) {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// <div className="w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800">
return (
<div className="flex-1 overflow-y-auto ">
{messages.map((message, i) => (
<Message
key={i}
sender={message.sender}
text={message.text}
/>
))}
<div ref={messagesEndRef} />
</div>
);
}

View file

@ -1,18 +1,33 @@
import React, { useState } from 'react';
import { SSE } from '../../app/sse';
const handleSubmit = (payload) => {
const handleSubmit = (text, messageHandler, convo, convoHandler) => {
let payload = { text };
if (convo.conversationId && convo.parentMessageId) {
payload = {
...payload,
conversationId: convo.conversationId,
parentMessageId: convo.parentMessageId
};
}
const events = new SSE('http://localhost:3050/ask', {
payload: JSON.stringify({ text: payload }),
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
console.log('we in handleSubmit');
events.onopen = function () {
console.log('connection is opened');
};
events.onmessage = function (e) {
console.log(e);
const data = JSON.parse(e.data);
if (!!data.message) {
messageHandler(data.text.replace(/^\n/, ''));
} else {
console.log(data);
convoHandler(data);
}
};
events.onerror = function (e) {
@ -23,8 +38,13 @@ const handleSubmit = (payload) => {
events.stream();
};
export default function TextChat() {
export default function TextChat({ messages, setMessages, conversation = null }) {
const [text, setText] = useState('');
const [convo, setConvo] = useState({ conversationId: null, parentMessageId: null });
if (!!conversation) {
setConvo(conversation);
}
const handleKeyPress = (e) => {
if (e.key === 'Enter' && e.shiftKey) {
@ -32,8 +52,21 @@ export default function TextChat() {
}
if (e.key === 'Enter' && !e.shiftKey) {
console.log('Submit Enter');
handleSubmit(text);
const payload = text.trim();
const currentMsg = { sender: 'user', text: payload, current: true };
setMessages([...messages, currentMsg]);
setText('');
const messageHandler = (data) => {
setMessages([...messages, currentMsg, { sender: 'GPT', text: data }]);
};
const convoHandler = (data) => {
if (convo.conversationId === null && convo.parentMessageId === null) {
const { conversationId, parentMessageId } = data;
setConvo({ conversationId, parentMessageId: data.id });
}
};
console.log('User Input:', payload);
handleSubmit(payload, messageHandler, convo, convoHandler);
}
};
@ -41,6 +74,7 @@ export default function TextChat() {
<>
<textarea
className="m-10 h-16 p-4"
value={text}
onKeyUp={handleKeyPress}
onChange={(e) => setText(e.target.value)}
/>