├── web ├── .editorconfig ├── .python-version ├── .tool-versions ├── .yarnrc.yml ├── src │ ├── components │ │ ├── common │ │ │ ├── yaml.worker.js │ │ │ ├── ChatContent.css │ │ │ ├── setupScript.ts │ │ │ ├── SettingsBlock.tsx │ │ │ ├── AbortTaskButton.tsx │ │ │ ├── RunTaskButton.tsx │ │ │ ├── PlannerConfig.tsx │ │ │ ├── QuickActionButton.tsx │ │ │ ├── ModelDropdown.tsx │ │ │ ├── VoiceInputButton.tsx │ │ │ ├── Thumbnails.tsx │ │ │ ├── SemanticLayer.tsx │ │ │ ├── CreditsPill.tsx │ │ │ ├── AutosizeTextarea.tsx │ │ │ ├── Subscription.tsx │ │ │ ├── Suggestions.tsx │ │ │ └── UIElements.ts │ │ └── devtools │ │ │ ├── CustomInstructions.tsx │ │ │ └── DataCatalog.tsx │ ├── assets │ │ └── img │ │ │ ├── logo.png │ │ │ ├── icon-128.png │ │ │ ├── icon-34.png │ │ │ ├── logo_x.svg │ │ │ └── logo.svg │ ├── declarations.d.ts │ ├── globals.d.ts │ ├── helpers │ │ ├── extensionId.ts │ │ ├── platformCustomization.ts │ │ ├── recordings.ts │ │ ├── templatize.ts │ │ ├── countTokens.ts │ │ ├── app.ts │ │ ├── slugg.d.ts │ │ ├── sqlProcessor.ts │ │ ├── tests │ │ │ └── script.js │ │ ├── templatize.test.ts │ │ ├── nativeEvents.ts │ │ ├── documentSubscription.ts │ │ ├── LLM │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── origin.ts │ │ ├── screenCapture │ │ │ └── extensionCapture.ts │ │ └── rpcCalls.ts │ ├── app │ │ ├── api │ │ │ ├── billing.ts │ │ │ ├── index.ts │ │ │ ├── notifications.ts │ │ │ ├── concurrency.ts │ │ │ └── planner.ts │ │ ├── toast.ts │ │ ├── index.html │ │ ├── clarification.ts │ │ ├── sidechat.ts │ │ ├── userConfirmation.ts │ │ └── appSettings.ts │ ├── planner │ │ ├── cotPlan.ts │ │ └── metaPlan.ts │ ├── env.defaults.json │ ├── state │ │ ├── dispatch.ts │ │ ├── configs │ │ │ └── reducer.ts │ │ ├── cache │ │ │ └── reducer.ts │ │ ├── chat │ │ │ └── types.ts │ │ ├── auth │ │ │ └── reducer.ts │ │ ├── billing │ │ │ └── reducer.ts │ │ ├── semantic-layer │ │ │ └── reducer.ts │ │ └── notifications │ │ │ └── reducer.ts │ ├── types.d.ts │ ├── types │ │ └── notifications.ts │ ├── chat │ │ └── chat.test.ts │ ├── package.ts │ ├── cache │ │ ├── events.ts │ │ ├── cache.test.ts │ │ └── indexedDb.ts │ ├── hooks │ │ └── useCustomCSS.ts │ └── tracking │ │ ├── init.ts │ │ └── custom │ │ └── index.ts ├── public │ ├── screenshots │ │ ├── addons-menu.png │ │ ├── addons-install.png │ │ ├── addons-search.png │ │ ├── addons-activate.png │ │ └── addons-unavailable.png │ ├── popup.html │ ├── logo_x.svg │ ├── logo_x_light.svg │ ├── extension.css │ └── extension.json ├── .vscode │ └── settings.json ├── jest.env.js ├── licenses │ ├── FOSS.md │ └── TaxyAI-license.md ├── .prettierrc ├── setupJest.js ├── docker-compose.yaml ├── babel.config.js ├── README.md ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── Dockerfile └── vite.config.ts ├── .python-version ├── .vscode └── settings.json ├── extension ├── .python-version ├── .tool-versions ├── .yarnrc.yml ├── src │ ├── content │ │ ├── RPCs │ │ │ ├── log.ts │ │ │ ├── copyToClipboard.ts │ │ │ ├── sendIFrameMessage.ts │ │ │ ├── ripple.ts │ │ │ ├── microphone.ts │ │ │ ├── elementScreenCapture.ts │ │ │ ├── crossInstanceComms.ts │ │ │ ├── fetchData.ts │ │ │ └── domActions.ts │ │ ├── apps.ts │ │ ├── debug.ts │ │ ├── types.d.ts │ │ ├── dragAndToggle.ts │ │ └── polyfill.ts │ ├── assets │ │ └── img │ │ │ ├── logo.png │ │ │ ├── icon-34.png │ │ │ ├── icon-128.png │ │ │ ├── logo_x.svg │ │ │ └── logo.svg │ ├── globals.d.ts │ ├── helpers │ │ ├── utils.ts │ │ ├── setupScript.ts │ │ ├── setupStyles.ts │ │ ├── pageParse │ │ │ ├── getElements.ts │ │ │ ├── querySelectorTypes.d.ts │ │ │ └── resolveSelectors.ts │ │ └── components │ │ │ └── RecursiveComponent.tsx │ ├── package.ts │ ├── popup │ │ ├── index.tsx │ │ ├── index.html │ │ └── Popup.tsx │ ├── background │ │ ├── RPCs │ │ │ ├── captureVisibleTab.ts │ │ │ └── chromeDebugger.ts │ │ ├── identifier.ts │ │ └── index.ts │ ├── env.defaults.json │ ├── apps-script │ │ ├── README.md │ │ └── gsheets │ │ │ └── appsscript.json │ ├── manifest.json │ ├── posthog │ │ └── index.ts │ ├── types.d.ts │ └── constants.ts ├── .vscode │ └── settings.json ├── licenses │ ├── FOSS.md │ └── TaxyAI-license.md ├── .prettierrc ├── utils │ ├── env.js │ ├── build.js │ └── webserver.js ├── babel.config.js ├── .gitignore ├── tsconfig.json ├── README.md ├── .eslintrc └── package.json ├── simulator ├── src │ ├── data │ │ └── authState.json │ ├── env.defaults.json │ ├── mods │ │ ├── types.ts │ │ └── runAllCells.ts │ ├── utils │ │ ├── constants.ts │ │ ├── login.ts │ │ ├── teardown.ts │ │ ├── appState.ts │ │ ├── setup.ts │ │ ├── minusxState.ts │ │ └── testRunner.ts │ ├── constants.ts │ ├── checks │ │ ├── types.ts │ │ ├── checkAppState.ts │ │ └── checkRespondToUser.ts │ ├── specs │ │ └── jupyter │ │ │ ├── test_01.spec.ts │ │ │ └── test_02.spec.ts │ └── fixtures │ │ ├── frame.ts │ │ ├── base.ts │ │ └── jupyter.ts ├── .gitignore ├── docker │ └── jupyter.Dockerfile ├── docker-compose.yaml ├── package.json ├── .github │ └── workflows │ │ └── playwright.yml └── README.md ├── .yarnrc.yml ├── apps ├── src │ ├── globals.d.ts │ ├── package.extension.ts │ ├── declarations.d.ts │ ├── google │ │ ├── appSetup.ts │ │ ├── fingerprint.ts │ │ ├── sampleCode.js │ │ └── googleSheetInternalState.ts │ ├── base │ │ ├── appSetup.ts │ │ ├── appHook.ts │ │ ├── prompts.ts │ │ └── appState.ts │ ├── posthog │ │ ├── appSetup.ts │ │ ├── fingerprint.ts │ │ ├── stateSchema.ts │ │ ├── posthogObserver.ts │ │ ├── defaultState.ts │ │ ├── types.ts │ │ ├── inject.ts │ │ └── querySelectorMap.ts │ ├── jupyter │ │ ├── nb-to-md-report │ │ │ ├── llmConfig.ts │ │ │ └── prompts.ts │ │ ├── appSetup.ts │ │ ├── fingerprint.ts │ │ └── defaultState.ts │ ├── metabase │ │ ├── helpers │ │ │ ├── slugg.d.ts │ │ │ ├── stateSubscriptions.ts │ │ │ ├── dashboard │ │ │ │ ├── util.ts │ │ │ │ └── runSqlQueryFromDashboard.ts │ │ │ ├── parseTables.ts │ │ │ └── parseSql.ts │ │ ├── appSetup.ts │ │ └── fingerprint.ts │ ├── appStateConfigs.ts │ ├── types.d.ts │ ├── appSetupConfigs.ts │ └── package.ts ├── jest.config.js ├── tsconfig.json └── package.json ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── .github └── workflows │ └── deploy.yml ├── LICENSE.txt └── setup.md /web/.editorconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.4 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /web/.python-version: -------------------------------------------------------------------------------- 1 | 3.11.4 2 | -------------------------------------------------------------------------------- /web/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 19.7.0 -------------------------------------------------------------------------------- /extension/.python-version: -------------------------------------------------------------------------------- 1 | 3.11.4 2 | -------------------------------------------------------------------------------- /extension/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 19.7.0 -------------------------------------------------------------------------------- /simulator/src/data/authState.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /apps/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md'; 2 | -------------------------------------------------------------------------------- /web/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /extension/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /web/src/components/common/yaml.worker.js: -------------------------------------------------------------------------------- 1 | import "monaco-yaml/yaml.worker.js"; -------------------------------------------------------------------------------- /apps/src/package.extension.ts: -------------------------------------------------------------------------------- 1 | export { getAppSetupConfigs } from "./appSetupConfigs"; -------------------------------------------------------------------------------- /simulator/src/env.defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "TEST_EMAIL": "", 3 | "TEST_OTP": "" 4 | } -------------------------------------------------------------------------------- /web/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/src/assets/img/logo.png -------------------------------------------------------------------------------- /extension/src/content/RPCs/log.ts: -------------------------------------------------------------------------------- 1 | export async function log(...args: any[]) { 2 | console.log(...args) 3 | } -------------------------------------------------------------------------------- /web/src/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/src/assets/img/icon-128.png -------------------------------------------------------------------------------- /web/src/assets/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/src/assets/img/icon-34.png -------------------------------------------------------------------------------- /extension/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/extension/src/assets/img/logo.png -------------------------------------------------------------------------------- /apps/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" 2 | { 3 | const content: string; 4 | export default content; 5 | } -------------------------------------------------------------------------------- /extension/src/assets/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/extension/src/assets/img/icon-34.png -------------------------------------------------------------------------------- /web/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" 2 | { 3 | const content: string; 4 | export default content; 5 | } -------------------------------------------------------------------------------- /extension/src/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/extension/src/assets/img/icon-128.png -------------------------------------------------------------------------------- /web/public/screenshots/addons-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/public/screenshots/addons-menu.png -------------------------------------------------------------------------------- /web/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // allow .png imports 2 | declare module '*.png' { 3 | const value: any; 4 | export = value; 5 | } 6 | -------------------------------------------------------------------------------- /extension/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // allow .png imports 2 | declare module '*.png' { 3 | const value: any; 4 | export = value; 5 | } 6 | -------------------------------------------------------------------------------- /web/public/screenshots/addons-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/public/screenshots/addons-install.png -------------------------------------------------------------------------------- /web/public/screenshots/addons-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/public/screenshots/addons-search.png -------------------------------------------------------------------------------- /simulator/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | .playwright/* -------------------------------------------------------------------------------- /web/public/screenshots/addons-activate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/public/screenshots/addons-activate.png -------------------------------------------------------------------------------- /extension/src/content/apps.ts: -------------------------------------------------------------------------------- 1 | import { getAppSetupConfigs } from "apps/extension"; 2 | 3 | export const appSetupConfigs = getAppSetupConfigs(); -------------------------------------------------------------------------------- /web/public/screenshots/addons-unavailable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minusxai/minusx/HEAD/web/public/screenshots/addons-unavailable.png -------------------------------------------------------------------------------- /web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.enablePromptUseWorkspaceTsdk": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.enablePromptUseWorkspaceTsdk": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /web/public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello world 6 | 7 | -------------------------------------------------------------------------------- /web/src/components/common/ChatContent.css: -------------------------------------------------------------------------------- 1 | .markdown > * { 2 | all: revert; 3 | } 4 | 5 | .markdown { 6 | width: 100%; 7 | overflow: auto; 8 | } -------------------------------------------------------------------------------- /web/jest.env.js: -------------------------------------------------------------------------------- 1 | // global.setImmediate = jest.useRealTimers; 2 | global.setImmediate = global.setImmediate || ((fn, ...args) => global.setTimeout(fn, 0, ...args)); -------------------------------------------------------------------------------- /web/licenses/FOSS.md: -------------------------------------------------------------------------------- 1 | Credit to the following FOSS tools which were used when building MinusX 2 | 3 | TaxyAI Browser Extension: https://github.com/TaxyAI/browser-extension -------------------------------------------------------------------------------- /extension/licenses/FOSS.md: -------------------------------------------------------------------------------- 1 | Credit to the following FOSS tools which were used when building MinusX 2 | 3 | TaxyAI Browser Extension: https://github.com/TaxyAI/browser-extension -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "requirePragma": false, 7 | "arrowParens": "always" 8 | } -------------------------------------------------------------------------------- /extension/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "requirePragma": false, 7 | "arrowParens": "always" 8 | } -------------------------------------------------------------------------------- /extension/utils/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | module.exports = { 3 | NODE_ENV: process.env.NODE_ENV || 'development', 4 | PORT: process.env.PORT || 3001, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} **/ 2 | export default { 3 | testEnvironment: "node", 4 | transform: { 5 | "^.+.tsx?$": ["ts-jest",{}], 6 | }, 7 | }; -------------------------------------------------------------------------------- /extension/src/content/RPCs/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | // copy provided text to clipboard 2 | export async function copyToClipboard(text: string) { 3 | await navigator.clipboard.writeText(text); 4 | } 5 | -------------------------------------------------------------------------------- /extension/src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(ms: number = 0) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | 5 | export type Subset = K; -------------------------------------------------------------------------------- /web/src/helpers/extensionId.ts: -------------------------------------------------------------------------------- 1 | import { getParsedIframeInfo } from './origin'; 2 | 3 | export const getExtensionID = () => { 4 | const parsed = getParsedIframeInfo() 5 | return parsed.r 6 | } -------------------------------------------------------------------------------- /web/src/helpers/platformCustomization.ts: -------------------------------------------------------------------------------- 1 | export function getPlatformShortcut() { 2 | const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; 3 | return isMac ? '⌘ + k' : 'Ctrl + k'; 4 | } 5 | -------------------------------------------------------------------------------- /web/setupJest.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | import dotenv from 'dotenv'; 3 | import path from 'path'; 4 | 5 | const envPath = path.resolve(__dirname, '.env.development'); 6 | dotenv.config({ path: envPath }); 7 | -------------------------------------------------------------------------------- /simulator/docker/jupyter.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jupyter/base-notebook 2 | RUN mkdir -p /home/jovyan/data 3 | RUN pip install --upgrade pip 4 | RUN pip install pandas numpy plotly 5 | COPY ./data /home/jovyan/data 6 | EXPOSE 8888 -------------------------------------------------------------------------------- /simulator/src/mods/types.ts: -------------------------------------------------------------------------------- 1 | import { Frame, Page } from "playwright/test"; 2 | 3 | interface TestModArg { 4 | frame: Frame; 5 | page: Page 6 | } 7 | 8 | export type TestMod = (modArg: TestModArg) => Promise 9 | -------------------------------------------------------------------------------- /extension/src/content/debug.ts: -------------------------------------------------------------------------------- 1 | import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; 2 | import { fireEvent } from '@testing-library/dom'; 3 | window.userEvent = userEvent 4 | window.fireEvent = fireEvent 5 | -------------------------------------------------------------------------------- /web/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web-server: 5 | build: 6 | context: .. 7 | dockerfile: ./web/Dockerfile 8 | restart: unless-stopped 9 | ports: 10 | - "3005:3005" 11 | -------------------------------------------------------------------------------- /extension/src/package.ts: -------------------------------------------------------------------------------- 1 | export { resolveSelector } from './helpers/pageParse/resolveSelectors' 2 | export { initWindowListener } from './content/RPCs/initListeners' 3 | export { sendIFrameMessage } from './content/RPCs/sendIFrameMessage' 4 | -------------------------------------------------------------------------------- /apps/src/google/appSetup.ts: -------------------------------------------------------------------------------- 1 | import { AppSetup } from "../base/appSetup"; 2 | import { googleDocFingerprintMatcher } from "./fingerprint" 3 | 4 | export class GoogleSetup extends AppSetup { 5 | fingerprintMatcher = googleDocFingerprintMatcher; 6 | } -------------------------------------------------------------------------------- /web/src/app/api/billing.ts: -------------------------------------------------------------------------------- 1 | import { configs } from '../../constants' 2 | import axios, { AxiosError } from 'axios'; 3 | 4 | export const getBillingInfo = async () => { 5 | const response = await axios.get(`${configs.SERVER_BASE_URL}/billing/info`) 6 | return response.data 7 | } -------------------------------------------------------------------------------- /simulator/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | jupyter: 5 | build: 6 | context: ./src 7 | dockerfile: ../docker/jupyter.Dockerfile 8 | ports: 9 | - "8888:8888" 10 | environment: 11 | JUPYTER_TOKEN: "jupyter_test_token" 12 | -------------------------------------------------------------------------------- /apps/src/base/appSetup.ts: -------------------------------------------------------------------------------- 1 | import { ToolMatcher } from 'extension/types' 2 | 3 | // runs in content script context 4 | export abstract class AppSetup { 5 | abstract fingerprintMatcher: ToolMatcher; 6 | 7 | // 1. Handles setup 8 | async setup(extensionConfigs: Promise) {}; 9 | } -------------------------------------------------------------------------------- /simulator/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | const base_constants = { 2 | JUPYTER_SERVER_URL: 'http://localhost:8888', 3 | JUPYTER_TOKEN: 'jupyter_test_token' 4 | } 5 | 6 | export const constants = { 7 | ...base_constants, 8 | JUPYTER_SERVER_PATH: `${base_constants.JUPYTER_SERVER_URL}/lab/tree/data` 9 | } -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { targets: { node: "current" } }], 5 | "@babel/preset-typescript", 6 | "@emotion/babel-preset-css-prop" 7 | ], 8 | plugins: [ 9 | "react-hot-loader/babel" 10 | ] 11 | } -------------------------------------------------------------------------------- /extension/src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { Popup } from './Popup'; 3 | import React from 'react'; 4 | 5 | const container = document.getElementById('root'); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/background/RPCs/captureVisibleTab.ts: -------------------------------------------------------------------------------- 1 | export const captureVisibleTab = async () => { 2 | return await new Promise((resolve) => { 3 | chrome.tabs.captureVisibleTab(null, { format: 'png' }, dataUrl => { 4 | console.log('Data url retrieved is', dataUrl) 5 | resolve(dataUrl) 6 | }); 7 | }) 8 | } -------------------------------------------------------------------------------- /extension/src/env.defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "BASE_SERVER_URL": "https://v2.minusxapi.com", 3 | "SERVER_PATH": "", 4 | "WEB_URL": "http://localhost:3005", 5 | "WEB_CSS_CONFIG_PATH": "/extension.css", 6 | "WEB_JSON_CONFIG_PATH": "/extension.json", 7 | "POSTHOG_API_KEY": "", 8 | "POSTHOG_CONFIGS": "{}" 9 | } -------------------------------------------------------------------------------- /extension/src/content/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IframeInfo { 2 | tool: string 3 | toolVersion: string 4 | origin: string 5 | mode: string 6 | r: string 7 | variant: 'default' | 'instructions' 8 | width: string, 9 | gitCommitId: string, 10 | npmPackageVersion: string, 11 | isEmbedded: boolean 12 | } -------------------------------------------------------------------------------- /extension/src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/helpers/recordings.ts: -------------------------------------------------------------------------------- 1 | let _transcripts: string[] = [] 2 | 3 | export const storeTranscripts = (transcript: string) => { 4 | _transcripts.push(transcript) 5 | } 6 | 7 | export const getTranscripts = () => { 8 | return _transcripts 9 | } 10 | 11 | export const endTranscript = () => { 12 | _transcripts = [] 13 | } -------------------------------------------------------------------------------- /simulator/src/constants.ts: -------------------------------------------------------------------------------- 1 | import defaults from './env.defaults.json' 2 | 3 | interface ENV { 4 | TEST_EMAIL: string, 5 | TEST_OTP: string, 6 | } 7 | 8 | export const configs: ENV = { 9 | TEST_EMAIL: process.env.TEST_EMAIL || defaults.TEST_EMAIL, 10 | TEST_OTP: process.env.TEST_EMAIL || defaults.TEST_EMAIL, 11 | } -------------------------------------------------------------------------------- /extension/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { targets: { node: "current" } }], 5 | "@babel/preset-typescript", 6 | "@emotion/babel-preset-css-prop" 7 | ], 8 | plugins: [ 9 | "react-hot-loader/babel", 10 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 11 | ] 12 | } -------------------------------------------------------------------------------- /simulator/src/utils/login.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from '@playwright/test'; 2 | import { configs } from '../constants'; 3 | 4 | export const login = async (frame: Frame) => { 5 | await frame.getByLabel('Enter Email').fill(configs.TEST_EMAIL) 6 | await frame.getByLabel('Sign in').click() 7 | await frame.getByLabel('Enter OTP').fill(configs.TEST_OTP) 8 | await frame.getByLabel('Verify OTP').click() 9 | } -------------------------------------------------------------------------------- /web/src/app/api/index.ts: -------------------------------------------------------------------------------- 1 | import auth from './auth' 2 | import { getLLMResponse } from './planner' 3 | import notifications from './notifications' 4 | import axios from 'axios'; 5 | 6 | const setAxiosJwt = (token: string) => { 7 | axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 8 | } 9 | 10 | export { 11 | auth, 12 | getLLMResponse, 13 | notifications, 14 | setAxiosJwt, 15 | } -------------------------------------------------------------------------------- /apps/src/posthog/appSetup.ts: -------------------------------------------------------------------------------- 1 | import { AppSetup } from "../base/appSetup"; 2 | import { posthogFingerprintMatcher } from "./fingerprint"; 3 | import { initObservePosthog } from "./posthogObserver"; 4 | 5 | export class PosthogSetup extends AppSetup { 6 | fingerprintMatcher = posthogFingerprintMatcher; 7 | 8 | async setup(extensionConfigs: Promise) { 9 | initObservePosthog() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/src/helpers/templatize.ts: -------------------------------------------------------------------------------- 1 | export const renderString = (template: string, context: Record): string => { 2 | return template.replace(/{{\s*(\w+)\s*}}/g, (match, variableName) => { 3 | // if the variable is not found, return the match. basically only replace vars that are in the context 4 | // otherwise keep it as is 5 | return context[variableName] || match; 6 | }); 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # env 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # yarn 12 | .pnp.* 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/plugins 16 | !.yarn/releases 17 | !.yarn/sdks 18 | !.yarn/versions 19 | 20 | # outputs 21 | *output*.txt 22 | *output*.log 23 | *semantic-layer*.md -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # MinusX 2 | 3 | The MinusX tests directory contains a set of tests that can be run to verify the MinusX extension is working correctly. 4 | 5 | ## Running the tests 6 | 7 | To run the tests, you can use the following command: 8 | 9 | ```bash 10 | yarn batchtest 11 | ``` 12 | 13 | This will run all the tests and generate a report in the `tests/report` directory. 14 | 15 | ## Writing tests 16 | -------------------------------------------------------------------------------- /extension/src/apps-script/README.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | 1. Copy the Code.gs, index.html files to the AppScript tab in doc/sheet: In the menu bar, `Extensions` -> `App Script`. 4 | 5 | 2. If you need to run the backend locally, you have to local tunnel your server so that Google App Script has access to it 6 | ``` 7 | ngrok http http://localhost:8000 8 | ``` 9 | 10 | 3. Replace the server URL in both web env file, and in the Code.gs file -------------------------------------------------------------------------------- /web/src/helpers/countTokens.ts: -------------------------------------------------------------------------------- 1 | export const countTokens = async (text: string, model_name: string) => { 2 | const response = await fetch('https://tiktoken-api.vercel.app/token_count', { 3 | method: 'POST', 4 | headers: { 5 | 'Content-Type': 'application/json', 6 | }, 7 | body: JSON.stringify({ 8 | text, 9 | model_name, 10 | }), 11 | }); 12 | const data = await response.json(); 13 | return data.token_count; 14 | }; 15 | -------------------------------------------------------------------------------- /simulator/src/checks/types.ts: -------------------------------------------------------------------------------- 1 | import { Expect } from "playwright/test"; 2 | import { JupyterNotebookState, RootState } from "web"; 3 | 4 | interface TestCheckArgs { 5 | instruction: string; 6 | initialMinusxState: RootState; 7 | initialAppState: JupyterNotebookState; 8 | finalMinusxState: RootState; 9 | finalAppState: JupyterNotebookState; 10 | } 11 | 12 | export type TestCheck = (testCheckArgs: TestCheckArgs, expect: Expect) => void | Promise; 13 | -------------------------------------------------------------------------------- /web/src/helpers/app.ts: -------------------------------------------------------------------------------- 1 | import { getAppStateConfigs } from "apps"; 2 | import { getParsedIframeInfo } from "./origin"; 3 | 4 | const appStateConfigs = getAppStateConfigs() 5 | 6 | export function getApp() { 7 | const parsed = getParsedIframeInfo() 8 | const tool = parsed.tool as keyof typeof appStateConfigs 9 | if (tool in appStateConfigs) { 10 | return appStateConfigs[tool] 11 | } 12 | // Default to jupyter 13 | return appStateConfigs['jupyter'] 14 | } -------------------------------------------------------------------------------- /web/src/helpers/slugg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "slugg" { 2 | type Separator = string; 3 | type ToStrip = string | RegExp; 4 | 5 | type Options = { 6 | separator?: Separator; 7 | toStrip?: ToStrip; 8 | toLowerCase?: boolean; 9 | }; 10 | 11 | // eslint-disable-next-line import/no-default-export -- deprecated usage 12 | export default function slugg( 13 | str: string, 14 | separatorOrOptions?: Separator | Options, 15 | toStrip?: ToStrip, 16 | ); 17 | } -------------------------------------------------------------------------------- /extension/src/helpers/setupScript.ts: -------------------------------------------------------------------------------- 1 | // Execute script in the webpage context 2 | export function setupScript(scriptURL: string): void { 3 | // Remember to add in manifest and webpack.config respectively 4 | // and webpack.config, options.entry 5 | const url = chrome.runtime.getURL(scriptURL); 6 | let script = document.createElement("script"); 7 | script.setAttribute('type', 'text/javascript'); 8 | script.setAttribute('src', url); 9 | document.head.appendChild(script); 10 | } -------------------------------------------------------------------------------- /web/src/components/common/setupScript.ts: -------------------------------------------------------------------------------- 1 | // Execute script in the webpage context 2 | export function setup(scriptURL: string): void { 3 | // Remember to add in manifest and webpack.config respectively 4 | // and webpack.config, options.entry 5 | const url = chrome.runtime.getURL(scriptURL); 6 | let script = document.createElement("script"); 7 | script.setAttribute('type', 'text/javascript'); 8 | script.setAttribute('src', url); 9 | document.body.appendChild(script); 10 | } -------------------------------------------------------------------------------- /extension/src/content/RPCs/sendIFrameMessage.ts: -------------------------------------------------------------------------------- 1 | import { configs } from "../../constants"; 2 | 3 | type IFrameKV = { 4 | key: string, 5 | value: any, 6 | } 7 | 8 | export const sendIFrameMessage = (payload: IFrameKV) => { 9 | const event = { 10 | type: 'STATE_SYNC', 11 | payload 12 | }; 13 | const iframe = document.getElementById('minusx-iframe') as HTMLIFrameElement; 14 | if (!iframe) { 15 | return; 16 | } 17 | iframe?.contentWindow?.postMessage(event, configs.WEB_URL); 18 | }; 19 | -------------------------------------------------------------------------------- /extension/src/helpers/setupStyles.ts: -------------------------------------------------------------------------------- 1 | // Execute script in the webpage context 2 | export function setupStyles(styleURL: string, isChrome = true): void { 3 | // Remember to add in manifest and webpack.config respectively 4 | // and webpack.config, options.entry 5 | const url = isChrome ? chrome.runtime.getURL(styleURL) : styleURL; 6 | let link = document.createElement("link"); 7 | link.setAttribute('rel', 'stylesheet'); 8 | link.setAttribute('href', url); 9 | document.head.appendChild(link); 10 | } -------------------------------------------------------------------------------- /apps/src/jupyter/nb-to-md-report/llmConfig.ts: -------------------------------------------------------------------------------- 1 | import { NB_TO_MD_REPORT_SYSTEM_PROMPT, NB_TO_MD_REPORT_USER_PROMPT } from "./prompts"; 2 | 3 | export const NB_TO_MD_REPORT_LLM_CONFIG = { 4 | type: "simple", 5 | llmSettings: { 6 | model: "gpt-4.1", 7 | temperature: 0, 8 | response_format: { 9 | type: "text", 10 | }, 11 | tool_choice: "none", 12 | }, 13 | systemPrompt: NB_TO_MD_REPORT_SYSTEM_PROMPT, 14 | userPrompt: NB_TO_MD_REPORT_USER_PROMPT, 15 | actionDescriptions: [], 16 | }; -------------------------------------------------------------------------------- /apps/src/metabase/helpers/slugg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "slugg" { 2 | type Separator = string; 3 | type ToStrip = string | RegExp; 4 | 5 | type Options = { 6 | separator?: Separator; 7 | toStrip?: ToStrip; 8 | toLowerCase?: boolean; 9 | }; 10 | 11 | // eslint-disable-next-line import/no-default-export -- deprecated usage 12 | export default function slugg( 13 | str: string, 14 | separatorOrOptions?: Separator | Options, 15 | toStrip?: ToStrip, 16 | ); 17 | } -------------------------------------------------------------------------------- /apps/src/appStateConfigs.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { once } from "lodash"; 3 | import { JupyterState } from "./jupyter/appState"; 4 | import { MetabaseState } from "./metabase/appState"; 5 | import { PosthogState } from "./posthog/appState"; 6 | import { GoogleAppState } from "./google/appState"; 7 | 8 | export const getAppStateConfigs = once(() => ({ 9 | metabase: new MetabaseState(), 10 | jupyter: new JupyterState(), 11 | posthog: new PosthogState(), 12 | google: new GoogleAppState() 13 | })); -------------------------------------------------------------------------------- /simulator/src/mods/runAllCells.ts: -------------------------------------------------------------------------------- 1 | import { JupyterNotebookState } from "web"; 2 | import { TestMod } from "./types"; 3 | import { getAppStateActions } from "../utils/appState"; 4 | import { runAppActions } from "../utils/appState"; 5 | 6 | export const runAllCells: TestMod = async ({ frame, page }) => { 7 | const state: JupyterNotebookState = (await getAppStateActions(frame)).state 8 | for (const cell of state.cells) { 9 | await runAppActions(frame, 'executeCell', { cell_index: cell.cellIndex }) 10 | } 11 | } -------------------------------------------------------------------------------- /simulator/src/checks/checkAppState.ts: -------------------------------------------------------------------------------- 1 | import { TestCheck } from "./types"; 2 | 3 | export const checkAppState = ({strInFinalAppState, strNotInFinalAppState}: {strInFinalAppState: string, strNotInFinalAppState: string}): TestCheck => { 4 | return function checkStrInAppState({ initialMinusxState, initialAppState, finalMinusxState, finalAppState }, expect){ 5 | expect(JSON.stringify(finalAppState)).toContain(strInFinalAppState) 6 | expect(JSON.stringify(finalAppState)).not.toContain(strNotInFinalAppState) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/src/helpers/sqlProcessor.ts: -------------------------------------------------------------------------------- 1 | // Simple SQL processor for CTEs 2 | export type CTE = [string, string]; 3 | 4 | export function addCtesToQuery(sql: string, ctes: CTE[]): string { 5 | if (!ctes || ctes.length === 0) { 6 | return sql; 7 | } 8 | 9 | const cteStatements = ctes.map(([name, query]) => `${name} AS (${query})`).join(',\n'); 10 | return `WITH ${cteStatements}\n${sql}`; 11 | } 12 | 13 | export function processSQLWithCtesOrModels(sql: string, ctes: CTE[]): string { 14 | return addCtesToQuery(sql, ctes); 15 | } -------------------------------------------------------------------------------- /web/src/planner/cotPlan.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { simplePlan } from './simplePlan'; 3 | import { CoTPlannerConfig } from 'apps/types'; 4 | 5 | export async function cotPlan(signal: AbortSignal, plannerConfig: CoTPlannerConfig) { 6 | { 7 | // Thinking stage 8 | await simplePlan(signal, {type: 'simple', ...plannerConfig.thinkingStage} ) 9 | } 10 | signal.throwIfAborted(); 11 | // Tool choice stage 12 | { 13 | await simplePlan(signal, {type: 'simple', ...plannerConfig.toolChoiceStage} ) 14 | } 15 | } -------------------------------------------------------------------------------- /web/src/env.defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "BASE_SERVER_URL": "https://v2.minusxapi.com", 3 | "SERVER_PATH": "", 4 | "AUTH_PATH": "/auth", 5 | "PLANNER_PATH": "/planner", 6 | "LOGGING_PATH": "/logging", 7 | "SEMANTIC_PATH": "/semantic", 8 | "ASSETS_PATH": "/assets", 9 | "DEEPRESEARCH_PATH": "/deepresearch", 10 | "ATLAS_PATH": "/atlas", 11 | "SOCKET_ENDPOINT": "/socket.io", 12 | "WEB_URL": "http://localhost:3005", 13 | "POSTHOG_API_KEY": "", 14 | "POSTHOG_CONFIGS": "{}", 15 | "IS_PROD": false 16 | } -------------------------------------------------------------------------------- /web/public/logo_x.svg: -------------------------------------------------------------------------------- 1 | 2 | logo2 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/public/logo_x_light.svg: -------------------------------------------------------------------------------- 1 | 2 | logo2 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/src/assets/img/logo_x.svg: -------------------------------------------------------------------------------- 1 | 2 | logo2 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { MetabaseAppState } from "./metabase/helpers/DOMToState"; 2 | import { JupyterNotebookState } from "./jupyter/helpers/DOMToState"; 3 | 4 | export type AppState = MetabaseAppState | JupyterNotebookState; 5 | export type { ActionDescription, ToolPlannerConfig, CoTPlannerConfig, SimplePlannerConfig } from "./base/defaultState"; 6 | export type { MetabaseContext } from "./metabase/defaultState"; 7 | export type { FormattedTable } from "./metabase/helpers/types"; 8 | export type { MetabaseModel, MetabaseDashboard } from "./metabase/helpers/metabaseAPITypes"; -------------------------------------------------------------------------------- /extension/src/assets/img/logo_x.svg: -------------------------------------------------------------------------------- 1 | 2 | logo2 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /web-build 12 | build.crx 13 | minusx.zip 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development 20 | .env.test.local 21 | .env.production 22 | .history 23 | 24 | # secrets 25 | secrets.*.js 26 | 27 | # yarn 28 | .pnp.* 29 | .yarn/* 30 | !.yarn/patches 31 | !.yarn/plugins 32 | !.yarn/releases 33 | !.yarn/sdks 34 | !.yarn/versions 35 | yarn.lock -------------------------------------------------------------------------------- /apps/src/google/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { ToolMatcher } from "extension/types"; 2 | 3 | export const googleDocFingerprintMatcher: ToolMatcher = { 4 | docs: { 5 | type: "combination", 6 | or: [ 7 | { 8 | type: "urlRegexCondition", 9 | urlRegex: "^https:\/\/docs\.google\.com\/document\/d", 10 | }, 11 | ], 12 | }, 13 | sheets: { 14 | type: "combination", 15 | or: [ 16 | { 17 | type: "urlRegexCondition", 18 | urlRegex: "^https:\/\/docs\.google\.com\/spreadsheets\/d", 19 | }, 20 | ], 21 | } 22 | }; -------------------------------------------------------------------------------- /apps/src/posthog/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { ToolMatcher } from "extension/types"; 2 | 3 | export const posthogFingerprintMatcher: ToolMatcher = { 4 | default: { 5 | type: "combination", 6 | or: [ 7 | { 8 | type: "domQueryCondition", 9 | domQuery: { 10 | selector: { 11 | type: "XPATH", 12 | // ends-with PostHog 13 | selector: "//title[substring(text(), string-length(text()) - string-length('PostHog') + 1) = 'PostHog']", 14 | }, 15 | }, 16 | }, 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /web-build 12 | /public 13 | build.crx 14 | minusx.zip 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | .env.local 20 | .env.development 21 | .env.test.local 22 | .env.production 23 | .history 24 | 25 | # secrets 26 | secrets.*.js 27 | 28 | # yarn 29 | .pnp.* 30 | .yarn/* 31 | !.yarn/patches 32 | !.yarn/plugins 33 | !.yarn/releases 34 | !.yarn/sdks 35 | !.yarn/versions -------------------------------------------------------------------------------- /extension/src/apps-script/gsheets/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "America/Los_Angeles", 3 | "dependencies": {}, 4 | "exceptionLogging": "STACKDRIVER", 5 | "runtimeVersion": "V8", 6 | "oauthScopes": [ 7 | "https://www.googleapis.com/auth/spreadsheets", 8 | "https://www.googleapis.com/auth/script.container.ui", 9 | "https://www.googleapis.com/auth/script.external_request" 10 | ], 11 | "addOns": { 12 | "sheets": { 13 | "homepageTrigger": { 14 | "runFunction": "onOpen", 15 | "enabled": true 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /web/src/app/toast.ts: -------------------------------------------------------------------------------- 1 | import { createStandaloneToast } from '@chakra-ui/react' 2 | import { getParsedIframeInfo } from '../helpers/origin' 3 | const { ToastContainer, toast: toastRaw } = createStandaloneToast() 4 | 5 | type ToastParams = Parameters[0] 6 | const width = getParsedIframeInfo().width 7 | const toast = (props: ToastParams) => { 8 | return toastRaw({ 9 | containerStyle: { 10 | width: `${parseInt(width) - 20}px` 11 | }, 12 | ...props 13 | }) 14 | } 15 | 16 | export { 17 | ToastContainer, 18 | toast 19 | } 20 | -------------------------------------------------------------------------------- /web/src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /web/src/state/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { store } from './store' 2 | 3 | export const dispatch: typeof store.dispatch = (...args: Parameters) => store.dispatch(...args) 4 | 5 | export const resetState = () => dispatch({ 6 | type: 'reset' 7 | }) 8 | 9 | export const logoutState = () => dispatch({ 10 | type: 'logout' 11 | }) 12 | 13 | export const uploadThread = (thread: []) => dispatch({ 14 | type: 'upload_thread', 15 | payload: thread 16 | }) 17 | 18 | export const uploadState = (state: {}) => dispatch({ 19 | type: 'upload_state', 20 | payload: state 21 | }) 22 | -------------------------------------------------------------------------------- /apps/src/base/appHook.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { once } from 'lodash'; 3 | 4 | function _createStore(internalState: T) { 5 | return create | ((prevState: T) => Partial)) => void; 7 | }>((set) => ({ 8 | ...internalState, 9 | update: (arg: Partial | ((prevState: T) => Partial)) => 10 | set((state) => ({ 11 | ...state, 12 | ...(typeof arg === 'function' ? arg(state as T) : arg), 13 | })), 14 | })); 15 | } 16 | 17 | export const createStore = once(_createStore) -------------------------------------------------------------------------------- /apps/src/metabase/appSetup.ts: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import { AppSetup } from "../base/appSetup"; 3 | import { metabaseFingerprintMatcher } from "./fingerprint"; 4 | 5 | export class MetabaseSetup extends AppSetup { 6 | fingerprintMatcher = metabaseFingerprintMatcher; 7 | 8 | async setup(extensionConfigs: Promise) { 9 | const localConfigs = await extensionConfigs 10 | const metabaseMatcher = get(localConfigs, "configs.versioned_tool_configs.metabase") 11 | if (metabaseMatcher) { 12 | this.fingerprintMatcher = metabaseMatcher 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/src/google/sampleCode.js: -------------------------------------------------------------------------------- 1 | function countCompaniesStartingWithR(sheetData) { 2 | // Extract the rows from the sheet data 3 | const rows = sheetData.cells; 4 | // Initialize a counter for companies starting with 'r' 5 | let count = 0; 6 | // Iterate over the rows, starting from the second row (index 1) to skip the header 7 | for (let i = 1; i < rows.length; i++) { 8 | const companyName = rows[i][2].value; 9 | // Check if the company name starts with 'r' or 'R' 10 | if (companyName.toLowerCase().startsWith('r')) { 11 | count++; 12 | } 13 | } 14 | return count; 15 | } -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"], 19 | "exclude": ["build", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"], 19 | "exclude": ["build", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /simulator/src/checks/checkRespondToUser.ts: -------------------------------------------------------------------------------- 1 | import { TestCheck } from "./types"; 2 | 3 | export const checkRespondToUser: TestCheck = ({ initialMinusxState, initialAppState, finalMinusxState, finalAppState }, expect) => { 4 | const activeThread = finalMinusxState.chat.activeThread 5 | const messages = finalMinusxState.chat.threads[activeThread].messages 6 | expect(messages.some(message => 7 | (message.role == 'assistant' && message.content.messageContent.length > 0) || 8 | (message.role == 'tool' && message.content.type == 'DEFAULT' && message.content.text.length > 0) 9 | )).toBe(true) 10 | } 11 | -------------------------------------------------------------------------------- /simulator/src/specs/jupyter/test_01.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkRespondToUser } from "../../checks/checkRespondToUser"; 2 | import { runAllCells } from "../../mods/runAllCells"; 3 | import { TestRunner, loadStateFromFile } from "../../utils/testRunner"; 4 | import { TestConfig } from '../../fixtures/jupyter'; 5 | 6 | const testConfig: TestConfig = { 7 | description: "Summarise a notebook", 8 | file: "sin_wave.ipynb", 9 | initialMinusxState: loadStateFromFile('authState.json'), 10 | init: [runAllCells], 11 | instruction: "Summarise this notebook", 12 | checks: [checkRespondToUser] 13 | } 14 | 15 | TestRunner(testConfig) 16 | -------------------------------------------------------------------------------- /web/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export type { DefaultMessageContent, BlankMessageContent, ActionRenderInfo } from './state/chat/types'; 2 | export type { GoogleState } from './state/google/types'; 3 | export type { SemanticFilter, TimeDimension, Order, SemanticQuery } from './state/thumbnails/reducer'; 4 | export type { Measure, Dimension } from './state/semantic-layer/reducer'; 5 | export type { TableDiff, TableInfo } from './state/settings/reducer'; 6 | export type { MetadataItem } from './helpers/metadataProcessor'; 7 | export type { Notification, NotificationContent, NotificationsResponse, NotificationActionResponse } from './types/notifications'; -------------------------------------------------------------------------------- /web/src/types/notifications.ts: -------------------------------------------------------------------------------- 1 | export interface NotificationContent { 2 | type: string; 3 | message: string; 4 | // Future content properties can be added here 5 | } 6 | 7 | export interface Notification { 8 | id: string; 9 | profile_id: string; 10 | content: NotificationContent; 11 | delivered_at: string | null; 12 | executed_at: string | null; 13 | created_at: string; 14 | updated_at: string; 15 | } 16 | 17 | export interface NotificationsResponse { 18 | success: boolean; 19 | notifications: Notification[]; 20 | } 21 | 22 | export interface NotificationActionResponse { 23 | success: boolean; 24 | } -------------------------------------------------------------------------------- /simulator/src/fixtures/frame.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from '@playwright/test'; 2 | import { test as base } from './base' 3 | 4 | export const test = base.extend<{ 5 | frame: Frame; 6 | url: string 7 | }>({ 8 | frame: async ({ page, url }, use) => { 9 | await page.goto(url); 10 | const minusxFrame = await (await page.$('#minusx-iframe'))?.contentFrame() 11 | if (!minusxFrame) { 12 | throw Error('minusxFrame not found') 13 | } 14 | await minusxFrame.evaluate('window.IS_PLAYWRIGHT = true;') 15 | await minusxFrame.waitForLoadState('load'); 16 | await use(minusxFrame); 17 | }, 18 | }); 19 | export const expect = test.expect; -------------------------------------------------------------------------------- /simulator/src/specs/jupyter/test_02.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkAppState } from "../../checks/checkAppState"; 2 | import { runAllCells } from "../../mods/runAllCells"; 3 | import { TestRunner, loadStateFromFile } from "../../utils/testRunner"; 4 | import { TestConfig } from '../../fixtures/jupyter'; 5 | 6 | const testConfig: TestConfig = { 7 | description: "line to bar", 8 | file: "cos_wave.ipynb", 9 | initialMinusxState: loadStateFromFile('authState.json'), 10 | init: [runAllCells], 11 | instruction: "convert line plot to bar plot", 12 | checks: [checkAppState({strInFinalAppState: 'px.bar', strNotInFinalAppState: 'px.line'})] 13 | } 14 | 15 | TestRunner(testConfig) 16 | -------------------------------------------------------------------------------- /apps/src/jupyter/appSetup.ts: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import { AppSetup } from "../base/appSetup"; 3 | import { jupyterFingerprintMatcher } from "./fingerprint"; 4 | import { initObserveJupyterApp } from "./jupyterObserver"; 5 | 6 | export class JupyterSetup extends AppSetup { 7 | fingerprintMatcher = jupyterFingerprintMatcher; 8 | 9 | async setup(extensionConfigs: Promise) { 10 | initObserveJupyterApp() 11 | const localConfigs = await extensionConfigs 12 | const jupyterMatcher = get(localConfigs, "configs.versioned_tool_configs.jupyter") 13 | if (jupyterMatcher) { 14 | this.fingerprintMatcher = jupyterMatcher 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/src/metabase/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { ToolMatcher } from "extension/types" 2 | 3 | export const metabaseFingerprintMatcher: ToolMatcher = { 4 | default: { 5 | type: "combination", 6 | or: [ 7 | { 8 | type: "domQueryCondition", 9 | domQuery: { 10 | selector: { 11 | type: "CSS", 12 | selector: "#_metabaseUserLocalization", 13 | }, 14 | }, 15 | }, 16 | { 17 | type: "domQueryCondition", 18 | domQuery: { 19 | selector: { 20 | type: "CSS", 21 | selector: "#_metabaseBootstrap", 22 | }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /apps/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | }, 20 | "include": ["src"], 21 | "exclude": ["build", "node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /web/src/helpers/tests/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | Helper file to debug HTML via query selectors 3 | */ 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const fs = require('fs'); 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const { JSDOM } = require('jsdom'); 9 | 10 | // Read file synchronously 11 | const filePath = './src/file.html'; // Adjust the file path to the html of the page 12 | const fullDom = fs.readFileSync(filePath, 'utf8'); 13 | // const dom = new DOMParser().parseFromString(fullDom, 'text/html'); 14 | const dom = new JSDOM(fullDom, { runScripts: 'dangerously' }); 15 | const document = dom.window.document; 16 | 17 | module.exports = document; 18 | -------------------------------------------------------------------------------- /web/src/helpers/templatize.test.ts: -------------------------------------------------------------------------------- 1 | import { renderString } from './templatize' 2 | 3 | test('renders template with context variables', () => { 4 | const template = "Hello {{ name }}! Welcome to {{ place }}."; 5 | const context = { name: 'John', place: 'Stack Overflow' }; 6 | const result = renderString(template, context); 7 | expect(result).toBe("Hello John! Welcome to Stack Overflow."); 8 | }); 9 | 10 | test('handles missing context variables gracefully', () => { 11 | const template = "Hello {{ name }}! Welcome to {{ place }}."; 12 | const context = { name: 'John' }; 13 | const result = renderString(template, context); 14 | expect(result).toBe("Hello John! Welcome to ."); 15 | }) -------------------------------------------------------------------------------- /simulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simulator", 3 | "version": "0.0.1", 4 | "description": "A simulator for MinusX", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com:minusxai/minusx.git" 9 | }, 10 | "scripts": { 11 | "base_test": "playwright test", 12 | "test": "RETRIES=3 yarn base_test", 13 | "ci_test": "RETRIES=3 HEADLESS=true REUSE_DOCKER=true yarn base_test", 14 | "report": "playwright show-report" 15 | }, 16 | "packageManager": "yarn@4.3.1", 17 | "dependencies": { 18 | "playwright": "^1.45.2" 19 | }, 20 | "devDependencies": { 21 | "@playwright/test": "^1.45.2", 22 | "@types/node": "^20.14.11" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/src/metabase/helpers/stateSubscriptions.ts: -------------------------------------------------------------------------------- 1 | import { RPCs } from "web" 2 | 3 | type Callback = (d: any) => void 4 | const listeners: Record = {} 5 | 6 | export const subscribeMB = async (path: string, callback: Callback) => { 7 | const id = await RPCs.subscribeMetabaseState(path) 8 | console.log('Subscribed to Metabase state path:', path, 'with ID:', id) 9 | listeners[id] = callback 10 | return id 11 | } 12 | 13 | export const onMBSubscription = (payload: { id: number; path: string; value: any }) => { 14 | console.log('Metabase state change', payload) 15 | const { id } = payload 16 | if (!(id in listeners)) { 17 | return 18 | } 19 | listeners[id](payload) 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "web", 5 | "extension", 6 | "simulator", 7 | "apps" 8 | ], 9 | "scripts": { 10 | "web": "yarn workspace web start", 11 | "tests": "yarn workspace simulator test", 12 | "test-reports": "yarn workspace simulator report", 13 | "web-prod": "yarn workspace web build && yarn workspace web serve", 14 | "extension": "yarn workspace extension start", 15 | "extension-build": "yarn workspace extension build" 16 | }, 17 | "packageManager": "yarn@4.3.1", 18 | "dependencies": { 19 | "react-diff-viewer-continued": "^3.4.0", 20 | "react-syntax-highlighter": "^15.5.0", 21 | "react-window": "^1.8.11" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /simulator/src/utils/teardown.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import * as path from 'path' 4 | 5 | const execPromise = promisify(exec); 6 | 7 | const teardown = async () => { 8 | try { 9 | // run docker-compose in node 10 | const pathToRootFolder = path.join(__dirname, '../../..'); 11 | if (process.env.REUSE_DOCKER !== 'true') { 12 | const { stdout, stderr } = await execPromise(`cd ${pathToRootFolder}/simulator && docker-compose down`); 13 | console.log('Stdout:', stdout); 14 | console.error('Stderr:', stderr); 15 | } 16 | } catch (error) { 17 | console.error('Error in teardown:', error.message); 18 | throw error 19 | } 20 | } 21 | 22 | export default teardown -------------------------------------------------------------------------------- /extension/src/helpers/pageParse/getElements.ts: -------------------------------------------------------------------------------- 1 | import { QuerySelector } from './querySelectorTypes'; 2 | import { resolveSelector } from './resolveSelectors'; 3 | 4 | 5 | export const getElementsFromQuerySelector = (selector: QuerySelector, parent?: Element) => { 6 | const elements = resolveSelector(selector, parent); 7 | const range = selector.range; 8 | const from = range?.from || 0; 9 | const to = range?.to; 10 | return to ? elements.slice(from, to) : elements.slice(from); 11 | }; 12 | 13 | export const getElementFromQuerySelector = (selector: QuerySelector, index: number = 0, parent?: Element) => { 14 | const elements = getElementsFromQuerySelector(selector, parent) 15 | if (elements && index in elements) { 16 | return elements[index] 17 | } 18 | } -------------------------------------------------------------------------------- /apps/src/base/prompts.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PLANNER_SYSTEM_PROMPT = `You are MinusX, the world's best data scientist and a master of analytics apps. 2 | Answer the user's request using relevant tools (if they are available)` 3 | export const DEFAULT_PLANNER_USER_PROMPT = ` 4 | 5 | {{ state }} 6 | 7 | 8 | 9 | {{ instructions }} 10 | ` 11 | export const DEFAULT_SUGGESTIONS_SYSTEM_PROMPT = `You are MinusX, the world's best data scientist and a master of analytics apps. 12 | Answer the user's request using relevant tools (if they are available)` 13 | export const DEFAULT_SUGGESTIONS_USER_PROMPT = ` 14 | 15 | {{ state }} 16 | 17 | 18 | 19 | {{ instructions }} 20 | ` -------------------------------------------------------------------------------- /apps/src/posthog/stateSchema.ts: -------------------------------------------------------------------------------- 1 | export const PosthogAppStateSchema = { 2 | type: "object", 3 | properties: { 4 | sqlQuery: { 5 | type: "string", 6 | }, 7 | relevantTables: { 8 | type: "array", 9 | items: { 10 | type: "object", 11 | properties: { 12 | name: { 13 | type: "string", 14 | }, 15 | description: { 16 | type: "string", 17 | }, 18 | schema: { 19 | type: "string", 20 | }, 21 | id: { 22 | type: "number", 23 | }, 24 | }, 25 | } 26 | }, 27 | sqlErrorMessage: { 28 | type: "string", 29 | }, 30 | outputTableMarkdown: { 31 | type: "string", 32 | }, 33 | }, 34 | } -------------------------------------------------------------------------------- /web/src/components/common/SettingsBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VStack, Box, Divider, AbsoluteCenter } from '@chakra-ui/react'; 3 | 4 | const SettingsHeader = ({ text }: { text: string }) => ( 5 | 6 | 7 | 8 | {text} 9 | 10 | 11 | ) 12 | 13 | export const SettingsBlock = ({title, ariaLabel, children}: {title: string, ariaLabel?: string, children: React.ReactNode}) => ( 14 | 15 | 16 | {children} 17 | 18 | ) -------------------------------------------------------------------------------- /extension/src/content/RPCs/ripple.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from '../../helpers/utils'; 2 | 3 | export default async function ripple(x: number, y: number, wait = 1000, style?: Record) { 4 | const rippleRadius = 30; 5 | const ripple = document.createElement('div'); 6 | ripple.classList.add('web-agent-ripple'); 7 | ripple.style.width = ripple.style.height = `${rippleRadius * 2}px`; 8 | // Take scroll position into account 9 | ripple.style.top = `${window.scrollY + y - rippleRadius}px`; 10 | ripple.style.left = `${x - rippleRadius}px`; 11 | if (style) { 12 | for (let key in style) { 13 | ripple.style[key] = style[key] 14 | } 15 | } 16 | 17 | document.body.appendChild(ripple); 18 | 19 | await sleep(wait); 20 | ripple.remove(); 21 | } 22 | -------------------------------------------------------------------------------- /web/src/helpers/nativeEvents.ts: -------------------------------------------------------------------------------- 1 | import { QuerySelector } from "extension/types" 2 | import { attachEventsListener } from "../app/rpc" 3 | 4 | type EventListener = (event: string) => void 5 | 6 | interface NativeEventPayload { 7 | eventID: number 8 | event: string 9 | } 10 | 11 | const listeners: Record = {} 12 | 13 | export const addNativeEventListener = async (selector: QuerySelector, listener: EventListener, events: string[]=['click']) => { 14 | const eventID = await attachEventsListener(selector, events) 15 | listeners[eventID] = listener 16 | } 17 | 18 | export const onNativeEvent = (payload: NativeEventPayload) => { 19 | const { eventID, event } = payload 20 | if (!(eventID in listeners)) { 21 | return 22 | } 23 | listeners[eventID](event) 24 | } -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # MinusX extension 2 | 3 | The MinusX chrome extension which exposes core browser APIs 4 | 5 | # Setup 6 | 7 | To start the chrome extension, run: 8 | 9 | ```sh 10 | yarn 11 | yarn start 12 | ``` 13 | 14 | Load your extension on Chrome by doing the following: 15 | 16 | 1. Navigate to chrome://extensions/ 17 | 2. Toggle Developer mode 18 | 3. Click on Load unpacked extension 19 | 4. Select the build folder that is generated by your previous command 20 | 21 | To Improve DevX: Install [Extensions Reloader Chrome Extension](https://chromewebstore.google.com/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid) to reload extension content & background scripts on change 22 | 23 | # Production 24 | To build for production, run: 25 | ```sh 26 | yarn 27 | yarn build 28 | ``` -------------------------------------------------------------------------------- /web/src/state/configs/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | export interface EmbedConfigs { 5 | embed_host?: string 6 | } 7 | 8 | interface ConfigsState { 9 | embed: EmbedConfigs 10 | } 11 | 12 | const initialState: ConfigsState = { 13 | embed: {} 14 | } 15 | 16 | export const configsSlice = createSlice({ 17 | name: 'configs', 18 | initialState, 19 | reducers: { 20 | setEmbedConfigs: (state, action: PayloadAction) => { 21 | state.embed = action.payload 22 | }, 23 | clearEmbedConfigs: (state) => { 24 | state.embed = {} 25 | } 26 | }, 27 | }) 28 | 29 | export const { setEmbedConfigs, clearEmbedConfigs } = configsSlice.actions 30 | 31 | export default configsSlice.reducer -------------------------------------------------------------------------------- /extension/src/background/identifier.ts: -------------------------------------------------------------------------------- 1 | import { get, throttle } from "lodash"; 2 | 3 | function generateIdentifier() { 4 | // E.g. 8 * 8 = 64 bits token 5 | var randomPool = new Uint8Array(8); 6 | crypto.getRandomValues(randomPool); 7 | var hex = ''; 8 | for (var i = 0; i < randomPool.length; ++i) { 9 | hex += randomPool[i].toString(16); 10 | } 11 | return hex; 12 | } 13 | 14 | async function _getExtensionID() { 15 | const localConfigs = await chrome.storage.local.get('id'); 16 | let id = get(localConfigs, "id") 17 | if (!id) { 18 | id = generateIdentifier() 19 | await chrome.storage.local.set({ id }); 20 | } 21 | return id 22 | } 23 | 24 | // Assume local storage read won't take 3 seconds 25 | export const getExtensionID = throttle(_getExtensionID, 3000); -------------------------------------------------------------------------------- /web/src/components/common/AbortTaskButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, HStack, Icon, Tooltip } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { BsStopFill } from 'react-icons/bs'; 4 | 5 | export default function AbortTaskButton({ abortTask, disabled }: { abortTask: () => void, disabled: boolean}) { 6 | 7 | let button = ( 8 | 9 | } 17 | disabled={disabled} 18 | /> 19 | 20 | ); 21 | 22 | return {button}; 23 | } 24 | -------------------------------------------------------------------------------- /simulator/.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | - name: Install dependencies 17 | run: npm install -g yarn && yarn 18 | - name: Install Playwright Browsers 19 | run: yarn playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: yarn playwright test 22 | - uses: actions/upload-artifact@v4 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /web/src/components/common/RunTaskButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, HStack, Icon, Tooltip } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { BsPlayFill, BsStopFill } from 'react-icons/bs'; 4 | 5 | export default function RunTaskButton({ runTask, disabled }: { runTask: () => void, disabled: boolean}) { 6 | 7 | let button = ( 8 | 9 | } 17 | disabled={disabled} 18 | /> 19 | 20 | ); 21 | 22 | return {button}; 23 | } 24 | -------------------------------------------------------------------------------- /extension/src/helpers/pageParse/querySelectorTypes.d.ts: -------------------------------------------------------------------------------- 1 | import type { Subset } from "../utils" 2 | 3 | type QuerySelectorType = 'XPATH' | 'CSS' 4 | export type QuerySelectorValue = string 5 | type QuerySelectorKey = string 6 | type QuerySelectorRange = { 7 | from?: number 8 | to?: number 9 | } 10 | 11 | interface BaseQuerySelector { 12 | type: QuerySelectorType 13 | selector: QuerySelectorValue 14 | range?: QuerySelectorRange 15 | } 16 | 17 | interface XPATHQuerySelector extends BaseQuerySelector { 18 | type: Subset 19 | } 20 | 21 | interface CSSQuerySelector extends BaseQuerySelector { 22 | type: Subset 23 | } 24 | 25 | export type QuerySelector = XPATHQuerySelector | CSSQuerySelector 26 | 27 | export type QuerySelectorMap = Record -------------------------------------------------------------------------------- /apps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apps", 3 | "version": "1.0.33", 4 | "description": "The Best AI Agent for Metabase", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com:minusxai/minusx.git" 9 | }, 10 | "type": "module", 11 | "exports": { 12 | ".": "./src/package.ts", 13 | "./extension": "./src/package.extension.ts", 14 | "./types": "./src/types.d.ts" 15 | }, 16 | "types": "src/package.types.ts", 17 | "dependencies": { 18 | "lodash": "^4.17.21", 19 | "react": "^18.3.1", 20 | "reflect-metadata": "^0.2.2", 21 | "slugg": "^1.2.1", 22 | "uuid": "^10.0.0", 23 | "zustand": "^4.5.5" 24 | }, 25 | "packageManager": "yarn@4.3.1", 26 | "devDependencies": { 27 | "@types/uuid": "^10", 28 | "jest": "^29.7.0", 29 | "ts-jest": "^29.2.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/src/metabase/helpers/dashboard/util.ts: -------------------------------------------------------------------------------- 1 | import { MetabaseAppStateDashboard } from '../DOMToState'; 2 | 3 | // if url has /dashboard/ in it, then it's a dashboard 4 | export const isDashboardPageUrl = (url: string) => { 5 | return url.includes('/dashboard/'); 6 | } 7 | 8 | export function getDashboardPrimaryDbId(appState: MetabaseAppStateDashboard | null) { 9 | if (!appState) { 10 | return undefined; 11 | } 12 | const dbIds = appState.cards.map(card => card.databaseId) 13 | // count and return the most frequently occurring dbId 14 | const counts = dbIds.reduce((acc, dbId) => { 15 | acc[dbId] = (acc[dbId] || 0) + 1 16 | return acc 17 | }, {} as Record) 18 | // sort counts by value, descending 19 | const sortedCounts = Object.entries(counts).sort((a, b) => b[1] - a[1]) 20 | // convert to integer 21 | return parseInt(sortedCounts[0][0]) 22 | } -------------------------------------------------------------------------------- /web/src/chat/chat.test.ts: -------------------------------------------------------------------------------- 1 | import { getState } from '../state/store' 2 | import chat from './chat' 3 | // import planner from '../planner/planner' 4 | // planner.init() 5 | 6 | describe('Chat Messages', () => { 7 | it('should return the initial state of the chat reducer', () => { 8 | expect(getState().chat).toEqual({ 9 | threads: [ 10 | { 11 | id: 0, 12 | messages: [], 13 | }, 14 | ], 15 | activeThread: 0, 16 | }) 17 | }) 18 | 19 | it('should handle addMessage action', async () => { 20 | const content = "Hello World" 21 | chat.addUserMessage({ 22 | content, 23 | contentType: 'text', 24 | tabId: 'testing_tabId' 25 | }) 26 | expect(getState().chat.threads[0].messages[0].sender).toEqual("user") 27 | expect(getState().chat.threads[0].messages[0].content).toEqual(content) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /apps/src/metabase/helpers/dashboard/runSqlQueryFromDashboard.ts: -------------------------------------------------------------------------------- 1 | import { DashboardInfo, DatasetResponse } from "./types"; 2 | import { executeQuery, executeMBQLQuery } from '../metabaseAPIHelpers'; 3 | 4 | export const runSQLQueryFromDashboard = async (sql: string, databaseId: number, templateTags = {}) => { 5 | try { 6 | return await executeQuery(sql, databaseId, templateTags) as DatasetResponse; 7 | } catch (error) { 8 | let errMessage = error?.response?.message || error.message 9 | if (errMessage.includes('403')) { 10 | errMessage += " - You do not have permission to run this query."; 11 | } 12 | return { 13 | error: errMessage 14 | } 15 | } 16 | } 17 | 18 | export const runMBQLQueryFromDashboard = async (mbql: any, databaseId: number) => { 19 | const response = await executeMBQLQuery(mbql, databaseId) as DatasetResponse; 20 | return response; 21 | } 22 | -------------------------------------------------------------------------------- /simulator/src/utils/appState.ts: -------------------------------------------------------------------------------- 1 | import { Frame, Page } from "playwright/test" 2 | 3 | export const getAppStateActions = async (frame: Frame) => await frame.evaluate(async () => { 4 | return await window.__GET_STATE_ACTION__() 5 | }) 6 | 7 | export const checkMinusXMode = async (page: Page, mode: string) => await page.evaluate((mode) => { 8 | return document.getElementById('minusx-root')?.className.includes(mode) 9 | }, mode) 10 | 11 | export const openMinusXFrame = async (page: Page) => { 12 | const isClosed = await checkMinusXMode(page, 'closed') 13 | if (isClosed) { 14 | await page.locator('#minusx-toggle').click() 15 | } 16 | } 17 | 18 | export const runAppActions = async (frame: Frame, fn: string, args: unknown) => await frame.evaluate(async ({fn, args}) => { 19 | await window.__EXECUTE_ACTION__({ 20 | index: 0, 21 | function:fn, 22 | args: args 23 | }) 24 | }, {fn, args}) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | MinusX is open source. From the very beginning, we've felt that MinusX could be extended to many more use-cases than we could ever build ourselves. The MinusX architecture reflects this as we've designed it to be easy to add support to new apps. 4 | 5 | The analogy of retrofitting is apt here as what MinusX does at its core is to retrofit your existing apps with AI capabilities. We believe that in the future, the power of LLMs will enable more people to augment existing software in ways their creators never imagined and we intend MinusX to be a part of that future. 6 | 7 | Development is coordinated through [Discord](https://minusx.ai/discord) and GitHub. 8 | 9 | Refer to the [Setup Guide](https://github.com/minusxai/minusx/blob/main/setup.md) to get started. 10 | 11 | We'll be adding more detailed examples and posts on how to add more tools and upstream them, soon! 12 | -------------------------------------------------------------------------------- /web/src/components/common/PlannerConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HStack, VStack,Text } from "@chakra-ui/react"; 3 | import ReactJson from "react-json-view"; 4 | import { ToolPlannerConfig } from 'apps/types'; 5 | export default function PlannerConfig({plannerConfig}: {plannerConfig: ToolPlannerConfig}) { 6 | let modelString; 7 | if (plannerConfig.type === "cot") { 8 | modelString = plannerConfig.thinkingStage.llmSettings.model + ", " + plannerConfig.toolChoiceStage.llmSettings.model; 9 | } else { 10 | modelString = plannerConfig.llmSettings.model; 11 | } 12 | return ( 13 | 14 | 15 | 16 | {plannerConfig.type} 17 | {modelString} 18 | 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /simulator/src/fixtures/base.ts: -------------------------------------------------------------------------------- 1 | import { test as base, chromium, type BrowserContext } from '@playwright/test'; 2 | import * as path from 'path' 3 | 4 | export const test = base.extend<{ 5 | context: BrowserContext; 6 | // extensionId: string; 7 | }>({ 8 | context: async ({ }, use) => { 9 | const isHeadless = process.env.HEADLESS == 'true'; 10 | const pathToExtension = path.join(__dirname, '../../../extension/build'); 11 | const browserContextArgs = [ 12 | `--disable-extensions-except=${pathToExtension}`, 13 | `--load-extension=${pathToExtension}`, 14 | ] 15 | if (isHeadless) { 16 | browserContextArgs.push("--headless=new") 17 | } 18 | const context = await chromium.launchPersistentContext('', { 19 | headless: isHeadless, 20 | args: browserContextArgs, 21 | }); 22 | await use(context); 23 | await context.close(); 24 | }, 25 | }); 26 | export const expect = test.expect; -------------------------------------------------------------------------------- /web/src/package.ts: -------------------------------------------------------------------------------- 1 | export * as RPCs from './app/rpc'; 2 | export * as utils from './helpers/utils'; 3 | export { memoize } from './cache/cache' 4 | export { subscribe, unsubscribe } from './helpers/documentSubscription'; 5 | export { addNativeEventListener } from './helpers/nativeEvents'; 6 | export { configs } from './constants'; 7 | export { renderString } from './helpers/templatize'; 8 | export { contains } from './helpers/utils'; 9 | export { addCtesToQuery, processSQLWithCtesOrModels } from './helpers/sqlProcessor'; 10 | export { GLOBAL_EVENTS, captureEvent } from './tracking' 11 | export { getParsedIframeInfo } from './helpers/origin'; 12 | export { processMetadata, processAllMetadata } from './helpers/metadataProcessor'; 13 | export { dispatch } from './state/dispatch'; 14 | export { updateIsDevToolsOpen, updateDevToolsTabName, addMemory } from './state/settings/reducer' 15 | export { setInstructions } from './state/thumbnails/reducer'; 16 | -------------------------------------------------------------------------------- /simulator/README.md: -------------------------------------------------------------------------------- 1 | # Simulator 2 | 3 | A simulator to run e2e MinusX tests 4 | 5 | ## Init Setup Stuff 6 | 1. Only first time, have to install Chromium stuff 7 | ``` 8 | yarn playwright install 9 | ``` 10 | 2. On macs, have to start docker app 11 | 12 | ## Run local tests (GUI) 13 | Ensure there is a valid extension build in `../extension/build` and that it points to a valid MinusX web server (Typically localhost:3005, for development builds). Then run: 14 | 15 | ```sh 16 | yarn test 17 | ``` 18 | 19 | ## Test configs 20 | Test configs can be edited at `src/configs.ts` 21 | 22 | ## See reports 23 | 24 | To see test reports, run: 25 | 26 | ```sh 27 | yarn report 28 | ``` 29 | 30 | ## Run headless or production tests 31 | 32 | To run additional tests, you can pass `HEADLESS=true` and `PROD=true` to run headless tests and tests against the production build respectively. For example: 33 | 34 | ```sh 35 | HEADLESS=true PROD=true yarn test 36 | ``` -------------------------------------------------------------------------------- /web/src/helpers/documentSubscription.ts: -------------------------------------------------------------------------------- 1 | import { DOMQuery, DOMQueryMap, DOMQueryMapResponse, SubscriptionPayload } from "extension/types"; 2 | import { attachMutationListener, detachMutationListener } from "../app/rpc"; 3 | import { captureEvent, GLOBAL_EVENTS } from "../tracking"; 4 | 5 | type Callback = (d: SubscriptionPayload) => void 6 | 7 | const listeners: Record = {} 8 | 9 | export const subscribe = async (domQueryMap: DOMQueryMap, callback: Callback) => { 10 | const id = await attachMutationListener(domQueryMap) 11 | listeners[id] = callback 12 | return id 13 | } 14 | 15 | export const unsubscribe = async (id: number) => { 16 | delete listeners[id] 17 | await detachMutationListener(id) 18 | } 19 | 20 | export const onSubscription = (payload: SubscriptionPayload) => { 21 | const { id, url } = payload 22 | if (!(id in listeners)) { 23 | return 24 | } 25 | listeners[id](payload) 26 | } -------------------------------------------------------------------------------- /web/src/state/cache/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | import { MxModel } from '../../helpers/utils' 4 | 5 | interface CacheState { 6 | mxCollectionId: number | null | undefined 7 | mxModels: MxModel[] 8 | } 9 | 10 | const initialState: CacheState = { 11 | mxCollectionId: undefined, 12 | mxModels: [], 13 | } 14 | 15 | export const cacheSlice = createSlice({ 16 | name: 'cache', 17 | initialState, 18 | reducers: { 19 | setMxModels: (state, action: PayloadAction) => { 20 | state.mxModels = action.payload 21 | }, 22 | setMxCollectionId: (state, action: PayloadAction) => { 23 | state.mxCollectionId = action.payload 24 | } 25 | }, 26 | }) 27 | 28 | // Action creators are generated for each case reducer function 29 | export const { setMxModels, setMxCollectionId } = cacheSlice.actions 30 | 31 | export default cacheSlice.reducer -------------------------------------------------------------------------------- /web/src/components/common/QuickActionButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Icon, 4 | IconButton, 5 | Tooltip, 6 | } from '@chakra-ui/react' 7 | 8 | export const QuickActionButton = ({tooltip, onclickFn, icon, isDisabled}: 9 | {tooltip: string, onclickFn: any, icon: React.ElementType, isDisabled: boolean}) => { 10 | return ( 11 | 12 | 13 | } 20 | isDisabled={isDisabled} 21 | _disabled={{ 22 | _hover: { 23 | bg: '#eee', 24 | color: 'minusxBW.500', 25 | cursor: 'not-allowed', 26 | }, 27 | bg: 'transparent', 28 | color: 'minusxBW.500', 29 | }} 30 | /> 31 | 32 | ) 33 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy (manual) 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy-web-minusxapi-com: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Set up SSH 10 | run: | 11 | mkdir -p ~/.ssh 12 | echo "${{ secrets.PROD_SERVER_SSH_KEY }}" > ~/.ssh/id_rsa 13 | echo "${{ secrets.PROD_SERVER_SSH_PUB_KEY }}" > ~/.ssh/id_rsa.pub 14 | chmod 600 ~/.ssh/id_rsa 15 | chmod 600 ~/.ssh/id_rsa.pub 16 | ssh-keyscan -H web.minusxapi.com >> ~/.ssh/known_hosts 17 | 18 | - name: Run frontend server (v1) 19 | env: 20 | REF: ${{ github.sha }} # ref to checkout 21 | run: | 22 | ssh minusx@web.minusxapi.com << EOF 23 | set -e # Exit on any error 24 | cd /home/minusx/minusx/web 25 | git fetch --all --tags 26 | git checkout $REF 27 | git reset --hard $REF 28 | docker compose up -d --build 29 | docker image prune -f 30 | EOF 31 | 32 | -------------------------------------------------------------------------------- /extension/src/background/index.ts: -------------------------------------------------------------------------------- 1 | import {initBackgroundRPC} from './RPCs' 2 | import { configs as appConfigs } from '../constants' 3 | import { getExtensionID } from './identifier'; 4 | 5 | initBackgroundRPC() 6 | 7 | if (!appConfigs.IS_DEV) { 8 | console.log = () => {} 9 | } 10 | 11 | async function fetchAndStoreConfigs() { 12 | try { 13 | const id = await getExtensionID() 14 | const configURL = `${appConfigs.WEB_JSON_CONFIG_URL}?r=${id}`; 15 | const response = await fetch(configURL); 16 | if (!response.ok) throw new Error('Network response was not ok'); 17 | const configs = await response.json(); 18 | await chrome.storage.local.set({ configs }); 19 | } catch (error) { 20 | console.error('Error fetching or storing configs:', error); 21 | } 22 | } 23 | 24 | fetchAndStoreConfigs() 25 | let CONFIG_REFRESH_TIME = 10*60*1000 26 | if (appConfigs.IS_DEV) { 27 | CONFIG_REFRESH_TIME = 5*1000 28 | } 29 | setInterval(fetchAndStoreConfigs, CONFIG_REFRESH_TIME); -------------------------------------------------------------------------------- /simulator/src/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import * as path from 'path' 4 | 5 | const execPromise = promisify(exec); 6 | 7 | const setup = async () => { 8 | try { 9 | // run docker-compose in node 10 | const pathToRootFolder = path.join(__dirname, '../../..'); 11 | if (process.env.REUSE_DOCKER !== 'true') { 12 | await execPromise(`cd ${pathToRootFolder}/simulator && docker-compose up -d --build`); 13 | await new Promise(resolve => setTimeout(resolve, 5000)); 14 | } 15 | if (process.env.PROD !== 'true') { 16 | return 17 | } 18 | const command = `cd ${pathToRootFolder} && yarn extension-build` 19 | console.log('Running', command) 20 | const { stdout, stderr } = await execPromise(command); 21 | console.log('Stdout:', stdout); 22 | console.error('Stderr:', stderr); 23 | } catch (error) { 24 | console.error('Error setting up:', error.message); 25 | throw error 26 | } 27 | } 28 | 29 | export default setup -------------------------------------------------------------------------------- /web/src/helpers/LLM/index.ts: -------------------------------------------------------------------------------- 1 | import { planActionsOpenAI } from "./OpenAI"; 2 | import { getState } from '../../state/store' 3 | import { planActionsRemote } from "./remote"; 4 | import { Tasks, ToolCalls } from "../../state/chat/reducer"; 5 | import { LLMContext, LLMResponse, LLMSettings, LLMMetadata } from "./types"; 6 | 7 | export type researchMode = 'simple' | 'deepResearchPlanner' | 'deepResearchTool' 8 | 9 | export type PlanActionsParams = { 10 | messages: LLMContext, 11 | actions: any, 12 | llmSettings: LLMSettings, 13 | signal: AbortSignal, 14 | deepResearch: researchMode, 15 | tasks: Tasks, 16 | conversationID: string, 17 | meta?: LLMMetadata, 18 | isPrewarm?: boolean, 19 | } 20 | export async function planActions(params: PlanActionsParams ) : Promise { 21 | const { isLocal } = getState().settings 22 | console.log('Message & Actions are', params.messages, params.actions, isLocal) 23 | if (isLocal) { 24 | return planActionsOpenAI(params) 25 | } 26 | return planActionsRemote(params) 27 | } 28 | -------------------------------------------------------------------------------- /web/src/cache/events.ts: -------------------------------------------------------------------------------- 1 | import { openDB } from 'idb'; 2 | 3 | const dbPromise = openDB('event-storage', 1, { 4 | upgrade(db) { 5 | if (!db.objectStoreNames.contains('events')) { 6 | db.createObjectStore('events', { keyPath: 'id', autoIncrement: true }); 7 | } 8 | }, 9 | }); 10 | 11 | type Event = { 12 | type: string; 13 | payload?: object; 14 | }; 15 | 16 | export async function saveEvent(event: Event) { 17 | const db = await dbPromise; 18 | await db.add('events', { event, timestamp: Date.now() }); 19 | } 20 | 21 | type Callback = (events: {event: Event}[]) => Promise; 22 | 23 | export async function sendBatch(callback: Callback) { 24 | const db = await dbPromise; 25 | const events = await db.getAll('events'); 26 | 27 | const success = await callback(events); 28 | 29 | if (success) { 30 | const tx = db.transaction('events', 'readwrite'); 31 | const store = tx.objectStore('events'); 32 | await Promise.all(events.map(event => store.delete(event.id))); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/src/appSetupConfigs.ts: -------------------------------------------------------------------------------- 1 | import { once } from "lodash"; 2 | import { AppSetup } from "./base/appSetup"; 3 | // import { JupyterSetup } from "./jupyter/appSetup"; 4 | import { MetabaseSetup } from "./metabase/appSetup"; 5 | // import { PosthogSetup } from "./posthog/appSetup"; 6 | // import { GoogleSetup } from "./google/appSetup"; 7 | 8 | interface AppSetupConfig { 9 | name: string; 10 | appSetup: AppSetup; 11 | inject?: boolean; 12 | } 13 | 14 | export const getAppSetupConfigs = once(() : AppSetupConfig[] => [ 15 | { 16 | name: "metabase", 17 | appSetup: new MetabaseSetup(), 18 | inject: true, 19 | }, 20 | // { 21 | // name: "jupyter", 22 | // appSetup: new JupyterSetup(), 23 | // inject: true, 24 | // }, 25 | // { 26 | // name: "posthog", 27 | // appSetup: new PosthogSetup(), 28 | // inject: true, 29 | // }, 30 | // { 31 | // name: "google", 32 | // appSetup: new GoogleSetup(), 33 | // inject: false 34 | // } 35 | ]); -------------------------------------------------------------------------------- /web/src/app/clarification.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from "../state/dispatch" 2 | import { getState } from "../state/store" 3 | import { sleep } from "../helpers/utils" 4 | import { toggleClarification, ClarificationQuestion, ClarificationAnswer } from "../state/chat/reducer" 5 | 6 | export async function clarify({questions}: {questions: ClarificationQuestion[]}): Promise { 7 | const state = getState() 8 | const thread = state.chat.activeThread 9 | 10 | // Show clarification modal with questions 11 | dispatch(toggleClarification({show: true, questions})) 12 | 13 | // Poll for completion 14 | while (true) { 15 | const currentState = getState() 16 | const clarification = currentState.chat.threads[thread].clarification 17 | 18 | if (clarification.show && clarification.isCompleted) { 19 | // Hide clarification modal and return answers 20 | dispatch(toggleClarification({show: false, questions: []})) 21 | return clarification.answers 22 | } 23 | 24 | await sleep(100) 25 | } 26 | } -------------------------------------------------------------------------------- /web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "prettier", "unused-imports"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:jest/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "indent": ["error", 2], 16 | "prettier/prettier": ["error", { "tabWidth": 2 }], 17 | "@typescript-eslint/no-unused-vars": "off", 18 | "unused-imports/no-unused-imports": "error", 19 | "unused-imports/no-unused-vars": [ 20 | "warn", 21 | { 22 | "vars": "all", 23 | "varsIgnorePattern": "^_", 24 | "args": "after-used", 25 | "argsIgnorePattern": "^_" 26 | } 27 | ] 28 | }, 29 | "env": { 30 | "browser": true, 31 | "es2021": true, 32 | "node": true, 33 | "jest": true 34 | }, 35 | "globals": { 36 | "chrome": "readonly", 37 | "PRODUCTION": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /extension/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "prettier", "unused-imports"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:jest/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "indent": ["error", 2], 16 | "prettier/prettier": ["error", { "tabWidth": 2 }], 17 | "@typescript-eslint/no-unused-vars": "off", 18 | "unused-imports/no-unused-imports": "error", 19 | "unused-imports/no-unused-vars": [ 20 | "warn", 21 | { 22 | "vars": "all", 23 | "varsIgnorePattern": "^_", 24 | "args": "after-used", 25 | "argsIgnorePattern": "^_" 26 | } 27 | ] 28 | }, 29 | "env": { 30 | "browser": true, 31 | "es2021": true, 32 | "node": true, 33 | "jest": true 34 | }, 35 | "globals": { 36 | "chrome": "readonly", 37 | "PRODUCTION": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/src/package.ts: -------------------------------------------------------------------------------- 1 | export { getAppStateConfigs } from "./appStateConfigs"; 2 | export { applyTableDiffs } from "./common/utils"; 3 | export { getTableContextYAML, filterTablesByCatalog } from "./metabase/helpers/catalog"; 4 | export { getAllCardsAndModels, getTableData, getRelevantTablesAndDetailsForSelectedDb, getDatabaseTablesAndModelsWithoutFields, getAllCardsAndModels as getAllCards, getAllFields, getAllFieldsFiltered, getAllDashboards } from "./metabase/helpers/metabaseAPIHelpers"; 5 | export { fetchModelInfo } from "./metabase/helpers/metabaseAPI"; 6 | export { getAllTemplateTagsInQuery, applySQLEdits, type SQLEdits } from "./metabase/helpers/sqlQuery"; 7 | export { getModelsWithFields, getSelectedAndRelevantModels, modifySqlForMetabaseModels, replaceLLMFriendlyIdentifiersInSqlWithModels } from "./metabase/helpers/metabaseModels"; 8 | export { getCurrentQuery, getDashboardState } from "./metabase/helpers/metabaseStateAPI"; 9 | export { subscribeMB, onMBSubscription } from "./metabase/helpers/stateSubscriptions"; 10 | export { getTablesFromSqlRegex } from "./metabase/helpers/parseSql"; -------------------------------------------------------------------------------- /web/src/state/chat/types.ts: -------------------------------------------------------------------------------- 1 | import { Subset } from '../../helpers/utils' 2 | 3 | export type Base64URL = string 4 | export type ImageType = 'BASE64' | 'EXTERNAL' 5 | 6 | // Image types 7 | export interface ImageContext { 8 | text: string 9 | } 10 | 11 | export interface Base64Image { 12 | url: Base64URL 13 | type: Subset 14 | width: number 15 | height: number 16 | context: ImageContext 17 | } 18 | 19 | export type Image = Base64Image 20 | 21 | export type ChatMessageContentType = 'BLANK' | 'DEFAULT' | 'ACTIONS' 22 | 23 | // Message Type: DEFAULT 24 | export interface BlankMessageContent { 25 | type: Subset 26 | content?: string 27 | } 28 | 29 | // Message Type: DEFAULT 30 | export interface DefaultMessageContent { 31 | type: Subset 32 | images: Image[] 33 | text: string 34 | } 35 | 36 | export type ActionRenderInfo = { 37 | text?: string 38 | code?: string 39 | oldCode?: string 40 | language?: string 41 | hidden?: boolean 42 | extraArgs?: Record 43 | } -------------------------------------------------------------------------------- /extension/utils/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'production'; 3 | process.env.NODE_ENV = 'production'; 4 | process.env.ASSET_PATH = '/'; 5 | 6 | var webpack = require('webpack'), 7 | config = require('../webpack.config'); 8 | 9 | delete config.chromeExtensionBoilerplate; 10 | 11 | config.mode = 'production'; 12 | 13 | webpack(config, function (err, stats) { 14 | if (err) { 15 | console.error('Webpack error:', err); 16 | throw err; 17 | } 18 | 19 | if (stats.hasErrors()) { 20 | console.error('Webpack compilation errors:'); 21 | console.error(stats.toString({ colors: true, errorDetails: true })); 22 | throw new Error('Webpack compilation failed'); 23 | } 24 | 25 | if (stats.hasWarnings()) { 26 | console.warn('Webpack compilation warnings:'); 27 | console.warn(stats.toString({ colors: true, warningsOnly: true })); 28 | } 29 | 30 | console.log('Webpack compilation successful!'); 31 | console.log(stats.toString({ colors: true, chunks: false, modules: false })); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 MinusX, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /extension/src/helpers/components/RecursiveComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Markdown from 'react-markdown' 3 | import remarkGfm from 'remark-gfm' 4 | 5 | type NodeType = keyof JSX.IntrinsicElements; 6 | 7 | interface MarkdownElement { 8 | type: "Markdown"; 9 | children: string; 10 | } 11 | 12 | interface JSXElement { 13 | type: NodeType; 14 | props?: Record; 15 | children?: TreeNode[] | string; 16 | } 17 | 18 | export type TreeNode = MarkdownElement | JSXElement; 19 | 20 | const RenderTree: React.FC<{ node: TreeNode }> = ({ node }) => { 21 | const { type, children } = node; 22 | if (type === 'Markdown') { 23 | return {children} 24 | } 25 | const { props } = node 26 | const Component = type 27 | 28 | if (typeof children === 'string') { 29 | return React.createElement(type, props, children); 30 | } 31 | 32 | return ( 33 | 34 | {children?.map((childNode, index) => ( 35 | 36 | ))} 37 | 38 | ); 39 | }; 40 | 41 | export default RenderTree; -------------------------------------------------------------------------------- /web/licenses/TaxyAI-license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Taxy AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /extension/licenses/TaxyAI-license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Taxy AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /web/src/app/sidechat.ts: -------------------------------------------------------------------------------- 1 | import chat from "../chat/chat"; 2 | import { DefaultMessageContent } from '../state/chat/types' 3 | import { getState } from "../state/store" 4 | import { sleep } from "../helpers/utils" 5 | import _ from "lodash" 6 | import { getApp } from "../helpers/app"; 7 | 8 | export async function useAppFromExternal({text}: {text: string}) { 9 | const content: DefaultMessageContent = { 10 | type: "DEFAULT", 11 | text: text, 12 | images: [] 13 | } 14 | 15 | chat.addUserMessage({content}) 16 | while (true){ 17 | await sleep(2000) // hack to avoid race condition 18 | const state = getState() 19 | const thread = state.chat.activeThread 20 | const threadStatus = state.chat.threads[thread].status 21 | if (threadStatus === "FINISHED") { 22 | console.log("Thread finished!") 23 | break; 24 | } 25 | console.log(threadStatus) 26 | await sleep(100) 27 | } 28 | const outputImgs = [await getApp().actionController.getOutputAsImage()] || [] 29 | const outputText = await getApp().actionController.getOutputAsText() || '' 30 | 31 | return { 32 | type: "DEFAULT", 33 | text: outputText, 34 | images: outputImgs 35 | } 36 | } -------------------------------------------------------------------------------- /web/src/components/common/ModelDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | // import { updateModel } from '../../state/settings/reducer'; 4 | import { useSelector } from 'react-redux'; 5 | import { dispatch } from '../../state/dispatch'; 6 | 7 | const ModelDropdown = () => { 8 | // // TODO(@arpit): removed this for now since handling it as a static value and not exposign to user 9 | // const model = useSelector(state => state.settings.model) 10 | 11 | // return ( 12 | // // Chakra UI Select component 13 | // 26 | // ); 27 | }; 28 | 29 | export default ModelDropdown; 30 | -------------------------------------------------------------------------------- /extension/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "MinusX", 4 | "description": "The Best AI Agent for Metabase", 5 | "version": "1.0.33", 6 | "background": { "service_worker": "background.bundle.js" }, 7 | "permissions": [ 8 | "storage" 9 | ], 10 | "action": { 11 | "default_icon": "icon-34.png", 12 | "default_title": "Toggle minusx" 13 | }, 14 | "icons": { 15 | "128": "icon-128.png" 16 | }, 17 | "content_scripts": [ 18 | { 19 | "matches": [ 20 | "http://*/*", 21 | "https://*/*" 22 | ], 23 | "js": ["contentScript.bundle.js"], 24 | "css": [], 25 | "run_at": "document_start" 26 | } 27 | ], 28 | "host_permissions": [ 29 | "" 30 | ], 31 | "web_accessible_resources": [ 32 | { 33 | "resources": ["content.styles.css", "icon-128.png", "icon-34.png", "*.svg", "metabase.bundle.js", "jupyter.bundle.js", "posthog.bundle.js", "debug.bundle.js"], 34 | "matches": [""] 35 | } 36 | ], 37 | "commands": { 38 | "open-chat": { 39 | "suggested_key": { 40 | "default": "Ctrl+K", 41 | "mac": "Command+K" 42 | }, 43 | "description": "Open Chat on the current page." 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/src/helpers/origin.ts: -------------------------------------------------------------------------------- 1 | import type { IframeInfo } from 'extension/types'; 2 | import { configs } from '../constants'; 3 | import queryString from 'query-string'; 4 | 5 | export interface IframeInfoWeb extends IframeInfo { 6 | webGitCommitId: string 7 | webNpmVersion: string 8 | } 9 | 10 | export const defaultIframeInfoWeb: IframeInfoWeb = { 11 | tool: '', 12 | toolVersion: '', 13 | origin: '', 14 | mode: '', 15 | r: '', 16 | variant: 'default', 17 | width: '350', 18 | gitCommitId: '', 19 | npmPackageVersion: '', 20 | webGitCommitId: '', 21 | webNpmVersion: '', 22 | isEmbedded: false 23 | } 24 | 25 | const parsedIframeInfo: IframeInfo = (queryString.parse(location.search) as unknown as IframeInfo || defaultIframeInfoWeb) 26 | const parsedWebIframeInfo: IframeInfoWeb = { 27 | ...defaultIframeInfoWeb, 28 | ...parsedIframeInfo, 29 | webGitCommitId: configs.GIT_COMMIT_ID, 30 | webNpmVersion: configs.NPM_PACKAGE_VERSION, 31 | isEmbedded: parsedIframeInfo.isEmbedded || false, 32 | } 33 | 34 | export const getParsedIframeInfo = (): IframeInfoWeb => { 35 | return parsedWebIframeInfo 36 | } 37 | 38 | export const getOrigin = () => { 39 | const parsed = getParsedIframeInfo() 40 | return parsed.origin 41 | } -------------------------------------------------------------------------------- /apps/src/metabase/helpers/parseTables.ts: -------------------------------------------------------------------------------- 1 | import { get, map } from 'lodash'; 2 | import { FormattedTable } from './types'; 3 | 4 | export const extractTableInfo = (table: any, includeFields: boolean = false, schemaKey: string = 'schema'): FormattedTable => ({ 5 | name: get(table, 'name', ''), 6 | ...(get(table, 'description', null) != null && { description: get(table, 'description', null) }), 7 | schema: get(table, schemaKey, ''), 8 | display_name: get(table, 'display_name', ''), 9 | id: get(table, 'id', 0), 10 | ...( 11 | get(table, 'count') ? { count: get(table, 'count') } : {} 12 | ), 13 | ...( 14 | includeFields 15 | ? { 16 | columns: map(get(table, 'fields', []), (field: any) => ({ 17 | name: get(field, 'name', ''), 18 | id: get(field, 'id'), 19 | type: field?.target?.id ? 'FOREIGN KEY' : get(field, 'database_type', null), 20 | // only keep description if it exists. helps prune down context 21 | ...(get(field, 'description', null) != null && { description: get(field, 'description', null) }), 22 | // get foreign key info 23 | ...(field?.target?.table_id != null && { fk_table_id: field?.target?.table_id }), 24 | ...(field?.target?.name != null && { foreign_key_target: field?.target?.name }), 25 | })) 26 | } 27 | : {} 28 | ), 29 | }) -------------------------------------------------------------------------------- /extension/src/background/RPCs/chromeDebugger.ts: -------------------------------------------------------------------------------- 1 | export function attachDebugger(tabId: number) { 2 | return new Promise((resolve, reject) => { 3 | try { 4 | chrome.debugger.attach({ tabId }, '1.2', async () => { 5 | if (chrome.runtime.lastError) { 6 | console.error( 7 | 'Failed to attach debugger:', 8 | chrome.runtime.lastError.message 9 | ); 10 | reject( 11 | new Error( 12 | `Failed to attach debugger: ${chrome.runtime.lastError.message}` 13 | ) 14 | ); 15 | } else { 16 | console.log('attached to debugger'); 17 | await chrome.debugger.sendCommand({ tabId }, 'DOM.enable'); 18 | console.log('DOM enabled'); 19 | await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable'); 20 | console.log('Runtime enabled'); 21 | resolve(); 22 | } 23 | }); 24 | } catch (e) { 25 | reject(e); 26 | } 27 | }); 28 | } 29 | 30 | export async function detachDebugger(tabId: number) { 31 | const targets = await chrome.debugger.getTargets(); 32 | const isAttached = targets.some( 33 | (target) => target.tabId === tabId && target.attached 34 | ); 35 | if (isAttached) { 36 | chrome.debugger.detach({ tabId: tabId }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/common/VoiceInputButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, HStack, Icon, Tooltip } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { BsMic, BsMicMuteFill } from "react-icons/bs"; 4 | import { configs } from '../../constants'; 5 | 6 | export function VoiceInputButton({ disabled, onClick, isRecording }: { disabled: boolean, onClick: () => void, isRecording: boolean }) { 7 | 8 | const icon = isRecording ? BsMicMuteFill: BsMic; 9 | const variant = isRecording ? 'solid' : 'ghost'; 10 | const label = isRecording ? 'Stop recording' : 'Type using voice'; 11 | 12 | let button = ( 13 | 14 | } 23 | _disabled={{ 24 | _hover: { 25 | bg: '#eee', 26 | color: 'minusxBW.500', 27 | cursor: 'not-allowed', 28 | }, 29 | bg: 'transparent', 30 | color: 'minusxBW.500', 31 | }} 32 | /> 33 | 34 | ); 35 | 36 | return {button}; 37 | } 38 | -------------------------------------------------------------------------------- /apps/src/base/appState.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "./appHook"; 2 | import { defaultInternalState, InternalState } from "./defaultState"; 3 | import { AppController } from "./appController"; 4 | 5 | // Runs in web context 6 | export abstract class AppState { 7 | abstract initialInternalState: T; 8 | abstract actionController: AppController; 9 | 10 | // 1. Handles setup 11 | async setup() {} 12 | 13 | // 2. Get diagnostics 14 | async getDiagnostics(): Promise { 15 | return {} 16 | } 17 | 18 | // 3. Get / set internal state 19 | public useStore() { 20 | return createStore(this.initialInternalState) 21 | } 22 | 23 | public abstract getState(): Promise; 24 | 25 | // Get / set internal state 26 | public async getPlannerConfig() { 27 | return this.useStore().getState().llmConfigs.default; 28 | } 29 | 30 | public async getSuggestionsConfig() { 31 | return this.useStore().getState().llmConfigs.suggestions; 32 | } 33 | 34 | // Get query selectors 35 | public async getQuerySelectorMap() { 36 | return this.useStore().getState().querySelectorMap; 37 | } 38 | 39 | public async triggerStateUpdate() { 40 | } 41 | } 42 | 43 | export abstract class DefaultAppState extends AppState { 44 | initialInternalState = defaultInternalState; 45 | } 46 | -------------------------------------------------------------------------------- /apps/src/posthog/posthogObserver.ts: -------------------------------------------------------------------------------- 1 | const MAX_OBSERVER_TIME = 3000 2 | 3 | // TODO: this is a wip to find if we can actally access redux state. doesn't work right now. 4 | // can probably even remove the observer script entirely. 5 | // Observe the document for the addition of the script tag 6 | export const initObservePosthog = () => { 7 | const observer = new MutationObserver(function (mutations) { 8 | mutations.forEach(function (mutation) { 9 | if (mutation.addedNodes.length > 0) { 10 | mutation.addedNodes.forEach(function (node) { 11 | const substrToCheck = "index-T6ZJDWBT.js" 12 | if (node.nodeName === 'SCRIPT' ) { 13 | let scriptNode: HTMLScriptElement = node as HTMLScriptElement 14 | if (scriptNode.src.includes(substrToCheck)) { 15 | observer.disconnect(); // Stop observing once we've found and processed the script 16 | } 17 | } 18 | }); 19 | } 20 | }); 21 | }); 22 | 23 | observer.observe(document.documentElement, { childList: true, subtree: true }); 24 | document.onload = function () { 25 | console.log('Removing observer') 26 | observer.disconnect(); 27 | } 28 | setTimeout(() => { 29 | console.log('Removing observer') 30 | observer.disconnect(); 31 | }, MAX_OBSERVER_TIME) 32 | console.log('Started observing') 33 | } 34 | -------------------------------------------------------------------------------- /web/src/app/api/notifications.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { configs } from '../../constants'; 3 | import { NotificationsResponse, NotificationActionResponse } from '../../types/notifications'; 4 | 5 | const API_BASE_URL = configs.BASE_SERVER_URL + '/notifications'; 6 | 7 | export default { 8 | async getNotifications(): Promise { 9 | try { 10 | const response = await axios.post(`${API_BASE_URL}/`); 11 | return response.data; 12 | } catch (error) { 13 | console.error('Error fetching notifications:', error); 14 | throw error; 15 | } 16 | }, 17 | 18 | async markDelivered(notificationId: string): Promise { 19 | try { 20 | const response = await axios.post(`${API_BASE_URL}/${notificationId}/delivered`); 21 | return response.data; 22 | } catch (error) { 23 | console.error('Error marking notification as delivered:', error); 24 | throw error; 25 | } 26 | }, 27 | 28 | async markExecuted(notificationId: string): Promise { 29 | try { 30 | const response = await axios.post(`${API_BASE_URL}/${notificationId}/executed`); 31 | return response.data; 32 | } catch (error) { 33 | console.error('Error marking notification as executed:', error); 34 | throw error; 35 | } 36 | } 37 | }; -------------------------------------------------------------------------------- /apps/src/google/googleSheetInternalState.ts: -------------------------------------------------------------------------------- 1 | import { InternalState } from "../base/defaultState"; 2 | import { 3 | DEFAULT_PLANNER_SYSTEM_PROMPT, 4 | DEFAULT_PLANNER_USER_PROMPT, 5 | ACTION_DESCRIPTIONS_PLANNER, 6 | } from "./prompts" 7 | import { 8 | DEFAULT_SUGGESTIONS_SYSTEM_PROMPT, 9 | DEFAULT_SUGGESTIONS_USER_PROMPT, 10 | } from "../jupyter/prompts"; 11 | 12 | export const googleSheetInternalState: InternalState = { 13 | isEnabled: { 14 | value: true, 15 | reason: "", 16 | }, 17 | llmConfigs: { 18 | default: { 19 | type: "simple", 20 | llmSettings: { 21 | model: "gpt-4.1", 22 | temperature: 0, 23 | response_format: { type: "text" }, 24 | tool_choice: "required", 25 | }, 26 | systemPrompt: DEFAULT_PLANNER_SYSTEM_PROMPT, 27 | userPrompt: DEFAULT_PLANNER_USER_PROMPT, 28 | actionDescriptions: ACTION_DESCRIPTIONS_PLANNER, 29 | }, 30 | suggestions: { 31 | type: "simple", 32 | llmSettings: { 33 | model: "gpt-4.1-mini", 34 | temperature: 0, 35 | response_format: { 36 | type: "json_object", 37 | }, 38 | tool_choice: "none", 39 | }, 40 | systemPrompt: DEFAULT_SUGGESTIONS_SYSTEM_PROMPT, 41 | userPrompt: DEFAULT_SUGGESTIONS_USER_PROMPT, 42 | actionDescriptions: [], 43 | }, 44 | }, 45 | querySelectorMap: {}, 46 | }; -------------------------------------------------------------------------------- /apps/src/jupyter/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { ToolMatcher } from "extension/types" 2 | 3 | export const jupyterFingerprintMatcher: ToolMatcher = { 4 | default: { 5 | type: "combination", 6 | or: [ 7 | { 8 | type: "domQueryCondition", 9 | domQuery: { 10 | selector: { 11 | type: "XPATH", 12 | selector: "//title[text()='JupyterLab']", 13 | }, 14 | }, 15 | }, 16 | { 17 | type: "domQueryCondition", 18 | domQuery: { 19 | selector: { 20 | type: "XPATH", 21 | selector: "//title[text()='JupyterLite']", 22 | }, 23 | }, 24 | }, 25 | { 26 | type: "domQueryCondition", 27 | domQuery: { 28 | selector: { 29 | type: "XPATH", 30 | selector: "//title[contains(., 'Jupyter Notebook')]", 31 | }, 32 | }, 33 | }, 34 | { 35 | type: "domQueryCondition", 36 | domQuery: { 37 | selector: { 38 | type: "XPATH", 39 | selector: "//div[text()='About JupyterLab']", 40 | }, 41 | }, 42 | }, 43 | { 44 | type: "domQueryCondition", 45 | domQuery: { 46 | selector: { 47 | type: "CSS", 48 | selector: "#jupyter-config-data", 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /web/src/state/auth/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | type token = string 5 | 6 | interface LoginState { 7 | session_jwt?: token; 8 | profile_id?: string; 9 | email?: string; 10 | } 11 | 12 | interface AuthState extends LoginState{ 13 | is_authenticated: boolean; 14 | } 15 | 16 | const initialState: AuthState = { 17 | is_authenticated: false, 18 | } 19 | 20 | export const authSlice = createSlice({ 21 | name: 'auth', 22 | initialState, 23 | reducers: { 24 | register: ( 25 | state, 26 | action: PayloadAction 27 | ) => { 28 | state.session_jwt = action.payload 29 | }, 30 | login: ( 31 | state, 32 | action: PayloadAction 33 | ) => { 34 | const { session_jwt, profile_id, email } = action.payload 35 | state.session_jwt = session_jwt 36 | state.profile_id = profile_id 37 | state.email = email 38 | state.is_authenticated = true 39 | }, 40 | update_profile: (state, action: PayloadAction>) => { 41 | const { email} = action.payload 42 | state.email = email ?? state.email 43 | }, 44 | }, 45 | }) 46 | 47 | // Action creators are generated for each case reducer function 48 | export const { register, login, update_profile } = authSlice.actions 49 | 50 | export default authSlice.reducer 51 | -------------------------------------------------------------------------------- /web/src/app/userConfirmation.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from "../state/dispatch" 2 | import { getState } from "../state/store" 3 | import { sleep } from "../helpers/utils" 4 | import { toggleUserConfirmation } from "../state/chat/reducer" 5 | import { abortPlan } from '../state/chat/reducer' 6 | import { isUndefined } from "lodash" 7 | 8 | export async function getUserConfirmation({content, contentTitle, oldContent, override}: {content: string, contentTitle: string, oldContent: string | undefined, override?: boolean}) { 9 | const state = getState() 10 | const isEnabled = isUndefined(override) ? state.settings.confirmChanges : override 11 | if (!isEnabled) return { userApproved: true, userFeedback: '' } 12 | const thread = state.chat.activeThread 13 | dispatch(toggleUserConfirmation({show: true, content: content, contentTitle: contentTitle, oldContent: oldContent})) 14 | 15 | while (true){ 16 | const state = getState() 17 | const userConfirmation = state.chat.threads[thread].userConfirmation 18 | if (userConfirmation.show && userConfirmation.content === content && userConfirmation.userInput != 'NULL'){ 19 | const userApproved = userConfirmation.userInput == 'APPROVE' 20 | const userFeedback = userConfirmation.userFeedback || '' 21 | dispatch(toggleUserConfirmation({show: false, content: '', contentTitle: '', oldContent: ''})) 22 | return { userApproved, userFeedback } 23 | } 24 | await sleep(100) 25 | } 26 | } -------------------------------------------------------------------------------- /web/src/app/appSettings.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from "../state/dispatch" 2 | import { getState, RootState } from "../state/store" 3 | import { SemanticQuery } from 'web/types' 4 | import { setSemanticQuery } from "../state/thumbnails/reducer" 5 | 6 | export const getAppSettings = () => { 7 | const state: RootState = getState() 8 | const settings = state.settings 9 | return { 10 | semanticPlanner: settings.demoMode, 11 | tableDiff: settings.tableDiff, 12 | drMode: settings.drMode, 13 | analystMode: settings.analystMode, 14 | modelsMode: settings.modelsMode, 15 | selectedModels: settings.selectedModels, 16 | enable_highlight_helpers: settings.enable_highlight_helpers, 17 | manuallyLimitContext: settings.manuallyLimitContext, 18 | useV2States: settings.useV2States, 19 | selectedAssetId: settings.selectedAssetId, 20 | } 21 | } 22 | 23 | export const getCache = () => { 24 | const state: RootState = getState() 25 | return { 26 | mxCollectionId: state.cache.mxCollectionId, 27 | mxModels: state.cache.mxModels, 28 | } 29 | } 30 | 31 | export const getSemanticInfo = () => { 32 | const state: RootState = getState() 33 | return { 34 | semanticLayer: state.semanticLayer, 35 | semanticQuery: state.thumbnails.semanticQuery, 36 | currentSemanticLayer: state.thumbnails.semanticLayer 37 | } 38 | } 39 | 40 | export const applySemanticQuery = (query: SemanticQuery) => { 41 | dispatch(setSemanticQuery(query)) 42 | } -------------------------------------------------------------------------------- /web/src/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | logo 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /extension/src/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | logo 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/src/components/common/Thumbnails.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | HStack, 4 | Icon, 5 | IconButton, 6 | Image as ImageComponent, 7 | } from '@chakra-ui/react' 8 | import React from 'react-redux' 9 | import { BsX } from "react-icons/bs"; 10 | import _ from 'lodash' 11 | import { dispatch } from '../../state/dispatch' 12 | import { removeThumbnail } from '../../state/thumbnails/reducer' 13 | import { Image } from '../../state/chat/reducer' 14 | 15 | export const Thumbnails: React.FC<{thumbnails: Image[]}> = ({ thumbnails }) => { 16 | if (!thumbnails) { 17 | return null; 18 | } 19 | if (thumbnails.length == 0) { 20 | return null 21 | } 22 | const ThumbnailComponents = thumbnails.map(({url}, index: number) => { 23 | return ( 24 | 25 | dispatch(removeThumbnail(index))} 28 | variant="solid" 29 | colorScheme="minusxGreen" 30 | aria-label="Refresh" 31 | size={'xs'} 32 | icon={} 33 | position={"absolute"} 34 | top={0} 35 | right={0} 36 | /> 37 | 38 | 39 | ) 40 | }) 41 | return ( 42 | 43 | {ThumbnailComponents} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /web/src/state/billing/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | 5 | interface BillingState { 6 | isSubscribed: boolean 7 | credits: number 8 | stripeCustomerId?: string 9 | infoLoaded: boolean 10 | isEnterpriseCustomer: boolean 11 | } 12 | 13 | const initialState: BillingState = { 14 | isSubscribed: false, 15 | credits: 0, 16 | infoLoaded: false, 17 | isEnterpriseCustomer: false 18 | } 19 | 20 | 21 | export const billingSlice = createSlice({ 22 | name: 'billing', 23 | initialState, 24 | reducers: { 25 | setBillingInfo: ( 26 | state, 27 | action: PayloadAction 28 | ) => { 29 | state.credits = action.payload.credits 30 | state.isSubscribed = action.payload.isSubscribed 31 | state.stripeCustomerId = action.payload.stripeCustomerId 32 | state.infoLoaded = action.payload.infoLoaded 33 | state.isEnterpriseCustomer = action.payload.isEnterpriseCustomer 34 | }, 35 | updateCredits: ( 36 | state, 37 | action: PayloadAction 38 | ) => { 39 | if (action.payload !== null && action.payload !== undefined) { 40 | state.credits = action.payload 41 | } 42 | } 43 | }, 44 | }) 45 | 46 | // Action creators are generated for each case reducer function 47 | export const { setBillingInfo, updateCredits } = billingSlice.actions 48 | 49 | export default billingSlice.reducer -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.3.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY web/install-pyenv.sh ./ 6 | 7 | RUN apk add --no-cache \ 8 | git \ 9 | bash \ 10 | build-base \ 11 | libffi-dev \ 12 | openssl-dev \ 13 | bzip2-dev \ 14 | zlib-dev \ 15 | readline-dev \ 16 | sqlite-dev 17 | 18 | RUN sh ./install-pyenv.sh 19 | 20 | # Set environment variables for pyenv 21 | ENV PATH="/root/.pyenv/shims:/root/.pyenv/bin:$PATH" 22 | ENV PYENV_ROOT="/root/.pyenv" 23 | 24 | # Install Python 3.11.4 using pyenv 25 | RUN pyenv install 3.11.4 26 | RUN pyenv global 3.11.4 27 | 28 | RUN mkdir -p web 29 | 30 | COPY web/.yarnrc.yml ./yarn.lock web/package.json ./web 31 | 32 | WORKDIR /app/web 33 | 34 | # to enable mac builds 35 | # https://github.com/nodejs/docker-node/issues/1912#issuecomment-2133772534 36 | ENV UV_USE_IO_URING=0 37 | 38 | RUN yarn set version 4.3.1 39 | 40 | RUN yarn install 41 | 42 | WORKDIR /app 43 | 44 | COPY . . 45 | 46 | RUN yarn set version 4.3.1 47 | 48 | RUN yarn install 49 | 50 | # Build extension 51 | WORKDIR /app/extension 52 | RUN yarn install 53 | RUN yarn build 54 | 55 | # Copy extension build to web public directory 56 | RUN cp -r /app/extension/build /app/web/public/extension-build 57 | 58 | WORKDIR /app/web 59 | 60 | RUN yarn install 61 | 62 | ENV PATH="/app/node_modules/.bin:$PATH" 63 | 64 | # CMD ["sh", "-c", "sleep 300"] 65 | ENV NODE_OPTIONS="--max-old-space-size=2048" 66 | 67 | RUN yarn build 68 | 69 | CMD ["yarn", "serve"] 70 | -------------------------------------------------------------------------------- /apps/src/posthog/defaultState.ts: -------------------------------------------------------------------------------- 1 | import { InternalState } from "../base/defaultState"; 2 | import { 3 | DEFAULT_SYSTEM_PROMPT, 4 | DEFAULT_USER_PROMPT, 5 | DEFAULT_SUGGESTIONS_SYSTEM_PROMPT, 6 | DEFAULT_SUGGESTIONS_USER_PROMPT, 7 | } from "./prompts"; 8 | import { ACTION_DESCRIPTIONS } from "./actionDescriptions"; 9 | import { querySelectorMap } from "./querySelectorMap"; 10 | 11 | export const posthogInternalState: InternalState = { 12 | isEnabled: { 13 | value: true, 14 | reason: "", 15 | }, 16 | llmConfigs: { 17 | default: { 18 | type: "simple", 19 | llmSettings: { 20 | model: "gpt-4.1", 21 | temperature: 0, 22 | response_format: { type: "text" }, 23 | tool_choice: "required", 24 | }, 25 | systemPrompt: DEFAULT_SYSTEM_PROMPT, 26 | userPrompt: DEFAULT_USER_PROMPT, 27 | actionDescriptions: ACTION_DESCRIPTIONS, 28 | }, 29 | suggestions: { 30 | type: "simple", 31 | llmSettings: { 32 | model: "gpt-4.1-mini", 33 | temperature: 0, 34 | response_format: { 35 | type: "json_object", 36 | }, 37 | tool_choice: "none", 38 | }, 39 | systemPrompt: DEFAULT_SUGGESTIONS_SYSTEM_PROMPT, 40 | userPrompt: DEFAULT_SUGGESTIONS_USER_PROMPT, 41 | actionDescriptions: [], 42 | }, 43 | }, 44 | querySelectorMap, 45 | whitelistQuery: { 46 | editor: { 47 | selector: querySelectorMap["hogql_query"], 48 | attrs: ["class"], 49 | }, 50 | }, 51 | }; -------------------------------------------------------------------------------- /simulator/src/utils/minusxState.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from "playwright/test"; 2 | import { RootState } from "web"; 3 | 4 | export const initialiseMinusxState = async (frame: Frame, state: RootState) => { 5 | await frame.evaluate(async (action) => { 6 | window.__DISPATCH__(action) 7 | }, { 8 | key: "root", 9 | type: "persist/REHYDRATE", 10 | payload: state 11 | }); 12 | } 13 | 14 | export const getMinusxState = async (frame: Frame) => { 15 | const reduxState: RootState = await frame.evaluate(async (action) => { 16 | return window.__GET_STATE__() 17 | }) 18 | return reduxState 19 | } 20 | 21 | type WaitingOptions = { 22 | timeout?: number, 23 | polling?: number, 24 | } 25 | 26 | const waitTillMinusxFinished = async (frame: Frame, options?: WaitingOptions) => { 27 | return await frame.waitForFunction(() => { 28 | const reduxState: RootState = window.__GET_STATE__() 29 | const activeThread = reduxState.chat.activeThread 30 | const currentThread = reduxState.chat.threads[activeThread] 31 | return currentThread.status === 'FINISHED' 32 | }, undefined, options) 33 | } 34 | 35 | export const runMinusxInstruction = async (frame: Frame, instruction: string, options?: WaitingOptions) => { 36 | await frame.getByLabel('Enter Instructions').fill(instruction) 37 | await frame.getByLabel('Done').click() 38 | await waitTillMinusxFinished(frame, { 39 | timeout: 40000, 40 | polling: 100, 41 | ...options 42 | }) 43 | } -------------------------------------------------------------------------------- /web/src/state/semantic-layer/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | export interface Layer { 5 | name: string 6 | description: string 7 | } 8 | 9 | export interface Measure { 10 | name: string 11 | description: string 12 | } 13 | 14 | export interface Dimension { 15 | name: string 16 | description: string 17 | sample_values?: string[] 18 | distinct_count?: number 19 | } 20 | 21 | interface SemanticLayerState { 22 | availableMeasures: Measure[] 23 | availableDimensions: Dimension[] 24 | availableLayers: Layer[] 25 | } 26 | 27 | const initialState: SemanticLayerState = { 28 | availableMeasures: [], 29 | availableDimensions: [], 30 | availableLayers: [], 31 | } 32 | 33 | export const semanticLayerSlice = createSlice({ 34 | name: 'semanticLayer', 35 | initialState, 36 | reducers: { 37 | setAvailableMeasures: (state, action: PayloadAction) => { 38 | state.availableMeasures = action.payload 39 | }, 40 | setAvailableDimensions: (state, action: PayloadAction) => { 41 | state.availableDimensions = action.payload 42 | }, 43 | setAvailableLayers: (state, action: PayloadAction) => { 44 | state.availableLayers = action.payload 45 | }, 46 | }, 47 | }) 48 | 49 | // Action creators are generated for each case reducer function 50 | export const { setAvailableMeasures, setAvailableDimensions, setAvailableLayers } = semanticLayerSlice.actions 51 | 52 | export default semanticLayerSlice.reducer -------------------------------------------------------------------------------- /web/src/components/common/SemanticLayer.tsx: -------------------------------------------------------------------------------- 1 | // [WIP] This component is a WIP 2 | import { VStack, Text, Textarea, HStack, Button } from "@chakra-ui/react" 3 | import React from "react" 4 | 5 | export const SemanticLayer = () => { 6 | return 12 |