From e8efb37dfbdaf6f63c50c402ec0f8262fdc42241 Mon Sep 17 00:00:00 2001 From: David Lane Date: Fri, 8 Mar 2024 15:05:59 -0800 Subject: [PATCH 1/7] Added a chat page --- app/src/components/Dashboard/ChatPage.tsx | 140 ++++++++++++++++++++ app/src/components/Dashboard/Dashboard.tsx | 3 + app/src/components/Dashboard/SideNavbar.tsx | 12 +- 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 app/src/components/Dashboard/ChatPage.tsx diff --git a/app/src/components/Dashboard/ChatPage.tsx b/app/src/components/Dashboard/ChatPage.tsx new file mode 100644 index 0000000..fc35db0 --- /dev/null +++ b/app/src/components/Dashboard/ChatPage.tsx @@ -0,0 +1,140 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + Icon, +} from '@chakra-ui/react'; +import { ipcRenderer } from 'electron'; +import log from 'electron-log'; +import { useEffect, useRef, useState } from 'react'; +import { FiAlertCircle } from 'react-icons/fi'; + +import { logEvent } from '../../utils/analytics'; +import { Footer } from '../Footer'; + +export function ChatPage({ onRefresh }: { onRefresh: () => void }) { + const [doesRequireRefresh, setDoesRequireRefresh] = useState(false); + const [showUpdateAvailable, setShowUpdateAvailable] = + useState(false); + + const cancelRef = useRef(); + + useEffect(() => { + const checkRequiresRefresh = async () => { + let requiresRefresh = false; + try { + requiresRefresh = await ipcRenderer.invoke('check-requires-refresh'); + } catch (e) { + log.error(e); + } + + if (requiresRefresh) { + setDoesRequireRefresh(requiresRefresh); + } + }; + + checkRequiresRefresh(); + }, []); + + useEffect(() => { + logEvent({ + eventName: 'LOADED_DASHBOARD', + }); + }, []); + + useEffect(() => { + ipcRenderer.send('listen-to-updates'); + + ipcRenderer.on('update-available', () => { + setShowUpdateAvailable(true); + }); + }, []); + + return ( +
+
+
LLM
+
+
+ {}} + leastDestructiveRef={cancelRef} + > + + + +
+ {' '} + Requires Refresh +
+
+ + {`We've added exciting new features that require a data refresh!`} + + + + +
+
+ {}} + leastDestructiveRef={cancelRef} + > + + + +
+ {' '} + Update Available +
+
+ + Restart to install new features, stability improvements, and overall + updates. + + + + + +
+
+
+ ); +} diff --git a/app/src/components/Dashboard/Dashboard.tsx b/app/src/components/Dashboard/Dashboard.tsx index aa71110..1abb270 100644 --- a/app/src/components/Dashboard/Dashboard.tsx +++ b/app/src/components/Dashboard/Dashboard.tsx @@ -10,6 +10,7 @@ import { SettingsPage } from '../Pages/SettingsPage'; import { GoldContext } from '../Premium/GoldContext'; import { MessageScheduler } from '../Productivity/MessageScheduler'; import { AnalyticsPage } from './AnalyticsPage'; +import { ChatPage } from './ChatPage'; import { GlobalContext, TGlobalContext } from './GlobalContext'; import { SIDEBAR_WIDTH, SideNavbar, TPages } from './SideNavbar'; import { WrappedPage } from './Wrapped/WrappedPage'; @@ -93,6 +94,8 @@ export function Dashboard({ onRefresh }: { onRefresh: () => void }) { )} + {activePage === 'Chat' && } + {activePage === 'Settings' && } {activePage === 'Wrapped' && } diff --git a/app/src/components/Dashboard/SideNavbar.tsx b/app/src/components/Dashboard/SideNavbar.tsx index a98f29f..592e394 100644 --- a/app/src/components/Dashboard/SideNavbar.tsx +++ b/app/src/components/Dashboard/SideNavbar.tsx @@ -11,6 +11,7 @@ import { ipcRenderer } from 'electron'; import { IconType } from 'react-icons'; import { BsLightningCharge } from 'react-icons/bs'; import { FiClipboard, FiGift, FiInbox } from 'react-icons/fi'; +import { IoChatbubblesOutline } from 'react-icons/io5'; import LogoWithText from '../../../assets/LogoWithText.svg'; import { APP_VERSION } from '../../constants/versions'; @@ -19,7 +20,13 @@ import { useGoldContext } from '../Premium/GoldContext'; import { PremiumModal } from '../Premium/PremiumModal'; import { EmailModal } from '../Support/EmailModal'; -const Pages = ['Wrapped', 'Analytics', 'Productivity', 'Settings'] as const; +const Pages = [ + 'Wrapped', + 'Analytics', + 'Productivity', + 'Chat', + 'Settings', +] as const; export const SIDEBAR_WIDTH = 200; @@ -111,6 +118,9 @@ export function SideNavbar({ if (page === 'Wrapped') { icon = FiGift; } + if (page === 'Chat') { + icon = IoChatbubblesOutline; + } return ( Date: Fri, 8 Mar 2024 17:42:18 -0800 Subject: [PATCH 2/7] Added a chat interface --- .../components/Dashboard/ChatInterface.tsx | 103 ++++++++++++++++++ app/src/components/Dashboard/ChatPage.tsx | 87 ++++++++++++++- 2 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 app/src/components/Dashboard/ChatInterface.tsx diff --git a/app/src/components/Dashboard/ChatInterface.tsx b/app/src/components/Dashboard/ChatInterface.tsx new file mode 100644 index 0000000..89cf46e --- /dev/null +++ b/app/src/components/Dashboard/ChatInterface.tsx @@ -0,0 +1,103 @@ +import { Box, Button, Flex, Input, Text } from '@chakra-ui/react'; +import { waitFor } from '@testing-library/react'; +import React, { useEffect, useRef, useState } from 'react'; + +interface Message { + text: string; + sender: 'user' | 'bot'; +} + +const testMessage: Message = { + text: 'Hi there :) You can ask me questions here about your iMessages! For example, try "What should I get mom for her birthday?"', + sender: 'bot', +}; + +export function ChatInterface() { + const [messages, setMessages] = useState([testMessage]); + const [awaitingResponse, setAwaitingResponse] = useState(false); + const [newMessage, setNewMessage] = useState(''); + const messagesContainerRef = useRef(null); + + const handleMessageChange = (e: React.ChangeEvent) => { + setNewMessage(e.target.value); + }; + + const handleSendMessage = () => { + if (newMessage.trim()) { + setMessages([...messages, { text: newMessage, sender: 'user' }]); + setNewMessage(''); + // Add logic to handle sending the message to the recipient + setAwaitingResponse(true); + } + }; + + useEffect(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = + messagesContainerRef.current.scrollHeight; + } + if (messages[messages.length - 1].sender === 'user') { + // call llamaindex, receive response + const handleResponseMessage = (response: string) => { + setMessages([...messages, { text: response, sender: 'bot' }]); + setAwaitingResponse(false); + }; + handleResponseMessage('Sample Response'); + } + }, [messages]); + + return ( + + + {messages.map((message, index) => ( + + + {message.text} + + + ))} + + + + + + + ); +} + +export default ChatInterface; diff --git a/app/src/components/Dashboard/ChatPage.tsx b/app/src/components/Dashboard/ChatPage.tsx index fc35db0..3af784f 100644 --- a/app/src/components/Dashboard/ChatPage.tsx +++ b/app/src/components/Dashboard/ChatPage.tsx @@ -5,16 +5,21 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, + Box, Button, Icon, + Text, + theme as defaultTheme, } from '@chakra-ui/react'; import { ipcRenderer } from 'electron'; import log from 'electron-log'; import { useEffect, useRef, useState } from 'react'; import { FiAlertCircle } from 'react-icons/fi'; +import { IoChatbubblesOutline } from 'react-icons/io5'; import { logEvent } from '../../utils/analytics'; import { Footer } from '../Footer'; +import { ChatInterface } from './ChatInterface'; export function ChatPage({ onRefresh }: { onRefresh: () => void }) { const [doesRequireRefresh, setDoesRequireRefresh] = useState(false); @@ -55,11 +60,85 @@ export function ChatPage({ onRefresh }: { onRefresh: () => void }) { }, []); return ( -
-
-
LLM
+
+
+
+ + + +
+
+ + LLM Chat + +
+ +
+ + Ask a chatbot questions about your iMessages + +
+
+
+
+ +
+ + +
-
{}} From c695d5ac3b622bb3b501fd0bb2f75e98bc275cac Mon Sep 17 00:00:00 2001 From: David Lane Date: Mon, 11 Mar 2024 02:01:21 -0700 Subject: [PATCH 3/7] Test connecting to in-memory DB --- app/src/analysis/queries/PrintDBTables.ts | 21 +++++++++++++++++++ .../components/Dashboard/ChatInterface.tsx | 7 ++++++- app/src/main/ipcListeners.ts | 7 +++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/src/analysis/queries/PrintDBTables.ts diff --git a/app/src/analysis/queries/PrintDBTables.ts b/app/src/analysis/queries/PrintDBTables.ts new file mode 100644 index 0000000..56972a3 --- /dev/null +++ b/app/src/analysis/queries/PrintDBTables.ts @@ -0,0 +1,21 @@ +import * as sqlite3 from 'sqlite3'; + +import * as sqlite3Wrapper from '../../utils/sqliteWrapper'; + +// A test function to be replaced with +// an engine for dynamic DB query generated by LLM +export async function printDBTableNames( + db: sqlite3.Database +): Promise { + const q = ` + SELECT + name + FROM + sqlite_master + WHERE + type='table' + ORDER BY + name + `; + return sqlite3Wrapper.allP(db, q); +} diff --git a/app/src/components/Dashboard/ChatInterface.tsx b/app/src/components/Dashboard/ChatInterface.tsx index 89cf46e..e0a593a 100644 --- a/app/src/components/Dashboard/ChatInterface.tsx +++ b/app/src/components/Dashboard/ChatInterface.tsx @@ -1,5 +1,6 @@ import { Box, Button, Flex, Input, Text } from '@chakra-ui/react'; import { waitFor } from '@testing-library/react'; +import { ipcRenderer } from 'electron'; import React, { useEffect, useRef, useState } from 'react'; interface Message { @@ -22,13 +23,17 @@ export function ChatInterface() { setNewMessage(e.target.value); }; - const handleSendMessage = () => { + const handleSendMessage = async () => { if (newMessage.trim()) { setMessages([...messages, { text: newMessage, sender: 'user' }]); setNewMessage(''); // Add logic to handle sending the message to the recipient setAwaitingResponse(true); } + + // To replace with a query to the DB + const toLog = await ipcRenderer.invoke('print-tables'); + console.log(toLog); }; useEffect(() => { diff --git a/app/src/main/ipcListeners.ts b/app/src/main/ipcListeners.ts index 8961314..77a6693 100644 --- a/app/src/main/ipcListeners.ts +++ b/app/src/main/ipcListeners.ts @@ -33,6 +33,7 @@ import { queryInboxRead, } from '../analysis/queries/InboxReadQuery'; import { queryInboxWrite } from '../analysis/queries/InboxWriteQuery'; +import { printDBTableNames } from '../analysis/queries/PrintDBTables'; import { queryRespondReminders } from '../analysis/queries/RespondReminders'; import { querySentimentOverTimeReceived, @@ -92,6 +93,12 @@ export function attachIpcListeners() { return true; }); + // TESTING + ipcMain.handle('print-tables', async () => { + const db = getDb(); + return printDBTableNames(db); + }); + ipcMain.handle('store-get-show-share-tooltip', async () => { return getShowShareTooltip(); }); From 8dc99b75a7104799b492c946dca9fa71a01c3a3d Mon Sep 17 00:00:00 2001 From: David Lane Date: Mon, 11 Mar 2024 12:28:06 -0700 Subject: [PATCH 4/7] Added a rudimentary Text > Query > Response flow (input contact name > query # messages) --- app/src/analysis/queries/RagEngine.ts | 37 ++++++++++++++ .../components/Dashboard/ChatInterface.tsx | 48 +++++++++++-------- app/src/main/ipcListeners.ts | 10 +++- 3 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 app/src/analysis/queries/RagEngine.ts diff --git a/app/src/analysis/queries/RagEngine.ts b/app/src/analysis/queries/RagEngine.ts new file mode 100644 index 0000000..f18685b --- /dev/null +++ b/app/src/analysis/queries/RagEngine.ts @@ -0,0 +1,37 @@ +import * as sqlite3 from 'sqlite3'; + +import * as sqlite3Wrapper from '../../utils/sqliteWrapper'; + +// A test function for me to understand the altered DB schema better :) +export async function printDBTableNames( + db: sqlite3.Database +): Promise { + const q = ` + SELECT + name + FROM + sqlite_master + WHERE + type='table' + ORDER BY + name + `; + return sqlite3Wrapper.allP(db, q); +} + +interface IRAGEngineResults { + message_count: string; +} +export type TRAGEngineResults = IRAGEngineResults[]; + +export async function queryRAGEngine( + db: sqlite3.Database, + message: string +): Promise { + const q = ` + SELECT COUNT(*) AS message_count + FROM core_main_table + WHERE LOWER(contact_name) = LOWER('${message}'); + `; + return sqlite3Wrapper.allP(db, q); +} diff --git a/app/src/components/Dashboard/ChatInterface.tsx b/app/src/components/Dashboard/ChatInterface.tsx index e0a593a..2e61d26 100644 --- a/app/src/components/Dashboard/ChatInterface.tsx +++ b/app/src/components/Dashboard/ChatInterface.tsx @@ -1,5 +1,6 @@ import { Box, Button, Flex, Input, Text } from '@chakra-ui/react'; import { waitFor } from '@testing-library/react'; +import { TRAGEngineResults } from 'analysis/queries/RagEngine'; import { ipcRenderer } from 'electron'; import React, { useEffect, useRef, useState } from 'react'; @@ -8,15 +9,18 @@ interface Message { sender: 'user' | 'bot'; } -const testMessage: Message = { - text: 'Hi there :) You can ask me questions here about your iMessages! For example, try "What should I get mom for her birthday?"', - sender: 'bot', -}; +const initialBotMessage = + 'Hi there :) For now you can type in a contact name to see how many messages have been sent between you!'; +// 'Hi there :) You can ask me questions here about your iMessages!'; // For example, try "What should I get mom for her birthday?"'; export function ChatInterface() { - const [messages, setMessages] = useState([testMessage]); - const [awaitingResponse, setAwaitingResponse] = useState(false); - const [newMessage, setNewMessage] = useState(''); + const [messages, setMessages] = useState([]); + + const [newMessage, setNewMessage] = useState(''); + const [response, setResponse] = useState(initialBotMessage); + + const [awaitingResponse, setAwaitingResponse] = useState(false); + const messagesContainerRef = useRef(null); const handleMessageChange = (e: React.ChangeEvent) => { @@ -29,11 +33,16 @@ export function ChatInterface() { setNewMessage(''); // Add logic to handle sending the message to the recipient setAwaitingResponse(true); - } - // To replace with a query to the DB - const toLog = await ipcRenderer.invoke('print-tables'); - console.log(toLog); + // To replace with a query to the DB + const llmResponse: TRAGEngineResults = await ipcRenderer.invoke( + 'rag-engine', + newMessage + ); + // setResponse(llmResponse); + console.log(llmResponse[0].message_count); + setResponse(llmResponse[0].message_count); + } }; useEffect(() => { @@ -41,16 +50,17 @@ export function ChatInterface() { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; } - if (messages[messages.length - 1].sender === 'user') { - // call llamaindex, receive response - const handleResponseMessage = (response: string) => { - setMessages([...messages, { text: response, sender: 'bot' }]); - setAwaitingResponse(false); - }; - handleResponseMessage('Sample Response'); - } }, [messages]); + useEffect(() => { + // call llamaindex, receive response + setMessages((prevMessages) => [ + ...prevMessages, + { text: response, sender: 'bot' }, + ]); + setAwaitingResponse(false); + }, [response]); + return ( { + const db = getDb(); + return queryRAGEngine(db, message); + }); + ipcMain.handle('store-get-show-share-tooltip', async () => { return getShowShareTooltip(); }); From 7c8e7848d56336769327a460d8b65f6ba15fad42 Mon Sep 17 00:00:00 2001 From: David Lane Date: Mon, 11 Mar 2024 12:28:27 -0700 Subject: [PATCH 5/7] remove an old file I forgot in last commit --- app/src/analysis/queries/PrintDBTables.ts | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 app/src/analysis/queries/PrintDBTables.ts diff --git a/app/src/analysis/queries/PrintDBTables.ts b/app/src/analysis/queries/PrintDBTables.ts deleted file mode 100644 index 56972a3..0000000 --- a/app/src/analysis/queries/PrintDBTables.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as sqlite3 from 'sqlite3'; - -import * as sqlite3Wrapper from '../../utils/sqliteWrapper'; - -// A test function to be replaced with -// an engine for dynamic DB query generated by LLM -export async function printDBTableNames( - db: sqlite3.Database -): Promise { - const q = ` - SELECT - name - FROM - sqlite_master - WHERE - type='table' - ORDER BY - name - `; - return sqlite3Wrapper.allP(db, q); -} From 0397a78e6e7b7f3a99f0f9a1237ad2876b7ff77d Mon Sep 17 00:00:00 2001 From: David Lane Date: Mon, 11 Mar 2024 16:29:59 -0700 Subject: [PATCH 6/7] A super rudimentary pipeline using OpenAI --- app/package.json | 1 + app/src/analysis/queries/RagEngine.ts | 97 +++++++++++++-- .../components/Dashboard/ChatInterface.tsx | 44 ++++--- app/src/components/Dashboard/ChatPage.tsx | 27 ++++- app/src/main/ipcListeners.ts | 4 +- app/yarn.lock | 112 ++++++++++++++++++ 6 files changed, 247 insertions(+), 38 deletions(-) diff --git a/app/package.json b/app/package.json index c485fea..6236091 100644 --- a/app/package.json +++ b/app/package.json @@ -142,6 +142,7 @@ "node-machine-id": "^1.1.12", "node-schedule": "^2.1.0", "nodemailer": "^6.7.7", + "openai": "^4.28.4", "path-browserify": "^1.0.1", "react": "^18.1.0", "react-chartjs-2": "^4.3.1", diff --git a/app/src/analysis/queries/RagEngine.ts b/app/src/analysis/queries/RagEngine.ts index f18685b..fd2ad7a 100644 --- a/app/src/analysis/queries/RagEngine.ts +++ b/app/src/analysis/queries/RagEngine.ts @@ -1,3 +1,4 @@ +import OpenAI from 'openai'; import * as sqlite3 from 'sqlite3'; import * as sqlite3Wrapper from '../../utils/sqliteWrapper'; @@ -19,19 +20,97 @@ export async function printDBTableNames( return sqlite3Wrapper.allP(db, q); } -interface IRAGEngineResults { - message_count: string; -} -export type TRAGEngineResults = IRAGEngineResults[]; - +// Put together a really hacky RAG pipeline... export async function queryRAGEngine( db: sqlite3.Database, - message: string -): Promise { - const q = ` + message: string, + key: string +): Promise { + const openai = new OpenAI({ + apiKey: key, + }); + + let q: string | null = null; + + let prompt = ` + Write a query for a table called core_main_table with this schema: + contact_name, + text (which has the message's text), + date (a unix timestamp number in nanoseconds when the message was sent), + is_from_me (a boolean indicating if I was the sender of the message) + + to answer the following query: ${message} + Please respond with only the raw unformatted SQL and no other text. If this is not possible, or it's hard to get a concrete result based on the schema, return 'Not Possible' + `; + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', // Change the model as per your requirement + messages: [{ role: 'system', content: prompt }], + temperature: 0.7, + max_tokens: 150, + }); + q = response.choices[0].message.content; + console.log(response.choices[0]); + } catch (error) { + console.error(error); + return new Promise((resolve, reject) => { + resolve('An error occurred. Check your API key and try a new message.'); + }); + } + + const query = ` SELECT COUNT(*) AS message_count FROM core_main_table WHERE LOWER(contact_name) = LOWER('${message}'); `; - return sqlite3Wrapper.allP(db, q); + + const queryResult = await sqlite3Wrapper.allP(db, q ?? query); + + function isObject(value: any): value is Record { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof Date) + ); + } + if (!isObject(queryResult[0])) { + console.log(queryResult[0]); + } + const resultString = JSON.stringify(queryResult[0]); + // Sanity check so you don't use don't accidentally use too many tokens... + if (resultString.length > 10000) { + return new Promise((resolve, reject) => { + resolve('An error occurred. Try a new message.'); + }); + } + + prompt = ` + Given this message from a user: ${message}, + this corresponding generated query over a database: ${query}, + and this result of the query ${resultString}: + interpret the result of the query in plain english as a response to the initial message. + `; + + let result = ''; + try { + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', // Change the model as per your requirement + messages: [{ role: 'system', content: prompt }], + temperature: 0.7, + max_tokens: 150, + }); + result = response.choices[0].message.content ?? 'An error occurred'; + console.log(response.choices[0]); + } catch (error) { + console.error(error); + return new Promise((resolve, reject) => { + resolve('An error occurred. Check your API key and try a new message.'); + }); + } + + return new Promise((resolve, reject) => { + resolve(result); // Resolve the promise with a string value + }); } diff --git a/app/src/components/Dashboard/ChatInterface.tsx b/app/src/components/Dashboard/ChatInterface.tsx index 2e61d26..d764d5c 100644 --- a/app/src/components/Dashboard/ChatInterface.tsx +++ b/app/src/components/Dashboard/ChatInterface.tsx @@ -1,6 +1,4 @@ import { Box, Button, Flex, Input, Text } from '@chakra-ui/react'; -import { waitFor } from '@testing-library/react'; -import { TRAGEngineResults } from 'analysis/queries/RagEngine'; import { ipcRenderer } from 'electron'; import React, { useEffect, useRef, useState } from 'react'; @@ -9,15 +7,20 @@ interface Message { sender: 'user' | 'bot'; } -const initialBotMessage = - 'Hi there :) For now you can type in a contact name to see how many messages have been sent between you!'; -// 'Hi there :) You can ask me questions here about your iMessages!'; // For example, try "What should I get mom for her birthday?"'; +const initialBotMessage: Message = { + text: 'Hi there :) You can ask me questions here about your iMessages! For example, try "Who is my best friend?"', + sender: 'bot', +}; -export function ChatInterface() { - const [messages, setMessages] = useState([]); +interface ChatInterfaceProps { + openAIKey: string; +} + +export function ChatInterface(props: ChatInterfaceProps) { + const { openAIKey } = props; + const [messages, setMessages] = useState([initialBotMessage]); const [newMessage, setNewMessage] = useState(''); - const [response, setResponse] = useState(initialBotMessage); const [awaitingResponse, setAwaitingResponse] = useState(false); @@ -31,17 +34,19 @@ export function ChatInterface() { if (newMessage.trim()) { setMessages([...messages, { text: newMessage, sender: 'user' }]); setNewMessage(''); - // Add logic to handle sending the message to the recipient setAwaitingResponse(true); - // To replace with a query to the DB - const llmResponse: TRAGEngineResults = await ipcRenderer.invoke( + const llmResponse: string = await ipcRenderer.invoke( 'rag-engine', - newMessage + newMessage, + openAIKey ); - // setResponse(llmResponse); - console.log(llmResponse[0].message_count); - setResponse(llmResponse[0].message_count); + + setMessages((prevMessages) => [ + ...prevMessages, + { text: llmResponse, sender: 'bot' }, + ]); + setAwaitingResponse(false); } }; @@ -52,15 +57,6 @@ export function ChatInterface() { } }, [messages]); - useEffect(() => { - // call llamaindex, receive response - setMessages((prevMessages) => [ - ...prevMessages, - { text: response, sender: 'bot' }, - ]); - setAwaitingResponse(false); - }, [response]); - return ( void }) { const [showUpdateAvailable, setShowUpdateAvailable] = useState(false); + const [openAIKey, setOpenAIKey] = useState(''); + + const handleKeyChange = (e: React.ChangeEvent) => { + setOpenAIKey(e.target.value); + }; + const cancelRef = useRef(); useEffect(() => { @@ -70,12 +77,19 @@ export function ChatPage({ onRefresh }: { onRefresh: () => void }) { justifyContent: 'space-between', }} > -
+
void }) {
+
void }) { height: 'inherit', }} > - +
diff --git a/app/src/main/ipcListeners.ts b/app/src/main/ipcListeners.ts index 9a05cf6..19abb5b 100644 --- a/app/src/main/ipcListeners.ts +++ b/app/src/main/ipcListeners.ts @@ -102,9 +102,9 @@ export function attachIpcListeners() { return printDBTableNames(db); }); - ipcMain.handle('rag-engine', async (event, message: string) => { + ipcMain.handle('rag-engine', async (event, message: string, key: string) => { const db = getDb(); - return queryRAGEngine(db, message); + return queryRAGEngine(db, message, key); }); ipcMain.handle('store-get-show-share-tooltip', async () => { diff --git a/app/yarn.lock b/app/yarn.lock index 14afdcb..56cac4e 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2137,6 +2137,14 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@^2.6.4": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node-schedule@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-2.1.0.tgz#60375640c0509bab963573def9d1f417f438c290" @@ -2164,6 +2172,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== +"@types/node@^18.11.18": + version "18.19.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.22.tgz#f622f92514b897e6b09903e97c16a0db8e94689f" + integrity sha512-p3pDIfuMg/aXBmhkyanPshdfJuX5c5+bQjYLIikPLXAUycEogij/c50n/C+8XOA5L93cU4ZRXtn+dNQGi0IZqQ== + dependencies: + undici-types "~5.26.4" + "@types/nodemailer@^6.4.4": version "6.4.4" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" @@ -2745,6 +2760,13 @@ abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5: version "1.3.7" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" @@ -3271,6 +3293,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" @@ -3634,6 +3661,11 @@ char-regex@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + chart.js@^3.8.2: version "3.8.2" resolved "https://registry.npmjs.org/chart.js/-/chart.js-3.8.2.tgz" @@ -4127,6 +4159,11 @@ crosspath@^1.0.0: dependencies: "@types/node" "^16.11.7" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" @@ -4507,6 +4544,14 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +digest-fetch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661" + integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA== + dependencies: + base-64 "^0.1.0" + md5 "^2.3.0" + dir-compare@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz" @@ -5450,6 +5495,11 @@ etag@~1.8.1: version "1.8.1" resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -5718,6 +5768,11 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -5727,6 +5782,14 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -6553,6 +6616,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-callable@^1.1.4, is-callable@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz" @@ -7798,6 +7866,15 @@ maximatch@^0.1.0: arrify "^1.0.0" minimatch "^3.0.0" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" @@ -8113,6 +8190,11 @@ node-api-version@^0.1.4: dependencies: semver "^7.3.5" +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -8346,6 +8428,21 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@^4.28.4: + version "4.28.4" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.28.4.tgz#d4bf1f53a89ef151bf066ef284489e12e7dd1657" + integrity sha512-RNIwx4MT/F0zyizGcwS+bXKLzJ8QE9IOyigDG/ttnwB220d58bYjYFp0qjvGwEFBO6+pvFVIDABZPGDl46RFsg== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + digest-fetch "^1.3.0" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + web-streams-polyfill "^3.2.1" + opener@^1.5.2: version "1.5.2" resolved "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz" @@ -10635,6 +10732,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + uniq@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz" @@ -10864,6 +10966,16 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + +web-streams-polyfill@^3.2.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 9127b8668c8b671d40b8b7fe8835e8477cc95b5c Mon Sep 17 00:00:00 2001 From: David Lane Date: Mon, 11 Mar 2024 21:56:01 -0700 Subject: [PATCH 7/7] Updated READMEs --- README.md | 1 + app/README.md | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4140ac..711e32f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Your texting data never leaves your computer. We are proudly open-source for thi - 🔍 filter by a word, friend, or time range - 💯 sentiment analysis - 🎁 "Your Year in Text" experience a.k.a iMessage Wrapped +- 💬 Chat with an LLM to query your data ## Download Left on Read for Mac diff --git a/app/README.md b/app/README.md index 58aabb0..a9357bf 100644 --- a/app/README.md +++ b/app/README.md @@ -116,9 +116,25 @@ You most likely have an old electron process running. If this is warning, you ca If you are getting a "Cannot find module" error, you likely forgot to install the packages. Be sure to run `yarn` in the `app/` directory. - ## Support Buy Me A Coffee Left on Read - iMessages supercharged | Product Hunt + +## LLM Chat (first go) + +How it works: + +1. User inputs a natural language query (How many texts did I send to Mom?) +2. This is passed to LLM to turn it into a SQL Query to run on core_main_table TODO: - give the LLM more of the DB schema +3. Execute the generated query and synthesize query result + original message/query with the LLM to output readable response to the user + +Most of the backend logic happens in app/src/analysis/queries/RAGEngine.ts + +Notes: + +- Currently does not account for messages in group chat vs. not in a group chat, so numbers are misleading. +- Must use the exact spelling of your contacts' name as it appears in the address book. +- Requires an OpenAI API Key and internet access +- Errors (including but not limited to internet connectivity, malformed SQL queries generated by LLM, etc.) are not all handled gracefully (some are OK, like invalid API key)