mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
feat: New Setting UI. (#955)
This commit is contained in:
parent
479512d389
commit
c3e62721fb
40 changed files with 3948 additions and 97 deletions
611
package-lock.json
generated
611
package-lock.json
generated
|
|
@ -19,9 +19,17 @@
|
|||
"@langchain/groq": "^0.1.2",
|
||||
"@langchain/openai": "^0.3.14",
|
||||
"@orama/orama": "^3.0.0-rc-2",
|
||||
"@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-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-tooltip": "^1.1.6",
|
||||
"@tabler/icons-react": "^2.14.0",
|
||||
"axios": "^1.3.4",
|
||||
|
|
@ -4606,6 +4614,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
|
|
@ -4633,6 +4647,122 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
|
||||
"integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
|
||||
"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-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "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/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-checkbox/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-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",
|
||||
"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-checkbox/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-collapsible": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
|
||||
|
|
@ -4816,6 +4946,204 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.3.tgz",
|
||||
"integrity": "sha512-ujGvqQNkZ0J7caQyl8XuZRj2/TIrYcOGwqz5TeD1OMcCdfBuEMP0D12ve+8J5F9XuNUth3FAKFWo/wt0E/GJrQ==",
|
||||
"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.2",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@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",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.6.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/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-dialog/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-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz",
|
||||
"integrity": "sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==",
|
||||
"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-dialog/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-dialog/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-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",
|
||||
"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-dialog/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-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
|
|
@ -4939,6 +5267,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
|
||||
"integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
|
||||
|
|
@ -5414,6 +5765,222 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"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"
|
||||
},
|
||||
"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/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-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==",
|
||||
"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.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",
|
||||
"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-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==",
|
||||
"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-select": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
|
||||
"integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@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.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.0",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.6.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-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",
|
||||
"integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.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-slider": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
||||
"integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@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.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "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-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
|
|
@ -5432,6 +5999,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "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": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
|
||||
|
|
@ -5720,6 +6316,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
|
||||
"integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
|
||||
"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-rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -80,8 +80,16 @@
|
|||
"@langchain/groq": "^0.1.2",
|
||||
"@langchain/openai": "^0.3.14",
|
||||
"@orama/orama": "^3.0.0-rc-2",
|
||||
"@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-label": "^2.1.0",
|
||||
"@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",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { CustomModel, getModelKey, ModelConfig, setModelKey } from "@/aiParams";
|
||||
import { BUILTIN_CHAT_MODELS, ChatModelProviders } from "@/constants";
|
||||
import { getDecryptedKey } from "@/encryptionService";
|
||||
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||
import { safeFetch } from "@/utils";
|
||||
import { getModelKeyFromModel, getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||
import { err2String, safeFetch } from "@/utils";
|
||||
import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { ChatCohere } from "@langchain/cohere";
|
||||
|
|
@ -78,8 +78,8 @@ export default class ChatModelManager {
|
|||
const isO1Model = modelName.startsWith("o1");
|
||||
const baseConfig: ModelConfig = {
|
||||
modelName: modelName,
|
||||
temperature: settings.temperature,
|
||||
streaming: true,
|
||||
temperature: customModel.temperature ?? settings.temperature,
|
||||
streaming: customModel.stream ?? true,
|
||||
maxRetries: 3,
|
||||
maxConcurrency: 3,
|
||||
enableCors: customModel.enableCors,
|
||||
|
|
@ -96,7 +96,7 @@ export default class ChatModelManager {
|
|||
fetch: customModel.enableCors ? safeFetch : undefined,
|
||||
},
|
||||
// @ts-ignore
|
||||
openAIOrgId: getDecryptedKey(settings.openAIOrgId),
|
||||
openAIOrgId: getDecryptedKey(customModel.openAIOrgId || settings.openAIOrgId),
|
||||
...this.handleOpenAIExtraArgs(isO1Model, settings.maxTokens, settings.temperature),
|
||||
},
|
||||
[ChatModelProviders.ANTHROPIC]: {
|
||||
|
|
@ -111,9 +111,11 @@ export default class ChatModelManager {
|
|||
},
|
||||
[ChatModelProviders.AZURE_OPENAI]: {
|
||||
azureOpenAIApiKey: await getDecryptedKey(customModel.apiKey || settings.azureOpenAIApiKey),
|
||||
azureOpenAIApiInstanceName: settings.azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName: settings.azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion: settings.azureOpenAIApiVersion,
|
||||
azureOpenAIApiInstanceName:
|
||||
customModel.azureOpenAIApiInstanceName || settings.azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName:
|
||||
customModel.azureOpenAIApiDeploymentName || settings.azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion: customModel.azureOpenAIApiVersion || settings.azureOpenAIApiVersion,
|
||||
configuration: {
|
||||
baseURL: customModel.baseUrl,
|
||||
fetch: customModel.enableCors ? safeFetch : undefined,
|
||||
|
|
@ -224,7 +226,7 @@ export default class ChatModelManager {
|
|||
const getDefaultApiKey = this.providerApiKeyMap[model.provider as ChatModelProviders];
|
||||
|
||||
const apiKey = model.apiKey || getDefaultApiKey();
|
||||
const modelKey = `${model.name}|${model.provider}`;
|
||||
const modelKey = getModelKeyFromModel(model);
|
||||
modelMap[modelKey] = {
|
||||
hasApiKey: Boolean(model.apiKey || apiKey),
|
||||
AIConstructor: constructor,
|
||||
|
|
@ -252,7 +254,7 @@ export default class ChatModelManager {
|
|||
}
|
||||
|
||||
async setChatModel(model: CustomModel): Promise<void> {
|
||||
const modelKey = `${model.name}|${model.provider}`;
|
||||
const modelKey = getModelKeyFromModel(model);
|
||||
if (!ChatModelManager.modelMap.hasOwnProperty(modelKey)) {
|
||||
throw new Error(`No model found for: ${modelKey}`);
|
||||
}
|
||||
|
|
@ -268,7 +270,7 @@ export default class ChatModelManager {
|
|||
|
||||
const modelConfig = await this.getModelConfig(model);
|
||||
|
||||
setModelKey(`${model.name}|${model.provider}`);
|
||||
setModelKey(modelKey);
|
||||
try {
|
||||
const newModelInstance = new selectedModel.AIConstructor({
|
||||
...modelConfig,
|
||||
|
|
@ -327,7 +329,7 @@ export default class ChatModelManager {
|
|||
// First try without CORS
|
||||
await tryPing(false);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (firstError) {
|
||||
console.log("First ping attempt failed, trying with CORS...");
|
||||
try {
|
||||
// Second try with CORS
|
||||
|
|
@ -337,8 +339,12 @@ export default class ChatModelManager {
|
|||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Chat model ping failed:", error);
|
||||
throw error;
|
||||
const msg =
|
||||
"\nwithout CORS Error: " +
|
||||
err2String(firstError) +
|
||||
"\nwith CORS Error: " +
|
||||
err2String(error);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { CustomModel } from "@/aiParams";
|
|||
import { BREVILABS_API_BASE_URL, EmbeddingModelProviders } from "@/constants";
|
||||
import { getDecryptedKey } from "@/encryptionService";
|
||||
import { CustomError } from "@/error";
|
||||
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||
import { safeFetch } from "@/utils";
|
||||
import { err2String, safeFetch } from "@/utils";
|
||||
import { getSettings, subscribeToSettingsChange, getModelKeyFromModel } from "@/settings/model";
|
||||
import { CohereEmbeddings } from "@langchain/cohere";
|
||||
import { Embeddings } from "@langchain/core/embeddings";
|
||||
import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
||||
|
|
@ -100,7 +100,7 @@ export default class EmbeddingManager {
|
|||
const apiKey =
|
||||
model.apiKey || this.providerApiKeyMap[model.provider as EmbeddingModelProviders]();
|
||||
|
||||
const modelKey = `${model.name}|${model.provider}`;
|
||||
const modelKey = getModelKeyFromModel(model);
|
||||
modelMap[modelKey] = {
|
||||
hasApiKey: Boolean(apiKey),
|
||||
EmbeddingConstructor: constructor,
|
||||
|
|
@ -126,7 +126,7 @@ export default class EmbeddingManager {
|
|||
// Get the custom model that matches the name and provider from the model key
|
||||
private getCustomModel(modelKey: string): CustomModel {
|
||||
return this.activeEmbeddingModels.filter((model) => {
|
||||
const key = `${model.name}|${model.provider}`;
|
||||
const key = getModelKeyFromModel(model);
|
||||
return modelKey === key;
|
||||
})[0];
|
||||
}
|
||||
|
|
@ -224,9 +224,12 @@ export default class EmbeddingManager {
|
|||
},
|
||||
[EmbeddingModelProviders.AZURE_OPENAI]: {
|
||||
azureOpenAIApiKey: await getDecryptedKey(customModel.apiKey || settings.azureOpenAIApiKey),
|
||||
azureOpenAIApiInstanceName: settings.azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName: settings.azureOpenAIApiEmbeddingDeploymentName,
|
||||
azureOpenAIApiVersion: settings.azureOpenAIApiVersion,
|
||||
azureOpenAIApiInstanceName:
|
||||
customModel.azureOpenAIApiInstanceName || settings.azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName:
|
||||
customModel.azureOpenAIApiEmbeddingDeploymentName ||
|
||||
settings.azureOpenAIApiEmbeddingDeploymentName,
|
||||
azureOpenAIApiVersion: customModel.azureOpenAIApiVersion || settings.azureOpenAIApiVersion,
|
||||
configuration: {
|
||||
baseURL: customModel.baseUrl,
|
||||
fetch: customModel.enableCors ? safeFetch : undefined,
|
||||
|
|
@ -274,7 +277,7 @@ export default class EmbeddingManager {
|
|||
// First try without CORS
|
||||
await tryPing(false);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (firstError) {
|
||||
console.log("First ping attempt failed, trying with CORS...");
|
||||
try {
|
||||
// Second try with CORS
|
||||
|
|
@ -284,8 +287,12 @@ export default class EmbeddingManager {
|
|||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Embedding model ping failed:", error);
|
||||
throw error;
|
||||
const msg =
|
||||
"\nwithout CORS Error: " +
|
||||
err2String(firstError) +
|
||||
"\nwith CORS Error: " +
|
||||
err2String(error);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,17 @@ export interface CustomModel {
|
|||
isBuiltIn?: boolean;
|
||||
enableCors?: boolean;
|
||||
core?: boolean;
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
context?: number;
|
||||
// OpenAI specific fields
|
||||
openAIOrgId?: string;
|
||||
|
||||
// Azure OpenAI specific fields
|
||||
azureOpenAIApiInstanceName?: string;
|
||||
azureOpenAIApiDeploymentName?: string;
|
||||
azureOpenAIApiVersion?: string;
|
||||
azureOpenAIApiEmbeddingDeploymentName?: string;
|
||||
}
|
||||
|
||||
export function setModelKey(modelKey: string) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomModel, useChainType, useModelKey } from "@/aiParams";
|
||||
import { useChainType, useModelKey } from "@/aiParams";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { AddImageModal } from "@/components/modals/AddImageModal";
|
||||
import { ListPromptModal } from "@/components/modals/ListPromptModal";
|
||||
|
|
@ -7,7 +7,7 @@ import { ContextProcessor } from "@/contextProcessor";
|
|||
import { CustomPromptProcessor } from "@/customPromptProcessor";
|
||||
import { COPILOT_TOOL_NAMES } from "@/LLMProviders/intentAnalyzer";
|
||||
import { Mention } from "@/mentions/Mention";
|
||||
import { useSettingsValue } from "@/settings/model";
|
||||
import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { getToolDescription } from "@/tools/toolManager";
|
||||
import { extractNoteTitles } from "@/utils";
|
||||
|
|
@ -40,8 +40,6 @@ interface ChatInputProps {
|
|||
chatHistory: ChatMessage[];
|
||||
}
|
||||
|
||||
const getModelKey = (model: CustomModel) => `${model.name}|${model.provider}`;
|
||||
|
||||
const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
||||
(
|
||||
{
|
||||
|
|
@ -447,8 +445,9 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
<div className="chat-input-left">
|
||||
<DropdownMenu.Root open={isModelDropdownOpen} onOpenChange={setIsModelDropdownOpen}>
|
||||
<DropdownMenu.Trigger className="model-select-button">
|
||||
{settings.activeModels.find((model) => getModelKey(model) === currentModelKey)
|
||||
?.name || "Select Model"}
|
||||
{settings.activeModels.find(
|
||||
(model) => getModelKeyFromModel(model) === currentModelKey
|
||||
)?.name || "Select Model"}
|
||||
<ChevronUp size={10} />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
|
|
@ -458,8 +457,8 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
.filter((model) => model.enabled)
|
||||
.map((model) => (
|
||||
<DropdownMenu.Item
|
||||
key={getModelKey(model)}
|
||||
onSelect={() => setCurrentModelKey(getModelKey(model))}
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={() => setCurrentModelKey(getModelKeyFromModel(model))}
|
||||
>
|
||||
{model.name}
|
||||
</DropdownMenu.Item>
|
||||
|
|
|
|||
50
src/components/ui/button.tsx
Normal file
50
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-interactive-accent text-on-accent shadow hover:bg-interactive-accent/90",
|
||||
destructive: "bg-modifier-error text-normal shadow-sm hover:bg-modifier-error/90",
|
||||
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",
|
||||
link: "text-accent underline-offset-4 hover:underline",
|
||||
},
|
||||
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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
28
src/components/ui/checkbox.tsx
Normal file
28
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-0 border-solid !bg-transparent !shadow transition-colors", // custom styles
|
||||
"hover:!bg-interactive-accent hover:!text-on-accent data-[state=checked]:!bg-interactive-accent data-[state=checked]:!text-on-accent", // custom styles
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-interactive-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-interactive-accent data-[state=checked]:text-on-accent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
105
src/components/ui/dialog.tsx
Normal file
105
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-modal data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"bg-overlay/50", // custom style
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
>(({ className, children, container, ...props }, ref) => (
|
||||
<DialogPortal container={container}>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-modal grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-primary p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-ring transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-interactive-accent data-[state=open]:text-muted">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"!h-9 !min-w-[50px] !border border-solid !rounded-md !bg-transparent !px-3 !py-1 md:!text-base !text-sm !transition-colors",
|
||||
"focus-visible:!shadow-sm focus-visible:!outline-none focus-visible:!ring-1 focus-visible:!ring-ring",
|
||||
"placeholder:text-xs", // custom styles
|
||||
"flex w-full shadow-sm placeholder:text-muted disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
19
src/components/ui/label.tsx
Normal file
19
src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
86
src/components/ui/password-input.tsx
Normal file
86
src/components/ui/password-input.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { getDecryptedKey } from "@/encryptionService";
|
||||
|
||||
export function PasswordInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
value?: string | number;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState<string | number>("");
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
// Handle all value transformations in a single effect
|
||||
useEffect(() => {
|
||||
const processValue = async () => {
|
||||
// Decrypt the value only on first load if it's an encrypted string
|
||||
if (isFirstLoad.current && typeof value === "string" && value) {
|
||||
try {
|
||||
const decrypted = await getDecryptedKey(value);
|
||||
setDisplayValue(decrypted);
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt value:", error);
|
||||
setDisplayValue(value);
|
||||
}
|
||||
isFirstLoad.current = false;
|
||||
} else {
|
||||
// For subsequent updates or non-encrypted values, use the value directly
|
||||
setDisplayValue(value || "");
|
||||
}
|
||||
};
|
||||
|
||||
processValue();
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={displayValue}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={cn("![padding-right:1.75rem] w-full")}
|
||||
/>
|
||||
<div
|
||||
onClick={() => !disabled && setShowPassword(!showPassword)}
|
||||
className={cn(
|
||||
"absolute right-2 top-1/2 -translate-y-1/2",
|
||||
"cursor-pointer",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
role="button"
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
"text-muted/60 hover:text-accent",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
"text-muted/60 hover:text-accent",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,15 +11,17 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
|
|||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal container={activeDocument.body}>
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal container={container ?? activeDocument.body}>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 border-border border-solid rounded-md border bg-secondary p-4 text-normal shadow-md outline-none 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",
|
||||
"z-popover w-72 rounded-md border border-border bg-primary p-4 text-normal shadow-md outline-none 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}
|
||||
|
|
|
|||
155
src/components/ui/select.tsx
Normal file
155
src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-solid !bg-dropdown !shadow-sm transition-colors",
|
||||
"focus-visible:shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
" hover:data-[state=closed]:!bg-interactive-accent hover:data-[state=closed]:!text-on-accent", // custom styles
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-border px-3 py-1 text-sm ring-offset-ring placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
>(({ className, children, position = "popper", container, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal container={container}>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-modal max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-primary 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-interactive-accent focus:text-on-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-primary-alt", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
269
src/components/ui/setting-item.tsx
Normal file
269
src/components/ui/setting-item.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { SettingSlider } from "@/components/ui/setting-slider";
|
||||
|
||||
// 定义输入控件的类型
|
||||
type InputType =
|
||||
| "text"
|
||||
| "password"
|
||||
| "number"
|
||||
| "textarea"
|
||||
| "switch"
|
||||
| "select"
|
||||
| "custom"
|
||||
| "slider"
|
||||
| "dialog";
|
||||
|
||||
// Select选项的类型
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
// 基础Props
|
||||
interface BaseSettingItemProps {
|
||||
type: InputType;
|
||||
title: string;
|
||||
description?: string | React.ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 不同类型输入控件的Props
|
||||
interface TextSettingItemProps extends BaseSettingItemProps {
|
||||
type: "text" | "password" | "number";
|
||||
value?: string | number;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface TextareaSettingItemProps extends BaseSettingItemProps {
|
||||
type: "textarea";
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
interface SwitchSettingItemProps extends BaseSettingItemProps {
|
||||
type: "switch";
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
interface SelectSettingItemProps extends BaseSettingItemProps {
|
||||
type: "select";
|
||||
value?: string | number;
|
||||
onChange?: (value: string) => void;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface CustomSettingItemProps extends BaseSettingItemProps {
|
||||
type: "custom";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// 添加 Slider 类型的 Props
|
||||
interface SliderSettingItemProps extends BaseSettingItemProps {
|
||||
type: "slider";
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
// 添加 Dialog 类型的 Props
|
||||
interface DialogSettingItemProps extends BaseSettingItemProps {
|
||||
type: "dialog";
|
||||
dialogTitle?: string;
|
||||
dialogDescription?: string;
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// 联合类型
|
||||
type SettingItemProps =
|
||||
| TextSettingItemProps
|
||||
| TextareaSettingItemProps
|
||||
| SwitchSettingItemProps
|
||||
| SelectSettingItemProps
|
||||
| CustomSettingItemProps
|
||||
| SliderSettingItemProps
|
||||
| DialogSettingItemProps;
|
||||
|
||||
export function SettingItem(props: SettingItemProps) {
|
||||
const { title, description, className, disabled } = props;
|
||||
const { modalContainer } = useTab();
|
||||
|
||||
const renderControl = () => {
|
||||
switch (props.type) {
|
||||
case "text":
|
||||
case "number":
|
||||
return (
|
||||
<Input
|
||||
type={props.type}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange?.(e.target.value)}
|
||||
placeholder={props.placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full sm:w-[200px]"
|
||||
/>
|
||||
);
|
||||
|
||||
case "password":
|
||||
return (
|
||||
<PasswordInput
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
placeholder={props.placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full sm:w-[200px]"
|
||||
/>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange?.(e.target.value)}
|
||||
placeholder={props.placeholder}
|
||||
rows={props.rows || 3}
|
||||
disabled={disabled}
|
||||
className="w-full sm:w-[300px] min-h-[80px]"
|
||||
/>
|
||||
);
|
||||
|
||||
case "switch":
|
||||
return (
|
||||
<SettingSwitch
|
||||
checked={props.checked}
|
||||
onCheckedChange={props.onCheckedChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
/*<Select
|
||||
value={props.value?.toString()}
|
||||
onValueChange={props.onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[200px]">
|
||||
<SelectValue placeholder={props.placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent container={modalContainer}>
|
||||
<SelectGroup>
|
||||
{props.options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>*/
|
||||
return (
|
||||
<div className="relative w-full sm:w-[200px] group">
|
||||
<select
|
||||
value={props.value?.toString()}
|
||||
onChange={(e) => props.onChange?.(e.target.value)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full appearance-none",
|
||||
"flex h-9 rounded-md border border-solid border-border bg-dropdown px-3 py-1 pr-8",
|
||||
"text-sm !shadow transition-colors",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"hover:bg-interactive-accent hover:text-on-accent"
|
||||
)}
|
||||
>
|
||||
{props.placeholder && (
|
||||
<option value="" disabled>
|
||||
{props.placeholder}
|
||||
</option>
|
||||
)}
|
||||
{props.options.map((option) => (
|
||||
<option key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2",
|
||||
"transition-colors group-hover:[&>svg]:text-on-accent",
|
||||
disabled && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "slider":
|
||||
return (
|
||||
<SettingSlider
|
||||
value={props.value || 0}
|
||||
onChange={props.onChange}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
disabled={disabled}
|
||||
className="w-full sm:w-[300px]"
|
||||
/>
|
||||
);
|
||||
|
||||
case "dialog":
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{props.trigger}</DialogTrigger>
|
||||
<DialogContent container={modalContainer}>
|
||||
{(props.dialogTitle || props.dialogDescription) && (
|
||||
<DialogHeader>
|
||||
{props.dialogTitle && <DialogTitle>{props.dialogTitle}</DialogTitle>}
|
||||
{props.dialogDescription && (
|
||||
<DialogDescription>{props.dialogDescription}</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
)}
|
||||
{props.children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
case "custom":
|
||||
return props.children;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col sm:flex-row items-start sm:items-center justify-between py-4 gap-4",
|
||||
"w-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1.5 w-full sm:w-[300px]">
|
||||
<div className="text-sm font-medium leading-none">{title}</div>
|
||||
{description && <div className="text-xs text-muted">{description}</div>}
|
||||
</div>
|
||||
<div className="flex-1 w-full sm:flex sm:justify-end">{renderControl()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/ui/setting-slider.tsx
Normal file
38
src/components/ui/setting-slider.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
interface SettingSliderProps {
|
||||
value: number;
|
||||
onChange?: (value: number) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SettingSlider({
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
disabled,
|
||||
className,
|
||||
}: SettingSliderProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-4", className)}>
|
||||
<Slider
|
||||
value={[value]}
|
||||
onValueChange={([value]) => onChange?.(value)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="min-w-[40px] text-sm text-right">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
src/components/ui/setting-switch.tsx
Normal file
59
src/components/ui/setting-switch.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SettingSwitchProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SettingSwitch = React.forwardRef<HTMLDivElement, SettingSwitchProps>(
|
||||
({ checked = false, onCheckedChange, disabled = false, className, ...props }, ref) => {
|
||||
const handleClick = () => {
|
||||
if (!disabled) {
|
||||
onCheckedChange?.(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (disabled) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onCheckedChange?.(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
data-state={checked ? "checked" : "unchecked"}
|
||||
data-disabled={disabled ? "" : undefined}
|
||||
ref={ref}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={cn(
|
||||
"relative inline-flex h-5.5 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
checked ? "bg-interactive-accent" : "bg-[--background-modifier-border-hover]",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-toggle-thumb shadow-lg ring-0 transition-transform",
|
||||
checked ? "translate-x-5.5" : "translate-x-0.5"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SettingSwitch.displayName = "SettingSwitch";
|
||||
|
||||
export { SettingSwitch };
|
||||
101
src/components/ui/setting-tabs.tsx
Normal file
101
src/components/ui/setting-tabs.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TabItem {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface TabItemProps {
|
||||
tab: TabItem;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
export const TabItem: React.FC<TabItemProps> = ({ tab, isSelected, onClick, isFirst, isLast }) => {
|
||||
return (
|
||||
<div
|
||||
role="tab"
|
||||
id={`tab-${tab.id}`}
|
||||
aria-controls={`tabpanel-${tab.id}`}
|
||||
aria-selected={isSelected}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex flex-row items-center",
|
||||
"h-8",
|
||||
"px-2 py-1",
|
||||
"gap-1.5",
|
||||
"cursor-pointer",
|
||||
"overflow-hidden",
|
||||
"whitespace-nowrap",
|
||||
"text-sm",
|
||||
"border border-border border-solid",
|
||||
"rounded-t-md rounded-b-[2px]",
|
||||
"bg-primary",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"hover:border-interactive-accent hover:border-b-0",
|
||||
isSelected && [
|
||||
"!bg-interactive-accent",
|
||||
"text-on-accent",
|
||||
"!px-3",
|
||||
"!max-w-full",
|
||||
"border-b-0",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"delay-200",
|
||||
],
|
||||
"lg:max-w-32",
|
||||
"md:max-w-32"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
"w-4 h-4",
|
||||
"transition-transform duration-200 ease-in-out",
|
||||
isSelected ? "opacity-100 max-w-[16px] translate-x-0" : "opacity-0 max-w-0 -translate-x-4"
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm",
|
||||
"font-medium",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"overflow-hidden whitespace-nowrap",
|
||||
"opacity-100 max-w-[100px] translate-x-0"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TabContentProps {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export const TabContent: React.FC<TabContentProps> = ({ id, children, isSelected }) => {
|
||||
if (!isSelected) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`tabpanel-${id}`}
|
||||
aria-labelledby={`tab-${id}`}
|
||||
className={cn(
|
||||
"pt-4",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
isSelected ? "opacity-100 translate-y-0" : "opacity-0 translate-y-2"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
src/components/ui/slider.tsx
Normal file
23
src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden border border-solid border-interactive-accent/30 rounded-full bg-interactive-accent/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-interactive-accent" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border bg-toggle-thumb shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
100
src/components/ui/table.tsx
Normal file
100
src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm border-collapse", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-primary-alt/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-primary-alt/50 data-[state=selected]:bg-primary-alt",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
// eslint-disable-next-line react/prop-types
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
// eslint-disable-next-line react/prop-types
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted", className)} {...props} />
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||
22
src/components/ui/textarea.tsx
Normal file
22
src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"border-solid",
|
||||
"flex min-h-[60px] w-full rounded-md border border-primary-alt bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
|
|
@ -41,6 +41,8 @@ export enum ChatModels {
|
|||
CLAUDE_3_5_HAIKU = "claude-3-5-haiku-latest",
|
||||
COMMAND_R = "command-r",
|
||||
COMMAND_R_PLUS = "command-r-plus",
|
||||
OPENROUTER_GPT_4o = "openai/chatgpt-4o-latest",
|
||||
GROQ_LLAMA_8b = "llama3-8b-8192",
|
||||
}
|
||||
|
||||
// Model Providers
|
||||
|
|
@ -210,6 +212,103 @@ export const NOMIC_EMBED_TEXT = "nomic-embed-text";
|
|||
// export const INSTRUCTOR_XL = 'hkunlp/instructor-xl'; // Inference API is off for this
|
||||
// export const MPNET_V2 = 'sentence-transformers/all-mpnet-base-v2'; // Inference API returns 400
|
||||
|
||||
export type Provider = ChatModelProviders | EmbeddingModelProviders;
|
||||
|
||||
export type DisplayKeyProviders = Exclude<
|
||||
ChatModelProviders,
|
||||
ChatModelProviders.OPENAI_FORMAT | ChatModelProviders.LM_STUDIO | ChatModelProviders.OLLAMA
|
||||
>;
|
||||
|
||||
// Provider metadata interface
|
||||
export interface ProviderMetadata {
|
||||
label: string;
|
||||
host: string;
|
||||
keyManagementURL: string;
|
||||
testModel?: ChatModels;
|
||||
}
|
||||
|
||||
// Unified provider information
|
||||
export const ProviderInfo: Record<Provider, ProviderMetadata> = {
|
||||
[ChatModelProviders.OPENAI]: {
|
||||
label: "OpenAI",
|
||||
host: "https://api.openai.com",
|
||||
keyManagementURL: "https://platform.openai.com/api-keys",
|
||||
testModel: ChatModels.GPT_4o,
|
||||
},
|
||||
[ChatModelProviders.AZURE_OPENAI]: {
|
||||
label: "Azure OpenAI",
|
||||
host: "",
|
||||
keyManagementURL: "",
|
||||
testModel: ChatModels.AZURE_OPENAI,
|
||||
},
|
||||
[ChatModelProviders.ANTHROPIC]: {
|
||||
label: "Anthropic",
|
||||
host: "https://api.anthropic.com/",
|
||||
keyManagementURL: "https://console.anthropic.com/settings/keys",
|
||||
testModel: ChatModels.CLAUDE_3_5_SONNET,
|
||||
},
|
||||
[ChatModelProviders.COHEREAI]: {
|
||||
label: "Cohere",
|
||||
host: "https://api.cohere.com",
|
||||
keyManagementURL: "https://dashboard.cohere.ai/api-keys",
|
||||
testModel: ChatModels.COMMAND_R,
|
||||
},
|
||||
[ChatModelProviders.GOOGLE]: {
|
||||
label: "Gemini",
|
||||
host: "https://generativelanguage.googleapis.com",
|
||||
keyManagementURL: "https://makersuite.google.com/app/apikey",
|
||||
testModel: ChatModels.GEMINI_FLASH,
|
||||
},
|
||||
[ChatModelProviders.OPENROUTERAI]: {
|
||||
label: "OpenRouter",
|
||||
host: "https://openrouter.ai/api/v1/",
|
||||
keyManagementURL: "https://openrouter.ai/keys",
|
||||
testModel: ChatModels.OPENROUTER_GPT_4o,
|
||||
},
|
||||
[ChatModelProviders.GROQ]: {
|
||||
label: "Groq",
|
||||
host: "https://api.groq.com/openai",
|
||||
keyManagementURL: "https://console.groq.com/keys",
|
||||
testModel: ChatModels.GROQ_LLAMA_8b,
|
||||
},
|
||||
[ChatModelProviders.OLLAMA]: {
|
||||
label: "Ollama",
|
||||
host: "http://localhost:11434/v1/",
|
||||
keyManagementURL: "",
|
||||
},
|
||||
[ChatModelProviders.LM_STUDIO]: {
|
||||
label: "LM Studio",
|
||||
host: "http://localhost:1234/v1",
|
||||
keyManagementURL: "",
|
||||
},
|
||||
[ChatModelProviders.OPENAI_FORMAT]: {
|
||||
label: "OpenAI Format",
|
||||
host: "https://api.example.com/v1",
|
||||
keyManagementURL: "",
|
||||
},
|
||||
[EmbeddingModelProviders.COPILOT_PLUS]: {
|
||||
label: "Copilot Plus",
|
||||
host: "https://api.brevilabs.com/v1",
|
||||
keyManagementURL: "",
|
||||
},
|
||||
[EmbeddingModelProviders.COPILOT_PLUS_JINA]: {
|
||||
label: "Copilot Plus",
|
||||
host: "https://api.brevilabs.com/v1",
|
||||
keyManagementURL: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Map provider to its settings key for API key
|
||||
export const ProviderSettingsKeyMap: Record<DisplayKeyProviders, keyof CopilotSettings> = {
|
||||
anthropic: "anthropicApiKey",
|
||||
openai: "openAIApiKey",
|
||||
"azure openai": "azureOpenAIApiKey",
|
||||
google: "googleApiKey",
|
||||
groq: "groqApiKey",
|
||||
openrouterai: "openRouterAiApiKey",
|
||||
cohereai: "cohereApiKey",
|
||||
};
|
||||
|
||||
export enum VAULT_VECTOR_STORE_STRATEGY {
|
||||
NEVER = "NEVER",
|
||||
ON_STARTUP = "ON STARTUP",
|
||||
|
|
|
|||
37
src/contexts/TabContext.tsx
Normal file
37
src/contexts/TabContext.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React, { createContext, useContext, useEffect, useState, useRef } from "react";
|
||||
|
||||
interface TabContextType {
|
||||
selectedTab: string;
|
||||
setSelectedTab: (tab: string) => void;
|
||||
modalContainer: HTMLElement | null;
|
||||
}
|
||||
|
||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||
|
||||
export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [selectedTab, setSelectedTab] = useState("basic");
|
||||
const [modalContainer, setModalContainer] = useState<HTMLElement | null>(null);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current) {
|
||||
const modal = document.querySelector(".modal-container") as HTMLElement;
|
||||
setModalContainer(modal);
|
||||
hasInitialized.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabContext.Provider value={{ selectedTab, setSelectedTab, modalContainer }}>
|
||||
{children}
|
||||
</TabContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTab = () => {
|
||||
const context = useContext(TabContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTab must be used within a TabProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -55,14 +55,23 @@ const generateOpacityClasses =
|
|||
|
||||
const processColorObject =
|
||||
(e: any, opacityUtilities: Record<string, any>) =>
|
||||
(colorValue: string | ColorValue, baseName: string) => {
|
||||
if (typeof colorValue === "string" && colorValue.startsWith("var(--")) {
|
||||
generateOpacityClasses(e, opacityUtilities)(colorValue, baseName);
|
||||
} else if (typeof colorValue === "object") {
|
||||
Object.entries(colorValue).forEach(([variantKey, variantValue]) => {
|
||||
if (typeof variantValue === "string" && variantValue.startsWith("var(--")) {
|
||||
const fullColorName = variantKey === "DEFAULT" ? baseName : `${baseName}-${variantKey}`;
|
||||
generateOpacityClasses(e, opacityUtilities)(variantValue, fullColorName);
|
||||
(colorValue: string | ColorValue, baseName: string, parentPath: string[] = []) => {
|
||||
const currentPath = [...parentPath, baseName];
|
||||
|
||||
if (typeof colorValue === "string") {
|
||||
if (colorValue.startsWith("var(--")) {
|
||||
if (colorValue.includes("-rgb")) {
|
||||
return;
|
||||
}
|
||||
const colorName = currentPath.join("-");
|
||||
generateOpacityClasses(e, opacityUtilities)(colorValue, colorName);
|
||||
}
|
||||
} else if (typeof colorValue === "object" && colorValue !== null) {
|
||||
Object.entries(colorValue).forEach(([key, value]) => {
|
||||
const nextBaseName = key === "DEFAULT" ? "" : key;
|
||||
const nextPath = nextBaseName ? currentPath : currentPath.slice(0, -1);
|
||||
if (value) {
|
||||
processColorObject(e, opacityUtilities)(value, nextBaseName, nextPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -70,18 +79,30 @@ const processColorObject =
|
|||
|
||||
/**
|
||||
* Tailwind plugin for adding color opacity support using color-mix
|
||||
* Supports nested color objects with variants like foreground
|
||||
* Supports deeply nested color objects and variants
|
||||
*
|
||||
* eg: bg-primary/20 -> .bg-primary\/20 { background-color: color-mix(in srgb, var(--interactive-accent) 20%, transparent); }
|
||||
* Examples:
|
||||
* bg-primary/20 -> .bg-primary\/20 { background-color: color-mix(in srgb, var(--interactive-accent) 20%, transparent); }
|
||||
* bg-modifier-error/50
|
||||
* text-background-modifier-success/30
|
||||
*/
|
||||
export const colorOpacityPlugin = plugin(function ({ addUtilities, theme, e }) {
|
||||
const opacityUtilities: Record<string, any> = {};
|
||||
const colors = theme("colors") as Record<string, string | ColorValue>;
|
||||
|
||||
// Process all colors
|
||||
Object.entries(colors).forEach(([colorName, colorValue]) => {
|
||||
processColorObject(e, opacityUtilities)(colorValue, colorName);
|
||||
});
|
||||
// 处理所有颜色相关的主题配置
|
||||
const processThemeColors = (themeKey: string, prefix?: string) => {
|
||||
const colors = theme(themeKey) as Record<string, string | ColorValue>;
|
||||
Object.entries(colors).forEach(([colorName, colorValue]) => {
|
||||
const baseName = prefix ? `${prefix}-${colorName}` : colorName;
|
||||
processColorObject(e, opacityUtilities)(colorValue, baseName);
|
||||
});
|
||||
};
|
||||
|
||||
// 处理所有颜色配置
|
||||
processThemeColors("textColor", "");
|
||||
processThemeColors("backgroundColor", "");
|
||||
processThemeColors("borderColor", "");
|
||||
processThemeColors("colors");
|
||||
|
||||
addUtilities(opacityUtilities);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { getAllQAMarkdownContent } from "@/search/searchUtils";
|
|||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
import { CopilotSettingTab } from "@/settings/SettingsPage";
|
||||
import {
|
||||
getModelKeyFromModel,
|
||||
getSettings,
|
||||
sanitizeSettings,
|
||||
setSettings,
|
||||
|
|
@ -614,11 +615,10 @@ export default class CopilotPlugin extends Plugin {
|
|||
const modelMap = new Map<string, CustomModel>();
|
||||
|
||||
// Create a unique key for each model, it's model (name + provider)
|
||||
const getModelKey = (model: CustomModel) => `${model.name}|${model.provider}`;
|
||||
|
||||
// Add or update existing models in the map
|
||||
existingActiveModels.forEach((model) => {
|
||||
const key = getModelKey(model);
|
||||
const key = getModelKeyFromModel(model);
|
||||
const existingModel = modelMap.get(key);
|
||||
if (existingModel) {
|
||||
// If it's a built-in model, preserve the built-in status
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import CopilotView from "@/components/CopilotView";
|
||||
import { CHAT_VIEWTYPE } from "@/constants";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { getSettings, updateSetting } from "@/settings/model";
|
||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { App, Notice, PluginSettingTab } from "obsidian";
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import SettingsMain from "./components/SettingsMain";
|
||||
import SettingsMainV2 from "@/settings/v2/SettingsMainV2";
|
||||
|
||||
export class CopilotSettingTab extends PluginSettingTab {
|
||||
plugin: CopilotPlugin;
|
||||
|
|
@ -44,35 +44,6 @@ export class CopilotSettingTab extends PluginSettingTab {
|
|||
const div = containerEl.createDiv("div");
|
||||
const sections = createRoot(div);
|
||||
|
||||
sections.render(<SettingsMain plugin={this.plugin} />);
|
||||
|
||||
const devModeHeader = containerEl.createEl("h1", { text: "Additional Settings" });
|
||||
devModeHeader.style.marginTop = "40px";
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Enable Encryption")
|
||||
.setDesc(
|
||||
createFragment((frag) => {
|
||||
frag.appendText("Enable encryption for the API keys.");
|
||||
})
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(getSettings().enableEncryption).onChange(async (value) => {
|
||||
updateSetting("enableEncryption", value);
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Debug mode")
|
||||
.setDesc(
|
||||
createFragment((frag) => {
|
||||
frag.appendText("Debug mode will log all API requests and prompts to the console.");
|
||||
})
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(getSettings().debug).onChange(async (value) => {
|
||||
updateSetting("debug", value);
|
||||
})
|
||||
);
|
||||
sections.render(<SettingsMainV2 plugin={this.plugin} />);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { CustomModel } from "@/aiParams";
|
|||
import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal";
|
||||
import { EmbeddingModelProviders, VAULT_VECTOR_STORE_STRATEGIES } from "@/constants";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
import { updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import React from "react";
|
||||
import {
|
||||
DropdownComponent,
|
||||
|
|
@ -68,7 +68,7 @@ const QASettings: React.FC<QASettingsProps> = ({ vectorStoreManager }) => {
|
|||
providers={Object.values(EmbeddingModelProviders)}
|
||||
onDeleteModel={(modelKey) => {
|
||||
const updatedActiveEmbeddingModels = settings.activeEmbeddingModels.filter(
|
||||
(model) => `${model.name}|${model.provider}` !== modelKey
|
||||
(model) => getModelKeyFromModel(model) !== modelKey
|
||||
);
|
||||
updateSetting("activeEmbeddingModels", updatedActiveEmbeddingModels);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ChatModelManager from "@/LLMProviders/chatModelManager";
|
|||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { App, Notice } from "obsidian";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getModelKeyFromModel } from "@/settings/model";
|
||||
|
||||
type DropdownComponentProps = {
|
||||
name: string;
|
||||
|
|
@ -303,7 +304,7 @@ const ModelSettingsComponent: React.FC<ModelSettingsComponentProps> = ({
|
|||
const [isAddModelOpen, setIsAddModelOpen] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
const getModelKey = (model: CustomModel) => `${model.name}|${model.provider}`;
|
||||
const getModelKey = getModelKeyFromModel;
|
||||
|
||||
const handleAddModel = () => {
|
||||
if (newModel.name && newModel.provider) {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,14 @@ function mergeAllActiveModelsWithCoreModels(settings: CopilotSettings): CopilotS
|
|||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique model key from a CustomModel instance
|
||||
* Format: modelName|provider
|
||||
*/
|
||||
export function getModelKeyFromModel(model: CustomModel): string {
|
||||
return `${model.name}|${model.provider}`;
|
||||
}
|
||||
|
||||
function mergeActiveModels(
|
||||
existingActiveModels: CustomModel[],
|
||||
builtInModels: CustomModel[]
|
||||
|
|
@ -172,18 +180,17 @@ function mergeActiveModels(
|
|||
const modelMap = new Map<string, CustomModel>();
|
||||
|
||||
// Create a unique key for each model, it's model (name + provider)
|
||||
const getModelKey = (model: CustomModel) => `${model.name}|${model.provider}`;
|
||||
|
||||
// Add core models to the map
|
||||
builtInModels
|
||||
.filter((model) => model.core)
|
||||
.forEach((model) => {
|
||||
modelMap.set(getModelKey(model), { ...model, core: true });
|
||||
modelMap.set(getModelKeyFromModel(model), { ...model, core: true });
|
||||
});
|
||||
|
||||
// Add or update existing models in the map
|
||||
existingActiveModels.forEach((model) => {
|
||||
const key = getModelKey(model);
|
||||
const key = getModelKeyFromModel(model);
|
||||
const existingModel = modelMap.get(key);
|
||||
if (existingModel) {
|
||||
// If it's a built-in model, preserve the built-in status
|
||||
|
|
|
|||
113
src/settings/v2/SettingsMainV2.tsx
Normal file
113
src/settings/v2/SettingsMainV2.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import React from "react";
|
||||
import { Cog, Cpu, Database, Wrench } from "lucide-react";
|
||||
import { TabContent, TabItem, type TabItem as TabItemType } from "@/components/ui/setting-tabs";
|
||||
import BasicSettings from "./components/BasicSettings";
|
||||
import ModelSettings from "./components/ModelSettings";
|
||||
import AdvancedSettings from "./components/AdvancedSettings";
|
||||
import QASettings from "./components/QASettings";
|
||||
import { TabProvider, useTab } from "@/contexts/TabContext";
|
||||
import { ResetSettingsConfirmModal } from "@/components/modals/ResetSettingsConfirmModal";
|
||||
import { resetSettings } from "@/settings/model";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const TAB_IDS = ["basic", "model", "QA", "advanced"] as const;
|
||||
type TabId = (typeof TAB_IDS)[number];
|
||||
|
||||
// tab icons
|
||||
const icons: Record<TabId, JSX.Element> = {
|
||||
basic: <Cog className="w-5 h-5" />,
|
||||
model: <Cpu className="w-5 h-5" />,
|
||||
QA: <Database className="w-5 h-5" />,
|
||||
advanced: <Wrench className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
// tab components
|
||||
const components = (plugin: CopilotPlugin): Record<TabId, React.FC> => ({
|
||||
basic: () => (
|
||||
<BasicSettings
|
||||
indexVaultToVectorStore={plugin.vectorStoreManager.indexVaultToVectorStore.bind(
|
||||
plugin.vectorStoreManager
|
||||
)}
|
||||
/>
|
||||
),
|
||||
model: () => <ModelSettings />,
|
||||
QA: () => (
|
||||
<QASettings
|
||||
indexVaultToVectorStore={plugin.vectorStoreManager.indexVaultToVectorStore.bind(
|
||||
plugin.vectorStoreManager
|
||||
)}
|
||||
/>
|
||||
),
|
||||
advanced: AdvancedSettings,
|
||||
});
|
||||
|
||||
// tabs
|
||||
const tabs: TabItemType[] = TAB_IDS.map((id) => ({
|
||||
id,
|
||||
icon: icons[id],
|
||||
label: id.charAt(0).toUpperCase() + id.slice(1),
|
||||
}));
|
||||
|
||||
const SettingsContent: React.FC<{ plugin: CopilotPlugin }> = ({ plugin }) => {
|
||||
const { selectedTab, setSelectedTab } = useTab();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="inline-flex rounded-lg">
|
||||
{tabs.map((tab, index) => (
|
||||
<TabItem
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
isSelected={selectedTab === tab.id}
|
||||
onClick={() => setSelectedTab(tab.id)}
|
||||
isFirst={index === 0}
|
||||
isLast={index === tabs.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-[100%] border border-solid" />
|
||||
|
||||
<div>
|
||||
{TAB_IDS.map((id) => {
|
||||
const Component = components(plugin)[id];
|
||||
return (
|
||||
<TabContent key={id} id={id} isSelected={selectedTab === id}>
|
||||
<Component />
|
||||
</TabContent>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SettingsMainV2Props {
|
||||
plugin: CopilotPlugin;
|
||||
}
|
||||
|
||||
const SettingsMainV2: React.FC<SettingsMainV2Props> = ({ plugin }) => (
|
||||
<TabProvider>
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
Copilot Settings <span className="text-xs">v{plugin.manifest.version}</span>
|
||||
</div>
|
||||
<div className="self-end sm:self-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => new ResetSettingsConfirmModal(app, () => resetSettings()).open()}
|
||||
>
|
||||
Reset Settings
|
||||
</Button>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<SettingsContent plugin={plugin} />
|
||||
</div>
|
||||
</TabProvider>
|
||||
);
|
||||
|
||||
export default SettingsMainV2;
|
||||
47
src/settings/v2/components/AdvancedSettings.tsx
Normal file
47
src/settings/v2/components/AdvancedSettings.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import React from "react";
|
||||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { updateSetting, useSettingsValue } from "@/settings/model";
|
||||
|
||||
const AdvancedSettings: React.FC = () => {
|
||||
const settings = useSettingsValue();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Privacy Settings Section */}
|
||||
<section>
|
||||
<SettingItem
|
||||
type="textarea"
|
||||
title="User System Prompt"
|
||||
description="Customize the system prompt for all messages, may result in unexpected behavior!"
|
||||
value={settings.userSystemPrompt}
|
||||
onChange={(value) => updateSetting("userSystemPrompt", value)}
|
||||
placeholder="Enter your system prompt here..."
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Enable Encryption"
|
||||
description="Enable encryption for the API keys."
|
||||
checked={settings.enableEncryption}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSetting("enableEncryption", checked);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Debug Mode"
|
||||
description="Debug mode will log some debug message to the console."
|
||||
checked={settings.debug}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSetting("debug", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSettings;
|
||||
206
src/settings/v2/components/ApiKeyDialog.tsx
Normal file
206
src/settings/v2/components/ApiKeyDialog.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { err2String, getProviderInfo, getProviderLabel } from "@/utils";
|
||||
import {
|
||||
ChatModelProviders,
|
||||
DisplayKeyProviders,
|
||||
EmbeddingModelProviders,
|
||||
Provider,
|
||||
ProviderInfo,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { Notice } from "obsidian";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { CopilotSettings } from "@/settings/model";
|
||||
|
||||
interface ApiKeyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
settings: Readonly<CopilotSettings>;
|
||||
updateSetting: (key: string, value: any) => void;
|
||||
modalContainer: HTMLElement | null;
|
||||
}
|
||||
|
||||
interface ProviderKeyItem {
|
||||
provider: DisplayKeyProviders;
|
||||
apiKey: string;
|
||||
isVerified: boolean;
|
||||
}
|
||||
|
||||
const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
settings,
|
||||
updateSetting,
|
||||
modalContainer,
|
||||
}) => {
|
||||
const [verifyingProviders, setVerifyingProviders] = useState<Set<DisplayKeyProviders>>(new Set());
|
||||
const [unverifiedKeys, setUnverifiedKeys] = useState<Set<DisplayKeyProviders>>(new Set());
|
||||
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
setUnverifiedKeys(new Set());
|
||||
} else {
|
||||
unverifiedKeys.forEach((provider) => {
|
||||
const settingKey = ProviderSettingsKeyMap[provider];
|
||||
updateSetting(settingKey, "");
|
||||
});
|
||||
}
|
||||
onOpenChange(isOpen);
|
||||
};
|
||||
|
||||
// List of providers to exclude
|
||||
const excludeProviders: Provider[] = [
|
||||
ChatModelProviders.OPENAI_FORMAT,
|
||||
ChatModelProviders.OLLAMA,
|
||||
ChatModelProviders.LM_STUDIO,
|
||||
ChatModelProviders.AZURE_OPENAI,
|
||||
EmbeddingModelProviders.COPILOT_PLUS,
|
||||
EmbeddingModelProviders.COPILOT_PLUS_JINA,
|
||||
];
|
||||
|
||||
// Get API key by provider
|
||||
const getApiKeyByProvider = (provider: DisplayKeyProviders): string => {
|
||||
const settingKey = ProviderSettingsKeyMap[provider];
|
||||
return (settings[settingKey] ?? "") as string;
|
||||
};
|
||||
|
||||
const providers: ProviderKeyItem[] = Object.entries(ProviderInfo)
|
||||
.filter(([key]) => !excludeProviders.includes(key as Provider))
|
||||
.map(([provider]) => {
|
||||
const providerKey = provider as DisplayKeyProviders;
|
||||
const apiKey = getApiKeyByProvider(providerKey);
|
||||
return {
|
||||
provider: providerKey,
|
||||
apiKey,
|
||||
isVerified: !!apiKey && !unverifiedKeys.has(providerKey),
|
||||
};
|
||||
});
|
||||
|
||||
const handleApiKeyChange = (provider: DisplayKeyProviders, value: string) => {
|
||||
const currentKey = getApiKeyByProvider(provider);
|
||||
if (currentKey !== value) {
|
||||
updateSetting(ProviderSettingsKeyMap[provider], value);
|
||||
setUnverifiedKeys((prev) => new Set(prev).add(provider));
|
||||
}
|
||||
};
|
||||
|
||||
const verifyApiKey = async (provider: DisplayKeyProviders, apiKey: string) => {
|
||||
setVerifyingProviders((prev) => new Set(prev).add(provider));
|
||||
try {
|
||||
if (settings.debug) console.log(`Verifying ${provider} API key:`, apiKey);
|
||||
const defaultTestModel = getProviderInfo(provider).testModel;
|
||||
|
||||
if (!defaultTestModel) {
|
||||
new Notice(
|
||||
"API key verification failed: No default test model found for the selected provider."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const customModel: CustomModel = {
|
||||
name: defaultTestModel,
|
||||
provider: provider,
|
||||
apiKey,
|
||||
enabled: true,
|
||||
};
|
||||
await ChatModelManager.getInstance().ping(customModel);
|
||||
|
||||
new Notice("API key verified successfully!");
|
||||
setUnverifiedKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(provider);
|
||||
return next;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("API key verification failed:", error);
|
||||
new Notice("API key verification failed: " + err2String(error));
|
||||
} finally {
|
||||
setVerifyingProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(provider);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent container={modalContainer} className="sm:max-w-[480px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Provider Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your AI providers by adding their API keys.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="space-y-4">
|
||||
{providers.map((item: ProviderKeyItem) => (
|
||||
<div key={item.provider} className="flex items-center gap-2">
|
||||
<div className="w-[120px] font-medium">
|
||||
<div className="truncate">{getProviderLabel(item.provider)}</div>
|
||||
{getProviderInfo(item.provider).keyManagementURL && (
|
||||
<a
|
||||
href={getProviderInfo(item.provider).keyManagementURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] text-accent hover:text-accent-hover"
|
||||
>
|
||||
Get {getProviderLabel(item.provider)} Key
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<div className="flex-1 pr-2">
|
||||
<PasswordInput
|
||||
className="w-full"
|
||||
value={item.apiKey}
|
||||
onChange={(v) => handleApiKeyChange(item.provider, v)}
|
||||
disabled={verifyingProviders.has(item.provider)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[72px]">
|
||||
{!item.isVerified ? (
|
||||
<Button
|
||||
onClick={() => verifyApiKey(item.provider, item.apiKey)}
|
||||
disabled={!item.apiKey || verifyingProviders.size > 0}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full whitespace-nowrap"
|
||||
>
|
||||
{verifyingProviders.has(item.provider) ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Verify
|
||||
</>
|
||||
) : (
|
||||
"Verify"
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-success text-sm flex items-center justify-center h-9">
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyDialog;
|
||||
371
src/settings/v2/components/BasicSettings.tsx
Normal file
371
src/settings/v2/components/BasicSettings.tsx
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
import React, { useState } from "react";
|
||||
import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight, ExternalLink, HelpCircle, Key } from "lucide-react";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { DEFAULT_OPEN_AREA } from "@/constants";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { getProviderLabel } from "@/utils";
|
||||
import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal";
|
||||
import ApiKeyDialog from "./ApiKeyDialog";
|
||||
import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||
|
||||
const ChainType2Label: Record<ChainType, string> = {
|
||||
[ChainType.LLM_CHAIN]: "Chat",
|
||||
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
|
||||
[ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus (Alpha)",
|
||||
};
|
||||
|
||||
interface BasicSettingsProps {
|
||||
indexVaultToVectorStore(overwrite?: boolean): Promise<number>;
|
||||
}
|
||||
|
||||
const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }) => {
|
||||
const { setSelectedTab, modalContainer } = useTab();
|
||||
const settings = useSettingsValue();
|
||||
const [openPopoverIds, setOpenPopoverIds] = useState<Set<string>>(new Set());
|
||||
const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false);
|
||||
|
||||
const handleSetDefaultEmbeddingModel = async (modelKey: string) => {
|
||||
if (modelKey !== settings.embeddingModelKey) {
|
||||
new RebuildIndexConfirmModal(app, async () => {
|
||||
updateSetting("embeddingModelKey", modelKey);
|
||||
await indexVaultToVectorStore(true);
|
||||
}).open();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopoverOpen = (id: string) => {
|
||||
setOpenPopoverIds((prev) => new Set([...prev, id]));
|
||||
};
|
||||
|
||||
const handlePopoverClose = (id: string) => {
|
||||
setOpenPopoverIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Copilot Plus</div>
|
||||
<div className="space-y-4">
|
||||
{/* copilot-plus */}
|
||||
<SettingItem
|
||||
type="password"
|
||||
title="License Key"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">
|
||||
Copilot Plus brings powerful AI agent capabilities
|
||||
</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("license-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("license-help");
|
||||
} else {
|
||||
handlePopoverClose("license-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("license-help")}
|
||||
onMouseLeave={() => handlePopoverClose("license-help")}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
container={modalContainer}
|
||||
className="w-[90vw] max-w-[400px] p-4 bg-primary border border-solid border-border shadow-sm"
|
||||
side="bottom"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
onMouseEnter={() => handlePopoverOpen("license-help")}
|
||||
onMouseLeave={() => handlePopoverClose("license-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-normal">
|
||||
Copilot Plus brings powerful AI agent capabilities to Obsidian.
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
Alpha access is limited to sponsors and early supporters.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted">
|
||||
Learn more at{" "}
|
||||
<a
|
||||
href="https://obsidiancopilot.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:text-accent-hover"
|
||||
>
|
||||
obsidiancopilot.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
value={settings.plusLicenseKey}
|
||||
onChange={(value) => {
|
||||
updateSetting("plusLicenseKey", value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end -mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open("https://obsidiancopilot.com", "_blank")}
|
||||
>
|
||||
Get Copilot Plus
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keys Section */}
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-4">API Keys</div>
|
||||
<div className="space-y-4">
|
||||
{/* API Key Section */}
|
||||
<SettingItem
|
||||
type="custom"
|
||||
title="API Keys"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Configure API keys for different AI providers</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("api-keys-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("api-keys-help");
|
||||
} else {
|
||||
handlePopoverClose("api-keys-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("api-keys-help")}
|
||||
onMouseLeave={() => handlePopoverClose("api-keys-help")}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
container={modalContainer}
|
||||
className="w-[90vw] max-w-[400px] p-4 bg-primary border border-solid border-border shadow-sm"
|
||||
side="bottom"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
onMouseEnter={() => handlePopoverOpen("api-keys-help")}
|
||||
onMouseLeave={() => handlePopoverClose("api-keys-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-accent">
|
||||
API key is required for chat and QA features
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
You'll need to provide an API key from your chosen provider to use the
|
||||
chat and QA functionality.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setIsApiKeyDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 w-full sm:w-auto justify-center sm:justify-start"
|
||||
>
|
||||
Set Keys
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
{/* API Key Dialog */}
|
||||
<ApiKeyDialog
|
||||
open={isApiKeyDialogOpen}
|
||||
onOpenChange={setIsApiKeyDialogOpen}
|
||||
settings={settings}
|
||||
updateSetting={updateSetting}
|
||||
modalContainer={modalContainer}
|
||||
/>
|
||||
<div className="flex justify-end -mt-2">
|
||||
<Button onClick={() => setSelectedTab("model")} variant="outline">
|
||||
More Model Settings
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* General Section */}
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">General</div>
|
||||
<div className="space-y-4">
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Default Chat Model"
|
||||
description="Select the Chat model to use"
|
||||
value={settings.defaultModelKey}
|
||||
onChange={(value) => {
|
||||
updateSetting("defaultModelKey", value);
|
||||
}}
|
||||
options={settings.activeModels
|
||||
.filter((m) => m.enabled)
|
||||
.map((model) => ({
|
||||
label: `${model.name} (${getProviderLabel(model.provider)})`,
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
placeholder="Model"
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Embedding Model"
|
||||
description="Select the Embedding model to use"
|
||||
value={settings.embeddingModelKey}
|
||||
onChange={handleSetDefaultEmbeddingModel}
|
||||
options={settings.activeEmbeddingModels
|
||||
.filter((m) => m.enabled)
|
||||
.map((model) => ({
|
||||
label: `${model.name} (${getProviderLabel(model.provider)})`,
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
placeholder="Model"
|
||||
/>
|
||||
|
||||
{/* Basic Configuration Group */}
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Default Mode"
|
||||
description="Select the default chat mode"
|
||||
value={settings.defaultChainType}
|
||||
onChange={(value) => updateSetting("defaultChainType", value as ChainType)}
|
||||
options={Object.entries(ChainType2Label).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Open Plugin In"
|
||||
description="Choose where to open the plugin"
|
||||
value={settings.defaultOpenArea}
|
||||
onChange={(value) => updateSetting("defaultOpenArea", value as DEFAULT_OPEN_AREA)}
|
||||
options={[
|
||||
{ label: "Sidebar View", value: DEFAULT_OPEN_AREA.VIEW },
|
||||
{ label: "Editor", value: DEFAULT_OPEN_AREA.EDITOR },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="text"
|
||||
title="Default Conversation Folder Name"
|
||||
description="The default folder name where chat conversations will be saved. Default is 'copilot-conversations'"
|
||||
value={settings.defaultSaveFolder}
|
||||
onChange={(value) => updateSetting("defaultSaveFolder", value)}
|
||||
placeholder="copilot-conversations"
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="text"
|
||||
title="Custom Prompts Folder Name"
|
||||
description="The default folder name where custom prompts will be saved. Default is 'copilot-custom-prompts'"
|
||||
value={settings.customPromptsFolder}
|
||||
onChange={(value) => updateSetting("customPromptsFolder", value)}
|
||||
placeholder="copilot-custom-prompts"
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="text"
|
||||
title="Default Conversation Tag"
|
||||
description="The default tag to be used when saving a conversation. Default is 'ai-conversations'"
|
||||
value={settings.defaultConversationTag}
|
||||
onChange={(value) => updateSetting("defaultConversationTag", value)}
|
||||
placeholder="ai-conversations"
|
||||
/>
|
||||
|
||||
{/* Feature Toggle Group */}
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Autosave Chat"
|
||||
description="Automatically save the chat when starting a new one or when the plugin reloads"
|
||||
checked={settings.autosaveChat}
|
||||
onCheckedChange={(checked) => updateSetting("autosaveChat", checked)}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Suggested Prompts"
|
||||
description="Show suggested prompts in the chat view"
|
||||
checked={settings.showSuggestedPrompts}
|
||||
onCheckedChange={(checked) => updateSetting("showSuggestedPrompts", checked)}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Relevant Notes"
|
||||
description="Show relevant notes in the chat view"
|
||||
checked={settings.showRelevantNotes}
|
||||
onCheckedChange={(checked) => updateSetting("showRelevantNotes", checked)}
|
||||
/>
|
||||
|
||||
{/* Advanced Configuration Group */}
|
||||
<SettingItem
|
||||
type="dialog"
|
||||
title="Command Settings"
|
||||
description="Configure chat commands"
|
||||
dialogTitle="Command Settings"
|
||||
dialogDescription="Enable or disable chat commands"
|
||||
trigger={<Button variant="outline">Manage Commands</Button>}
|
||||
>
|
||||
<div className="h-[50vh] sm:h-[400px] overflow-y-auto px-1 py-2">
|
||||
<div className="space-y-4">
|
||||
{Object.entries(settings.enabledCommands).map(([command, commandInfo]) => (
|
||||
<div
|
||||
key={command}
|
||||
className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 py-2 sm:py-0.5"
|
||||
>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<div className="text-sm font-medium">{commandInfo.name}</div>
|
||||
</div>
|
||||
<SettingSwitch
|
||||
checked={commandInfo.enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
const newEnabledCommands = {
|
||||
...settings.enabledCommands,
|
||||
[command]: {
|
||||
...commandInfo,
|
||||
enabled: checked,
|
||||
},
|
||||
};
|
||||
updateSetting("enabledCommands", newEnabledCommands);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSettings;
|
||||
496
src/settings/v2/components/ModelAddDialog.tsx
Normal file
496
src/settings/v2/components/ModelAddDialog.tsx
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import {
|
||||
ChatModelProviders,
|
||||
DisplayKeyProviders,
|
||||
EmbeddingModelProviders,
|
||||
Provider,
|
||||
ProviderMetadata,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { err2String, getProviderInfo, getProviderLabel, omit } from "@/utils";
|
||||
import { Notice } from "obsidian";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
description?: string;
|
||||
errorMessage?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const FormField: React.FC<FormFieldProps> = ({
|
||||
label,
|
||||
required = false,
|
||||
error = false,
|
||||
description,
|
||||
errorMessage = "This field is required",
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className={error ? "text-error" : ""}>
|
||||
{label} {required && <span className="text-error">*</span>}
|
||||
</Label>
|
||||
{children}
|
||||
{error && <p className="text-xs text-error">{errorMessage}</p>}
|
||||
{description && <p className="text-sm text-muted">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormErrors {
|
||||
name: boolean;
|
||||
instanceName: boolean;
|
||||
deploymentName: boolean;
|
||||
embeddingDeploymentName: boolean;
|
||||
apiVersion: boolean;
|
||||
}
|
||||
|
||||
interface ModelAddDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (model: CustomModel) => void;
|
||||
ping: (model: CustomModel) => Promise<boolean>;
|
||||
isEmbeddingModel?: boolean;
|
||||
}
|
||||
|
||||
export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onAdd,
|
||||
ping,
|
||||
isEmbeddingModel = false,
|
||||
}) => {
|
||||
const { modalContainer } = useTab();
|
||||
const settings = getSettings();
|
||||
const defaultProvider = isEmbeddingModel
|
||||
? EmbeddingModelProviders.OPENAI
|
||||
: ChatModelProviders.OPENAI;
|
||||
|
||||
const [dialogElement, setDialogElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [errors, setErrors] = useState<FormErrors>({
|
||||
name: false,
|
||||
instanceName: false,
|
||||
deploymentName: false,
|
||||
embeddingDeploymentName: false,
|
||||
apiVersion: false,
|
||||
});
|
||||
|
||||
const setError = (field: keyof FormErrors, value: boolean) => {
|
||||
setErrors((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
setErrors({
|
||||
name: false,
|
||||
instanceName: false,
|
||||
deploymentName: false,
|
||||
embeddingDeploymentName: false,
|
||||
apiVersion: false,
|
||||
});
|
||||
};
|
||||
|
||||
const validateFields = (): boolean => {
|
||||
let isValid = true;
|
||||
const newErrors = { ...errors };
|
||||
|
||||
// Validate name
|
||||
newErrors.name = !model.name;
|
||||
if (!model.name) isValid = false;
|
||||
|
||||
// Validate Azure OpenAI specific fields
|
||||
if (model.provider === ChatModelProviders.AZURE_OPENAI) {
|
||||
newErrors.instanceName = !model.azureOpenAIApiInstanceName;
|
||||
newErrors.apiVersion = !model.azureOpenAIApiVersion;
|
||||
|
||||
if (isEmbeddingModel) {
|
||||
newErrors.embeddingDeploymentName = !model.azureOpenAIApiEmbeddingDeploymentName;
|
||||
if (!model.azureOpenAIApiEmbeddingDeploymentName) isValid = false;
|
||||
} else {
|
||||
newErrors.deploymentName = !model.azureOpenAIApiDeploymentName;
|
||||
if (!model.azureOpenAIApiDeploymentName) isValid = false;
|
||||
}
|
||||
|
||||
if (!model.azureOpenAIApiInstanceName || !model.azureOpenAIApiVersion) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const getDefaultApiKey = (provider: Provider): string => {
|
||||
return (settings[ProviderSettingsKeyMap[provider as DisplayKeyProviders]] as string) || "";
|
||||
};
|
||||
|
||||
const getInitialModel = (provider = defaultProvider): CustomModel => {
|
||||
const baseModel = {
|
||||
name: "",
|
||||
provider,
|
||||
enabled: true,
|
||||
isBuiltIn: false,
|
||||
baseUrl: "",
|
||||
apiKey: getDefaultApiKey(provider),
|
||||
isEmbeddingModel,
|
||||
};
|
||||
|
||||
if (!isEmbeddingModel) {
|
||||
return {
|
||||
...baseModel,
|
||||
temperature: 0.1,
|
||||
context: 1000,
|
||||
stream: true,
|
||||
};
|
||||
}
|
||||
|
||||
return baseModel;
|
||||
};
|
||||
|
||||
const [model, setModel] = useState<CustomModel>(getInitialModel());
|
||||
const [providerInfo, setProviderInfo] = useState<ProviderMetadata>(
|
||||
getProviderInfo(defaultProvider)
|
||||
);
|
||||
|
||||
// Check if the form has required fields filled
|
||||
const isFormValid = (): boolean => {
|
||||
return Boolean(model.name && model.provider);
|
||||
};
|
||||
|
||||
// Check if buttons should be disabled
|
||||
const isButtonDisabled = (): boolean => {
|
||||
return isVerifying || !isFormValid();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!validateFields()) {
|
||||
new Notice("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(model);
|
||||
onOpenChange(false);
|
||||
setModel(getInitialModel());
|
||||
clearErrors();
|
||||
};
|
||||
|
||||
const handleProviderChange = (provider: ChatModelProviders) => {
|
||||
setProviderInfo(getProviderInfo(provider));
|
||||
setModel({
|
||||
...model,
|
||||
provider,
|
||||
apiKey: getDefaultApiKey(provider),
|
||||
...(provider === ChatModelProviders.AZURE_OPENAI
|
||||
? { openAIOrgId: settings.openAIOrgId }
|
||||
: {}),
|
||||
...(provider === ChatModelProviders.AZURE_OPENAI
|
||||
? {
|
||||
azureInstanceName: settings.azureOpenAIApiInstanceName,
|
||||
azureDeploymentName: settings.azureOpenAIApiDeploymentName,
|
||||
azureApiVersion: settings.azureOpenAIApiVersion,
|
||||
azureOpenAIApiEmbeddingDeploymentName: settings.azureOpenAIApiEmbeddingDeploymentName,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setModel(getInitialModel());
|
||||
clearErrors();
|
||||
setIsOpen(false);
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!validateFields()) {
|
||||
new Notice("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
await ping(model);
|
||||
new Notice("Model verification successful!");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const errStr = err2String(err);
|
||||
new Notice("Model verification failed: " + errStr);
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderProviderSpecificFields = () => {
|
||||
const fields = () => {
|
||||
switch (model.provider) {
|
||||
case ChatModelProviders.OPENAI:
|
||||
return (
|
||||
<FormField
|
||||
label="OpenAI Organization ID"
|
||||
description="Enter OpenAI Organization ID if applicable"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter OpenAI Organization ID if applicable"
|
||||
value={model.openAIOrgId || ""}
|
||||
onChange={(e) => setModel({ ...model, openAIOrgId: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
case ChatModelProviders.AZURE_OPENAI:
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
label="Instance Name"
|
||||
required
|
||||
error={errors.instanceName}
|
||||
errorMessage="Instance name is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter Azure OpenAI API Instance Name"
|
||||
value={model.azureOpenAIApiInstanceName || ""}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, azureOpenAIApiInstanceName: e.target.value });
|
||||
setError("instanceName", false);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{!isEmbeddingModel ? (
|
||||
<FormField
|
||||
label="Deployment Name"
|
||||
required
|
||||
error={errors.deploymentName}
|
||||
errorMessage="Deployment name is required"
|
||||
description="This is your actual model, no need to pass a model name separately."
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter Azure OpenAI API Deployment Name"
|
||||
value={model.azureOpenAIApiDeploymentName || ""}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, azureOpenAIApiDeploymentName: e.target.value });
|
||||
setError("deploymentName", false);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
) : (
|
||||
<FormField
|
||||
label="Embedding Deployment Name"
|
||||
required
|
||||
error={errors.embeddingDeploymentName}
|
||||
errorMessage="Embedding deployment name is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter Azure OpenAI API Embedding Deployment Name"
|
||||
value={model.azureOpenAIApiEmbeddingDeploymentName || ""}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, azureOpenAIApiEmbeddingDeploymentName: e.target.value });
|
||||
setError("embeddingDeploymentName", false);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
label="API Version"
|
||||
required
|
||||
error={errors.apiVersion}
|
||||
errorMessage="API version is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter Azure OpenAI API Version"
|
||||
value={model.azureOpenAIApiVersion || ""}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, azureOpenAIApiVersion: e.target.value });
|
||||
setError("apiVersion", false);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const content = fields();
|
||||
if (!content) return null;
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className="space-y-2 border rounded-lg pt-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Additional {getProviderLabel(model.provider)} Settings</Label>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="w-9 p-0">
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-4 max-h-[200px] overflow-y-auto pl-0.5 pr-2 pb-0.5">
|
||||
{content}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const getPlaceholderUrl = () => {
|
||||
if (model.provider !== ChatModelProviders.AZURE_OPENAI) {
|
||||
return providerInfo.host;
|
||||
}
|
||||
|
||||
const instanceName = model.azureOpenAIApiInstanceName || "[instance]";
|
||||
const deploymentName = isEmbeddingModel
|
||||
? model.azureOpenAIApiEmbeddingDeploymentName || "[deployment]"
|
||||
: model.azureOpenAIApiDeploymentName || "[deployment]";
|
||||
const apiVersion = model.azureOpenAIApiVersion || "[api-version]";
|
||||
const endpoint = isEmbeddingModel ? "embeddings" : "chat/completions";
|
||||
|
||||
return `https://${instanceName}.openai.azure.com/openai/deployments/${deploymentName}/${endpoint}?api-version=${apiVersion}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
className="sm:max-w-[425px]"
|
||||
container={modalContainer}
|
||||
ref={(el) => setDialogElement(el)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Custom {isEmbeddingModel ? "Embedding" : "Chat"} Model</DialogTitle>
|
||||
<DialogDescription>Add a new model to your collection.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<FormField
|
||||
label="Model Name"
|
||||
required
|
||||
error={errors.name}
|
||||
errorMessage="Model name is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={`Enter model name (e.g. ${
|
||||
isEmbeddingModel ? "text-embedding-3-small" : "gpt-4"
|
||||
})`}
|
||||
value={model.name}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, name: e.target.value });
|
||||
setError("name", false);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Provider">
|
||||
<Select value={model.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent container={dialogElement}>
|
||||
{Object.values(
|
||||
isEmbeddingModel
|
||||
? omit(EmbeddingModelProviders, ["COPILOT_PLUS", "COPILOT_PLUS_JINA"])
|
||||
: ChatModelProviders
|
||||
).map((provider) => (
|
||||
<SelectItem key={provider} value={provider}>
|
||||
{getProviderLabel(provider)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Base URL" description="Leave it blank, unless you are using a proxy.">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={getPlaceholderUrl() || "https://api.example.com/v1"}
|
||||
value={model.baseUrl || ""}
|
||||
onChange={(e) => setModel({ ...model, baseUrl: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="API Key">
|
||||
<PasswordInput
|
||||
placeholder={`Enter ${providerInfo.label} API Key`}
|
||||
value={model.apiKey || ""}
|
||||
onChange={(value) => setModel({ ...model, apiKey: value })}
|
||||
/>
|
||||
{providerInfo.keyManagementURL && (
|
||||
<p className="text-xs text-muted">
|
||||
<a href={providerInfo.keyManagementURL} target="_blank" rel="noopener noreferrer">
|
||||
Get {providerInfo.label} API Key
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{renderProviderSpecificFields()}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="enable-cors"
|
||||
checked={model.enableCors || false}
|
||||
onCheckedChange={(checked: boolean) => setModel({ ...model, enableCors: checked })}
|
||||
/>
|
||||
<Label htmlFor="enable-cors" className="text-sm">
|
||||
Enable CORS
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleAdd} disabled={isButtonDisabled()}>
|
||||
Add Model
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleVerify} disabled={isButtonDisabled()}>
|
||||
{isVerifying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Verify
|
||||
</>
|
||||
) : (
|
||||
"Verify"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
100
src/settings/v2/components/ModelEditDialog.tsx
Normal file
100
src/settings/v2/components/ModelEditDialog.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { SettingSlider } from "@/components/ui/setting-slider";
|
||||
import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||
|
||||
interface ModelEditDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
model: CustomModel | null;
|
||||
onUpdate: (model: CustomModel) => void;
|
||||
}
|
||||
|
||||
export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
model,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const { modalContainer } = useTab();
|
||||
const [localModel, setLocalModel] = useState<CustomModel | null>(model);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalModel(model);
|
||||
}, [model]);
|
||||
|
||||
if (!localModel) return null;
|
||||
|
||||
const handleUpdate = (field: keyof CustomModel, value: any) => {
|
||||
const updatedModel = {
|
||||
...localModel,
|
||||
[field]: value,
|
||||
};
|
||||
setLocalModel(updatedModel);
|
||||
onUpdate(updatedModel);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]" container={modalContainer}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Model Settings - {localModel.name}</DialogTitle>
|
||||
<DialogDescription>Customize model parameters.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
Temperature
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingSlider
|
||||
value={localModel.temperature ?? 0.1}
|
||||
onChange={(value) => handleUpdate("temperature", value)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
Context
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingSlider
|
||||
value={localModel.context ?? 1000}
|
||||
onChange={(value) => handleUpdate("context", value)}
|
||||
max={16000}
|
||||
min={0}
|
||||
step={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="py-2 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">Stream output</div>
|
||||
<SettingSwitch
|
||||
checked={localModel.stream ?? true}
|
||||
onCheckedChange={(checked) => handleUpdate("stream", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
162
src/settings/v2/components/ModelSettings.tsx
Normal file
162
src/settings/v2/components/ModelSettings.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import React, { useState } from "react";
|
||||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { setSettings, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog";
|
||||
import { ModelTable } from "@/settings/v2/components/ModelTable";
|
||||
|
||||
const ModelSettings: React.FC = () => {
|
||||
const settings = useSettingsValue();
|
||||
// const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showAddEmbeddingDialog, setShowAddEmbeddingDialog] = useState(false);
|
||||
|
||||
const onDeleteModel = (modelKey: string) => {
|
||||
const [modelName, provider] = modelKey.split("|");
|
||||
const updatedActiveModels = settings.activeModels.filter(
|
||||
(model) => !(model.name === modelName && model.provider === provider)
|
||||
);
|
||||
|
||||
let newDefaultModelKey = settings.defaultModelKey;
|
||||
if (modelKey === settings.defaultModelKey) {
|
||||
const newDefaultModel = updatedActiveModels.find((model) => model.enabled);
|
||||
newDefaultModelKey = newDefaultModel
|
||||
? `${newDefaultModel.name}|${newDefaultModel.provider}`
|
||||
: "";
|
||||
}
|
||||
|
||||
setSettings({
|
||||
activeModels: updatedActiveModels,
|
||||
defaultModelKey: newDefaultModelKey,
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelUpdate = (updatedModel: CustomModel) => {
|
||||
const updatedModels = settings.activeModels.map((m) =>
|
||||
m.name === updatedModel.name && m.provider === updatedModel.provider ? updatedModel : m
|
||||
);
|
||||
updateSetting("activeModels", updatedModels);
|
||||
};
|
||||
|
||||
const onDeleteEmbeddingModel = (modelKey: string) => {
|
||||
const [modelName, provider] = modelKey.split("|");
|
||||
const updatedModels = settings.activeEmbeddingModels.filter(
|
||||
(model) => !(model.name === modelName && model.provider === provider)
|
||||
);
|
||||
updateSetting("activeEmbeddingModels", updatedModels);
|
||||
};
|
||||
|
||||
const handleEmbeddingModelUpdate = (updatedModel: CustomModel) => {
|
||||
const updatedModels = settings.activeEmbeddingModels.map((m) =>
|
||||
m.name === updatedModel.name && m.provider === updatedModel.provider ? updatedModel : m
|
||||
);
|
||||
updateSetting("activeEmbeddingModels", updatedModels);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Chat Models</div>
|
||||
<ModelTable
|
||||
models={settings.activeModels}
|
||||
// onEdit={setEditingModel}
|
||||
onDelete={onDeleteModel}
|
||||
onAdd={() => setShowAddDialog(true)}
|
||||
onUpdateModel={handleModelUpdate}
|
||||
title="Chat Model"
|
||||
/>
|
||||
|
||||
{/* model edit dialog*/}
|
||||
{/* <ModelEditDialog
|
||||
open={!!editingModel}
|
||||
onOpenChange={(open) => !open && setEditingModel(null)}
|
||||
model={editingModel}
|
||||
onUpdate={handleModelUpdate}
|
||||
/>*/}
|
||||
|
||||
{/* model add dialog */}
|
||||
<ModelAddDialog
|
||||
open={showAddDialog}
|
||||
onOpenChange={setShowAddDialog}
|
||||
onAdd={(model) => {
|
||||
const updatedModels = [...settings.activeModels, model];
|
||||
updateSetting("activeModels", updatedModels);
|
||||
}}
|
||||
ping={(model) => ChatModelManager.getInstance().ping(model)}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingItem
|
||||
type="slider"
|
||||
title="Temperature"
|
||||
description="Default is 0.1. Higher values will result in more creativeness, but also more mistakes. Set to 0 for no randomness."
|
||||
value={settings.temperature}
|
||||
onChange={(value) => updateSetting("temperature", value)}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="slider"
|
||||
title="Token limit"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
The maximum number of <em>output tokens</em> to generate. Default is 1000.
|
||||
</p>
|
||||
<em>
|
||||
This number plus the length of your prompt (input tokens) must be smaller than the
|
||||
context window of the model.
|
||||
</em>
|
||||
</>
|
||||
}
|
||||
value={settings.maxTokens}
|
||||
onChange={(value) => updateSetting("maxTokens", value)}
|
||||
min={0}
|
||||
max={16000}
|
||||
step={100}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
type="slider"
|
||||
title="Conversation turns in context"
|
||||
description="The number of previous conversation turns to include in the context. Default is 15 turns, i.e. 30 messages."
|
||||
value={settings.contextTurns}
|
||||
onChange={(value) => updateSetting("contextTurns", value)}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Embedding Models</div>
|
||||
<ModelTable
|
||||
models={settings.activeEmbeddingModels}
|
||||
onDelete={onDeleteEmbeddingModel}
|
||||
onAdd={() => setShowAddEmbeddingDialog(true)}
|
||||
onUpdateModel={handleEmbeddingModelUpdate}
|
||||
title="Embedding Model"
|
||||
/>
|
||||
|
||||
{/* Embedding model add dialog */}
|
||||
<ModelAddDialog
|
||||
open={showAddEmbeddingDialog}
|
||||
onOpenChange={setShowAddEmbeddingDialog}
|
||||
onAdd={(model) => {
|
||||
const updatedModels = [...settings.activeEmbeddingModels, model];
|
||||
updateSetting("activeEmbeddingModels", updatedModels);
|
||||
}}
|
||||
isEmbeddingModel={true}
|
||||
ping={(model) => EmbeddingManager.getInstance().ping(model)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSettings;
|
||||
220
src/settings/v2/components/ModelTable.tsx
Normal file
220
src/settings/v2/components/ModelTable.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import { CustomModel } from "@/aiParams";
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getModelKeyFromModel } from "@/settings/model";
|
||||
import { getProviderLabel } from "@/utils";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, ChevronRight, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ModelTableProps {
|
||||
models: CustomModel[];
|
||||
onEdit?: (model: CustomModel) => void;
|
||||
onDelete: (modelKey: string) => void;
|
||||
onAdd: () => void;
|
||||
onUpdateModel: (model: CustomModel) => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ModelCard: React.FC<{
|
||||
model: CustomModel;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onUpdateModel: (model: CustomModel) => void;
|
||||
}> = ({ model, onEdit, onDelete, onUpdateModel }) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Card className="mb-2">
|
||||
<CardHeader className="p-3">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer border-l-[0] border-r-[0] border-t-[0] border-b border-solid border-border pb-2"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 stroke-[7]" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 stroke-[7]" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{model.name}</div>
|
||||
<span className="text-sm text-muted bg-secondary">
|
||||
{getProviderLabel(model.provider)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-in-out",
|
||||
isExpanded ? "max-h-20 opacity-100" : "max-h-0 opacity-0 overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="flex justify-around">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Enabled</span>
|
||||
<Checkbox
|
||||
checked={model.enabled}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">CORS</span>
|
||||
<Checkbox
|
||||
checked={model.enableCors}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enableCors: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelTable: React.FC<ModelTableProps> = ({
|
||||
models,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAdd,
|
||||
onUpdateModel,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{/* Desktop View */}
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead className="text-center">Enable</TableHead>
|
||||
<TableHead className="text-center">CORS</TableHead>
|
||||
<TableHead className="w-[100px] text-center">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{models.map((model) => (
|
||||
<TableRow
|
||||
key={getModelKeyFromModel(model)}
|
||||
className="hover:bg-interactive-accent/10"
|
||||
>
|
||||
<TableCell>{model.name}</TableCell>
|
||||
<TableCell>{getProviderLabel(model.provider)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enabled`}
|
||||
checked={model.enabled}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enabled: checked })
|
||||
}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enableCors`}
|
||||
checked={model.enableCors}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enableCors: checked })
|
||||
}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(model)}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(getModelKeyFromModel(model))}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Mobile View */}
|
||||
<div className="md:hidden">
|
||||
<div className="space-y-2">
|
||||
{models.map((model) => (
|
||||
<ModelCard
|
||||
key={getModelKeyFromModel(model)}
|
||||
model={model}
|
||||
onEdit={onEdit ? () => onEdit(model) : undefined}
|
||||
onDelete={() => onDelete(getModelKeyFromModel(model))}
|
||||
onUpdateModel={onUpdateModel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button onClick={onAdd} variant="outline" className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Custom Model
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
230
src/settings/v2/components/QASettings.tsx
Normal file
230
src/settings/v2/components/QASettings.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import React from "react";
|
||||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { VAULT_VECTOR_STORE_STRATEGIES } from "@/constants";
|
||||
import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
|
||||
interface QASettingsProps {
|
||||
indexVaultToVectorStore(overwrite?: boolean): Promise<number>;
|
||||
}
|
||||
|
||||
const QASettings: React.FC<QASettingsProps> = ({ indexVaultToVectorStore }) => {
|
||||
const { modalContainer } = useTab();
|
||||
const settings = useSettingsValue();
|
||||
const [openPopoverIds, setOpenPopoverIds] = React.useState<Set<string>>(new Set());
|
||||
|
||||
const handlePopoverOpen = (id: string) => {
|
||||
setOpenPopoverIds((prev) => new Set([...prev, id]));
|
||||
};
|
||||
|
||||
const handlePopoverClose = (id: string) => {
|
||||
setOpenPopoverIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePartitionsChange = (value: string) => {
|
||||
const numValue = parseInt(value);
|
||||
if (numValue !== settings.numPartitions) {
|
||||
new RebuildIndexConfirmModal(app, async () => {
|
||||
updateSetting("numPartitions", numValue);
|
||||
await indexVaultToVectorStore(true);
|
||||
}).open();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="space-y-4">
|
||||
{/* Auto-Index Strategy */}
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Auto-Index Strategy"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Decide when you want the vault to be indexed.</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("index-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("index-help");
|
||||
} else {
|
||||
handlePopoverClose("index-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("index-help")}
|
||||
onMouseLeave={() => handlePopoverClose("index-help")}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
container={modalContainer}
|
||||
className="w-[90vw] max-w-[400px] p-2 sm:p-3"
|
||||
side="bottom"
|
||||
align="center"
|
||||
sideOffset={0}
|
||||
onMouseEnter={() => handlePopoverOpen("index-help")}
|
||||
onMouseLeave={() => handlePopoverClose("index-help")}
|
||||
>
|
||||
<div className="space-y-2 sm:space-y-2.5">
|
||||
<div className="rounded bg-callout-warning/10 p-1.5 sm:p-2 ring-ring">
|
||||
<p className="text-callout-warning text-xs sm:text-sm">
|
||||
Warning: Cost implications for large vaults with paid models
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 sm:space-y-1.5">
|
||||
<p className="text-muted text-[11px] sm:text-xs">
|
||||
Choose when to index your vault:
|
||||
</p>
|
||||
<ul className="space-y-1 pl-2 sm:pl-3 list-disc text-[11px] sm:text-xs">
|
||||
<li>
|
||||
<strong className="inline-block whitespace-nowrap">NEVER:</strong>
|
||||
<span>Manual indexing via command or refresh only</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="inline-block whitespace-nowrap">ON STARTUP:</strong>
|
||||
<span>Index updates when plugin loads or reloads</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="inline-block whitespace-nowrap">
|
||||
ON MODE SWITCH:
|
||||
</strong>
|
||||
<span>Updates when entering QA mode (Recommended)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
value={settings.indexVaultToVectorStore}
|
||||
onChange={(value) => {
|
||||
updateSetting("indexVaultToVectorStore", value);
|
||||
}}
|
||||
options={VAULT_VECTOR_STORE_STRATEGIES.map((strategy) => ({
|
||||
label: strategy,
|
||||
value: strategy,
|
||||
}))}
|
||||
placeholder="Strategy"
|
||||
/>
|
||||
|
||||
{/* Max Sources */}
|
||||
<SettingItem
|
||||
type="slider"
|
||||
title="Max Sources"
|
||||
description="Copilot goes through your vault to find relevant blocks and passes the top N blocks to the LLM. Default for N is 3. Increase if you want more sources included in the answer generation step."
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
value={settings.maxSourceChunks}
|
||||
onChange={(value) => updateSetting("maxSourceChunks", value)}
|
||||
/>
|
||||
|
||||
{/* Requests per second */}
|
||||
<SettingItem
|
||||
type="slider"
|
||||
title="Requests per second"
|
||||
description="Default is 10. Decrease if you are rate limited by your embedding provider."
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
value={settings.embeddingRequestsPerSecond}
|
||||
onChange={(value) => updateSetting("embeddingRequestsPerSecond", value)}
|
||||
/>
|
||||
|
||||
{/* Number of Partitions */}
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Number of Partitions"
|
||||
description="Number of partitions for Copilot index. Default is 1. Increase if you have issues indexing large vaults. Warning: Changes require clearing and rebuilding the index!"
|
||||
value={settings.numPartitions.toString()}
|
||||
onChange={handlePartitionsChange}
|
||||
options={[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"12",
|
||||
"16",
|
||||
"20",
|
||||
"24",
|
||||
"28",
|
||||
"32",
|
||||
"36",
|
||||
"40",
|
||||
].map((it) => ({
|
||||
label: it,
|
||||
value: it,
|
||||
}))}
|
||||
/>
|
||||
|
||||
{/* Exclusions */}
|
||||
<SettingItem
|
||||
type="textarea"
|
||||
title="Exclusions"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
Comma separated list of paths, tags, note titles or file extension will be
|
||||
excluded from the indexing process. e.g. folder1, folder1/folder2, #tag1, #tag2,
|
||||
[[note1]], [[note2]], *.jpg, *.excallidraw.md etc,
|
||||
</p>
|
||||
<em>
|
||||
NOTE: Tags must be in the note properties, not the note content. Files which were
|
||||
previously indexed will remain in the index unless you force re-index.
|
||||
</em>
|
||||
</>
|
||||
}
|
||||
value={settings.qaExclusions}
|
||||
onChange={(value) => updateSetting("qaExclusions", value)}
|
||||
placeholder="folder1, folder1/folder2, #tag1, #tag2, [[note1]], [[note2]], *.jpg, *.excallidraw.md"
|
||||
/>
|
||||
|
||||
{/* Inclusions */}
|
||||
<SettingItem
|
||||
type="textarea"
|
||||
title="Inclusions"
|
||||
description="When specified, ONLY these paths, tags, or note titles will be indexed (comma separated). Takes precedence over exclusions. Files which were previously indexed will remain in the index unless you force re-index. Format: folder1, folder1/folder2, #tag1, #tag2, [[note1]], [[note2]]"
|
||||
value={settings.qaInclusions}
|
||||
onChange={(value) => updateSetting("qaInclusions", value)}
|
||||
placeholder="folder1, #tag1, [[note1]]"
|
||||
/>
|
||||
|
||||
{/* Enable Obsidian Sync */}
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Enable Obsidian Sync for Copilot index"
|
||||
description="If enabled, the index will be stored in the .obsidian folder and synced with Obsidian Sync by default. If disabled, it will be stored in .copilot-index folder at vault root."
|
||||
checked={settings.enableIndexSync}
|
||||
onCheckedChange={(checked) => updateSetting("enableIndexSync", checked)}
|
||||
/>
|
||||
|
||||
{/* Disable index loading on mobile */}
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Disable index loading on mobile"
|
||||
description="When enabled, Copilot index won't be loaded on mobile devices to save resources. Only chat mode will be available. Any existing index from desktop sync will be preserved. Uncheck to enable QA modes on mobile."
|
||||
checked={settings.disableIndexOnMobile}
|
||||
onCheckedChange={(checked) => updateSetting("disableIndexOnMobile", checked)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QASettings;
|
||||
30
src/utils.ts
30
src/utils.ts
|
|
@ -1,11 +1,17 @@
|
|||
import { ChainType, Document } from "@/chainFactory";
|
||||
import { NOMIC_EMBED_TEXT, USER_SENDER } from "@/constants";
|
||||
import {
|
||||
NOMIC_EMBED_TEXT,
|
||||
Provider,
|
||||
ProviderInfo,
|
||||
ProviderMetadata,
|
||||
USER_SENDER,
|
||||
} from "@/constants";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { MemoryVariables } from "@langchain/core/memory";
|
||||
import { RunnableSequence } from "@langchain/core/runnables";
|
||||
import { BaseChain, RetrievalQAChain } from "langchain/chains";
|
||||
import moment from "moment";
|
||||
import { TFile, Vault, parseYaml, requestUrl } from "obsidian";
|
||||
import { parseYaml, requestUrl, TFile, Vault } from "obsidian";
|
||||
import { CustomModel } from "./aiParams";
|
||||
|
||||
export const getModelNameFromKey = (modelKey: string): string => {
|
||||
|
|
@ -643,3 +649,23 @@ export function findCustomModel(modelKey: string, activeModels: CustomModel[]):
|
|||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
export function getProviderInfo(provider: string): ProviderMetadata {
|
||||
const info = ProviderInfo[provider as Provider];
|
||||
return {
|
||||
...info,
|
||||
label: info.label || provider,
|
||||
};
|
||||
}
|
||||
|
||||
export function getProviderLabel(provider: string): string {
|
||||
return ProviderInfo[provider as Provider]?.label || provider;
|
||||
}
|
||||
|
||||
export function getProviderHost(provider: string): string {
|
||||
return ProviderInfo[provider as Provider]?.host || "";
|
||||
}
|
||||
|
||||
export function getProviderKeyManagementURL(provider: string): string {
|
||||
return ProviderInfo[provider as Provider]?.keyManagementURL || "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ module.exports = {
|
|||
"accent-hover": "var(--text-accent-hover)",
|
||||
selection: "var(--text-selection)",
|
||||
"highlight-bg": "var(--text-highlight-bg)",
|
||||
callout: {
|
||||
warning: "rgba(var(--callout-warning),<alpha-value>)",
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
inherit: colors.inherit,
|
||||
|
|
@ -52,9 +55,6 @@ module.exports = {
|
|||
"success-rgb": "rgba(var(--background-modifier-success-rgb),<alpha-value>)",
|
||||
message: "var(--background-modifier-message)",
|
||||
"form-field": "var(--background-form-field)",
|
||||
dropdown: {
|
||||
DEFAULT: "var(--dropdown-background)",
|
||||
},
|
||||
},
|
||||
interactive: {
|
||||
normal: "var(--interactive-normal)",
|
||||
|
|
@ -63,6 +63,18 @@ module.exports = {
|
|||
"accent-hsl": "hsl(var(--interactive-accent-hsl),<alpha-value>)",
|
||||
"accent-hover": "var(--interactive-accent-hover)",
|
||||
},
|
||||
dropdown: {
|
||||
DEFAULT: "var(--dropdown-background)",
|
||||
},
|
||||
callout: {
|
||||
warning: "rgba(var(--callout-warning),<alpha-value>)",
|
||||
},
|
||||
overlay: {
|
||||
DEFAULT: "#000",
|
||||
},
|
||||
toggle: {
|
||||
thumb: "var(--toggle-thumb-color)",
|
||||
},
|
||||
},
|
||||
borderColor: {
|
||||
inherit: colors.inherit,
|
||||
|
|
@ -71,7 +83,15 @@ module.exports = {
|
|||
border: "var(--background-modifier-border)",
|
||||
"border-hover": "var(--background-modifier-border-hover)",
|
||||
"border-focus": "var(--background-modifier-border-focus)",
|
||||
"interactive-accent": "var(--interactive-accent)",
|
||||
},
|
||||
ringColor: {
|
||||
ring: "var(--interactive-accent)",
|
||||
},
|
||||
ringOffsetColor: {
|
||||
ring: "var(--interactive-accent)",
|
||||
},
|
||||
|
||||
colors: {
|
||||
inherit: colors.inherit,
|
||||
current: colors.current,
|
||||
|
|
|
|||
Loading…
Reference in a new issue