diff --git a/package.json b/package.json index d93f972..fdc043f 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "version": "4.0.2", "private": true, "scripts": { - "dev": "tauri dev", + "dev:tauri": "tauri dev", "build": "tauri build", "//build:debug": "this can be done easily from the CLI, so this is more for documentation", "build:debug": "tauri build --debug", - "ui:dev": "vite dev --host 0.0.0.0", + "dev": "vite dev --host 0.0.0.0", + "ui:dev": "pnpm run dev", "ui:build": "cross-env NODE_ENV=production vite build", "ui:build-static": "cross-env NODE_ENV=production SVELTE_ADAPTER=static vite build", "dev:server": "pnpm tsx --tsconfig ./tsconfig.server.json --watch ./sync-server/server.ts", @@ -16,7 +17,7 @@ "release": "direnv reload && yarn version && ./scripts/release.sh", "gh:publish": "./scripts/publish.sh", "preview": "vite preview", - "test": "playwright test", + "test": "playwright test --retries=2", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:unit": "vitest" @@ -44,19 +45,23 @@ "tsx": "4.6.2", "typescript": "^5.0.2", "vite": "^4.3.5", + "vite-plugin-top-level-await": "1.4.1", + "vite-plugin-wasm": "3.3.0", "vitest": "~0.31.0" }, "type": "module", "dependencies": { "@fastify/cors": "8.4.2", "@microsoft/fetch-event-source": "2.0.1", + "@radix-ui/react-progress": "1.0.3", "@tauri-apps/api": "1.5.2", "@vkontakte/vk-qr": "2.0.13", - "@vlcn.io/crsqlite": "0.15.1", - "@vlcn.io/crsqlite-wasm": "0.16.0-next.2", + "@vlcn.io/crsqlite": "0.16.1", + "@vlcn.io/crsqlite-wasm": "0.16.0", "@vlcn.io/rx-tbl": "0.15.0-next.0", "@vlcn.io/ws-common": "0.1.2", "@vlcn.io/xplat-api": "0.14.1", + "@xenova/transformers": "2.13.4", "better-sqlite3": "9.2.2", "bits-ui": "^0.11.8", "classnames": "2.3.2", @@ -65,13 +70,17 @@ "fastify": "4.24.3", "lucide-svelte": "0.298.0", "marked": "^4.0.0", + "mode-watcher": "0.1.2", "nanoid": "4.0.2", "openai": "^4.24.1", "prism-svelte": "0.5.0", "prismjs": "1.29.0", + "statsig-js": "4.45.1", "svelte-markdown": "0.4.0", + "svelte-sonner": "0.3.11", "tailwind-merge": "2.1.0", "tailwind-variants": "0.1.19", - "uuid": "9.0.0" + "uuid": "9.0.0", + "victor-db": "0.1.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef3225e..24406c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@microsoft/fetch-event-source': specifier: 2.0.1 version: 2.0.1 + '@radix-ui/react-progress': + specifier: 1.0.3 + version: 1.0.3(react-dom@18.2.0)(react@18.2.0) '@tauri-apps/api': specifier: 1.5.2 version: 1.5.2 @@ -18,11 +21,11 @@ dependencies: specifier: 2.0.13 version: 2.0.13 '@vlcn.io/crsqlite': - specifier: 0.15.1 - version: 0.15.1 + specifier: 0.16.1 + version: 0.16.1 '@vlcn.io/crsqlite-wasm': - specifier: 0.16.0-next.2 - version: 0.16.0-next.2 + specifier: 0.16.0 + version: 0.16.0 '@vlcn.io/rx-tbl': specifier: 0.15.0-next.0 version: 0.15.0-next.0 @@ -32,6 +35,9 @@ dependencies: '@vlcn.io/xplat-api': specifier: 0.14.1 version: 0.14.1 + '@xenova/transformers': + specifier: 2.13.4 + version: 2.13.4 better-sqlite3: specifier: 9.2.2 version: 9.2.2 @@ -56,6 +62,9 @@ dependencies: marked: specifier: ^4.0.0 version: 4.3.0 + mode-watcher: + specifier: 0.1.2 + version: 0.1.2(svelte@4.0.0) nanoid: specifier: 4.0.2 version: 4.0.2 @@ -68,9 +77,15 @@ dependencies: prismjs: specifier: 1.29.0 version: 1.29.0 + statsig-js: + specifier: 4.45.1 + version: 4.45.1 svelte-markdown: specifier: 0.4.0 version: 0.4.0(svelte@4.0.0) + svelte-sonner: + specifier: 0.3.11 + version: 0.3.11(svelte@4.0.0) tailwind-merge: specifier: 2.1.0 version: 2.1.0 @@ -80,6 +95,9 @@ dependencies: uuid: specifier: 9.0.0 version: 9.0.0 + victor-db: + specifier: 0.1.3 + version: 0.1.3 devDependencies: '@playwright/test': @@ -148,6 +166,12 @@ devDependencies: vite: specifier: ^4.3.5 version: 4.3.5(@types/node@18.15.11) + vite-plugin-top-level-await: + specifier: 1.4.1 + version: 1.4.1(vite@4.3.5) + vite-plugin-wasm: + specifier: 3.3.0 + version: 3.3.0(vite@4.3.5) vitest: specifier: ~0.31.0 version: 0.31.0 @@ -610,6 +634,11 @@ packages: resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} dev: false + /@huggingface/jinja@0.1.2: + resolution: {integrity: sha512-x5mpbfJt1nKmVep5WNP5VjNsjWApWNj8pPYI+uYMkBWH9bWUJmQmHt2lbf0VCoQd54Oq3XuFEh/UyoVh7rPxmg==} + engines: {node: '>=18'} + dev: false + /@internationalized/date@3.5.0: resolution: {integrity: sha512-nw0Q+oRkizBWMioseI8+2TeUPEyopJVz5YxoYVzR0W1v+2YytiYah7s/ot35F149q/xAg4F1gT/6eTd+tsUpFQ==} dependencies: @@ -706,6 +735,138 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + + /@radix-ui/react-compose-refs@1.0.1(react@18.2.0): + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + react: 18.2.0 + dev: false + + /@radix-ui/react-context@1.0.1(react@18.2.0): + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + react: 18.2.0 + dev: false + + /@radix-ui/react-primitive@1.0.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/react-slot': 1.0.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-progress@1.0.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/react-context': 1.0.1(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-slot@1.0.2(react@18.2.0): + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + react: 18.2.0 + dev: false + + /@rollup/plugin-virtual@3.0.2: + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dev: true + /@sveltejs/adapter-static@2.0.2(@sveltejs/kit@1.22.3): resolution: {integrity: sha512-9wYtf6s6ew7DHUHMrt55YpD1FgV7oWql2IGsW5BXquLxqcY9vjrqCFo0TzzDpo+ZPZkW/v77k0eOP6tsAb8HmQ==} peerDependencies: @@ -777,12 +938,135 @@ packages: - supports-color dev: true + /@swc/core-darwin-arm64@1.3.102: + resolution: {integrity: sha512-CJDxA5Wd2cUMULj3bjx4GEoiYyyiyL8oIOu4Nhrs9X+tlg8DnkCm4nI57RJGP8Mf6BaXPIJkHX8yjcefK2RlDA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.3.102: + resolution: {integrity: sha512-X5akDkHwk6oAer49oER0qZMjNMkLH3IOZaV1m98uXIasAGyjo5WH1MKPeMLY1sY6V6TrufzwiSwD4ds571ytcg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.3.102: + resolution: {integrity: sha512-kJH3XtZP9YQdjq/wYVBeFuiVQl4HaC4WwRrIxAHwe2OyvrwUI43dpW3LpxSggBnxXcVCXYWf36sTnv8S75o2Gw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.3.102: + resolution: {integrity: sha512-flQP2WDyCgO24WmKA1wjjTx+xfCmavUete2Kp6yrM+631IHLGnr17eu7rYJ/d4EnDBId/ytMyrnWbTVkaVrpbQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.3.102: + resolution: {integrity: sha512-bQEQSnC44DyoIGLw1+fNXKVGoCHi7eJOHr8BdH0y1ooy9ArskMjwobBFae3GX4T1AfnrTaejyr0FvLYIb0Zkog==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.3.102: + resolution: {integrity: sha512-dFvnhpI478svQSxqISMt00MKTDS0e4YtIr+ioZDG/uJ/q+RpcNy3QI2KMm05Fsc8Y0d4krVtvCKWgfUMsJZXAg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.3.102: + resolution: {integrity: sha512-+a0M3CvjeIRNA/jTCzWEDh2V+mhKGvLreHOL7J97oULZy5yg4gf7h8lQX9J8t9QLbf6fsk+0F8bVH1Ie/PbXjA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.3.102: + resolution: {integrity: sha512-w76JWLjkZNOfkB25nqdWUNCbt0zJ41CnWrJPZ+LxEai3zAnb2YtgB/cCIrwxDebRuMgE9EJXRj7gDDaTEAMOOQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.3.102: + resolution: {integrity: sha512-vlDb09HiGqKwz+2cxDS9T5/461ipUQBplvuhW+cCbzzGuPq8lll2xeyZU0N1E4Sz3MVdSPx1tJREuRvlQjrwNg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.3.102: + resolution: {integrity: sha512-E/jfSD7sShllxBwwgDPeXp1UxvIqehj/ShSUqq1pjR/IDRXngcRSXKJK92mJkNFY7suH6BcCWwzrxZgkO7sWmw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.3.102: + resolution: {integrity: sha512-OAjNLY/f6QWKSDzaM3bk31A+OYHu6cPa9P/rFIx8X5d24tHXUpRiiq6/PYI6SQRjUPlB72GjsjoEU8F+ALadHg==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.2 + '@swc/types': 0.1.5 + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.102 + '@swc/core-darwin-x64': 1.3.102 + '@swc/core-linux-arm-gnueabihf': 1.3.102 + '@swc/core-linux-arm64-gnu': 1.3.102 + '@swc/core-linux-arm64-musl': 1.3.102 + '@swc/core-linux-x64-gnu': 1.3.102 + '@swc/core-linux-x64-musl': 1.3.102 + '@swc/core-win32-arm64-msvc': 1.3.102 + '@swc/core-win32-ia32-msvc': 1.3.102 + '@swc/core-win32-x64-msvc': 1.3.102 + dev: true + + /@swc/counter@0.1.2: + resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==} + dev: true + /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: tslib: 2.4.1 dev: false + /@swc/types@0.1.5: + resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} + dev: true + /@tailwindcss/typography@0.5.9(tailwindcss@3.3.1): resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==} peerDependencies: @@ -930,6 +1214,10 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + /@types/long@4.0.2: + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + dev: false + /@types/marked@4.0.8: resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==} dev: true @@ -999,16 +1287,16 @@ packages: resolution: {integrity: sha512-yskZf4k0TgJV2atS4WgxjqICeGg1Z+hj8tjvsH2Clf17EJXAczDvn4x1zyqC0CRHDjiOkcbne/FhCKq/nykYiQ==} dev: false - /@vlcn.io/crsqlite-wasm@0.16.0-next.2: - resolution: {integrity: sha512-XnPzc3Y2RhnxonLjccqTW2E2iGGsy+u9FNeEMGaR7nzxQCK0qdNq5iEkLTD9GtzJ1sMgwkosZrqcKxKNIsMXJA==} + /@vlcn.io/crsqlite-wasm@0.16.0: + resolution: {integrity: sha512-5gf52eyMYvZirxuEUo4QS65JhEsw3fndoO+tCtCEOxuiIEtvaKB2/6wuuKGRlMVkxIp4Bls70D3DCF5v9lcJxA==} dependencies: - '@vlcn.io/wa-sqlite': 0.22.0-next.2 - '@vlcn.io/xplat-api': 0.15.0-next.2 + '@vlcn.io/wa-sqlite': 0.22.0 + '@vlcn.io/xplat-api': 0.15.0 async-mutex: 0.4.0 dev: false - /@vlcn.io/crsqlite@0.15.1: - resolution: {integrity: sha512-TE5Vks7/aOqfhSZ1BPV0+C+X4Mz6aTZ7SqwDUNAMTrxdkC3wgzaXB+8MbsEpfFyd7dcAMjdE1bBnFXNFANGNeQ==} + /@vlcn.io/crsqlite@0.16.1: + resolution: {integrity: sha512-ju6dONV/xq3haiHUVkY/mUuz14lXqak1GAyCsY8YqEoZUUORIx8nXa8aIOM6A+n31+KreTsJ4qSiG0VCnZLveA==} requiresBuild: true dev: false @@ -1018,8 +1306,8 @@ packages: '@vlcn.io/xplat-api': 0.15.0-next.0 dev: false - /@vlcn.io/wa-sqlite@0.22.0-next.2: - resolution: {integrity: sha512-b5S+w8iub6LmYdiAhvx/7iV2PH3CjT6H8i//OtZfaBJ8OeKI6FH8L0agcPaGSjzUnN3QD3Iv7vOLoC1XNumnbA==} + /@vlcn.io/wa-sqlite@0.22.0: + resolution: {integrity: sha512-OujKro0mAqP7/efUeCGB6zBiyMoSCFVe7jQKPF0n47U9ZhOaIW3kQUVCwF+CmzvzQfN1Vl4PrFQRNNxlSwTCNQ==} dev: false /@vlcn.io/ws-common@0.1.2: @@ -1034,16 +1322,26 @@ packages: comlink: 4.4.1 dev: false + /@vlcn.io/xplat-api@0.15.0: + resolution: {integrity: sha512-2/aE7VgI3EbIO5EcJGrskAJuCa2pteY1rWNWfhovFKMERe9NhJdlDMIB1I31X0sN/WC2DnF30RqcdTXNfYyzhQ==} + dependencies: + comlink: 4.4.1 + dev: false + /@vlcn.io/xplat-api@0.15.0-next.0: resolution: {integrity: sha512-z4+EkN3xPZCyxHuXmxylAqbCQc9v5bKk7OOiAihrL+fszgYzodQOgLsTPKNr1P5TgAZ/Qp/7ZJWwmCQyvMb99A==} dependencies: comlink: 4.4.1 dev: false - /@vlcn.io/xplat-api@0.15.0-next.2: - resolution: {integrity: sha512-zqU3np+gWhCpbGBiZwQSGQUfugEdBT40OC2nonIRQ6frpm2528wGT+8gBQ5fOt+aiVGyAb+hQ/bbEvoc3BeXww==} + /@xenova/transformers@2.13.4: + resolution: {integrity: sha512-yk8yDvQaCTEZJsasoUj+FWEM9dVcNdDXlushzJ0KjFe2oUOJ3XqICAJz1Htz/vWtM20ErGt509EKogACGYlpxA==} dependencies: - comlink: 4.4.1 + '@huggingface/jinja': 0.1.2 + onnxruntime-web: 1.14.0 + sharp: 0.32.6 + optionalDependencies: + onnxruntime-node: 1.14.0 dev: false /abort-controller@3.0.0: @@ -1179,6 +1477,10 @@ packages: dependencies: dequal: 2.0.3 + /b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1378,9 +1680,31 @@ packages: estree-walker: 3.0.3 periscopic: 3.1.0 + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: false + /colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: true @@ -1661,6 +1985,10 @@ packages: resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1757,6 +2085,10 @@ packages: safe-regex2: 2.0.0 dev: false + /flatbuffers@1.12.0: + resolution: {integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==} + dev: false + /focus-trap@7.5.4: resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} dependencies: @@ -1880,6 +2212,10 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true + /guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + dev: false + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -1928,6 +2264,10 @@ packages: engines: {node: '>= 0.10'} dev: false + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1979,11 +2319,19 @@ packages: engines: {node: '>=10'} dev: true + /js-sha256@0.10.1: + resolution: {integrity: sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==} + dev: false + /js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} dev: true + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + /json-schema-ref-resolver@1.0.1: resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} dependencies: @@ -2050,6 +2398,17 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true + /long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: @@ -2193,6 +2552,14 @@ packages: obliterator: 2.0.4 dev: false + /mode-watcher@0.1.2(svelte@4.0.0): + resolution: {integrity: sha512-XTdPCdqC3kqSvB+Q262Kor983YJkkB2Z3vj9uqg5IqKQpOdiz+xB99Jihp8sWbyM67drC7KKp0Nt5FzCypZi2g==} + peerDependencies: + svelte: ^4.0.0 + dependencies: + svelte: 4.0.0 + dev: false + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2241,6 +2608,10 @@ packages: semver: 7.5.1 dev: false + /node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + dev: false + /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2292,6 +2663,36 @@ packages: dependencies: wrappy: 1.0.2 + /onnx-proto@4.0.4: + resolution: {integrity: sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==} + dependencies: + protobufjs: 6.11.4 + dev: false + + /onnxruntime-common@1.14.0: + resolution: {integrity: sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==} + dev: false + + /onnxruntime-node@1.14.0: + resolution: {integrity: sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==} + os: [win32, darwin, linux] + requiresBuild: true + dependencies: + onnxruntime-common: 1.14.0 + dev: false + optional: true + + /onnxruntime-web@1.14.0: + resolution: {integrity: sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==} + dependencies: + flatbuffers: 1.12.0 + guid-typescript: 1.0.9 + long: 4.0.0 + onnx-proto: 4.0.4 + onnxruntime-common: 1.14.0 + platform: 1.3.6 + dev: false + /openai@4.24.1: resolution: {integrity: sha512-ezm/O3eiZMnyBqirUnWm9N6INJU1WhNtz+nK/Zj/2oyKvRz9pgpViDxa5wYOtyGYXPn1sIKBV0I/S4BDhtydqw==} hasBin: true @@ -2420,6 +2821,10 @@ packages: pathe: 1.1.0 dev: true + /platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + dev: false + /playwright-core@1.40.1: resolution: {integrity: sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==} engines: {node: '>=16'} @@ -2585,6 +2990,26 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + /protobufjs@6.11.4: + resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} + hasBin: true + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + '@types/node': 18.15.11 + long: 4.0.0 + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -2607,6 +3032,10 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false @@ -2625,10 +3054,27 @@ packages: strip-json-comments: 2.0.1 dev: false + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: true + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -2756,6 +3202,12 @@ packages: rimraf: 2.7.1 dev: true + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -2777,6 +3229,21 @@ packages: /set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + /sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.1 + semver: 7.5.4 + simple-get: 4.0.1 + tar-fs: 3.0.4 + tunnel-agent: 0.6.0 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2805,6 +3272,12 @@ packages: simple-concat: 1.0.1 dev: false + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + /sirv@2.0.2: resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} engines: {node: '>= 10'} @@ -2841,6 +3314,13 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /statsig-js@4.45.1: + resolution: {integrity: sha512-h94RzFQsJCQCNwQXpZ9OBXcvCxDnkXF6OrCekd81ySvY2l4JSowpxMWX3Iw6IDFzfTfKdER9JQzFLhMSQbT+YQ==} + dependencies: + js-sha256: 0.10.1 + uuid: 8.3.2 + dev: false + /std-env@3.3.3: resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} dev: true @@ -2850,6 +3330,13 @@ packages: engines: {node: '>=10.0.0'} dev: true + /streamx@2.15.6: + resolution: {integrity: sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -2989,6 +3476,14 @@ packages: typescript: 5.0.4 dev: true + /svelte-sonner@0.3.11(svelte@4.0.0): + resolution: {integrity: sha512-TkjgDC7zr0waky81Z9CShXMD+4NQ7UASuRx0BhgQo8ZTDQQYk8X8MzJa3zVtZVa6RYJEiahHBXx8Zt/Ie9G5hg==} + peerDependencies: + svelte: '>=3 <5' + dependencies: + svelte: 4.0.0 + dev: false + /svelte@4.0.0: resolution: {integrity: sha512-+yCYu3AEUu9n91dnQNGIbnVp8EmNQtuF/YImW4+FTXRHard7NMo+yTsWzggPAbj3fUEJ1FBJLkql/jkp6YB5pg==} engines: {node: '>=16'} @@ -3074,6 +3569,14 @@ packages: tar-stream: 2.2.0 dev: false + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.6 + dev: false + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -3085,6 +3588,14 @@ packages: readable-stream: 3.6.2 dev: false + /tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + dependencies: + b4a: 1.6.4 + fast-fifo: 1.3.2 + streamx: 2.15.6 + dev: false + /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3206,11 +3717,25 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: true + + /victor-db@0.1.3: + resolution: {integrity: sha512-Z671naLtqHWxXf7fBWOYqyxkmZ9f6jY5Mj+b5h8XLpmnBiksHn92bn0lL9tj9f4n11rLvHWBw0AKR2WCr1w4jg==} + dev: false + /vite-node@0.31.0(@types/node@18.15.11): resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} engines: {node: '>=v14.18.0'} @@ -3232,6 +3757,28 @@ packages: - terser dev: true + /vite-plugin-top-level-await@1.4.1(vite@4.3.5): + resolution: {integrity: sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==} + peerDependencies: + vite: '>=2.8' + dependencies: + '@rollup/plugin-virtual': 3.0.2 + '@swc/core': 1.3.102 + uuid: 9.0.1 + vite: 4.3.5(@types/node@18.15.11) + transitivePeerDependencies: + - '@swc/helpers' + - rollup + dev: true + + /vite-plugin-wasm@3.3.0(vite@4.3.5): + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + dependencies: + vite: 4.3.5(@types/node@18.15.11) + dev: true + /vite@4.3.5(@types/node@18.15.11): resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/app.postcss b/src/app.postcss index 1da7f02..7db1d0f 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -108,4 +108,8 @@ button { cursor: default; } + .prose.prose-invert ul, + .prose.prose-invert ol { + @apply pl-8; + } } diff --git a/src/lib/components/ActionsMenu.svelte b/src/lib/components/ActionsMenu.svelte index eca2fc7..3ce3f98 100644 --- a/src/lib/components/ActionsMenu.svelte +++ b/src/lib/components/ActionsMenu.svelte @@ -33,8 +33,11 @@ import { toast } from "$lib/toast"; import { chatModels, llmProviders, modelPickerOpen } from "$lib/stores/stores/llmProvider"; import IconOpenAi from "./IconOpenAI.svelte"; - import { Brain } from "lucide-svelte"; import IconBrain from "./IconBrain.svelte"; + import { Atom, Command as CommandIcon, FlaskConical, Flag } from "lucide-svelte"; + import { goto } from "$app/navigation"; + import { page } from "$app/stores"; + import { featureFlags } from "$lib/featureFlags"; let _class: string = ""; export { _class as class }; @@ -50,6 +53,7 @@ { name: "Generate Title...", icon: IconThreadTitle, + when: () => $page.url.pathname === "/", execute: () => { generateThreadTitle({ threadId: $currentThread.id }).catch((error) => { toast({ @@ -62,6 +66,7 @@ }, { name: "Regenerate Response", + when: () => $page.url.pathname === "/", icon: IconRefreshOutline, execute: currentChatThread.regenerateResponse, }, @@ -69,7 +74,7 @@ name: "Enable OpenAI", when: () => { const oai = llmProviders.getOpenAi(); - return !oai.apiKey && oai.enabled; + return !oai.apiKey && oai.enabled && $page.url.pathname === "/"; }, icon: IconOpenAi, execute: () => { @@ -77,14 +82,14 @@ }, }, { - when: () => !sys.isBrowser, + when: () => !sys.isBrowser && $page.url.pathname === "/", name: "Chat History", icon: IconHistoryClock, keyboard: { shortcut: "meta+p" }, execute: () => ($threadMenu.open = !$threadMenu.open), }, { - when: () => sys.isBrowser, + when: () => sys.isBrowser && $page.url.pathname === "/", name: "Chat History", icon: IconHistoryClock, keyboard: { shortcut: "ctrl+p" }, @@ -92,7 +97,7 @@ }, { - when: () => !sys.isBrowser, + when: () => !sys.isBrowser && $page.url.pathname === "/", name: "New Chat", icon: IconSparkle, keyboard: { shortcut: "meta+n" }, // NOTE Meta key with N only works in the Tauri app. In a browser this opens a new window @@ -100,7 +105,7 @@ execute: currentThread.reset, }, { - when: () => sys.isBrowser, + when: () => sys.isBrowser && $page.url.pathname === "/", name: "New Chat", icon: IconSparkle, keyboard: { shortcut: "ctrl+n" }, @@ -109,7 +114,7 @@ }, { - when: () => !sys.isBrowser, + when: () => !sys.isBrowser && $page.url.pathname === "/", name: "Choose LLM Model", icon: IconBrain, keyboard: { shortcut: "meta+l" }, // NOTE Meta key with N only works in the Tauri app. In a browser this opens a new window @@ -119,7 +124,7 @@ }, }, { - when: () => sys.isBrowser, + when: () => sys.isBrowser && $page.url.pathname === "/", name: "Choose LLM Model", icon: IconBrain, keyboard: { shortcut: "ctrl+l" }, @@ -132,13 +137,14 @@ { name: "Archive Chat", icon: IconArchiveIn, - when: () => !$currentThread.archived && !isNewThread($currentThread), + when: () => + !$currentThread.archived && !isNewThread($currentThread) && $page.url.pathname === "/", execute: currentThread.archive, }, { name: "Unarchive Chat", icon: IconArchiveOut, - when: () => $currentThread.archived, + when: () => $currentThread.archived && $page.url.pathname === "/", execute: currentThread.unarchive, }, { @@ -148,6 +154,31 @@ $showSettings = true; }, }, + { + name: "Back to chat", + icon: IconBrain, + when: () => $page.url.pathname !== "/", + execute: () => { + goto("/"); + }, + }, + { + name: "Feature Flags", + icon: Flag, + altFilterText: "beta alpha experimental lab", + when: () => $page.url.pathname !== "/dev/feature-flags", + execute: () => { + goto("/dev/feature-flags"); + }, + }, + { + name: "Dev Experiments", + icon: FlaskConical, + when: () => $page.url.pathname !== "/dev" && featureFlags.check("dev_experiments"), + execute: () => { + goto("/dev"); + }, + }, { name: "Refresh Models List", icon: IconRefreshOutline, @@ -170,6 +201,7 @@ { name: "Attempt Restore DB", icon: IconTerminalPrompt, + when: () => dev && $devStore.showDebug, execute: async () => { try { await reinstatePriorData(); @@ -436,16 +468,21 @@ data-testid="CommandMenuButton" type="button" on:click={toggleMenu} - class={classNames("font-bold px-4 py-2", _class, {})} + class={classNames( + "font-bold px-2 py-2 flex items-center justify-center border border-zinc-700 rounded-lg h-[42px] w-[42px]", + _class, + { + "bg-zinc-800": menuOpen, + } + )} > - Command - {#if menuOpen} diff --git a/src/lib/components/DevTooling.svelte b/src/lib/components/DevTooling.svelte index a8f54e5..4a357f7 100644 --- a/src/lib/components/DevTooling.svelte +++ b/src/lib/components/DevTooling.svelte @@ -13,8 +13,10 @@ LLMProvider, _clearDatabase, _get_db_instance, + VecToFrag, } from "$lib/db"; import { currentChatThread, currentThread, insertPendingMessage } from "$lib/stores/stores"; + import { featureFlags } from "$lib/featureFlags"; onMount(() => { // This used to be locked behind a dev flag but I find it useful to have access to it for debugging in the prod app. @@ -25,7 +27,9 @@ ["Preferences", Preferences], ["Fragment", Fragment], ["LLMProvider", LLMProvider], + ["VecToFrag", VecToFrag], ["db", _get_db_instance()], + ["featureFlags", featureFlags], [ "insertPendingMessage", ({ content = "" }) => { diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 0054bbe..942ba68 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -9,13 +9,16 @@ import AutosizeTextarea from "./AutosizeTextarea.svelte"; import { getSystem } from "$lib/gui"; import { onMount } from "svelte"; - import { ChatMessage, Thread, getLatestDbName } from "$lib/db"; + import { ChatMessage, LLMProvider, Thread, getLatestDbName } from "$lib/db"; import { mapKeys, toCamelCase } from "$lib/utils"; import CloseButton from "./CloseButton.svelte"; import { env } from "$env/dynamic/public"; import { toast } from "$lib/toast"; import { Circle, HelpCircle } from "lucide-svelte"; import LlmProviderList from "./LLMProviderList.svelte"; + import { Button } from "./ui/button"; + import { Progress } from "./ui/progress"; + import { slide } from "svelte/transition"; const versionString = env.PUBLIC_VERSION_STRING; @@ -30,16 +33,100 @@ dbName = getLatestDbName() || ""; }); - let modelsLoading = false; - let showAdvanced = false; $: if ($showSettings) { chatModels.refresh(); } - $: hasCustomModel = - $gptProfileStore.model && !$chatModels.models.some((x) => x.id == $gptProfileStore.model); + // Note these are not all the tables, but some tables (fragment for instance) are derived. + const tables = ["thread", "message", "llm_provider"]; + + const handleExport = async () => { + if (!$db) { + console.error("No database"); + throw new Error("No database"); + } + + const sys = getSystem(); + + const tableData: [tableName: string, tableData: any[]][] = []; + for (const table of tables) { + tableData.push([table, await $db.execO(`SELECT * FROM ${table}`)]); + } + + const version = 2; + + const data = { + tables: tableData, + exportDate: new Date().toISOString(), + version, + }; + + await sys.saveAs(`${Date.now()}_prompta.v${version}.json`, JSON.stringify(data)); + }; + + let importRowCount = 0; + let importProgress = 0; + let isImporting = false; + + const handleImportV2 = async (json: any) => { + if (!$db) { + console.error("No database"); + throw new Error("No database"); + } + + const sys = getSystem(); + + const { tables } = json; + + if ( + !(await sys.confirm( + `Are you sure you want to import ${tables.length} tables? No data will be deleted, but this cannot be undone.` + )) + ) { + console.log("Cancelled"); + return; + } + + try { + isImporting = true; + importRowCount = tables.reduce((acc, [_, tableData]) => acc + tableData.length, 0); + importProgress = 0; + + let pct = importProgress / importRowCount || 1; /* avoid div by zero */ + + await $db.tx(async (tx) => { + for (const [tableName, tableData] of tables) { + for (const row of tableData) { + importProgress += 1; + pct = importProgress / importRowCount; + + console.log( + `Importing ${tableName} ${importProgress}/${importRowCount} (${Math.round( + pct * 100 + )}%)` + ); + + const x = mapKeys(row, (x) => { + return toCamelCase(String(x)); + }); + if (tableName === "message") await ChatMessage.upsert(x, tx); + else if (tableName === "thread") await Thread.upsert(x, tx); + else if (tableName === "llm_provider") await LLMProvider.upsert(x, tx); + else throw new Error("Unknown table"); + } + } + }); + + console.log("%c[import/v2] success", "color:salmon;", tables.length, "tables imported"); + } catch (error) { + console.error("%c[import/v2] error", "color:salmon;", error); + toast({ title: "Error importing", message: error.message, type: "error" }); + } finally { + isImporting = false; + } + }; @@ -144,39 +231,20 @@
- - Export All - -

- Will download multiple files. -

+
+

Export your database so you can import it again somewhere else

+
- -

- Run multiple times to import multiple prior exports. If records have the same ID, - the later one will overwrite. -

+ + {#if isImporting} +
+

+ Importing {importRowCount} records... If it's taking a while, + please be patient. If you're importing thousands of records it + may take some time and the window may become unresponsive. +

+ +
+ {:else} +
+

Import records you've exported.

+
+ {/if}
diff --git a/src/lib/components/ThreadMenuList.svelte b/src/lib/components/ThreadMenuList.svelte index 288762e..aea82d0 100644 --- a/src/lib/components/ThreadMenuList.svelte +++ b/src/lib/components/ThreadMenuList.svelte @@ -1,4 +1,5 @@ + + +
+ diff --git a/src/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..d722b0d --- /dev/null +++ b/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/tabs/index.ts b/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000..968804c --- /dev/null +++ b/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import { Tabs as TabsPrimitive } from "bits-ui"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +const Root = TabsPrimitive.Root; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/src/lib/components/ui/tabs/tabs-content.svelte b/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000..3866292 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/tabs/tabs-list.svelte b/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000..8905c77 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/ui/tabs/tabs-trigger.svelte b/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000..d8cac33 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/db.ts b/src/lib/db.ts index 17de99e..d5a233d 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,7 +1,8 @@ import initWasm, { SQLite3, DB } from "@vlcn.io/crsqlite-wasm"; import type { TXAsync, Schema, DBAsync } from "@vlcn.io/xplat-api"; import wasmUrl from "@vlcn.io/crsqlite-wasm/crsqlite.wasm?url"; -import { db, sqlite, currentThread, syncStore, isNewThread, newThread } from "../lib/stores/stores"; +import { db, currentThread, syncStore, isNewThread, newThread } from "../lib/stores/stores"; +import { vecDbStore } from "../lib/stores/stores/vecDbStore"; import { dev } from "$app/environment"; import { nanoid } from "nanoid"; import type OpenAI from "openai"; @@ -10,12 +11,16 @@ import { basename, debounce, groupBy, mapKeys, sha1sum, toCamelCase, toSnakeCase import { extractFragments } from "./markdown"; import tblrx, { TblRx } from "@vlcn.io/rx-tbl"; -import schemaUrl from "$lib/migrations/0002_schema.sql?url"; +import schema_0002 from "$lib/migrations/0002_schema.sql?url"; +import schema_0003 from "$lib/migrations/0003_schema_vecdb.sql?url"; -export { schemaUrl }; +// Default schema. May be overwritten via feature flags (see references) +let schemaUrl = schema_0002; import { llmProviders, openAiConfig } from "./stores/stores/llmProvider"; import { profilesStore } from "./stores/stores/llmProfile"; +import { featureFlags } from "./featureFlags"; +import type { VecDB } from "./vecDb"; const legacyDbNames = [ "chat_db-v1", @@ -79,13 +84,17 @@ export const incrementDbName = () => { return next; }; -type RoleEnum = OpenAI.Chat.Completions.ChatCompletionMessage["role"]; +// NOTE: Not all of thse are usable by openai. Some are custom +type RoleEnum = "user" | "system" | "assistant" | "comment"; +type OpenAICompatibleRole = "user" | "system" | "assistant"; let _sqlite: SQLite3; let _db: DB; +let _vecDb: VecDB; /** For use in debugging */ export const _get_db_instance = () => _db; +export const _get_vecDb_instance = () => _vecDb; /** * A helper for testing purposes. @@ -190,6 +199,11 @@ export const reinstatePriorData = async (database: DBAsync = _db as DBAsync) => }; export const getCurrentSchema = async () => { + // Alternate schemas as needed + if (featureFlags.check("vector_search_features")) { + schemaUrl = schema_0003; + } + const schemaRaw = await fetch(schemaUrl).then((r) => (r.ok ? r.text() : Promise.reject(r))); const u = new URL(schemaUrl, window.location.href); const schemaName = basename(u.pathname) as string; @@ -211,11 +225,29 @@ export const initDb = async (dbName: string) => { // @note This only works in the browser. Don't use SSR anywhere where you need this _sqlite = await initWasm(() => wasmUrl); - sqlite.set(_sqlite); - _db = await _sqlite.open(dbName); db.set(_db); + if (featureFlags.check("vector_search_features")) { + const { VecDB } = await import("$lib/vecDb"); + const { createEmbedding } = await import("$lib/embeddings"); + + _vecDb = await VecDB.create({ + db: _get_db_instance(), + embedString: async (s: string) => { + const tensor = await createEmbedding(s); + const x = tensor?.list[0]; + if (!x) { + throw new Error("Could not create embedding"); + } + + return x; + }, + }); + + vecDbStore.init(_vecDb); + } + const { name, content } = await getCurrentSchema(); const schema: Schema = { @@ -301,6 +333,15 @@ export type LLMProvider = Omit & { enabled: boolean; }; +export interface VecToFragRow { + id: string; + vec_id: string; + frag_id: number; + created_at: string; +} + +export type VecToFrag = Omit; + export interface FragmentRow { id: string; entity_id: string; @@ -635,6 +676,10 @@ export const LLMProvider = { }), }; +export const VecToFrag = crud({ + tableName: "vec_to_frag", +}); + export const ChatMessage = { ...crud({ tableName: "message", @@ -655,7 +700,7 @@ export const ChatMessage = { threadId, }: { threadId: string; - }): Promise & { role: RoleEnum }>> { + }): Promise & { role: OpenAICompatibleRole }>> { const context = await ChatMessage.findMany({ where: { threadId, @@ -665,7 +710,8 @@ export const ChatMessage = { }, orderBy: { createdAt: "ASC" }, }); - return context as Array & { role: RoleEnum }>; + + return context as Array & { role: OpenAICompatibleRole }>; }, }; @@ -714,6 +760,25 @@ export type FullTextSearchFilter = { offset?: number; }; +export interface SemanticFragmentRow { + id: number; + role: string; + value: string; + thread_id: string; + entity_type: string; + message_id: string; + created_at: string; +} + +interface SemanticFragment { + id: number; + role: RoleEnum | string; + value: string; + threadId: string; + messageId: string; + parentCreatedAt: Date; +} + export const Fragment = { ...crud({ generateId: async (x) => { @@ -728,6 +793,51 @@ export const Fragment = { }; }, }), + + /** + * Find fragments for use in semantic search. Fragments returned from this + * function are expected to be used for indexing in a vector store. + * + * @todo We may need to track whether something has been indexed here. not sure if the vec db can do it. + */ + async findSemanticFragments({ + where, // TODO: use the where clause + limit = 500, + includeProcessedRows = false, + }: { + where?: { threadId?: string; role?: RoleEnum; before?: Date }; + limit?: number; + includeProcessedRows?: boolean; + } = {}): Promise> { + const rows = await _db.execO( + ` + SELECT + m.role, + m.thread_id, + m.created_at, + m.id as 'message_id', + f.id, + f.value, + f.entity_type + FROM + fragment f + INNER JOIN message m ON m.id = f.entity_id + ORDER BY + m.created_at ASC + LIMIT + ?; + `, + [limit] + ); + + return rows.map(({ created_at, thread_id, message_id, ...row }) => ({ + ...row, + threadId: thread_id, + messageId: message_id, + parentCreatedAt: dateFromSqlite(created_at), + })); + }, + async fullTextSearch( content: string, { archived = false, limit = 500, offset = 0 }: FullTextSearchFilter = {} @@ -873,6 +983,11 @@ const syncMessageFragments = debounce(async () => { AND fragment.entity_id NOT IN ( SELECT id FROM "message")` ); }); + + if (featureFlags.check("vector_search_features")) { + console.debug("syncing vector db"); + _vecDb.ingestFragments(); + } }, 1000); /** diff --git a/src/lib/embeddings.ts b/src/lib/embeddings.ts new file mode 100644 index 0000000..1c790f8 --- /dev/null +++ b/src/lib/embeddings.ts @@ -0,0 +1,50 @@ +import { FeatureExtractionPipeline, pipeline } from "@xenova/transformers"; + +// Good for english. Small in size. Only english supported afaik +// ~ 25MB quantized +// const EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2"; + +// I got very bad results with these unfortunately in English. Ideally we do not +// want degrated english performance. +// ~ 120MB quantized +// const EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2"; +// const EMBEDDING_MODEL = "Xenova/multilingual-e5-small"; + +// Results seem decent, but IMO not better than the base english model. Much, +// much slower. Would need to be put into a web worker to be viable. Embeddings +// taking a second or more. Also, cannot count on all users having a powerful +// machine. +// ~ 500mb quantized +// const EMBEDDING_MODEL = "Xenova/multilingual-e5-large"; + +// ~ 30MB quantized +const EMBEDDING_MODEL = "Supabase/gte-small"; + +let extractor: FeatureExtractionPipeline | undefined; + +export const createEmbedding = async (s: string) => { + if (!s.trim()) { + throw new Error("Cannot create embedding for empty string"); + } + + // Load once + if (!extractor) { + console.time("loading model"); + extractor = await pipeline("feature-extraction", EMBEDDING_MODEL); + console.timeEnd("loading model"); + } + + try { + const tensor = await extractor(s, { pooling: "mean", normalize: true }); + return { + dims: tensor.dims, + data: tensor.data, + type: tensor.type, + list: tensor.tolist().map((x) => new Float64Array(x)), + size: tensor.size, + }; + } catch (error) { + console.warn("Could not create ensore for string:\n", s); + console.error(error); + } +}; diff --git a/src/lib/featureFlags.ts b/src/lib/featureFlags.ts new file mode 100644 index 0000000..e3a9cc5 --- /dev/null +++ b/src/lib/featureFlags.ts @@ -0,0 +1,90 @@ +import statsig, { type StatsigUser } from "statsig-js"; + +const lsKey = "feature-flags:custom"; + +const flagList = [ + { + id: "vector_search_features" as const, + name: "Semantic Search Features", + description: ` +Use semantic search to find similar messages when using the search feature. This +is a prerequisite to semantic conversation history compression. + `.trim(), + }, + { + id: "dev_experiments" as const, + name: "Dev Experiments", + description: ` +Enables the dev experiments page. Access the page via the command menu once enabled. + `, + }, +]; + +type FeatureFlag = (typeof flagList)[number]; +type FlagsEnum = FeatureFlag["id"]; + +const user: Omit & { + custom: Record | undefined>; +} = { + custom: {}, +}; + +/** + * Feature Flags API. Note that when reading feature flags (the `check` method), + * the value could be stale. + */ +export const featureFlags = { + /** + * This is intentionally not implemented as a store + * because feature flags may be used during the app initialization phase, which + * requires a fresh start. After changing flags the app should be fully reloaded. + */ + check: (flag: FlagsEnum): boolean => { + try { + return statsig.checkGate(flag); + } catch (error) { + // Ignore errors related to statsig not yet being initialized + if (error.message.includes("wait for initialize()")) { + console.warn( + `Statsig not initialized but feature flag checked '${flag}'. Error follow:`, + error + ); + return Boolean(user.custom[flag] ?? false); + } + + throw error; + } + }, + + flagList, + + initialize: async (sdkKey: string, u: StatsigUser) => { + const custom = JSON.parse(localStorage.getItem(lsKey) || "{}"); + u.custom = custom; + Object.assign(user, u); + const res = await statsig.initialize(sdkKey, u); + return res; + }, + + updateUser: statsig.updateUser, + + setUser: (u: StatsigUser) => { + Object.assign(user, u); + }, + + setFlag: (flag: FlagsEnum, value: boolean) => { + if (!user.custom) { + user.custom = {}; + } + + user.custom[flag] = value; + localStorage.setItem(lsKey, JSON.stringify(user.custom)); + return statsig.updateUser(user); + }, + + getUser: () => { + return user; + }, + + _statsig: statsig, +}; diff --git a/src/lib/llm/openai.ts b/src/lib/llm/openai.ts index c2b49f4..b721fc8 100644 --- a/src/lib/llm/openai.ts +++ b/src/lib/llm/openai.ts @@ -24,7 +24,10 @@ const openAiFetchWrapper = (url: RequestInfo, options?: RequestInit) => { hs[k] = v; } - console.debug("OpenAI Fetch ::", (options?.method || "get").toUpperCase(), url, hs); + console.debug("OpenAI Fetch ::", (options?.method || "get").toUpperCase(), url, { + ...hs, + Authorization: hs.Authorization ? hs.Authorization?.slice(0, 14) + "..." : undefined, + }); return fetch(url, { ...options, diff --git a/src/lib/migrations/0003_schema_vecdb.sql b/src/lib/migrations/0003_schema_vecdb.sql new file mode 100644 index 0000000..af1d769 --- /dev/null +++ b/src/lib/migrations/0003_schema_vecdb.sql @@ -0,0 +1,183 @@ +-- +-- The unified schema. To modify the schema copy it to a new file, modify, +-- reference in db.ts. Keeping the old-yet-cumulative schemas around is for the +-- sync functionality. Schema name should not be duplicated. +-- + +CREATE TABLE + if NOT EXISTS "thread" ( + "id" VARCHAR(255) PRIMARY KEY NOT NULL, -- Key should be supplied by user. nanoid, uuid, etc + "title" VARCHAR(255), + "archived" BOOLEAN DEFAULT FALSE, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +SELECT crsql_as_crr ('thread'); + +CREATE TABLE + if NOT EXISTS "message" ( + "id" VARCHAR(255) PRIMARY KEY NOT NULL, + "role" VARCHAR(63) NOT NULL DEFAULT '', + "model" VARCHAR(255), + "cancelled" BOOLEAN DEFAULT FALSE, + "content" TEXT, + "thread_id" VARCHAR(255), + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +SELECT crsql_as_crr ('message'); + +CREATE INDEX if NOT EXISTS messagethreadidx ON "message" ("thread_id"); + +CREATE TABLE + if NOT EXISTS "preferences" ("key" VARCHAR(255) PRIMARY KEY, "value" TEXT); + +CREATE TABLE + if NOT EXISTS "deleted_record" ( + "id" VARCHAR(255) PRIMARY KEY NOT NULL, + "table_name" VARCHAR(255), + "deleted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "data" JSON NOT NULL DEFAULT '{}' + ); + +SELECT crsql_as_crr ('deleted_record'); + +-- Make sure to update the version! Otherwise this will be run again +CREATE TABLE + if NOT EXISTS "fragment" ( + "id" INTEGER PRIMARY KEY NOT NULL, -- auto increment doesn't work well for synced tables. also, must be int for use in fts rowid + "entity_id" VARCHAR(255), -- references some other thing in the db. for now, either a thread or a message + "entity_type" VARCHAR(255), -- what table this belongs to. not quite using sql the way it was intended here + "attribute" VARCHAR(255), -- the name of the attribute that this fragment is for + "value" TEXT, -- the value of the attribute that this fragment is for + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +CREATE VIRTUAL TABLE if NOT EXISTS "fragment_fts" USING fts5 ( + "entity_id" UNINDEXED, + "entity_type" UNINDEXED, + "attribute", + "value", + "created_at" UNINDEXED, + content = "fragment", + content_rowid = "id", + tokenize = "trigram" +); + +CREATE TRIGGER if NOT EXISTS "fragment_ai" AFTER INSERT ON "fragment" BEGIN +INSERT INTO + "fragment_fts" ( + "rowid", + "entity_id", + "entity_type", + "attribute", + "value", + "created_at" + ) +VALUES + ( + NEW."id", + NEW."entity_id", + NEW."entity_type", + NEW."attribute", + NEW."value", + NEW."created_at" + ); + +END; + +CREATE TRIGGER if NOT EXISTS "fragment_ad" AFTER DELETE ON "fragment" BEGIN +INSERT INTO + "fragment_fts" ( + "fragment_fts", + "rowid", + "entity_id", + "entity_type", + "attribute", + "value", + "created_at" + ) +VALUES + ( + 'delete', + OLD."id", + OLD."entity_id", + OLD."entity_type", + OLD."attribute", + OLD."value", + OLD."created_at" + ); + +END; + +CREATE TRIGGER if NOT EXISTS "fragment_au" AFTER +UPDATE ON "fragment" BEGIN +INSERT INTO + "fragment_fts" ( + "fragment_fts", + "rowid", + "entity_id", + "entity_type", + "attribute", + "value", + "created_at" + ) +VALUES + ( + 'delete', + OLD."id", + OLD."entity_id", + OLD."entity_type", + OLD."attribute", + OLD."value", + OLD."created_at" + ); + +INSERT INTO + "fragment_fts" ( + "rowid", + "entity_id", + "entity_type", + "attribute", + "value", + "created_at" + ) +VALUES + ( + NEW."id", + NEW."entity_id", + NEW."entity_type", + NEW."attribute", + NEW."value", + NEW."created_at" + ); + +END; + +CREATE TABLE + if NOT EXISTS "llm_provider" ( + "id" VARCHAR(255) PRIMARY KEY NOT NULL, + "name" VARCHAR(255), + "base_url" VARCHAR(2000), + "api_key" TEXT, + "enabled" BOOLEAN DEFAULT TRUE, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +SELECT crsql_as_crr ('llm_provider'); + +-- Map vecs to frags. For now i'm using victor for vec store and it doesn't pass back +-- metadata. So we need to store the vec id and the frag id together. +CREATE TABLE + if NOT EXISTS "vec_to_frag" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "vec_id" VARCHAR(255) NOT NULL, + "frag_id" INTEGER NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +CREATE INDEX + if NOT EXISTS "idx_vec_id" ON "vec_to_frag" ("vec_id"); + +CREATE INDEX + if NOT EXISTS "idx_frag_id" ON "vec_to_frag" ("frag_id"); diff --git a/src/lib/stores/stores/index.ts b/src/lib/stores/stores/index.ts index 4e13200..526e640 100644 --- a/src/lib/stores/stores/index.ts +++ b/src/lib/stores/stores/index.ts @@ -24,6 +24,7 @@ import { createSyncer, getDefaultEndpoint, type Syncer } from "$lib/sync/vlcn"; import { PENDING_THREAD_TITLE, hasThreadTitle, persistentStore } from "../storeUtils"; import { chatModels, llmProviders, openAiConfig } from "./llmProvider"; import { activeProfileName, getOpenAi, gptProfileStore } from "./llmProfile"; +import { featureFlags } from "$lib/featureFlags"; export const showSettings = writable(false); export const showInitScreen = writable(false); @@ -50,7 +51,7 @@ export const generateThreadTitle = async ({ threadId }: { threadId: string }) => return; } - const prompt: OpenAI.Chat.CompletionCreateParamsNonStreaming = { + const prompt: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: modelName, temperature: 0.2, // Playing around with this value the lower value seems to be more accurate? messages: [ @@ -62,7 +63,7 @@ Do not include any of the chat instructions or prompts in the summary. Do not prefix with "title" or "example" etc Do not provide a word count or add quotation marks. `.trim(), - role: "user", + role: "user" as const, }, ], }; @@ -95,7 +96,6 @@ Do not provide a word count or add quotation marks. }; const NEWTHREAD = "newthread"; -export const sqlite = writable(null); export const db = writable(null); export const newThread: Thread = { id: NEWTHREAD, @@ -432,7 +432,7 @@ export const currentChatThread = (() => { messageContext = [ { content: systemMessage, - role: "system", + role: "system" as const, }, ...messageContext, ]; diff --git a/src/lib/stores/stores/vecDbStore.ts b/src/lib/stores/stores/vecDbStore.ts new file mode 100644 index 0000000..ce3f457 --- /dev/null +++ b/src/lib/stores/stores/vecDbStore.ts @@ -0,0 +1,61 @@ +import type { VecDB } from "$lib/vecDb"; +import { writable } from "svelte/store"; + +export const vecDbStore = (() => { + const store = writable({ + loading: false, + progress: 0, + total: 0, + error: null, + vecDb: null, + }); + + let vecDb: VecDB | null = null; + + const handleIngestStart = (e: CustomEvent<{ remaining: number }>) => { + store.update((x) => ({ ...x, loading: true, progress: 0, total: e.detail.remaining })); + }; + + const handleProgress = (e: CustomEvent<{ remaining: number }>) => { + store.update((x) => ({ ...x, progress: x.total - e.detail.remaining })); + }; + + const handleIngestEnd = () => { + store.update((x) => ({ ...x, loading: false, progress: 0, total: 0 })); + }; + + return { + subscribe: store.subscribe, + + ingest() { + if (vecDb) { + vecDb.ingestFragments(); + } + }, + + init(x: VecDB) { + if (vecDb) { + console.warn("VecDB already initialized"); + return; + } + + // @ts-ignore + x.events.addEventListener("ingest-start", handleIngestStart); + // @ts-ignore + x.events.addEventListener("ingest-progress", handleProgress); + x.events.addEventListener("ingest-end", handleIngestEnd); + + vecDb = x; + }, + + dispose() { + if (vecDb) { + // @ts-ignore + vecDb.events.removeEventListener("ingest-start", handleIngestStart); + // @ts-ignore + vecDb.events.removeEventListener("ingest-progress", handleProgress); + vecDb.events.removeEventListener("ingest-end", handleIngestEnd); + } + }, + }; +})(); diff --git a/src/lib/toast/index.ts b/src/lib/toast/index.ts index bc4d293..5aa11c8 100644 --- a/src/lib/toast/index.ts +++ b/src/lib/toast/index.ts @@ -1,4 +1,5 @@ import { writable } from "svelte/store"; +import { toast as _toast } from "svelte-sonner"; export interface ToastProps { type: "info" | "success" | "error"; @@ -7,62 +8,12 @@ export interface ToastProps { autocloseTimeout?: number; } -class Toast { - static of = (props: ToastProps) => { - return new Toast(props, toastStore); - }; - - props: ToastProps; - timeout: any; - startedAt: number; - store: typeof toastStore; - autocloseTimeout: number; - - constructor(props: ToastProps, store: typeof toastStore) { - this.store = store; - this.props = props; - this.autocloseTimeout = props.autocloseTimeout || 5000; - this.timeout = setTimeout(() => { - this.remove(); - }, this.autocloseTimeout); - this.startedAt = Date.now(); - } - - pause() { - if (this.timeout) clearTimeout(this.timeout); - } - - resume() { - this.timeout = setTimeout(() => { - this.remove(); - }, this.autocloseTimeout - (Date.now() - this.startedAt)); - } - - remove() { - if (this.timeout) clearTimeout(this.timeout); - this.store.update((store) => { - const newToasts = store.toasts.filter((toast) => toast !== this); - return { - ...store, - toasts: newToasts, - }; - }); +export const toast = (props: ToastProps) => { + if (props.autocloseTimeout) { + console.warn("autocloseTimeout is not supported"); } -} -export const toastStore = writable({ - toasts: [] as Toast[], -}); - -const timeouts: Record = {}; - -export const toast = (props: ToastProps) => { - toastStore.update((store) => { - const newToast = Toast.of(props); - const newToasts = [...store.toasts, newToast]; - return { - ...store, - toasts: newToasts, - }; + _toast[props.type](props.title, { + description: props.message, }); }; diff --git a/src/lib/vecDb.ts b/src/lib/vecDb.ts new file mode 100644 index 0000000..0580c27 --- /dev/null +++ b/src/lib/vecDb.ts @@ -0,0 +1,288 @@ +import type { FragmentRow, SemanticFragmentRow } from "$lib/db"; +import type { DBAsync } from "@vlcn.io/xplat-api"; +import { Db } from "victor-db"; + +interface VictorDBRecord { + similarity: number; + embedding: VictorEmbedding; + content: string; +} + +interface VictorEmbedding { + id: string; + vector: VictorVector; +} + +interface VictorVector { + data: number[]; + min: number; + max: number; +} + +interface Opts { + embedString: (str: string) => Promise; + db: DBAsync; +} + +export class VecDB { + victor: Db | null = null; + db: DBAsync; + events = new EventTarget(); + + static async create(opts: Opts) { + const db = new VecDB(opts); + await db.init(); + return db; + } + + /** + * Given a string, return a vector embedding of it. The VecDB class is + * agnostic to how this is done. + */ + embedString: (str: string) => Promise; + + constructor(opts: Opts) { + this.embedString = opts.embedString; + this.db = opts.db; + } + + async init() { + // NOTE: This IS NECESSARY! The constructor returns a promise. Probably + // something to do with being a Rust lib rather than a JS lib. + this.victor = await new Db(); + } + + isReady(): this is { victor: Db } { + return this.victor !== null; + } + + async searchEmbedding( + embedding: Float64Array, + { tags = [], limit = 10 }: { tags?: string[]; limit?: number } = {} + ) { + if (!this.isReady()) { + throw new Error("DB not ready"); + } + + console.time("search"); + const result: VictorDBRecord[] = await this.victor.search(embedding, tags, limit); + console.timeEnd("search"); + + const vecIds = result.map((x) => x.embedding.id); + const xs = await this.db.execO( + ` + SELECT + f.*, + vf.vec_id + FROM + vec_to_frag vf + INNER JOIN + fragment f ON vf.frag_id = f.id + WHERE + vf.vec_id IN (${vecIds.map((_) => "?").join(", ")}) + `, + vecIds + ); + + return Promise.all( + result.map(async (x) => { + const fragment = xs.find((y) => y.vec_id === x.embedding.id); + return { + fragment, + similarity: x.similarity, + content: x.content, + embedding: x.embedding, + }; + }) + ); + } + + search = async ( + q: string, + { tags = [], limit = 10 }: { tags?: string[]; limit?: number } = {} + ) => { + if (!this.isReady()) { + throw new Error("DB not ready"); + } + + console.time("embedString"); + const embedding = await this.embedString(q); + console.timeEnd("embedString"); + + return this.searchEmbedding(embedding, { tags, limit }); + }; + + /** + * Upsert a record making sure the content is unique to the vector store for this set of tags. + */ + upsert = async (record: { content: string; tags?: string[] }) => { + if (!this.isReady()) { + throw new Error("Could not connect to database"); + } + + let embedding: Float64Array | undefined; + let error: Error | undefined; + console.time("embedString"); + for (let i = 0; i < 3; i++) { + try { + embedding = await this.embedString(record.content); + break; + } catch (e) { + console.warn("Error embedding string. Retrying..."); + error = e; + } + } + console.timeEnd("embedString"); + + if (!embedding) { + throw new Error("Could not embed string: " + error?.message || "Unknown error"); + } + + const result = await this.searchEmbedding(embedding, { + tags: record.tags, + limit: 1, + }); + + if (result[0]?.content === record.content) { + console.log("Already exists", record); + return { + record: result[0].embedding, + inserted: false, + }; + } + + console.time("insert"); + await this.victor.insert(record.content, embedding, record.tags); + console.timeEnd("insert"); + + // Return the new record. Victor does not return it on insert... + const xs = await this.searchEmbedding(embedding, { + tags: record.tags, + limit: 1, + }); + + const newRecord = xs[0].embedding; + + return { + record: newRecord, + inserted: true, + }; + }; + + /** + * This is a very destructive operation, but the vectors are all derived data + * so it should be fine. It just takes a bit of time to derive them. + */ + clear = async () => { + if (!this.isReady()) { + throw new Error("Could not connect to database"); + } + + await this.victor.clear(); + await this.db.exec(`DELETE FROM vec_to_frag`); + }; + + #ingestInProgress = false; + + ingestFragments = async () => { + if (!this.isReady()) { + throw new Error("Could not connect to database"); + } + + if (this.#ingestInProgress) { + console.warn("Ingest already in progress. Ignoring ingest request"); + return; + } + + this.#ingestInProgress = true; + + // How many to do at once? + const batchSize = 100; + + const getCount = async () => { + let [{ count }] = await this.db.execO<{ count: number }>(` + SELECT + COUNT(*) as 'count' + FROM + fragment f + INNER JOIN message m ON m.id = f.entity_id + WHERE + f.id NOT IN (SELECT vf.frag_id FROM vec_to_frag vf); + `); + return count; + }; + + try { + let remaining = await getCount(); + + this.events.dispatchEvent(new CustomEvent("ingest-start", { detail: { remaining } })); + + while (remaining > 0) { + console.log("records to process:", remaining); + const fragments = await this.db.execO( + ` + SELECT + m.role, + m.thread_id, + m.created_at, + m.id as 'message_id', + f.id, + f.value, + f.entity_type + FROM + fragment f + INNER JOIN message m ON m.id = f.entity_id + WHERE + f.id NOT IN (SELECT vf.frag_id FROM vec_to_frag vf) + ORDER BY + m.created_at ASC + LIMIT + ?; + `, + [batchSize] + ); + + const records = fragments.map((x) => { + return { + content: x.value, + tags: [x.role, x.thread_id, x.entity_type], + id: x.id, + }; + }); + + // @todo Maybe it would make more sense to do this in the upsert call + // Victor does NOT like parallel upserts it seems. Last time I tried, it crashed. + console.time("upsert fragments"); + for (const [i, { id, ...record }] of records.entries()) { + const result = await this.upsert(record); + console.debug("inserted", result); + const [existing] = await this.db.execO<{ id: string }>( + `SELECT id FROM "vec_to_frag" WHERE "frag_id" = ?`, + [id] + ); + if (!existing) { + await this.db.exec(`INSERT INTO "vec_to_frag" ("vec_id", "frag_id") VALUES(?, ?)`, [ + result.record.id, + id, + ]); + } + + if (i % 3 === 0) { + this.events.dispatchEvent( + new CustomEvent("ingest-progress", { detail: { remaining: remaining - i } }) + ); + } + } + console.timeEnd("upsert fragments"); + + remaining = await getCount(); + } + } catch (error) { + console.error("Error ingesting fragments", error); + throw error; + } finally { + this.#ingestInProgress = false; + this.events.dispatchEvent(new CustomEvent("ingest-end")); + } + }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4e6995a..624263b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,7 @@ import { onDestroy, onMount } from "svelte"; import { DatabaseMeta, + getCurrentSchema, getLatestDbName, importFromDatabase, incrementDbName, @@ -13,12 +14,18 @@ import { getSystem } from "$lib/gui"; import classNames from "classnames"; import { dev } from "$app/environment"; - import Toaster from "$lib/toast/Toaster.svelte"; + import { Toaster } from "$lib/components/ui/sonner"; import { assets } from "$app/paths"; import FullScreenError from "$lib/components/FullScreenError.svelte"; import { debounce, wrapError } from "$lib/utils"; import DevTooling from "$lib/components/DevTooling.svelte"; import { openAiConfig } from "$lib/stores/stores/llmProvider"; + import { env } from "$env/dynamic/public"; + import { featureFlags } from "$lib/featureFlags"; + import ActionsMenu from "$lib/components/ActionsMenu.svelte"; + import { ChevronLeftCircle } from "lucide-svelte"; + import { page } from "$app/stores"; + import { fly, slide } from "svelte/transition"; const sys = getSystem(); let startupError: Error | null = null; @@ -44,9 +51,24 @@ location.reload(); }; + /** + * NOTE: Teardown is not currently used because the layout only gets torn down when the tab closes + */ let teardown: any; const handleStartup = async () => { + // Initialize feature flags. Feature flags may be used during app bootstrap, + // which is why they are initialized first. + if (env.PUBLIC_STATSIG_CLIENT_SDK_KEY) { + try { + await featureFlags.initialize(env.PUBLIC_STATSIG_CLIENT_SDK_KEY, { + appVersion: env.PUBLIC_VERSION_STRING, + }); + } catch (error) { + console.error("featureFlags error", error); + } + } + // throw up after a time if the app is hanging let _timeout = setTimeout(() => { throw new Error("Timed out trying to initialize"); @@ -100,10 +122,6 @@ } }); - onDestroy(async () => { - await teardown?.(); - }); - function isExternalUrl(href: any) { if (typeof href !== "string") return false; @@ -190,9 +208,9 @@ {#if !telemetryDisabled} {/if} @@ -209,10 +227,13 @@ level={isPendingImport ? "info" : "error"} >
-
-

- Database Path: {getLatestDbName()} -

+
+

Database Path: {getLatestDbName()}

+ {#await getCurrentSchema() then schema} +

Schema Name: {schema.name}

+ {:catch} +

Schema: unknown

+ {/await}
{#if isPendingImport}

@@ -277,8 +298,25 @@ {/if}

- + + {#if appReady} + +
+ {#if $page.url.pathname !== "/"} + + Back to chat + + {/if} + +
{/if} + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3510211..eadb837 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -11,6 +11,7 @@ pendingMessageStore, isNewThread, } from "../lib/stores/stores"; + import { ArrowUpCircle, XCircle } from "lucide-svelte"; import ThreadMenuList from "$lib/components/ThreadMenuList.svelte"; import SmallSpinner from "$lib/components/SmallSpinner.svelte"; import ThreadMenuButton from "$lib/components/ThreadMenuButton.svelte"; @@ -113,7 +114,11 @@ if (!s) { console.debug("No string. Not sending."); - toast({ type: "error", title: "No message" }); + toast({ + type: "error", + title: "Message is empty", + message: "Please enter a message to send.", + }); return; } @@ -241,7 +246,7 @@
@@ -250,7 +255,7 @@ e.preventDefault(); handleSubmit($messageText); }} - class={classNames("flex items-end rounded-lg border border-zinc-700", { + class={classNames("flex flex-1 items-end rounded-lg border border-zinc-700", { "shadow-[0_0_0_2px_#5baba4] bg-teal-800/20 text-teal-200": isCommand, "bg-zinc-800": !isCommand, })} @@ -284,19 +289,24 @@ {:else} - {/if} + +
@@ -324,10 +334,12 @@
diff --git a/src/routes/dev/experimental_ToastyPage.svelte b/src/routes/dev/experimental_ToastyPage.svelte new file mode 100644 index 0000000..79bb3e0 --- /dev/null +++ b/src/routes/dev/experimental_ToastyPage.svelte @@ -0,0 +1,20 @@ + + +
+
+

Test your toasts.

+
+ +
diff --git a/src/routes/dev/feature-flags/+page.svelte b/src/routes/dev/feature-flags/+page.svelte new file mode 100644 index 0000000..b35424f --- /dev/null +++ b/src/routes/dev/feature-flags/+page.svelte @@ -0,0 +1,75 @@ + + +
+
+

+ + Feature Flags +

+

+ Enable or disable these as you like. They are experimental and might cause breakage, but they + are also pretty sweet. +

+ {#if dev} +

+ Add more flags in Statsig +

+ {/if} +
+
+ + {#each featureFlags.flagList as flag} +
+
+ { + featureFlags.setFlag(flag.id, !featureFlags.check(flag.id)); + dirty = true; + }} + class="mr-2" + /> + +
+
+ + id: + {flag.id} + + + {featureFlags.check(flag.id) ? "Enabled" : "Disabled"} + +
+
+ {#if flag.description} +
{flag.description}
+ {/if} + {/each} + + {#if dirty} +
+ +
+ {/if} +
diff --git a/tests/providers.spec.ts b/tests/providers.spec.ts index 60ec621..435d7ff 100644 --- a/tests/providers.spec.ts +++ b/tests/providers.spec.ts @@ -3,7 +3,7 @@ import { test, expect, type Page } from "@playwright/test"; const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; const openSettings = async (page: Page) => { - await page.locator("text=Command").click(); + await page.getByTestId("CommandMenuButton").click(); await page.getByPlaceholder("Search for actions").fill("settings"); await page.keyboard.press("Enter"); await expect(page.getByText("Settings", { exact: true })).toBeVisible(); diff --git a/vite.config.ts b/vite.config.ts index 44fa69a..e2b4770 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,10 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vitest/config"; +import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ - plugins: [sveltekit()], + plugins: [sveltekit(), wasm(), topLevelAwait()], build: { // Disable asset inlining. This is currently required for our migration // system (yikes!). Client-side migrations are sort of odd, because we