mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-17 21:32:36 +01:00
Add chrome extension pages (#3629)
This commit is contained in:
parent
76ca650972
commit
66d8b8bb10
@ -13,7 +13,6 @@ const cspHeader = `
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
${
|
||||
process.env.NEXT_PUBLIC_CLOUD_ENABLED === "true"
|
||||
? "upgrade-insecure-requests;"
|
||||
@ -27,6 +26,17 @@ const nextConfig = {
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
},
|
||||
images: {
|
||||
// Used to fetch favicons
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "www.google.com",
|
||||
port: "",
|
||||
pathname: "/s2/favicons/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@ -44,17 +54,12 @@ const nextConfig = {
|
||||
key: "Referrer-Policy",
|
||||
value: "strict-origin-when-cross-origin",
|
||||
},
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
// Deny all permissions by default
|
||||
value:
|
||||
"accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()",
|
||||
},
|
||||
|
307
web/package-lock.json
generated
307
web/package-lock.json
generated
@ -17,7 +17,9 @@
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
@ -77,6 +79,7 @@
|
||||
"devDependencies": {
|
||||
"@chromatic-com/playwright": "^0.10.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/chrome": "^0.0.287",
|
||||
"chromatic": "^11.18.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
@ -2912,6 +2915,85 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
|
||||
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/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-label/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-label/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-popover": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
|
||||
@ -3063,6 +3145,196 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz",
|
||||
"integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==",
|
||||
"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-direction": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.1.1",
|
||||
"@radix-ui/react-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-radio-group/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-radio-group/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group/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-radio-group/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"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-radio-group/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-radio-group/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-radio-group/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
|
||||
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "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-radio-group/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-roving-focus": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||
@ -4655,6 +4927,17 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.0.287",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz",
|
||||
"integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*",
|
||||
"@types/har-format": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
||||
@ -4738,6 +5021,30 @@
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/filesystem": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
|
||||
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filewriter": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/filewriter": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
|
||||
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/har-format": {
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
|
||||
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
|
@ -19,7 +19,9 @@
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
@ -79,6 +81,7 @@
|
||||
"devDependencies": {
|
||||
"@chromatic-com/playwright": "^0.10.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/chrome": "^0.0.287",
|
||||
"chromatic": "^11.18.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
|
@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NEXT_PUBLIC_CLOUD_DOMAIN } from "@/lib/constants";
|
||||
import { NEXT_PUBLIC_WEB_DOMAIN } from "@/lib/constants";
|
||||
import { ClipboardIcon } from "@/components/icons/icons";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
@ -118,7 +118,7 @@ export function AnonymousUserPath({
|
||||
<div className="flex flex-col gap-2 justify-center items-start">
|
||||
<div className="w-full flex-grow flex items-center rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm h-10">
|
||||
{NEXT_PUBLIC_CLOUD_DOMAIN}/anonymous/
|
||||
{NEXT_PUBLIC_WEB_DOMAIN}/anonymous/
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
@ -143,7 +143,7 @@ export function AnonymousUserPath({
|
||||
className="h-10 px-4"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${NEXT_PUBLIC_CLOUD_DOMAIN}/anonymous/${anonymousUserPath}`
|
||||
`${NEXT_PUBLIC_WEB_DOMAIN}/anonymous/${anonymousUserPath}`
|
||||
);
|
||||
setPopup({
|
||||
message: "Invite link copied!",
|
||||
|
106
web/src/app/auth/login/LoginPage.tsx
Normal file
106
web/src/app/auth/login/LoginPage.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { AuthTypeMetadata } from "@/lib/userSS";
|
||||
import { LoginText } from "./LoginText";
|
||||
import Link from "next/link";
|
||||
import { SignInButton } from "./SignInButton";
|
||||
import { EmailPasswordForm } from "./EmailPasswordForm";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import Title from "@/components/ui/title";
|
||||
import { useSendAuthRequiredMessage } from "@/lib/extension/utils";
|
||||
|
||||
export default function LoginPage({
|
||||
authUrl,
|
||||
authTypeMetadata,
|
||||
nextUrl,
|
||||
searchParams,
|
||||
showPageRedirect,
|
||||
}: {
|
||||
authUrl: string | null;
|
||||
authTypeMetadata: AuthTypeMetadata | null;
|
||||
nextUrl: string | null;
|
||||
searchParams:
|
||||
| {
|
||||
[key: string]: string | string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
showPageRedirect?: boolean;
|
||||
}) {
|
||||
useSendAuthRequiredMessage();
|
||||
return (
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<h2 className="text-center text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</h2>
|
||||
|
||||
<SignInButton
|
||||
authorizeUrl={authUrl}
|
||||
authType={authTypeMetadata?.authType}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "cloud" && (
|
||||
<div className="mt-4 w-full justify-center">
|
||||
<div className="flex items-center w-full my-4">
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
<span className="px-4 text-gray-500">or</span>
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
</div>
|
||||
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
||||
|
||||
<div className="flex mt-4 justify-between">
|
||||
<Link
|
||||
href={`/auth/signup${
|
||||
searchParams?.next ? `?next=${searchParams.next}` : ""
|
||||
}`}
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
|
||||
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "basic" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</Title>
|
||||
</div>
|
||||
<EmailPasswordForm nextUrl={nextUrl} />
|
||||
<div className="flex flex-col gap-y-2 items-center"></div>
|
||||
</>
|
||||
)}
|
||||
{showPageRedirect && (
|
||||
<p className="text-center mt-4">
|
||||
Don't have an account?{" "}
|
||||
<span
|
||||
onClick={() => {
|
||||
if (typeof window !== "undefined" && window.top) {
|
||||
window.top.location.href = "/auth/signup";
|
||||
} else {
|
||||
window.location.href = "/auth/signup";
|
||||
}
|
||||
}}
|
||||
className="text-link font-medium cursor-pointer"
|
||||
>
|
||||
Create an account
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -46,7 +46,7 @@ export function SignInButton({
|
||||
|
||||
return (
|
||||
<a
|
||||
className="mx-auto mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
|
||||
className="mx-auto mb-4 mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
|
||||
href={finalAuthorizeUrl}
|
||||
>
|
||||
{button}
|
||||
|
@ -7,18 +7,8 @@ import {
|
||||
AuthTypeMetadata,
|
||||
} from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SignInButton } from "./SignInButton";
|
||||
import { EmailPasswordForm } from "./EmailPasswordForm";
|
||||
import Title from "@/components/ui/title";
|
||||
import Text from "@/components/ui/text";
|
||||
import Link from "next/link";
|
||||
import { LoginText } from "./LoginText";
|
||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { useContext } from "react";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
const Page = async (props: {
|
||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
@ -49,13 +39,7 @@ const Page = async (props: {
|
||||
}
|
||||
|
||||
// if user is already logged in, take them to the main app page
|
||||
const secondsTillExpiration = getSecondsUntilExpiration(currentUser);
|
||||
if (
|
||||
currentUser &&
|
||||
currentUser.is_active &&
|
||||
!currentUser.is_anonymous_user &&
|
||||
(secondsTillExpiration === null || secondsTillExpiration > 0)
|
||||
) {
|
||||
if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
|
||||
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
@ -83,55 +67,12 @@ const Page = async (props: {
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<h2 className="text-center text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</h2>
|
||||
|
||||
<SignInButton
|
||||
authorizeUrl={authUrl}
|
||||
authType={authTypeMetadata?.authType}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "cloud" && (
|
||||
<div className="mt-4 w-full justify-center">
|
||||
<div className="flex items-center w-full my-4">
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
<span className="px-4 text-gray-500">or</span>
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
||||
|
||||
<div className="flex mt-4 justify-between">
|
||||
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "basic" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</Title>
|
||||
</div>
|
||||
<EmailPasswordForm nextUrl={nextUrl} />
|
||||
<div className="flex flex-col gap-y-2 items-center"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<LoginPage
|
||||
authUrl={authUrl}
|
||||
authTypeMetadata={authTypeMetadata}
|
||||
nextUrl={nextUrl!}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</AuthFlowContainer>
|
||||
</div>
|
||||
);
|
||||
|
@ -14,7 +14,7 @@ export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
|
||||
<div
|
||||
onMouseEnter={() => setHoveredAssistant(true)}
|
||||
onMouseLeave={() => setHoveredAssistant(false)}
|
||||
className="p-4 scale-[.7] cursor-pointer border-dashed rounded-full flex border border-gray-300 border-2 border-dashed"
|
||||
className="mobile:hidden p-4 scale-[.7] cursor-pointer border-dashed rounded-full flex border border-gray-300 border-2 border-dashed"
|
||||
>
|
||||
<AssistantIcon
|
||||
disableToolip
|
||||
@ -22,7 +22,7 @@ export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
|
||||
assistant={selectedPersona}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-full mr-1 w-[300px] top-0">
|
||||
<div className="absolute right-full mr-1 mobile:mr-0 w-[300px] top-0">
|
||||
{hoveredAssistant && (
|
||||
<DisplayAssistantCard selectedPersona={selectedPersona} />
|
||||
)}
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
BackendChatSession,
|
||||
BackendMessage,
|
||||
@ -52,13 +52,11 @@ import {
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { SEARCH_PARAM_NAMES, shouldSubmitOnLoad } from "./searchParams";
|
||||
import { useDocumentSelection } from "./useDocumentSelection";
|
||||
import { LlmOverride, useFilters, useLlmOverride } from "@/lib/hooks";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { ChatState, FeedbackType, RegenerationState } from "./types";
|
||||
import { ChatFilters } from "./documentSidebar/ChatFilters";
|
||||
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
|
||||
@ -110,7 +108,11 @@ import AssistantBanner from "../../components/assistants/AssistantBanner";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import AssistantSelector from "@/components/chat_search/AssistantSelector";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { createPostponedAbortSignal } from "next/dist/server/app-render/dynamic-rendering";
|
||||
import { useSendMessageToParent } from "@/lib/extension/utils";
|
||||
import {
|
||||
CHROME_MESSAGE,
|
||||
SUBMIT_MESSAGE_TYPES,
|
||||
} from "@/lib/extension/constants";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@ -120,10 +122,12 @@ export function ChatPage({
|
||||
toggle,
|
||||
documentSidebarInitialWidth,
|
||||
toggledSidebar,
|
||||
firstMessage,
|
||||
}: {
|
||||
toggle: (toggled?: boolean) => void;
|
||||
documentSidebarInitialWidth?: number;
|
||||
toggledSidebar: boolean;
|
||||
firstMessage?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@ -140,6 +144,7 @@ export function ChatPage({
|
||||
shouldShowWelcomeModal,
|
||||
refreshChatSessions,
|
||||
} = useChatContext();
|
||||
|
||||
function useScreenSize() {
|
||||
const [screenSize, setScreenSize] = useState({
|
||||
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
||||
@ -192,9 +197,6 @@ export function ChatPage({
|
||||
const { user, isAdmin } = useUser();
|
||||
const slackChatId = searchParams.get("slackChatId");
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
|
||||
);
|
||||
|
||||
const modelVersionFromSearchParams = searchParams.get(
|
||||
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
||||
@ -210,24 +212,34 @@ export function ChatPage({
|
||||
toggle(false);
|
||||
}
|
||||
}, [user]);
|
||||
// Effect to handle sendOnLoad
|
||||
useEffect(() => {
|
||||
if (sendOnLoad) {
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);
|
||||
|
||||
// Update the URL without the send-on-load parameter
|
||||
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
||||
const processSearchParamsAndSubmitMessage = (searchParamsString: string) => {
|
||||
const newSearchParams = new URLSearchParams(searchParamsString);
|
||||
const message = newSearchParams.get("user-prompt");
|
||||
|
||||
// Update our local state to reflect the change
|
||||
setSendOnLoad(null);
|
||||
filterManager.buildFiltersFromQueryString(
|
||||
newSearchParams.toString(),
|
||||
availableSources,
|
||||
documentSets.map((ds) => ds.name),
|
||||
tags
|
||||
);
|
||||
|
||||
// If there's a message, submit it
|
||||
if (message) {
|
||||
onSubmit({ messageOverride: message });
|
||||
}
|
||||
const fileDescriptorString = newSearchParams.get(SEARCH_PARAM_NAMES.FILES);
|
||||
const overrideFileDescriptors: FileDescriptor[] = fileDescriptorString
|
||||
? JSON.parse(decodeURIComponent(fileDescriptorString))
|
||||
: [];
|
||||
|
||||
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);
|
||||
|
||||
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
||||
|
||||
// If there's a message, submit it
|
||||
if (message) {
|
||||
console.log("SUBMITTING MESSAGE");
|
||||
setSubmittedMessage(message);
|
||||
onSubmit({ messageOverride: message, overrideFileDescriptors });
|
||||
}
|
||||
}, [sendOnLoad, searchParams, router]);
|
||||
};
|
||||
|
||||
const existingChatSessionId = existingChatIdRaw ? existingChatIdRaw : null;
|
||||
|
||||
@ -312,14 +324,6 @@ export function ChatPage({
|
||||
const noAssistants = liveAssistant == null || liveAssistant == undefined;
|
||||
|
||||
const availableSources = ccPairs.map((ccPair) => ccPair.source);
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona: availableAssistants.find(
|
||||
(assistant) => assistant.id === liveAssistant?.id
|
||||
),
|
||||
availableSources: availableSources,
|
||||
availableDocumentSets: documentSets,
|
||||
});
|
||||
|
||||
// always set the model override for the chat session, when an assistant, llm provider, or user preference exists
|
||||
useEffect(() => {
|
||||
@ -399,8 +403,6 @@ export function ChatPage({
|
||||
setIsReady(true);
|
||||
}, []);
|
||||
|
||||
// this is triggered every time the user switches which chat
|
||||
// session they are using
|
||||
useEffect(() => {
|
||||
const priorChatSessionId = chatSessionIdRef.current;
|
||||
const loadedSessionId = loadedIdSessionRef.current;
|
||||
@ -456,7 +458,6 @@ export function ChatPage({
|
||||
}
|
||||
return;
|
||||
}
|
||||
setIsReady(true);
|
||||
const shouldScrollToBottom =
|
||||
visibleRange.get(existingChatSessionId) === undefined ||
|
||||
visibleRange.get(existingChatSessionId)?.end == 0;
|
||||
@ -651,10 +652,10 @@ export function ChatPage({
|
||||
currentMessageMap(completeMessageDetail)
|
||||
);
|
||||
|
||||
const [submittedMessage, setSubmittedMessage] = useState("");
|
||||
const [submittedMessage, setSubmittedMessage] = useState(firstMessage || "");
|
||||
|
||||
const [chatState, setChatState] = useState<Map<string | null, ChatState>>(
|
||||
new Map([[chatSessionIdRef.current, "input"]])
|
||||
new Map([[chatSessionIdRef.current, firstMessage ? "loading" : "input"]])
|
||||
);
|
||||
|
||||
const [regenerationState, setRegenerationState] = useState<
|
||||
@ -798,6 +799,19 @@ export function ChatPage({
|
||||
}
|
||||
}, [defaultAssistantId, availableAssistants, messageHistory.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
submittedMessage &&
|
||||
currentSessionChatState === "loading" &&
|
||||
messageHistory.length == 0
|
||||
) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.LOAD_NEW_CHAT_PAGE },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}, [submittedMessage, currentSessionChatState]);
|
||||
|
||||
const [
|
||||
selectedDocuments,
|
||||
toggleDocumentSelection,
|
||||
@ -997,12 +1011,32 @@ export function ChatPage({
|
||||
}
|
||||
}, [chatSessionIdRef.current]);
|
||||
|
||||
const loadNewPageLogic = (event: MessageEvent) => {
|
||||
if (event.data.type === SUBMIT_MESSAGE_TYPES.PAGE_CHANGE) {
|
||||
try {
|
||||
const url = new URL(event.data.href);
|
||||
processSearchParamsAndSubmitMessage(url.searchParams.toString());
|
||||
} catch (error) {
|
||||
console.error("Error parsing URL:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Equivalent to `loadNewPageLogic`
|
||||
useEffect(() => {
|
||||
adjustDocumentSidebarWidth(); // Adjust the width on initial render
|
||||
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
|
||||
if (searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) {
|
||||
processSearchParamsAndSubmitMessage(searchParams.toString());
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
useEffect(() => {
|
||||
adjustDocumentSidebarWidth();
|
||||
window.addEventListener("resize", adjustDocumentSidebarWidth);
|
||||
window.addEventListener("message", loadNewPageLogic);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", adjustDocumentSidebarWidth); // Cleanup the event listener
|
||||
window.removeEventListener("message", loadNewPageLogic);
|
||||
window.removeEventListener("resize", adjustDocumentSidebarWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -1078,6 +1112,7 @@ export function ChatPage({
|
||||
alternativeAssistantOverride = null,
|
||||
modelOverRide,
|
||||
regenerationRequest,
|
||||
overrideFileDescriptors,
|
||||
}: {
|
||||
messageIdToResend?: number;
|
||||
messageOverride?: string;
|
||||
@ -1087,6 +1122,7 @@ export function ChatPage({
|
||||
alternativeAssistantOverride?: Persona | null;
|
||||
modelOverRide?: LlmOverride;
|
||||
regenerationRequest?: RegenerationRequest | null;
|
||||
overrideFileDescriptors?: FileDescriptor[];
|
||||
} = {}) => {
|
||||
let frozenSessionId = currentSessionId();
|
||||
updateCanContinue(false, frozenSessionId);
|
||||
@ -1113,6 +1149,7 @@ export function ChatPage({
|
||||
|
||||
let currChatSessionId: string;
|
||||
const isNewSession = chatSessionIdRef.current === null;
|
||||
|
||||
const searchParamBasedChatSessionName =
|
||||
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
|
||||
|
||||
@ -1228,7 +1265,7 @@ export function ChatPage({
|
||||
signal: controller.signal, // Add this line
|
||||
message: currMessage,
|
||||
alternateAssistantId: currentAssistantId,
|
||||
fileDescriptors: currentMessageFiles,
|
||||
fileDescriptors: overrideFileDescriptors || currentMessageFiles,
|
||||
parentMessageId:
|
||||
regenerationRequest?.parentMessage.messageId ||
|
||||
lastSuccessfulMessageId,
|
||||
@ -1815,6 +1852,7 @@ export function ChatPage({
|
||||
end: 0,
|
||||
mostVisibleMessageId: null,
|
||||
};
|
||||
useSendMessageToParent();
|
||||
|
||||
useEffect(() => {
|
||||
if (noAssistants) {
|
||||
@ -1889,6 +1927,7 @@ export function ChatPage({
|
||||
|
||||
handleSlackChatRedirect();
|
||||
}, [searchParams, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
@ -1957,6 +1996,10 @@ export function ChatPage({
|
||||
});
|
||||
};
|
||||
}
|
||||
if (!user) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
if (noAssistants)
|
||||
return (
|
||||
<>
|
||||
@ -2039,7 +2082,11 @@ export function ChatPage({
|
||||
|
||||
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
|
||||
<div className="md:hidden">
|
||||
<Modal noPadding noScroll>
|
||||
<Modal
|
||||
onOutsideClick={() => setDocumentSidebarToggled(false)}
|
||||
noPadding
|
||||
noScroll
|
||||
>
|
||||
<ChatFilters
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
modal={true}
|
||||
@ -2247,7 +2294,11 @@ export function ChatPage({
|
||||
)}
|
||||
|
||||
{documentSidebarInitialWidth !== undefined && isReady ? (
|
||||
<Dropzone onDrop={handleImageUpload} noClick>
|
||||
<Dropzone
|
||||
key={currentSessionId()}
|
||||
onDrop={handleImageUpload}
|
||||
noClick
|
||||
>
|
||||
{({ getRootProps }) => (
|
||||
<div className="flex h-full w-full">
|
||||
{!settings?.isMobile && (
|
||||
@ -2327,7 +2378,8 @@ export function ChatPage({
|
||||
{messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages &&
|
||||
currentSessionChatState == "input" &&
|
||||
!loadingError && (
|
||||
!loadingError &&
|
||||
!submittedMessage && (
|
||||
<div className="h-full w-[95%] mx-auto mt-12 flex flex-col justify-center items-center">
|
||||
<ChatIntro selectedPersona={liveAssistant} />
|
||||
|
||||
@ -2344,7 +2396,7 @@ export function ChatPage({
|
||||
currentSessionChatState == "input" &&
|
||||
!loadingError &&
|
||||
allAssistants.length > 1 && (
|
||||
<div className="mx-auto px-4 w-full max-w-[750px] flex flex-col items-center">
|
||||
<div className="mobile:hidden mx-auto px-4 w-full max-w-[750px] flex flex-col items-center">
|
||||
<Separator className="mx-2 w-full my-12" />
|
||||
<div className="text-sm text-black font-medium mb-4">
|
||||
Recent Assistants
|
||||
@ -2362,8 +2414,9 @@ export function ChatPage({
|
||||
)}
|
||||
|
||||
<div
|
||||
key={currentSessionId()}
|
||||
className={
|
||||
"-ml-4 w-full mx-auto " +
|
||||
"desktop:-ml-4 w-full mx-auto " +
|
||||
"absolute mobile:top-0 desktop:top-12 left-0 " +
|
||||
(settings?.enterpriseSettings
|
||||
?.two_lines_for_chat_header
|
||||
@ -2774,6 +2827,7 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
<ChatInputBar
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
removeDocs={() => {
|
||||
clearSelectedDocuments();
|
||||
}}
|
||||
@ -2786,7 +2840,6 @@ export function ChatPage({
|
||||
}
|
||||
chatState={currentSessionChatState}
|
||||
stopGenerating={stopGenerating}
|
||||
openModelSettings={() => setSettingsToggled(true)}
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
selectedAssistant={liveAssistant}
|
||||
@ -2796,7 +2849,6 @@ export function ChatPage({
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onSubmit={onSubmit}
|
||||
filterManager={filterManager}
|
||||
files={currentMessageFiles}
|
||||
setFiles={setCurrentMessageFiles}
|
||||
toggleFilters={
|
||||
|
@ -4,14 +4,20 @@ import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper";
|
||||
|
||||
export default function WrappedChat({
|
||||
initiallyToggled,
|
||||
firstMessage,
|
||||
}: {
|
||||
initiallyToggled: boolean;
|
||||
firstMessage?: string;
|
||||
}) {
|
||||
return (
|
||||
<FunctionalWrapper
|
||||
initiallyToggled={initiallyToggled}
|
||||
content={(toggledSidebar, toggle) => (
|
||||
<ChatPage toggle={toggle} toggledSidebar={toggledSidebar} />
|
||||
<ChatPage
|
||||
toggle={toggle}
|
||||
toggledSidebar={toggledSidebar}
|
||||
firstMessage={firstMessage}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
@ -79,7 +79,9 @@ export function ChatDocumentDisplay({
|
||||
document.updated_at || Object.keys(document.metadata).length > 0;
|
||||
return (
|
||||
<div
|
||||
className={`max-w-[400px] opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}
|
||||
className={`desktop:max-w-[400px] opacity-100 ${
|
||||
modal ? "w-[90vw]" : "w-full"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex relative flex-col gap-0.5 rounded-xl mx-2 my-1 ${
|
||||
|
@ -3,7 +3,7 @@ import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { FilterManager, LlmOverrideManager } from "@/lib/hooks";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { getFinalLLM } from "@/lib/llm/utils";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
@ -31,13 +31,73 @@ import { ChatState } from "../types";
|
||||
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { XIcon } from "lucide-react";
|
||||
import FiltersDisplay from "./FilterDisplay";
|
||||
import { fetchTitleFromUrl } from "@/lib/sources";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
const SelectedUrlChip = ({
|
||||
url,
|
||||
onRemove,
|
||||
}: {
|
||||
url: string;
|
||||
onRemove: (url: string) => void;
|
||||
}) => (
|
||||
<div className="bg-white border border-gray-200 shadow-sm rounded-lg p-2 flex items-center space-x-2">
|
||||
<img
|
||||
src={`https://www.google.com/s2/favicons?domain=${new URL(url).hostname}`}
|
||||
alt="Website favicon"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<p className="text-sm font-medium text-gray-700 truncate">
|
||||
{new URL(url).hostname}
|
||||
</p>
|
||||
<XIcon
|
||||
onClick={() => onRemove(url)}
|
||||
size={16}
|
||||
className="text-text-400 hover:text-text-600 ml-auto cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SentUrlChip = ({
|
||||
url,
|
||||
onRemove,
|
||||
onClick,
|
||||
title,
|
||||
}: {
|
||||
url: string;
|
||||
onRemove: (url: string) => void;
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className="bg-white/80 opacity-50 group-hover:opacity-100 border border-gray-200/50 shadow-sm rounded-lg p-2 flex items-center space-x-2 hover:bg-white hover:border-gray-200 transition-all duration-200"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
src={`https://www.google.com/s2/favicons?domain=${
|
||||
new URL(url).hostname
|
||||
}`}
|
||||
alt="Website favicon"
|
||||
className="w-4 h-4 "
|
||||
/>
|
||||
<p className="text-sm font-medium text-gray-600 truncate group-hover:text-gray-700">
|
||||
{title}
|
||||
</p>
|
||||
<XIcon
|
||||
onClick={(e) => {
|
||||
onRemove(url);
|
||||
}}
|
||||
size={16}
|
||||
className="text-text-300 hover:text-text-500 ml-auto transition-colors duration-200"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatInputBarProps {
|
||||
removeDocs: () => void;
|
||||
openModelSettings: () => void;
|
||||
showDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
selectedDocuments: OnyxDocument[];
|
||||
@ -45,7 +105,7 @@ interface ChatInputBarProps {
|
||||
setMessage: (message: string) => void;
|
||||
stopGenerating: () => void;
|
||||
onSubmit: () => void;
|
||||
filterManager: FilterManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
chatState: ChatState;
|
||||
alternativeAssistant: Persona | null;
|
||||
// assistants
|
||||
@ -61,7 +121,6 @@ interface ChatInputBarProps {
|
||||
|
||||
export function ChatInputBar({
|
||||
removeDocs,
|
||||
openModelSettings,
|
||||
showDocs,
|
||||
showConfigureAPIKey,
|
||||
selectedDocuments,
|
||||
@ -69,7 +128,6 @@ export function ChatInputBar({
|
||||
setMessage,
|
||||
stopGenerating,
|
||||
onSubmit,
|
||||
filterManager,
|
||||
chatState,
|
||||
|
||||
// assistants
|
||||
@ -408,7 +466,7 @@ export function ChatInputBar({
|
||||
style={{ scrollbarWidth: "thin" }}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Ask me anything.."
|
||||
placeholder="Ask me anything..."
|
||||
value={message}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
@ -453,16 +511,6 @@ export function ChatInputBar({
|
||||
onClick={toggleFilters}
|
||||
/>
|
||||
)}
|
||||
{(filterManager.selectedSources.length > 0 ||
|
||||
filterManager.selectedDocumentSets.length > 0 ||
|
||||
filterManager.selectedTags.length > 0 ||
|
||||
filterManager.timeRange) &&
|
||||
toggleFilters && (
|
||||
<FiltersDisplay
|
||||
filterManager={filterManager}
|
||||
toggleFilters={toggleFilters}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||
|
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal file
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal file
@ -0,0 +1,244 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { FiPlusCircle } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import {
|
||||
InputBarPreview,
|
||||
InputBarPreviewImageProvider,
|
||||
} from "../files/InputBarPreview";
|
||||
import { SendIcon } from "@/components/icons/icons";
|
||||
import { HorizontalSourceSelector } from "@/components/search/filtering/HorizontalSourceSelector";
|
||||
import { Tag } from "@/lib/types";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
interface ChatInputBarProps {
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
onSubmit: () => void;
|
||||
files: FileDescriptor[];
|
||||
setFiles: (files: FileDescriptor[]) => void;
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
filterManager?: FilterManager;
|
||||
existingSources: string[];
|
||||
availableDocumentSets: { name: string }[];
|
||||
availableTags: Tag[];
|
||||
}
|
||||
|
||||
export function SimplifiedChatInputBar({
|
||||
message,
|
||||
setMessage,
|
||||
onSubmit,
|
||||
files,
|
||||
setFiles,
|
||||
handleFileUpload,
|
||||
textAreaRef,
|
||||
filterManager,
|
||||
existingSources,
|
||||
availableDocumentSets,
|
||||
availableTags,
|
||||
}: ChatInputBarProps) {
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "0px";
|
||||
textarea.style.height = `${Math.min(
|
||||
textarea.scrollHeight,
|
||||
MAX_INPUT_HEIGHT
|
||||
)}px`;
|
||||
}
|
||||
}, [message, textAreaRef]);
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (items) {
|
||||
const pastedFiles = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === "file") {
|
||||
const file = items[i].getAsFile();
|
||||
if (file) pastedFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (pastedFiles.length > 0) {
|
||||
event.preventDefault();
|
||||
handleFileUpload(pastedFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const text = event.target.value;
|
||||
setMessage(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="onyx-chat-input"
|
||||
className="
|
||||
w-full
|
||||
relative
|
||||
mx-auto
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
opacity-100
|
||||
w-full
|
||||
h-fit
|
||||
flex
|
||||
flex-col
|
||||
border
|
||||
border-[#E5E7EB]
|
||||
rounded-lg
|
||||
relative
|
||||
text-text-chatbar
|
||||
bg-background-chatbar
|
||||
[&:has(textarea:focus)]::ring-1
|
||||
[&:has(textarea:focus)]::ring-black
|
||||
"
|
||||
>
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-x-2 px-2 pt-2">
|
||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||
{files.map((file) => (
|
||||
<div className="flex-none" key={file.id}>
|
||||
{file.type === ChatFileType.IMAGE ? (
|
||||
<InputBarPreviewImageProvider
|
||||
file={file}
|
||||
onDelete={() => {
|
||||
setFiles(
|
||||
files.filter(
|
||||
(fileInFilter) => fileInFilter.id !== file.id
|
||||
)
|
||||
);
|
||||
}}
|
||||
isUploading={file.isUploading || false}
|
||||
/>
|
||||
) : (
|
||||
<InputBarPreview
|
||||
file={file}
|
||||
onDelete={() => {
|
||||
setFiles(
|
||||
files.filter(
|
||||
(fileInFilter) => fileInFilter.id !== file.id
|
||||
)
|
||||
);
|
||||
}}
|
||||
isUploading={file.isUploading || false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
onPaste={handlePaste}
|
||||
onChange={handleInputChange}
|
||||
ref={textAreaRef}
|
||||
className={`
|
||||
m-0
|
||||
w-full
|
||||
shrink
|
||||
resize-none
|
||||
rounded-lg
|
||||
border-0
|
||||
bg-background-chatbar
|
||||
placeholder:text-text-chatbar-subtle
|
||||
${
|
||||
textAreaRef.current &&
|
||||
textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT
|
||||
? "overflow-y-auto mt-2"
|
||||
: ""
|
||||
}
|
||||
whitespace-normal
|
||||
break-word
|
||||
overscroll-contain
|
||||
outline-none
|
||||
placeholder-subtle
|
||||
resize-none
|
||||
px-5
|
||||
py-4
|
||||
h-14
|
||||
`}
|
||||
autoFocus
|
||||
style={{ scrollbarWidth: "thin" }}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Ask me anything..."
|
||||
value={message}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!(event.nativeEvent as any).isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
}
|
||||
}}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
name="File"
|
||||
Icon={FiPlusCircle}
|
||||
onClick={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true; // Allow multiple files
|
||||
input.onchange = (event: any) => {
|
||||
const selectedFiles = Array.from(
|
||||
event?.target?.files || []
|
||||
) as File[];
|
||||
if (selectedFiles.length > 0) {
|
||||
handleFileUpload(selectedFiles);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
/>
|
||||
|
||||
{filterManager && (
|
||||
<HorizontalSourceSelector
|
||||
timeRange={filterManager.timeRange}
|
||||
setTimeRange={filterManager.setTimeRange}
|
||||
selectedSources={filterManager.selectedSources}
|
||||
setSelectedSources={filterManager.setSelectedSources}
|
||||
selectedDocumentSets={filterManager.selectedDocumentSets}
|
||||
setSelectedDocumentSets={filterManager.setSelectedDocumentSets}
|
||||
selectedTags={filterManager.selectedTags}
|
||||
setSelectedTags={filterManager.setSelectedTags}
|
||||
existingSources={existingSources}
|
||||
availableDocumentSets={availableDocumentSets}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-2 mobile:right-4 desktop:right-4">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
size={28}
|
||||
className={`text-emphasis text-white p-1 rounded-full ${
|
||||
message ? "bg-submit-background" : "bg-disabled-submit-background"
|
||||
} `}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
62
web/src/app/chat/layout.tsx
Normal file
62
web/src/app/chat/layout.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
noStore();
|
||||
|
||||
// Ensure searchParams is an object, even if it's empty
|
||||
const safeSearchParams = {};
|
||||
|
||||
const data = await fetchChatData(
|
||||
safeSearchParams as { [key: string]: string }
|
||||
);
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
chatSessions,
|
||||
availableSources,
|
||||
user,
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChatProvider>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
import { Citation } from "@/components/search/results/Citation";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { LoadedOnyxDocument, OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { getSourceMetadata, SOURCE_METADATA_MAP } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import React, { memo } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { SlackIcon } from "@/components/icons/icons";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
|
||||
export const MemoizedAnchor = memo(
|
||||
@ -66,7 +62,6 @@ export const MemoizedLink = memo((props: any) => {
|
||||
<Citation
|
||||
url={document?.url}
|
||||
icon={document?.icon as React.ReactNode}
|
||||
link={rest?.href}
|
||||
document={document as LoadedOnyxDocument}
|
||||
updatePresentingDocument={updatePresentingDocument}
|
||||
>
|
||||
|
@ -383,14 +383,14 @@ export const AIMessage = ({
|
||||
<div
|
||||
id="onyx-ai-message"
|
||||
ref={trackedElementRef}
|
||||
className={`py-5 ml-4 px-5 relative flex `}
|
||||
className={`py-5 ml-4 lg:px-5 relative flex `}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto ${
|
||||
shared ? "w-full" : "w-[90%]"
|
||||
} max-w-message-max`}
|
||||
>
|
||||
<div className={`desktop:mr-12 ${!shared && "mobile:ml-0 md:ml-8"}`}>
|
||||
<div className={`lg:mr-12 ${!shared && "mobile:ml-0 md:ml-8"}`}>
|
||||
<div className="flex">
|
||||
<AssistantIcon
|
||||
size="small"
|
||||
@ -399,7 +399,7 @@ export const AIMessage = ({
|
||||
|
||||
<div className="w-full">
|
||||
<div className="max-w-message-max break-words">
|
||||
<div className="w-full ml-4">
|
||||
<div className="w-full lg:ml-4">
|
||||
<div className="max-w-message-max break-words">
|
||||
{!toolCall || toolCall.tool_name === SEARCH_TOOL_NAME ? (
|
||||
<>
|
||||
@ -410,6 +410,8 @@ export const AIMessage = ({
|
||||
query={query}
|
||||
finished={toolCall?.tool_result != undefined}
|
||||
handleSearchQueryEdit={handleSearchQueryEdit}
|
||||
docs={docs || []}
|
||||
toggleDocumentSelection={toggleDocumentSelection!}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -465,7 +467,7 @@ export const AIMessage = ({
|
||||
)}
|
||||
|
||||
{docs && docs.length > 0 && (
|
||||
<div className="mt-2 -mx-8 w-full mb-4 flex relative">
|
||||
<div className="mobile:hidden mt-2 -mx-8 w-full mb-4 flex relative">
|
||||
<div className="w-full">
|
||||
<div className="px-8 flex gap-x-2">
|
||||
{!settings?.isMobile &&
|
||||
@ -768,7 +770,7 @@ export const HumanMessage = ({
|
||||
return (
|
||||
<div
|
||||
id="onyx-human-message"
|
||||
className="pt-5 pb-1 px-2 lg:px-5 flex -mr-6 relative"
|
||||
className="pt-5 pb-1 w-full lg:px-5 flex -mr-6 relative"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
@ -778,7 +780,7 @@ export const HumanMessage = ({
|
||||
} max-w-[790px]`}
|
||||
>
|
||||
<div className="xl:ml-8">
|
||||
<div className="flex flex-col mr-4">
|
||||
<div className="flex flex-col desktop:mr-4">
|
||||
<FileDisplay alignBubble files={files || []} />
|
||||
|
||||
<div className="flex justify-end">
|
||||
|
@ -4,12 +4,15 @@ import {
|
||||
} from "@/components/BasicClickable";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FiCheck, FiEdit2, FiSearch, FiX } from "react-icons/fi";
|
||||
|
||||
@ -45,11 +48,15 @@ export function SearchSummary({
|
||||
query,
|
||||
finished,
|
||||
handleSearchQueryEdit,
|
||||
docs,
|
||||
toggleDocumentSelection,
|
||||
}: {
|
||||
index: number;
|
||||
finished: boolean;
|
||||
query: string;
|
||||
handleSearchQueryEdit?: (query: string) => void;
|
||||
docs: OnyxDocument[];
|
||||
toggleDocumentSelection: () => void;
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [finalQuery, setFinalQuery] = useState(query);
|
||||
@ -87,27 +94,63 @@ export function SearchSummary({
|
||||
}, [query, isEditing]);
|
||||
|
||||
const searchingForDisplay = (
|
||||
<div className={`flex p-1 rounded ${isOverflowed && "cursor-default"}`}>
|
||||
<FiSearch className="flex-none mr-2 my-auto" size={14} />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div
|
||||
className={`${!finished && "loading-text"}
|
||||
!text-sm !line-clamp-1 !break-all px-0.5`}
|
||||
ref={searchingForRef}
|
||||
className={`flex items-center w-full rounded ${
|
||||
isOverflowed && "cursor-default"
|
||||
}`}
|
||||
>
|
||||
{finished ? "Searched" : "Searching"} for:{" "}
|
||||
<i>
|
||||
{index === 1
|
||||
? finalQuery.length > 50
|
||||
? `${finalQuery.slice(0, 50)}...`
|
||||
: finalQuery
|
||||
: finalQuery}
|
||||
</i>
|
||||
<FiSearch className="mobile:hidden flex-none mr-2" size={14} />
|
||||
<div
|
||||
className={`${
|
||||
!finished && "loading-text"
|
||||
} text-xs desktop:text-sm mobile:ml-auto !line-clamp-1 !break-all px-0.5 flex-grow`}
|
||||
ref={searchingForRef}
|
||||
>
|
||||
{finished ? "Searched" : "Searching"} for:{" "}
|
||||
<i>
|
||||
{index === 1
|
||||
? finalQuery.length > 50
|
||||
? `${finalQuery.slice(0, 50)}...`
|
||||
: finalQuery
|
||||
: finalQuery}
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="desktop:hidden">
|
||||
{" "}
|
||||
{docs && (
|
||||
<button
|
||||
className="cursor-pointer mr-2 flex items-center gap-0.5"
|
||||
onClick={() => toggleDocumentSelection()}
|
||||
>
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type)))
|
||||
.slice(0, 3)
|
||||
.map((sourceType, idx) => (
|
||||
<div key={idx} className="rounded-full">
|
||||
<SourceIcon sourceType={sourceType} iconSize={14} />
|
||||
</div>
|
||||
))}
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type))).length >
|
||||
3 && (
|
||||
<div className="rounded-full bg-gray-200 w-3.5 h-3.5 flex items-center justify-center">
|
||||
<span className="text-[8px]">
|
||||
+
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type)))
|
||||
.length - 3}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs underline">View sources</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const editInput = handleSearchQueryEdit ? (
|
||||
<div className="flex w-full mr-3">
|
||||
<div className="mobile:hidden flex w-full mr-3">
|
||||
<div className="my-2 w-full">
|
||||
<input
|
||||
ref={editQueryRef}
|
||||
@ -155,12 +198,12 @@ export function SearchSummary({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
{isEditing ? (
|
||||
editInput
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm">
|
||||
<div className="mobile:w-full mobile:mr-2 text-sm mobile:flex-grow">
|
||||
{isOverflowed ? (
|
||||
<HoverPopup
|
||||
mainContent={searchingForDisplay}
|
||||
@ -176,12 +219,13 @@ export function SearchSummary({
|
||||
searchingForDisplay
|
||||
)}
|
||||
</div>
|
||||
|
||||
{handleSearchQueryEdit && (
|
||||
<TooltipProvider delayDuration={1000}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="my-auto hover:bg-hover p-1.5 rounded"
|
||||
className="ml-2 mobile:hidden hover:bg-hover p-1 rounded flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EmphasizedClickable } from "@/components/BasicClickable";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { FiBook } from "react-icons/fi";
|
||||
|
||||
export function SkippedSearch({
|
||||
@ -7,22 +7,29 @@ export function SkippedSearch({
|
||||
handleForceSearch: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex text-sm !pt-0 p-1">
|
||||
<div className="flex mb-auto">
|
||||
<FiBook className="my-auto flex-none mr-2" size={14} />
|
||||
<div className="my-auto cursor-default">
|
||||
<div className="flex w-full text-sm !pt-0 p-1">
|
||||
<div className="flex w-full mb-auto">
|
||||
<FiBook className="mobile:hidden my-auto flex-none mr-2" size={14} />
|
||||
<div className="my-auto flex w-full items-center justify-between cursor-default">
|
||||
<span className="mobile:hidden">
|
||||
The AI decided this query didn't need a search
|
||||
</span>
|
||||
<span className="desktop:hidden">No search</span>
|
||||
<p className="text-xs desktop:hidden">No search performed</p>
|
||||
<CustomTooltip
|
||||
content="Perform a search for this query"
|
||||
showTick
|
||||
line
|
||||
wrap
|
||||
>
|
||||
<button
|
||||
onClick={handleForceSearch}
|
||||
className="ml-auto mr-4 text-xs desktop:hidden underline-dotted decoration-dotted underline cursor-pointer"
|
||||
>
|
||||
Force search?
|
||||
</button>
|
||||
</CustomTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto my-auto" onClick={handleForceSearch}>
|
||||
<EmphasizedClickable size="sm">
|
||||
<div className="w-24 text-xs">Force Search</div>
|
||||
</EmphasizedClickable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
384
web/src/app/chat/nrf/NRFPage.tsx
Normal file
384
web/src/app/chat/nrf/NRFPage.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SimplifiedChatInputBar } from "../input/SimplifiedChatInputBar";
|
||||
import { Menu } from "lucide-react";
|
||||
import { Shortcut } from "./interfaces";
|
||||
import {
|
||||
MaxShortcutsReachedModal,
|
||||
NewShortCutModal,
|
||||
} from "@/components/extension/Shortcuts";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { useNightTime } from "@/lib/dateUtils";
|
||||
import { useFilters } from "@/lib/hooks";
|
||||
import { uploadFilesForChat } from "../lib";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import Dropzone from "react-dropzone";
|
||||
import { useSendMessageToParent } from "@/lib/extension/utils";
|
||||
import { useNRFPreferences } from "@/components/context/NRFPreferencesContext";
|
||||
import { SettingsPanel } from "../../components/nrf/SettingsPanel";
|
||||
import { ShortcutsDisplay } from "../../components/nrf/ShortcutsDisplay";
|
||||
import LoginPage from "../../auth/login/LoginPage";
|
||||
import { AuthType, NEXT_PUBLIC_WEB_DOMAIN } from "@/lib/constants";
|
||||
import { sendSetDefaultNewTabMessage } from "@/lib/extension/utils";
|
||||
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||
import { CHROME_MESSAGE } from "@/lib/extension/constants";
|
||||
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
|
||||
|
||||
export default function NRFPage({
|
||||
requestCookies,
|
||||
}: {
|
||||
requestCookies: ReadonlyRequestCookies;
|
||||
}) {
|
||||
const {
|
||||
theme,
|
||||
defaultLightBackgroundUrl,
|
||||
defaultDarkBackgroundUrl,
|
||||
shortcuts: shortCuts,
|
||||
setShortcuts: setShortCuts,
|
||||
setUseOnyxAsNewTab,
|
||||
showShortcuts,
|
||||
} = useNRFPreferences();
|
||||
|
||||
const filterManager = useFilters();
|
||||
const { isNight } = useNightTime();
|
||||
const { user } = useUser();
|
||||
const { ccPairs, documentSets, tags, llmProviders } = useChatContext();
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
// State
|
||||
const [message, setMessage] = useState("");
|
||||
const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
|
||||
const [editingShortcut, setEditingShortcut] = useState<Shortcut | null>(null);
|
||||
const [backgroundUrl, setBackgroundUrl] = useState<string>(
|
||||
theme === "light" ? defaultLightBackgroundUrl : defaultDarkBackgroundUrl
|
||||
);
|
||||
|
||||
// Modals
|
||||
const [showTurnOffModal, setShowTurnOffModal] = useState<boolean>(false);
|
||||
const [showShortCutModal, setShowShortCutModal] = useState(false);
|
||||
const [showMaxShortcutsModal, setShowMaxShortcutsModal] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState<boolean>(!user);
|
||||
|
||||
// Refs
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setBackgroundUrl(
|
||||
theme === "light" ? defaultLightBackgroundUrl : defaultDarkBackgroundUrl
|
||||
);
|
||||
}, [theme, defaultLightBackgroundUrl, defaultDarkBackgroundUrl]);
|
||||
|
||||
useSendMessageToParent();
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleSettings = () => {
|
||||
setSettingsOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
// If user toggles the "Use Onyx" switch to off, prompt a modal
|
||||
const handleUseOnyxToggle = (checked: boolean) => {
|
||||
if (!checked) {
|
||||
setShowTurnOffModal(true);
|
||||
} else {
|
||||
setUseOnyxAsNewTab(true);
|
||||
sendSetDefaultNewTabMessage(true);
|
||||
}
|
||||
};
|
||||
|
||||
const availableSources = ccPairs.map((ccPair) => ccPair.source);
|
||||
|
||||
const [currentMessageFiles, setCurrentMessageFiles] = useState<
|
||||
FileDescriptor[]
|
||||
>([]);
|
||||
|
||||
const handleImageUpload = async (acceptedFiles: File[]) => {
|
||||
const tempFileDescriptors = acceptedFiles.map((file) => ({
|
||||
id: uuidv4(),
|
||||
type: file.type.startsWith("image/")
|
||||
? ChatFileType.IMAGE
|
||||
: ChatFileType.DOCUMENT,
|
||||
isUploading: true,
|
||||
}));
|
||||
|
||||
// only show loading spinner for reasonably large files
|
||||
const totalSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
if (totalSize > 50 * 1024) {
|
||||
setCurrentMessageFiles((prev) => [...prev, ...tempFileDescriptors]);
|
||||
}
|
||||
|
||||
const removeTempFiles = (prev: FileDescriptor[]) => {
|
||||
return prev.filter(
|
||||
(file) => !tempFileDescriptors.some((newFile) => newFile.id === file.id)
|
||||
);
|
||||
};
|
||||
|
||||
await uploadFilesForChat(acceptedFiles).then(([files, error]) => {
|
||||
if (error) {
|
||||
setCurrentMessageFiles((prev) => removeTempFiles(prev));
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: error,
|
||||
});
|
||||
} else {
|
||||
setCurrentMessageFiles((prev) => [...removeTempFiles(prev), ...files]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmTurnOff = () => {
|
||||
setUseOnyxAsNewTab(false);
|
||||
setShowTurnOffModal(false);
|
||||
sendSetDefaultNewTabMessage(false);
|
||||
};
|
||||
|
||||
// Auth related
|
||||
const [authType, setAuthType] = useState<AuthType | null>(null);
|
||||
const [fetchingAuth, setFetchingAuth] = useState(false);
|
||||
useEffect(() => {
|
||||
// If user is already logged in, no need to fetch auth data
|
||||
if (user) return;
|
||||
|
||||
async function fetchAuthData() {
|
||||
setFetchingAuth(true);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/auth/type", {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch auth type: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setAuthType(data.auth_type);
|
||||
} catch (err) {
|
||||
console.error("Error fetching auth data:", err);
|
||||
} finally {
|
||||
setFetchingAuth(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchAuthData();
|
||||
}, [user]);
|
||||
|
||||
const onSubmit = async ({
|
||||
messageOverride,
|
||||
}: {
|
||||
messageOverride?: string;
|
||||
} = {}) => {
|
||||
const userMessage = messageOverride || message;
|
||||
|
||||
let filterString = filterManager?.getFilterString();
|
||||
|
||||
if (currentMessageFiles.length > 0) {
|
||||
filterString +=
|
||||
"&files=" + encodeURIComponent(JSON.stringify(currentMessageFiles));
|
||||
}
|
||||
|
||||
const newHref =
|
||||
`${NEXT_PUBLIC_WEB_DOMAIN}/chat?send-on-load=true&user-prompt=` +
|
||||
encodeURIComponent(userMessage) +
|
||||
filterString;
|
||||
|
||||
if (typeof window !== "undefined" && window.parent) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.LOAD_NEW_PAGE, href: newHref },
|
||||
"*"
|
||||
);
|
||||
} else {
|
||||
window.location.href = newHref;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full flex flex-col min-h-screen bg-cover bg-center bg-no-repeat overflow-hidden transition-[background-image] duration-300 ease-in-out"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundUrl})`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-4 z-10">
|
||||
<button
|
||||
aria-label="Open settings"
|
||||
onClick={toggleSettings}
|
||||
className="bg-white bg-opacity-70 rounded-full p-2.5 cursor-pointer hover:bg-opacity-80 transition-colors duration-200"
|
||||
>
|
||||
<Menu size={12} className="text-neutral-900" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Dropzone onDrop={handleImageUpload} noClick>
|
||||
{({ getRootProps }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="absolute top-20 left-0 w-full h-full flex flex-col"
|
||||
>
|
||||
<div className="pointer-events-auto absolute top-[40%] left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-[90%] lg:max-w-3xl">
|
||||
<h1
|
||||
className={`pl-2 text-xl text-left w-full mb-4 ${
|
||||
theme === "light" ? "text-neutral-800" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{isNight
|
||||
? "End your day with Onyx"
|
||||
: "Start your day with Onyx"}
|
||||
</h1>
|
||||
|
||||
<SimplifiedChatInputBar
|
||||
onSubmit={onSubmit}
|
||||
handleFileUpload={handleImageUpload}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
files={currentMessageFiles}
|
||||
setFiles={setCurrentMessageFiles}
|
||||
filterManager={filterManager}
|
||||
textAreaRef={textAreaRef}
|
||||
existingSources={availableSources}
|
||||
availableDocumentSets={documentSets}
|
||||
availableTags={tags}
|
||||
/>
|
||||
|
||||
<ShortcutsDisplay
|
||||
shortCuts={shortCuts}
|
||||
showShortcuts={showShortcuts}
|
||||
setEditingShortcut={setEditingShortcut}
|
||||
setShowShortCutModal={setShowShortCutModal}
|
||||
openShortCutModal={() => {
|
||||
if (shortCuts.length >= 6) {
|
||||
setShowMaxShortcutsModal(true);
|
||||
} else {
|
||||
setEditingShortcut(null);
|
||||
setShowShortCutModal(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
{showMaxShortcutsModal && (
|
||||
<MaxShortcutsReachedModal
|
||||
onClose={() => setShowMaxShortcutsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showShortCutModal && (
|
||||
<NewShortCutModal
|
||||
setPopup={setPopup}
|
||||
onDelete={(shortcut: Shortcut) => {
|
||||
setShortCuts(
|
||||
shortCuts.filter((s: Shortcut) => s.name !== shortcut.name)
|
||||
);
|
||||
setShowShortCutModal(false);
|
||||
}}
|
||||
isOpen={showShortCutModal}
|
||||
onClose={() => {
|
||||
setEditingShortcut(null);
|
||||
setShowShortCutModal(false);
|
||||
}}
|
||||
onAdd={(shortCut: Shortcut) => {
|
||||
if (editingShortcut) {
|
||||
setShortCuts(
|
||||
shortCuts
|
||||
.filter((s) => s.name !== editingShortcut.name)
|
||||
.concat(shortCut)
|
||||
);
|
||||
} else {
|
||||
setShortCuts([...shortCuts, shortCut]);
|
||||
}
|
||||
setShowShortCutModal(false);
|
||||
}}
|
||||
editingShortcut={editingShortcut}
|
||||
/>
|
||||
)}
|
||||
<SettingsPanel
|
||||
settingsOpen={settingsOpen}
|
||||
toggleSettings={toggleSettings}
|
||||
handleUseOnyxToggle={handleUseOnyxToggle}
|
||||
/>
|
||||
|
||||
<Dialog open={showTurnOffModal} onOpenChange={setShowTurnOffModal}>
|
||||
<DialogContent className="w-fit max-w-[95%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Turn off Onyx new tab page?</DialogTitle>
|
||||
<DialogDescription>
|
||||
You'll see your browser's default new tab page instead.
|
||||
<br />
|
||||
You can turn it back on anytime in your Onyx settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex gap-2 justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowTurnOffModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={confirmTurnOff}>
|
||||
Turn off
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{!user && authType !== "disabled" && showLoginModal ? (
|
||||
<Modal className="max-w-md mx-auto">
|
||||
{fetchingAuth ? (
|
||||
<p className="p-4">Loading login info…</p>
|
||||
) : authType == "basic" ? (
|
||||
<LoginPage
|
||||
showPageRedirect
|
||||
authUrl={null}
|
||||
authTypeMetadata={{
|
||||
authType: authType as AuthType,
|
||||
autoRedirect: false,
|
||||
requiresVerification: false,
|
||||
anonymousUserEnabled: null,
|
||||
}}
|
||||
nextUrl="/nrf"
|
||||
searchParams={{}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<h2 className="text-center text-xl text-strong font-bold mb-4">
|
||||
Welcome to Onyx
|
||||
</h2>
|
||||
<Button
|
||||
className="bg-accent w-full hover:bg-accent-hover text-white"
|
||||
onClick={() => {
|
||||
if (window.top) {
|
||||
window.top.location.href = "/auth/login";
|
||||
} else {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}}
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
) : (
|
||||
llmProviders.length == 0 && <ApiKeyModal setPopup={setPopup} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
5
web/src/app/chat/nrf/interfaces.ts
Normal file
5
web/src/app/chat/nrf/interfaces.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Shortcut {
|
||||
name: string;
|
||||
url: string;
|
||||
favicon?: string;
|
||||
}
|
20
web/src/app/chat/nrf/page.tsx
Normal file
20
web/src/app/chat/nrf/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { cookies } from "next/headers";
|
||||
import NRFPage from "./NRFPage";
|
||||
import { NRFPreferencesProvider } from "../../../components/context/NRFPreferencesContext";
|
||||
|
||||
export default async function Page() {
|
||||
noStore();
|
||||
const requestCookies = await cookies();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-black">
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<NRFPreferencesProvider>
|
||||
<NRFPage requestCookies={requestCookies} />
|
||||
</NRFPreferencesProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,65 +1,10 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import WrappedChat from "./WrappedChat";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
noStore();
|
||||
const requestCookies = await cookies();
|
||||
const data = await fetchChatData(searchParams);
|
||||
const firstMessage = searchParams.firstMessage;
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
availableSources,
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
toggleSidebar,
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldShowWelcomeModal && (
|
||||
<WelcomeModal user={user} requestCookies={requestCookies} />
|
||||
)}
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
<WrappedChat
|
||||
initiallyToggled={toggleSidebar && !user?.is_anonymous_user}
|
||||
/>
|
||||
</ChatProvider>
|
||||
</>
|
||||
);
|
||||
return <WrappedChat firstMessage={firstMessage} initiallyToggled={false} />;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export const SEARCH_PARAM_NAMES = {
|
||||
SUBMIT_ON_LOAD: "submit-on-load",
|
||||
// chat title
|
||||
TITLE: "title",
|
||||
FILES: "files",
|
||||
// for seeding chats
|
||||
SEEDED: "seeded",
|
||||
SEND_ON_LOAD: "send-on-load",
|
||||
|
179
web/src/app/components/nrf/SettingsPanel.tsx
Normal file
179
web/src/app/components/nrf/SettingsPanel.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useNRFPreferences } from "../../../components/context/NRFPreferencesContext";
|
||||
import {
|
||||
darkExtensionImages,
|
||||
lightExtensionImages,
|
||||
} from "@/lib/extension/constants";
|
||||
|
||||
const SidebarSwitch = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
label,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
label: string;
|
||||
}) => (
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-sm text-gray-300">{label}</span>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
className="data-[state=checked]:bg-white data-[state=checked]:border-neutral-200 data-[state=unchecked]:bg-gray-600"
|
||||
circleClassName="data-[state=checked]:bg-neutral-200"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RadioOption = ({
|
||||
value,
|
||||
label,
|
||||
description,
|
||||
groupValue,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
groupValue: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<div className="flex items-start space-x-2 mb-2">
|
||||
<RadioGroupItem
|
||||
value={value}
|
||||
id={value}
|
||||
className="mt-1 border border-gray-600 data-[state=checked]:border-white data-[state=checked]:bg-white"
|
||||
/>
|
||||
<Label htmlFor={value} className="flex flex-col">
|
||||
<span className="text-sm text-gray-300">{label}</span>
|
||||
{description && (
|
||||
<span className="text-xs text-gray-500">{description}</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsPanel = ({
|
||||
settingsOpen,
|
||||
toggleSettings,
|
||||
handleUseOnyxToggle,
|
||||
}: {
|
||||
settingsOpen: boolean;
|
||||
toggleSettings: () => void;
|
||||
handleUseOnyxToggle: (checked: boolean) => void;
|
||||
}) => {
|
||||
const {
|
||||
theme,
|
||||
setTheme,
|
||||
defaultLightBackgroundUrl,
|
||||
setDefaultLightBackgroundUrl,
|
||||
defaultDarkBackgroundUrl,
|
||||
setDefaultDarkBackgroundUrl,
|
||||
useOnyxAsNewTab,
|
||||
showShortcuts,
|
||||
setShowShortcuts,
|
||||
} = useNRFPreferences();
|
||||
|
||||
const toggleTheme = (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
const updateBackgroundUrl = (url: string) => {
|
||||
if (theme === "light") {
|
||||
setDefaultLightBackgroundUrl(url);
|
||||
} else {
|
||||
setDefaultDarkBackgroundUrl(url);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 right-0 w-[360px] h-full bg-[#202124] text-gray-300 overflow-y-auto z-20 transition-transform duration-300 ease-in-out transform"
|
||||
style={{
|
||||
transform: settingsOpen ? "translateX(0)" : "translateX(100%)",
|
||||
boxShadow: "-2px 0 10px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Home page settings
|
||||
</h2>
|
||||
<button
|
||||
aria-label="Close"
|
||||
onClick={toggleSettings}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-semibold mb-2">General</h3>
|
||||
<SidebarSwitch
|
||||
checked={useOnyxAsNewTab}
|
||||
onCheckedChange={handleUseOnyxToggle}
|
||||
label="Use Onyx as new tab page"
|
||||
/>
|
||||
|
||||
<SidebarSwitch
|
||||
checked={showShortcuts}
|
||||
onCheckedChange={setShowShortcuts}
|
||||
label="Show bookmarks"
|
||||
/>
|
||||
|
||||
<h3 className="text-sm font-semibold mt-6 mb-2">Theme</h3>
|
||||
<RadioGroup
|
||||
value={theme}
|
||||
onValueChange={toggleTheme}
|
||||
className="space-y-2"
|
||||
>
|
||||
<RadioOption
|
||||
value="light"
|
||||
label="Light theme"
|
||||
description="Light theme"
|
||||
groupValue={theme}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
<RadioOption
|
||||
value="dark"
|
||||
label="Dark theme"
|
||||
description="Dark theme"
|
||||
groupValue={theme}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
<h3 className="text-sm font-semibold mt-6 mb-2">Background</h3>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{(theme === "dark" ? darkExtensionImages : lightExtensionImages).map(
|
||||
(bg: string, index: number) => (
|
||||
<div
|
||||
key={bg}
|
||||
onClick={() => updateBackgroundUrl(bg)}
|
||||
className={`relative ${
|
||||
index === 0 ? "col-span-2 row-span-2" : ""
|
||||
} cursor-pointer rounded-sm overflow-hidden`}
|
||||
style={{
|
||||
paddingBottom: index === 0 ? "100%" : "50%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
/>
|
||||
{(theme === "light"
|
||||
? defaultLightBackgroundUrl
|
||||
: defaultDarkBackgroundUrl) === bg && (
|
||||
<div className="absolute inset-0 border-2 border-blue-400 rounded" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
46
web/src/app/components/nrf/ShortcutsDisplay.tsx
Normal file
46
web/src/app/components/nrf/ShortcutsDisplay.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { ShortCut, AddShortCut } from "@/components/extension/Shortcuts";
|
||||
import { Shortcut } from "@/app/chat/nrf/interfaces";
|
||||
|
||||
interface ShortcutsDisplayProps {
|
||||
shortCuts: Shortcut[];
|
||||
showShortcuts: boolean;
|
||||
setEditingShortcut: (shortcut: Shortcut | null) => void;
|
||||
setShowShortCutModal: (show: boolean) => void;
|
||||
openShortCutModal: () => void;
|
||||
}
|
||||
|
||||
export const ShortcutsDisplay: React.FC<ShortcutsDisplayProps> = ({
|
||||
shortCuts,
|
||||
showShortcuts,
|
||||
setEditingShortcut,
|
||||
setShowShortCutModal,
|
||||
openShortCutModal,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
mx-auto flex flex-wrap justify-center gap-x-6 gap-y-4 mt-12
|
||||
transition-all duration-700 ease-in-out
|
||||
${
|
||||
showShortcuts
|
||||
? "opacity-100 max-h-[500px]"
|
||||
: "opacity-0 max-h-0 overflow-hidden pointer-events-none"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{shortCuts.map((shortCut: Shortcut, index: number) => (
|
||||
<ShortCut
|
||||
key={index}
|
||||
onEdit={() => {
|
||||
setEditingShortcut(shortCut);
|
||||
setShowShortCutModal(true);
|
||||
}}
|
||||
shortCut={shortCut}
|
||||
/>
|
||||
))}
|
||||
<AddShortCut openShortCutModal={openShortCutModal} />
|
||||
</div>
|
||||
);
|
||||
};
|
0
web/src/app/ee/Hori
Normal file
0
web/src/app/ee/Hori
Normal file
@ -13,7 +13,6 @@ import { Metadata } from "next";
|
||||
import { buildClientUrl } from "@/lib/utilsSS";
|
||||
import { Inter } from "next/font/google";
|
||||
import { EnterpriseSettings, GatingType } from "./admin/settings/interfaces";
|
||||
import { HeaderTitle } from "@/components/header/HeaderTitle";
|
||||
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
|
||||
import { AppProvider } from "@/components/context/AppProvider";
|
||||
import { PHProvider } from "./providers";
|
||||
|
@ -33,6 +33,7 @@ export function AssistantIcon({
|
||||
|
||||
return (
|
||||
<CustomTooltip
|
||||
className="hidden lg:block"
|
||||
disabled={disableToolip || !assistant.description}
|
||||
showTick
|
||||
line
|
||||
|
@ -66,10 +66,12 @@ const AssistantSelector = ({
|
||||
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
|
||||
|
||||
// Initialize selectedTab from localStorage
|
||||
const [selectedTab, setSelectedTab] = useState<number>(() => {
|
||||
const [selectedTab, setSelectedTab] = useState<number | undefined>();
|
||||
useEffect(() => {
|
||||
const storedTab = localStorage.getItem("assistantSelectorSelectedTab");
|
||||
return storedTab !== null ? Number(storedTab) : 0;
|
||||
});
|
||||
const tab = storedTab !== null ? Number(storedTab) : 0;
|
||||
setSelectedTab(tab);
|
||||
}, [localStorage]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
|
@ -72,15 +72,20 @@ export const useSidebarVisibility = ({
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setShowDocSidebar(false);
|
||||
if (!mobile) {
|
||||
setShowDocSidebar(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleEvent);
|
||||
document.addEventListener("mouseleave", handleMouseLeave);
|
||||
if (!mobile) {
|
||||
document.addEventListener("mousemove", handleEvent);
|
||||
document.addEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleEvent);
|
||||
document.removeEventListener("mouseleave", handleMouseLeave);
|
||||
if (!mobile) {
|
||||
document.removeEventListener("mousemove", handleEvent);
|
||||
document.removeEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showDocSidebar, toggledSidebar, sidebarElementRef, mobile]);
|
||||
|
123
web/src/components/context/NRFPreferencesContext.tsx
Normal file
123
web/src/components/context/NRFPreferencesContext.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { Shortcut } from "@/app/chat/nrf/interfaces";
|
||||
import { notifyExtensionOfThemeChange } from "@/lib/extension/utils";
|
||||
import {
|
||||
darkExtensionImages,
|
||||
lightExtensionImages,
|
||||
LocalStorageKeys,
|
||||
} from "@/lib/extension/constants";
|
||||
|
||||
interface NRFPreferencesContextValue {
|
||||
theme: string;
|
||||
setTheme: (t: string) => void;
|
||||
defaultLightBackgroundUrl: string;
|
||||
setDefaultLightBackgroundUrl: (val: string) => void;
|
||||
defaultDarkBackgroundUrl: string;
|
||||
setDefaultDarkBackgroundUrl: (val: string) => void;
|
||||
shortcuts: Shortcut[];
|
||||
setShortcuts: (s: Shortcut[]) => void;
|
||||
useOnyxAsNewTab: boolean;
|
||||
setUseOnyxAsNewTab: (v: boolean) => void;
|
||||
showShortcuts: boolean;
|
||||
setShowShortcuts: (v: boolean) => void;
|
||||
}
|
||||
|
||||
const NRFPreferencesContext = createContext<
|
||||
NRFPreferencesContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
function useLocalStorageState<T>(
|
||||
key: string,
|
||||
defaultValue: T
|
||||
): [T, (value: T) => void] {
|
||||
const [state, setState] = useState<T>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedValue = localStorage.getItem(key);
|
||||
return storedValue ? JSON.parse(storedValue) : defaultValue;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const setValue = (value: T) => {
|
||||
setState(value);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
return [state, setValue];
|
||||
}
|
||||
|
||||
export function NRFPreferencesProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [theme, setTheme] = useLocalStorageState<string>(
|
||||
LocalStorageKeys.THEME,
|
||||
"dark"
|
||||
);
|
||||
const [defaultLightBackgroundUrl, setDefaultLightBackgroundUrl] =
|
||||
useLocalStorageState<string>(
|
||||
LocalStorageKeys.LIGHT_BG_URL,
|
||||
lightExtensionImages[0]
|
||||
);
|
||||
const [defaultDarkBackgroundUrl, setDefaultDarkBackgroundUrl] =
|
||||
useLocalStorageState<string>(
|
||||
LocalStorageKeys.DARK_BG_URL,
|
||||
darkExtensionImages[0]
|
||||
);
|
||||
const [shortcuts, setShortcuts] = useLocalStorageState<Shortcut[]>(
|
||||
LocalStorageKeys.SHORTCUTS,
|
||||
[]
|
||||
);
|
||||
const [showShortcuts, setShowShortcuts] = useLocalStorageState<boolean>(
|
||||
LocalStorageKeys.SHOW_SHORTCUTS,
|
||||
false
|
||||
);
|
||||
const [useOnyxAsNewTab, setUseOnyxAsNewTab] = useLocalStorageState<boolean>(
|
||||
LocalStorageKeys.USE_ONYX_AS_NEW_TAB,
|
||||
true
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === "dark") {
|
||||
notifyExtensionOfThemeChange(theme, defaultDarkBackgroundUrl);
|
||||
} else {
|
||||
notifyExtensionOfThemeChange(theme, defaultLightBackgroundUrl);
|
||||
}
|
||||
}, [theme, defaultLightBackgroundUrl, defaultDarkBackgroundUrl]);
|
||||
|
||||
return (
|
||||
<NRFPreferencesContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
setTheme,
|
||||
defaultLightBackgroundUrl,
|
||||
setDefaultLightBackgroundUrl,
|
||||
defaultDarkBackgroundUrl,
|
||||
setDefaultDarkBackgroundUrl,
|
||||
shortcuts,
|
||||
setShortcuts,
|
||||
useOnyxAsNewTab,
|
||||
setUseOnyxAsNewTab,
|
||||
showShortcuts,
|
||||
setShowShortcuts,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NRFPreferencesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNRFPreferences() {
|
||||
const context = useContext(NRFPreferencesContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useNRFPreferences must be used within an NRFPreferencesProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
257
web/src/components/extension/Shortcuts.tsx
Normal file
257
web/src/components/extension/Shortcuts.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Shortcut } from "@/app/chat/nrf/interfaces";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PencilIcon, PlusIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { Modal } from "../Modal";
|
||||
import { QuestionMarkIcon } from "../icons/icons";
|
||||
|
||||
export const validateUrl = (input: string) => {
|
||||
try {
|
||||
new URL(input);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const ShortCut = ({
|
||||
shortCut,
|
||||
onEdit,
|
||||
}: {
|
||||
shortCut: Shortcut;
|
||||
onEdit: (shortcut: Shortcut) => void;
|
||||
}) => {
|
||||
const [faviconError, setFaviconError] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-24 h-24 flex-none rounded-xl shadow-lg relative group transition-all duration-300 ease-in-out hover:scale-105 bg-white/10 backdrop-blur-sm">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(shortCut);
|
||||
}}
|
||||
className="absolute top-1 right-1 p-1 bg-white/20 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
>
|
||||
<PencilIcon className="w-3 h-3 text-white" />
|
||||
</button>
|
||||
<div
|
||||
onClick={() => window.open(shortCut.url, "_blank")}
|
||||
className="w-full h-full flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="w-8 h-8 mb-2 relative">
|
||||
{shortCut.favicon && !faviconError ? (
|
||||
<Image
|
||||
src={shortCut.favicon}
|
||||
alt={shortCut.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-sm"
|
||||
onError={() => setFaviconError(true)}
|
||||
/>
|
||||
) : (
|
||||
<QuestionMarkIcon size={32} className="text-white w-full h-full" />
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-white w-full text-center font-semibold text-sm truncate px-2">
|
||||
{shortCut.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddShortCut = ({
|
||||
openShortCutModal,
|
||||
}: {
|
||||
openShortCutModal: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={openShortCutModal}
|
||||
className="w-24 h-24 flex-none rounded-xl bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all duration-300 ease-in-out flex flex-col items-center justify-center"
|
||||
>
|
||||
<PlusIcon className="w-8 h-8 text-white mb-2" />
|
||||
<h1 className="text-white text-xs font-medium">New Bookmark</h1>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewShortCutModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onAdd,
|
||||
editingShortcut,
|
||||
onDelete,
|
||||
setPopup,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDelete: (shortcut: Shortcut) => void;
|
||||
onAdd: (shortcut: Shortcut) => void;
|
||||
editingShortcut?: Shortcut | null;
|
||||
setPopup: (popup: PopupSpec) => void;
|
||||
}) => {
|
||||
const [name, setName] = useState(editingShortcut?.name || "");
|
||||
const [url, setUrl] = useState(editingShortcut?.url || "");
|
||||
const [faviconError, setFaviconError] = useState(false);
|
||||
const [isValidUrl, setIsValidUrl] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isValidUrl) {
|
||||
const faviconUrl = `https://www.google.com/s2/favicons?domain=${
|
||||
new URL(url).hostname
|
||||
}&sz=64`;
|
||||
onAdd({ name, url, favicon: faviconUrl });
|
||||
onClose();
|
||||
} else {
|
||||
console.error("Invalid URL submitted");
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Please enter a valid URL",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newUrl = e.target.value;
|
||||
setUrl(newUrl);
|
||||
setIsValidUrl(validateUrl(newUrl));
|
||||
setFaviconError(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsValidUrl(validateUrl(url));
|
||||
}, [url]);
|
||||
|
||||
const faviconUrl = isValidUrl
|
||||
? `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=64`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-[95%] sm:max-w-[425px] bg-neutral-900 border-none text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingShortcut ? "Edit Shortcut" : "Add New Shortcut"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingShortcut
|
||||
? "Modify your existing shortcut."
|
||||
: "Create a new shortcut for quick access to your favorite websites."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-6">
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-neutral-300"
|
||||
>
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="Enter shortcut name"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label
|
||||
htmlFor="url"
|
||||
className="text-sm font-medium text-neutral-300"
|
||||
>
|
||||
URL
|
||||
</Label>
|
||||
<Input
|
||||
id="url"
|
||||
value={url}
|
||||
onChange={handleUrlChange}
|
||||
className={`bg-neutral-800 border-neutral-700 text-white ${
|
||||
!isValidUrl && url ? "border-red-500" : ""
|
||||
}`}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
{!isValidUrl && url && (
|
||||
<p className="text-red-500 text-sm">Please enter a valid URL</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label className="text-sm font-medium text-neutral-300">
|
||||
Favicon Preview:
|
||||
</Label>
|
||||
<div className="w-8 h-8 relative flex items-center justify-center">
|
||||
{isValidUrl && !faviconError ? (
|
||||
<Image
|
||||
src={faviconUrl}
|
||||
alt="Favicon"
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full rounded-sm"
|
||||
onError={() => setFaviconError(true)}
|
||||
/>
|
||||
) : (
|
||||
<QuestionMarkIcon size={32} className="w-full h-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
disabled={!isValidUrl || !name}
|
||||
>
|
||||
{editingShortcut ? "Save Changes" : "Add Shortcut"}
|
||||
</Button>
|
||||
{editingShortcut && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => onDelete(editingShortcut)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const MaxShortcutsReachedModal = ({
|
||||
onClose,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
width="max-w-md"
|
||||
title="Maximum Shortcuts Reached"
|
||||
onOutsideClick={onClose}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-left text-neutral-900">
|
||||
You've reached the maximum limit of 8 shortcuts. To add a new
|
||||
shortcut, please remove an existing one.
|
||||
</p>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -119,7 +119,7 @@ export default function LogoWithText({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="mr-3 my-auto ml-auto"
|
||||
className="mr-3 my-auto ml-auto"
|
||||
onClick={() => {
|
||||
toggleSidebar();
|
||||
if (toggled) {
|
||||
@ -138,7 +138,7 @@ export default function LogoWithText({
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent className="!border-none">
|
||||
{toggled ? `Unpin sidebar` : "Pin sidebar"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
@ -2799,3 +2799,24 @@ export const AirtableIcon = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const QuestionMarkIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
_CompletedWelcomeFlowDummyComponent,
|
||||
_WelcomeModal,
|
||||
|
@ -10,7 +10,7 @@ export const ApiKeyModal = ({
|
||||
hide,
|
||||
setPopup,
|
||||
}: {
|
||||
hide: () => void;
|
||||
hide?: () => void;
|
||||
setPopup: (popup: PopupSpec) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -28,18 +28,25 @@ export const ApiKeyModal = ({
|
||||
<Modal
|
||||
title="Configure a Generative AI Model"
|
||||
width="max-w-3xl w-full"
|
||||
onOutsideClick={() => hide()}
|
||||
onOutsideClick={hide ? () => hide() : undefined}
|
||||
>
|
||||
<>
|
||||
<div className="mb-5 text-sm text-gray-700">
|
||||
Please provide an API Key – you can always change this or switch
|
||||
models later.
|
||||
<br />
|
||||
If you would rather look around first, you can{" "}
|
||||
<strong onClick={() => hide()} className="text-link cursor-pointer">
|
||||
skip this step
|
||||
</strong>
|
||||
.
|
||||
{hide && (
|
||||
<>
|
||||
If you would rather look around first, you can{" "}
|
||||
<strong
|
||||
onClick={() => hide()}
|
||||
className="text-link cursor-pointer"
|
||||
>
|
||||
skip this step
|
||||
</strong>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ApiKeyForm
|
||||
@ -47,7 +54,7 @@ export const ApiKeyModal = ({
|
||||
onSuccess={() => {
|
||||
router.refresh();
|
||||
refreshProviderInfo();
|
||||
hide();
|
||||
hide?.();
|
||||
}}
|
||||
providerOptions={providerOptions}
|
||||
/>
|
||||
|
@ -14,8 +14,10 @@ export const usePopupFromQuery = (messages: PopupMessages) => {
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Get the value for search param with key "message"
|
||||
const messageValue = searchParams.get("message");
|
||||
|
||||
// Check if any key from messages object is present in search params
|
||||
if (messageValue && messageValue in messages) {
|
||||
const popupMessage = messages[messageValue];
|
||||
|
@ -5,6 +5,7 @@ interface Option {
|
||||
key: string;
|
||||
display: string | JSX.Element;
|
||||
displayName?: string;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
export function FilterDropdown({
|
||||
options,
|
||||
@ -65,6 +66,7 @@ export function FilterDropdown({
|
||||
flex-none
|
||||
w-fit
|
||||
text-emphasis
|
||||
items-center
|
||||
gap-x-1
|
||||
${dropdownColor || "bg-background"}
|
||||
hover:bg-hover
|
||||
@ -80,6 +82,7 @@ export function FilterDropdown({
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{option.icon}
|
||||
{option.display}
|
||||
{isSelected && (
|
||||
<div className="ml-auto my-auto mr-1">
|
||||
|
226
web/src/components/search/filtering/HorizontalSourceSelector.tsx
Normal file
226
web/src/components/search/filtering/HorizontalSourceSelector.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover"; // shadcn popover
|
||||
import { FiBook, FiMap, FiTag, FiCalendar } from "react-icons/fi";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { Calendar } from "@/components/ui/calendar"; // or wherever your Calendar component lives
|
||||
import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";
|
||||
import { listSourceMetadata } from "@/lib/sources";
|
||||
import { getDateRangeString } from "@/lib/dateUtils";
|
||||
import { DateRangePickerValue } from "../../../app/ee/admin/performance/DateRangeSelector";
|
||||
import { Tag } from "@/lib/types";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
export interface SourceSelectorProps {
|
||||
timeRange: DateRangePickerValue | null;
|
||||
setTimeRange: React.Dispatch<
|
||||
React.SetStateAction<DateRangePickerValue | null>
|
||||
>;
|
||||
selectedSources: SourceMetadata[];
|
||||
setSelectedSources: React.Dispatch<React.SetStateAction<SourceMetadata[]>>;
|
||||
selectedDocumentSets: string[];
|
||||
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedTags: Tag[];
|
||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||
existingSources: string[]; // e.g. list of internalName that exist
|
||||
availableDocumentSets: { name: string }[];
|
||||
availableTags: Tag[];
|
||||
}
|
||||
|
||||
export function HorizontalSourceSelector({
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
existingSources,
|
||||
availableDocumentSets,
|
||||
availableTags,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSourceSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
if (prev.map((s) => s.internalName).includes(source.internalName)) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: Tag) => {
|
||||
setSelectedTags((prev: Tag[]) => {
|
||||
if (
|
||||
prev.some(
|
||||
(t) => t.tag_key === tag.tag_key && t.tag_value === tag.tag_value
|
||||
)
|
||||
) {
|
||||
return prev.filter(
|
||||
(t) => !(t.tag_key === tag.tag_key && t.tag_value === tag.tag_value)
|
||||
);
|
||||
} else {
|
||||
return [...prev, tag];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetSources = () => {
|
||||
setSelectedSources([]);
|
||||
};
|
||||
|
||||
const resetDocuments = () => {
|
||||
setSelectedDocumentSets([]);
|
||||
};
|
||||
|
||||
const resetTags = () => {
|
||||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center space-x-2">
|
||||
{/* Date Range Popover */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="
|
||||
flex items-center space-x-1 border
|
||||
border-border rounded-lg px-3 py-1.5
|
||||
hover:bg-hover text-sm cursor-pointer
|
||||
bg-background-search-filter
|
||||
"
|
||||
>
|
||||
<FiCalendar size={14} />
|
||||
<span>
|
||||
{timeRange?.from
|
||||
? getDateRangeString(timeRange.from, timeRange.to)
|
||||
: "Date Range"}
|
||||
</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background-search-filter border border-border rounded-md z-[200] p-2"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? { from: new Date(timeRange.from), to: new Date(timeRange.to) }
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const initialDate = daterange?.from || new Date();
|
||||
const endDate = daterange?.to || new Date();
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Sources Popover */}
|
||||
{existingSources.length > 0 && (
|
||||
<FilterDropdown
|
||||
icon={<FiMap size={14} />}
|
||||
backgroundColor="bg-background-search-filter"
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
dropdownWidth="w-40"
|
||||
defaultDisplay="Sources"
|
||||
resetValues={resetSources}
|
||||
width="w-fit"
|
||||
options={listSourceMetadata()
|
||||
.filter((source) => existingSources.includes(source.internalName))
|
||||
.map((source) => ({
|
||||
icon: (
|
||||
<SourceIcon sourceType={source.internalName} iconSize={14} />
|
||||
),
|
||||
key: source.internalName,
|
||||
display: (
|
||||
<span className="flex items-center space-x-2">
|
||||
<span>{source.displayName}</span>
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
optionClassName="truncate w-full break-all"
|
||||
selected={selectedSources.map((src) => src.internalName)}
|
||||
handleSelect={(option) => {
|
||||
const s = listSourceMetadata().find(
|
||||
(m) => m.internalName === option.key
|
||||
);
|
||||
if (s) handleSourceSelect(s);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Document Sets Popover */}
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<FilterDropdown
|
||||
icon={<FiBook size={14} />}
|
||||
backgroundColor="bg-background-search-filter"
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
dropdownWidth="w-40"
|
||||
defaultDisplay="Sets"
|
||||
resetValues={resetDocuments}
|
||||
width="w-fit"
|
||||
options={availableDocumentSets.map((docSet) => ({
|
||||
key: docSet.name,
|
||||
display: <>{docSet.name}</>,
|
||||
}))}
|
||||
optionClassName="truncate w-full break-all"
|
||||
selected={selectedDocumentSets}
|
||||
handleSelect={(option) => handleDocumentSetSelect(option.key)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tags Popover */}
|
||||
{availableTags.length > 0 && (
|
||||
<FilterDropdown
|
||||
icon={<FiTag size={14} />}
|
||||
backgroundColor="bg-background-search-filter"
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
dropdownWidth="w-64"
|
||||
defaultDisplay="Tags"
|
||||
resetValues={resetTags}
|
||||
width="w-fit"
|
||||
options={availableTags.map((tag) => ({
|
||||
key: `${tag.tag_key}=${tag.tag_value}`,
|
||||
display: (
|
||||
<span className="text-sm">
|
||||
{tag.tag_key}
|
||||
<b>=</b>
|
||||
{tag.tag_value}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
optionClassName="truncate w-full break-all"
|
||||
selected={selectedTags.map((t) => `${t.tag_key}=${t.tag_value}`)}
|
||||
handleSelect={(option) => {
|
||||
const [tKey, tValue] = option.key.split("=");
|
||||
const foundTag = availableTags.find(
|
||||
(tg) => tg.tag_key === tKey && tg.tag_value === tValue
|
||||
);
|
||||
if (foundTag) handleTagSelect(foundTag);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -12,14 +12,12 @@ import { openDocument } from "@/lib/search/utils";
|
||||
|
||||
export function Citation({
|
||||
children,
|
||||
link,
|
||||
document,
|
||||
index,
|
||||
updatePresentingDocument,
|
||||
icon,
|
||||
url,
|
||||
}: {
|
||||
link?: string;
|
||||
children?: JSX.Element | string | null | ReactNode;
|
||||
index?: number;
|
||||
updatePresentingDocument: (document: OnyxDocument) => void;
|
||||
|
@ -48,6 +48,7 @@ export const CustomTooltip = ({
|
||||
delay = 500,
|
||||
position = "bottom",
|
||||
disabled = false,
|
||||
className,
|
||||
}: {
|
||||
medium?: boolean;
|
||||
content: string | ReactNode;
|
||||
@ -61,6 +62,7 @@ export const CustomTooltip = ({
|
||||
citation?: boolean;
|
||||
position?: "top" | "bottom";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
|
||||
@ -115,7 +117,7 @@ export const CustomTooltip = ({
|
||||
<>
|
||||
<span
|
||||
ref={triggerRef}
|
||||
className="relative inline-block"
|
||||
className={`relative inline-block ${className}`}
|
||||
onMouseEnter={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
@ -125,9 +127,11 @@ export const CustomTooltip = ({
|
||||
!disabled &&
|
||||
createPortal(
|
||||
<div
|
||||
className={`min-w-8 fixed z-[1000] ${
|
||||
citation ? "max-w-[350px]" : "w-40"
|
||||
} ${large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"}
|
||||
className={`min-w-8 fixed z-[1000]
|
||||
${className}
|
||||
${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
|
||||
}
|
||||
transform -translate-x-1/2 text-sm
|
||||
${
|
||||
light
|
||||
|
26
web/src/components/ui/label.tsx
Normal file
26
web/src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
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 };
|
44
web/src/components/ui/radio-group.tsx
Normal file
44
web/src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-neutral-200 border-neutral-900 text-neutral-900 ring-offset-white focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:border-neutral-50 dark:text-neutral-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
@ -7,8 +7,10 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
|
||||
circleClassName?: string;
|
||||
}
|
||||
>(({ circleClassName, className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
|
||||
@ -19,7 +21,8 @@ const Switch = React.forwardRef<
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950"
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950",
|
||||
circleClassName
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
@ -24,7 +24,10 @@ import {
|
||||
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME,
|
||||
} from "@/components/resizable/constants";
|
||||
import { hasCompletedWelcomeFlowSS } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN } from "../constants";
|
||||
import {
|
||||
NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN,
|
||||
NEXT_PUBLIC_ENABLE_CHROME_EXTENSION,
|
||||
} from "../constants";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface FetchChatDataResult {
|
||||
@ -98,7 +101,9 @@ export async function fetchChatData(searchParams: {
|
||||
? `${fullUrl}?${searchParamsString}`
|
||||
: fullUrl;
|
||||
|
||||
return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`);
|
||||
if (!NEXT_PUBLIC_ENABLE_CHROME_EXTENSION) {
|
||||
return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (user && !user.is_verified && authTypeMetadata?.requiresVerification) {
|
||||
|
@ -91,6 +91,7 @@ export async function fetchSomeChatData(
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
|
||||
let user: User | null = null;
|
||||
|
||||
if (fetchOptions.includes("user")) {
|
||||
user = results.shift();
|
||||
if (!authDisabled && !user) {
|
||||
|
@ -87,5 +87,8 @@ export const NEXT_PUBLIC_TEST_ENV =
|
||||
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =
|
||||
process.env.NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED?.toLowerCase() === "true";
|
||||
|
||||
export const NEXT_PUBLIC_CLOUD_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_CLOUD_DOMAIN || "http://127.0.0.1:3000";
|
||||
export const NEXT_PUBLIC_ENABLE_CHROME_EXTENSION =
|
||||
process.env.NEXT_PUBLIC_ENABLE_CHROME_EXTENSION?.toLowerCase() === "true";
|
||||
|
||||
export const NEXT_PUBLIC_WEB_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_WEB_DOMAIN || "http://127.0.0.1:3000";
|
||||
|
@ -1,4 +1,23 @@
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useNightTime = () => {
|
||||
const [isNight, setIsNight] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkNightTime = () => {
|
||||
const currentHour = new Date().getHours();
|
||||
setIsNight(currentHour >= 18 || currentHour < 6);
|
||||
};
|
||||
|
||||
checkNightTime();
|
||||
const interval = setInterval(checkNightTime, 60000); // Check every minute
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return { isNight };
|
||||
};
|
||||
|
||||
export function getXDaysAgo(daysAgo: number) {
|
||||
const today = new Date();
|
||||
|
33
web/src/lib/extension/constants.ts
Normal file
33
web/src/lib/extension/constants.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const darkExtensionImages = [
|
||||
"https://images.unsplash.com/photo-1692520883599-d543cfe6d43d?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1520330461350-508fab483d6a?q=80&w=2723&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
];
|
||||
|
||||
export const lightExtensionImages = [
|
||||
"https://images.unsplash.com/photo-1473830439578-14e9a9e61d55?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1500964757637-c85e8a162699?q=80&w=2703&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
];
|
||||
|
||||
// Chrome message types
|
||||
export const CHROME_MESSAGE = {
|
||||
PREFERENCES_UPDATED: "PREFERENCES_UPDATED",
|
||||
ONYX_APP_LOADED: "ONYX_APP_LOADED",
|
||||
SET_DEFAULT_NEW_TAB: "SET_DEFAULT_NEW_TAB",
|
||||
LOAD_NEW_CHAT_PAGE: "LOAD_NEW_CHAT_PAGE",
|
||||
LOAD_NEW_PAGE: "LOAD_NEW_PAGE",
|
||||
AUTH_REQUIRED: "AUTH_REQUIRED",
|
||||
};
|
||||
|
||||
export const SUBMIT_MESSAGE_TYPES = {
|
||||
PAGE_CHANGE: "PAGE_CHANGE",
|
||||
};
|
||||
|
||||
export const LocalStorageKeys = {
|
||||
THEME: "onyxTheme",
|
||||
LIGHT_BG_URL: "lightBgUrl",
|
||||
DARK_BG_URL: "darkBgUrl",
|
||||
SHORTCUTS: "shortCuts",
|
||||
SHOW_SHORTCUTS: "showShortcuts",
|
||||
USE_ONYX_AS_NEW_TAB: "useOnyxAsDefaultNewTab",
|
||||
};
|
51
web/src/lib/extension/utils.ts
Normal file
51
web/src/lib/extension/utils.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useEffect } from "react";
|
||||
import { CHROME_MESSAGE } from "./constants";
|
||||
export function sendSetDefaultNewTabMessage(value: boolean) {
|
||||
if (typeof window !== "undefined" && window.parent) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.SET_DEFAULT_NEW_TAB, value },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const sendAuthRequiredMessage = () => {
|
||||
if (typeof window !== "undefined" && window.parent) {
|
||||
window.parent.postMessage({ type: CHROME_MESSAGE.AUTH_REQUIRED }, "*");
|
||||
}
|
||||
};
|
||||
|
||||
export const useSendAuthRequiredMessage = () => {
|
||||
useEffect(() => {
|
||||
sendAuthRequiredMessage();
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const sendMessageToParent = () => {
|
||||
if (typeof window !== "undefined" && window.parent) {
|
||||
window.parent.postMessage({ type: CHROME_MESSAGE.ONYX_APP_LOADED }, "*");
|
||||
}
|
||||
};
|
||||
export const useSendMessageToParent = () => {
|
||||
useEffect(() => {
|
||||
sendMessageToParent();
|
||||
}, []);
|
||||
};
|
||||
|
||||
export function notifyExtensionOfThemeChange(
|
||||
newTheme: string,
|
||||
newBgUrl: string
|
||||
) {
|
||||
if (typeof window !== "undefined" && window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: CHROME_MESSAGE.PREFERENCES_UPDATED,
|
||||
payload: {
|
||||
theme: newTheme,
|
||||
backgroundUrl: newBgUrl,
|
||||
},
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
@ -7,12 +7,13 @@ import {
|
||||
UserGroup,
|
||||
ConnectorStatus,
|
||||
CCPairBasicInfo,
|
||||
ValidSources,
|
||||
} from "@/lib/types";
|
||||
import useSWR, { mutate, useSWRConfig } from "swr";
|
||||
import { errorHandlingFetcher } from "./fetcher";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { SourceMetadata } from "./search/interfaces";
|
||||
import { Filters, SourceMetadata } from "./search/interfaces";
|
||||
import { destructureValue, structureValue } from "./llm/utils";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { AllUsersResponse } from "./types";
|
||||
@ -24,6 +25,8 @@ import {
|
||||
LLMProviderDescriptor,
|
||||
} from "@/app/admin/configuration/llm/interfaces";
|
||||
import { isAnthropic } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { getSourceMetadata } from "./sources";
|
||||
import { buildFilters } from "./search/utils";
|
||||
|
||||
const CREDENTIAL_URL = "/api/manage/admin/credential";
|
||||
|
||||
@ -147,6 +150,13 @@ export interface FilterManager {
|
||||
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedTags: Tag[];
|
||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||
getFilterString: () => string;
|
||||
buildFiltersFromQueryString: (
|
||||
filterString: string,
|
||||
availableSources: ValidSources[],
|
||||
availableDocumentSets: string[],
|
||||
availableTags: Tag[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function useFilters(): FilterManager {
|
||||
@ -157,6 +167,97 @@ export function useFilters(): FilterManager {
|
||||
);
|
||||
const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
|
||||
|
||||
const getFilterString = () => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (timeRange) {
|
||||
params.set("from", timeRange.from.toISOString());
|
||||
params.set("to", timeRange.to.toISOString());
|
||||
}
|
||||
|
||||
if (selectedSources.length > 0) {
|
||||
const sourcesParam = selectedSources
|
||||
.map((source) => encodeURIComponent(source.internalName))
|
||||
.join(",");
|
||||
params.set("sources", sourcesParam);
|
||||
}
|
||||
|
||||
if (selectedDocumentSets.length > 0) {
|
||||
const docSetsParam = selectedDocumentSets
|
||||
.map((ds) => encodeURIComponent(ds))
|
||||
.join(",");
|
||||
params.set("documentSets", docSetsParam);
|
||||
}
|
||||
|
||||
if (selectedTags.length > 0) {
|
||||
const tagsParam = selectedTags
|
||||
.map((tag) => encodeURIComponent(tag.tag_value))
|
||||
.join(",");
|
||||
params.set("tags", tagsParam);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
return queryString ? `&${queryString}` : "";
|
||||
};
|
||||
|
||||
function buildFiltersFromQueryString(
|
||||
filterString: string,
|
||||
availableSources: ValidSources[],
|
||||
availableDocumentSets: string[],
|
||||
availableTags: Tag[]
|
||||
): void {
|
||||
const params = new URLSearchParams(filterString);
|
||||
|
||||
// Parse the "from" parameter as a DateRangePickerValue
|
||||
let newTimeRange: DateRangePickerValue | null = null;
|
||||
const fromParam = params.get("from");
|
||||
const toParam = params.get("to");
|
||||
if (fromParam && toParam) {
|
||||
const fromDate = new Date(fromParam);
|
||||
const toDate = new Date(toParam);
|
||||
if (!isNaN(fromDate.getTime()) && !isNaN(toDate.getTime())) {
|
||||
newTimeRange = { from: fromDate, to: toDate, selectValue: "" };
|
||||
}
|
||||
}
|
||||
|
||||
// Parse sources
|
||||
const availableSourcesMetadata = availableSources.map(getSourceMetadata);
|
||||
let newSelectedSources: SourceMetadata[] = [];
|
||||
const sourcesParam = params.get("sources");
|
||||
if (sourcesParam) {
|
||||
const sourceNames = sourcesParam.split(",").map(decodeURIComponent);
|
||||
newSelectedSources = availableSourcesMetadata.filter((source) =>
|
||||
sourceNames.includes(source.internalName)
|
||||
);
|
||||
}
|
||||
|
||||
// Parse document sets
|
||||
let newSelectedDocSets: string[] = [];
|
||||
const docSetsParam = params.get("documentSets");
|
||||
if (docSetsParam) {
|
||||
const docSetNames = docSetsParam.split(",").map(decodeURIComponent);
|
||||
newSelectedDocSets = availableDocumentSets.filter((ds) =>
|
||||
docSetNames.includes(ds)
|
||||
);
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
let newSelectedTags: Tag[] = [];
|
||||
const tagsParam = params.get("tags");
|
||||
if (tagsParam) {
|
||||
const tagValues = tagsParam.split(",").map(decodeURIComponent);
|
||||
newSelectedTags = availableTags.filter((tag) =>
|
||||
tagValues.includes(tag.tag_value)
|
||||
);
|
||||
}
|
||||
|
||||
// Update filter manager's values instead of returning
|
||||
setTimeRange(newTimeRange);
|
||||
setSelectedSources(newSelectedSources);
|
||||
setSelectedDocumentSets(newSelectedDocSets);
|
||||
setSelectedTags(newSelectedTags);
|
||||
}
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
@ -166,6 +267,8 @@ export function useFilters(): FilterManager {
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
getFilterString,
|
||||
buildFiltersFromQueryString,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { Tag, ValidSources } from "../types";
|
||||
import {
|
||||
Filters,
|
||||
LoadedOnyxDocument,
|
||||
OnyxDocument,
|
||||
SourceMetadata,
|
||||
} from "./interfaces";
|
||||
import { Filters, OnyxDocument, SourceMetadata } from "./interfaces";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
|
||||
export const buildFilters = (
|
||||
|
@ -388,3 +388,26 @@ export function getSourcesForPersona(persona: Persona): ValidSources[] {
|
||||
});
|
||||
return personaSources;
|
||||
}
|
||||
|
||||
export async function fetchTitleFromUrl(url: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
// If the remote site has no CORS header, this may fail in the browser
|
||||
mode: "cors",
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Non-200 response, treat as a failure
|
||||
return null;
|
||||
}
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
// If the site has <title>My Demo Page</title>, we retrieve "My Demo Page"
|
||||
const pageTitle = doc.querySelector("title")?.innerText.trim() ?? null;
|
||||
return pageTitle;
|
||||
} catch (error) {
|
||||
console.error("Error fetching page title:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user