diff --git a/package-lock.json b/package-lock.json index 2dbd251f..60d4f18f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,13 +32,14 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.4", - "@radix-ui/react-scroll-area": "^1.2.2", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.1", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-tooltip": "^1.1.6", + "@radix-ui/react-tooltip": "^1.2.7", "@tabler/icons-react": "^2.14.0", "async-mutex": "^0.5.0", "axios": "^1.3.4", @@ -65,6 +66,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.3.5", "react-markdown": "^9.0.1", + "react-resizable-panels": "^3.0.2", "react-syntax-highlighter": "^15.5.0", "sse": "github:mpetazzoni/sse.js", "tailwind-merge": "^2.5.5", @@ -5175,6 +5177,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", @@ -6254,6 +6274,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", @@ -6374,20 +6412,20 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz", - "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6404,16 +6442,22 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -6425,13 +6469,44 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6448,14 +6523,49 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6509,6 +6619,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", @@ -6532,6 +6660,52 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz", @@ -6566,12 +6740,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -6583,6 +6757,21 @@ } } }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-switch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz", @@ -6762,23 +6951,23 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", - "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.1", - "@radix-ui/react-portal": "1.1.3", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -6796,18 +6985,18 @@ } }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", - "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -6825,9 +7014,24 @@ } }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -6840,16 +7044,16 @@ } }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", - "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6866,22 +7070,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", - "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6899,13 +7121,37 @@ } }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", - "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6923,12 +7169,12 @@ } }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -6945,13 +7191,29 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -6963,6 +7225,81 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -6994,6 +7331,39 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", @@ -7075,12 +7445,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", - "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -7097,28 +7467,13 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -7135,24 +7490,6 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", @@ -18688,6 +19025,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz", + "integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", diff --git a/package.json b/package.json index 1f6ec86e..e7edf344 100644 --- a/package.json +++ b/package.json @@ -99,13 +99,14 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.4", - "@radix-ui/react-scroll-area": "^1.2.2", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.1", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-tooltip": "^1.1.6", + "@radix-ui/react-tooltip": "^1.2.7", "@tabler/icons-react": "^2.14.0", "async-mutex": "^0.5.0", "axios": "^1.3.4", @@ -132,6 +133,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.3.5", "react-markdown": "^9.0.1", + "react-resizable-panels": "^3.0.2", "react-syntax-highlighter": "^15.5.0", "sse": "github:mpetazzoni/sse.js", "tailwind-merge": "^2.5.5", diff --git a/src/components/chat-components/ProjectList.tsx b/src/components/chat-components/ProjectList.tsx index 50f89d3a..8f4d982f 100644 --- a/src/components/chat-components/ProjectList.tsx +++ b/src/components/chat-components/ProjectList.tsx @@ -1,5 +1,5 @@ import { ProjectConfig, setCurrentProject } from "@/aiParams"; -import { AddProjectModal } from "@/components/modals/AddProjectModal"; +import { AddProjectModal } from "@/components/modals/project/AddProjectModal"; import { ConfirmModal } from "@/components/modals/ConfirmModal"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -11,6 +11,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { SearchBar } from "@/components/ui/SearchBar"; import { cn } from "@/lib/utils"; import { logError } from "@/logger"; import { updateSetting } from "@/settings/model"; @@ -25,12 +26,10 @@ import { Search, Trash2, X, - XCircle, } from "lucide-react"; import { App, Notice } from "obsidian"; import React, { memo, useEffect, useMemo, useState } from "react"; import { filterProjects } from "@/utils/projectUtils"; -import { Input } from "../ui/input"; function ProjectItem({ project, @@ -356,23 +355,11 @@ export const ProjectList = memo( {projects.length > 0 && (
- setSearchQuery(e.target.value)} + onChange={setSearchQuery} + placeholder="Search projects..." /> - {searchQuery && ( - - )} -
)} diff --git a/src/components/modals/ExtensionInputModal.tsx b/src/components/modals/ExtensionInputModal.tsx index c82a2b00..316dbcf9 100644 --- a/src/components/modals/ExtensionInputModal.tsx +++ b/src/components/modals/ExtensionInputModal.tsx @@ -25,6 +25,8 @@ function ExtensionInputModalContent({ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); validateAndConfirm(extension); } }; diff --git a/src/components/modals/PatternMatchingModal.tsx b/src/components/modals/PatternMatchingModal.tsx index ce3d208b..782fd917 100644 --- a/src/components/modals/PatternMatchingModal.tsx +++ b/src/components/modals/PatternMatchingModal.tsx @@ -4,8 +4,11 @@ import React, { useState } from "react"; import { createRoot, Root } from "react-dom/client"; import { categorizePatterns, - getDecodedPatterns, createPatternSettingsValue, + getDecodedPatterns, + getExtensionPattern, + getFilePattern, + getTagPattern, } from "@/search/searchUtils"; import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react"; import { @@ -148,7 +151,7 @@ function PatternMatchingModalContent({ { new TagSearchModal(app, (tag) => { - const tagPattern = `#${tag}`; + const tagPattern = getTagPattern(tag); if (tagPatterns.includes(tagPattern)) { return; } @@ -185,7 +188,7 @@ function PatternMatchingModalContent({ new AddContextNoteModal({ app, onNoteSelect: (note) => { - const notePattern = `[[${note.basename}]]`; + const notePattern = getFilePattern(note); if (notePatterns.includes(notePattern)) { return; } @@ -206,7 +209,7 @@ function PatternMatchingModalContent({ { new ExtensionInputModal(app, (extension) => { - const extensionPattern = `*.${extension}`; + const extensionPattern = getExtensionPattern(extension); if (extensionPatterns.includes(extensionPattern)) { return; } diff --git a/src/components/modals/ProjectPatternMatchingModal.tsx b/src/components/modals/ProjectPatternMatchingModal.tsx index e525fd08..288c43b2 100644 --- a/src/components/modals/ProjectPatternMatchingModal.tsx +++ b/src/components/modals/ProjectPatternMatchingModal.tsx @@ -10,6 +10,9 @@ import { categorizePatterns, createPatternSettingsValue, getDecodedPatterns, + getExtensionPattern, + getFilePattern, + getTagPattern, } from "@/search/searchUtils"; import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react"; import { App, Modal, TFile } from "obsidian"; @@ -148,7 +151,7 @@ function ProjectPatternMatchingModalContent({ { new TagSearchModal(app, (tag) => { - const tagPattern = `#${tag}`; + const tagPattern = getTagPattern(tag); if (tagPatterns.includes(tagPattern)) { return; } @@ -185,7 +188,7 @@ function ProjectPatternMatchingModalContent({ new ProjectFileSelectModal({ app, onFileSelect: (file: TFile) => { - const filePattern = `[[${file.basename}]]`; + const filePattern = getFilePattern(file); if (notePatterns.includes(filePattern)) { return; } @@ -206,7 +209,7 @@ function ProjectPatternMatchingModalContent({ { new ExtensionInputModal(app, (extension) => { - const extensionPattern = `*.${extension}`; + const extensionPattern = getExtensionPattern(extension); if (extensionPatterns.includes(extensionPattern)) { return; } diff --git a/src/components/modals/AddProjectModal.tsx b/src/components/modals/project/AddProjectModal.tsx similarity index 86% rename from src/components/modals/AddProjectModal.tsx rename to src/components/modals/project/AddProjectModal.tsx index c1cb355e..898ed111 100644 --- a/src/components/modals/AddProjectModal.tsx +++ b/src/components/modals/project/AddProjectModal.tsx @@ -1,5 +1,4 @@ import { ProjectConfig } from "@/aiParams"; -import { ProjectPatternMatchingModal } from "@/components/modals/ProjectPatternMatchingModal"; import { Button } from "@/components/ui/button"; import { FormField } from "@/components/ui/form-field"; import { Input } from "@/components/ui/input"; @@ -14,6 +13,9 @@ import { HelpCircle } from "lucide-react"; import { App, Modal, Notice } from "obsidian"; import React, { useState } from "react"; import { createRoot, Root } from "react-dom/client"; +import { ContextManageModal } from "@/components/modals/project/context-manage-modal"; +import { TruncatedText } from "@/components/TruncatedText"; +import { getDecodedPatterns } from "@/search/searchUtils"; interface AddProjectModalContentProps { initialProject?: ProjectConfig; @@ -31,7 +33,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject inclusions: false, }); - const [formData, setFormData] = useState>( + const [formData, setFormData] = useState( initialProject || { id: randomUUID(), name: "", @@ -53,6 +55,23 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject } ); + const showContext = getDecodedPatterns( + formData.contextSource.inclusions || formData.contextSource.exclusions || "nothing" + ) + .reverse() + .join(","); + + const handleEditProjectContext = (originP: ProjectConfig) => { + const modal = new ContextManageModal( + app, + async (updatedProject: ProjectConfig) => { + setFormData(updatedProject); + }, + originP + ); + modal.open(); + }; + const isFormValid = () => { return formData.name && formData.projectModelKey; }; @@ -130,7 +149,9 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject return (
-
Add New Project
+
+ {initialProject ? "Edit Project" : "New Project"} +
- Inclusions + Project Context @@ -261,53 +282,19 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject description="Define patterns to include specific files or folders in the project context" >
-
- {formData.contextSource?.inclusions?.trim() - ? "Patterns configured" - : "No patterns configured"} +
+ Patterns configure: + + {showContext} +
-
- - - -
-
- {formData.contextSource?.exclusions?.trim() - ? "Patterns configured" - : "No patterns configured"} -
-
diff --git a/src/components/modals/project/context-manage-modal.tsx b/src/components/modals/project/context-manage-modal.tsx new file mode 100644 index 00000000..c51507ff --- /dev/null +++ b/src/components/modals/project/context-manage-modal.tsx @@ -0,0 +1,1059 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + FileAudio, + FileImage, + FileText, + FileVideo, + FolderIcon, + Plus, + PlusCircle, + TagIcon, + XIcon, +} from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { App, Modal, Notice, TFile } from "obsidian"; +import { createRoot, Root } from "react-dom/client"; +import { ProjectConfig } from "@/aiParams"; +import { Button } from "@/components/ui/button"; +import { SearchBar } from "@/components/ui/SearchBar"; +import { TagSearchModal } from "@/components/modals/TagSearchModal"; +import { FolderSearchModal } from "@/components/modals/FolderSearchModal"; +import { ProjectFileSelectModal } from "@/components/modals/ProjectFileSelectModal"; +import { + createPatternSettingsValue, + getFilePattern, + getMatchingPatterns, + getTagPattern, + PatternCategory, + shouldIndexFile, +} from "@/search/searchUtils"; +import { getTagsFromNote } from "@/utils"; +import { TruncatedText } from "@/components/TruncatedText"; + +function FileIcon({ extension, size = "tw-size-4" }: { extension: string; size?: string }) { + const ext = extension.toLowerCase().replace("*.", ""); + if (["jpg", "jpeg", "png", "gif", "svg"].includes(ext)) + return ; + if (["mp3", "wav", "ogg"].includes(ext)) + return ; + if (["mp4", "mov", "avi"].includes(ext)) + return ; + if (["md", "txt", "mdx", "markdown"].includes(ext)) return ; + + return ; +} + +interface ParsedQuery { + tags: string[]; + titles: string[]; + extensions: string[]; +} + +type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles" | "search" | null; +type ActiveItem = string | null; + +interface SectionHeaderProps { + IconComponent: React.ComponentType; + title: string; + iconColorClassName: string; + onAddClick: () => void; +} + +const SectionHeader: React.FC = ({ + IconComponent, + title, + iconColorClassName, + onAddClick, +}) => { + return ( +
+
+ +

{title}

+
+ +
+ ); +}; + +interface SectionItem { + id: string; + name: string; + count?: number; +} + +interface SectionListProps { + title: string; + IconComponent: React.ComponentType; + iconColorClassName: string; + items: SectionItem[]; + activeItem: string | null; + activeSection: string | null; + sectionType: string; + renderIcon?: (item: SectionItem) => React.ReactNode; + onItemClick: (itemId: string, itemName?: string) => void; + onAddClick: () => void; + onDeleteItem: (e: React.MouseEvent, item: SectionItem) => void; +} + +const SectionList: React.FC = ({ + title, + IconComponent, + iconColorClassName, + items, + activeItem, + activeSection, + sectionType, + renderIcon, + onItemClick, + onAddClick, + onDeleteItem, +}) => { + return ( +
+ +
+ {items.map((item) => ( +
onItemClick(item.id, item.name)} + > +
+ {renderIcon ? ( + renderIcon(item) + ) : ( + + )} + {item.name} +
+
+ + {item.count || 0} + + onDeleteItem(e, item)} + /> +
+
+ ))} +
+
+ ); +}; + +interface ItemCardProps { + item: GroupItem; + viewMode: "list"; + onDelete: (e: React.MouseEvent, item: GroupItem) => void; +} + +function ItemCard({ item, viewMode, onDelete }: ItemCardProps) { + const extension = item.id.split(".").pop() || ""; + + // add or remove + const IconComponent = item.isIgnored ? Plus : XIcon; + + return ( +
+
+ +
+
+ + {item.isIgnored && (Ignored files)} + {item.name} + + {item.id && ( + {item.id} + )} +
+ +
+ onDelete(e, item)} + /> +
+
+ ); +} + +interface ContextManageProps { + initialProject: ProjectConfig; + onSave: (project: ProjectConfig) => void; + onCancel: () => void; + app: App; +} + +interface GroupItem { + id: string; + name: string; + isIgnored?: boolean; +} + +interface GroupListItem { + tags: Record>; + folders: Record>; + extensions: Record>; + notes: Array; +} + +interface IgnoreItems { + files: Set; +} + +function ContextManage({ initialProject, onSave, onCancel, app }: ContextManageProps) { + const { inclusions: inclusionPatterns, exclusions: exclusionPatterns } = useMemo(() => { + return getMatchingPatterns({ + inclusions: initialProject?.contextSource.inclusions, + exclusions: initialProject?.contextSource.exclusions, + isProject: true, + }); + }, [initialProject.contextSource]); + + const appAllFiles = useMemo(() => { + return app.vault.getFiles(); + }, [app.vault]); + + // init groupList + const createAndPopulateGroupList = useCallback( + ( + appFiles: TFile[], + inclusionPatterns: PatternCategory | null, + exclusionPatterns: PatternCategory | null + ): GroupListItem => { + const projectAllFiles = appFiles.filter((file) => + shouldIndexFile(file, inclusionPatterns, exclusionPatterns) + ); + + const processPatternGroup = ( + file: TFile, + patterns: string[] | undefined, + patternType: "tagPatterns" | "folderPatterns" | "extensionPatterns", + targetGroup: Record> + ) => { + if (patterns) { + patterns.forEach((pattern) => { + const singlePatternConfig = { [patternType]: [pattern] }; + if ( + shouldIndexFile(file, singlePatternConfig, null) && + !targetGroup[pattern].some((item) => item.id === file.path) + ) { + targetGroup[pattern].push({ + id: file.path, + name: file.basename, + }); + } + }); + } + }; + + // initialize groups + const tags: Record> = {}; + const folders: Record> = {}; + const extensions: Record> = {}; + const notes: Array = []; + + (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => { + tags[tag] = []; + }); + (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => { + folders[folder] = []; + }); + (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => { + extensions[extension] = []; + }); + + // Traverse the files and populate them into corresponding groups + projectAllFiles.forEach((file) => { + // tag + processPatternGroup(file, inclusionPatterns?.tagPatterns, "tagPatterns", tags); + + // folder + processPatternGroup(file, inclusionPatterns?.folderPatterns, "folderPatterns", folders); + + // extension + processPatternGroup( + file, + inclusionPatterns?.extensionPatterns, + "extensionPatterns", + extensions + ); + + // note/file + if ( + inclusionPatterns?.notePatterns && + shouldIndexFile(file, { notePatterns: inclusionPatterns.notePatterns }, null) && + !notes.some((item) => item.id === file.path) + ) { + notes.push({ + id: file.path, + name: file.basename, + }); + } + }); + + return { + tags, + folders, + extensions, + notes, + }; + }, + [] + ); + + const [groupList, setGroupList] = useState(() => { + // init include files + return createAndPopulateGroupList(appAllFiles, inclusionPatterns, exclusionPatterns); + }); + const [ignoreItems, setIgnoreItems] = useState(() => { + // init exclude files + const excludeFiles = appAllFiles.filter( + (file) => exclusionPatterns && shouldIndexFile(file, exclusionPatterns, null) + ); + return { + files: new Set(excludeFiles), + }; + }); + + const latestGroupList = useRef(groupList); + const latestIgnoreItems = useRef(ignoreItems); + + const [searchTerm, setSearchTerm] = useState(""); + const [activeSection, setActiveSection] = useState(null); + const [activeItem, setActiveItem] = useState(null); + + // groupList convert to inclusions format + const convertGroupListToInclusions = useCallback( + (list: GroupListItem, appFiles: TFile[]): string => { + const tagPatterns = Object.keys(list.tags); + const folderPatterns = Object.keys(list.folders); + const extensionPatterns = Object.keys(list.extensions); + const notePatterns = list.notes + .map((note) => { + const file = appFiles.find((file) => file.path === note.id); + if (file) { + return getFilePattern(file); + } + }) + .filter(Boolean) as string[]; + + return createPatternSettingsValue({ + tagPatterns, + folderPatterns, + extensionPatterns, + notePatterns, + }); + }, + [] + ); + + // ignore file items convert to exclusions format + const convertDeletedItemsToExclusions = useCallback((items: IgnoreItems): string => { + const notePatterns = new Array(...items.files).map((file) => getFilePattern(file)); + + return createPatternSettingsValue({ notePatterns: notePatterns }) || ""; + }, []); + + useEffect(() => { + latestGroupList.current = groupList; + latestIgnoreItems.current = ignoreItems; + }, [groupList, ignoreItems]); + + const allItems: Array<{ id: string; name: string }> = useMemo(() => { + const items: Array<{ id: string; name: string }> = []; + + const addFilesToItems = ( + items: Array<{ id: string; name: string }>, + groupItems: GroupItem[] + ): void => { + groupItems.forEach((groupItem) => { + if (!items.some((item) => item.id === groupItem.id)) { + items.push({ + id: groupItem.id, + name: groupItem.name, + }); + } + }); + }; + + const arr = [ + groupList.tags, + groupList.folders, + groupList.extensions, + { notes: groupList.notes }, + ]; + + arr.forEach((item) => { + Object.values(item).forEach((groupItems) => { + addFilesToItems(items, groupItems); + }); + }); + + return items; + }, [groupList]); + + const parseSearchQuery = useCallback((query: string): ParsedQuery => { + const tags: string[] = []; + const titles: string[] = []; + const extensions: string[] = []; + + const parts = query + .split(",") + .map((p) => p.trim()) + .filter((p) => p); + + parts.forEach((part) => { + if (part.startsWith("#")) { + // tag + tags.push(part.substring(1)); + } else if (part.startsWith(".") || part.startsWith("*.")) { + extensions.push(part.replace("*", "")); + } else { + // All other content is matched as title. + titles.push(part); + } + }); + return { tags, titles, extensions }; + }, []); + + const getDisplayItems = useMemo(() => { + if (searchTerm) { + // Custom search + const parsedQuery = parseSearchQuery(searchTerm); + return allItems + .filter((item) => { + const fileObj = appAllFiles.find((f) => f.path === item.id); + if (!fileObj) return false; + + const isNote = fileObj.extension === "md"; + + const matchesTag = + parsedQuery.tags.length > 0 && + isNote && + parsedQuery.tags.some((queryTag) => { + const fileTags = getTagsFromNote(fileObj); + return fileTags.some((tag) => { + const cleanTag = tag.startsWith("#") ? tag.substring(1) : tag; + return cleanTag.toLowerCase().includes(queryTag.toLowerCase()); + }); + }); + + const matchesTitle = + parsedQuery.titles.length > 0 && + parsedQuery.titles.some((t) => + fileObj.basename.toLowerCase().includes(t.toLowerCase()) + ); + + const matchesExtension = + parsedQuery.extensions.length > 0 && + parsedQuery.extensions.some( + (ext) => `.${fileObj.extension}`.toLowerCase() === ext.toLowerCase() + ); + + const hasSpecificFilters = + parsedQuery.tags.length > 0 || + parsedQuery.titles.length > 0 || + parsedQuery.extensions.length > 0; + + if (hasSpecificFilters) { + return matchesTag || matchesTitle || matchesExtension; + } + return false; + }) + .map((item) => { + return { + id: item.id, + name: item.name, + }; + }); + } + + if (activeSection === "tags" && activeItem) { + const tagFiles = groupList.tags[activeItem]; + if (tagFiles) { + return tagFiles; + } + return []; + } + + if (activeSection === "folders" && activeItem) { + const folderFiles = groupList.folders[activeItem]; + if (folderFiles) { + return folderFiles; + } + return []; + } + + if (activeSection === "files") { + return groupList.notes; + } + + if (activeSection === "extensions" && activeItem) { + const extensionFiles = groupList.extensions[activeItem]; + if (extensionFiles) { + return extensionFiles; + } + return []; + } + + if (activeSection === "ignoreFiles") { + return Array.from(ignoreItems.files).map((file) => ({ + id: file.path, + name: file.basename, + })); + } + + return []; + }, [ + searchTerm, + activeSection, + activeItem, + parseSearchQuery, + allItems, + appAllFiles, + groupList.tags, + groupList.folders, + groupList.notes, + groupList.extensions, + ignoreItems.files, + ]); + + const sortItems = useCallback((items: GroupItem[]) => { + return [...items].sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + }, []); + + const makeSectionItem = useCallback( + ( + groupData: Record>, + nameTransform?: (name: string) => string + ): SectionItem[] => { + return Object.entries(groupData) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([itemName, itemFiles]) => ({ + id: itemName, + name: nameTransform ? nameTransform(itemName) : itemName, + count: itemFiles.length, + })); + }, + [] + ); + + const addPatternToGroup = useCallback( + ( + groupType: "tags" | "folders" | "extensions", + pattern: string, + patternConfig: PatternCategory + ) => { + const getMatchingFilesFromApp = (patterns: PatternCategory): GroupItem[] => { + return appAllFiles + .filter((file) => shouldIndexFile(file, patterns, null)) + .map((file) => ({ + id: file.path, + name: file.basename, + })); + }; + + const ignoreFiles = [...latestIgnoreItems.current.files]; + const matchingFiles: GroupItem[] = getMatchingFilesFromApp(patternConfig).map((v) => ({ + ...v, + // add flag if file is ignored + isIgnored: ignoreFiles.some((f) => f.path === v.id), + })); + + setGroupList((prev) => ({ + ...prev, + [groupType]: { + ...prev[groupType], + [pattern]: matchingFiles, + }, + })); + }, + [appAllFiles] + ); + + const removeFileFromGroupList = useCallback( + (groupList: GroupListItem, filePath: string): GroupListItem => { + const newGroupList: GroupListItem = { + tags: { ...groupList.tags }, + folders: { ...groupList.folders }, + extensions: { ...groupList.extensions }, + notes: [...groupList.notes], + }; + + const removeFileFromGroupObject = (groupObject: Record>) => { + Object.keys(groupObject).forEach((key) => { + groupObject[key] = groupObject[key].filter((item) => item.id !== filePath); + }); + }; + + removeFileFromGroupObject(newGroupList.tags); + removeFileFromGroupObject(newGroupList.folders); + removeFileFromGroupObject(newGroupList.extensions); + + // Remove file from notes + newGroupList.notes = newGroupList.notes.filter((item) => item.id !== filePath); + + return newGroupList; + }, + [] + ); + + const setActiveState = useCallback( + (section: ActiveSection, item: ActiveItem = null, clearSearch: boolean = true) => { + setActiveSection(section); + setActiveItem(item); + if (clearSearch) { + setSearchTerm(""); + } + }, + [] + ); + + // Unified processor + const groupHandlers = useMemo(() => { + const createDeleteHandler = (groupType: keyof Omit) => { + return (e: React.MouseEvent, item: SectionItem) => { + e.stopPropagation(); + + setGroupList((prev) => { + const newGroup = { ...prev[groupType] }; + delete newGroup[item.id]; + return { + ...prev, + [groupType]: newGroup, + }; + }); + }; + }; + + return { + delete: { + tag: createDeleteHandler("tags"), + folder: createDeleteHandler("folders"), + extension: createDeleteHandler("extensions"), + }, + + add: { + tag: () => { + new TagSearchModal(app, (tagName) => { + const tagPattern = getTagPattern(tagName); + addPatternToGroup("tags", tagPattern, { tagPatterns: [tagPattern] }); + }).open(); + }, + + folder: () => { + new FolderSearchModal(app, (folderPath) => { + addPatternToGroup("folders", folderPath, { folderPatterns: [folderPath] }); + }).open(); + }, + + file: () => { + new ProjectFileSelectModal({ + app, + onFileSelect: (file: TFile) => { + const existingNote = groupList.notes.find((note) => note.id === file.path); + if (existingNote) return; + + // remove file from ignore + setIgnoreItems((prev) => { + const newFiles = new Set(prev.files); + newFiles.delete(file); + return { ...prev, files: newFiles }; + }); + + setGroupList((prev) => ({ + ...prev, + notes: [...prev.notes, { id: file.path, name: file.basename }], + })); + }, + excludeFilePaths: [], + titleOnly: false, + }).open(); + }, + + extension: () => { + // todo(emt-lin):maybe use this in the future + new Notice("Adding extension is temporarily not supported."); + return; + /*new ExtensionInputModal(app, (extension: string) => { + if (extension.trim() === "") return; + const extensionPattern = getExtensionPattern(extension); + addPatternToGroup("extensions", extensionPattern, { + extensionPatterns: [extensionPattern], + }); + }).open();*/ + }, + + ignoreFile: () => { + new ProjectFileSelectModal({ + app, + onFileSelect: (file: TFile) => { + const isAlreadyIgnored = ignoreItems.files.has(file); + if (isAlreadyIgnored) return; + + setIgnoreItems((prev) => { + const newFiles = new Set(prev.files); + newFiles.add(file); + return { ...prev, files: newFiles }; + }); + + // Remove related files from the groupList + setGroupList((prev) => removeFileFromGroupList(prev, file.path)); + }, + excludeFilePaths: [], + titleOnly: false, + }).open(); + }, + }, + + click: { + tag: (tagId: string) => { + setActiveState("tags", tagId); + }, + + folder: (folderId: string) => { + setActiveState("folders", folderId); + }, + + extension: (extension: string) => { + setActiveState("extensions", extension); + }, + + files: () => { + setActiveState("files"); + }, + + ignoreFiles: () => { + setActiveState("ignoreFiles"); + }, + }, + }; + }, [ + app, + groupList.notes, + ignoreItems.files, + addPatternToGroup, + removeFileFromGroupList, + setActiveState, + ]); + + const getDisplayTitle = () => { + if (searchTerm) return `Search Results for: "${searchTerm}"`; + if (activeSection === "tags" && activeItem) { + return `Tag: ${activeItem}`; + } + if (activeSection === "folders" && activeItem) { + return `Folder: ${activeItem}`; + } + if (activeSection === "files") return "Files"; + if (activeSection === "extensions" && activeItem) { + return `Extension: ${activeItem}`; + } + if (activeSection === "ignoreFiles") return "Ignore Files"; + return "Select a category to view items"; + }; + + const handleDeleteItem = (e: React.MouseEvent, item: GroupItem) => { + e.stopPropagation(); + + const file = appAllFiles.find((file) => file.path === item.id); + if (file) { + // add file to ignore + setIgnoreItems((prev) => { + const newFiles = new Set(prev.files); + newFiles.add(file); + return { ...prev, files: newFiles }; + }); + + setGroupList((prev) => removeFileFromGroupList(prev, item.id)); + } + }; + + const refreshGroupListFromCurrentPatterns = useCallback(() => { + const currentInclude = convertGroupListToInclusions(latestGroupList.current, appAllFiles); + const currentExclude = convertDeletedItemsToExclusions(latestIgnoreItems.current); + + const { inclusions, exclusions } = getMatchingPatterns({ + inclusions: currentInclude, + exclusions: currentExclude, + isProject: true, + }); + + const newGroupList = createAndPopulateGroupList(appAllFiles, inclusions, exclusions); + setGroupList(newGroupList); + }, [ + appAllFiles, + convertDeletedItemsToExclusions, + convertGroupListToInclusions, + createAndPopulateGroupList, + ]); + + const handleDeleteIgnoreItem = (e: React.MouseEvent, item: GroupItem) => { + e.stopPropagation(); + + const file = appAllFiles.find((file) => file.path === item.id); + + if (file) { + setIgnoreItems((prev) => { + const newFiles = new Set(prev.files); + newFiles.delete(file); + return { ...prev, files: newFiles }; + }); + + // refresh groupList + refreshGroupListFromCurrentPatterns(); + } + }; + + const handleSave = () => { + const include = convertGroupListToInclusions(groupList, appAllFiles); + const exclude = convertDeletedItemsToExclusions(ignoreItems); + onSave({ + ...initialProject, + contextSource: { + ...initialProject.contextSource, + inclusions: include, + exclusions: exclude, + }, + }); + }; + + return ( +
+ + {/* Left Sidebar - Navigation */} + +
+ {/* Header */} +
+

Project Context

+
+ + +
+ {/* Tags Section */} + name.slice(1))} + activeItem={activeItem} + activeSection={activeSection} + sectionType="tags" + renderIcon={(_item) => #} + onItemClick={groupHandlers.click.tag} + onAddClick={groupHandlers.add.tag} + onDeleteItem={(e, item) => groupHandlers.delete.tag(e, item)} + /> + + + + {/* Folders Section */} + groupHandlers.delete.folder(e, item)} + /> + + + + {/* Files Section */} +
+ +
+ Files ({groupList.notes.length}) +
+
+ + + + {/* todo(emt-lin):maybe use this in the future */} + {/* Extensions Section */} + {/* } + onItemClick={groupHandlers.click.extension} + onAddClick={groupHandlers.add.extension} + onDeleteItem={(e, item) => groupHandlers.delete.extension(e, item)} + /> + + */} + + {/* Ignore Files Section */} +
+ +
+ Ignore Files ({ignoreItems.files.size}) +
+
+
+
+
+
+ + + + {/* Right Content Area */} + +
+ {/* Header */} +
+ { + setSearchTerm(v); + if (v) { + setActiveState("search", null, false); + } + }} + /> +
+ + {/* Content Title */} +
+

{getDisplayTitle()}

+
+ + {/* Content Area */} + + {getDisplayItems.length === 0 ? ( +
+ {activeSection + ? "No items found." + : "Select a category from the sidebar to view items."} +
+ ) : ( +
+ {sortItems(getDisplayItems).map((item) => ( + + ))} +
+ )} +
+
+
+
+
+ + +
+
+ ); +} + +export class ContextManageModal extends Modal { + private root: Root; + + constructor( + app: App, + private onSave: (project: ProjectConfig) => void, + private initialProject: ProjectConfig + ) { + super(app); + } + + onOpen() { + const { contentEl, modalEl } = this; + this.root = createRoot(contentEl); + + modalEl.style.minWidth = "50vw"; + + const handleSave = (project: ProjectConfig) => { + this.onSave(project); + this.close(); + }; + + const handleCancel = () => { + this.close(); + }; + + this.root.render( + + ); + } + + onClose() { + if (this.root) { + this.root.unmount(); + } + } +} diff --git a/src/components/ui/SearchBar.tsx b/src/components/ui/SearchBar.tsx new file mode 100644 index 00000000..c504cfe2 --- /dev/null +++ b/src/components/ui/SearchBar.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { XCircle, Search } from "lucide-react"; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export const SearchBar: React.FC = ({ + value, + onChange, + placeholder = "Search...", +}) => { + return ( +
+ onChange(e.target.value)} + className="tw-pr-10" // Add padding to prevent text overlap with icons + /> + {value && ( + + )} + +
+ ); +}; diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx new file mode 100644 index 00000000..ba4e7543 --- /dev/null +++ b/src/components/ui/resizable.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { GripVertical } from "lucide-react"; +import * as ResizablePrimitive from "react-resizable-panels"; + +import { cn } from "@/lib/utils"; + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +const ResizablePanel = ResizablePrimitive.Panel; + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}) => ( + // tw-bg-border -> --divider-color: var(--background-modifier-border); + div]:tw-rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+); + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..253a9f97 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; + +import { cn } from "@/lib/utils"; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {/*[&>div:first-child]:!tw-block:for override the first child div style's (display: table)*/} + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + {/* tw-bg-border -> --divider-color: var(--background-modifier-border); */} + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 00000000..14a4a35b --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( + // tw-bg-border -> --divider-color: var(--background-modifier-border); + +)); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/search/searchUtils.ts b/src/search/searchUtils.ts index c4275b33..4111fc73 100644 --- a/src/search/searchUtils.ts +++ b/src/search/searchUtils.ts @@ -5,7 +5,7 @@ import { getTagsFromNote, stripHash } from "@/utils"; import { Embeddings } from "@langchain/core/embeddings"; import { App, TFile } from "obsidian"; -interface PatternCategory { +export interface PatternCategory { tagPatterns?: string[]; extensionPatterns?: string[]; folderPatterns?: string[]; @@ -331,3 +331,15 @@ export function extractAppIgnoreSettings(app: App): string[] { return appIgnoreFolders; } + +export function getTagPattern(tag: string): string { + return `#${tag}`; +} + +export function getFilePattern(file: TFile): string { + return `[[${file.basename}]]`; +} + +export function getExtensionPattern(extension: string): string { + return `*.${extension}`; +} diff --git a/src/tests/context-manage-modal.test.ts b/src/tests/context-manage-modal.test.ts new file mode 100644 index 00000000..359cd0e5 --- /dev/null +++ b/src/tests/context-manage-modal.test.ts @@ -0,0 +1,546 @@ +import { TFile } from "obsidian"; +import { + createPatternSettingsValue, + getFilePattern, + shouldIndexFile, + PatternCategory, +} from "@/search/searchUtils"; + +// Mock dependencies +jest.mock("obsidian", () => ({ + TFile: class TFile { + path: string; + basename: string; + extension: string; + + constructor(path: string = "") { + this.path = path; + const parts = path.split("/"); + const filename = parts[parts.length - 1]; + this.basename = filename.replace(/\.[^/.]+$/, ""); + this.extension = filename.split(".").pop() || ""; + } + }, +})); + +jest.mock("@/search/searchUtils", () => ({ + getMatchingPatterns: jest.fn(), + shouldIndexFile: jest.fn(), + getFilePattern: jest.fn(), + createPatternSettingsValue: jest.fn(), + categorizePatterns: jest.fn(), + getDecodedPatterns: jest.fn(), +})); + +// Mock types and interfaces +interface GroupItem { + id: string; + name: string; + isIgnored?: boolean; +} + +interface GroupListItem { + tags: Record>; + folders: Record>; + extensions: Record>; + notes: Array; +} + +interface IgnoreItems { + files: Set; +} + +// Test helper functions +const createTestFile = (path: string): TFile => { + const file = new TFile(); + file.path = path; + const parts = path.split("/"); + const filename = parts[parts.length - 1]; + file.basename = filename.replace(/\.[^/.]+$/, ""); + file.extension = filename.split(".").pop() || ""; + return file; +}; + +const createMockPatternCategory = (overrides: Partial = {}): PatternCategory => ({ + tagPatterns: [], + folderPatterns: [], + extensionPatterns: [], + notePatterns: [], + ...overrides, +}); + +// Implementation of the functions to test +const createAndPopulateGroupList = ( + appFiles: TFile[], + inclusionPatterns: PatternCategory | null, + exclusionPatterns: PatternCategory | null +): GroupListItem => { + const projectAllFiles = appFiles.filter((file) => + shouldIndexFile(file, inclusionPatterns, exclusionPatterns) + ); + + // Initialize groups + const tags: Record> = {}; + const folders: Record> = {}; + const extensions: Record> = {}; + const notes: Array = []; + + (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => { + tags[tag] = []; + }); + (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => { + folders[folder] = []; + }); + (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => { + extensions[extension] = []; + }); + + // Populate with matching files + projectAllFiles.forEach((file) => { + const groupItem: GroupItem = { + id: file.path, + name: file.basename, + }; + + // Add to notes array + notes.push(groupItem); + + // Add to appropriate groups based on patterns + (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => { + if (tags[tag]) { + tags[tag].push(groupItem); + } + }); + + (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => { + if (folders[folder] && file.path.includes(folder)) { + folders[folder].push(groupItem); + } + }); + + (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => { + if (extensions[extension] && file.extension === extension.replace("*.", "")) { + extensions[extension].push(groupItem); + } + }); + }); + + return { tags, folders, extensions, notes }; +}; + +const convertGroupListToInclusions = (list: GroupListItem, appFiles: TFile[]): string => { + const tagPatterns = Object.keys(list.tags); + const folderPatterns = Object.keys(list.folders); + const extensionPatterns = Object.keys(list.extensions); + const notePatterns = list.notes + .map((note) => { + const file = appFiles.find((file) => file.path === note.id); + if (file) { + return getFilePattern(file); + } + }) + .filter(Boolean) as string[]; + + return createPatternSettingsValue({ + tagPatterns, + folderPatterns, + extensionPatterns, + notePatterns, + }); +}; + +const convertDeletedItemsToExclusions = (items: IgnoreItems): string => { + const notePatterns = Array.from(items.files).map((file) => getFilePattern(file)); + return createPatternSettingsValue({ notePatterns }) || ""; +}; + +describe("Context Manage Modal Functions", () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + (shouldIndexFile as jest.Mock).mockReturnValue(true); + (getFilePattern as jest.Mock).mockImplementation((file: TFile) => `[[${file.basename}]]`); + (createPatternSettingsValue as jest.Mock).mockImplementation( + ({ tagPatterns = [], folderPatterns = [], extensionPatterns = [], notePatterns = [] }) => { + const patterns = [...tagPatterns, ...folderPatterns, ...extensionPatterns, ...notePatterns]; + return patterns.join(","); + } + ); + }); + + describe("createAndPopulateGroupList", () => { + it("Should create empty GroupList when there are no files", () => { + const result = createAndPopulateGroupList([], null, null); + + expect(result).toEqual({ + tags: {}, + folders: {}, + extensions: {}, + notes: [], + }); + }); + + it("Should correctly filter files based on inclusion and exclusion patterns", () => { + const files = [ + createTestFile("test1.md"), + createTestFile("test2.pdf"), + createTestFile("test3.txt"), + ]; + + (shouldIndexFile as jest.Mock) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + + const inclusionPatterns = createMockPatternCategory(); + const exclusionPatterns = createMockPatternCategory(); + + const result = createAndPopulateGroupList(files, inclusionPatterns, exclusionPatterns); + + expect(shouldIndexFile).toHaveBeenCalledTimes(3); + expect(result.notes).toHaveLength(2); + expect(result.notes[0].id).toBe("test1.md"); + expect(result.notes[1].id).toBe("test3.txt"); + }); + + it("Should initialize groups based on inclusion patterns", () => { + const files = [createTestFile("test.md")]; + const inclusionPatterns = createMockPatternCategory({ + tagPatterns: ["#important", "#work"], + folderPatterns: ["docs", "projects"], + extensionPatterns: ["*.md", "*.pdf"], + }); + + const result = createAndPopulateGroupList(files, inclusionPatterns, null); + + expect(Object.keys(result.tags)).toEqual(["#important", "#work"]); + expect(Object.keys(result.folders)).toEqual(["docs", "projects"]); + expect(Object.keys(result.extensions)).toEqual(["*.md", "*.pdf"]); + }); + + it("Should add files to appropriate groups", () => { + const files = [createTestFile("docs/test.md"), createTestFile("projects/work.pdf")]; + + const inclusionPatterns = createMockPatternCategory({ + folderPatterns: ["docs", "projects"], + extensionPatterns: ["*.md", "*.pdf"], + }); + + const result = createAndPopulateGroupList(files, inclusionPatterns, null); + + expect(result.folders["docs"]).toHaveLength(1); + expect(result.folders["docs"][0].name).toBe("test"); + expect(result.folders["projects"]).toHaveLength(1); + expect(result.folders["projects"][0].name).toBe("work"); + }); + + it("Should handle duplicate patterns", () => { + const files = [createTestFile("test.md")]; + const inclusionPatterns = createMockPatternCategory({ + tagPatterns: ["#tag", "#tag", "#tag"], + folderPatterns: ["folder", "folder"], + }); + + const result = createAndPopulateGroupList(files, inclusionPatterns, null); + + expect(Object.keys(result.tags)).toEqual(["#tag"]); + expect(Object.keys(result.folders)).toEqual(["folder"]); + }); + }); + + describe("convertGroupListToInclusions", () => { + it("Should convert empty GroupList to empty string", () => { + const emptyGroupList: GroupListItem = { + tags: {}, + folders: {}, + extensions: {}, + notes: [], + }; + + const result = convertGroupListToInclusions(emptyGroupList, []); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: [], + folderPatterns: [], + extensionPatterns: [], + notePatterns: [], + }); + expect(result).toBe(""); + }); + + it("Should correctly extract all pattern types", () => { + const groupList: GroupListItem = { + tags: { "#important": [], "#work": [] }, + folders: { docs: [], projects: [] }, + extensions: { "*.md": [], "*.pdf": [] }, + notes: [ + { id: "note1.md", name: "note1" }, + { id: "note2.md", name: "note2" }, + ], + }; + + const appFiles = [createTestFile("note1.md"), createTestFile("note2.md")]; + + convertGroupListToInclusions(groupList, appFiles); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: ["#important", "#work"], + folderPatterns: ["docs", "projects"], + extensionPatterns: ["*.md", "*.pdf"], + notePatterns: ["[[note1]]", "[[note2]]"], + }); + }); + + it("Should handle notes without corresponding files", () => { + const groupList: GroupListItem = { + tags: {}, + folders: {}, + extensions: {}, + notes: [ + { id: "missing.md", name: "missing" }, + { id: "existing.md", name: "existing" }, + ], + }; + + const appFiles = [createTestFile("existing.md")]; + + convertGroupListToInclusions(groupList, appFiles); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: [], + folderPatterns: [], + extensionPatterns: [], + notePatterns: ["[[existing]]"], + }); + }); + }); + + describe("convertDeletedItemsToExclusions", () => { + it("Should convert empty IgnoreItems to empty string", () => { + const emptyIgnoreItems: IgnoreItems = { + files: new Set(), + }; + + const result = convertDeletedItemsToExclusions(emptyIgnoreItems); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + notePatterns: [], + }); + expect(result).toBe(""); + }); + + it("Should correctly convert ignored files to exclusion patterns", () => { + const file1 = createTestFile("ignore1.md"); + const file2 = createTestFile("ignore2.pdf"); + + const ignoreItems: IgnoreItems = { + files: new Set([file1, file2]), + }; + + convertDeletedItemsToExclusions(ignoreItems); + + expect(getFilePattern).toHaveBeenCalledWith(file1); + expect(getFilePattern).toHaveBeenCalledWith(file2); + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + notePatterns: ["[[ignore1]]", "[[ignore2]]"], + }); + }); + + it("Should handle a single ignored file", () => { + const file = createTestFile("single.md"); + const ignoreItems: IgnoreItems = { + files: new Set([file]), + }; + + convertDeletedItemsToExclusions(ignoreItems); + + expect(getFilePattern).toHaveBeenCalledWith(file); + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + notePatterns: ["[[single]]"], + }); + }); + + it("Should handle case when createPatternSettingsValue returns undefined", () => { + (createPatternSettingsValue as jest.Mock).mockReturnValueOnce(undefined); + + const file = createTestFile("test.md"); + const ignoreItems: IgnoreItems = { + files: new Set([file]), + }; + + const result = convertDeletedItemsToExclusions(ignoreItems); + + expect(result).toBe(""); + }); + }); + + // Boundary conditions and error handling tests + describe("Boundary Condition Tests", () => { + describe("createAndPopulateGroupList", () => { + it("Should handle null inclusion patterns", () => { + const files = [createTestFile("test.md")]; + const result = createAndPopulateGroupList(files, null, null); + + expect(result.tags).toEqual({}); + expect(result.folders).toEqual({}); + expect(result.extensions).toEqual({}); + expect(result.notes).toHaveLength(1); + }); + + it("Should handle file paths with special characters", () => { + const files = [ + createTestFile("folder with spaces/file-with-dashes.md"), + createTestFile("中文文件夹/中文文件.md"), + createTestFile("folder/file@#$%.md"), + ]; + + const result = createAndPopulateGroupList(files, null, null); + + expect(result.notes).toHaveLength(3); + expect(result.notes[0].name).toBe("file-with-dashes"); + expect(result.notes[1].name).toBe("中文文件"); + expect(result.notes[2].name).toBe("file@#$%"); + }); + }); + + describe("convertGroupListToInclusions", () => { + it("Should handle large number of patterns", () => { + const tags: Record> = {}; + const folders: Record> = {}; + const extensions: Record> = {}; + + // Create many patterns + for (let i = 0; i < 100; i++) { + tags[`#tag${i}`] = []; + folders[`folder${i}`] = []; + extensions[`*.ext${i}`] = []; + } + + const groupList: GroupListItem = { tags, folders, extensions, notes: [] }; + convertGroupListToInclusions(groupList, []); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: expect.arrayContaining([`#tag0`, `#tag99`]), + folderPatterns: expect.arrayContaining([`folder0`, `folder99`]), + extensionPatterns: expect.arrayContaining([`*.ext0`, `*.ext99`]), + notePatterns: [], + }); + }); + + it("Should handle invalid file paths in notes", () => { + const groupList: GroupListItem = { + tags: {}, + folders: {}, + extensions: {}, + notes: [ + { id: "", name: "" }, + { id: " ", name: " " }, + { id: "valid.md", name: "valid" }, + ], + }; + + const appFiles = [createTestFile("valid.md")]; + convertGroupListToInclusions(groupList, appFiles); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: [], + folderPatterns: [], + extensionPatterns: [], + notePatterns: ["[[valid]]"], + }); + }); + }); + + describe("convertDeletedItemsToExclusions", () => { + it("Should handle large number of ignored files", () => { + const files = new Set(); + for (let i = 0; i < 1000; i++) { + files.add(createTestFile(`file${i}.md`)); + } + + const ignoreItems: IgnoreItems = { files }; + convertDeletedItemsToExclusions(ignoreItems); + + expect(getFilePattern).toHaveBeenCalledTimes(1000); + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + notePatterns: expect.arrayContaining(["[[file0]]", "[[file999]]"]), + }); + }); + + it("Should handle filenames with special characters", () => { + const files = [ + createTestFile("file with spaces.md"), + createTestFile("file-with-dashes.md"), + createTestFile("file_with_underscores.md"), + createTestFile("file@#$%.md"), + ]; + + const ignoreItems: IgnoreItems = { files: new Set(files) }; + convertDeletedItemsToExclusions(ignoreItems); + + expect(getFilePattern).toHaveBeenCalledTimes(4); + files.forEach((file) => { + expect(getFilePattern).toHaveBeenCalledWith(file); + }); + }); + }); + }); + + // Integration tests + describe("Integration Tests", () => { + it("Should correctly handle the complete workflow", () => { + // 1. Create initial file list + const appFiles = [ + createTestFile("docs/readme.md"), + createTestFile("src/main.ts"), + createTestFile("tests/test.spec.ts"), + createTestFile("config.json"), + ]; + + // 2. Set inclusion patterns + const inclusionPatterns = createMockPatternCategory({ + folderPatterns: ["docs", "src"], + extensionPatterns: ["*.md", "*.ts"], + }); + + // 3. Create GroupList + const groupList = createAndPopulateGroupList(appFiles, inclusionPatterns, null); + + // 4. Convert to inclusions + const inclusions = convertGroupListToInclusions(groupList, appFiles); + + // 5. Create ignore items and convert to exclusions + const ignoreItems: IgnoreItems = { + files: new Set([createTestFile("config.json")]), + }; + const exclusions = convertDeletedItemsToExclusions(ignoreItems); + + // Verify results + expect(groupList.folders).toHaveProperty("docs"); + expect(groupList.folders).toHaveProperty("src"); + expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.md")).toBe(true); + expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.ts")).toBe(true); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + tagPatterns: [], + folderPatterns: ["docs", "src"], + extensionPatterns: ["*.md", "*.ts"], + notePatterns: expect.any(Array), + }); + + expect(createPatternSettingsValue).toHaveBeenCalledWith({ + notePatterns: ["[[config]]"], + }); + + // Verify inclusions and exclusions variables + // Since createPatternSettingsValue is mocked to return all patterns as comma-separated list + expect(inclusions).toContain("docs"); + expect(inclusions).toContain("src"); + expect(inclusions).toContain("*.md"); + expect(inclusions).toContain("*.ts"); + + expect(exclusions).toBe("[[config]]"); + }); + }); +}); diff --git a/tailwind.config.js b/tailwind.config.js index 8d8b7edd..e58ec7e7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -40,6 +40,14 @@ module.exports = { green: "var(--color-green)", blue: "var(--color-blue)", }, + "context-manager": { + blue: "var(--color-blue)", + green: "var(--color-green)", + red: "var(--color-red)", + yellow: "var(--color-yellow)", + orange: "var(--color-orange)", + purple: "var(--color-purple)", + }, }, backgroundColor: { inherit: colors.inherit,