diff --git a/dev/main.tsx b/dev/main.tsx new file mode 100644 index 0000000..1b9bff0 --- /dev/null +++ b/dev/main.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +// Import styles FIRST to ensure CSS variables are loaded +import '../src/styles/variables.css'; +// Then import the component (which also imports variables, but order matters) +import { ChatWidget } from '../src'; + +// ============================================ +// 🔧 CONFIGURE YOUR TEST WIDGET HERE +// ============================================ + +const TEST_CONFIG = { + // Replace with your real embed ID to test with real API data + embedId: 'erakRkiwWD3XPpAWbjgSi', + + // Set to true to use mock responses instead of real API + mockMode: false, + + // Widget starts closed so we can test auto-open + defaultOpen: false, + + // Position: 'bottom-right' | 'bottom-left' | 'inline' + position: 'bottom-right' as const, + + theme: 'dark' as const, + + // Auto-open after 3 seconds and send trigger message + timeToOpen: 3, + + // Voice mode configuration + voiceTokenUrl: 'https://lk-demo-beta.vercel.app/api/token', + voiceAgentName: 'voice-agent', + enableVoiceMode: true, + + // Show collapse button when widget is open (default: true) + showCollapseButton: true, + + // Home page configuration + homeImage: 'https://images.unsplash.com/photo-1531746790731-6c087fecd65a?w=400&h=300&fit=crop', + homeTitle: 'Getting Started Guide', + homeDescription: 'Learn how to get the most out of our platform with our comprehensive guide.', + homeLink: 'https://docs.brainbase.com/getting-started', + + // Optional overrides (leave undefined to use values from API/database) + // primaryColor: '#1a1a2e', + // agentName: 'Test Agent', + // welcomeMessage: 'Hello! This is a test message.', +}; + +// ============================================ + +function App() { + return ( + console.log('Session started:', sessionId)} + onSessionEnd={(session) => console.log('Session ended:', session)} + onMessage={(message) => console.log('Message:', message)} + onError={(error) => console.error('Error:', error)} + /> + ); +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..96a4f05 --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Chat Widget Test + + + +
+

🧪 Chat Widget Test Page

+

This page loads the ChatWidget with real API data. Edit the embedId in dev/main.tsx to test different deployments.

+ +
+

Sample Content

+

+ This is a sample page to test the chat widget overlay. + The widget should appear in the bottom-right corner. + Click the chat button to open it and test your UI changes! +

