diff --git a/package-lock.json b/package-lock.json index 3968318..69cbde6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "godmode", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "godmode", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -41,6 +41,7 @@ "@types/jest": "^29.5.2", "@types/node": "20.2.5", "@types/react": "^18.2.8", + "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.4", "@types/react-test-renderer": "^18.0.0", "@types/terser-webpack-plugin": "^5.0.4", @@ -4791,6 +4792,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz", + "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", @@ -23925,6 +23935,15 @@ "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz", + "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", diff --git a/package.json b/package.json index aa48e51..c276bc6 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/jest": "^29.5.2", "@types/node": "20.2.5", "@types/react": "^18.2.8", + "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.4", "@types/react-test-renderer": "^18.0.0", "@types/terser-webpack-plugin": "^5.0.4", diff --git a/src/lib/types.ts b/src/lib/types.ts index 7f9b736..ef3e27b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,7 +5,7 @@ export interface ProviderInterface { fullName: string; shortName: string; webviewId: string; - getWebview(): HTMLElement; + getWebview(): HTMLElement | null; url: string; paneId(): string; setupCustomPasteBehavior(): void; @@ -16,7 +16,12 @@ export interface ProviderInterface { getUserAgent(): string; isEnabled(): boolean; setEnabled(enabled: boolean): void; - clearCookies(): void; + clearCookies?(): void; + + codeForInputElement?: string; + codeForSetInputElementValue?(prompt: string): void; + codeForClickingSubmit?: string; + codeForExtractingResponse?: string; } export interface Settings { diff --git a/src/main/apify.ts b/src/main/apify.ts new file mode 100644 index 0000000..30b7ad9 --- /dev/null +++ b/src/main/apify.ts @@ -0,0 +1,91 @@ +import { ProviderInterface } from 'lib/types'; +import { BrowserWindow } from 'electron'; + +export async function streamChatResponse(opts: { + provider: ProviderInterface; + prompt: string; + sendFn: (...args: any[]) => void | undefined; +}) { + const win = new BrowserWindow({ + // show: true, + show: false, + // titleBarStyle: 'hidden', + // width: 800, + // height: 600, + // webPreferences: { + // webviewTag: true, + // nodeIntegration: true, + // }, + }); + win.loadURL(opts.provider.url); + + return new Promise((resolve, reject) => { + win.webContents.on('dom-ready', async () => { + try { + // check if logged in (and inputElement exists) + await win.webContents.executeJavaScript( + `{${opts.provider.codeForInputElement}}`, + ); + } catch (err) { + console.error( + 'input element doesnt exist: ', + opts.provider.codeForInputElement, + ); + return reject(err); + } + await timeout(500); + const script = `{ + ${opts.provider.codeForInputElement} + ${opts.provider.codeForSetInputElementValue!(opts.prompt)} + ${opts.provider.codeForClickingSubmit} + }`; + await win.webContents.executeJavaScript(script); + console.log('script', script); + + // Define two variables to store the previous responses + let lastResponseHTML = null; + let secondLastResponseHTML = null; + + console.log('looping'); + // Loop until our condition is met + await timeout(300); + while (true) { + await timeout(300); + // await win.webContents.executeJavaScript( + // `console.log('hiii', [...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')]);`, + // ); + var responseHTML = await win.webContents.executeJavaScript( + `${opts.provider.codeForExtractingResponse}.innerHTML`, + ); + var responseText = await win.webContents.executeJavaScript( + `${opts.provider.codeForExtractingResponse}.innerText`, + ); + + console.log({ responseHTML, secondLastResponseHTML }); + // If responseHTML hasn't changed for 2 invocations, break + if ( + responseHTML === lastResponseHTML && + responseHTML === secondLastResponseHTML + ) { + console.log('prompting'); + break; + } + + // Shift our stored responses for the next loop iteration + secondLastResponseHTML = lastResponseHTML; + lastResponseHTML = responseHTML; + + console.log('sendFn', responseText); + opts.sendFn(responseHTML, responseText); // stream incomplete responses back + } + console.log('closing'); + win.close(); + return resolve({ responseHTML, responseText }); + }); + }); +} +// thanks claude + +function timeout(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/main/main.ts b/src/main/main.ts index 84ccb5c..c1fc8d7 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,8 +23,10 @@ import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; import Store from 'electron-store'; import MenuBuilder from './menu'; +import { streamChatResponse } from './apify'; import { resolveHtmlPath } from './util'; import { isValidShortcut } from '../lib/utils'; +import PerplexityLlama from '../providers/perplexity-llama'; let store = new Store(); @@ -66,61 +68,15 @@ ipcMain.on('get-always-on-top', async (event, property, val) => { event.returnValue = bool; }); -// thanks claude - -function timeout(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -async function getLlamaResponse(prompt: string) { - const win = new BrowserWindow({ - // show: true, - show: false, - // titleBarStyle: 'hidden', - // width: 800, - // height: 600, - // webPreferences: { - // webviewTag: true, - // nodeIntegration: true, - // }, +ipcMain.on('prompt-hidden-chat', async (event, channel: string, prompt) => { + const sendFn = (...args: any[]) => + mainWindow?.webContents.send(channel, ...args); + const done = await streamChatResponse({ + provider: PerplexityLlama, + prompt, + sendFn, }); - win.loadURL('https://labs.perplexity.ai'); - return new Promise((resolve, reject) => { - win.webContents.on('dom-ready', async () => { - await win.webContents.executeJavaScript(`{ - var selectElement = document.querySelector('#lamma-select'); - selectElement.value = 'llama-2-70b-chat'; - - var inputElement = document.querySelector('textarea[placeholder*="Ask"]'); // can be "Ask anything" or "Ask follow-up" - inputElement.focus(); - var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; - nativeTextAreaValueSetter.call(inputElement, \`${prompt}\`); - - var event = new Event('input', { bubbles: true}); - inputElement.dispatchEvent(event); - var buttons = Array.from(document.querySelectorAll('button.bg-super')); - var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); - var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; - button.click(); - }`); - await timeout(5000); - // const temp = await win.webContents.executeJavaScript(` - // [...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')].map(x => x.innerHTML) - // `); - // console.log('temp', temp); - const responseHTML = await win.webContents.executeJavaScript(` - [...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')].slice(-1)[0].innerHTML - `); - const responseText = await win.webContents.executeJavaScript(` - [...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')].slice(-1)[0].innerText - `); - resolve({ responseHTML, responseText }); - win.close(); - }); - }); -} -ipcMain.on('prompt-llama2', async (event, val) => { - const response = await getLlamaResponse(val); - event.returnValue = response; + event.returnValue = done; // {responseHTML, responseText} }); /* @@ -221,11 +177,11 @@ const createWindow = async () => { const menuBuilder = new MenuBuilder(mainWindow); menuBuilder.buildMenu(); - // // Open urls in the user's browser - // mainWindow.webContents.setWindowOpenHandler((edata) => { - // shell.openExternal(edata.url); - // return { action: 'allow' }; - // }); + // Open urls in the user's browser + mainWindow.webContents.setWindowOpenHandler((edata) => { + shell.openExternal(edata.url); + return { action: 'allow' }; + }); // Remove this if your app does not use auto updates // eslint-disable-next-line diff --git a/src/main/preload.ts b/src/main/preload.ts index f9e559d..d6681b9 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -2,7 +2,7 @@ /* eslint no-unused-vars: off */ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -export type Channels = 'ipc-example'; +export type Channels = 'ipc-example' | 'perplexity-llama2'; const electronHandler = { ipcRenderer: { @@ -44,9 +44,8 @@ const electronHandler = { setAlwaysOnTop(val: any) { ipcRenderer.send('set-always-on-top', val); }, - promptLlama2(prompt: string) { - const response = ipcRenderer.sendSync('prompt-llama2', prompt); - return response; + promptHiddenChat(prompt: string) { + ipcRenderer.send('prompt-hidden-chat', 'perplexity-llama2', prompt); }, }, }; diff --git a/src/providers/bing.js b/src/providers/bing.js index 36093f7..c354df0 100644 --- a/src/providers/bing.js +++ b/src/providers/bing.js @@ -20,15 +20,9 @@ class Bing extends Provider { // SERP Shadow DOM var serpDOM = document.querySelector('.cib-serp-main'); - if (!serpDOM) { - console.error('serpDOM for ${fullName} doesnt exist, have you logged in or are you on the right page?') - } // Action Bar Shadow DOM var inputDOM = serpDOM.shadowRoot.querySelector('#cib-action-bar-main'); - if (!inputDOM) { - console.error('inputDOM for ${fullName} doesnt exist, have you logged in or are you on the right page?') - } // Text Input Shadow DOM var textInputDOM = inputDOM.shadowRoot.querySelector('cib-text-input'); @@ -36,7 +30,11 @@ class Bing extends Provider { // This inner cib-text-input Shadow DOM is not always present var inputElement = textInputDOM ? textInputDOM.shadowRoot.querySelector('#searchbox') : inputDOM.shadowRoot.querySelector('#searchbox'); - simulateUserInput(inputElement, \`${input}\`); + if (!inputElement) { + console.error('inputElement for \`${fullName}\` doesnt exist, have you logged in or are you on the right page?') + } else { + simulateUserInput(inputElement, \`${input}\`); + } } `); } diff --git a/src/providers/claude2.js b/src/providers/claude2.js index 3a08ba0..36648ed 100644 --- a/src/providers/claude2.js +++ b/src/providers/claude2.js @@ -78,7 +78,7 @@ class Claude2 extends Provider { } static isEnabled() { - return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); + return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); } } diff --git a/src/providers/perplexity-llama.js b/src/providers/perplexity-llama.js index ada744e..01c4e2d 100644 --- a/src/providers/perplexity-llama.js +++ b/src/providers/perplexity-llama.js @@ -27,6 +27,25 @@ class PerplexityLlama extends Provider { } } + static codeForInputElement = `var inputElement = document.querySelector('textarea[placeholder*="Ask"]');`; + static codeForSetInputElementValue(prompt) { + return ` + var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; + nativeTextAreaValueSetter.call(inputElement, \`${prompt}\`); + var event = new Event('input', { bubbles: true}); + inputElement.dispatchEvent(event); + `; + } + static codeForClickingSubmit = ` + var buttons = Array.from(document.querySelectorAll('button.bg-super')); + var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); + + var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; + + button.click(); + `; + static codeForExtractingResponse = `[...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')].slice(-1)[0]`; // dont append semicolon, we will append innerhtml etc + static handleSubmit() { try { this.getWebview().executeJavaScript(`{ diff --git a/src/providers/provider.js b/src/providers/provider.js index 34a5869..1e33b8d 100644 --- a/src/providers/provider.js +++ b/src/providers/provider.js @@ -74,7 +74,7 @@ class Provider { } static getUserAgent() { - return false; + return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.37'; } static isEnabled() { diff --git a/src/providers/smol.js b/src/providers/smol.js index 0161900..42a5b1e 100644 --- a/src/providers/smol.js +++ b/src/providers/smol.js @@ -42,11 +42,13 @@ class SmolTalk extends Provider { this.getWebview().executeJavaScript(`{ var btn = document.querySelector('#smol-submitbtn'); + if (btn) { + btn.focus(); + btn.setAttribute("aria-disabled", "false"); // doesnt work alone + btn.disabled = false; + btn.click() + } - btn.focus(); - btn.setAttribute("aria-disabled", "false"); // doesnt work alone - btn.disabled = false; - btn.click() }`); } diff --git a/src/renderer/browserPane.tsx b/src/renderer/browserPane.tsx index 2c737da..d023961 100644 --- a/src/renderer/browserPane.tsx +++ b/src/renderer/browserPane.tsx @@ -5,26 +5,17 @@ import { ChevronUpDownIcon, CheckIcon, Cog6ToothIcon, - SparklesIcon, + // SparklesIcon, } from '@heroicons/react/20/solid'; import { BookmarkIcon, BookmarkSlashIcon } from '@heroicons/react/20/solid'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { type paneInfo } from './layout'; import { ProviderInterface } from 'lib/types'; -// @ts-ignore -import vex from 'vex-js'; -// Main css -import 'vex-js/dist/css/vex.css'; -// Themes (Import all themes you want to use here) -import 'vex-js/dist/css/vex-theme-default.css'; -import 'vex-js/dist/css/vex-theme-os.css'; -vex.registerPlugin(require('vex-dialog')); -vex.defaultOptions.className = 'vex-theme-os'; -import { promptCritic, promptImprover } from './promptImprover'; +import { PromptCritic } from './promptCritic'; // https://tailwindui.com/components/application-ui/elements/dropdowns -function classNames(...classes) { +function classNames(...classes: string[]) { return classes.filter(Boolean).join(' '); } @@ -66,67 +57,7 @@ export function BrowserPane({ webviewId: 'nullProvider', shortName: 'Select a provider', fullName: 'Select a provider', - }; - - async function runPromptCritic() { - if (superprompt.length < 10) { - alert( - 'superprompt is too short. write a longer one! e.g. "write a receipe for scrambled eggs"', - ); - return; - } - console.log('promptCritic', superprompt); - var llama2response = window.electron.browserWindow.promptLlama2( - promptCritic(superprompt), - ); - // console.log('stage 1 response', llama2response); - llama2response = await new Promise((res) => - vex.dialog.prompt({ - unsafeMessage: ` -
-

PromptCritic analysis

-
- ${llama2response.responseHTML}`, - placeholder: `what you'd like to change about your prompt`, - callback: res, - }), - ); - if (llama2response === null) return; - // console.log('stage 2 response', llama2response); - var prospectivePrompt = window.electron.browserWindow.promptLlama2( - promptImprover(superprompt, llama2response), - ); - // console.log('stage 3 response', prospectivePrompt); - - const textareavalue = prospectivePrompt.responseText.replace( - /\r|\n/, - '
', - ); - var finalPrompt: string | null = await new Promise((res) => - vex.dialog.prompt({ - unsafeMessage: ` -
-

PromptCritic's Improved suggestion

-
- ${prospectivePrompt.responseHTML}`, - value: prospectivePrompt.responseText, - input: ``, - placeholder: `your final prompt; copy and paste from above if it helps`, - callback: (data: any) => { - console.log({ data }); - if (!data) { - console.log('Cancelled'); - } else { - res(data); - } - }, - }), - ); - console.log('finalPrompt', finalPrompt); - if (finalPrompt != null) { - setSuperprompt(finalPrompt); - } - } + } as ProviderInterface; function onDragEnd(result: { source: { index: number }; @@ -316,18 +247,9 @@ export function BrowserPane({ {({ active }) => ( - + )} @@ -375,7 +297,12 @@ export function BrowserPane({ } // https://tailwindui.com/components/application-ui/forms/select-menus -export default function ListBox({ selected, setSelected, selectList }) { +export default function ListBox(props: { + selected: ProviderInterface; + setSelected: (p: ProviderInterface) => void; + selectList: ProviderInterface[]; +}) { + const { selected, setSelected, selectList } = props; return ( {({ open }) => ( diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index cfb7576..2bacbfd 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -11,3 +11,12 @@ window.electron.ipcRenderer.once('ipc-example', (arg) => { console.log(arg); }); window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']); + +window.electron.ipcRenderer.on('perplexity-llama2', (args) => { + console.log(`args[0]`, args); + // comes from main.ts ipcMain.on('prompt-hidden-chat',...) sendFn(responseHTML, responseText) + var target = document.getElementById('streamingPromptResponseContainer'); + if (target) { + target.innerHTML = args as string; + } +}); diff --git a/src/renderer/layout.tsx b/src/renderer/layout.tsx index 0c7b736..be0c0eb 100644 --- a/src/renderer/layout.tsx +++ b/src/renderer/layout.tsx @@ -12,16 +12,6 @@ import { BrowserPane } from './browserPane'; import { ProviderInterface } from 'lib/types'; import { TitleBar } from './TitleBar'; import SettingsMenu from './components/settings'; -// import { promptCritic, promptImprover } from './promptImprover'; -// // @ts-ignore -// import vex from 'vex-js'; -// // Main css -// import 'vex-js/dist/css/vex.css'; -// // Themes (Import all themes you want to use here) -// import 'vex-js/dist/css/vex-theme-default.css'; -// import 'vex-js/dist/css/vex-theme-os.css'; -// vex.registerPlugin(require('vex-dialog')); -// vex.defaultOptions.className = 'vex-theme-os'; // @ts-ignore export type paneInfo = { webviewId: string; shortName: string }; @@ -252,79 +242,6 @@ export default function Layout() { - New chat: Cmd+R or Reset windows evenly: Cmd+Shift+A" />
- {/* */} + ); +} + +// https://tailwindui.com/components/application-ui/elements/dropdowns +function classNames(...classes: string[]) { + return classes.filter(Boolean).join(' '); +} diff --git a/src/renderer/promptImprover.ts b/src/renderer/promptImprover.ts deleted file mode 100644 index 3168423..0000000 --- a/src/renderer/promptImprover.ts +++ /dev/null @@ -1,43 +0,0 @@ -export function promptCritic(originalPrompt: string) { - return `I need to improve the original prompt: - - --- Original Prompt --- - ${originalPrompt} - --- End Original Prompt --- - - There are known ways to improve prompts for better LLM performance. - Can you please briefly (in <200 words, mostly
  1. bullet points with bolded headings per bullet) criticize the original prompt? - `; - // For example: - // - for general knowledge questions, appending "Let's think step by step to get to the right answer." is known to do well. - // - for creative writing, setting temperature=200 and adding exciting adjectives, writing in the style of Hunter S Thompson and Winston Churchill and other well known authors. - // - for code generation, first ask for the high level implementation plan in comments, then make sure each non-trivial line of code is preceded by a comment explaining what it does. -} - -export function promptImprover( - originalPrompt: string, - modifyInstructions?: string, -) { - return `I need to add more detail to the original prompt: - - --- Original Prompt --- - ${originalPrompt} - --- End Original Prompt --- - - ${ - modifyInstructions - ? `My modification instructions are: ${modifyInstructions}` - : '' - } - Please suggest a newer, more detailed (still <300 words) version of this prompt that improves LLM performance. - Do not preface with any conversation or small talk, only reply with the improved prompt. - `; - - // For example: - // - for general knowledge questions, appending "Let's think step by step to get to the right answer." is known to do well. - // - for creative writing, setting temperature=200 and adding exciting adjectives, writing in the style of Hunter S Thompson and Winston Churchill and other well known authors. - // - for code generation, first ask for the high level implementation plan in comments, then make sure each non-trivial line of code is preceded by a comment explaining what it does. - - // Do not preface with any conversation or small talk, only reply with the improved prompt. - // `; -}