Chat UI revamp (#1074)

This commit is contained in:
Zero Liu 2025-01-21 22:03:22 -08:00 committed by GitHub
parent 45ca3b239c
commit 40c5f50c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1136 additions and 722 deletions

621
package-lock.json generated
View file

@ -22,7 +22,7 @@
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@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",
@ -4698,30 +4698,6 @@
}
}
},
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-checkbox/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",
@ -4814,30 +4790,6 @@
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-collapsible/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",
@ -5079,30 +5031,6 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-dialog/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",
@ -5185,16 +5113,17 @@
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz",
"integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz",
"integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "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-id": "1.1.0",
"@radix-ui/react-menu": "2.1.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-menu": "2.1.4",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
@ -5212,6 +5141,68 @@
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/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==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dropdown-menu/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-dropdown-menu/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==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "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-dropdown-menu/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/react-focus-guards": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
@ -5291,28 +5282,29 @@
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
"integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz",
"integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "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-dismissable-layer": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-roving-focus": "1.1.0",
"@radix-ui/react-slot": "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-roving-focus": "1.1.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
"react-remove-scroll": "^2.6.1"
},
"peerDependencies": {
"@types/react": "*",
@ -5329,6 +5321,250 @@
}
}
},
"node_modules/@radix-ui/react-menu/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==",
"license": "MIT"
},
"node_modules/@radix-ui/react-menu/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==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.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-menu/node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"license": "MIT",
"dependencies": {
"@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-slot": "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-menu/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-menu/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==",
"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"
},
"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-menu/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"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-menu/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==",
"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"
},
"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-menu/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==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-menu/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==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "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-menu/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/react-menu/node_modules/react-remove-scroll": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz",
@ -5518,30 +5754,6 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-popover/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",
@ -5677,11 +5889,12 @@
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
@ -5699,6 +5912,21 @@
}
}
},
"node_modules/@radix-ui/react-presence/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-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
@ -5722,17 +5950,18 @@
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
"integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "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-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
@ -5751,10 +5980,84 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"node_modules/@radix-ui/react-roving-focus/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==",
"license": "MIT"
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"license": "MIT",
"dependencies": {
"@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-slot": "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-roving-focus/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-roving-focus/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==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "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-roving-focus/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"
@ -5817,30 +6120,6 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
@ -6189,30 +6468,6 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"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-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",

View file

@ -83,14 +83,14 @@
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@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-select": "^2.1.2",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@tabler/icons-react": "^2.14.0",
"axios": "^1.3.4",

View file

@ -1,6 +1,7 @@
import { useChainType, useModelKey } from "@/aiParams";
import { ChainType } from "@/chainFactory";
import { updateChatMemory } from "@/chatUtils";
import { ChatControls } from "@/components/chat-components/ChatControls";
import ChatInput from "@/components/chat-components/ChatInput";
import ChatMessages from "@/components/chat-components/ChatMessages";
import { ABORT_REASON, COMMAND_IDS, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants";
@ -312,25 +313,6 @@ ${chatContent}`;
[app, chatHistory, currentModelKey, settings.defaultConversationTag, settings.defaultSaveFolder]
);
const refreshVaultContext = useCallback(async () => {
if (!app) {
console.error("App instance is not available.");
return;
}
try {
await plugin.vectorStoreManager.indexVaultToVectorStore();
new Notice("Vault index refreshed.");
} catch (error) {
console.error("Error refreshing vault index:", error);
new Notice("Failed to refresh vault index. Check console for details.");
}
}, [app, plugin.vectorStoreManager]);
const clearCurrentAiMessage = useCallback(() => {
setCurrentAiMessage("");
}, []);
const handleStopGenerating = useCallback(
(reason?: ABORT_REASON) => {
if (abortController) {
@ -533,6 +515,25 @@ ${chatContent}`;
setInputMessage((prev) => `${prev} ${prompt} `);
}, []);
const handleNewChat = useCallback(async () => {
handleStopGenerating(ABORT_REASON.NEW_CHAT);
if (settings.autosaveChat && chatHistory.length > 0) {
await handleSaveAsNote(true);
}
clearMessages();
chainManager.memoryManager.clearChatMemory();
setCurrentAiMessage("");
setContextNotes([]);
setIncludeActiveNote(false);
}, [
handleStopGenerating,
settings.autosaveChat,
chatHistory.length,
clearMessages,
chainManager.memoryManager,
handleSaveAsNote,
]);
return (
<div className="chat-container">
<ChatMessages
@ -548,6 +549,7 @@ ${chatContent}`;
onReplaceChat={setInputMessage}
/>
<div className="bottom-container">
<ChatControls onNewChat={handleNewChat} onSaveAsNote={() => handleSaveAsNote(true)} />
<ChatInput
ref={inputRef}
inputMessage={inputMessage}
@ -557,19 +559,6 @@ ${chatContent}`;
onStopGenerating={() => handleStopGenerating(ABORT_REASON.USER_STOPPED)}
app={app}
navigateHistory={navigateHistory}
onNewChat={async (openNote: boolean) => {
handleStopGenerating(ABORT_REASON.NEW_CHAT);
if (settings.autosaveChat && chatHistory.length > 0) {
await handleSaveAsNote(openNote);
}
clearMessages();
chainManager.memoryManager.clearChatMemory();
clearCurrentAiMessage();
setContextNotes([]);
setIncludeActiveNote(false);
}}
onSaveAsNote={() => handleSaveAsNote(true)}
onRefreshVaultContext={refreshVaultContext}
contextNotes={contextNotes}
setContextNotes={setContextNotes}
includeActiveNote={includeActiveNote}
@ -578,7 +567,6 @@ ${chatContent}`;
selectedImages={selectedImages}
onAddImage={(files: File[]) => setSelectedImages((prev) => [...prev, ...files])}
setSelectedImages={setSelectedImages}
chatHistory={chatHistory}
/>
</div>
</div>

View file

@ -1,4 +1,8 @@
import { Button } from "@/components/ui/button";
import { Platform } from "obsidian";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { USER_SENDER } from "@/constants";
import { cn } from "@/lib/utils";
import { ChatMessage } from "@/sharedState";
import {
Check,
@ -35,39 +39,79 @@ export const ChatButtons: React.FC<ChatButtonsProps> = ({
hasSources,
}) => {
return (
<div className="chat-message-buttons">
<button onClick={onCopy} className="clickable-icon" title="Copy">
{isCopied ? <Check /> : <Copy />}
</button>
<div
className={cn("flex", {
"group-hover:opacity-100 opacity-0": !Platform.isMobile,
})}
>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost2" size="fit" onClick={onCopy} title="Copy">
{isCopied ? <Check className="size-4" /> : <Copy className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>Copy</TooltipContent>
</Tooltip>
{message.sender === USER_SENDER ? (
<>
<button onClick={onEdit} className="clickable-icon" title="Edit">
<PenSquare />
</button>
<button onClick={onDelete} className="clickable-icon" title="Delete">
<Trash2 />
</button>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onEdit} variant="ghost2" size="fit" title="Edit">
<PenSquare className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onDelete} variant="ghost2" size="fit" title="Delete">
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</>
) : (
<>
{hasSources && (
<button onClick={onShowSources} className="clickable-icon" title="Show Sources">
<LibraryBig />
</button>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onShowSources} variant="ghost2" size="fit" title="Show Sources">
<LibraryBig className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Show Sources</TooltipContent>
</Tooltip>
)}
<button
onClick={onInsertIntoEditor}
className="clickable-icon"
title="Insert to note at cursor"
>
<TextCursorInput />
</button>
<button onClick={onRegenerate} className="clickable-icon" title="Regenerate">
<RotateCw />
</button>
<button onClick={onDelete} className="clickable-icon" title="Delete">
<Trash2 />
</button>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onInsertIntoEditor}
variant="ghost2"
size="fit"
title="Insert to note at cursor"
>
<TextCursorInput className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Insert to note at cursor</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onRegenerate} variant="ghost2" size="fit" title="Regenerate">
<RotateCw className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Regenerate</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onDelete} variant="ghost2" size="fit" title="Delete">
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</>
)}
</div>

View file

@ -1,7 +1,8 @@
import { Plus } from "lucide-react";
import { Plus, X } from "lucide-react";
import { TFile } from "obsidian";
import React from "react";
import { TooltipActionButton } from "./TooltipActionButton";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
interface ChatContextMenuProps {
activeNote: TFile | null;
@ -12,6 +13,53 @@ interface ChatContextMenuProps {
onRemoveUrl: (url: string) => void;
}
function ContextNote({
note,
isActive = false,
onRemoveContext,
}: {
note: TFile;
isActive: boolean;
onRemoveContext: (path: string) => void;
}) {
return (
<Badge className="text-xs pl-2 pr-0.5 py-0 items-center">
<div className="flex items-center gap-1">
<span className="max-w-40 truncate">{note.basename}</span>
{isActive && <span className="text-xs text-faint">Current</span>}
{note.extension === "pdf" && <span className="text-xs text-faint">pdf</span>}
</div>
<Button
variant="ghost2"
size="fit"
onClick={() => onRemoveContext(note.path)}
aria-label="Remove from context"
>
<X className="size-4" />
</Button>
</Badge>
);
}
function ContextUrl({ url, onRemoveUrl }: { url: string; onRemoveUrl: (url: string) => void }) {
return (
<Badge className="text-xs pl-2 pr-0.5 py-0 items-center">
<div className="flex items-center gap-1">
<span className="max-w-40 truncate">{url}</span>
<span className="text-xs text-faint">Link</span>
</div>
<Button
variant="ghost2"
size="fit"
onClick={() => onRemoveUrl(url)}
aria-label="Remove from context"
>
<X className="size-4" />
</Button>
</Badge>
);
}
export const ChatContextMenu: React.FC<ChatContextMenuProps> = ({
activeNote,
contextNotes,
@ -36,49 +84,40 @@ export const ChatContextMenu: React.FC<ChatContextMenuProps> = ({
const uniqueUrls = React.useMemo(() => Array.from(new Set(contextUrls)), [contextUrls]);
const renderNote = (note: TFile, isActive = false) => (
<div key={note.path} className={`context-note ${isActive ? "active" : "with-hover"}`}>
<span className="note-name">{note.basename}</span>
{isActive && <span className="note-badge">current</span>}
{note.extension === "pdf" && <span className="note-badge pdf">pdf</span>}
<button
className="remove-note"
onClick={() => onRemoveContext(note.path)}
aria-label="Remove from context"
>
×
</button>
</div>
);
const hasContext = uniqueNotes.length > 0 || uniqueUrls.length > 0 || !!activeNote;
return (
<div className="chat-context-menu">
<TooltipActionButton onClick={onAddContext} Icon={<Plus className="icon-scaler" />}>
Add Note to Context
</TooltipActionButton>
<div className="context-notes">
{activeNote && renderNote(activeNote, true)}
{uniqueNotes.map((note) => renderNote(note))}
<div className="flex items-center w-full gap-1">
<div className="flex items-start h-full">
<Button
onClick={onAddContext}
variant="ghost2"
size="fit"
className="border border-border border-solid rounded-sm ml-1"
>
<Plus className="size-4" />
{!hasContext && <span className="text-xs leading-4 pr-1">Add context</span>}
</Button>
</div>
<div className="flex gap-1 flex-wrap flex-1">
{activeNote && (
<ContextNote
key={activeNote.path}
note={activeNote}
isActive={true}
onRemoveContext={onRemoveContext}
/>
)}
{uniqueNotes.map((note) => (
<ContextNote
key={note.path}
note={note}
isActive={false}
onRemoveContext={onRemoveContext}
/>
))}
{uniqueUrls.map((url) => (
<div key={url} className="context-note url">
<span className="note-name" title={url}>
{(() => {
const hostname = new URL(url).hostname;
const cleanUrl = url.replace(/\/+$/, "");
const path = cleanUrl.slice(cleanUrl.indexOf(hostname) + hostname.length);
if (path.length <= 1) return hostname;
return `${hostname}/...${cleanUrl.slice(-2)}`;
})()}
</span>
<span className="note-badge">url</span>
<button
className="remove-note"
onClick={() => onRemoveUrl(url)}
aria-label="Remove URL from context"
>
×
</button>
</div>
<ContextUrl key={url} url={url} onRemoveUrl={onRemoveUrl} />
))}
</div>
</div>

View file

@ -1,172 +1,128 @@
import { useChainType } from "@/aiParams";
import { App } from "obsidian";
import React from "react";
import {
Download,
MoreHorizontal,
Sparkles,
FileText,
RefreshCw,
MessageCirclePlus,
ChevronDown,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { SettingSwitch } from "@/components/ui/setting-switch";
import { updateSetting, useSettingsValue } from "@/settings/model";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useChainType } from "@/aiParams";
import { ChainType } from "@/chainFactory";
import { TooltipActionButton } from "@/components/chat-components/TooltipActionButton";
import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal";
import { stringToChainType } from "@/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { ChevronDown, Download, MessageCirclePlus, Puzzle } from "lucide-react";
import { Notice } from "obsidian";
import VectorStoreManager from "@/search/vectorStoreManager";
import { NewChatConfirmModal } from "@/components/modals/NewChatConfirmModal";
import { useSettingsValue } from "@/settings/model";
import { ChatMessage } from "@/sharedState";
import { TFile } from "obsidian";
import { ChatContextMenu } from "./ChatContextMenu";
interface ChatControlsProps {
onNewChat: (openNote: boolean) => void;
onSaveAsNote: () => void;
onRefreshVaultContext: () => void;
app: App;
contextNotes: TFile[];
setContextNotes: React.Dispatch<React.SetStateAction<TFile[]>>;
includeActiveNote: boolean;
setIncludeActiveNote: React.Dispatch<React.SetStateAction<boolean>>;
activeNote: TFile | null;
contextUrls: string[];
onRemoveUrl: (url: string) => void;
chatHistory: ChatMessage[];
export async function refreshVaultIndex() {
try {
await VectorStoreManager.getInstance().indexVaultToVectorStore();
new Notice("Vault index refreshed.");
} catch (error) {
console.error("Error refreshing vault index:", error);
new Notice("Failed to refresh vault index. Check console for details.");
}
}
const ChatControls: React.FC<ChatControlsProps> = ({
onNewChat,
onSaveAsNote,
onRefreshVaultContext,
app,
contextNotes,
setContextNotes,
includeActiveNote,
setIncludeActiveNote,
activeNote,
contextUrls,
onRemoveUrl,
chatHistory,
}) => {
const [selectedChain, setSelectedChain] = useChainType();
interface ChatControlsProps {
onNewChat: () => void;
onSaveAsNote: () => void;
}
export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
const settings = useSettingsValue();
const handleChainChange = async ({ value }: { value: string }) => {
const newChain = stringToChainType(value);
setSelectedChain(newChain);
};
const handleAddContext = () => {
const excludeNotes = [
...contextNotes.map((note) => note.path),
...(includeActiveNote && activeNote ? [activeNote.path] : []),
].filter(Boolean) as string[];
new AddContextNoteModal({
app,
onNoteSelect: (note) => {
if (activeNote && note.path === activeNote.path) {
setIncludeActiveNote(true);
// Remove the note from contextNotes if it exists there
setContextNotes((prev) => prev.filter((n) => n.path !== note.path));
} else {
// Add wasAddedManually flag to distinguish from reference-added notes
setContextNotes((prev) => [...prev, Object.assign(note, { wasAddedManually: true })]);
}
},
excludeNotes,
}).open();
};
const handleRemoveContext = (path: string) => {
// First check if this note was added manually
const noteToRemove = contextNotes.find((note) => note.path === path);
const wasAddedManually = noteToRemove && (noteToRemove as any).wasAddedManually;
if (wasAddedManually) {
// If it was added manually, just remove it from contextNotes
setContextNotes((prev) => prev.filter((note) => note.path !== path));
} else {
// If it wasn't added manually, it could be either:
// 1. The active note (controlled by includeActiveNote)
// 2. A note added via [[reference]]
// In either case, we should:
setIncludeActiveNote(false); // Turn off includeActiveNote if this was the active note
setContextNotes((prev) => prev.filter((note) => note.path !== path)); // Remove from contextNotes if it was there
}
};
const [selectedChain, setSelectedChain] = useChainType();
return (
<div className="chat-controls-wrapper">
<div className="chat-icons-container">
{selectedChain === ChainType.COPILOT_PLUS_CHAIN && (
<ChatContextMenu
activeNote={includeActiveNote ? activeNote : null}
contextNotes={contextNotes}
onAddContext={handleAddContext}
onRemoveContext={handleRemoveContext}
contextUrls={contextUrls}
onRemoveUrl={onRemoveUrl}
/>
)}
<div className="chat-icons-right">
<TooltipActionButton
onClick={() => {
if (!settings.autosaveChat && chatHistory.length > 0) {
new NewChatConfirmModal(app, () => {
onNewChat(false);
}).open();
} else {
onNewChat(false);
}
}}
Icon={<MessageCirclePlus className="icon-scaler" />}
>
<div>New Chat</div>
{!settings.autosaveChat && <div>(Unsaved history will be lost)</div>}
</TooltipActionButton>
<TooltipActionButton onClick={onSaveAsNote} Icon={<Download className="icon-scaler" />}>
Save as Note
</TooltipActionButton>
{(selectedChain === ChainType.VAULT_QA_CHAIN ||
selectedChain === ChainType.COPILOT_PLUS_CHAIN) && (
<TooltipActionButton
onClick={onRefreshVaultContext}
Icon={<Puzzle className="icon-scaler" />}
>
Refresh Index for Vault
</TooltipActionButton>
)}
<div className="chat-icon-selection-tooltip">
<DropdownMenu.Root>
<DropdownMenu.Trigger className="chain-select-button">
{selectedChain === ChainType.LLM_CHAIN && "chat"}
{selectedChain === ChainType.VAULT_QA_CHAIN && "vault QA (basic)"}
{selectedChain === ChainType.COPILOT_PLUS_CHAIN && "copilot plus (beta)"}
<ChevronDown size={10} />
</DropdownMenu.Trigger>
<div className="w-full py-1 flex justify-between items-center px-1">
<div className="flex-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost2" size="fit" className="ml-1">
{selectedChain === ChainType.LLM_CHAIN && "chat"}
{selectedChain === ChainType.VAULT_QA_CHAIN && "vault QA (basic)"}
{selectedChain === ChainType.COPILOT_PLUS_CHAIN && "copilot plus (beta)"}
<ChevronDown className="size-5 mt-0.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.LLM_CHAIN)}>
chat
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.VAULT_QA_CHAIN)}>
vault QA (basic)
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}>
copilot plus (beta)
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost2" size="icon" title="New Chat" onClick={onNewChat}>
<MessageCirclePlus className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost2" size="icon" title="Save Chat as Note" onClick={onSaveAsNote}>
<Download className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Save Chat as Note</TooltipContent>
</Tooltip>
<DropdownMenu.Portal container={activeDocument.body}>
<DropdownMenu.Content className="chain-select-content" align="end" sideOffset={5}>
<DropdownMenu.Item
onSelect={() => handleChainChange({ value: ChainType.LLM_CHAIN })}
>
chat
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => handleChainChange({ value: ChainType.VAULT_QA_CHAIN })}
>
vault QA (basic)
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => handleChainChange({ value: ChainType.COPILOT_PLUS_CHAIN })}
>
copilot plus (beta)
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost2" size="icon" title="Advanced Settings">
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuItem
className="flex justify-between"
onSelect={(e) => {
e.preventDefault();
updateSetting("showSuggestedPrompts", !settings.showSuggestedPrompts);
}}
>
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
Suggested Prompt
</div>
<SettingSwitch checked={settings.showSuggestedPrompts} />
</DropdownMenuItem>
<DropdownMenuItem
className="flex justify-between"
onSelect={(e) => {
e.preventDefault();
updateSetting("showRelevantNotes", !settings.showRelevantNotes);
}}
>
<div className="flex items-center gap-2">
<FileText className="size-4" />
Relevant Note
</div>
<SettingSwitch checked={settings.showRelevantNotes} />
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => refreshVaultIndex()}
>
<RefreshCw className="size-4" />
Refresh Vault Index
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
};
export default ChatControls;
}

View file

@ -8,15 +8,27 @@ import { CustomPromptProcessor } from "@/customPromptProcessor";
import { COPILOT_TOOL_NAMES } from "@/LLMProviders/intentAnalyzer";
import { Mention } from "@/mentions/Mention";
import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
import { ChatMessage } from "@/sharedState";
import { getToolDescription } from "@/tools/toolManager";
import { extractNoteTitles } from "@/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { ArrowBigUp, ChevronUp, Command, CornerDownLeft, Image, StopCircle } from "lucide-react";
import {
ArrowBigUp,
ChevronDown,
Command,
CornerDownLeft,
Image,
StopCircle,
X,
} from "lucide-react";
import { App, Platform, TFile } from "obsidian";
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import ChatControls from "./ChatControls";
import { TooltipActionButton } from "./TooltipActionButton";
import ContextControl from "./ContextControl";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
interface ChatInputProps {
inputMessage: string;
@ -30,9 +42,6 @@ interface ChatInputProps {
onStopGenerating: () => void;
app: App;
navigateHistory: (direction: "up" | "down") => string;
onNewChat: (openNote: boolean) => void;
onSaveAsNote: () => void;
onRefreshVaultContext: () => void;
contextNotes: TFile[];
setContextNotes: React.Dispatch<React.SetStateAction<TFile[]>>;
includeActiveNote: boolean;
@ -41,7 +50,6 @@ interface ChatInputProps {
selectedImages: File[];
onAddImage: (files: File[]) => void;
setSelectedImages: React.Dispatch<React.SetStateAction<File[]>>;
chatHistory: ChatMessage[];
}
const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
@ -54,9 +62,6 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
onStopGenerating,
app,
navigateHistory,
onNewChat,
onSaveAsNote,
onRefreshVaultContext,
contextNotes,
setContextNotes,
includeActiveNote,
@ -65,7 +70,6 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
selectedImages,
onAddImage,
setSelectedImages,
chatHistory,
},
ref
) => {
@ -352,11 +356,11 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
}, [app.workspace]);
return (
<div className="chat-input-container" ref={containerRef}>
<ChatControls
onNewChat={onNewChat}
onSaveAsNote={onSaveAsNote}
onRefreshVaultContext={onRefreshVaultContext}
<div
className="flex flex-col gap-0.5 w-full border border-border border-solid rounded-md pt-2 pb-1 px-1"
ref={containerRef}
>
<ContextControl
app={app}
contextNotes={contextNotes}
setContextNotes={setContextNotes}
@ -365,7 +369,6 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
activeNote={currentActiveNote}
contextUrls={contextUrls}
onRemoveUrl={(url: string) => setContextUrls((prev) => prev.filter((u) => u !== url))}
chatHistory={chatHistory}
/>
{selectedImages.length > 0 && (
@ -382,7 +385,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
onClick={() => setSelectedImages((prev) => prev.filter((_, i) => i !== index))}
title="Remove image"
>
×
<X className="size-4" />
</button>
</div>
))}
@ -391,7 +394,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
<textarea
ref={textAreaRef}
className="chat-input-textarea"
className="w-full bg-transparent focus-visible:ring-0 border-none min-h-10 max-h-40 overflow-y-auto resize-none px-2 rounded-md text-sm text-normal"
placeholder={
"Ask anything. [[ for notes. / for custom prompts. " +
(currentChain === ChainType.COPILOT_PLUS_CHAIN ? "@ for tools." : "")
@ -401,79 +404,93 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
onKeyDown={handleKeyDown}
/>
<div className="chat-input-controls">
<div className="chat-input-left">
<DropdownMenu.Root open={isModelDropdownOpen} onOpenChange={setIsModelDropdownOpen}>
<DropdownMenu.Trigger className="model-select-button">
{settings.activeModels.find(
(model) => getModelKeyFromModel(model) === currentModelKey
)?.name || "Select Model"}
<ChevronUp size={10} />
</DropdownMenu.Trigger>
<div className="flex gap-1 justify-between px-1">
<div className="flex items-center gap-1">
<DropdownMenu open={isModelDropdownOpen} onOpenChange={setIsModelDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost2" size="fit">
{settings.activeModels.find(
(model) => getModelKeyFromModel(model) === currentModelKey
)?.name || "Select Model"}
<ChevronDown className="size-5 mt-0.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenu.Portal container={activeDocument.body}>
<DropdownMenu.Content className="model-select-content" align="start">
{settings.activeModels
.filter((model) => model.enabled)
.map((model) => (
<DropdownMenu.Item
key={getModelKeyFromModel(model)}
onSelect={() => setCurrentModelKey(getModelKeyFromModel(model))}
>
{model.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<DropdownMenuContent align="start">
{settings.activeModels
.filter((model) => model.enabled)
.map((model) => (
<DropdownMenuItem
key={getModelKeyFromModel(model)}
onSelect={() => setCurrentModelKey(getModelKeyFromModel(model))}
>
{model.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{currentChain === ChainType.COPILOT_PLUS_CHAIN && (
<TooltipActionButton
<Button
variant="ghost2"
size="fit"
onClick={() => {
new AddImageModal(app, onAddImage).open();
}}
Icon={
<div className="button-content">
<span>image</span>
<Image className="icon-scaler" />
</div>
}
>
Add Image
</TooltipActionButton>
<div className="flex items-center gap-1">
<span>image</span>
<Image className="!size-3" />
</div>
</Button>
)}
</div>
<div className="chat-input-buttons">
<div className="flex items-center gap-1">
{isGenerating && (
<button onClick={() => onStopGenerating()} className="submit-button cancel">
<StopCircle />
</button>
<Button
variant="ghost2"
size="fit"
className="text-muted"
onClick={() => onStopGenerating()}
>
<StopCircle className="size-4" />
</Button>
)}
<button onClick={() => onSendMessage(false)} className="submit-button">
<CornerDownLeft size={16} />
<Button
variant="ghost2"
size="fit"
className="text-muted"
onClick={() => onSendMessage(false)}
>
<CornerDownLeft className="!size-3" />
<span>chat</span>
</button>
</Button>
{currentChain === "copilot_plus" && (
<button onClick={() => onSendMessage(true)} className="submit-button vault">
<div className="button-content">
<Button
variant="ghost2"
size="fit"
className="text-muted"
onClick={() => onSendMessage(true)}
>
<div className="flex items-center gap-1">
{Platform.isMacOS ? (
<>
<Command size={12} />
<ArrowBigUp size={16} />
<CornerDownLeft size={16} />
</>
<div className="flex items-center">
<Command className="!size-3" />
<ArrowBigUp className="!size-3" />
<CornerDownLeft className="!size-3" />
</div>
) : (
<>
<div className="flex items-center">
<span>Ctrl</span>
<ArrowBigUp size={16} />
<CornerDownLeft size={16} />
</>
<ArrowBigUp className="size-4" />
<CornerDownLeft className="!size-3" />
</div>
)}
<span>vault</span>
</div>
</button>
</Button>
)}
</div>
</div>

View file

@ -248,8 +248,8 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
<div className="flex flex-col w-full my-1">
<div
className={cn(
"flex rounded-md p-2 mx-2 gap-2",
message.sender === USER_SENDER && "bg-primary-alt"
"flex rounded-md p-2 mx-2 gap-2 group",
message.sender === USER_SENDER && "border border-border border-solid"
)}
>
<div className="w-6 shrink-0">{message.sender === USER_SENDER ? <User /> : <Bot />}</div>
@ -258,8 +258,8 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
<div className="message-content">{renderMessageContent()}</div>
{!isStreaming && (
<div className="message-buttons-wrapper">
<div className="message-timestamp">{message.timestamp?.display}</div>
<div className="flex justify-between items-center">
<div className="text-faint text-xs">{message.timestamp?.display}</div>
<ChatButtons
message={message}
onCopy={copyToClipboard}

View file

@ -0,0 +1,89 @@
import { useChainType } from "@/aiParams";
import { App } from "obsidian";
import React from "react";
import { ChainType } from "@/chainFactory";
import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal";
import { TFile } from "obsidian";
import { ChatContextMenu } from "./ChatContextMenu";
interface ChatControlsProps {
app: App;
contextNotes: TFile[];
setContextNotes: React.Dispatch<React.SetStateAction<TFile[]>>;
includeActiveNote: boolean;
setIncludeActiveNote: React.Dispatch<React.SetStateAction<boolean>>;
activeNote: TFile | null;
contextUrls: string[];
onRemoveUrl: (url: string) => void;
}
const ContextControl: React.FC<ChatControlsProps> = ({
app,
contextNotes,
setContextNotes,
includeActiveNote,
setIncludeActiveNote,
activeNote,
contextUrls,
onRemoveUrl,
}) => {
const [selectedChain] = useChainType();
const handleAddContext = () => {
const excludeNotes = [
...contextNotes.map((note) => note.path),
...(includeActiveNote && activeNote ? [activeNote.path] : []),
].filter(Boolean) as string[];
new AddContextNoteModal({
app,
onNoteSelect: (note) => {
if (activeNote && note.path === activeNote.path) {
setIncludeActiveNote(true);
// Remove the note from contextNotes if it exists there
setContextNotes((prev) => prev.filter((n) => n.path !== note.path));
} else {
// Add wasAddedManually flag to distinguish from reference-added notes
setContextNotes((prev) => [...prev, Object.assign(note, { wasAddedManually: true })]);
}
},
excludeNotes,
}).open();
};
const handleRemoveContext = (path: string) => {
// First check if this note was added manually
const noteToRemove = contextNotes.find((note) => note.path === path);
const wasAddedManually = noteToRemove && (noteToRemove as any).wasAddedManually;
if (wasAddedManually) {
// If it was added manually, just remove it from contextNotes
setContextNotes((prev) => prev.filter((note) => note.path !== path));
} else {
// If it wasn't added manually, it could be either:
// 1. The active note (controlled by includeActiveNote)
// 2. A note added via [[reference]]
// In either case, we should:
setIncludeActiveNote(false); // Turn off includeActiveNote if this was the active note
setContextNotes((prev) => prev.filter((note) => note.path !== path)); // Remove from contextNotes if it was there
}
};
if (selectedChain !== ChainType.COPILOT_PLUS_CHAIN) {
return null;
}
return (
<ChatContextMenu
activeNote={includeActiveNote ? activeNote : null}
contextNotes={contextNotes}
onAddContext={handleAddContext}
onRemoveContext={handleRemoveContext}
contextUrls={contextUrls}
onRemoveUrl={onRemoveUrl}
/>
);
};
export default ContextControl;

View file

@ -21,6 +21,7 @@ import { Notice, TFile } from "obsidian";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { useActiveFile } from "@/hooks/useActiveFile";
import { Button } from "@/components/ui/button";
function useRelevantNotes(refresher: number) {
const [relevantNotes, setRelevantNotes] = useState<RelevantNoteEntry[]>([]);
@ -135,12 +136,9 @@ function RelevantNote({
<div className="flex gap-2">
<Tooltip>
<TooltipTrigger asChild>
<button
className="size-6 p-0 !bg-transparent border-none !shadow-none hover:!bg-interactive-hover"
onClick={onAddToChat}
>
<Button variant="ghost2" size="icon" onClick={onAddToChat}>
<PlusCircle className="size-4" />
</button>
</Button>
</TooltipTrigger>
<TooltipContent>Add to Chat</TooltipContent>
</Tooltip>
@ -225,7 +223,7 @@ export const RelevantNotes = memo(
)}
>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="flex justify-between items-center px-2 pb-2">
<div className="flex justify-between items-center pl-1 pb-2">
<div className="flex gap-2 items-center flex-1">
<span className="font-semibold text-normal">Relevant Notes</span>
<Tooltip>
@ -246,34 +244,31 @@ export const RelevantNotes = memo(
</Tooltip>
)}
</div>
<div className="flex gap-2 items-center">
<div className="flex items-center">
<Tooltip>
<TooltipTrigger asChild>
<button
className="size-6 p-0 !bg-transparent border-none !shadow-none hover:!bg-interactive-hover"
onClick={refreshIndex}
>
<Button variant="ghost2" size="icon" onClick={refreshIndex}>
<RefreshCcw className="size-4" />
</button>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Reindex Current Note</TooltipContent>
</Tooltip>
{relevantNotes.length > 0 && (
<CollapsibleTrigger asChild>
<button className="size-6 p-0 !bg-transparent border-none !shadow-none hover:!bg-interactive-hover">
{isOpen ? <ChevronUp className="size-4" /> : <ChevronDown className="size-4" />}
</button>
<Button variant="ghost2" size="icon">
{isOpen ? <ChevronUp className="size-5" /> : <ChevronDown className="size-5" />}
</Button>
</CollapsibleTrigger>
)}
</div>
</div>
{relevantNotes.length === 0 && (
<div className="flex flex-wrap gap-x-2 gap-y-1 max-h-12 overflow-y-hidden px-2">
<div className="flex flex-wrap gap-x-2 gap-y-1 max-h-12 overflow-y-hidden px-1">
<span className="text-xs text-muted">No relevant notes found</span>
</div>
)}
{!isOpen && relevantNotes.length > 0 && (
<div className="flex flex-wrap gap-x-2 gap-y-1 max-h-6 overflow-y-hidden px-2">
<div className="flex flex-wrap gap-x-2 gap-y-1 max-h-6 overflow-y-hidden px-1">
{relevantNotes.map((note) => (
<RelevantNotePopover
key={note.document.path}
@ -295,7 +290,7 @@ export const RelevantNotes = memo(
</div>
)}
<CollapsibleContent>
<div className="p-2 max-h-96 overflow-y-auto flex flex-col gap-2 @2xl:grid @2xl:grid-cols-2 @4xl:grid-cols-3">
<div className="px-1 py-2 max-h-96 overflow-y-auto flex flex-col gap-2 @2xl:grid @2xl:grid-cols-2 @4xl:grid-cols-3">
{relevantNotes.map((note) => (
<RelevantNote
showPath={!inSameFolder(activeFile?.path ?? "", note.document.path)}

View file

@ -1,5 +1,6 @@
import { useChainType } from "@/aiParams";
import { ChainType } from "@/chainFactory";
import { Button } from "@/components/ui/button";
import { Card, CardTitle, CardContent, CardHeader } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { VAULT_VECTOR_STORE_STRATEGY } from "@/constants";
@ -99,7 +100,7 @@ export const SuggestedPrompts: React.FC<SuggestedPromptsProps> = ({ onClick }) =
<CardHeader className="px-2">
<CardTitle>Suggested Prompts</CardTitle>
</CardHeader>
<CardContent className="p-2 px-2 pt-0">
<CardContent className="p-2 pt-0">
<div className="flex flex-col gap-2">
{prompts.map((prompt, i) => (
<div
@ -110,17 +111,21 @@ export const SuggestedPrompts: React.FC<SuggestedPromptsProps> = ({ onClick }) =
<div className="text-muted">{prompt.title}</div>
<div>{prompt.text}</div>
</div>
<Tooltip>
<TooltipTrigger asChild>
<button
className="size-6 p-0 !bg-transparent border-none !shadow-none hover:!bg-interactive-hover"
onClick={() => onClick(prompt.text)}
>
<PlusCircle className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent>Add to Chat</TooltipContent>
</Tooltip>
<div className="flex items-start h-full">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost2"
size="fit"
className="text-muted"
onClick={() => onClick(prompt.text)}
>
<PlusCircle className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Add to Chat</TooltipContent>
</Tooltip>
</div>
</div>
))}
</div>

View file

@ -1,25 +0,0 @@
import React from "react";
import * as Tooltip from "@radix-ui/react-tooltip";
import { PropsWithChildren } from "react";
interface Props {
onClick: () => void;
Icon: React.ReactNode;
}
export function TooltipActionButton({ onClick, Icon, children }: PropsWithChildren<Props>) {
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button className="chat-icon-button clickable-icon" onClick={onClick}>
{Icon}
</button>
</Tooltip.Trigger>
<Tooltip.Portal container={activeDocument.body}>
<Tooltip.Content sideOffset={5} className="tooltip-text">
{children}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
);
}

View file

@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0",
{
variants: {
variant: {
@ -14,14 +14,17 @@ const buttonVariants = cva(
outline:
"border border-border bg-primary shadow-sm hover:bg-interactive-accent hover:text-on-accent",
secondary: "bg-secondary text-normal shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-interactive-accent hover:text-on-accent",
ghost: "clickable-icon bg-transparent hover:bg-interactive-accent hover:text-on-accent",
link: "text-accent underline-offset-4 hover:underline",
ghost2:
"text-faint clickable-icon bg-transparent hover:bg-opacity-100 hover:text-on-accent hover:bg-transparent outline-none focus-visible:outline-none focus-visible:text-on-accent focus-visible:ring-0",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
icon: "size-7",
fit: "px-1 text-xs gap-1",
},
},
defaultVariants: {

View file

@ -0,0 +1,186 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-dropdown-hover data-[state=open]:bg-dropdown-hover [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-primary p-1 text-normal shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal container={activeDocument.body}>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border border-solid bg-primary p-1 text-normal shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-dropdown-hover focus:text-normal data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-dropdown-hover focus:text-normal data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-dropdown-hover focus:text-normal data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -100,14 +100,6 @@ If your plugin does not need CSS, delete this file.
box-sizing: border-box;
}
.chat-icons-container {
display: flex;
align-items: center;
width: 100%;
padding: 0 0 8px;
gap: 4px;
}
.chat-context-menu {
display: flex;
align-items: center;
@ -117,37 +109,6 @@ If your plugin does not need CSS, delete this file.
flex: 1; /* Take up available space */
}
.context-notes {
display: flex;
gap: 4px;
flex-wrap: wrap;
flex: 1;
}
.context-note {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
background: var(--background-secondary-alt);
border-radius: 4px;
font-size: 10px;
height: 20px;
line-height: 1;
}
.context-note.active {
background: var(--background-modifier-hover);
color: var(--text-normal);
border-left: 2px solid var(--interactive-accent);
}
.context-note.with-hover {
/* Use the same background as active note but without border */
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.note-badge {
font-size: 10px;
padding: 0 4px;
@ -269,54 +230,6 @@ If your plugin does not need CSS, delete this file.
opacity: 1;
}
.chat-input-container {
display: flex;
flex-direction: column;
width: 100%;
background: var(--background-secondary-alt);
border-radius: 4px;
padding: 4px;
gap: 2px;
}
.chat-input-textarea {
width: 100%;
min-height: 44px;
max-height: 440px;
overflow-y: auto;
resize: none;
padding: 2px 12px;
background: var(--background-secondary-alt);
border: none;
border-radius: 4px;
color: var(--text-normal);
font-size: 13px;
}
.chat-input-textarea::placeholder {
color: var(--text-muted);
opacity: 0.5;
}
.chat-input-textarea:focus {
outline: none;
border: none;
box-shadow: none;
}
.chat-input-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
}
.chat-input-left {
display: flex;
align-items: center;
gap: 12px;
}
.selected-images {
display: flex;
flex-wrap: wrap;
@ -379,11 +292,6 @@ If your plugin does not need CSS, delete this file.
height: 18px;
}
.chat-input-buttons {
display: flex;
gap: 12px;
}
.model-select-content {
min-width: 120px;
background: var(--background-secondary);
@ -567,54 +475,6 @@ If your plugin does not need CSS, delete this file.
border: 1px solid var(--background-modifier-border);
}
.message-buttons-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
.message-timestamp {
font-size: 0.7em;
color: var(--text-faint, #bbb);
opacity: 0.7;
font-style: italic;
}
.chat-message-buttons {
display: flex;
gap: 4px;
align-items: center;
height: 20px;
margin-left: auto;
}
.chat-message-buttons button {
width: 20px;
height: 20px;
padding: 0;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
opacity: 0.5;
border: none;
cursor: pointer;
color: var(--text-muted);
transition: color 0.2s ease;
}
.chat-message-buttons button:hover {
color: var(--text-normal);
opacity: 1;
}
.chat-message-buttons button svg {
width: 14px;
height: 14px;
}
.copilot-command-modal {
display: flex;
flex-direction: column;

View file

@ -65,6 +65,8 @@ module.exports = {
},
dropdown: {
DEFAULT: "var(--dropdown-background)",
blend: "var(--dropdown-background-blend-mode)",
hover: "var(--dropdown-background-hover)",
},
callout: {
warning: "rgba(var(--callout-warning),<alpha-value>)",