+
+
+ +
+ + + diff --git a/package-lock.json b/package-lock.json index bd06a04..9eb3515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.19", "license": "MIT", "dependencies": { + "livekit-client": "^2.17.0", + "lucide-react": "^0.563.0", "react-markdown": "^10.1.0" }, "devDependencies": { @@ -78,7 +80,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -308,6 +309,7 @@ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -360,6 +362,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1179,6 +1187,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.42.2", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.42.2.tgz", + "integrity": "sha512-0jeCwoMJKcwsZICg5S6RZM4xhJoF78qMvQELjACJQn6/VB+jmiySQKOSELTXvPBVafHfEbMlqxUw2UR1jTXs2g==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -1725,7 +1748,6 @@ "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -2144,7 +2166,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2225,6 +2248,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2289,7 +2319,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2353,7 +2382,6 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2777,7 +2805,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2984,7 +3011,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3387,7 +3413,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -3504,7 +3531,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3781,6 +3807,15 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -4354,6 +4389,15 @@ "dev": true, "license": "MIT" }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4464,6 +4508,27 @@ "node": ">= 0.8.0" } }, + "node_modules/livekit-client": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.17.0.tgz", + "integrity": "sha512-BD1QUS44ancVTBdnAher0aO7DV5holFYH2lYradYT/HgXtn6R8xPyvtDAH3UH40jGcesDo9fEopCFwEdOgrIhg==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.42.2", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "ts-debounce": "^4.0.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "^9.0.1" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, "node_modules/local-pkg": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", @@ -4512,6 +4577,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4551,12 +4629,22 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5556,6 +5644,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5571,6 +5660,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -5620,7 +5710,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5666,7 +5755,6 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5680,7 +5768,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -5843,7 +5932,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5893,6 +5981,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5903,6 +6001,21 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -6006,7 +6119,6 @@ "integrity": "sha512-dK1p2LKzAdea60APGo/vMbF+X/D7eVZsv8ijnLVvfMBjScdDBgxfIn025mRtOwqECb/UN9cIpPs5XEWAeLpYMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", @@ -6255,7 +6367,6 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6346,6 +6457,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-debounce": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ts-debounce/-/ts-debounce-4.0.0.tgz", + "integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==", + "license": "MIT" + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -6375,7 +6492,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -6391,13 +6507,21 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6635,7 +6759,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6756,6 +6879,19 @@ "dev": true, "license": "MIT" }, + "node_modules/webrtc-adapter": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz", + "integrity": "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 410990a..c527f7e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,8 @@ "vite-plugin-dts": "^4.3.0" }, "dependencies": { + "livekit-client": "^2.17.0", + "lucide-react": "^0.563.0", "react-markdown": "^10.1.0" } } diff --git a/src/api/client.ts b/src/api/client.ts index 828b98a..1b3a87d 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -30,6 +30,7 @@ export function createAPIClient( flowId: '', welcomeMessage: data.welcomeMessage, agentName: data.agentName, + agentRole: data.agentRole, agentLogoUrl: data.agentLogoUrl, primaryColor: data.primaryColor, styling: data.styling, diff --git a/src/api/mock.ts b/src/api/mock.ts index f802014..c55a81b 100644 --- a/src/api/mock.ts +++ b/src/api/mock.ts @@ -12,6 +12,7 @@ const DEFAULT_MOCK_CONFIG: DeploymentConfig = { flowId: 'mock-flow-id', // Note: welcomeMessage is handled by the engine, not the widget agentName: 'AI Assistant', + agentRole: 'AI Agent', agentLogoUrl: undefined, primaryColor: '#1a1a2e', styling: {}, diff --git a/src/components/ChatContainer/ChatContainer.module.css b/src/components/ChatContainer/ChatContainer.module.css index 007a11c..62a6305 100644 --- a/src/components/ChatContainer/ChatContainer.module.css +++ b/src/components/ChatContainer/ChatContainer.module.css @@ -10,18 +10,10 @@ box-shadow: var(--bb-shadow-xl); overflow: hidden; overscroll-behavior: contain; - animation: slideUp 0.3s ease; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } + /* Smooth transition for expand/collapse */ + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), + height 0.3s cubic-bezier(0.4, 0, 0.2, 1), + max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .body { diff --git a/src/components/ChatContainer/ChatContainer.tsx b/src/components/ChatContainer/ChatContainer.tsx index e84fe92..4c94782 100644 --- a/src/components/ChatContainer/ChatContainer.tsx +++ b/src/components/ChatContainer/ChatContainer.tsx @@ -4,6 +4,7 @@ import { ChatHeader } from '../ChatHeader'; import { MessageList } from '../MessageList'; import { MessageInput } from '../MessageInput'; import { PoweredBy } from '../PoweredBy'; +import { VoiceMode } from '../VoiceMode'; import styles from './ChatContainer.module.css'; export interface ChatContainerProps { @@ -11,9 +12,17 @@ export interface ChatContainerProps { messages: Message[]; toolCalls?: ToolCall[]; isLoading: boolean; + showTypingIndicator?: boolean; + isExpanded?: boolean; + headerSubtitle?: string; + voiceTokenUrl?: string; + voiceAgentName?: string; + enableVoiceMode?: boolean; onSendMessage: (message: string) => void; onClose?: () => void; + onBack?: () => void; onNewChat?: () => void; + onExpandWindow?: () => void; } export const ChatContainer: React.FC = ({ @@ -21,13 +30,58 @@ export const ChatContainer: React.FC = ({ messages, toolCalls = [], isLoading, + showTypingIndicator = false, + isExpanded = false, + headerSubtitle, + voiceTokenUrl, + voiceAgentName = 'voice-agent', + enableVoiceMode = false, onSendMessage, onClose, + onBack, onNewChat, + onExpandWindow, }) => { const [showConfirmation, setShowConfirmation] = useState(false); + const [isVoiceMode, setIsVoiceMode] = useState(false); const hasMessages = messages.length > 0; + // Voice mode is available if both enableVoiceMode is true AND voiceTokenUrl is provided + const isVoiceAvailable = enableVoiceMode && !!voiceTokenUrl; + + const handleVoiceClick = () => { + if (isVoiceAvailable) { + setIsVoiceMode(true); + } + }; + + const handleVoiceClose = () => { + setIsVoiceMode(false); + }; + + // Download transcript as a text file + const handleDownloadTranscript = () => { + if (messages.length === 0) return; + + const transcript = messages + .map((msg) => { + const role = msg.role === 'user' ? 'You' : (config.agentName || 'Assistant'); + const timestamp = new Date(msg.timestamp).toLocaleString(); + return `[${timestamp}] ${role}:\n${msg.content}\n`; + }) + .join('\n'); + + const blob = new Blob([transcript], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `chat-transcript-${new Date().toISOString().split('T')[0]}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + const handleNewChatRequest = () => { setShowConfirmation(true); }; @@ -46,26 +100,48 @@ export const ChatContainer: React.FC = ({
- - + {isVoiceMode && isVoiceAvailable ? ( + + ) : ( + <> + + {/* TODO: implement attachment */}} + onEmoji={() => {/* TODO: implement emoji picker */}} + onGif={() => {/* TODO: implement GIF picker */}} + onVoice={isVoiceAvailable ? handleVoiceClick : undefined} + /> + + )}
diff --git a/src/components/ChatHeader/ChatHeader.module.css b/src/components/ChatHeader/ChatHeader.module.css index fb78f1c..3d84fb4 100644 --- a/src/components/ChatHeader/ChatHeader.module.css +++ b/src/components/ChatHeader/ChatHeader.module.css @@ -1,6 +1,6 @@ .header { position: relative; - overflow: hidden; + overflow: visible; /* Smooth transition with better easing */ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } @@ -20,24 +20,23 @@ .headerBackground { position: absolute; inset: 0; - background: linear-gradient( - 135deg, - var(--bb-primary-color) 0%, - color-mix(in srgb, var(--bb-primary-color) 70%, #2d2d5a) 50%, - color-mix(in srgb, var(--bb-primary-color) 50%, #1a1a2e) 100% - ); + background: var(--bb-primary-color); transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1); } +/* Gradient overlay - only shown when primaryGradient is enabled */ .headerBackground::before { content: ''; position: absolute; inset: 0; - background: radial-gradient( - ellipse at 30% 20%, - rgba(255, 255, 255, 0.08) 0%, - transparent 50% + background: linear-gradient( + 135deg, + transparent 0%, + color-mix(in srgb, var(--bb-primary-color) 70%, #2d2d5a) 50%, + color-mix(in srgb, var(--bb-primary-color) 50%, #1a1a2e) 100% ); + opacity: var(--bb-primary-gradient, 0); + transition: opacity 0.3s ease; } .headerBackground::after { @@ -48,7 +47,7 @@ right: 0; height: 60px; background: linear-gradient(to top, rgba(0, 0, 0, 0.1), transparent); - opacity: 1; + opacity: var(--bb-primary-gradient, 0); transition: opacity 0.3s ease; } @@ -83,41 +82,39 @@ } .agentLogo { - border-radius: var(--bb-radius-md); + border-radius: var(--bb-radius-sm); object-fit: cover; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .header:not(.compact) .agentLogo { - width: 36px; - height: 36px; + width: 42px; + height: 42px; } .compact .agentLogo { - width: 32px; - height: 32px; + width: 36px; + height: 36px; } .agentLogoPlaceholder { - border-radius: var(--bb-radius-md); - background: var(--bb-primary-color); + border-radius: var(--bb-radius-sm); + background: var(--bb-accent-color-custom, var(--bb-primary-color)); display: flex; align-items: center; justify-content: center; color: var(--bb-text-inverse); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .header:not(.compact) .agentLogoPlaceholder { - width: 36px; - height: 36px; + width: 42px; + height: 42px; } .compact .agentLogoPlaceholder { - width: 32px; - height: 32px; + width: 36px; + height: 36px; } .brainbaseLogo { @@ -125,20 +122,66 @@ } .header:not(.compact) .brainbaseLogo { + width: 28px; + height: 28px; +} + +.compact .brainbaseLogo { width: 24px; height: 24px; } -.compact .brainbaseLogo { - width: 22px; - height: 22px; +/* Back button / left chevron */ +.backButton { + width: 24px; + height: 24px; + border-radius: var(--bb-radius-full); + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--bb-header-text-color, var(--bb-text-inverse)); + opacity: 0.8; + transition: opacity var(--bb-transition-fast), transform var(--bb-transition-fast); + margin-right: 2px; + margin-left: -4px; +} + +.backButton:hover { + opacity: 1; +} + +.backButton:active { + transform: scale(0.9); +} + +.backButton svg { + width: 20px; + height: 20px; +} + +.agentTextWrapper { + display: flex; + flex-direction: column; + gap: 1px; } .agentName { - color: var(--bb-text-inverse); - font-size: var(--bb-font-size-md); + color: var(--bb-header-text-color, var(--bb-text-inverse)); + font-size: var(--bb-agent-name-font-size, 16px); font-weight: var(--bb-font-weight-semibold); opacity: 0.95; + line-height: 1.2; +} + +.headerSubtitle { + color: var(--bb-header-text-color, var(--bb-text-inverse)); + font-size: calc(var(--bb-agent-name-font-size, 16px) - 2px); + font-weight: var(--bb-font-weight-normal); + opacity: 0.7; + line-height: 1.3; } .actions { @@ -150,13 +193,13 @@ width: 32px; height: 32px; border-radius: var(--bb-radius-full); - background: rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; - color: var(--bb-text-inverse); + color: var(--bb-header-text-color, var(--bb-text-inverse)); transition: background var(--bb-transition-fast), transform var(--bb-transition-fast); } @@ -181,8 +224,8 @@ /* Welcome text with smooth collapse */ .welcomeText { - color: var(--bb-text-inverse); - overflow: hidden; + color: var(--bb-header-text-color, var(--bb-text-inverse)); + overflow: visible; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } @@ -214,3 +257,78 @@ line-height: 1.2; } +/* Menu container for dropdown positioning */ +.menuContainer { + position: relative; +} + +/* Dropdown menu */ +.dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 180px; + background: var(--bb-surface-bg); + border-radius: var(--bb-radius-md); + box-shadow: var(--bb-shadow-lg); + padding: var(--bb-spacing-xs); + z-index: 9999; + animation: dropdownSlideIn 0.15s ease; +} + +@keyframes dropdownSlideIn { + from { + opacity: 0; + transform: translateY(-4px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.dropdownItem { + display: flex; + align-items: center; + gap: var(--bb-spacing-sm); + width: 100%; + padding: var(--bb-spacing-sm) var(--bb-spacing-md); + background: transparent; + border: none; + border-radius: var(--bb-radius-sm); + cursor: pointer; + font-family: var(--bb-font-family); + font-size: var(--bb-font-size-sm); + font-weight: var(--bb-font-weight-medium); + color: var(--bb-text-primary); + text-align: left; + white-space: nowrap; + transition: background var(--bb-transition-fast); +} + +.dropdownItem:hover { + background: var(--bb-surface-secondary); +} + +.dropdownItem:active { + background: var(--bb-surface-tertiary); +} + +/* Explicitly size SVG icons in dropdown items */ +.dropdownItem svg { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + flex-shrink: 0; + color: var(--bb-text-secondary); +} + +.dropdownIcon { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + flex-shrink: 0; + color: var(--bb-text-secondary); +} diff --git a/src/components/ChatHeader/ChatHeader.tsx b/src/components/ChatHeader/ChatHeader.tsx index da4b513..d19d30a 100644 --- a/src/components/ChatHeader/ChatHeader.tsx +++ b/src/components/ChatHeader/ChatHeader.tsx @@ -1,15 +1,22 @@ -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { BrainbaseLogo } from '../BrainbaseLogo'; import styles from './ChatHeader.module.css'; export interface ChatHeaderProps { agentName?: string; agentLogoUrl?: string; + /** Description shown below agent name (e.g., "The team can also help") */ + headerSubtitle?: string; welcomeTitle?: string; welcomeSubtitle?: string; onClose?: () => void; + onBack?: () => void; onNewChatRequest?: () => void; - showNewChatButton?: boolean; + onExpandWindow?: () => void; + onDownloadTranscript?: () => void; + showMenuButton?: boolean; + /** Whether the widget is currently expanded */ + isExpanded?: boolean; /** When true, shows a compact header without welcome text */ compact?: boolean; } @@ -17,13 +24,37 @@ export interface ChatHeaderProps { export const ChatHeader: React.FC = ({ agentName = 'AI Assistant', agentLogoUrl, + headerSubtitle, welcomeTitle, welcomeSubtitle, onClose, + onBack, onNewChatRequest, - showNewChatButton = false, + onExpandWindow, + onDownloadTranscript, + showMenuButton = false, + isExpanded = false, compact = false, }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const menuRef = useRef(null); + + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + }; + + if (isMenuOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isMenuOpen]); + // Use provided values or defaults const displayTitle = welcomeTitle || 'Hello there.'; // Only show default subtitle if no custom welcomeTitle was provided @@ -31,12 +62,34 @@ export const ChatHeader: React.FC = ({ ? welcomeSubtitle : (welcomeTitle ? undefined : 'How can we help?'); + const handleMenuItemClick = (action: () => void) => { + action(); + setIsMenuOpen(false); + }; + return (
+ {/* Left chevron / back button */} + {agentLogoUrl ? ( = ({ />
)} - {agentName} +
+ {agentName} + {headerSubtitle && ( + {headerSubtitle} + )} +
- {showNewChatButton && onNewChatRequest && ( - + {showMenuButton && ( +
+ + + {isMenuOpen && ( +
+ {onExpandWindow && ( + + )} + {onDownloadTranscript && ( + + )} + {onNewChatRequest && ( + + )} +
+ )} +
)} {onClose && ( + )} +
) : ( setIsOpen(true)} agentName={effectiveConfig.agentName} agentLogoUrl={effectiveConfig.agentLogoUrl} + customIcon={toggleIcon} /> )}
diff --git a/src/components/HomePage/HomePage.module.css b/src/components/HomePage/HomePage.module.css new file mode 100644 index 0000000..5201701 --- /dev/null +++ b/src/components/HomePage/HomePage.module.css @@ -0,0 +1,272 @@ +.container { + position: relative; + display: flex; + flex-direction: column; + width: var(--bb-widget-width); + height: var(--bb-widget-height); + max-height: var(--bb-widget-max-height); + background: var(--bb-surface-bg); + border-radius: var(--bb-radius-xl); + box-shadow: var(--bb-shadow-xl); + overflow: hidden; + overscroll-behavior: contain; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), + height 0.3s cubic-bezier(0.4, 0, 0.2, 1), + max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Responsive adjustments */ +@media (max-width: 480px) { + .container { + width: 100vw; + height: 100vh; + max-height: 100vh; + border-radius: 0; + } +} + +/* Header */ +.header { + position: relative; + padding: var(--bb-spacing-lg) var(--bb-spacing-xl); +} + +.headerBackground { + position: absolute; + inset: 0; + background: var(--bb-primary-color); +} + +.headerContent { + position: relative; + z-index: 1; +} + +.agentInfo { + display: flex; + align-items: center; + gap: var(--bb-spacing-md); +} + +.headerLogo { + width: 42px; + height: 42px; + border-radius: var(--bb-radius-sm); + object-fit: cover; +} + +.headerLogoPlaceholder { + width: 42px; + height: 42px; + border-radius: var(--bb-radius-sm); + background: var(--bb-accent-color-custom, var(--bb-primary-color)); + display: flex; + align-items: center; + justify-content: center; +} + +.headerBrainbaseLogo { + width: 28px; + height: 28px; +} + +.headerAgentName { + color: var(--bb-header-text-color, var(--bb-text-inverse)); + font-size: var(--bb-agent-name-font-size, 16px); + font-weight: var(--bb-font-weight-semibold); +} + +.content { + flex: 1; + overflow-y: auto; + padding: var(--bb-spacing-xl); + display: flex; + flex-direction: column; + gap: var(--bb-spacing-xl); +} + +/* Info Card - clickable module below Ask a Question */ +.infoCard { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; + background: var(--bb-surface-secondary); + border: 1px solid var(--bb-border-color); + border-radius: var(--bb-radius-lg); + text-align: left; + width: 100%; + cursor: default; + overflow: hidden; + transition: background var(--bb-transition-fast), border-color var(--bb-transition-fast), transform var(--bb-transition-fast); +} + +.infoCard.clickable { + cursor: pointer; +} + +.infoCard.clickable:hover { + background: var(--bb-surface-tertiary); + border-color: var(--bb-text-tertiary); + transform: translateY(-2px); +} + +.infoCard.clickable:active { + transform: translateY(0); +} + +.infoCard:disabled { + cursor: default; +} + +.infoImageWrapper { + width: 100%; + height: 180px; + overflow: hidden; +} + +.infoImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.infoContent { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--bb-spacing-lg); + gap: var(--bb-spacing-md); +} + +.infoText { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.infoTitle { + margin: 0; + font-size: var(--bb-font-size-lg); + font-weight: var(--bb-font-weight-semibold); + color: var(--bb-text-primary); + line-height: 1.3; +} + +.infoDescription { + margin: 0; + font-size: var(--bb-font-size-sm); + color: var(--bb-text-secondary); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.infoChevron { + flex-shrink: 0; + width: 24px; + height: 24px; + color: var(--bb-text-tertiary); +} + +/* Ask a Question Card */ +.askCard { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--bb-spacing-lg); + background: var(--bb-surface-secondary); + border: 1px solid var(--bb-border-color); + border-radius: var(--bb-radius-lg); + cursor: pointer; + transition: background var(--bb-transition-fast), border-color var(--bb-transition-fast); + text-align: left; + width: 100%; +} + +.askCard:hover { + background: var(--bb-surface-tertiary); + border-color: var(--bb-border-color); +} + +.askContent { + display: flex; + flex-direction: column; + gap: 2px; +} + +.askTitle { + font-size: var(--bb-font-size-md); + font-weight: var(--bb-font-weight-semibold); + color: var(--bb-text-primary); +} + +.askSubtitle { + font-size: var(--bb-font-size-sm); + color: var(--bb-text-secondary); +} + +.askIcon { + display: flex; + align-items: center; + gap: var(--bb-spacing-sm); + color: var(--bb-text-tertiary); +} + +.agentLogo { + width: 24px; + height: 24px; + border-radius: var(--bb-radius-sm); + object-fit: cover; +} + +.brainbaseLogo { + width: 24px; + height: 24px; +} + +.chevron { + width: 20px; + height: 20px; +} + +/* Footer Navigation */ +.footer { + display: flex; + justify-content: space-around; + padding: var(--bb-spacing-md) var(--bb-spacing-lg); + background: var(--bb-surface-bg); + border-top: 1px solid var(--bb-border-color); +} + +.navItem { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--bb-spacing-xs); + padding: var(--bb-spacing-sm) var(--bb-spacing-md); + background: transparent; + border: none; + cursor: pointer; + color: var(--bb-text-tertiary); + font-size: var(--bb-font-size-xs); + font-family: var(--bb-font-family); + transition: color var(--bb-transition-fast); +} + +.navItem:hover { + color: var(--bb-text-secondary); +} + +.navItem.active { + color: var(--bb-text-primary); +} + +.navIcon { + width: 24px; + height: 24px; +} diff --git a/src/components/HomePage/HomePage.tsx b/src/components/HomePage/HomePage.tsx new file mode 100644 index 0000000..fd1ab08 --- /dev/null +++ b/src/components/HomePage/HomePage.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { BrainbaseLogo } from '../BrainbaseLogo'; +import styles from './HomePage.module.css'; + +export interface HomePageProps { + agentName?: string; + agentLogoUrl?: string; + homeImage?: string; + homeTitle?: string; + homeDescription?: string; + homeLink?: string; + onStartChat: () => void; + onNavigate: (page: 'home' | 'messages') => void; + currentPage: 'home' | 'messages'; +} + +export const HomePage: React.FC = ({ + agentName = 'AI Assistant', + agentLogoUrl, + homeImage, + homeTitle, + homeDescription, + homeLink, + onStartChat, + onNavigate, + currentPage, +}) => { + const handleHomeCardClick = () => { + if (homeLink) { + window.open(homeLink, '_blank', 'noopener,noreferrer'); + } + }; + return ( +
+ {/* Header */} +
+
+
+
+ {agentLogoUrl ? ( + {agentName} + ) : ( +
+ +
+ )} + {agentName} +
+
+
+ +
+ {/* Ask a Question Card */} + + + {/* Info Card - clickable module that opens homeLink */} + {(homeImage || homeTitle || homeDescription) && ( + + )} +
+ + {/* Footer Navigation */} + +
+ ); +}; diff --git a/src/components/HomePage/index.ts b/src/components/HomePage/index.ts new file mode 100644 index 0000000..aa9cb40 --- /dev/null +++ b/src/components/HomePage/index.ts @@ -0,0 +1,2 @@ +export { HomePage } from './HomePage'; +export type { HomePageProps } from './HomePage'; diff --git a/src/components/Message/Message.module.css b/src/components/Message/Message.module.css index d551894..468e874 100644 --- a/src/components/Message/Message.module.css +++ b/src/components/Message/Message.module.css @@ -57,20 +57,20 @@ .messageBubble { padding: var(--bb-spacing-md) var(--bb-spacing-lg); - border-radius: var(--bb-radius-lg); + border-radius: 22px; position: relative; } .user .messageBubble { background: var(--bb-user-message-bg); color: var(--bb-user-message-text); - border-bottom-right-radius: var(--bb-spacing-xs); + border-bottom-right-radius: 6px; } .assistant .messageBubble { background: var(--bb-assistant-message-bg); color: var(--bb-assistant-message-text); - border-bottom-left-radius: var(--bb-spacing-xs); + border-bottom-left-radius: 6px; } .messageBubble.error { @@ -79,7 +79,7 @@ } .content { - font-size: var(--bb-font-size-md); + font-size: var(--bb-message-font-size, 15px); line-height: var(--bb-line-height); word-break: break-word; } @@ -213,3 +213,67 @@ height: 14px; } +/* Clickable message wrapper for older assistant messages - no visual feedback */ + +/* Message content wrapper for info bar placement */ +.messageContent { + display: flex; + flex-direction: column; + gap: var(--bb-spacing-xs); + min-width: 0; +} + +/* Agent info bar below message */ +.messageInfo { + display: flex; + align-items: center; + gap: var(--bb-spacing-xs); + font-size: calc(var(--bb-message-font-size, 15px) - 2px); + color: var(--bb-text-tertiary); + padding-left: var(--bb-spacing-lg); + animation: slideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-4px); + } +} + +.messageInfo.fadeOut { + animation: slideOut 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.messageInfo .agentName { + font-weight: var(--bb-font-weight-medium); + color: var(--bb-text-secondary); +} + +.messageInfo .agentRole { + color: var(--bb-text-tertiary); +} + +.messageInfo .separator { + color: var(--bb-text-tertiary); + opacity: 0.6; +} + +.messageInfo .timestamp { + color: var(--bb-text-tertiary); +} diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index 2dd5a27..7b14954 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -1,76 +1,124 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import type { Message as MessageType } from '../../types'; -import { BrainbaseLogo } from '../BrainbaseLogo'; import styles from './Message.module.css'; export interface MessageProps { message: MessageType; agentName?: string; - agentLogoUrl?: string; + agentRole?: string; + isLastAssistantMessage?: boolean; } +// Format relative time (e.g., "2m", "1h", "Just now") +const formatRelativeTime = (timestamp: number): string => { + const now = Date.now(); + const diff = now - timestamp; + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m`; + if (hours < 24) return `${hours}h`; + return `${days}d`; +}; + export const Message: React.FC = ({ message, agentName, - agentLogoUrl, + agentRole, + isLastAssistantMessage = false, }) => { + const [showInfo, setShowInfo] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [isAnimatingOut, setIsAnimatingOut] = useState(false); + const isUser = message.role === 'user'; const isStreaming = message.status === 'streaming'; const isError = message.status === 'error'; + const isAssistant = message.role === 'assistant'; + + // Determine if info should be shown + const shouldShowInfo = isAssistant && (isLastAssistantMessage || showInfo); + + // Handle visibility with animation + useEffect(() => { + if (shouldShowInfo && !isVisible) { + setIsAnimatingOut(false); + setIsVisible(true); + } else if (!shouldShowInfo && isVisible) { + setIsAnimatingOut(true); + const timer = setTimeout(() => { + setIsVisible(false); + setIsAnimatingOut(false); + }, 200); // Match animation duration + return () => clearTimeout(timer); + } + }, [shouldShowInfo, isVisible]); + + const handleMessageClick = () => { + if (isAssistant && !isLastAssistantMessage) { + setShowInfo(!showInfo); + } + }; + + const messageWrapperClasses = [ + styles.messageWrapper, + isUser ? styles.user : styles.assistant, + isAssistant && !isLastAssistantMessage ? styles.clickable : '', + ].filter(Boolean).join(' '); return ( -
- {!isUser && ( -
- {agentLogoUrl ? ( - {agentName - ) : ( -
- +
+
+
+
+ {isUser ? ( + message.content + ) : ( + + href ? ( + + {children} + + ) : ( + <>{children} + ), + }} + > + {message.content} + + )} + {isStreaming && } +
+ {isError && ( +
+ + + + + Failed to send
)}
- )} -
-
- {isUser ? ( - message.content - ) : ( - - href ? ( - - {children} - - ) : ( - <>{children} - ), - }} - > - {message.content} - - )} - {isStreaming && } -
- {isError && ( -
- - - - - Failed to send + {isVisible && ( +
+ {agentName || 'AI'} + {agentRole && ( + <> + • + {agentRole} + + )} + • + {formatRelativeTime(message.timestamp)}
)}
diff --git a/src/components/MessageInput/MessageInput.module.css b/src/components/MessageInput/MessageInput.module.css index 1161e71..81b848a 100644 --- a/src/components/MessageInput/MessageInput.module.css +++ b/src/components/MessageInput/MessageInput.module.css @@ -6,9 +6,9 @@ .inputContainer { display: flex; - align-items: flex-end; + flex-direction: column; gap: var(--bb-spacing-sm); - padding: var(--bb-spacing-sm) var(--bb-spacing-md); + padding: var(--bb-spacing-md) var(--bb-spacing-lg); background: var(--bb-surface-secondary); border-radius: var(--bb-radius-xl); border: 1px solid var(--bb-border-color); @@ -16,12 +16,12 @@ } .inputContainer:focus-within { - border-color: var(--bb-accent-color); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); + border-color: var(--bb-focus-color, var(--bb-accent-color)); + box-shadow: 0 0 0 3px var(--bb-focus-shadow, rgba(99, 102, 241, 0.1)); } .textarea { - flex: 1; + width: 100%; border: none; background: transparent; resize: none; @@ -29,7 +29,7 @@ font-size: var(--bb-font-size-md); line-height: var(--bb-line-height); color: var(--bb-text-primary); - padding: var(--bb-spacing-sm) 0; + padding: 0; min-height: 24px; max-height: 150px; } @@ -47,28 +47,74 @@ cursor: not-allowed; } +/* Bottom row with icons and send button */ +.inputActions { + display: flex; + align-items: center; + justify-content: space-between; +} + +.actionIcons { + display: flex; + align-items: center; + gap: var(--bb-spacing-xs); +} + +.iconButton { + width: 32px; + height: 32px; + border-radius: var(--bb-radius-md); + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--bb-text-tertiary); + transition: color var(--bb-transition-fast), background var(--bb-transition-fast); +} + +.iconButton:hover:not(:disabled) { + color: var(--bb-text-secondary); + background: var(--bb-surface-tertiary); +} + +.iconButton:active:not(:disabled) { + transform: scale(0.95); +} + +.iconButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.iconButton svg { + width: 20px; + height: 20px; +} + .sendButton { flex-shrink: 0; width: 36px; height: 36px; border-radius: var(--bb-radius-full); - background: var(--bb-primary-color); + background: var(--bb-accent-color-custom, var(--bb-primary-color)); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--bb-text-inverse); - transition: background var(--bb-transition-fast), transform var(--bb-transition-fast); + transition: background var(--bb-transition-fast), transform var(--bb-transition-fast), opacity var(--bb-transition-fast), filter var(--bb-transition-fast); } .sendButton:hover:not(:disabled) { - background: var(--bb-primary-hover); + filter: brightness(0.9); transform: scale(1.05); } .sendButton:active:not(:disabled) { - transform: scale(0.98); + transform: scale(0.95); } .sendButton:disabled { @@ -76,6 +122,12 @@ cursor: not-allowed; } +/* Audio button state (when input is empty) */ +.sendButton.audioButton { + opacity: 1; + cursor: pointer; +} + .sendButton:focus-visible { outline: 2px solid var(--bb-accent-color); outline-offset: 2px; @@ -86,20 +138,7 @@ height: 18px; } -.hint { - margin-top: var(--bb-spacing-sm); - font-size: var(--bb-font-size-xs); - color: var(--bb-text-tertiary); - text-align: center; +.sendButton svg.audioIcon { + width: 22px; + height: 22px; } - -.hint kbd { - display: inline-block; - padding: 2px 6px; - font-family: var(--bb-font-family); - font-size: var(--bb-font-size-xs); - background: var(--bb-surface-tertiary); - border-radius: 4px; - border: 1px solid var(--bb-border-color); -} - diff --git a/src/components/MessageInput/MessageInput.tsx b/src/components/MessageInput/MessageInput.tsx index 96c4aa0..eddbed4 100644 --- a/src/components/MessageInput/MessageInput.tsx +++ b/src/components/MessageInput/MessageInput.tsx @@ -5,12 +5,20 @@ export interface MessageInputProps { onSend: (message: string) => void; disabled?: boolean; placeholder?: string; + onAttachment?: () => void; + onEmoji?: () => void; + onGif?: () => void; + onVoice?: () => void; } export const MessageInput: React.FC = ({ onSend, disabled = false, - placeholder = 'Send a message...', + placeholder = 'Message...', + onAttachment, + onEmoji, + onGif, + onVoice, }) => { const [value, setValue] = useState(''); const textareaRef = useRef(null); @@ -68,25 +76,108 @@ export const MessageInput: React.FC = ({ rows={1} aria-label="Message input" /> - -
-
- Press Enter to send, Shift + Enter for new line +
+
+ {onAttachment && ( + + )} + {onEmoji && ( + + )} + {onGif && ( + + )} + {onVoice && ( + + )} +
+ +
); diff --git a/src/components/MessageList/MessageList.tsx b/src/components/MessageList/MessageList.tsx index fb98418..8866cf4 100644 --- a/src/components/MessageList/MessageList.tsx +++ b/src/components/MessageList/MessageList.tsx @@ -9,7 +9,9 @@ export interface MessageListProps { messages: MessageType[]; toolCalls?: ToolCall[]; isLoading: boolean; + showTypingIndicator?: boolean; agentName?: string; + agentRole?: string; agentLogoUrl?: string; } @@ -17,21 +19,29 @@ export const MessageList: React.FC = ({ messages, toolCalls = [], isLoading, + showTypingIndicator = false, agentName, + agentRole, agentLogoUrl, }) => { + // Find the last assistant message index + const lastAssistantMessageIndex = messages.reduce((lastIndex, msg, index) => { + return msg.role === 'assistant' ? index : lastIndex; + }, -1); const listRef = useRef(null); const bottomRef = useRef(null); const prevMessageCountRef = useRef(messages.length); const prevToolCallCountRef = useRef(toolCalls.length); - // Auto-scroll to bottom only when new messages/tool calls are added - // Using scrollTop instead of scrollIntoView to avoid scrolling parent containers + // Auto-scroll to bottom when: + // - New messages are added (user sends or agent responds) + // - Typing indicator appears + // - Tool calls are added useEffect(() => { const hasNewMessages = messages.length > prevMessageCountRef.current; const hasNewToolCalls = toolCalls.length > prevToolCallCountRef.current; - if (hasNewMessages || hasNewToolCalls || (isLoading && messages.length > 0)) { + if (hasNewMessages || hasNewToolCalls || showTypingIndicator) { const list = listRef.current; if (list) { list.scrollTo({ @@ -43,7 +53,7 @@ export const MessageList: React.FC = ({ prevMessageCountRef.current = messages.length; prevToolCallCountRef.current = toolCalls.length; - }, [messages.length, toolCalls.length, isLoading]); + }, [messages.length, toolCalls.length, showTypingIndicator]); // Filter tool calls that are currently executing const activeToolCalls = toolCalls.filter( @@ -69,12 +79,13 @@ export const MessageList: React.FC = ({
)} - {messages.map((message) => ( + {messages.map((message, index) => ( ))} @@ -82,7 +93,7 @@ export const MessageList: React.FC = ({ ))} - {isLoading && messages[messages.length - 1]?.role === 'user' && ( + {showTypingIndicator && ( )} diff --git a/src/components/VoiceMode/VoiceMode.module.css b/src/components/VoiceMode/VoiceMode.module.css new file mode 100644 index 0000000..c3e4779 --- /dev/null +++ b/src/components/VoiceMode/VoiceMode.module.css @@ -0,0 +1,113 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; + background: #0a0a0f; + position: relative; + overflow: hidden; +} + +/* Three.js orb container */ +.orbContainer { + flex: 1; + cursor: pointer; + min-height: 200px; +} + +.orbContainer canvas { + display: block; +} + +/* Status text at top */ +.status { + position: absolute; + top: 16px; + left: 50%; + transform: translateX(-50%); + color: rgba(255, 255, 255, 0.6); + font-size: var(--bb-font-size-sm); + z-index: 10; + text-align: center; +} + +/* Instruction text */ +.instruction { + position: absolute; + bottom: 100px; + left: 50%; + transform: translateX(-50%); + color: rgba(255, 255, 255, 0.5); + font-size: var(--bb-font-size-sm); + z-index: 10; + text-align: center; + pointer-events: none; +} + +/* Controls at bottom */ +.controls { + position: absolute; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: var(--bb-spacing-md); + z-index: 10; +} + +.controlButton { + width: 48px; + height: 48px; + border-radius: var(--bb-radius-full); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.15s ease, background 0.15s ease; + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.controlButton:hover { + transform: scale(1.05); + background: rgba(255, 255, 255, 0.2); +} + +.controlButton:active { + transform: scale(0.95); +} + +.controlButton svg { + width: 24px; + height: 24px; +} + +.controlButton.muted { + background: rgba(239, 68, 68, 0.8); +} + +.controlButton.muted:hover { + background: rgb(239, 68, 68); +} + +/* Back to chat button */ +.backButton { + padding: 12px 24px; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: var(--bb-radius-full); + color: white; + font-size: var(--bb-font-size-sm); + cursor: pointer; + transition: background 0.15s ease, transform 0.15s ease; +} + +.backButton:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.02); +} + +.backButton:active { + transform: scale(0.98); +} diff --git a/src/components/VoiceMode/VoiceMode.tsx b/src/components/VoiceMode/VoiceMode.tsx new file mode 100644 index 0000000..eb93e4d --- /dev/null +++ b/src/components/VoiceMode/VoiceMode.tsx @@ -0,0 +1,651 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { Room, RoomEvent, Track } from 'livekit-client'; +import styles from './VoiceMode.module.css'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// THREE.js and GSAP are loaded dynamically from CDN, so we use 'any' for their types +type ThreeJS = any; +type GSAP = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +declare global { + interface Window { + THREE: ThreeJS; + gsap: GSAP; + webkitAudioContext: typeof AudioContext; + } +} + +export interface VoiceModeProps { + tokenUrl: string; + agentName?: string; + onClose: () => void; + accentColor?: string; +} + +export const VoiceMode: React.FC = ({ + tokenUrl, + agentName = 'voice-agent', + onClose, +}) => { + const containerRef = useRef(null); + const [status, setStatus] = useState(''); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [isMicMuted, setIsMicMuted] = useState(false); + + const roomRef = useRef(null); + const animationRef = useRef(null); + const audioContextRef = useRef(null); + const localAnalyserRef = useRef(null); + const remoteAnalyserRef = useRef(null); + const localDataArrayRef = useRef | null>(null); + const remoteDataArrayRef = useRef | null>(null); + const smoothedAmplitudeRef = useRef(0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const materialRef = useRef(null); + const sceneInitializedRef = useRef(false); + const morphTargetRef = useRef(0); + const currentMorphRef = useRef(0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rendererRef = useRef(null); + + const getAmplitude = useCallback(() => { + let localAmplitude = 0; + let remoteAmplitude = 0; + + if (localAnalyserRef.current && localDataArrayRef.current) { + localAnalyserRef.current.getByteFrequencyData(localDataArrayRef.current); + const sum = localDataArrayRef.current.reduce((a, b) => a + b, 0); + localAmplitude = sum / localDataArrayRef.current.length / 255; + } + + if (remoteAnalyserRef.current && remoteDataArrayRef.current) { + remoteAnalyserRef.current.getByteFrequencyData(remoteDataArrayRef.current); + const sum = remoteDataArrayRef.current.reduce((a, b) => a + b, 0); + remoteAmplitude = sum / remoteDataArrayRef.current.length / 255; + } + + const targetAmplitude = Math.max(localAmplitude, remoteAmplitude); + smoothedAmplitudeRef.current += (targetAmplitude - smoothedAmplitudeRef.current) * 0.25; + return smoothedAmplitudeRef.current; + }, []); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setupLocalAudioAnalyser = async (track: any) => { + if (!audioContextRef.current) { + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); + } + + if (audioContextRef.current.state === 'suspended') { + await audioContextRef.current.resume(); + } + + try { + const mediaStream = new MediaStream([track.mediaStreamTrack]); + const source = audioContextRef.current.createMediaStreamSource(mediaStream); + localAnalyserRef.current = audioContextRef.current.createAnalyser(); + localAnalyserRef.current.fftSize = 256; + source.connect(localAnalyserRef.current); + localDataArrayRef.current = new Uint8Array(localAnalyserRef.current.frequencyBinCount); + } catch (err) { + console.error('Failed to set up local audio analyser:', err); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setupRemoteAudioAnalyser = async (track: any) => { + if (!audioContextRef.current) { + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); + } + + if (audioContextRef.current.state === 'suspended') { + await audioContextRef.current.resume(); + } + + const audioEl = track.attach() as HTMLAudioElement; + audioEl.style.display = 'none'; + audioEl.autoplay = true; + audioEl.setAttribute('playsinline', 'true'); + document.body.appendChild(audioEl); + + try { + await audioEl.play(); + } catch { + console.log('Audio play failed, will retry on interaction'); + } + + const mediaStream = new MediaStream([track.mediaStreamTrack]); + const source = audioContextRef.current.createMediaStreamSource(mediaStream); + remoteAnalyserRef.current = audioContextRef.current.createAnalyser(); + remoteAnalyserRef.current.fftSize = 256; + source.connect(remoteAnalyserRef.current); + remoteDataArrayRef.current = new Uint8Array(remoteAnalyserRef.current.frequencyBinCount); + }; + + // Initialize Three.js scene + useEffect(() => { + if (sceneInitializedRef.current) return; + + const loadScript = (src: string): Promise => { + return new Promise((resolve, reject) => { + if (src.includes('three') && window.THREE) { + resolve(); + return; + } + if (src.includes('gsap') && window.gsap) { + resolve(); + return; + } + const script = document.createElement('script'); + script.src = src; + script.onload = () => resolve(); + script.onerror = reject; + document.head.appendChild(script); + }); + }; + + const initScene = async () => { + try { + await loadScript('https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'); + await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js'); + + if (!containerRef.current || !window.THREE) return; + + sceneInitializedRef.current = true; + const THREE = window.THREE; + const container = containerRef.current; + + const scene = new THREE.Scene(); + const width = container.clientWidth; + const height = container.clientHeight; + const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + + rendererRef.current = renderer; + renderer.setSize(width, height); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + container.appendChild(renderer.domElement); + + camera.position.z = 300; + + // Particle system - Torus (donut) shape + const particleCount = 12000; + const positions = new Float32Array(particleCount * 3); + const colors = new Float32Array(particleCount * 3); + const sizes = new Float32Array(particleCount); + + const torusPositions = new Float32Array(particleCount * 3); + const spherePositions = new Float32Array(particleCount * 3); + + const torusRadius = 50; + const tubeRadius = 22; + const sphereRadius = 60; + + for (let i = 0; i < particleCount; i++) { + const u = Math.random() * Math.PI * 2; + const v = Math.random() * Math.PI * 2; + const r = tubeRadius * Math.sqrt(Math.random()); + + torusPositions[i * 3] = (torusRadius + r * Math.cos(v)) * Math.cos(u); + torusPositions[i * 3 + 1] = (torusRadius + r * Math.cos(v)) * Math.sin(u); + torusPositions[i * 3 + 2] = r * Math.sin(v); + + const sRadius = sphereRadius * Math.cbrt(Math.random()); + const sTheta = Math.random() * Math.PI * 2; + const sPhi = Math.acos(Math.random() * 2 - 1); + + spherePositions[i * 3] = sRadius * Math.sin(sPhi) * Math.cos(sTheta); + spherePositions[i * 3 + 1] = sRadius * Math.sin(sPhi) * Math.sin(sTheta); + spherePositions[i * 3 + 2] = sRadius * Math.cos(sPhi); + + positions[i * 3] = torusPositions[i * 3]; + positions[i * 3 + 1] = torusPositions[i * 3 + 1]; + positions[i * 3 + 2] = torusPositions[i * 3 + 2]; + + const brightness = 0.8 + Math.random() * 0.2; + colors[i * 3] = brightness; + colors[i * 3 + 1] = brightness; + colors[i * 3 + 2] = brightness; + + sizes[i] = 1.4 + Math.random() * 1.0; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('torusPos', new THREE.BufferAttribute(torusPositions, 3)); + geometry.setAttribute('spherePos', new THREE.BufferAttribute(spherePositions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); + + const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0 }, + mouse: { value: new THREE.Vector3(9999, 9999, 0) }, + hoverRadius: { value: 35.0 }, + hoverStrength: { value: 30.0 }, + audioAmplitude: { value: 0.0 }, + audioExpansion: { value: 100.0 }, + isConnected: { value: 0.0 }, + }, + vertexShader: ` + attribute float size; + attribute vec3 color; + attribute vec3 torusPos; + attribute vec3 spherePos; + varying vec3 vColor; + uniform float time; + uniform vec3 mouse; + uniform float hoverRadius; + uniform float hoverStrength; + uniform float audioAmplitude; + uniform float audioExpansion; + uniform float isConnected; + + float hash(vec3 p) { + p = fract(p * 0.3183099 + 0.1); + p *= 17.0; + return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); + } + + float noise(vec3 p) { + vec3 i = floor(p); + vec3 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix( + mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x), + mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y), + mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x), + mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y), f.z); + } + + void main() { + vec3 disconnectedColor = vec3(0.7, 0.85, 0.87); + vec3 connectedColor = color; + vColor = mix(disconnectedColor, connectedColor, isConnected); + + vec3 basePos = mix(torusPos, spherePos, isConnected); + vec3 pos = basePos; + + float distFromCenter = length(basePos); + vec3 dirFromCenter = normalize(basePos); + float audioNoise = noise(basePos * 0.02 + time * 2.0); + float expansion = audioAmplitude * audioExpansion * (0.5 + audioNoise); + pos += dirFromCenter * expansion; + + float turbulence = audioAmplitude * 40.0; + pos.x += sin(time * 3.0 + position.y * 0.05) * turbulence * audioNoise; + pos.y += cos(time * 3.0 + position.x * 0.05) * turbulence * audioNoise; + pos.z += sin(time * 2.0 + position.z * 0.05) * turbulence * audioNoise; + + vec3 toMouse = pos - mouse; + float dist = length(toMouse); + + float noiseVal = noise(basePos * 0.05 + time * 0.5); + float featheredRadius = hoverRadius * (0.6 + noiseVal * 0.8); + + if (dist < featheredRadius && dist > 0.0) { + vec3 pushDir = normalize(toMouse); + float falloff = 1.0 - smoothstep(0.0, featheredRadius, dist); + falloff = pow(falloff, 0.5); + + float angleNoise = noise(basePos * 0.1 + time) * 2.0 - 1.0; + pushDir.x += angleNoise * 0.3; + pushDir.y += noise(basePos * 0.1 - time) * 0.3 - 0.15; + pushDir = normalize(pushDir); + + float pushAmount = falloff * hoverStrength * (0.7 + noiseVal * 0.6); + pos += pushDir * pushAmount; + } + + float flowSpeed = 0.3; + float swirl = time * flowSpeed; + + float angle = atan(basePos.z, basePos.x); + float flowOffset = sin(swirl + angle * 2.0) * 4.0; + + float torusFlow = 1.0 - isConnected; + + pos.x += sin(time * 0.5 + basePos.y * 0.02 + angle) * 3.0 * torusFlow; + pos.z += cos(time * 0.5 + basePos.y * 0.02 + angle) * 3.0 * torusFlow; + pos.y += flowOffset * torusFlow; + + pos.x += sin(time * 0.8 + basePos.y * 0.05) * 2.0; + pos.y += cos(time * 0.6 + basePos.x * 0.03) * 2.0; + pos.z += sin(time * 0.7 + basePos.z * 0.04) * 2.0; + + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + float sizeBoost = 1.0 + audioAmplitude * 0.5; + gl_PointSize = size * sizeBoost * (200.0 / -mvPosition.z); + gl_Position = projectionMatrix * mvPosition; + } + `, + fragmentShader: ` + varying vec3 vColor; + + void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.3, 0.5, dist); + gl_FragColor = vec4(vColor, alpha); + } + `, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + + materialRef.current = material; + + const particles = new THREE.Points(geometry, material); + scene.add(particles); + + // Mouse tracking + const mouse = new THREE.Vector2(); + const mouseWorld = new THREE.Vector3(); + const inverseMatrix = new THREE.Matrix4(); + + const handleMouseMove = (event: MouseEvent) => { + const rect = container.getBoundingClientRect(); + mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + + mouseWorld.set(mouse.x, mouse.y, 0.5); + mouseWorld.unproject(camera); + + const dir = mouseWorld.sub(camera.position).normalize(); + const distance = -camera.position.z / dir.z; + const pos = camera.position.clone().add(dir.multiplyScalar(distance)); + + inverseMatrix.copy(particles.matrixWorld).invert(); + pos.applyMatrix4(inverseMatrix); + + material.uniforms.mouse.value.copy(pos); + }; + + const handleMouseLeave = () => { + material.uniforms.mouse.value.set(9999, 9999, 0); + }; + + container.addEventListener('mousemove', handleMouseMove); + container.addEventListener('mouseleave', handleMouseLeave); + + const handleResize = () => { + const newWidth = container.clientWidth; + const newHeight = container.clientHeight; + camera.aspect = newWidth / newHeight; + camera.updateProjectionMatrix(); + renderer.setSize(newWidth, newHeight); + }; + + const resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(container); + + let time = 0; + const animate = () => { + animationRef.current = requestAnimationFrame(animate); + + time += 0.01; + material.uniforms.time.value = time; + + const amplitude = getAmplitude(); + material.uniforms.audioAmplitude.value = amplitude; + + currentMorphRef.current += (morphTargetRef.current - currentMorphRef.current) * 0.05; + material.uniforms.isConnected.value = currentMorphRef.current; + + particles.updateMatrixWorld(); + renderer.render(scene, camera); + }; + + animate(); + + return () => { + container.removeEventListener('mousemove', handleMouseMove); + container.removeEventListener('mouseleave', handleMouseLeave); + resizeObserver.disconnect(); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + renderer.dispose(); + }; + } catch (error) { + console.error('Failed to initialize scene:', error); + } + }; + + initScene(); + }, [getAmplitude]); + + const connect = async () => { + if (isConnecting) return; + setIsConnecting(true); + setStatus(''); + + if (!audioContextRef.current) { + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); + } + if (audioContextRef.current.state === 'suspended') { + await audioContextRef.current.resume(); + } + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const body: any = {}; + if (agentName && agentName.trim()) { + body.room_config = { + agents: [{ agentName: agentName.trim() }], + }; + } + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + + const room = new Room({ + adaptiveStream: true, + dynacast: true, + }); + + roomRef.current = room; + + room.on(RoomEvent.Connected, () => { + setStatus(''); + setIsConnected(true); + setIsConnecting(false); + morphTargetRef.current = 1.0; + }); + + room.on(RoomEvent.Disconnected, () => { + const audioElements = document.querySelectorAll('audio'); + audioElements.forEach((el) => { + el.pause(); + el.srcObject = null; + el.remove(); + }); + + setStatus(''); + setIsConnected(false); + setIsConnecting(false); + localAnalyserRef.current = null; + remoteAnalyserRef.current = null; + localDataArrayRef.current = null; + remoteDataArrayRef.current = null; + smoothedAmplitudeRef.current = 0; + morphTargetRef.current = 0.0; + }); + + room.on(RoomEvent.ParticipantConnected, () => { + setStatus(''); + }); + + room.on(RoomEvent.TrackSubscribed, (track, _publication, _participant) => { + if (track.kind === Track.Kind.Audio) { + setupRemoteAudioAnalyser(track); + setStatus(''); + } + }); + + room.on(RoomEvent.LocalTrackPublished, (publication) => { + if (publication.track && publication.track.kind === Track.Kind.Audio) { + setupLocalAudioAnalyser(publication.track); + } + }); + + await room.connect(data.server_url, data.participant_token); + await room.localParticipant.setMicrophoneEnabled(true); + + const localTracks = room.localParticipant.audioTrackPublications; + localTracks.forEach((pub) => { + if (pub.track) { + setupLocalAudioAnalyser(pub.track); + } + }); + } catch (error) { + console.error('Connection failed:', error); + setStatus('Connection failed'); + setIsConnecting(false); + } + }; + + const disconnect = async () => { + if (roomRef.current) { + try { + await roomRef.current.localParticipant.setMicrophoneEnabled(false); + } catch { + // Ignore errors + } + roomRef.current.disconnect(true); + roomRef.current = null; + } + + const audioElements = document.querySelectorAll('audio'); + audioElements.forEach((el) => { + el.pause(); + el.srcObject = null; + el.remove(); + }); + + localAnalyserRef.current = null; + remoteAnalyserRef.current = null; + localDataArrayRef.current = null; + remoteDataArrayRef.current = null; + smoothedAmplitudeRef.current = 0; + + setIsConnected(false); + setIsMicMuted(false); + setStatus(''); + morphTargetRef.current = 0.0; + onClose(); + }; + + const handleOrbClick = () => { + if (isConnected) { + disconnect(); + } else if (!isConnecting) { + connect(); + } + }; + + const toggleMic = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (roomRef.current) { + const newMutedState = !isMicMuted; + await roomRef.current.localParticipant.setMicrophoneEnabled(!newMutedState); + setIsMicMuted(newMutedState); + + if (!newMutedState) { + setTimeout(() => { + if (roomRef.current) { + const localTracks = roomRef.current.localParticipant.audioTrackPublications; + localTracks.forEach((pub) => { + if (pub.track) { + setupLocalAudioAnalyser(pub.track); + } + }); + } + }, 100); + } + } + }; + + // Cleanup on unmount + useEffect(() => { + return () => { + if (roomRef.current) { + roomRef.current.disconnect(true); + roomRef.current = null; + } + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + if (rendererRef.current) { + rendererRef.current.dispose(); + } + const audioElements = document.querySelectorAll('audio'); + audioElements.forEach((el) => { + el.pause(); + el.srcObject = null; + el.remove(); + }); + }; + }, []); + + return ( +
+ {/* Status */} + {status &&
{status}
} + + {/* Three.js canvas container */} +
+ + {/* Instruction text */} +
+ {isConnecting + ? 'Connecting...' + : isConnected + ? 'Tap orb to end call' + : 'Tap orb to start'} +
+ + {/* Controls */} +
+ {isConnected && ( + + )} + +
+
+ ); +}; diff --git a/src/components/VoiceMode/index.ts b/src/components/VoiceMode/index.ts new file mode 100644 index 0000000..24a9df8 --- /dev/null +++ b/src/components/VoiceMode/index.ts @@ -0,0 +1,2 @@ +export { VoiceMode } from './VoiceMode'; +export type { VoiceModeProps } from './VoiceMode'; diff --git a/src/components/index.ts b/src/components/index.ts index 802b3f3..c004900 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,21 +4,24 @@ export { ChatHeader } from './ChatHeader'; export { ChatToggleButton } from './ChatToggleButton'; export { BrainbaseLogo } from './BrainbaseLogo'; export { ErrorState } from './ErrorState'; +export { HomePage } from './HomePage'; export { Message } from './Message'; export { MessageList } from './MessageList'; export { MessageInput } from './MessageInput'; export { PoweredBy } from './PoweredBy'; export { ToolCallDisplay } from './ToolCallDisplay'; export { TypingIndicator } from './TypingIndicator'; +export { VoiceMode } from './VoiceMode'; export type { ChatContainerProps } from './ChatContainer'; export type { ChatHeaderProps } from './ChatHeader'; export type { ChatToggleButtonProps } from './ChatToggleButton'; export type { BrainbaseLogoProps } from './BrainbaseLogo'; export type { ErrorStateProps } from './ErrorState'; +export type { HomePageProps } from './HomePage'; export type { MessageProps } from './Message'; export type { MessageListProps } from './MessageList'; export type { MessageInputProps } from './MessageInput'; export type { ToolCallDisplayProps } from './ToolCallDisplay'; export type { TypingIndicatorProps } from './TypingIndicator'; - +export type { VoiceModeProps } from './VoiceMode'; \ No newline at end of file diff --git a/src/embed/ChatWidgetElement.ts b/src/embed/ChatWidgetElement.ts index 4901b19..8332284 100644 --- a/src/embed/ChatWidgetElement.ts +++ b/src/embed/ChatWidgetElement.ts @@ -29,9 +29,32 @@ class ChatWidgetElement extends HTMLElement { 'position', 'primary-color', 'agent-name', + 'agent-role', 'welcome-message', 'api-base-url', 'default-open', + 'toggle-icon', + 'width', + 'height', + 'message-font-size', + 'theme', + 'header-subtitle', + 'agent-name-font-size', + 'accent-color', + 'primary-gradient', + 'accent-gradient', + 'header-text-color', + 'stream-messages', + 'artificial-delay', + 'home-image', + 'home-title', + 'home-description', + 'home-link', + 'time-to-open', + 'voice-token-url', + 'voice-agent-name', + 'enable-voice-mode', + 'show-collapse-button', ]; } @@ -86,17 +109,66 @@ class ChatWidgetElement extends HTMLElement { } private getProps(): ChatWidgetProps { + const widthAttr = this.getAttribute('width'); + const heightAttr = this.getAttribute('height'); + const messageFontSizeAttr = this.getAttribute('message-font-size'); + const agentNameFontSizeAttr = this.getAttribute('agent-name-font-size'); + const timeToOpenAttr = this.getAttribute('time-to-open'); + return { embedId: this.getAttribute('embed-id') || '', position: this.getPositionAttribute(), primaryColor: this.getAttribute('primary-color') || undefined, + accentColor: this.getAttribute('accent-color') || undefined, + primaryGradient: this.getAttribute('primary-gradient') === 'true', + accentGradient: this.getAttribute('accent-gradient') === 'true', + headerTextColor: this.getAttribute('header-text-color') || undefined, + streamMessages: this.getAttribute('stream-messages') === 'true', + artificialDelay: this.getArtificialDelayAttribute(), + homeImage: this.getAttribute('home-image') || undefined, + homeTitle: this.getAttribute('home-title') || undefined, + homeDescription: this.getAttribute('home-description') || undefined, + homeLink: this.getAttribute('home-link') || undefined, + timeToOpen: timeToOpenAttr ? parseFloat(timeToOpenAttr) : undefined, + voiceTokenUrl: this.getAttribute('voice-token-url') || undefined, + voiceAgentName: this.getAttribute('voice-agent-name') || undefined, + enableVoiceMode: this.getAttribute('enable-voice-mode') === 'true', + showCollapseButton: this.getAttribute('show-collapse-button') !== 'false', agentName: this.getAttribute('agent-name') || undefined, + agentRole: this.getAttribute('agent-role') || undefined, + headerSubtitle: this.getAttribute('header-subtitle') || undefined, welcomeMessage: this.getAttribute('welcome-message') || undefined, apiBaseUrl: this.getAttribute('api-base-url') || undefined, defaultOpen: this.getAttribute('default-open') === 'true', + toggleIcon: this.getAttribute('toggle-icon') || undefined, + width: widthAttr ? parseInt(widthAttr, 10) : undefined, + height: heightAttr ? parseInt(heightAttr, 10) : undefined, + messageFontSize: messageFontSizeAttr ? parseInt(messageFontSizeAttr, 10) : undefined, + agentNameFontSize: agentNameFontSizeAttr ? parseInt(agentNameFontSizeAttr, 10) : undefined, + theme: this.getThemeAttribute(), }; } + private getThemeAttribute(): 'light' | 'dark' | 'granite' | undefined { + const theme = this.getAttribute('theme'); + if (theme === 'dark' || theme === 'granite' || theme === 'light') { + return theme; + } + return undefined; + } + + private getArtificialDelayAttribute(): [number, number] | undefined { + const delay = this.getAttribute('artificial-delay'); + if (!delay) return undefined; + + // Support formats: "10,50" or "10-50" + const parts = delay.split(/[,-]/).map(s => parseFloat(s.trim())); + if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { + return [parts[0], parts[1]]; + } + return undefined; + } + private getPositionAttribute(): 'bottom-right' | 'bottom-left' | 'inline' { const position = this.getAttribute('position'); if (position === 'bottom-left' || position === 'inline') { diff --git a/src/hooks/useChat.ts b/src/hooks/useChat.ts index 5082cc7..6748beb 100644 --- a/src/hooks/useChat.ts +++ b/src/hooks/useChat.ts @@ -18,6 +18,8 @@ interface UseChatOptions { config: DeploymentConfig; apiClient: BrainbaseAPIClient | MockAPIClient; mockMode?: boolean; + streamMessages?: boolean; + artificialDelay?: [number, number]; onSessionStart?: (sessionId: string) => void; onSessionEnd?: (session: Session) => void; onMessage?: (message: Message) => void; @@ -28,9 +30,11 @@ export interface UseChatReturn { messages: Message[]; toolCalls: ToolCall[]; isLoading: boolean; + showTypingIndicator: boolean; error: Error | null; sessionId: string | null; sendMessage: (content: string) => Promise; + sendTriggerMessage: () => Promise; endSession: () => Promise; clearMessages: () => void; startNewSession: () => Promise; @@ -41,6 +45,8 @@ export function useChat(options: UseChatOptions): UseChatReturn { config, apiClient, mockMode, + streamMessages = false, + artificialDelay, onSessionStart, onSessionEnd, onMessage, @@ -50,12 +56,22 @@ export function useChat(options: UseChatOptions): UseChatReturn { const [messages, setMessages] = useState([]); const [toolCalls, setToolCalls] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [showTypingIndicator, setShowTypingIndicator] = useState(false); const [error, setError] = useState(null); const [sessionId, setSessionId] = useState(null); const sessionStartTime = useRef(0); const isInitialized = useRef(false); const streamBuffers = useRef>({}); + const typingIndicatorTimeoutRef = useRef | null>(null); + const responseDelayTimeoutRef = useRef | null>(null); + + // Helper to get random delay from artificial delay range (in ms) + const getRandomDelay = useCallback(() => { + if (!artificialDelay) return 0; + const [min, max] = artificialDelay; + return (min + Math.random() * (max - min)) * 1000; // Convert seconds to ms + }, [artificialDelay]); // Initialize or restore session useEffect(() => { @@ -100,13 +116,13 @@ export function useChat(options: UseChatOptions): UseChatReturn { return ''; }, [config.embedId]); - const upsertAssistantMessage = useCallback((messageId: string, content: string) => { + const addMessageToState = useCallback((messageId: string, content: string, status: 'streaming' | 'sent') => { setMessages((prev) => { let found = false; const next = prev.map((m) => { if (m.id === messageId) { found = true; - return { ...m, content, status: 'streaming' as const }; + return { ...m, content, status }; } return m; }); @@ -117,7 +133,7 @@ export function useChat(options: UseChatOptions): UseChatReturn { role: 'assistant', content, timestamp: Date.now(), - status: 'streaming', + status, }); } @@ -125,6 +141,27 @@ export function useChat(options: UseChatOptions): UseChatReturn { }); }, []); + const upsertAssistantMessage = useCallback((messageId: string, content: string, forceAdd = false) => { + // In non-streaming mode, we don't update the UI until the message is complete + if (!streamMessages && !forceAdd) { + // Just buffer the content, don't update messages yet + return; + } + + if (forceAdd && artificialDelay) { + // Apply artificial delay before showing the complete message + const responseDelay = getRandomDelay(); + if (responseDelayTimeoutRef.current) { + clearTimeout(responseDelayTimeoutRef.current); + } + responseDelayTimeoutRef.current = setTimeout(() => { + addMessageToState(messageId, content, 'sent'); + }, responseDelay); + } else { + addMessageToState(messageId, content, forceAdd ? 'sent' : 'streaming'); + } + }, [streamMessages, artificialDelay, getRandomDelay, addMessageToState]); + const handleSSEEvent = useCallback( (event: SSEEvent, messageId: string, updateSessionId: (id: string) => void) => { switch (event.type) { @@ -215,14 +252,31 @@ export function useChat(options: UseChatOptions): UseChatReturn { } case 'done': { // Stream complete - setMessages((prev) => - prev.map((m) => (m.id === messageId ? { ...m, status: 'sent' as const } : m)) - ); + if (!streamMessages) { + // In non-streaming mode, add the complete message now + const finalContent = streamBuffers.current[messageId] ?? ''; + if (finalContent) { + upsertAssistantMessage(messageId, finalContent, true); + } + } else { + // In streaming mode, just mark as sent + setMessages((prev) => + prev.map((m) => (m.id === messageId ? { ...m, status: 'sent' as const } : m)) + ); + } delete streamBuffers.current[messageId]; break; } case 'completed': { // Conversation ended by agent + if (!streamMessages) { + // In non-streaming mode, add the complete message now + const finalContent = streamBuffers.current[messageId] ?? ''; + if (finalContent) { + upsertAssistantMessage(messageId, finalContent, true); + } + } + setMessages((prev) => { const updatedMessages = prev.map((m) => m.id === messageId ? { ...m, status: 'sent' as const } : m @@ -262,7 +316,7 @@ export function useChat(options: UseChatOptions): UseChatReturn { } } }, - [config, sessionId, onSessionStart, upsertAssistantMessage] + [config, sessionId, streamMessages, onSessionStart, upsertAssistantMessage] ); const processSSEStream = useCallback( @@ -321,19 +375,32 @@ export function useChat(options: UseChatOptions): UseChatReturn { setMessages((prev) => [...prev, userMessage]); onMessage?.(userMessage); - // Create placeholder for assistant response + // Create placeholder for assistant response (only in streaming mode) const assistantMessageId = `assistant-${Date.now()}`; - const assistantMessage: Message = { - id: assistantMessageId, - role: 'assistant', - content: '', - timestamp: Date.now(), - status: 'streaming', - }; - setMessages((prev) => [...prev, assistantMessage]); + if (streamMessages) { + const assistantMessage: Message = { + id: assistantMessageId, + role: 'assistant', + content: '', + timestamp: Date.now(), + status: 'streaming', + }; + setMessages((prev) => [...prev, assistantMessage]); + } setIsLoading(true); setError(null); + + // Clear any existing typing indicator timeout + if (typingIndicatorTimeoutRef.current) { + clearTimeout(typingIndicatorTimeoutRef.current); + } + + // Start typing indicator after 1 second delay (+ artificial delay if set) + const typingDelay = 1000 + getRandomDelay(); + typingIndicatorTimeoutRef.current = setTimeout(() => { + setShowTypingIndicator(true); + }, typingDelay); // Create a callback to update session ID when received from server const updateSessionId = (newSessionId: string) => { @@ -357,29 +424,39 @@ export function useChat(options: UseChatOptions): UseChatReturn { await processSSEStream(stream, assistantMessageId, updateSessionId); } - // Mark as sent if still streaming - setMessages((prev) => - prev.map((m) => - m.id === assistantMessageId && m.status === 'streaming' - ? { ...m, status: 'sent' } - : m - ) - ); + // Mark as sent if still streaming (only in streaming mode) + if (streamMessages) { + setMessages((prev) => + prev.map((m) => + m.id === assistantMessageId && m.status === 'streaming' + ? { ...m, status: 'sent' } + : m + ) + ); + } } catch (err) { const error = err instanceof Error ? err : new Error('Failed to send message'); setError(error); onError?.(error); - // Mark message as error - setMessages((prev) => - prev.map((m) => - m.id === assistantMessageId - ? { ...m, status: 'error', content: 'Failed to get response' } - : m - ) - ); + // Mark message as error (only if message exists in streaming mode) + if (streamMessages) { + setMessages((prev) => + prev.map((m) => + m.id === assistantMessageId + ? { ...m, status: 'error', content: 'Failed to get response' } + : m + ) + ); + } } finally { + // Clear typing indicator timeout if still pending + if (typingIndicatorTimeoutRef.current) { + clearTimeout(typingIndicatorTimeoutRef.current); + typingIndicatorTimeoutRef.current = null; + } + setShowTypingIndicator(false); setIsLoading(false); } }, @@ -387,6 +464,8 @@ export function useChat(options: UseChatOptions): UseChatReturn { sessionId, apiClient, mockMode, + streamMessages, + getRandomDelay, config.embedId, handleSSEEvent, processSSEStream, @@ -395,6 +474,110 @@ export function useChat(options: UseChatOptions): UseChatReturn { ] ); + // Send a trigger message to start the conversation without showing a user message + const sendTriggerMessage = useCallback( + async (): Promise => { + // Create placeholder for assistant response (only in streaming mode) + const assistantMessageId = `assistant-${Date.now()}`; + if (streamMessages) { + const assistantMessage: Message = { + id: assistantMessageId, + role: 'assistant', + content: '', + timestamp: Date.now(), + status: 'streaming', + }; + setMessages((prev) => [...prev, assistantMessage]); + } + + setIsLoading(true); + setError(null); + + // Clear any existing typing indicator timeout + if (typingIndicatorTimeoutRef.current) { + clearTimeout(typingIndicatorTimeoutRef.current); + } + + // Start typing indicator after 1 second delay (+ artificial delay if set) + const typingDelay = 1000 + getRandomDelay(); + typingIndicatorTimeoutRef.current = setTimeout(() => { + setShowTypingIndicator(true); + }, typingDelay); + + // Create a callback to update session ID when received from server + const updateSessionId = (newSessionId: string) => { + setSessionId(newSessionId); + }; + + try { + // Send an empty/trigger message - the backend should handle this as a conversation starter + const triggerContent = ''; + + if (mockMode) { + // Handle AsyncGenerator (mock mode) + const generator = (apiClient as MockAPIClient).sendMessage(triggerContent); + for await (const event of generator) { + handleSSEEvent(event, assistantMessageId, updateSessionId); + } + } else { + // Handle ReadableStream (real API) + const stream = await (apiClient as BrainbaseAPIClient).sendMessage({ + embedId: config.embedId, + message: triggerContent, + sessionId: sessionId ?? undefined, + }); + await processSSEStream(stream, assistantMessageId, updateSessionId); + } + + // Mark as sent if still streaming (only in streaming mode) + if (streamMessages) { + setMessages((prev) => + prev.map((m) => + m.id === assistantMessageId && m.status === 'streaming' + ? { ...m, status: 'sent' } + : m + ) + ); + } + } catch (err) { + const error = + err instanceof Error ? err : new Error('Failed to send message'); + setError(error); + onError?.(error); + + // Mark message as error (only if message exists in streaming mode) + if (streamMessages) { + setMessages((prev) => + prev.map((m) => + m.id === assistantMessageId + ? { ...m, status: 'error', content: 'Failed to get response' } + : m + ) + ); + } + } finally { + // Clear typing indicator timeout if still pending + if (typingIndicatorTimeoutRef.current) { + clearTimeout(typingIndicatorTimeoutRef.current); + typingIndicatorTimeoutRef.current = null; + } + setShowTypingIndicator(false); + setIsLoading(false); + } + }, + [ + sessionId, + apiClient, + mockMode, + streamMessages, + getRandomDelay, + config.embedId, + handleSSEEvent, + processSSEStream, + onError, + ] + ); + const endCurrentSession = useCallback(async (): Promise => { // Session ending is handled server-side when conversation completes // This just clears local state @@ -434,9 +617,11 @@ export function useChat(options: UseChatOptions): UseChatReturn { messages, toolCalls, isLoading, + showTypingIndicator, error, sessionId, sendMessage, + sendTriggerMessage, endSession: endCurrentSession, clearMessages, startNewSession, diff --git a/src/styles/variables.css b/src/styles/variables.css index 50344c7..b38aff2 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -10,6 +10,10 @@ --bb-accent-color: #6366f1; --bb-accent-hover: #5558e3; + /* Focus color */ + --bb-focus-color: #6366f1; + --bb-focus-shadow: rgba(99, 102, 241, 0.1); + /* Surface colors */ --bb-surface-bg: #ffffff; --bb-surface-secondary: #f8f9fb; @@ -72,13 +76,82 @@ --bb-transition-slow: 300ms ease; /* Widget dimensions */ - --bb-widget-width: 400px; - --bb-widget-height: 600px; - --bb-widget-max-height: 85vh; + --bb-widget-width: 440px; + --bb-widget-height: 720px; + --bb-widget-max-height: 90vh; --bb-toggle-size: 60px; } -/* Dark mode support */ +/* Dark theme - matches the Intercom/Fin style dark UI */ +[data-bb-theme="dark"] { + --bb-primary-color: #1a1a2e; + --bb-primary-hover: #16162a; + --bb-primary-light: rgba(26, 26, 46, 0.3); + + --bb-surface-bg: #0d0d14; + --bb-surface-secondary: #1a1a2e; + --bb-surface-tertiary: #252542; + + --bb-text-primary: #ffffff; + --bb-text-secondary: #a1a1aa; + --bb-text-tertiary: #71717a; + --bb-text-inverse: #0d0d14; + + /* Dark theme: user messages are light, assistant messages are dark */ + --bb-user-message-bg: #f5f5f0; + --bb-user-message-text: #1a1a2e; + --bb-assistant-message-bg: #1a1a30; + --bb-assistant-message-text: #ffffff; + + --bb-border-color: #2d2d4a; + --bb-border-color-light: #1f1f35; + + --bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --bb-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3); + --bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.4); + --bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.5); + + --bb-accent-color: #818cf8; + --bb-accent-hover: #6366f1; + + --bb-focus-color: #ffffff; + --bb-focus-shadow: rgba(255, 255, 255, 0.15); +} + +/* Granite theme - sophisticated stone-inspired with warm grays */ +[data-bb-theme="granite"] { + --bb-primary-color: #374151; + --bb-primary-hover: #1f2937; + --bb-primary-light: rgba(55, 65, 81, 0.15); + + --bb-surface-bg: #1f2937; + --bb-surface-secondary: #374151; + --bb-surface-tertiary: #4b5563; + + --bb-text-primary: #f3f4f6; + --bb-text-secondary: #d1d5db; + --bb-text-tertiary: #9ca3af; + --bb-text-inverse: #111827; + + /* Granite: user messages warm stone, assistant slate */ + --bb-user-message-bg: #e5e7eb; + --bb-user-message-text: #1f2937; + --bb-assistant-message-bg: #4b5563; + --bb-assistant-message-text: #f9fafb; + + --bb-border-color: #4b5563; + --bb-border-color-light: #374151; + + --bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.15); + --bb-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25); + --bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.35); + --bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.45); + + --bb-accent-color: #60a5fa; + --bb-accent-hover: #3b82f6; +} + +/* Auto dark mode support (system preference) */ @media (prefers-color-scheme: dark) { :root[data-bb-theme="auto"] { --bb-surface-bg: #1a1a2e; diff --git a/src/types/index.ts b/src/types/index.ts index c9f7cb0..40fc305 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ export interface DeploymentConfig { flowId: string; welcomeMessage?: string; agentName?: string; + agentRole?: string; agentLogoUrl?: string; primaryColor?: string; styling?: Record; @@ -61,6 +62,9 @@ export interface Session { status: 'active' | 'completed' | 'error'; } +// Theme options +export type ChatWidgetTheme = 'light' | 'dark' | 'granite'; + // Widget props export interface ChatWidgetProps { /** The embed ID from your Brainbase deployment */ @@ -84,18 +88,98 @@ export interface ChatWidgetProps { /** Override primary color */ primaryColor?: string; + /** Accent color for buttons and icons - NOT header (defaults to primaryColor) */ + accentColor?: string; + + /** Use gradient on header (default: false) */ + primaryGradient?: boolean; + + /** Use gradient on accent elements like buttons and icons (default: false) */ + accentGradient?: boolean; + + /** Header text color (default: based on theme - black for light/dark, gray for granite) */ + headerTextColor?: string; + + /** Stream messages token by token (default: false - show full message when complete) */ + streamMessages?: boolean; + + /** Artificial delay range in seconds [min, max] for typing indicator and response. + * Adds random delay before showing dots and before showing response. + * Default: undefined (no artificial delay, just 1s delay for typing indicator) */ + artificialDelay?: [number, number]; + + /** Home page hero image URL (optional) */ + homeImage?: string; + + /** Home page title text (optional) */ + homeTitle?: string; + + /** Home page description text (optional) */ + homeDescription?: string; + + /** URL to open when home page info card is clicked (optional) */ + homeLink?: string; + + /** Time in seconds before the widget auto-opens and sends a trigger message. + * When set, the widget will automatically open after this delay and send a hidden + * message to trigger the agent. Default: undefined (never auto-opens) */ + timeToOpen?: number; + + /** URL for the LiveKit token endpoint (e.g., 'https://your-app.com/api/token'). + * Required for voice mode to work. */ + voiceTokenUrl?: string; + + /** Agent name for voice mode dispatch. Default: 'voice-agent' */ + voiceAgentName?: string; + + /** Enable voice mode (audio button in input). Default: false */ + enableVoiceMode?: boolean; + + /** Show the collapse button (circle with down arrow) when widget is open. Default: true */ + showCollapseButton?: boolean; + /** Override agent name */ agentName?: string; + /** Override agent role (e.g., "AI Agent", "Support Bot") */ + agentRole?: string; + + /** Description shown below agent name in header (e.g., "The team can also help") */ + headerSubtitle?: string; + /** Override agent logo URL */ agentLogoUrl?: string; + /** Agent name font size in pixels (default: 16) */ + agentNameFontSize?: number; + + /** Custom icon for the toggle button (when closed). Can be a URL or React node */ + toggleIcon?: string | React.ReactNode; + /** Override welcome message */ welcomeMessage?: string; /** Override branding visibility */ showBranding?: boolean; + /** Widget width in pixels (default: 420) */ + width?: number; + + /** Widget height in pixels (default: 720) */ + height?: number; + + /** Expanded widget width in pixels (default: 640) */ + expandedWidth?: number; + + /** Expanded widget height in pixels (default: 800) */ + expandedHeight?: number; + + /** Message font size in pixels (default: 15) */ + messageFontSize?: number; + + /** Theme: 'light' (default), 'dark', or 'granite' */ + theme?: ChatWidgetTheme; + /** Custom CSS class */ className?: string; diff --git a/src/utils/icons.tsx b/src/utils/icons.tsx new file mode 100644 index 0000000..8fb2531 --- /dev/null +++ b/src/utils/icons.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { + MessageCircle, + MessageSquare, + MessagesSquare, + HelpCircle, + Headphones, + Bot, + Sparkles, + Send, + Mail, + Phone, + Users, + Heart, + Star, + Zap, + Coffee, + Smile, + type LucideIcon, +} from 'lucide-react'; + +// Map of available icon names to Lucide components +export const iconMap: Record = { + 'message-circle': MessageCircle, + 'message-square': MessageSquare, + 'messages-square': MessagesSquare, + 'help-circle': HelpCircle, + 'headphones': Headphones, + 'bot': Bot, + 'sparkles': Sparkles, + 'send': Send, + 'mail': Mail, + 'phone': Phone, + 'users': Users, + 'heart': Heart, + 'star': Star, + 'zap': Zap, + 'coffee': Coffee, + 'smile': Smile, +}; + +// List of available icon names for documentation/validation +export const availableIcons = Object.keys(iconMap) as IconName[]; + +// Type for icon names +export type IconName = keyof typeof iconMap; + +// Helper to get an icon component by name +export function getIconByName(name: string): React.ReactNode | null { + const IconComponent = iconMap[name]; + if (!IconComponent) { + console.warn(`[Brainbase Chat] Unknown icon name: "${name}". Available icons: ${availableIcons.join(', ')}`); + return null; + } + return ; +} + +// Check if a string is a valid icon name +export function isValidIconName(name: string): name is IconName { + return name in iconMap; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 4a48aaa..5f93f33 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export { generateSessionId } from './generateSessionId'; export { getStoredSession, storeSession, clearSession } from './sessionStorage'; - +export { getIconByName, isValidIconName, iconMap, availableIcons } from './icons'; +export type { IconName } from './icons'; diff --git a/styles.css b/styles.css index d4e4064..c5fd890 100644 --- a/styles.css +++ b/styles.css @@ -1 +1 @@ -:root{--bb-primary-color: #1a1a2e;--bb-primary-hover: #16162a;--bb-primary-light: rgba(26, 26, 46, .1);--bb-accent-color: #6366f1;--bb-accent-hover: #5558e3;--bb-surface-bg: #ffffff;--bb-surface-secondary: #f8f9fb;--bb-surface-tertiary: #f1f3f9;--bb-text-primary: #1a1a2e;--bb-text-secondary: #6b7280;--bb-text-tertiary: #9ca3af;--bb-text-inverse: #ffffff;--bb-user-message-bg: var(--bb-primary-color);--bb-user-message-text: var(--bb-text-inverse);--bb-assistant-message-bg: var(--bb-surface-secondary);--bb-assistant-message-text: var(--bb-text-primary);--bb-border-color: #e5e7eb;--bb-border-color-light: #f1f3f9;--bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, .04);--bb-shadow-md: 0 4px 12px rgba(0, 0, 0, .08);--bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, .12);--bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, .16);--bb-radius-sm: 8px;--bb-radius-md: 12px;--bb-radius-lg: 16px;--bb-radius-xl: 20px;--bb-radius-full: 9999px;--bb-spacing-xs: 4px;--bb-spacing-sm: 8px;--bb-spacing-md: 12px;--bb-spacing-lg: 16px;--bb-spacing-xl: 20px;--bb-spacing-2xl: 24px;--bb-font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--bb-font-size-xs: 11px;--bb-font-size-sm: 13px;--bb-font-size-md: 14px;--bb-font-size-lg: 16px;--bb-font-size-xl: 18px;--bb-font-size-2xl: 24px;--bb-font-weight-normal: 400;--bb-font-weight-medium: 500;--bb-font-weight-semibold: 600;--bb-font-weight-bold: 700;--bb-line-height: 1.5;--bb-transition-fast: .15s ease;--bb-transition-normal: .2s ease;--bb-transition-slow: .3s ease;--bb-widget-width: 400px;--bb-widget-height: 600px;--bb-widget-max-height: 85vh;--bb-toggle-size: 60px}@media(prefers-color-scheme:dark){:root[data-bb-theme=auto]{--bb-surface-bg: #1a1a2e;--bb-surface-secondary: #252542;--bb-surface-tertiary: #2d2d4a;--bb-text-primary: #f8f9fb;--bb-text-secondary: #9ca3af;--bb-text-tertiary: #6b7280;--bb-border-color: #3d3d5c;--bb-border-color-light: #2d2d4a;--bb-assistant-message-bg: var(--bb-surface-secondary);--bb-assistant-message-text: var(--bb-text-primary)}}._header_1p6z5_1{position:relative;overflow:hidden;transition:all .4s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9){padding:var(--bb-spacing-xl) var(--bb-spacing-xl) var(--bb-spacing-2xl);min-height:140px}._header_1p6z5_1._compact_1p6z5_9{padding:var(--bb-spacing-lg) var(--bb-spacing-xl);min-height:56px}._headerBackground_1p6z5_20{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(135deg,var(--bb-primary-color) 0%,color-mix(in srgb,var(--bb-primary-color) 70%,#2d2d5a) 50%,color-mix(in srgb,var(--bb-primary-color) 50%,#1a1a2e) 100%);transition:opacity .4s cubic-bezier(.4,0,.2,1)}._headerBackground_1p6z5_20:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:radial-gradient(ellipse at 30% 20%,rgba(255,255,255,.08) 0%,transparent 50%)}._headerBackground_1p6z5_20:after{content:"";position:absolute;bottom:0;left:0;right:0;height:60px;background:linear-gradient(to top,rgba(0,0,0,.1),transparent);opacity:1;transition:opacity .3s ease}._compact_1p6z5_9 ._headerBackground_1p6z5_20:after{opacity:0}._headerContent_1p6z5_59{position:relative;z-index:1}._topRow_1p6z5_64{display:flex;align-items:center;justify-content:space-between;transition:margin .4s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9) ._topRow_1p6z5_64{margin-bottom:var(--bb-spacing-xl)}._compact_1p6z5_9 ._topRow_1p6z5_64{margin-bottom:0}._agentInfo_1p6z5_79{display:flex;align-items:center;gap:var(--bb-spacing-md)}._agentLogo_1p6z5_85{border-radius:var(--bb-radius-md);object-fit:cover;box-shadow:0 2px 8px #0003;transition:all .3s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9) ._agentLogo_1p6z5_85{width:36px;height:36px}._compact_1p6z5_9 ._agentLogo_1p6z5_85{width:32px;height:32px}._agentLogoPlaceholder_1p6z5_102{border-radius:var(--bb-radius-md);background:var(--bb-primary-color);display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);box-shadow:0 2px 8px #0003;transition:all .3s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9) ._agentLogoPlaceholder_1p6z5_102{width:36px;height:36px}._compact_1p6z5_9 ._agentLogoPlaceholder_1p6z5_102{width:32px;height:32px}._brainbaseLogo_1p6z5_123{transition:all .3s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9) ._brainbaseLogo_1p6z5_123{width:24px;height:24px}._compact_1p6z5_9 ._brainbaseLogo_1p6z5_123{width:22px;height:22px}._agentName_1p6z5_137{color:var(--bb-text-inverse);font-size:var(--bb-font-size-md);font-weight:var(--bb-font-weight-semibold);opacity:.95}._actions_1p6z5_144{display:flex;gap:var(--bb-spacing-sm)}._actionButton_1p6z5_149{width:32px;height:32px;border-radius:var(--bb-radius-full);background:#ffffff1a;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:background var(--bb-transition-fast),transform var(--bb-transition-fast)}._actionButton_1p6z5_149:hover{background:#fff3;transform:scale(1.05)}._actionButton_1p6z5_149:active{transform:scale(.95)}._actionButton_1p6z5_149:focus-visible{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}._actionButton_1p6z5_149 svg{width:18px;height:18px}._welcomeText_1p6z5_183{color:var(--bb-text-inverse);overflow:hidden;transition:all .4s cubic-bezier(.4,0,.2,1)}._header_1p6z5_1:not(._compact_1p6z5_9) ._welcomeText_1p6z5_183{opacity:1;max-height:100px;transform:translateY(0)}._compact_1p6z5_9 ._welcomeText_1p6z5_183{opacity:0;max-height:0;transform:translateY(-10px);pointer-events:none}._title_1p6z5_202{margin:0;font-size:var(--bb-font-size-2xl);font-weight:var(--bb-font-weight-normal);opacity:.7;line-height:1.2}._subtitle_1p6z5_210{margin:var(--bb-spacing-xs) 0 0;font-size:var(--bb-font-size-2xl);font-weight:var(--bb-font-weight-bold);line-height:1.2}._messageWrapper_1thak_1{display:flex;gap:var(--bb-spacing-md);max-width:85%;animation:_messageSlideIn_1thak_1 .3s ease}@keyframes _messageSlideIn_1thak_1{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}._messageWrapper_1thak_1._user_1thak_19{flex-direction:row-reverse;margin-left:auto}._messageWrapper_1thak_1._assistant_1thak_24{margin-right:auto}._avatar_1thak_28{flex-shrink:0;width:28px;height:28px;border-radius:var(--bb-radius-full);overflow:hidden;margin-top:2px}._avatar_1thak_28 img{width:100%;height:100%;object-fit:cover}._avatarPlaceholder_1thak_43{width:100%;height:100%;background:var(--bb-primary-color);display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse)}._brainbaseLogo_1thak_53{width:18px;height:18px}._messageBubble_1thak_58{padding:var(--bb-spacing-md) var(--bb-spacing-lg);border-radius:var(--bb-radius-lg);position:relative}._user_1thak_19 ._messageBubble_1thak_58{background:var(--bb-user-message-bg);color:var(--bb-user-message-text);border-bottom-right-radius:var(--bb-spacing-xs)}._assistant_1thak_24 ._messageBubble_1thak_58{background:var(--bb-assistant-message-bg);color:var(--bb-assistant-message-text);border-bottom-left-radius:var(--bb-spacing-xs)}._messageBubble_1thak_58._error_1thak_76{background:#fef2f2;border:1px solid #fecaca}._content_1thak_81{font-size:var(--bb-font-size-md);line-height:var(--bb-line-height);word-break:break-word}._user_1thak_19 ._content_1thak_81{white-space:pre-wrap}._content_1thak_81._markdown_1thak_92 p{margin:0}._content_1thak_81._markdown_1thak_92 p+p{margin-top:var(--bb-spacing-sm)}._content_1thak_81._markdown_1thak_92 strong{font-weight:var(--bb-font-weight-semibold)}._content_1thak_81._markdown_1thak_92 em{font-style:italic}._content_1thak_81._markdown_1thak_92 ul,._content_1thak_81._markdown_1thak_92 ol{margin:var(--bb-spacing-sm) 0;padding-left:var(--bb-spacing-lg)}._content_1thak_81._markdown_1thak_92 li{margin:var(--bb-spacing-xs) 0}._content_1thak_81._markdown_1thak_92 li::marker{color:var(--bb-text-secondary)}._content_1thak_81._markdown_1thak_92 code{background:var(--bb-surface-tertiary);padding:2px 6px;border-radius:var(--bb-radius-sm);font-family:SF Mono,Monaco,Courier New,monospace;font-size:.9em}._content_1thak_81._markdown_1thak_92 pre{background:var(--bb-surface-tertiary);padding:var(--bb-spacing-md);border-radius:var(--bb-radius-sm);overflow-x:auto;margin:var(--bb-spacing-sm) 0}._content_1thak_81._markdown_1thak_92 pre code{background:none;padding:0}._content_1thak_81._markdown_1thak_92 a{color:var(--bb-accent-color);text-decoration:none}._content_1thak_81._markdown_1thak_92 a:hover{text-decoration:underline}._content_1thak_81._markdown_1thak_92 blockquote{border-left:3px solid var(--bb-border-color);margin:var(--bb-spacing-sm) 0;padding-left:var(--bb-spacing-md);color:var(--bb-text-secondary)}._content_1thak_81._markdown_1thak_92 hr{border:none;border-top:1px solid var(--bb-border-color);margin:var(--bb-spacing-md) 0}._content_1thak_81._markdown_1thak_92 h1,._content_1thak_81._markdown_1thak_92 h2,._content_1thak_81._markdown_1thak_92 h3,._content_1thak_81._markdown_1thak_92 h4,._content_1thak_81._markdown_1thak_92 h5,._content_1thak_81._markdown_1thak_92 h6{font-weight:var(--bb-font-weight-semibold);margin:var(--bb-spacing-sm) 0 var(--bb-spacing-xs) 0;line-height:1.3}._content_1thak_81._markdown_1thak_92 h1{font-size:1.3em}._content_1thak_81._markdown_1thak_92 h2{font-size:1.2em}._content_1thak_81._markdown_1thak_92 h3{font-size:1.1em}._content_1thak_81._markdown_1thak_92 h4,._content_1thak_81._markdown_1thak_92 h5,._content_1thak_81._markdown_1thak_92 h6{font-size:1em}._cursor_1thak_183{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:2px;animation:_blink_1thak_1 1s infinite;vertical-align:text-bottom}@keyframes _blink_1thak_1{0%,50%{opacity:1}51%,to{opacity:0}}._errorIndicator_1thak_202{display:flex;align-items:center;gap:var(--bb-spacing-xs);margin-top:var(--bb-spacing-sm);color:#ef4444;font-size:var(--bb-font-size-sm)}._errorIndicator_1thak_202 svg{width:14px;height:14px}._toolCall_1wby1_1{display:flex;align-items:center;gap:var(--bb-spacing-md);padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-surface-tertiary);border-radius:var(--bb-radius-md);font-size:var(--bb-font-size-sm);max-width:85%;animation:_slideIn_1wby1_1 .2s ease}@keyframes _slideIn_1wby1_1{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}._iconWrapper_1wby1_24{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0}._spinner_1wby1_33{width:16px;height:16px;border:2px solid var(--bb-border-color);border-top-color:var(--bb-accent-color);border-radius:var(--bb-radius-full);animation:_spin_1wby1_33 .8s linear infinite}@keyframes _spin_1wby1_33{to{transform:rotate(360deg)}}._checkIcon_1wby1_48{width:16px;height:16px;color:#10b981}._errorIcon_1wby1_54{width:16px;height:16px;color:#ef4444}._content_1wby1_60{display:flex;flex-direction:column;gap:2px}._label_1wby1_66{font-size:var(--bb-font-size-xs);color:var(--bb-text-tertiary);text-transform:uppercase;letter-spacing:.5px}._name_1wby1_73{color:var(--bb-text-secondary);font-weight:var(--bb-font-weight-medium)}._pending_1wby1_78{border-left:3px solid var(--bb-accent-color)}._completed_1wby1_82{border-left:3px solid #10b981}._error_1wby1_54{border-left:3px solid #ef4444;background:#fef2f2}._wrapper_10rss_2{display:flex;align-items:flex-start;gap:var(--bb-spacing-md);max-width:85%;margin-right:auto;animation:_slideIn_10rss_1 .3s ease}@keyframes _slideIn_10rss_1{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}._avatar_10rss_22{flex-shrink:0;width:28px;height:28px;border-radius:var(--bb-radius-full);overflow:hidden;margin-top:2px}._avatar_10rss_22 img{width:100%;height:100%;object-fit:cover}._avatarPlaceholder_10rss_37{width:100%;height:100%;background:var(--bb-primary-color);display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse)}._brainbaseLogo_10rss_47{width:18px;height:18px}._bubble_10rss_52{display:flex;align-items:center;gap:5px;padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-assistant-message-bg);border-radius:var(--bb-radius-lg);border-bottom-left-radius:var(--bb-spacing-xs);min-height:44px}._dot_10rss_63{width:8px;height:8px;background:var(--bb-text-tertiary);border-radius:var(--bb-radius-full);animation:_bounce_10rss_1 1.4s infinite ease-in-out both}._dot_10rss_63:nth-child(1){animation-delay:-.32s}._dot_10rss_63:nth-child(2){animation-delay:-.16s}._dot_10rss_63:nth-child(3){animation-delay:0s}@keyframes _bounce_10rss_1{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}._messageList_9tkjc_1{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:var(--bb-spacing-xl);display:flex;flex-direction:column;gap:var(--bb-spacing-lg);scroll-behavior:smooth}._messageList_9tkjc_1::-webkit-scrollbar{width:6px}._messageList_9tkjc_1::-webkit-scrollbar-track{background:transparent}._messageList_9tkjc_1::-webkit-scrollbar-thumb{background:var(--bb-border-color);border-radius:var(--bb-radius-full)}._messageList_9tkjc_1::-webkit-scrollbar-thumb:hover{background:var(--bb-text-tertiary)}._emptyState_9tkjc_30{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--bb-text-tertiary);text-align:center;padding:var(--bb-spacing-2xl)}._emptyIcon_9tkjc_41{width:48px;height:48px;margin-bottom:var(--bb-spacing-lg);opacity:.5}._emptyIcon_9tkjc_41 svg{width:100%;height:100%}._emptyText_9tkjc_53{margin:0;font-size:var(--bb-font-size-md)}._inputWrapper_5lgg7_1{padding:var(--bb-spacing-lg);background:var(--bb-surface-bg);border-top:1px solid var(--bb-border-color-light)}._inputContainer_5lgg7_7{display:flex;align-items:flex-end;gap:var(--bb-spacing-sm);padding:var(--bb-spacing-sm) var(--bb-spacing-md);background:var(--bb-surface-secondary);border-radius:var(--bb-radius-xl);border:1px solid var(--bb-border-color);transition:border-color var(--bb-transition-fast),box-shadow var(--bb-transition-fast)}._inputContainer_5lgg7_7:focus-within{border-color:var(--bb-accent-color);box-shadow:0 0 0 3px #6366f11a}._textarea_5lgg7_23{flex:1;border:none;background:transparent;resize:none;font-family:var(--bb-font-family);font-size:var(--bb-font-size-md);line-height:var(--bb-line-height);color:var(--bb-text-primary);padding:var(--bb-spacing-sm) 0;min-height:24px;max-height:150px}._textarea_5lgg7_23::placeholder{color:var(--bb-text-tertiary)}._textarea_5lgg7_23:focus{outline:none}._textarea_5lgg7_23:disabled{opacity:.6;cursor:not-allowed}._sendButton_5lgg7_50{flex-shrink:0;width:36px;height:36px;border-radius:var(--bb-radius-full);background:var(--bb-primary-color);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:background var(--bb-transition-fast),transform var(--bb-transition-fast)}._sendButton_5lgg7_50:hover:not(:disabled){background:var(--bb-primary-hover);transform:scale(1.05)}._sendButton_5lgg7_50:active:not(:disabled){transform:scale(.98)}._sendButton_5lgg7_50:disabled{opacity:.4;cursor:not-allowed}._sendButton_5lgg7_50:focus-visible{outline:2px solid var(--bb-accent-color);outline-offset:2px}._sendButton_5lgg7_50 svg{width:18px;height:18px}._hint_5lgg7_89{margin-top:var(--bb-spacing-sm);font-size:var(--bb-font-size-xs);color:var(--bb-text-tertiary);text-align:center}._hint_5lgg7_89 kbd{display:inline-block;padding:2px 6px;font-family:var(--bb-font-family);font-size:var(--bb-font-size-xs);background:var(--bb-surface-tertiary);border-radius:4px;border:1px solid var(--bb-border-color)}._poweredBy_9jh5q_1{display:flex;align-items:center;justify-content:center;gap:4px;padding:var(--bb-spacing-sm) var(--bb-spacing-md);background:var(--bb-surface-secondary);border-top:1px solid var(--bb-border-color-light);color:var(--bb-text-tertiary);text-decoration:none;font-size:var(--bb-font-size-xs);font-weight:var(--bb-font-weight-medium);transition:color var(--bb-transition-fast)}._poweredBy_9jh5q_1:hover{color:var(--bb-text-secondary)}._poweredBy_9jh5q_1:hover ._logo_9jh5q_20{opacity:.9}._poweredText_9jh5q_24{opacity:.7}._logo_9jh5q_20{width:12px;height:12px;opacity:.6;transition:opacity var(--bb-transition-fast);margin-left:2px}._text_9jh5q_36{letter-spacing:.01em;font-weight:var(--bb-font-weight-semibold)}._container_tgamx_1{position:relative;display:flex;flex-direction:column;width:var(--bb-widget-width);height:var(--bb-widget-height);max-height:var(--bb-widget-max-height);background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);box-shadow:var(--bb-shadow-xl);overflow:hidden;overscroll-behavior:contain;animation:_slideUp_tgamx_1 .3s ease}@keyframes _slideUp_tgamx_1{0%{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}._body_tgamx_27{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;overscroll-behavior:contain;background:var(--bb-surface-bg)}._confirmationOverlay_tgamx_38{position:absolute;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100;animation:_overlayFadeIn_tgamx_1 .2s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}@keyframes _overlayFadeIn_tgamx_1{0%{opacity:0}to{opacity:1}}._confirmationDialog_tgamx_59{background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);padding:var(--bb-spacing-2xl);margin:var(--bb-spacing-xl);box-shadow:var(--bb-shadow-xl);animation:_dialogSlideUp_tgamx_1 .25s cubic-bezier(.4,0,.2,1);max-width:300px;width:calc(100% - 48px)}@keyframes _dialogSlideUp_tgamx_1{0%{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}._confirmationText_tgamx_81{margin:0 0 var(--bb-spacing-xl);font-size:var(--bb-font-size-md);color:var(--bb-text-primary);line-height:var(--bb-line-height);text-align:center}._confirmationButtons_tgamx_89{display:flex;gap:var(--bb-spacing-md)}._cancelButton_tgamx_94,._confirmButton_tgamx_95{flex:1;padding:var(--bb-spacing-md) var(--bb-spacing-lg);border-radius:var(--bb-radius-md);font-size:var(--bb-font-size-sm);font-weight:var(--bb-font-weight-medium);cursor:pointer;transition:all var(--bb-transition-fast)}._cancelButton_tgamx_94{background:var(--bb-surface-secondary);border:1px solid var(--bb-border-color);color:var(--bb-text-secondary)}._cancelButton_tgamx_94:hover{background:var(--bb-surface-tertiary);color:var(--bb-text-primary)}._confirmButton_tgamx_95{background:#ef4444;border:none;color:#fff}._confirmButton_tgamx_95:hover{background:#dc2626}._confirmButton_tgamx_95:active,._cancelButton_tgamx_94:active{transform:scale(.98)}@media(max-width:480px){._container_tgamx_1{width:100vw;height:100vh;max-height:100vh;border-radius:0}}._toggleButton_11dqz_1{position:relative;display:flex;align-items:center;justify-content:center;width:var(--bb-toggle-size);height:var(--bb-toggle-size);border-radius:var(--bb-radius-full);background:linear-gradient(135deg,var(--bb-primary-color) 0%,#2d2d5a 100%);border:none;cursor:pointer;box-shadow:var(--bb-shadow-lg);transition:transform var(--bb-transition-normal),box-shadow var(--bb-transition-normal);color:var(--bb-text-inverse)}._toggleButton_11dqz_1:hover{transform:scale(1.05);box-shadow:var(--bb-shadow-xl)}._toggleButton_11dqz_1:active{transform:scale(.98)}._toggleButton_11dqz_1:focus-visible{outline:2px solid var(--bb-accent-color);outline-offset:2px}._icon_11dqz_31{width:28px;height:28px}._agentLogo_11dqz_36{width:36px;height:36px;border-radius:var(--bb-radius-full);object-fit:cover}._unreadBadge_11dqz_43{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;padding:0 6px;border-radius:var(--bb-radius-full);background:#ef4444;color:#fff;font-size:var(--bb-font-size-xs);font-weight:var(--bb-font-weight-bold);display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px #0003}._container_kgfgt_1{display:flex;flex-direction:column;width:var(--bb-widget-width);height:var(--bb-widget-height);max-height:var(--bb-widget-max-height);background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);box-shadow:var(--bb-shadow-xl);overflow:hidden;animation:_slideUp_kgfgt_1 .3s ease}@keyframes _slideUp_kgfgt_1{0%{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}._header_kgfgt_25{display:flex;align-items:center;justify-content:space-between;padding:var(--bb-spacing-lg) var(--bb-spacing-xl);background:linear-gradient(135deg,var(--bb-primary-color) 0%,color-mix(in srgb,var(--bb-primary-color) 70%,#2d2d5a) 50%,color-mix(in srgb,var(--bb-primary-color) 50%,#1a1a2e) 100%)}._logoWrapper_kgfgt_38{width:32px;height:32px;display:flex;align-items:center;justify-content:center}._logo_kgfgt_38{width:24px;height:24px}._closeButton_kgfgt_51{width:32px;height:32px;border-radius:var(--bb-radius-full);background:#ffffff1a;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:background var(--bb-transition-fast)}._closeButton_kgfgt_51:hover{background:#fff3}._closeButton_kgfgt_51 svg{width:18px;height:18px}._content_kgfgt_74{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--bb-spacing-2xl);text-align:center}._iconWrapper_kgfgt_84{width:64px;height:64px;border-radius:var(--bb-radius-full);background:var(--bb-surface-secondary);display:flex;align-items:center;justify-content:center;margin-bottom:var(--bb-spacing-xl)}._errorIcon_kgfgt_95{width:32px;height:32px;color:var(--bb-text-tertiary)}._title_kgfgt_101{margin:0;font-size:var(--bb-font-size-xl);font-weight:var(--bb-font-weight-semibold);color:var(--bb-text-primary);margin-bottom:var(--bb-spacing-sm)}._description_kgfgt_109{margin:0;font-size:var(--bb-font-size-md);color:var(--bb-text-secondary);line-height:var(--bb-line-height);max-width:280px}._retryButton_kgfgt_117{margin-top:var(--bb-spacing-xl);padding:var(--bb-spacing-sm) var(--bb-spacing-xl);border-radius:var(--bb-radius-full);background:var(--bb-primary-color);border:none;color:var(--bb-text-inverse);font-size:var(--bb-font-size-md);font-weight:var(--bb-font-weight-medium);cursor:pointer;transition:all var(--bb-transition-fast)}._retryButton_kgfgt_117:hover{transform:scale(1.02);box-shadow:var(--bb-shadow-md)}._retryButton_kgfgt_117:active{transform:scale(.98)}._footer_kgfgt_139{padding:var(--bb-spacing-md);background:var(--bb-surface-secondary);display:flex;justify-content:center}._poweredBy_kgfgt_146{display:flex;align-items:center;gap:var(--bb-spacing-xs);font-size:var(--bb-font-size-xs);color:var(--bb-text-tertiary);text-decoration:none;transition:color var(--bb-transition-fast)}._poweredBy_kgfgt_146:hover{color:var(--bb-text-secondary)}._footerLogo_kgfgt_160{width:14px;height:14px}@media(max-width:480px){._container_kgfgt_1{width:100vw;height:100vh;max-height:100vh;border-radius:0}}._widget_1ehud_1{font-family:var(--bb-font-family);font-size:var(--bb-font-size-md);line-height:var(--bb-line-height);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}._bottom-right_1ehud_10{position:fixed;bottom:20px;right:20px;z-index:9999}._bottom-left_1ehud_17{position:fixed;bottom:20px;left:20px;z-index:9999}._inline_1ehud_25{position:relative;width:100%;max-width:var(--bb-widget-width)}@media(max-width:480px){._bottom-right_1ehud_10,._bottom-left_1ehud_17{bottom:0;right:0;left:0}} +:root{--bb-primary-color: #1a1a2e;--bb-primary-hover: #16162a;--bb-primary-light: rgba(26, 26, 46, .1);--bb-accent-color: #6366f1;--bb-accent-hover: #5558e3;--bb-focus-color: #6366f1;--bb-focus-shadow: rgba(99, 102, 241, .1);--bb-surface-bg: #ffffff;--bb-surface-secondary: #f8f9fb;--bb-surface-tertiary: #f1f3f9;--bb-text-primary: #1a1a2e;--bb-text-secondary: #6b7280;--bb-text-tertiary: #9ca3af;--bb-text-inverse: #ffffff;--bb-user-message-bg: var(--bb-primary-color);--bb-user-message-text: var(--bb-text-inverse);--bb-assistant-message-bg: var(--bb-surface-secondary);--bb-assistant-message-text: var(--bb-text-primary);--bb-border-color: #e5e7eb;--bb-border-color-light: #f1f3f9;--bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, .04);--bb-shadow-md: 0 4px 12px rgba(0, 0, 0, .08);--bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, .12);--bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, .16);--bb-radius-sm: 8px;--bb-radius-md: 12px;--bb-radius-lg: 16px;--bb-radius-xl: 20px;--bb-radius-full: 9999px;--bb-spacing-xs: 4px;--bb-spacing-sm: 8px;--bb-spacing-md: 12px;--bb-spacing-lg: 16px;--bb-spacing-xl: 20px;--bb-spacing-2xl: 24px;--bb-font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--bb-font-size-xs: 11px;--bb-font-size-sm: 13px;--bb-font-size-md: 14px;--bb-font-size-lg: 16px;--bb-font-size-xl: 18px;--bb-font-size-2xl: 24px;--bb-font-weight-normal: 400;--bb-font-weight-medium: 500;--bb-font-weight-semibold: 600;--bb-font-weight-bold: 700;--bb-line-height: 1.5;--bb-transition-fast: .15s ease;--bb-transition-normal: .2s ease;--bb-transition-slow: .3s ease;--bb-widget-width: 440px;--bb-widget-height: 720px;--bb-widget-max-height: 90vh;--bb-toggle-size: 60px}[data-bb-theme=dark]{--bb-primary-color: #1a1a2e;--bb-primary-hover: #16162a;--bb-primary-light: rgba(26, 26, 46, .3);--bb-surface-bg: #0d0d14;--bb-surface-secondary: #1a1a2e;--bb-surface-tertiary: #252542;--bb-text-primary: #ffffff;--bb-text-secondary: #a1a1aa;--bb-text-tertiary: #71717a;--bb-text-inverse: #0d0d14;--bb-user-message-bg: #f5f5f0;--bb-user-message-text: #1a1a2e;--bb-assistant-message-bg: #1a1a30;--bb-assistant-message-text: #ffffff;--bb-border-color: #2d2d4a;--bb-border-color-light: #1f1f35;--bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, .2);--bb-shadow-md: 0 4px 12px rgba(0, 0, 0, .3);--bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, .4);--bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, .5);--bb-accent-color: #818cf8;--bb-accent-hover: #6366f1;--bb-focus-color: #ffffff;--bb-focus-shadow: rgba(255, 255, 255, .15)}[data-bb-theme=granite]{--bb-primary-color: #374151;--bb-primary-hover: #1f2937;--bb-primary-light: rgba(55, 65, 81, .15);--bb-surface-bg: #1f2937;--bb-surface-secondary: #374151;--bb-surface-tertiary: #4b5563;--bb-text-primary: #f3f4f6;--bb-text-secondary: #d1d5db;--bb-text-tertiary: #9ca3af;--bb-text-inverse: #111827;--bb-user-message-bg: #e5e7eb;--bb-user-message-text: #1f2937;--bb-assistant-message-bg: #4b5563;--bb-assistant-message-text: #f9fafb;--bb-border-color: #4b5563;--bb-border-color-light: #374151;--bb-shadow-sm: 0 1px 2px rgba(0, 0, 0, .15);--bb-shadow-md: 0 4px 12px rgba(0, 0, 0, .25);--bb-shadow-lg: 0 12px 40px rgba(0, 0, 0, .35);--bb-shadow-xl: 0 20px 60px rgba(0, 0, 0, .45);--bb-accent-color: #60a5fa;--bb-accent-hover: #3b82f6}@media(prefers-color-scheme:dark){:root[data-bb-theme=auto]{--bb-surface-bg: #1a1a2e;--bb-surface-secondary: #252542;--bb-surface-tertiary: #2d2d4a;--bb-text-primary: #f8f9fb;--bb-text-secondary: #9ca3af;--bb-text-tertiary: #6b7280;--bb-border-color: #3d3d5c;--bb-border-color-light: #2d2d4a;--bb-assistant-message-bg: var(--bb-surface-secondary);--bb-assistant-message-text: var(--bb-text-primary)}}._header_f8qt7_1{position:relative;overflow:visible;transition:all .4s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9){padding:var(--bb-spacing-xl) var(--bb-spacing-xl) var(--bb-spacing-2xl);min-height:140px}._header_f8qt7_1._compact_f8qt7_9{padding:var(--bb-spacing-lg) var(--bb-spacing-xl);min-height:56px}._headerBackground_f8qt7_20{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--bb-primary-color);transition:opacity .4s cubic-bezier(.4,0,.2,1)}._headerBackground_f8qt7_20:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(135deg,transparent 0%,color-mix(in srgb,var(--bb-primary-color) 70%,#2d2d5a) 50%,color-mix(in srgb,var(--bb-primary-color) 50%,#1a1a2e) 100%);opacity:var(--bb-primary-gradient, 0);transition:opacity .3s ease}._headerBackground_f8qt7_20:after{content:"";position:absolute;bottom:0;left:0;right:0;height:60px;background:linear-gradient(to top,rgba(0,0,0,.1),transparent);opacity:var(--bb-primary-gradient, 0);transition:opacity .3s ease}._compact_f8qt7_9 ._headerBackground_f8qt7_20:after{opacity:0}._headerContent_f8qt7_58{position:relative;z-index:1}._topRow_f8qt7_63{display:flex;align-items:center;justify-content:space-between;transition:margin .4s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9) ._topRow_f8qt7_63{margin-bottom:var(--bb-spacing-xl)}._compact_f8qt7_9 ._topRow_f8qt7_63{margin-bottom:0}._agentInfo_f8qt7_78{display:flex;align-items:center;gap:var(--bb-spacing-md)}._agentLogo_f8qt7_84{border-radius:var(--bb-radius-sm);object-fit:cover;transition:all .3s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9) ._agentLogo_f8qt7_84{width:42px;height:42px}._compact_f8qt7_9 ._agentLogo_f8qt7_84{width:36px;height:36px}._agentLogoPlaceholder_f8qt7_100{border-radius:var(--bb-radius-sm);background:var(--bb-accent-color-custom, var(--bb-primary-color));display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:all .3s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9) ._agentLogoPlaceholder_f8qt7_100{width:42px;height:42px}._compact_f8qt7_9 ._agentLogoPlaceholder_f8qt7_100{width:36px;height:36px}._brainbaseLogo_f8qt7_120{transition:all .3s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9) ._brainbaseLogo_f8qt7_120{width:28px;height:28px}._compact_f8qt7_9 ._brainbaseLogo_f8qt7_120{width:24px;height:24px}._backButton_f8qt7_135{width:24px;height:24px;border-radius:var(--bb-radius-full);background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-header-text-color, var(--bb-text-inverse));opacity:.8;transition:opacity var(--bb-transition-fast),transform var(--bb-transition-fast);margin-right:2px;margin-left:-4px}._backButton_f8qt7_135:hover{opacity:1}._backButton_f8qt7_135:active{transform:scale(.9)}._backButton_f8qt7_135 svg{width:20px;height:20px}._agentTextWrapper_f8qt7_165{display:flex;flex-direction:column;gap:1px}._agentName_f8qt7_171{color:var(--bb-header-text-color, var(--bb-text-inverse));font-size:var(--bb-agent-name-font-size, 16px);font-weight:var(--bb-font-weight-semibold);opacity:.95;line-height:1.2}._headerSubtitle_f8qt7_179{color:var(--bb-header-text-color, var(--bb-text-inverse));font-size:calc(var(--bb-agent-name-font-size, 16px) - 2px);font-weight:var(--bb-font-weight-normal);opacity:.7;line-height:1.3}._actions_f8qt7_187{display:flex;gap:var(--bb-spacing-sm)}._actionButton_f8qt7_192{width:32px;height:32px;border-radius:var(--bb-radius-full);background:#0000001a;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-header-text-color, var(--bb-text-inverse));transition:background var(--bb-transition-fast),transform var(--bb-transition-fast)}._actionButton_f8qt7_192:hover{background:#fff3;transform:scale(1.05)}._actionButton_f8qt7_192:active{transform:scale(.95)}._actionButton_f8qt7_192:focus-visible{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}._actionButton_f8qt7_192 svg{width:18px;height:18px}._welcomeText_f8qt7_226{color:var(--bb-header-text-color, var(--bb-text-inverse));overflow:visible;transition:all .4s cubic-bezier(.4,0,.2,1)}._header_f8qt7_1:not(._compact_f8qt7_9) ._welcomeText_f8qt7_226{opacity:1;max-height:100px;transform:translateY(0)}._compact_f8qt7_9 ._welcomeText_f8qt7_226{opacity:0;max-height:0;transform:translateY(-10px);pointer-events:none}._title_f8qt7_245{margin:0;font-size:var(--bb-font-size-2xl);font-weight:var(--bb-font-weight-normal);opacity:.7;line-height:1.2}._subtitle_f8qt7_253{margin:var(--bb-spacing-xs) 0 0;font-size:var(--bb-font-size-2xl);font-weight:var(--bb-font-weight-bold);line-height:1.2}._menuContainer_f8qt7_261{position:relative}._dropdown_f8qt7_266{position:absolute;top:calc(100% + 8px);right:0;min-width:180px;background:var(--bb-surface-bg);border-radius:var(--bb-radius-md);box-shadow:var(--bb-shadow-lg);padding:var(--bb-spacing-xs);z-index:9999;animation:_dropdownSlideIn_f8qt7_1 .15s ease}@keyframes _dropdownSlideIn_f8qt7_1{0%{opacity:0;transform:translateY(-4px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}._dropdownItem_f8qt7_290{display:flex;align-items:center;gap:var(--bb-spacing-sm);width:100%;padding:var(--bb-spacing-sm) var(--bb-spacing-md);background:transparent;border:none;border-radius:var(--bb-radius-sm);cursor:pointer;font-family:var(--bb-font-family);font-size:var(--bb-font-size-sm);font-weight:var(--bb-font-weight-medium);color:var(--bb-text-primary);text-align:left;white-space:nowrap;transition:background var(--bb-transition-fast)}._dropdownItem_f8qt7_290:hover{background:var(--bb-surface-secondary)}._dropdownItem_f8qt7_290:active{background:var(--bb-surface-tertiary)}._dropdownItem_f8qt7_290 svg{width:16px;height:16px;min-width:16px;min-height:16px;flex-shrink:0;color:var(--bb-text-secondary)}._dropdownIcon_f8qt7_327{width:16px;height:16px;min-width:16px;min-height:16px;flex-shrink:0;color:var(--bb-text-secondary)}._messageWrapper_1lg4d_1{display:flex;gap:var(--bb-spacing-md);max-width:85%;animation:_messageSlideIn_1lg4d_1 .3s ease}@keyframes _messageSlideIn_1lg4d_1{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}._messageWrapper_1lg4d_1._user_1lg4d_19{flex-direction:row-reverse;margin-left:auto}._messageWrapper_1lg4d_1._assistant_1lg4d_24{margin-right:auto}._avatar_1lg4d_28{flex-shrink:0;width:28px;height:28px;border-radius:var(--bb-radius-full);overflow:hidden;margin-top:2px}._avatar_1lg4d_28 img{width:100%;height:100%;object-fit:cover}._avatarPlaceholder_1lg4d_43{width:100%;height:100%;background:var(--bb-primary-color);display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse)}._brainbaseLogo_1lg4d_53{width:18px;height:18px}._messageBubble_1lg4d_58{padding:var(--bb-spacing-md) var(--bb-spacing-lg);border-radius:22px;position:relative}._user_1lg4d_19 ._messageBubble_1lg4d_58{background:var(--bb-user-message-bg);color:var(--bb-user-message-text);border-bottom-right-radius:6px}._assistant_1lg4d_24 ._messageBubble_1lg4d_58{background:var(--bb-assistant-message-bg);color:var(--bb-assistant-message-text);border-bottom-left-radius:6px}._messageBubble_1lg4d_58._error_1lg4d_76{background:#fef2f2;border:1px solid #fecaca}._content_1lg4d_81{font-size:var(--bb-message-font-size, 15px);line-height:var(--bb-line-height);word-break:break-word}._user_1lg4d_19 ._content_1lg4d_81{white-space:pre-wrap}._content_1lg4d_81._markdown_1lg4d_92 p{margin:0}._content_1lg4d_81._markdown_1lg4d_92 p+p{margin-top:var(--bb-spacing-sm)}._content_1lg4d_81._markdown_1lg4d_92 strong{font-weight:var(--bb-font-weight-semibold)}._content_1lg4d_81._markdown_1lg4d_92 em{font-style:italic}._content_1lg4d_81._markdown_1lg4d_92 ul,._content_1lg4d_81._markdown_1lg4d_92 ol{margin:var(--bb-spacing-sm) 0;padding-left:var(--bb-spacing-lg)}._content_1lg4d_81._markdown_1lg4d_92 li{margin:var(--bb-spacing-xs) 0}._content_1lg4d_81._markdown_1lg4d_92 li::marker{color:var(--bb-text-secondary)}._content_1lg4d_81._markdown_1lg4d_92 code{background:var(--bb-surface-tertiary);padding:2px 6px;border-radius:var(--bb-radius-sm);font-family:SF Mono,Monaco,Courier New,monospace;font-size:.9em}._content_1lg4d_81._markdown_1lg4d_92 pre{background:var(--bb-surface-tertiary);padding:var(--bb-spacing-md);border-radius:var(--bb-radius-sm);overflow-x:auto;margin:var(--bb-spacing-sm) 0}._content_1lg4d_81._markdown_1lg4d_92 pre code{background:none;padding:0}._content_1lg4d_81._markdown_1lg4d_92 a{color:var(--bb-accent-color);text-decoration:none}._content_1lg4d_81._markdown_1lg4d_92 a:hover{text-decoration:underline}._content_1lg4d_81._markdown_1lg4d_92 blockquote{border-left:3px solid var(--bb-border-color);margin:var(--bb-spacing-sm) 0;padding-left:var(--bb-spacing-md);color:var(--bb-text-secondary)}._content_1lg4d_81._markdown_1lg4d_92 hr{border:none;border-top:1px solid var(--bb-border-color);margin:var(--bb-spacing-md) 0}._content_1lg4d_81._markdown_1lg4d_92 h1,._content_1lg4d_81._markdown_1lg4d_92 h2,._content_1lg4d_81._markdown_1lg4d_92 h3,._content_1lg4d_81._markdown_1lg4d_92 h4,._content_1lg4d_81._markdown_1lg4d_92 h5,._content_1lg4d_81._markdown_1lg4d_92 h6{font-weight:var(--bb-font-weight-semibold);margin:var(--bb-spacing-sm) 0 var(--bb-spacing-xs) 0;line-height:1.3}._content_1lg4d_81._markdown_1lg4d_92 h1{font-size:1.3em}._content_1lg4d_81._markdown_1lg4d_92 h2{font-size:1.2em}._content_1lg4d_81._markdown_1lg4d_92 h3{font-size:1.1em}._content_1lg4d_81._markdown_1lg4d_92 h4,._content_1lg4d_81._markdown_1lg4d_92 h5,._content_1lg4d_81._markdown_1lg4d_92 h6{font-size:1em}._cursor_1lg4d_183{display:inline-block;width:2px;height:1em;background:currentColor;margin-left:2px;animation:_blink_1lg4d_1 1s infinite;vertical-align:text-bottom}@keyframes _blink_1lg4d_1{0%,50%{opacity:1}51%,to{opacity:0}}._errorIndicator_1lg4d_202{display:flex;align-items:center;gap:var(--bb-spacing-xs);margin-top:var(--bb-spacing-sm);color:#ef4444;font-size:var(--bb-font-size-sm)}._errorIndicator_1lg4d_202 svg{width:14px;height:14px}._messageContent_1lg4d_219{display:flex;flex-direction:column;gap:var(--bb-spacing-xs);min-width:0}._messageInfo_1lg4d_227{display:flex;align-items:center;gap:var(--bb-spacing-xs);font-size:calc(var(--bb-message-font-size, 15px) - 2px);color:var(--bb-text-tertiary);padding-left:var(--bb-spacing-lg);animation:_slideIn_1lg4d_1 .25s cubic-bezier(.4,0,.2,1)}@keyframes _slideIn_1lg4d_1{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes _slideOut_1lg4d_1{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}._messageInfo_1lg4d_227._fadeOut_1lg4d_259{animation:_slideOut_1lg4d_1 .2s cubic-bezier(.4,0,.2,1) forwards}._messageInfo_1lg4d_227 ._agentName_1lg4d_263{font-weight:var(--bb-font-weight-medium);color:var(--bb-text-secondary)}._messageInfo_1lg4d_227 ._agentRole_1lg4d_268{color:var(--bb-text-tertiary)}._messageInfo_1lg4d_227 ._separator_1lg4d_272{color:var(--bb-text-tertiary);opacity:.6}._messageInfo_1lg4d_227 ._timestamp_1lg4d_277{color:var(--bb-text-tertiary)}._toolCall_1wby1_1{display:flex;align-items:center;gap:var(--bb-spacing-md);padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-surface-tertiary);border-radius:var(--bb-radius-md);font-size:var(--bb-font-size-sm);max-width:85%;animation:_slideIn_1wby1_1 .2s ease}@keyframes _slideIn_1wby1_1{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}._iconWrapper_1wby1_24{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0}._spinner_1wby1_33{width:16px;height:16px;border:2px solid var(--bb-border-color);border-top-color:var(--bb-accent-color);border-radius:var(--bb-radius-full);animation:_spin_1wby1_33 .8s linear infinite}@keyframes _spin_1wby1_33{to{transform:rotate(360deg)}}._checkIcon_1wby1_48{width:16px;height:16px;color:#10b981}._errorIcon_1wby1_54{width:16px;height:16px;color:#ef4444}._content_1wby1_60{display:flex;flex-direction:column;gap:2px}._label_1wby1_66{font-size:var(--bb-font-size-xs);color:var(--bb-text-tertiary);text-transform:uppercase;letter-spacing:.5px}._name_1wby1_73{color:var(--bb-text-secondary);font-weight:var(--bb-font-weight-medium)}._pending_1wby1_78{border-left:3px solid var(--bb-accent-color)}._completed_1wby1_82{border-left:3px solid #10b981}._error_1wby1_54{border-left:3px solid #ef4444;background:#fef2f2}._wrapper_10rss_2{display:flex;align-items:flex-start;gap:var(--bb-spacing-md);max-width:85%;margin-right:auto;animation:_slideIn_10rss_1 .3s ease}@keyframes _slideIn_10rss_1{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}._avatar_10rss_22{flex-shrink:0;width:28px;height:28px;border-radius:var(--bb-radius-full);overflow:hidden;margin-top:2px}._avatar_10rss_22 img{width:100%;height:100%;object-fit:cover}._avatarPlaceholder_10rss_37{width:100%;height:100%;background:var(--bb-primary-color);display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse)}._brainbaseLogo_10rss_47{width:18px;height:18px}._bubble_10rss_52{display:flex;align-items:center;gap:5px;padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-assistant-message-bg);border-radius:var(--bb-radius-lg);border-bottom-left-radius:var(--bb-spacing-xs);min-height:44px}._dot_10rss_63{width:8px;height:8px;background:var(--bb-text-tertiary);border-radius:var(--bb-radius-full);animation:_bounce_10rss_1 1.4s infinite ease-in-out both}._dot_10rss_63:nth-child(1){animation-delay:-.32s}._dot_10rss_63:nth-child(2){animation-delay:-.16s}._dot_10rss_63:nth-child(3){animation-delay:0s}@keyframes _bounce_10rss_1{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}._messageList_9tkjc_1{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:var(--bb-spacing-xl);display:flex;flex-direction:column;gap:var(--bb-spacing-lg);scroll-behavior:smooth}._messageList_9tkjc_1::-webkit-scrollbar{width:6px}._messageList_9tkjc_1::-webkit-scrollbar-track{background:transparent}._messageList_9tkjc_1::-webkit-scrollbar-thumb{background:var(--bb-border-color);border-radius:var(--bb-radius-full)}._messageList_9tkjc_1::-webkit-scrollbar-thumb:hover{background:var(--bb-text-tertiary)}._emptyState_9tkjc_30{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--bb-text-tertiary);text-align:center;padding:var(--bb-spacing-2xl)}._emptyIcon_9tkjc_41{width:48px;height:48px;margin-bottom:var(--bb-spacing-lg);opacity:.5}._emptyIcon_9tkjc_41 svg{width:100%;height:100%}._emptyText_9tkjc_53{margin:0;font-size:var(--bb-font-size-md)}._inputWrapper_6o34z_1{padding:var(--bb-spacing-lg);background:var(--bb-surface-bg);border-top:1px solid var(--bb-border-color-light)}._inputContainer_6o34z_7{display:flex;flex-direction:column;gap:var(--bb-spacing-sm);padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-surface-secondary);border-radius:var(--bb-radius-xl);border:1px solid var(--bb-border-color);transition:border-color var(--bb-transition-fast),box-shadow var(--bb-transition-fast)}._inputContainer_6o34z_7:focus-within{border-color:var(--bb-focus-color, var(--bb-accent-color));box-shadow:0 0 0 3px var(--bb-focus-shadow, rgba(99, 102, 241, .1))}._textarea_6o34z_23{width:100%;border:none;background:transparent;resize:none;font-family:var(--bb-font-family);font-size:var(--bb-font-size-md);line-height:var(--bb-line-height);color:var(--bb-text-primary);padding:0;min-height:24px;max-height:150px}._textarea_6o34z_23::placeholder{color:var(--bb-text-tertiary)}._textarea_6o34z_23:focus{outline:none}._textarea_6o34z_23:disabled{opacity:.6;cursor:not-allowed}._inputActions_6o34z_51{display:flex;align-items:center;justify-content:space-between}._actionIcons_6o34z_57{display:flex;align-items:center;gap:var(--bb-spacing-xs)}._iconButton_6o34z_63{width:32px;height:32px;border-radius:var(--bb-radius-md);background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-tertiary);transition:color var(--bb-transition-fast),background var(--bb-transition-fast)}._iconButton_6o34z_63:hover:not(:disabled){color:var(--bb-text-secondary);background:var(--bb-surface-tertiary)}._iconButton_6o34z_63:active:not(:disabled){transform:scale(.95)}._iconButton_6o34z_63:disabled{opacity:.4;cursor:not-allowed}._iconButton_6o34z_63 svg{width:20px;height:20px}._sendButton_6o34z_96{flex-shrink:0;width:36px;height:36px;border-radius:var(--bb-radius-full);background:var(--bb-accent-color-custom, var(--bb-primary-color));border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:background var(--bb-transition-fast),transform var(--bb-transition-fast),opacity var(--bb-transition-fast),filter var(--bb-transition-fast)}._sendButton_6o34z_96:hover:not(:disabled){filter:brightness(.9);transform:scale(1.05)}._sendButton_6o34z_96:active:not(:disabled){transform:scale(.95)}._sendButton_6o34z_96:disabled{opacity:.4;cursor:not-allowed}._sendButton_6o34z_96._audioButton_6o34z_126{opacity:1;cursor:pointer}._sendButton_6o34z_96:focus-visible{outline:2px solid var(--bb-accent-color);outline-offset:2px}._sendButton_6o34z_96 svg{width:18px;height:18px}._sendButton_6o34z_96 svg._audioIcon_6o34z_141{width:22px;height:22px}._poweredBy_9jh5q_1{display:flex;align-items:center;justify-content:center;gap:4px;padding:var(--bb-spacing-sm) var(--bb-spacing-md);background:var(--bb-surface-secondary);border-top:1px solid var(--bb-border-color-light);color:var(--bb-text-tertiary);text-decoration:none;font-size:var(--bb-font-size-xs);font-weight:var(--bb-font-weight-medium);transition:color var(--bb-transition-fast)}._poweredBy_9jh5q_1:hover{color:var(--bb-text-secondary)}._poweredBy_9jh5q_1:hover ._logo_9jh5q_20{opacity:.9}._poweredText_9jh5q_24{opacity:.7}._logo_9jh5q_20{width:12px;height:12px;opacity:.6;transition:opacity var(--bb-transition-fast);margin-left:2px}._text_9jh5q_36{letter-spacing:.01em;font-weight:var(--bb-font-weight-semibold)}._container_1p2zy_1{display:flex;flex-direction:column;height:100%;background:#0a0a0f;position:relative;overflow:hidden}._orbContainer_1p2zy_11{flex:1;cursor:pointer;min-height:200px}._orbContainer_1p2zy_11 canvas{display:block}._status_1p2zy_22{position:absolute;top:16px;left:50%;transform:translate(-50%);color:#fff9;font-size:var(--bb-font-size-sm);z-index:10;text-align:center}._instruction_1p2zy_34{position:absolute;bottom:100px;left:50%;transform:translate(-50%);color:#ffffff80;font-size:var(--bb-font-size-sm);z-index:10;text-align:center;pointer-events:none}._controls_1p2zy_47{position:absolute;bottom:24px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:var(--bb-spacing-md);z-index:10}._controlButton_1p2zy_58{width:48px;height:48px;border-radius:var(--bb-radius-full);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform .15s ease,background .15s ease;background:#ffffff1a;color:#fff}._controlButton_1p2zy_58:hover{transform:scale(1.05);background:#fff3}._controlButton_1p2zy_58:active{transform:scale(.95)}._controlButton_1p2zy_58 svg{width:24px;height:24px}._controlButton_1p2zy_58._muted_1p2zy_86{background:#ef4444cc}._controlButton_1p2zy_58._muted_1p2zy_86:hover{background:#ef4444}._backButton_1p2zy_95{padding:12px 24px;background:#ffffff1a;border:none;border-radius:var(--bb-radius-full);color:#fff;font-size:var(--bb-font-size-sm);cursor:pointer;transition:background .15s ease,transform .15s ease}._backButton_1p2zy_95:hover{background:#fff3;transform:scale(1.02)}._backButton_1p2zy_95:active{transform:scale(.98)}._container_zospk_1{position:relative;display:flex;flex-direction:column;width:var(--bb-widget-width);height:var(--bb-widget-height);max-height:var(--bb-widget-max-height);background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);box-shadow:var(--bb-shadow-xl);overflow:hidden;overscroll-behavior:contain;transition:width .3s cubic-bezier(.4,0,.2,1),height .3s cubic-bezier(.4,0,.2,1),max-height .3s cubic-bezier(.4,0,.2,1)}._body_zospk_19{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;overscroll-behavior:contain;background:var(--bb-surface-bg)}._confirmationOverlay_zospk_30{position:absolute;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100;animation:_overlayFadeIn_zospk_1 .2s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}@keyframes _overlayFadeIn_zospk_1{0%{opacity:0}to{opacity:1}}._confirmationDialog_zospk_51{background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);padding:var(--bb-spacing-2xl);margin:var(--bb-spacing-xl);box-shadow:var(--bb-shadow-xl);animation:_dialogSlideUp_zospk_1 .25s cubic-bezier(.4,0,.2,1);max-width:300px;width:calc(100% - 48px)}@keyframes _dialogSlideUp_zospk_1{0%{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}._confirmationText_zospk_73{margin:0 0 var(--bb-spacing-xl);font-size:var(--bb-font-size-md);color:var(--bb-text-primary);line-height:var(--bb-line-height);text-align:center}._confirmationButtons_zospk_81{display:flex;gap:var(--bb-spacing-md)}._cancelButton_zospk_86,._confirmButton_zospk_87{flex:1;padding:var(--bb-spacing-md) var(--bb-spacing-lg);border-radius:var(--bb-radius-md);font-size:var(--bb-font-size-sm);font-weight:var(--bb-font-weight-medium);cursor:pointer;transition:all var(--bb-transition-fast)}._cancelButton_zospk_86{background:var(--bb-surface-secondary);border:1px solid var(--bb-border-color);color:var(--bb-text-secondary)}._cancelButton_zospk_86:hover{background:var(--bb-surface-tertiary);color:var(--bb-text-primary)}._confirmButton_zospk_87{background:#ef4444;border:none;color:#fff}._confirmButton_zospk_87:hover{background:#dc2626}._confirmButton_zospk_87:active,._cancelButton_zospk_86:active{transform:scale(.98)}@media(max-width:480px){._container_zospk_1{width:100vw;height:100vh;max-height:100vh;border-radius:0}}._toggleButton_186ig_1{position:relative;display:flex;align-items:center;justify-content:center;width:var(--bb-toggle-size);height:var(--bb-toggle-size);border-radius:var(--bb-radius-full);background:var(--bb-accent-color-custom, var(--bb-primary-color));border:none;cursor:pointer;box-shadow:var(--bb-shadow-lg);transition:transform var(--bb-transition-normal),box-shadow var(--bb-transition-normal),filter var(--bb-transition-normal);color:var(--bb-text-inverse);animation:_buttonAppear_186ig_1 .3s ease}._toggleButton_186ig_1:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;border-radius:var(--bb-radius-full);background:linear-gradient(135deg,transparent 0%,rgba(0,0,0,.3) 100%);opacity:var(--bb-accent-gradient, 0);pointer-events:none}@keyframes _buttonAppear_186ig_1{0%{opacity:0;transform:scale(.8) rotate(90deg)}to{opacity:1;transform:scale(1) rotate(0)}}._toggleButton_186ig_1:hover{transform:scale(1.05);box-shadow:var(--bb-shadow-xl)}._toggleButton_186ig_1:active{transform:scale(.98)}._toggleButton_186ig_1:focus-visible{outline:2px solid var(--bb-accent-color);outline-offset:2px}._icon_186ig_54{width:28px;height:28px}._customIcon_186ig_59{display:flex;align-items:center;justify-content:center;width:28px;height:28px}._customIcon_186ig_59 svg{width:100%;height:100%}._agentLogo_186ig_72{width:36px;height:36px;border-radius:var(--bb-radius-full);object-fit:cover}._unreadBadge_186ig_79{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;padding:0 6px;border-radius:var(--bb-radius-full);background:#ef4444;color:#fff;font-size:var(--bb-font-size-xs);font-weight:var(--bb-font-weight-bold);display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px #0003}._container_kgfgt_1{display:flex;flex-direction:column;width:var(--bb-widget-width);height:var(--bb-widget-height);max-height:var(--bb-widget-max-height);background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);box-shadow:var(--bb-shadow-xl);overflow:hidden;animation:_slideUp_kgfgt_1 .3s ease}@keyframes _slideUp_kgfgt_1{0%{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}._header_kgfgt_25{display:flex;align-items:center;justify-content:space-between;padding:var(--bb-spacing-lg) var(--bb-spacing-xl);background:linear-gradient(135deg,var(--bb-primary-color) 0%,color-mix(in srgb,var(--bb-primary-color) 70%,#2d2d5a) 50%,color-mix(in srgb,var(--bb-primary-color) 50%,#1a1a2e) 100%)}._logoWrapper_kgfgt_38{width:32px;height:32px;display:flex;align-items:center;justify-content:center}._logo_kgfgt_38{width:24px;height:24px}._closeButton_kgfgt_51{width:32px;height:32px;border-radius:var(--bb-radius-full);background:#ffffff1a;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:background var(--bb-transition-fast)}._closeButton_kgfgt_51:hover{background:#fff3}._closeButton_kgfgt_51 svg{width:18px;height:18px}._content_kgfgt_74{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--bb-spacing-2xl);text-align:center}._iconWrapper_kgfgt_84{width:64px;height:64px;border-radius:var(--bb-radius-full);background:var(--bb-surface-secondary);display:flex;align-items:center;justify-content:center;margin-bottom:var(--bb-spacing-xl)}._errorIcon_kgfgt_95{width:32px;height:32px;color:var(--bb-text-tertiary)}._title_kgfgt_101{margin:0;font-size:var(--bb-font-size-xl);font-weight:var(--bb-font-weight-semibold);color:var(--bb-text-primary);margin-bottom:var(--bb-spacing-sm)}._description_kgfgt_109{margin:0;font-size:var(--bb-font-size-md);color:var(--bb-text-secondary);line-height:var(--bb-line-height);max-width:280px}._retryButton_kgfgt_117{margin-top:var(--bb-spacing-xl);padding:var(--bb-spacing-sm) var(--bb-spacing-xl);border-radius:var(--bb-radius-full);background:var(--bb-primary-color);border:none;color:var(--bb-text-inverse);font-size:var(--bb-font-size-md);font-weight:var(--bb-font-weight-medium);cursor:pointer;transition:all var(--bb-transition-fast)}._retryButton_kgfgt_117:hover{transform:scale(1.02);box-shadow:var(--bb-shadow-md)}._retryButton_kgfgt_117:active{transform:scale(.98)}._footer_kgfgt_139{padding:var(--bb-spacing-md);background:var(--bb-surface-secondary);display:flex;justify-content:center}._poweredBy_kgfgt_146{display:flex;align-items:center;gap:var(--bb-spacing-xs);font-size:var(--bb-font-size-xs);color:var(--bb-text-tertiary);text-decoration:none;transition:color var(--bb-transition-fast)}._poweredBy_kgfgt_146:hover{color:var(--bb-text-secondary)}._footerLogo_kgfgt_160{width:14px;height:14px}@media(max-width:480px){._container_kgfgt_1{width:100vw;height:100vh;max-height:100vh;border-radius:0}}._container_cgn0s_1{position:relative;display:flex;flex-direction:column;width:var(--bb-widget-width);height:var(--bb-widget-height);max-height:var(--bb-widget-max-height);background:var(--bb-surface-bg);border-radius:var(--bb-radius-xl);box-shadow:var(--bb-shadow-xl);overflow:hidden;overscroll-behavior:contain;transition:width .3s cubic-bezier(.4,0,.2,1),height .3s cubic-bezier(.4,0,.2,1),max-height .3s cubic-bezier(.4,0,.2,1)}@media(max-width:480px){._container_cgn0s_1{width:100vw;height:100vh;max-height:100vh;border-radius:0}}._header_cgn0s_29{position:relative;padding:var(--bb-spacing-lg) var(--bb-spacing-xl)}._headerBackground_cgn0s_34{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--bb-primary-color)}._headerContent_cgn0s_40{position:relative;z-index:1}._agentInfo_cgn0s_45{display:flex;align-items:center;gap:var(--bb-spacing-md)}._headerLogo_cgn0s_51{width:42px;height:42px;border-radius:var(--bb-radius-sm);object-fit:cover}._headerLogoPlaceholder_cgn0s_58{width:42px;height:42px;border-radius:var(--bb-radius-sm);background:var(--bb-accent-color-custom, var(--bb-primary-color));display:flex;align-items:center;justify-content:center}._headerBrainbaseLogo_cgn0s_68{width:28px;height:28px}._headerAgentName_cgn0s_73{color:var(--bb-header-text-color, var(--bb-text-inverse));font-size:var(--bb-agent-name-font-size, 16px);font-weight:var(--bb-font-weight-semibold)}._content_cgn0s_79{flex:1;overflow-y:auto;padding:var(--bb-spacing-xl);display:flex;flex-direction:column;gap:var(--bb-spacing-xl)}._infoCard_cgn0s_89{display:flex;flex-direction:column;align-items:stretch;padding:0;background:var(--bb-surface-secondary);border:1px solid var(--bb-border-color);border-radius:var(--bb-radius-lg);text-align:left;width:100%;cursor:default;overflow:hidden;transition:background var(--bb-transition-fast),border-color var(--bb-transition-fast),transform var(--bb-transition-fast)}._infoCard_cgn0s_89._clickable_cgn0s_104{cursor:pointer}._infoCard_cgn0s_89._clickable_cgn0s_104:hover{background:var(--bb-surface-tertiary);border-color:var(--bb-text-tertiary);transform:translateY(-2px)}._infoCard_cgn0s_89._clickable_cgn0s_104:active{transform:translateY(0)}._infoCard_cgn0s_89:disabled{cursor:default}._infoImageWrapper_cgn0s_122{width:100%;height:180px;overflow:hidden}._infoImage_cgn0s_122{width:100%;height:100%;object-fit:cover}._infoContent_cgn0s_134{display:flex;align-items:center;justify-content:space-between;padding:var(--bb-spacing-lg);gap:var(--bb-spacing-md)}._infoText_cgn0s_142{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}._infoTitle_cgn0s_150{margin:0;font-size:var(--bb-font-size-lg);font-weight:var(--bb-font-weight-semibold);color:var(--bb-text-primary);line-height:1.3}._infoDescription_cgn0s_158{margin:0;font-size:var(--bb-font-size-sm);color:var(--bb-text-secondary);line-height:1.5;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}._infoChevron_cgn0s_169{flex-shrink:0;width:24px;height:24px;color:var(--bb-text-tertiary)}._askCard_cgn0s_177{display:flex;align-items:center;justify-content:space-between;padding:var(--bb-spacing-lg);background:var(--bb-surface-secondary);border:1px solid var(--bb-border-color);border-radius:var(--bb-radius-lg);cursor:pointer;transition:background var(--bb-transition-fast),border-color var(--bb-transition-fast);text-align:left;width:100%}._askCard_cgn0s_177:hover{background:var(--bb-surface-tertiary);border-color:var(--bb-border-color)}._askContent_cgn0s_196{display:flex;flex-direction:column;gap:2px}._askTitle_cgn0s_202{font-size:var(--bb-font-size-md);font-weight:var(--bb-font-weight-semibold);color:var(--bb-text-primary)}._askSubtitle_cgn0s_208{font-size:var(--bb-font-size-sm);color:var(--bb-text-secondary)}._askIcon_cgn0s_213{display:flex;align-items:center;gap:var(--bb-spacing-sm);color:var(--bb-text-tertiary)}._agentLogo_cgn0s_220{width:24px;height:24px;border-radius:var(--bb-radius-sm);object-fit:cover}._brainbaseLogo_cgn0s_227{width:24px;height:24px}._chevron_cgn0s_232{width:20px;height:20px}._footer_cgn0s_238{display:flex;justify-content:space-around;padding:var(--bb-spacing-md) var(--bb-spacing-lg);background:var(--bb-surface-bg);border-top:1px solid var(--bb-border-color)}._navItem_cgn0s_246{display:flex;flex-direction:column;align-items:center;gap:var(--bb-spacing-xs);padding:var(--bb-spacing-sm) var(--bb-spacing-md);background:transparent;border:none;cursor:pointer;color:var(--bb-text-tertiary);font-size:var(--bb-font-size-xs);font-family:var(--bb-font-family);transition:color var(--bb-transition-fast)}._navItem_cgn0s_246:hover{color:var(--bb-text-secondary)}._navItem_cgn0s_246._active_cgn0s_265{color:var(--bb-text-primary)}._navIcon_cgn0s_269{width:24px;height:24px}._widget_1ntwj_1{font-family:var(--bb-font-family);font-size:var(--bb-font-size-md);line-height:var(--bb-line-height);color:var(--bb-text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}._bottom-right_1ntwj_11{position:fixed;bottom:20px;right:20px;z-index:9999}._bottom-left_1ntwj_18{position:fixed;bottom:20px;left:20px;z-index:9999}._inline_1ntwj_26{position:relative;width:100%;max-width:var(--bb-widget-width)}@media(max-width:480px){._bottom-right_1ntwj_11,._bottom-left_1ntwj_18{bottom:0;right:0;left:0}}._openContainer_1ntwj_43{display:flex;flex-direction:column;align-items:flex-end;gap:var(--bb-spacing-md)}._collapseButton_1ntwj_51{position:relative;width:var(--bb-toggle-size);height:var(--bb-toggle-size);border-radius:var(--bb-radius-full);background:var(--bb-accent-color-custom, var(--bb-primary-color));border:none;box-shadow:var(--bb-shadow-lg);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--bb-text-inverse);transition:transform var(--bb-transition-normal),box-shadow var(--bb-transition-normal),filter var(--bb-transition-normal);animation:_buttonAppear_1ntwj_1 .3s ease}._collapseButton_1ntwj_51:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;border-radius:var(--bb-radius-full);background:linear-gradient(135deg,transparent 0%,rgba(0,0,0,.3) 100%);opacity:var(--bb-accent-gradient, 0);pointer-events:none}@keyframes _buttonAppear_1ntwj_1{0%{opacity:0;transform:scale(.8) rotate(-90deg)}to{opacity:1;transform:scale(1) rotate(0)}}._collapseButton_1ntwj_51:hover{transform:scale(1.05);box-shadow:var(--bb-shadow-xl)}._collapseButton_1ntwj_51:active{transform:scale(.95)}._collapseButton_1ntwj_51 svg{width:28px;height:28px}