瀏覽代碼

Initial commit: Homelegance chatbot with Claude API and PostgreSQL

- Replace Manus/Forge LLM proxy with Anthropic SDK (claude-sonnet-4-6)
- Switch database driver from mysql2 to postgres (PostgreSQL)
- Convert Drizzle schema from mysql-core to pg-core with pgEnum definitions
- Add ERP bridge and SSO env vars (ERP_API_URL, DEALER_PORTAL_SSO_SECRET)
- Stub out Manus-specific notification service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tony T 3 周之前
當前提交
4081e41cb8
共有 100 個文件被更改,包括 18257 次插入0 次删除
  1. 119 0
      .gitignore
  2. 0 0
      .gitkeep
  3. 35 0
      .prettierignore
  4. 15 0
      .prettierrc
  5. 24 0
      client/index.html
  6. 0 0
      client/public/.gitkeep
  7. 821 0
      client/public/__manus__/debug-collector.js
  8. 84 0
      client/src/App.tsx
  9. 84 0
      client/src/_core/hooks/useAuth.ts
  10. 335 0
      client/src/components/AIChatBox.tsx
  11. 191 0
      client/src/components/ArchitectureDiagram.tsx
  12. 343 0
      client/src/components/ChatbotWidget.tsx
  13. 315 0
      client/src/components/ChatbotWidgetLive.tsx
  14. 74 0
      client/src/components/CodeSnippet.tsx
  15. 378 0
      client/src/components/DashboardLayout.tsx
  16. 46 0
      client/src/components/DashboardLayoutSkeleton.tsx
  17. 62 0
      client/src/components/ErrorBoundary.tsx
  18. 89 0
      client/src/components/ManusDialog.tsx
  19. 155 0
      client/src/components/Map.tsx
  20. 254 0
      client/src/components/WorkflowDiagram.tsx
  21. 64 0
      client/src/components/ui/accordion.tsx
  22. 155 0
      client/src/components/ui/alert-dialog.tsx
  23. 66 0
      client/src/components/ui/alert.tsx
  24. 9 0
      client/src/components/ui/aspect-ratio.tsx
  25. 51 0
      client/src/components/ui/avatar.tsx
  26. 46 0
      client/src/components/ui/badge.tsx
  27. 109 0
      client/src/components/ui/breadcrumb.tsx
  28. 83 0
      client/src/components/ui/button-group.tsx
  29. 60 0
      client/src/components/ui/button.tsx
  30. 211 0
      client/src/components/ui/calendar.tsx
  31. 92 0
      client/src/components/ui/card.tsx
  32. 239 0
      client/src/components/ui/carousel.tsx
  33. 355 0
      client/src/components/ui/chart.tsx
  34. 30 0
      client/src/components/ui/checkbox.tsx
  35. 31 0
      client/src/components/ui/collapsible.tsx
  36. 184 0
      client/src/components/ui/command.tsx
  37. 250 0
      client/src/components/ui/context-menu.tsx
  38. 209 0
      client/src/components/ui/dialog.tsx
  39. 133 0
      client/src/components/ui/drawer.tsx
  40. 255 0
      client/src/components/ui/dropdown-menu.tsx
  41. 104 0
      client/src/components/ui/empty.tsx
  42. 242 0
      client/src/components/ui/field.tsx
  43. 168 0
      client/src/components/ui/form.tsx
  44. 42 0
      client/src/components/ui/hover-card.tsx
  45. 168 0
      client/src/components/ui/input-group.tsx
  46. 75 0
      client/src/components/ui/input-otp.tsx
  47. 70 0
      client/src/components/ui/input.tsx
  48. 193 0
      client/src/components/ui/item.tsx
  49. 28 0
      client/src/components/ui/kbd.tsx
  50. 22 0
      client/src/components/ui/label.tsx
  51. 274 0
      client/src/components/ui/menubar.tsx
  52. 168 0
      client/src/components/ui/navigation-menu.tsx
  53. 127 0
      client/src/components/ui/pagination.tsx
  54. 46 0
      client/src/components/ui/popover.tsx
  55. 29 0
      client/src/components/ui/progress.tsx
  56. 43 0
      client/src/components/ui/radio-group.tsx
  57. 54 0
      client/src/components/ui/resizable.tsx
  58. 56 0
      client/src/components/ui/scroll-area.tsx
  59. 185 0
      client/src/components/ui/select.tsx
  60. 26 0
      client/src/components/ui/separator.tsx
  61. 139 0
      client/src/components/ui/sheet.tsx
  62. 734 0
      client/src/components/ui/sidebar.tsx
  63. 13 0
      client/src/components/ui/skeleton.tsx
  64. 61 0
      client/src/components/ui/slider.tsx
  65. 23 0
      client/src/components/ui/sonner.tsx
  66. 16 0
      client/src/components/ui/spinner.tsx
  67. 29 0
      client/src/components/ui/switch.tsx
  68. 114 0
      client/src/components/ui/table.tsx
  69. 64 0
      client/src/components/ui/tabs.tsx
  70. 67 0
      client/src/components/ui/textarea.tsx
  71. 73 0
      client/src/components/ui/toggle-group.tsx
  72. 45 0
      client/src/components/ui/toggle.tsx
  73. 59 0
      client/src/components/ui/tooltip.tsx
  74. 21 0
      client/src/const.ts
  75. 64 0
      client/src/contexts/ThemeContext.tsx
  76. 81 0
      client/src/hooks/useComposition.ts
  77. 21 0
      client/src/hooks/useMobile.tsx
  78. 20 0
      client/src/hooks/usePersistFn.ts
  79. 150 0
      client/src/index.css
  80. 4 0
      client/src/lib/trpc.ts
  81. 6 0
      client/src/lib/utils.ts
  82. 63 0
      client/src/main.tsx
  83. 213 0
      client/src/pages/AcceptInvite.tsx
  84. 981 0
      client/src/pages/AgentDashboard.tsx
  85. 305 0
      client/src/pages/Analytics.tsx
  86. 1437 0
      client/src/pages/ComponentShowcase.tsx
  87. 652 0
      client/src/pages/DataSources.tsx
  88. 274 0
      client/src/pages/ForgotPassword.tsx
  89. 854 0
      client/src/pages/Home.tsx
  90. 298 0
      client/src/pages/Login.tsx
  91. 52 0
      client/src/pages/NotFound.tsx
  92. 445 0
      client/src/pages/Playground.tsx
  93. 323 0
      client/src/pages/Register.tsx
  94. 362 0
      client/src/pages/ResetPassword.tsx
  95. 978 0
      client/src/pages/UserManagement.tsx
  96. 945 0
      client/src/pages/WorkflowDesigner.tsx
  97. 19 0
      components.json
  98. 15 0
      drizzle.config.ts
  99. 13 0
      drizzle/0000_stormy_crystal.sql
  100. 48 0
      drizzle/0001_huge_sway.sql

+ 119 - 0
.gitignore

@@ -0,0 +1,119 @@
+# Dependencies
+**/node_modules
+.pnpm-store/
+
+# Build outputs
+dist/
+build/
+*.dist
+
+# Environment variables
+.env
+.env.*
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+.env.production
+
+# IDE and editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+*.bak
+
+# Coverage directory used by tools like istanbul
+coverage/
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Dependency directories
+jspm_packages/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+
+# Gatsby files
+.cache/
+
+# Storybook build outputs
+.out
+.storybook-out
+
+# Temporary folders
+tmp/
+temp/
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# Webdev artifacts (checkpoint zips, migrations, etc.)
+.webdev/
+
+# Local tool state
+.omc/
+.claude/
+
+# Manus version file (auto-generated, not part of source)
+client/public/__manus__/version.json

+ 0 - 0
.gitkeep


+ 35 - 0
.prettierignore

@@ -0,0 +1,35 @@
+# Dependencies
+node_modules/
+.pnpm-store/
+
+# Build outputs
+dist/
+build/
+*.dist
+
+# Generated files
+*.tsbuildinfo
+coverage/
+
+# Package files
+package-lock.json
+pnpm-lock.yaml
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# Logs
+*.log
+
+# Environment files
+.env*
+
+# IDE files
+.vscode/
+.idea/
+
+# OS files
+.DS_Store
+Thumbs.db

+ 15 - 0
.prettierrc

@@ -0,0 +1,15 @@
+{
+  "semi": true,
+  "trailingComma": "es5",
+  "singleQuote": false,
+  "printWidth": 80,
+  "tabWidth": 2,
+  "useTabs": false,
+  "bracketSpacing": true,
+  "bracketSameLine": false,
+  "arrowParens": "avoid",
+  "endOfLine": "lf",
+  "quoteProps": "as-needed",
+  "jsxSingleQuote": false,
+  "proseWrap": "preserve"
+}

+ 24 - 0
client/index.html

@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en">
+
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1" />
+    <title>Homelegance Chatbot Integration Demo</title>    
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Source+Sans+3:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet" />
+  </head>
+
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+    <script
+      defer
+      src="%VITE_ANALYTICS_ENDPOINT%/umami"
+      data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
+  </body>
+
+</html>

+ 0 - 0
client/public/.gitkeep


+ 821 - 0
client/public/__manus__/debug-collector.js

@@ -0,0 +1,821 @@
+/**
+ * Manus Debug Collector (agent-friendly)
+ *
+ * Captures:
+ * 1) Console logs
+ * 2) Network requests (fetch + XHR)
+ * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.)
+ *
+ * Data is periodically sent to /__manus__/logs
+ * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log
+ */
+(function () {
+  "use strict";
+
+  // Prevent double initialization
+  if (window.__MANUS_DEBUG_COLLECTOR__) return;
+
+  // ==========================================================================
+  // Configuration
+  // ==========================================================================
+  const CONFIG = {
+    reportEndpoint: "/__manus__/logs",
+    bufferSize: {
+      console: 500,
+      network: 200,
+      // semantic, agent-friendly UI events
+      ui: 500,
+    },
+    reportInterval: 2000,
+    sensitiveFields: [
+      "password",
+      "token",
+      "secret",
+      "key",
+      "authorization",
+      "cookie",
+      "session",
+    ],
+    maxBodyLength: 10240,
+    // UI event logging privacy policy:
+    // - inputs matching sensitiveFields or type=password are masked by default
+    // - non-sensitive inputs log up to 200 chars
+    uiInputMaxLen: 200,
+    uiTextMaxLen: 80,
+    // Scroll throttling: minimum ms between scroll events
+    scrollThrottleMs: 500,
+  };
+
+  // ==========================================================================
+  // Storage
+  // ==========================================================================
+  const store = {
+    consoleLogs: [],
+    networkRequests: [],
+    uiEvents: [],
+    lastReportTime: Date.now(),
+    lastScrollTime: 0,
+  };
+
+  // ==========================================================================
+  // Utility Functions
+  // ==========================================================================
+
+  function sanitizeValue(value, depth) {
+    if (depth === void 0) depth = 0;
+    if (depth > 5) return "[Max Depth]";
+    if (value === null) return null;
+    if (value === undefined) return undefined;
+
+    if (typeof value === "string") {
+      return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value;
+    }
+
+    if (typeof value !== "object") return value;
+
+    if (Array.isArray(value)) {
+      return value.slice(0, 100).map(function (v) {
+        return sanitizeValue(v, depth + 1);
+      });
+    }
+
+    var sanitized = {};
+    for (var k in value) {
+      if (Object.prototype.hasOwnProperty.call(value, k)) {
+        var isSensitive = CONFIG.sensitiveFields.some(function (f) {
+          return k.toLowerCase().indexOf(f) !== -1;
+        });
+        if (isSensitive) {
+          sanitized[k] = "[REDACTED]";
+        } else {
+          sanitized[k] = sanitizeValue(value[k], depth + 1);
+        }
+      }
+    }
+    return sanitized;
+  }
+
+  function formatArg(arg) {
+    try {
+      if (arg instanceof Error) {
+        return { type: "Error", message: arg.message, stack: arg.stack };
+      }
+      if (typeof arg === "object") return sanitizeValue(arg);
+      return String(arg);
+    } catch (e) {
+      return "[Unserializable]";
+    }
+  }
+
+  function formatArgs(args) {
+    var result = [];
+    for (var i = 0; i < args.length; i++) result.push(formatArg(args[i]));
+    return result;
+  }
+
+  function pruneBuffer(buffer, maxSize) {
+    if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize);
+  }
+
+  function tryParseJson(str) {
+    if (typeof str !== "string") return str;
+    try {
+      return JSON.parse(str);
+    } catch (e) {
+      return str;
+    }
+  }
+
+  // ==========================================================================
+  // Semantic UI Event Logging (agent-friendly)
+  // ==========================================================================
+
+  function shouldIgnoreTarget(target) {
+    try {
+      if (!target || !(target instanceof Element)) return false;
+      return !!target.closest(".manus-no-record");
+    } catch (e) {
+      return false;
+    }
+  }
+
+  function compactText(s, maxLen) {
+    try {
+      var t = (s || "").trim().replace(/\s+/g, " ");
+      if (!t) return "";
+      return t.length > maxLen ? t.slice(0, maxLen) + "…" : t;
+    } catch (e) {
+      return "";
+    }
+  }
+
+  function elText(el) {
+    try {
+      var t = el.innerText || el.textContent || "";
+      return compactText(t, CONFIG.uiTextMaxLen);
+    } catch (e) {
+      return "";
+    }
+  }
+
+  function describeElement(el) {
+    if (!el || !(el instanceof Element)) return null;
+
+    var getAttr = function (name) {
+      return el.getAttribute(name);
+    };
+
+    var tag = el.tagName ? el.tagName.toLowerCase() : null;
+    var id = el.id || null;
+    var name = getAttr("name") || null;
+    var role = getAttr("role") || null;
+    var ariaLabel = getAttr("aria-label") || null;
+
+    var dataLoc = getAttr("data-loc") || null;
+    var testId =
+      getAttr("data-testid") ||
+      getAttr("data-test-id") ||
+      getAttr("data-test") ||
+      null;
+
+    var type = tag === "input" ? (getAttr("type") || "text") : null;
+    var href = tag === "a" ? getAttr("href") || null : null;
+
+    // a small, stable hint for agents (avoid building full CSS paths)
+    var selectorHint = null;
+    if (testId) selectorHint = '[data-testid="' + testId + '"]';
+    else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]';
+    else if (id) selectorHint = "#" + id;
+    else selectorHint = tag || "unknown";
+
+    return {
+      tag: tag,
+      id: id,
+      name: name,
+      type: type,
+      role: role,
+      ariaLabel: ariaLabel,
+      testId: testId,
+      dataLoc: dataLoc,
+      href: href,
+      text: elText(el),
+      selectorHint: selectorHint,
+    };
+  }
+
+  function isSensitiveField(el) {
+    if (!el || !(el instanceof Element)) return false;
+    var tag = el.tagName ? el.tagName.toLowerCase() : "";
+    if (tag !== "input" && tag !== "textarea") return false;
+
+    var type = (el.getAttribute("type") || "").toLowerCase();
+    if (type === "password") return true;
+
+    var name = (el.getAttribute("name") || "").toLowerCase();
+    var id = (el.id || "").toLowerCase();
+
+    return CONFIG.sensitiveFields.some(function (f) {
+      return name.indexOf(f) !== -1 || id.indexOf(f) !== -1;
+    });
+  }
+
+  function getInputValueSafe(el) {
+    if (!el || !(el instanceof Element)) return null;
+    var tag = el.tagName ? el.tagName.toLowerCase() : "";
+    if (tag !== "input" && tag !== "textarea" && tag !== "select") return null;
+
+    var v = "";
+    try {
+      v = el.value != null ? String(el.value) : "";
+    } catch (e) {
+      v = "";
+    }
+
+    if (isSensitiveField(el)) return { masked: true, length: v.length };
+
+    if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
+    return v;
+  }
+
+  function logUiEvent(kind, payload) {
+    var entry = {
+      timestamp: Date.now(),
+      kind: kind,
+      url: location.href,
+      viewport: { width: window.innerWidth, height: window.innerHeight },
+      payload: sanitizeValue(payload),
+    };
+    store.uiEvents.push(entry);
+    pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
+  }
+
+  function installUiEventListeners() {
+    // Clicks
+    document.addEventListener(
+      "click",
+      function (e) {
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("click", {
+          target: describeElement(t),
+          x: e.clientX,
+          y: e.clientY,
+        });
+      },
+      true
+    );
+
+    // Typing "commit" events
+    document.addEventListener(
+      "change",
+      function (e) {
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("change", {
+          target: describeElement(t),
+          value: getInputValueSafe(t),
+        });
+      },
+      true
+    );
+
+    document.addEventListener(
+      "focusin",
+      function (e) {
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("focusin", { target: describeElement(t) });
+      },
+      true
+    );
+
+    document.addEventListener(
+      "focusout",
+      function (e) {
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("focusout", {
+          target: describeElement(t),
+          value: getInputValueSafe(t),
+        });
+      },
+      true
+    );
+
+    // Enter/Escape are useful for form flows & modals
+    document.addEventListener(
+      "keydown",
+      function (e) {
+        if (e.key !== "Enter" && e.key !== "Escape") return;
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("keydown", { key: e.key, target: describeElement(t) });
+      },
+      true
+    );
+
+    // Form submissions
+    document.addEventListener(
+      "submit",
+      function (e) {
+        var t = e.target;
+        if (shouldIgnoreTarget(t)) return;
+        logUiEvent("submit", { target: describeElement(t) });
+      },
+      true
+    );
+
+    // Throttled scroll events
+    window.addEventListener(
+      "scroll",
+      function () {
+        var now = Date.now();
+        if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return;
+        store.lastScrollTime = now;
+
+        logUiEvent("scroll", {
+          scrollX: window.scrollX,
+          scrollY: window.scrollY,
+          documentHeight: document.documentElement.scrollHeight,
+          viewportHeight: window.innerHeight,
+        });
+      },
+      { passive: true }
+    );
+
+    // Navigation tracking for SPAs
+    function nav(reason) {
+      logUiEvent("navigate", { reason: reason });
+    }
+
+    var origPush = history.pushState;
+    history.pushState = function () {
+      origPush.apply(this, arguments);
+      nav("pushState");
+    };
+
+    var origReplace = history.replaceState;
+    history.replaceState = function () {
+      origReplace.apply(this, arguments);
+      nav("replaceState");
+    };
+
+    window.addEventListener("popstate", function () {
+      nav("popstate");
+    });
+    window.addEventListener("hashchange", function () {
+      nav("hashchange");
+    });
+  }
+
+  // ==========================================================================
+  // Console Interception
+  // ==========================================================================
+
+  var originalConsole = {
+    log: console.log.bind(console),
+    debug: console.debug.bind(console),
+    info: console.info.bind(console),
+    warn: console.warn.bind(console),
+    error: console.error.bind(console),
+  };
+
+  ["log", "debug", "info", "warn", "error"].forEach(function (method) {
+    console[method] = function () {
+      var args = Array.prototype.slice.call(arguments);
+
+      var entry = {
+        timestamp: Date.now(),
+        level: method.toUpperCase(),
+        args: formatArgs(args),
+        stack: method === "error" ? new Error().stack : null,
+      };
+
+      store.consoleLogs.push(entry);
+      pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
+
+      originalConsole[method].apply(console, args);
+    };
+  });
+
+  window.addEventListener("error", function (event) {
+    store.consoleLogs.push({
+      timestamp: Date.now(),
+      level: "ERROR",
+      args: [
+        {
+          type: "UncaughtError",
+          message: event.message,
+          filename: event.filename,
+          lineno: event.lineno,
+          colno: event.colno,
+          stack: event.error ? event.error.stack : null,
+        },
+      ],
+      stack: event.error ? event.error.stack : null,
+    });
+    pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
+
+    // Mark an error moment in UI event stream for agents
+    logUiEvent("error", {
+      message: event.message,
+      filename: event.filename,
+      lineno: event.lineno,
+      colno: event.colno,
+    });
+  });
+
+  window.addEventListener("unhandledrejection", function (event) {
+    var reason = event.reason;
+    store.consoleLogs.push({
+      timestamp: Date.now(),
+      level: "ERROR",
+      args: [
+        {
+          type: "UnhandledRejection",
+          reason: reason && reason.message ? reason.message : String(reason),
+          stack: reason && reason.stack ? reason.stack : null,
+        },
+      ],
+      stack: reason && reason.stack ? reason.stack : null,
+    });
+    pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
+
+    logUiEvent("unhandledrejection", {
+      reason: reason && reason.message ? reason.message : String(reason),
+    });
+  });
+
+  // ==========================================================================
+  // Fetch Interception
+  // ==========================================================================
+
+  var originalFetch = window.fetch.bind(window);
+
+  window.fetch = function (input, init) {
+    init = init || {};
+    var startTime = Date.now();
+    // Handle string, Request object, or URL object
+    var url = typeof input === "string"
+      ? input
+      : (input && (input.url || input.href || String(input))) || "";
+    var method = init.method || (input && input.method) || "GET";
+
+    // Don't intercept internal requests
+    if (url.indexOf("/__manus__/") === 0) {
+      return originalFetch(input, init);
+    }
+
+    // Safely parse headers (avoid breaking if headers format is invalid)
+    var requestHeaders = {};
+    try {
+      if (init.headers) {
+        requestHeaders = Object.fromEntries(new Headers(init.headers).entries());
+      }
+    } catch (e) {
+      requestHeaders = { _parseError: true };
+    }
+
+    var entry = {
+      timestamp: startTime,
+      type: "fetch",
+      method: method.toUpperCase(),
+      url: url,
+      request: {
+        headers: requestHeaders,
+        body: init.body ? sanitizeValue(tryParseJson(init.body)) : null,
+      },
+      response: null,
+      duration: null,
+      error: null,
+    };
+
+    return originalFetch(input, init)
+      .then(function (response) {
+        entry.duration = Date.now() - startTime;
+
+        var contentType = (response.headers.get("content-type") || "").toLowerCase();
+        var contentLength = response.headers.get("content-length");
+
+        entry.response = {
+          status: response.status,
+          statusText: response.statusText,
+          headers: Object.fromEntries(response.headers.entries()),
+          body: null,
+        };
+
+        // Semantic network hint for agents on failures (sync, no need to wait for body)
+        if (response.status >= 400) {
+          logUiEvent("network_error", {
+            kind: "fetch",
+            method: entry.method,
+            url: entry.url,
+            status: response.status,
+            statusText: response.statusText,
+          });
+        }
+
+        // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks
+        var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
+                          contentType.indexOf("application/stream") !== -1 ||
+                          contentType.indexOf("application/x-ndjson") !== -1;
+        if (isStreaming) {
+          entry.response.body = "[Streaming response - not captured]";
+          store.networkRequests.push(entry);
+          pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+          return response;
+        }
+
+        // Skip body capture for large responses to avoid memory issues
+        if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) {
+          entry.response.body = "[Response too large: " + contentLength + " bytes]";
+          store.networkRequests.push(entry);
+          pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+          return response;
+        }
+
+        // Skip body capture for binary content types
+        var isBinary = contentType.indexOf("image/") !== -1 ||
+                       contentType.indexOf("video/") !== -1 ||
+                       contentType.indexOf("audio/") !== -1 ||
+                       contentType.indexOf("application/octet-stream") !== -1 ||
+                       contentType.indexOf("application/pdf") !== -1 ||
+                       contentType.indexOf("application/zip") !== -1;
+        if (isBinary) {
+          entry.response.body = "[Binary content: " + contentType + "]";
+          store.networkRequests.push(entry);
+          pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+          return response;
+        }
+
+        // For text responses, clone and read body in background
+        var clonedResponse = response.clone();
+
+        // Async: read body in background, don't block the response
+        clonedResponse
+          .text()
+          .then(function (text) {
+            if (text.length <= CONFIG.maxBodyLength) {
+              entry.response.body = sanitizeValue(tryParseJson(text));
+            } else {
+              entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
+            }
+          })
+          .catch(function () {
+            entry.response.body = "[Unable to read body]";
+          })
+          .finally(function () {
+            store.networkRequests.push(entry);
+            pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+          });
+
+        // Return response immediately, don't wait for body reading
+        return response;
+      })
+      .catch(function (error) {
+        entry.duration = Date.now() - startTime;
+        entry.error = { message: error.message, stack: error.stack };
+
+        store.networkRequests.push(entry);
+        pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+
+        logUiEvent("network_error", {
+          kind: "fetch",
+          method: entry.method,
+          url: entry.url,
+          message: error.message,
+        });
+
+        throw error;
+      });
+  };
+
+  // ==========================================================================
+  // XHR Interception
+  // ==========================================================================
+
+  var originalXHROpen = XMLHttpRequest.prototype.open;
+  var originalXHRSend = XMLHttpRequest.prototype.send;
+
+  XMLHttpRequest.prototype.open = function (method, url) {
+    this._manusData = {
+      method: (method || "GET").toUpperCase(),
+      url: url,
+      startTime: null,
+    };
+    return originalXHROpen.apply(this, arguments);
+  };
+
+  XMLHttpRequest.prototype.send = function (body) {
+    var xhr = this;
+
+    if (
+      xhr._manusData &&
+      xhr._manusData.url &&
+      xhr._manusData.url.indexOf("/__manus__/") !== 0
+    ) {
+      xhr._manusData.startTime = Date.now();
+      xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null;
+
+      xhr.addEventListener("load", function () {
+        var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase();
+        var responseBody = null;
+
+        // Skip body capture for streaming responses
+        var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
+                          contentType.indexOf("application/stream") !== -1 ||
+                          contentType.indexOf("application/x-ndjson") !== -1;
+
+        // Skip body capture for binary content types
+        var isBinary = contentType.indexOf("image/") !== -1 ||
+                       contentType.indexOf("video/") !== -1 ||
+                       contentType.indexOf("audio/") !== -1 ||
+                       contentType.indexOf("application/octet-stream") !== -1 ||
+                       contentType.indexOf("application/pdf") !== -1 ||
+                       contentType.indexOf("application/zip") !== -1;
+
+        if (isStreaming) {
+          responseBody = "[Streaming response - not captured]";
+        } else if (isBinary) {
+          responseBody = "[Binary content: " + contentType + "]";
+        } else {
+          // Safe to read responseText for text responses
+          try {
+            var text = xhr.responseText || "";
+            if (text.length > CONFIG.maxBodyLength) {
+              responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
+            } else {
+              responseBody = sanitizeValue(tryParseJson(text));
+            }
+          } catch (e) {
+            // responseText may throw for non-text responses
+            responseBody = "[Unable to read response: " + e.message + "]";
+          }
+        }
+
+        var entry = {
+          timestamp: xhr._manusData.startTime,
+          type: "xhr",
+          method: xhr._manusData.method,
+          url: xhr._manusData.url,
+          request: { body: xhr._manusData.requestBody },
+          response: {
+            status: xhr.status,
+            statusText: xhr.statusText,
+            body: responseBody,
+          },
+          duration: Date.now() - xhr._manusData.startTime,
+          error: null,
+        };
+
+        store.networkRequests.push(entry);
+        pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+
+        if (entry.response && entry.response.status >= 400) {
+          logUiEvent("network_error", {
+            kind: "xhr",
+            method: entry.method,
+            url: entry.url,
+            status: entry.response.status,
+            statusText: entry.response.statusText,
+          });
+        }
+      });
+
+      xhr.addEventListener("error", function () {
+        var entry = {
+          timestamp: xhr._manusData.startTime,
+          type: "xhr",
+          method: xhr._manusData.method,
+          url: xhr._manusData.url,
+          request: { body: xhr._manusData.requestBody },
+          response: null,
+          duration: Date.now() - xhr._manusData.startTime,
+          error: { message: "Network error" },
+        };
+
+        store.networkRequests.push(entry);
+        pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+
+        logUiEvent("network_error", {
+          kind: "xhr",
+          method: entry.method,
+          url: entry.url,
+          message: "Network error",
+        });
+      });
+    }
+
+    return originalXHRSend.apply(this, arguments);
+  };
+
+  // ==========================================================================
+  // Data Reporting
+  // ==========================================================================
+
+  function reportLogs() {
+    var consoleLogs = store.consoleLogs.splice(0);
+    var networkRequests = store.networkRequests.splice(0);
+    var uiEvents = store.uiEvents.splice(0);
+
+    // Skip if no new data
+    if (
+      consoleLogs.length === 0 &&
+      networkRequests.length === 0 &&
+      uiEvents.length === 0
+    ) {
+      return Promise.resolve();
+    }
+
+    var payload = {
+      timestamp: Date.now(),
+      consoleLogs: consoleLogs,
+      networkRequests: networkRequests,
+      // Mirror uiEvents to sessionEvents for sessionReplay.log
+      sessionEvents: uiEvents,
+      // agent-friendly semantic events
+      uiEvents: uiEvents,
+    };
+
+    return originalFetch(CONFIG.reportEndpoint, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(payload),
+    }).catch(function () {
+      // Put data back on failure (but respect limits)
+      store.consoleLogs = consoleLogs.concat(store.consoleLogs);
+      store.networkRequests = networkRequests.concat(store.networkRequests);
+      store.uiEvents = uiEvents.concat(store.uiEvents);
+
+      pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
+      pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
+      pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
+    });
+  }
+
+  // Periodic reporting
+  setInterval(reportLogs, CONFIG.reportInterval);
+
+  // Report on page unload
+  window.addEventListener("beforeunload", function () {
+    var consoleLogs = store.consoleLogs;
+    var networkRequests = store.networkRequests;
+    var uiEvents = store.uiEvents;
+
+    if (
+      consoleLogs.length === 0 &&
+      networkRequests.length === 0 &&
+      uiEvents.length === 0
+    ) {
+      return;
+    }
+
+    var payload = {
+      timestamp: Date.now(),
+      consoleLogs: consoleLogs,
+      networkRequests: networkRequests,
+      // Mirror uiEvents to sessionEvents for sessionReplay.log
+      sessionEvents: uiEvents,
+      uiEvents: uiEvents,
+    };
+
+    if (navigator.sendBeacon) {
+      var payloadStr = JSON.stringify(payload);
+      // sendBeacon has ~64KB limit, truncate if too large
+      var MAX_BEACON_SIZE = 60000; // Leave some margin
+      if (payloadStr.length > MAX_BEACON_SIZE) {
+        // Prioritize: keep recent events, drop older logs
+        var truncatedPayload = {
+          timestamp: Date.now(),
+          consoleLogs: consoleLogs.slice(-50),
+          networkRequests: networkRequests.slice(-20),
+          sessionEvents: uiEvents.slice(-100),
+          uiEvents: uiEvents.slice(-100),
+          _truncated: true,
+        };
+        payloadStr = JSON.stringify(truncatedPayload);
+      }
+      navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr);
+    }
+  });
+
+  // ==========================================================================
+  // Initialization
+  // ==========================================================================
+
+  // Install semantic UI listeners ASAP
+  try {
+    installUiEventListeners();
+  } catch (e) {
+    console.warn("[Manus] Failed to install UI listeners:", e);
+  }
+
+  // Mark as initialized
+  window.__MANUS_DEBUG_COLLECTOR__ = {
+    version: "2.0-no-rrweb",
+    store: store,
+    forceReport: reportLogs,
+  };
+
+  console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)");
+})();

+ 84 - 0
client/src/App.tsx

@@ -0,0 +1,84 @@
+import { Toaster } from "@/components/ui/sonner";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import NotFound from "@/pages/NotFound";
+import { Route, Switch } from "wouter";
+import ErrorBoundary from "./components/ErrorBoundary";
+import { ThemeProvider } from "./contexts/ThemeContext";
+import DashboardLayout from "./components/DashboardLayout";
+import Home from "./pages/Home";
+import Login from "./pages/Login";
+import Register from "./pages/Register";
+import ForgotPassword from "./pages/ForgotPassword";
+import ResetPassword from "./pages/ResetPassword";
+import AgentDashboard from "./pages/AgentDashboard";
+import WorkflowDesigner from "./pages/WorkflowDesigner";
+import UserManagement from "./pages/UserManagement";
+import AcceptInvite from "./pages/AcceptInvite";
+import Playground from "./pages/Playground";
+import Analytics from "./pages/Analytics";
+import DataSources from "./pages/DataSources";
+
+function Router() {
+  return (
+    <Switch>
+      {/* Public pages */}
+      <Route path={"/"} component={Home} />
+      <Route path={"/login"} component={Login} />
+      <Route path={"/register"} component={Register} />
+      <Route path={"/forgot-password"} component={ForgotPassword} />
+      <Route path={"/reset-password/:token"} component={ResetPassword} />
+      <Route path="/invite/:token" component={AcceptInvite} />
+
+      {/* Protected pages — DashboardLayout handles auth gating */}
+      <Route path={"/dashboard"}>
+        <DashboardLayout requiredRole="agent">
+          <AgentDashboard />
+        </DashboardLayout>
+      </Route>
+      <Route path={"/dashboard/users"}>
+        <DashboardLayout requiredRole="admin">
+          <UserManagement />
+        </DashboardLayout>
+      </Route>
+      <Route path={"/workflow-designer"}>
+        <DashboardLayout requiredRole="admin">
+          <WorkflowDesigner />
+        </DashboardLayout>
+      </Route>
+      <Route path={"/playground"}>
+        <DashboardLayout requiredRole="agent">
+          <Playground />
+        </DashboardLayout>
+      </Route>
+      <Route path={"/analytics"}>
+        <DashboardLayout requiredRole="admin">
+          <Analytics />
+        </DashboardLayout>
+      </Route>
+      <Route path={"/data-sources"}>
+        <DashboardLayout requiredRole="admin">
+          <DataSources />
+        </DashboardLayout>
+      </Route>
+
+      <Route path={"/404"} component={NotFound} />
+      {/* Final fallback route */}
+      <Route component={NotFound} />
+    </Switch>
+  );
+}
+
+function App() {
+  return (
+    <ErrorBoundary>
+      <ThemeProvider defaultTheme="light">
+        <TooltipProvider>
+          <Toaster />
+          <Router />
+        </TooltipProvider>
+      </ThemeProvider>
+    </ErrorBoundary>
+  );
+}
+
+export default App;

+ 84 - 0
client/src/_core/hooks/useAuth.ts

@@ -0,0 +1,84 @@
+import { getLoginUrl } from "@/const";
+import { trpc } from "@/lib/trpc";
+import { TRPCClientError } from "@trpc/client";
+import { useCallback, useEffect, useMemo } from "react";
+
+type UseAuthOptions = {
+  redirectOnUnauthenticated?: boolean;
+  redirectPath?: string;
+};
+
+export function useAuth(options?: UseAuthOptions) {
+  const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } =
+    options ?? {};
+  const utils = trpc.useUtils();
+
+  const meQuery = trpc.auth.me.useQuery(undefined, {
+    retry: false,
+    refetchOnWindowFocus: false,
+  });
+
+  const logoutMutation = trpc.auth.logout.useMutation({
+    onSuccess: () => {
+      utils.auth.me.setData(undefined, null);
+    },
+  });
+
+  const logout = useCallback(async () => {
+    try {
+      await logoutMutation.mutateAsync();
+    } catch (error: unknown) {
+      if (
+        error instanceof TRPCClientError &&
+        error.data?.code === "UNAUTHORIZED"
+      ) {
+        return;
+      }
+      throw error;
+    } finally {
+      utils.auth.me.setData(undefined, null);
+      await utils.auth.me.invalidate();
+    }
+  }, [logoutMutation, utils]);
+
+  const state = useMemo(() => {
+    localStorage.setItem(
+      "manus-runtime-user-info",
+      JSON.stringify(meQuery.data)
+    );
+    return {
+      user: meQuery.data ?? null,
+      loading: meQuery.isLoading || logoutMutation.isPending,
+      error: meQuery.error ?? logoutMutation.error ?? null,
+      isAuthenticated: Boolean(meQuery.data),
+    };
+  }, [
+    meQuery.data,
+    meQuery.error,
+    meQuery.isLoading,
+    logoutMutation.error,
+    logoutMutation.isPending,
+  ]);
+
+  useEffect(() => {
+    if (!redirectOnUnauthenticated) return;
+    if (meQuery.isLoading || logoutMutation.isPending) return;
+    if (state.user) return;
+    if (typeof window === "undefined") return;
+    if (window.location.pathname === redirectPath) return;
+
+    window.location.href = redirectPath
+  }, [
+    redirectOnUnauthenticated,
+    redirectPath,
+    logoutMutation.isPending,
+    meQuery.isLoading,
+    state.user,
+  ]);
+
+  return {
+    ...state,
+    refresh: () => meQuery.refetch(),
+    logout,
+  };
+}

+ 335 - 0
client/src/components/AIChatBox.tsx

@@ -0,0 +1,335 @@
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { cn } from "@/lib/utils";
+import { Loader2, Send, User, Sparkles } from "lucide-react";
+import { useState, useEffect, useRef } from "react";
+import { Streamdown } from "streamdown";
+
+/**
+ * Message type matching server-side LLM Message interface
+ */
+export type Message = {
+  role: "system" | "user" | "assistant";
+  content: string;
+};
+
+export type AIChatBoxProps = {
+  /**
+   * Messages array to display in the chat.
+   * Should match the format used by invokeLLM on the server.
+   */
+  messages: Message[];
+
+  /**
+   * Callback when user sends a message.
+   * Typically you'll call a tRPC mutation here to invoke the LLM.
+   */
+  onSendMessage: (content: string) => void;
+
+  /**
+   * Whether the AI is currently generating a response
+   */
+  isLoading?: boolean;
+
+  /**
+   * Placeholder text for the input field
+   */
+  placeholder?: string;
+
+  /**
+   * Custom className for the container
+   */
+  className?: string;
+
+  /**
+   * Height of the chat box (default: 600px)
+   */
+  height?: string | number;
+
+  /**
+   * Empty state message to display when no messages
+   */
+  emptyStateMessage?: string;
+
+  /**
+   * Suggested prompts to display in empty state
+   * Click to send directly
+   */
+  suggestedPrompts?: string[];
+};
+
+/**
+ * A ready-to-use AI chat box component that integrates with the LLM system.
+ *
+ * Features:
+ * - Matches server-side Message interface for seamless integration
+ * - Markdown rendering with Streamdown
+ * - Auto-scrolls to latest message
+ * - Loading states
+ * - Uses global theme colors from index.css
+ *
+ * @example
+ * ```tsx
+ * const ChatPage = () => {
+ *   const [messages, setMessages] = useState<Message[]>([
+ *     { role: "system", content: "You are a helpful assistant." }
+ *   ]);
+ *
+ *   const chatMutation = trpc.ai.chat.useMutation({
+ *     onSuccess: (response) => {
+ *       // Assuming your tRPC endpoint returns the AI response as a string
+ *       setMessages(prev => [...prev, {
+ *         role: "assistant",
+ *         content: response
+ *       }]);
+ *     },
+ *     onError: (error) => {
+ *       console.error("Chat error:", error);
+ *       // Optionally show error message to user
+ *     }
+ *   });
+ *
+ *   const handleSend = (content: string) => {
+ *     const newMessages = [...messages, { role: "user", content }];
+ *     setMessages(newMessages);
+ *     chatMutation.mutate({ messages: newMessages });
+ *   };
+ *
+ *   return (
+ *     <AIChatBox
+ *       messages={messages}
+ *       onSendMessage={handleSend}
+ *       isLoading={chatMutation.isPending}
+ *       suggestedPrompts={[
+ *         "Explain quantum computing",
+ *         "Write a hello world in Python"
+ *       ]}
+ *     />
+ *   );
+ * };
+ * ```
+ */
+export function AIChatBox({
+  messages,
+  onSendMessage,
+  isLoading = false,
+  placeholder = "Type your message...",
+  className,
+  height = "600px",
+  emptyStateMessage = "Start a conversation with AI",
+  suggestedPrompts,
+}: AIChatBoxProps) {
+  const [input, setInput] = useState("");
+  const scrollAreaRef = useRef<HTMLDivElement>(null);
+  const containerRef = useRef<HTMLDivElement>(null);
+  const inputAreaRef = useRef<HTMLFormElement>(null);
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+  // Filter out system messages
+  const displayMessages = messages.filter((msg) => msg.role !== "system");
+
+  // Calculate min-height for last assistant message to push user message to top
+  const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0);
+
+  useEffect(() => {
+    if (containerRef.current && inputAreaRef.current) {
+      const containerHeight = containerRef.current.offsetHeight;
+      const inputHeight = inputAreaRef.current.offsetHeight;
+      const scrollAreaHeight = containerHeight - inputHeight;
+
+      // Reserve space for:
+      // - padding (p-4 = 32px top+bottom)
+      // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px
+      // Note: margin-bottom is not counted because it naturally pushes the assistant message down
+      const userMessageReservedHeight = 56;
+      const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight;
+
+      setMinHeightForLastMessage(Math.max(0, calculatedHeight));
+    }
+  }, []);
+
+  // Scroll to bottom helper function with smooth animation
+  const scrollToBottom = () => {
+    const viewport = scrollAreaRef.current?.querySelector(
+      '[data-radix-scroll-area-viewport]'
+    ) as HTMLDivElement;
+
+    if (viewport) {
+      requestAnimationFrame(() => {
+        viewport.scrollTo({
+          top: viewport.scrollHeight,
+          behavior: 'smooth'
+        });
+      });
+    }
+  };
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    const trimmedInput = input.trim();
+    if (!trimmedInput || isLoading) return;
+
+    onSendMessage(trimmedInput);
+    setInput("");
+
+    // Scroll immediately after sending
+    scrollToBottom();
+
+    // Keep focus on input
+    textareaRef.current?.focus();
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (e.key === "Enter" && !e.shiftKey) {
+      e.preventDefault();
+      handleSubmit(e);
+    }
+  };
+
+  return (
+    <div
+      ref={containerRef}
+      className={cn(
+        "flex flex-col bg-card text-card-foreground rounded-lg border shadow-sm",
+        className
+      )}
+      style={{ height }}
+    >
+      {/* Messages Area */}
+      <div ref={scrollAreaRef} className="flex-1 overflow-hidden">
+        {displayMessages.length === 0 ? (
+          <div className="flex h-full flex-col p-4">
+            <div className="flex flex-1 flex-col items-center justify-center gap-6 text-muted-foreground">
+              <div className="flex flex-col items-center gap-3">
+                <Sparkles className="size-12 opacity-20" />
+                <p className="text-sm">{emptyStateMessage}</p>
+              </div>
+
+              {suggestedPrompts && suggestedPrompts.length > 0 && (
+                <div className="flex max-w-2xl flex-wrap justify-center gap-2">
+                  {suggestedPrompts.map((prompt, index) => (
+                    <button
+                      key={index}
+                      onClick={() => onSendMessage(prompt)}
+                      disabled={isLoading}
+                      className="rounded-lg border border-border bg-card px-4 py-2 text-sm transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
+                    >
+                      {prompt}
+                    </button>
+                  ))}
+                </div>
+              )}
+            </div>
+          </div>
+        ) : (
+          <ScrollArea className="h-full">
+            <div className="flex flex-col space-y-4 p-4">
+              {displayMessages.map((message, index) => {
+                // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it)
+                const isLastMessage = index === displayMessages.length - 1;
+                const shouldApplyMinHeight =
+                  isLastMessage && !isLoading && minHeightForLastMessage > 0;
+
+                return (
+                  <div
+                    key={index}
+                    className={cn(
+                      "flex gap-3",
+                      message.role === "user"
+                        ? "justify-end items-start"
+                        : "justify-start items-start"
+                    )}
+                    style={
+                      shouldApplyMinHeight
+                        ? { minHeight: `${minHeightForLastMessage}px` }
+                        : undefined
+                    }
+                  >
+                    {message.role === "assistant" && (
+                      <div className="size-8 shrink-0 mt-1 rounded-full bg-primary/10 flex items-center justify-center">
+                        <Sparkles className="size-4 text-primary" />
+                      </div>
+                    )}
+
+                    <div
+                      className={cn(
+                        "max-w-[80%] rounded-lg px-4 py-2.5",
+                        message.role === "user"
+                          ? "bg-primary text-primary-foreground"
+                          : "bg-muted text-foreground"
+                      )}
+                    >
+                      {message.role === "assistant" ? (
+                        <div className="prose prose-sm dark:prose-invert max-w-none">
+                          <Streamdown>{message.content}</Streamdown>
+                        </div>
+                      ) : (
+                        <p className="whitespace-pre-wrap text-sm">
+                          {message.content}
+                        </p>
+                      )}
+                    </div>
+
+                    {message.role === "user" && (
+                      <div className="size-8 shrink-0 mt-1 rounded-full bg-secondary flex items-center justify-center">
+                        <User className="size-4 text-secondary-foreground" />
+                      </div>
+                    )}
+                  </div>
+                );
+              })}
+
+              {isLoading && (
+                <div
+                  className="flex items-start gap-3"
+                  style={
+                    minHeightForLastMessage > 0
+                      ? { minHeight: `${minHeightForLastMessage}px` }
+                      : undefined
+                  }
+                >
+                  <div className="size-8 shrink-0 mt-1 rounded-full bg-primary/10 flex items-center justify-center">
+                    <Sparkles className="size-4 text-primary" />
+                  </div>
+                  <div className="rounded-lg bg-muted px-4 py-2.5">
+                    <Loader2 className="size-4 animate-spin text-muted-foreground" />
+                  </div>
+                </div>
+              )}
+            </div>
+          </ScrollArea>
+        )}
+      </div>
+
+      {/* Input Area */}
+      <form
+        ref={inputAreaRef}
+        onSubmit={handleSubmit}
+        className="flex gap-2 p-4 border-t bg-background/50 items-end"
+      >
+        <Textarea
+          ref={textareaRef}
+          value={input}
+          onChange={(e) => setInput(e.target.value)}
+          onKeyDown={handleKeyDown}
+          placeholder={placeholder}
+          className="flex-1 max-h-32 resize-none min-h-9"
+          rows={1}
+        />
+        <Button
+          type="submit"
+          size="icon"
+          disabled={!input.trim() || isLoading}
+          className="shrink-0 h-[38px] w-[38px]"
+        >
+          {isLoading ? (
+            <Loader2 className="size-4 animate-spin" />
+          ) : (
+            <Send className="size-4" />
+          )}
+        </Button>
+      </form>
+    </div>
+  );
+}

+ 191 - 0
client/src/components/ArchitectureDiagram.tsx

@@ -0,0 +1,191 @@
+/**
+ * ArchitectureDiagram — System architecture visualization
+ * Design: Warm Showroom — forest green, terracotta, cream palette
+ * Shows how the chatbot integrates with Homelegance's existing systems
+ */
+import { motion } from "framer-motion";
+import {
+  Globe,
+  Server,
+  Database,
+  Brain,
+  MessageSquare,
+  ShieldCheck,
+  Webhook,
+  Users,
+} from "lucide-react";
+
+interface ArchBlock {
+  id: string;
+  label: string;
+  sublabel: string;
+  icon: React.ReactNode;
+  color: string;
+  tier: "frontend" | "middleware" | "backend" | "external";
+}
+
+const BLOCKS: ArchBlock[] = [
+  {
+    id: "website",
+    label: "homelegance.com",
+    sublabel: "Website Frontend",
+    icon: <Globe className="w-5 h-5" />,
+    color: "#14532D",
+    tier: "frontend",
+  },
+  {
+    id: "widget",
+    label: "Chat Widget",
+    sublabel: "Embedded JS SDK",
+    icon: <MessageSquare className="w-5 h-5" />,
+    color: "#14532D",
+    tier: "frontend",
+  },
+  {
+    id: "gateway",
+    label: "API Gateway",
+    sublabel: "Auth & Rate Limiting",
+    icon: <ShieldCheck className="w-5 h-5" />,
+    color: "#78716C",
+    tier: "middleware",
+  },
+  {
+    id: "webhook",
+    label: "Webhook Router",
+    sublabel: "Event Processing",
+    icon: <Webhook className="w-5 h-5" />,
+    color: "#78716C",
+    tier: "middleware",
+  },
+  {
+    id: "nlp",
+    label: "NLP Engine",
+    sublabel: "Intent & Entity Recognition",
+    icon: <Brain className="w-5 h-5" />,
+    color: "#C2410C",
+    tier: "backend",
+  },
+  {
+    id: "dialog",
+    label: "Dialog Manager",
+    sublabel: "Conversation State",
+    icon: <MessageSquare className="w-5 h-5" />,
+    color: "#C2410C",
+    tier: "backend",
+  },
+  {
+    id: "crm",
+    label: "CRM / ERP",
+    sublabel: "Homelegance Systems",
+    icon: <Database className="w-5 h-5" />,
+    color: "#78716C",
+    tier: "external",
+  },
+  {
+    id: "agents",
+    label: "Live Agents",
+    sublabel: "Human Handoff",
+    icon: <Users className="w-5 h-5" />,
+    color: "#78716C",
+    tier: "external",
+  },
+];
+
+const TIERS = [
+  { key: "frontend", label: "Client Layer", color: "#14532D" },
+  { key: "middleware", label: "Middleware", color: "#78716C" },
+  { key: "backend", label: "AI & Logic", color: "#C2410C" },
+  { key: "external", label: "Integrations", color: "#78716C" },
+];
+
+export default function ArchitectureDiagram() {
+  return (
+    <div className="w-full">
+      <div className="space-y-4">
+        {TIERS.map((tier, tierIdx) => {
+          const tierBlocks = BLOCKS.filter((b) => b.tier === tier.key);
+          return (
+            <motion.div
+              key={tier.key}
+              initial={{ opacity: 0, x: -20 }}
+              whileInView={{ opacity: 1, x: 0 }}
+              viewport={{ once: true }}
+              transition={{ delay: tierIdx * 0.1 }}
+            >
+              {/* Tier label */}
+              <div className="flex items-center gap-3 mb-3">
+                <span
+                  className="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full"
+                  style={{
+                    background: tier.color + "12",
+                    color: tier.color,
+                    fontFamily: "'Source Sans 3', sans-serif",
+                  }}
+                >
+                  {tier.label}
+                </span>
+                <div className="flex-1 h-px" style={{ background: tier.color + "20" }} />
+              </div>
+
+              {/* Blocks row */}
+              <div className="grid grid-cols-2 gap-3">
+                {tierBlocks.map((block, blockIdx) => (
+                  <motion.div
+                    key={block.id}
+                    initial={{ opacity: 0, y: 10 }}
+                    whileInView={{ opacity: 1, y: 0 }}
+                    viewport={{ once: true }}
+                    transition={{ delay: tierIdx * 0.1 + blockIdx * 0.05 }}
+                    whileHover={{ y: -3, boxShadow: `0 8px 24px ${block.color}18` }}
+                    className="p-4 rounded-xl border transition-all"
+                    style={{
+                      background: "#fff",
+                      borderColor: "#e7e0d5",
+                    }}
+                  >
+                    <div className="flex items-start gap-3">
+                      <div
+                        className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0"
+                        style={{ background: block.color + "12", color: block.color }}
+                      >
+                        {block.icon}
+                      </div>
+                      <div>
+                        <h5
+                          className="text-sm font-semibold"
+                          style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}
+                        >
+                          {block.label}
+                        </h5>
+                        <p className="text-xs mt-0.5" style={{ color: "#a8a29e" }}>
+                          {block.sublabel}
+                        </p>
+                      </div>
+                    </div>
+                  </motion.div>
+                ))}
+              </div>
+
+              {/* Connector arrows between tiers */}
+              {tierIdx < TIERS.length - 1 && (
+                <div className="flex justify-center py-2">
+                  <div className="flex flex-col items-center">
+                    <div className="w-0.5 h-3" style={{ background: "#d6d3d1" }} />
+                    <div
+                      className="w-0 h-0"
+                      style={{
+                        borderLeft: "5px solid transparent",
+                        borderRight: "5px solid transparent",
+                        borderTop: "6px solid #d6d3d1",
+                      }}
+                    />
+                  </div>
+                </div>
+              )}
+            </motion.div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 343 - 0
client/src/components/ChatbotWidget.tsx

@@ -0,0 +1,343 @@
+/**
+ * ChatbotWidget — Interactive demo chatbot widget
+ * Design: Warm Showroom — forest green header, cream body, terracotta accents
+ * Floats in bottom-right corner, exactly as it would appear on homelegance.com
+ */
+import { useState, useRef, useEffect } from "react";
+import { MessageCircle, X, Send, Bot, User } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+
+interface Message {
+  id: number;
+  text: string;
+  sender: "bot" | "user";
+  options?: string[];
+}
+
+const INITIAL_MESSAGES: Message[] = [
+  {
+    id: 1,
+    text: "Welcome to Homelegance! I'm your virtual assistant. How can I help you today?",
+    sender: "bot",
+    options: [
+      "Browse Products",
+      "Find a Dealer",
+      "Order Status",
+      "Parts & Warranty",
+      "Talk to Sales Rep",
+    ],
+  },
+];
+
+const BOT_RESPONSES: Record<string, Message> = {
+  "Browse Products": {
+    id: 0,
+    text: "I'd love to help you find the right furniture! Which category are you interested in?",
+    sender: "bot",
+    options: ["Bedroom", "Dining", "Seating", "Youth", "Accent Chairs", "Office"],
+  },
+  "Find a Dealer": {
+    id: 0,
+    text: "I can help you locate an authorized Homelegance dealer near you. Could you share your zip code or city?",
+    sender: "bot",
+  },
+  "Order Status": {
+    id: 0,
+    text: "To check your order status, please provide your order number or the email address associated with your account.",
+    sender: "bot",
+  },
+  "Parts & Warranty": {
+    id: 0,
+    text: "Homelegance offers a limited warranty on all products. I can help you with a parts request or warranty claim. What do you need?",
+    sender: "bot",
+    options: ["Request Replacement Parts", "File Warranty Claim", "View Warranty Policy"],
+  },
+  "Talk to Sales Rep": {
+    id: 0,
+    text: "I'll connect you with a sales representative right away. Please hold for a moment while I transfer you to our team.",
+    sender: "bot",
+  },
+  Bedroom: {
+    id: 0,
+    text: "Great choice! Our bedroom collections include the popular Lofton Collection with modern elegance, and the new Sloane Collection for youth rooms. Would you like to see best sellers or new arrivals?",
+    sender: "bot",
+    options: ["Best Sellers", "New Arrivals", "View All Bedroom"],
+  },
+  Dining: {
+    id: 0,
+    text: "Our dining collections range from traditional to contemporary. The Isla Collection offers versatile modular options. Would you like to explore?",
+    sender: "bot",
+    options: ["Best Sellers", "New Arrivals", "View All Dining"],
+  },
+  Seating: {
+    id: 0,
+    text: "From sofas to accent chairs, we have a wide selection. Our Isla modular seating collection is very popular with dealers. What are you looking for?",
+    sender: "bot",
+    options: ["Sofas & Sectionals", "Accent Chairs", "Recliners"],
+  },
+  default: {
+    id: 0,
+    text: "Thank you for your message! Let me look into that for you. Is there anything else I can help with?",
+    sender: "bot",
+    options: ["Browse Products", "Find a Dealer", "Talk to Sales Rep"],
+  },
+};
+
+export default function ChatbotWidget() {
+  const [isOpen, setIsOpen] = useState(false);
+  const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
+  const [inputValue, setInputValue] = useState("");
+  const [isTyping, setIsTyping] = useState(false);
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+  const inputRef = useRef<HTMLInputElement>(null);
+  let nextId = useRef(2);
+
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [messages, isTyping]);
+
+  useEffect(() => {
+    if (isOpen) {
+      setTimeout(() => inputRef.current?.focus(), 300);
+    }
+  }, [isOpen]);
+
+  const addBotResponse = (userText: string) => {
+    setIsTyping(true);
+    setTimeout(() => {
+      const response = BOT_RESPONSES[userText] || BOT_RESPONSES["default"];
+      const botMsg: Message = {
+        ...response,
+        id: nextId.current++,
+      };
+      setMessages((prev) => [...prev, botMsg]);
+      setIsTyping(false);
+    }, 800 + Math.random() * 600);
+  };
+
+  const handleSend = (text?: string) => {
+    const messageText = text || inputValue.trim();
+    if (!messageText) return;
+
+    const userMsg: Message = {
+      id: nextId.current++,
+      text: messageText,
+      sender: "user",
+    };
+    setMessages((prev) => [...prev, userMsg]);
+    setInputValue("");
+    addBotResponse(messageText);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter") {
+      e.preventDefault();
+      handleSend();
+    }
+  };
+
+  return (
+    <>
+      {/* Floating trigger button */}
+      <AnimatePresence>
+        {!isOpen && (
+          <motion.button
+            initial={{ scale: 0, opacity: 0 }}
+            animate={{ scale: 1, opacity: 1 }}
+            exit={{ scale: 0, opacity: 0 }}
+            whileHover={{ scale: 1.08 }}
+            whileTap={{ scale: 0.95 }}
+            onClick={() => setIsOpen(true)}
+            className="fixed bottom-6 right-6 z-50 w-16 h-16 rounded-full flex items-center justify-center shadow-lg"
+            style={{
+              background: "linear-gradient(135deg, #14532D 0%, #1a6b3a 100%)",
+              boxShadow: "0 8px 32px rgba(20, 83, 45, 0.35), 0 2px 8px rgba(0,0,0,0.1)",
+            }}
+          >
+            <MessageCircle className="w-7 h-7 text-white" />
+            {/* Pulse ring */}
+            <span className="absolute inset-0 rounded-full animate-ping opacity-20" style={{ background: "#14532D" }} />
+          </motion.button>
+        )}
+      </AnimatePresence>
+
+      {/* Chat window */}
+      <AnimatePresence>
+        {isOpen && (
+          <motion.div
+            initial={{ opacity: 0, y: 20, scale: 0.95 }}
+            animate={{ opacity: 1, y: 0, scale: 1 }}
+            exit={{ opacity: 0, y: 20, scale: 0.95 }}
+            transition={{ type: "spring", damping: 25, stiffness: 300 }}
+            className="fixed bottom-6 right-6 z-50 w-[380px] max-w-[calc(100vw-2rem)] rounded-2xl overflow-hidden flex flex-col"
+            style={{
+              height: "520px",
+              boxShadow: "0 20px 60px rgba(20, 83, 45, 0.2), 0 4px 16px rgba(0,0,0,0.08)",
+            }}
+          >
+            {/* Header */}
+            <div
+              className="px-5 py-4 flex items-center justify-between shrink-0"
+              style={{
+                background: "linear-gradient(135deg, #14532D 0%, #1a6b3a 100%)",
+              }}
+            >
+              <div className="flex items-center gap-3">
+                <div className="w-10 h-10 rounded-full bg-white/15 flex items-center justify-center">
+                  <Bot className="w-5 h-5 text-white" />
+                </div>
+                <div>
+                  <h3 className="text-white font-semibold text-sm" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+                    Homelegance Assistant
+                  </h3>
+                  <div className="flex items-center gap-1.5">
+                    <span className="w-2 h-2 rounded-full bg-green-300 animate-pulse" />
+                    <span className="text-green-200 text-xs">Online</span>
+                  </div>
+                </div>
+              </div>
+              <button
+                onClick={() => setIsOpen(false)}
+                className="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
+              >
+                <X className="w-4 h-4 text-white" />
+              </button>
+            </div>
+
+            {/* Messages area */}
+            <div
+              className="flex-1 overflow-y-auto px-4 py-4 space-y-4"
+              style={{ background: "#FFFBEB" }}
+            >
+              {messages.map((msg) => (
+                <motion.div
+                  key={msg.id}
+                  initial={{ opacity: 0, y: 8 }}
+                  animate={{ opacity: 1, y: 0 }}
+                  transition={{ duration: 0.3 }}
+                  className={`flex gap-2 ${msg.sender === "user" ? "justify-end" : "justify-start"}`}
+                >
+                  {msg.sender === "bot" && (
+                    <div className="w-7 h-7 rounded-full shrink-0 flex items-center justify-center mt-1" style={{ background: "#14532D" }}>
+                      <Bot className="w-3.5 h-3.5 text-white" />
+                    </div>
+                  )}
+                  <div className={`max-w-[75%] ${msg.sender === "user" ? "" : ""}`}>
+                    <div
+                      className="px-4 py-2.5 rounded-2xl text-sm leading-relaxed"
+                      style={
+                        msg.sender === "user"
+                          ? {
+                              background: "#14532D",
+                              color: "#fff",
+                              borderBottomRightRadius: "6px",
+                              fontFamily: "'Source Sans 3', sans-serif",
+                            }
+                          : {
+                              background: "#fff",
+                              color: "#292524",
+                              borderBottomLeftRadius: "6px",
+                              boxShadow: "0 1px 4px rgba(120, 113, 108, 0.1)",
+                              fontFamily: "'Source Sans 3', sans-serif",
+                            }
+                      }
+                    >
+                      {msg.text}
+                    </div>
+                    {msg.options && (
+                      <div className="flex flex-wrap gap-1.5 mt-2">
+                        {msg.options.map((opt) => (
+                          <button
+                            key={opt}
+                            onClick={() => handleSend(opt)}
+                            className="px-3 py-1.5 text-xs rounded-full border transition-all hover:shadow-sm"
+                            style={{
+                              borderColor: "#C2410C",
+                              color: "#C2410C",
+                              background: "rgba(194, 65, 12, 0.05)",
+                              fontFamily: "'Source Sans 3', sans-serif",
+                              fontWeight: 500,
+                            }}
+                            onMouseEnter={(e) => {
+                              e.currentTarget.style.background = "#C2410C";
+                              e.currentTarget.style.color = "#fff";
+                            }}
+                            onMouseLeave={(e) => {
+                              e.currentTarget.style.background = "rgba(194, 65, 12, 0.05)";
+                              e.currentTarget.style.color = "#C2410C";
+                            }}
+                          >
+                            {opt}
+                          </button>
+                        ))}
+                      </div>
+                    )}
+                  </div>
+                  {msg.sender === "user" && (
+                    <div className="w-7 h-7 rounded-full shrink-0 flex items-center justify-center mt-1" style={{ background: "#78716C" }}>
+                      <User className="w-3.5 h-3.5 text-white" />
+                    </div>
+                  )}
+                </motion.div>
+              ))}
+
+              {/* Typing indicator */}
+              {isTyping && (
+                <motion.div
+                  initial={{ opacity: 0 }}
+                  animate={{ opacity: 1 }}
+                  className="flex gap-2 items-start"
+                >
+                  <div className="w-7 h-7 rounded-full shrink-0 flex items-center justify-center" style={{ background: "#14532D" }}>
+                    <Bot className="w-3.5 h-3.5 text-white" />
+                  </div>
+                  <div className="px-4 py-3 rounded-2xl bg-white" style={{ borderBottomLeftRadius: "6px", boxShadow: "0 1px 4px rgba(120, 113, 108, 0.1)" }}>
+                    <div className="flex gap-1">
+                      <span className="w-2 h-2 rounded-full bg-stone-400 animate-bounce" style={{ animationDelay: "0ms" }} />
+                      <span className="w-2 h-2 rounded-full bg-stone-400 animate-bounce" style={{ animationDelay: "150ms" }} />
+                      <span className="w-2 h-2 rounded-full bg-stone-400 animate-bounce" style={{ animationDelay: "300ms" }} />
+                    </div>
+                  </div>
+                </motion.div>
+              )}
+              <div ref={messagesEndRef} />
+            </div>
+
+            {/* Input area */}
+            <div className="px-4 py-3 border-t shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+              <div className="flex items-center gap-2">
+                <input
+                  ref={inputRef}
+                  type="text"
+                  value={inputValue}
+                  onChange={(e) => setInputValue(e.target.value)}
+                  onKeyDown={handleKeyDown}
+                  placeholder="Type your message..."
+                  className="flex-1 px-4 py-2.5 rounded-full text-sm border-none outline-none"
+                  style={{
+                    background: "#f5f0e8",
+                    color: "#292524",
+                    fontFamily: "'Source Sans 3', sans-serif",
+                  }}
+                />
+                <button
+                  onClick={() => handleSend()}
+                  disabled={!inputValue.trim()}
+                  className="w-10 h-10 rounded-full flex items-center justify-center transition-all disabled:opacity-40"
+                  style={{
+                    background: inputValue.trim() ? "#14532D" : "#d6d3d1",
+                  }}
+                >
+                  <Send className="w-4 h-4 text-white" />
+                </button>
+              </div>
+              <p className="text-center text-[10px] mt-2" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>
+                Powered by Homelegance AI
+              </p>
+            </div>
+          </motion.div>
+        )}
+      </AnimatePresence>
+    </>
+  );
+}

+ 315 - 0
client/src/components/ChatbotWidgetLive.tsx

@@ -0,0 +1,315 @@
+/**
+ * Live Chatbot Widget — Tidio-inspired greeting with quick-reply buttons
+ * Shows: "What can we help you with today?" + Orders, Shipping, Returning, Cancelling buttons
+ */
+import { useState, useRef, useEffect } from "react";
+import { trpc } from "@/lib/trpc";
+import { Bot, X, Send, Loader2, User, Headphones, Sparkles, ChevronDown, MoreVertical } from "lucide-react";
+import { Streamdown } from "streamdown";
+
+const QUICK_REPLIES = [
+  { label: "Orders", value: "I need help with my order" },
+  { label: "Shipping", value: "I have a shipping question" },
+  { label: "Returning", value: "I want to return an item" },
+  { label: "Cancelling", value: "I need to cancel my order" },
+];
+
+export default function ChatbotWidgetLive() {
+  const [isOpen, setIsOpen] = useState(false);
+  const [inputText, setInputText] = useState("");
+  const [sessionId, setSessionId] = useState<string | null>(null);
+  const [localMessages, setLocalMessages] = useState<Array<{ id: number; sender: string; content: string; createdAt: string }>>([]);
+  const [isTyping, setIsTyping] = useState(false);
+  const [showQuickReplies, setShowQuickReplies] = useState(true);
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  const startSession = trpc.chat.startSession.useMutation({
+    onSuccess: (data) => {
+      setSessionId(data.sessionId);
+    },
+  });
+
+  const { data: messagesData } = trpc.chat.getMessages.useQuery(
+    { sessionId: sessionId || "" },
+    { enabled: !!sessionId, refetchInterval: 3000 }
+  );
+
+  const sendMessage = trpc.chat.sendMessage.useMutation({
+    onSuccess: () => {
+      setIsTyping(false);
+    },
+    onError: () => {
+      setIsTyping(false);
+    },
+  });
+
+  // Sync server messages to local state
+  useEffect(() => {
+    if (messagesData?.messages) {
+      setLocalMessages(messagesData.messages.map((m: any) => ({
+        id: m.id,
+        sender: m.sender,
+        content: m.content,
+        createdAt: m.createdAt?.toString() || new Date().toISOString(),
+      })));
+    }
+  }, [messagesData]);
+
+  // Auto-scroll
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [localMessages, isTyping]);
+
+  const handleOpen = () => {
+    setIsOpen(true);
+    if (!sessionId) {
+      startSession.mutate({});
+    }
+  };
+
+  const handleSend = (text?: string) => {
+    const content = text || inputText.trim();
+    if (!content || !sessionId || isTyping) return;
+    setInputText("");
+    setIsTyping(true);
+    setShowQuickReplies(false);
+
+    // Optimistically add user message
+    setLocalMessages(prev => [...prev, {
+      id: Date.now(),
+      sender: "visitor",
+      content,
+      createdAt: new Date().toISOString(),
+    }]);
+
+    sendMessage.mutate({ sessionId, content });
+  };
+
+  const handleQuickReply = (reply: typeof QUICK_REPLIES[0]) => {
+    handleSend(reply.value);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter" && !e.shiftKey) {
+      e.preventDefault();
+      handleSend();
+    }
+  };
+
+  // Determine if we should show the greeting state (no user messages yet)
+  const hasUserMessages = localMessages.some(m => m.sender === "visitor");
+
+  return (
+    <>
+      {/* Chat window */}
+      {isOpen && (
+        <div
+          className="fixed bottom-20 right-4 z-50 w-[380px] max-w-[calc(100vw-2rem)] rounded-2xl shadow-2xl overflow-hidden flex flex-col"
+          style={{
+            height: "520px",
+            background: "#FFFBEB",
+            border: "1px solid #e7e0d5",
+            boxShadow: "0 20px 60px rgba(0,0,0,0.15)",
+          }}
+        >
+          {/* Header — Tidio-inspired gradient */}
+          <div
+            className="px-4 py-3 flex items-center justify-between relative"
+            style={{
+              background: "linear-gradient(135deg, #14532D 0%, #166534 50%, #15803d 100%)",
+            }}
+          >
+            <div className="flex items-center gap-2.5">
+              <div className="relative">
+                <div className="w-10 h-10 rounded-full flex items-center justify-center overflow-hidden" style={{ background: "rgba(255,255,255,0.2)" }}>
+                  <Sparkles className="w-5 h-5 text-white" />
+                </div>
+                {/* Online indicator */}
+                <div className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-green-800" style={{ background: "#4ade80" }} />
+              </div>
+              <div>
+                <div className="text-sm font-semibold text-white" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+                  Ellie
+                </div>
+                <div className="flex items-center gap-1">
+                  <div className="w-1.5 h-1.5 rounded-full" style={{ background: "#4ade80" }} />
+                  <span className="text-[10px] text-green-200">
+                    {messagesData?.status === "escalated" ? "Connected to agent" : "Always happy to help"}
+                  </span>
+                </div>
+              </div>
+            </div>
+            <div className="flex items-center gap-1">
+              <button className="p-1.5 rounded-lg hover:bg-white/10 transition-colors">
+                <MoreVertical className="w-4 h-4 text-white/70" />
+              </button>
+              <button onClick={() => setIsOpen(false)} className="p-1.5 rounded-lg hover:bg-white/10 transition-colors">
+                <ChevronDown className="w-4 h-4 text-white/70" />
+              </button>
+            </div>
+          </div>
+
+          {/* Messages */}
+          <div className="flex-1 overflow-y-auto p-3 space-y-3">
+            {startSession.isPending ? (
+              <div className="flex items-center justify-center py-8">
+                <Loader2 className="w-5 h-5 animate-spin" style={{ color: "#14532D" }} />
+                <span className="ml-2 text-sm" style={{ color: "#78716C" }}>Starting conversation...</span>
+              </div>
+            ) : (
+              <>
+                {/* Greeting bubble — always shown at top */}
+                {!hasUserMessages && (
+                  <div className="flex gap-2 justify-start">
+                    <div
+                      className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1"
+                      style={{ background: "#14532D", color: "#fff" }}
+                    >
+                      <Sparkles className="w-3 h-3" />
+                    </div>
+                    <div
+                      className="max-w-[80%] px-3 py-2.5 rounded-xl text-[13px] leading-relaxed"
+                      style={{
+                        background: "#f5f0e8",
+                        color: "#292524",
+                        fontFamily: "'Source Sans 3', sans-serif",
+                        borderBottomLeftRadius: "4px",
+                      }}
+                    >
+                      <span>👋 What can we help you with today?</span>
+                    </div>
+                  </div>
+                )}
+
+                {/* Quick reply buttons — Tidio-style */}
+                {showQuickReplies && !hasUserMessages && sessionId && (
+                  <div className="flex flex-wrap gap-2 justify-center py-2">
+                    {QUICK_REPLIES.map((reply) => (
+                      <button
+                        key={reply.label}
+                        onClick={() => handleQuickReply(reply)}
+                        disabled={isTyping}
+                        className="px-4 py-2 rounded-full text-[13px] font-medium transition-all hover:shadow-md active:scale-95 disabled:opacity-50"
+                        style={{
+                          background: "#fff",
+                          color: "#0369a1",
+                          border: "1.5px solid #0369a1",
+                          fontFamily: "'Source Sans 3', sans-serif",
+                        }}
+                      >
+                        {reply.label}
+                      </button>
+                    ))}
+                  </div>
+                )}
+
+                {/* Conversation messages */}
+                {localMessages.map((msg) => {
+                  const isVisitor = msg.sender === "visitor";
+                  const isBot = msg.sender === "bot";
+                  return (
+                    <div key={msg.id} className={`flex gap-2 ${isVisitor ? "justify-end" : "justify-start"}`}>
+                      {!isVisitor && (
+                        <div
+                          className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1"
+                          style={{ background: isBot ? "#14532D" : "#C2410C", color: "#fff" }}
+                        >
+                          {isBot ? <Sparkles className="w-3 h-3" /> : <Headphones className="w-3 h-3" />}
+                        </div>
+                      )}
+                      <div
+                        className="max-w-[80%] px-3 py-2 rounded-xl text-[13px] leading-relaxed"
+                        style={{
+                          background: isVisitor ? "#14532D" : "#f5f0e8",
+                          color: isVisitor ? "#fff" : "#292524",
+                          fontFamily: "'Source Sans 3', sans-serif",
+                          borderBottomRightRadius: isVisitor ? "4px" : undefined,
+                          borderBottomLeftRadius: !isVisitor ? "4px" : undefined,
+                        }}
+                      >
+                        {isBot || msg.sender === "agent" ? (
+                          <div className="prose prose-sm max-w-none" style={{ color: "inherit" }}>
+                            <Streamdown>{msg.content}</Streamdown>
+                          </div>
+                        ) : (
+                          msg.content
+                        )}
+                      </div>
+                      {isVisitor && (
+                        <div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1" style={{ background: "#78716C20", color: "#78716C" }}>
+                          <User className="w-3 h-3" />
+                        </div>
+                      )}
+                    </div>
+                  );
+                })}
+              </>
+            )}
+            {isTyping && (
+              <div className="flex gap-2 items-start">
+                <div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0" style={{ background: "#14532D", color: "#fff" }}>
+                  <Sparkles className="w-3 h-3" />
+                </div>
+                <div className="px-3 py-2 rounded-xl" style={{ background: "#f5f0e8" }}>
+                  <div className="flex gap-1">
+                    <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "0ms" }} />
+                    <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "150ms" }} />
+                    <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "300ms" }} />
+                  </div>
+                </div>
+              </div>
+            )}
+            <div ref={messagesEndRef} />
+          </div>
+
+          {/* Input area */}
+          <div className="p-3 border-t" style={{ borderColor: "#e7e0d5" }}>
+            {showQuickReplies && !hasUserMessages ? (
+              <div className="text-center py-1">
+                <span className="text-xs" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>
+                  Hit the buttons to respond
+                </span>
+              </div>
+            ) : null}
+            <div className="flex gap-2">
+              <input
+                type="text"
+                value={inputText}
+                onChange={(e) => setInputText(e.target.value)}
+                onKeyDown={handleKeyDown}
+                placeholder="Type your message..."
+                className="flex-1 px-3 py-2 rounded-xl text-sm border"
+                style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif", background: "#fff" }}
+                disabled={isTyping || startSession.isPending}
+              />
+              <button
+                onClick={() => handleSend()}
+                disabled={!inputText.trim() || isTyping || !sessionId}
+                className="w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-40"
+                style={{ background: "#14532D", color: "#fff" }}
+              >
+                <Send className="w-4 h-4" />
+              </button>
+            </div>
+            <div className="text-center mt-2">
+              <span className="text-[10px]" style={{ color: "#d6d3d1" }}>Powered by Ellie · Homelegance AI</span>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* Floating button */}
+      <button
+        onClick={handleOpen}
+        className="fixed bottom-4 right-4 z-50 w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all hover:scale-105"
+        style={{
+          background: "#14532D",
+          color: "#fff",
+          boxShadow: "0 4px 20px rgba(20, 83, 45, 0.35)",
+        }}
+      >
+        {isOpen ? <X className="w-5 h-5" /> : <Bot className="w-6 h-6" />}
+      </button>
+    </>
+  );
+}

+ 74 - 0
client/src/components/CodeSnippet.tsx

@@ -0,0 +1,74 @@
+/**
+ * CodeSnippet — Styled code block with copy functionality
+ * Design: Warm Showroom — dark surface with warm tones
+ */
+import { useState } from "react";
+import { Check, Copy } from "lucide-react";
+
+interface CodeSnippetProps {
+  code: string;
+  language: string;
+  title?: string;
+  filename?: string;
+}
+
+export default function CodeSnippet({ code, language, title, filename }: CodeSnippetProps) {
+  const [copied, setCopied] = useState(false);
+
+  const handleCopy = async () => {
+    await navigator.clipboard.writeText(code);
+    setCopied(true);
+    setTimeout(() => setCopied(false), 2000);
+  };
+
+  return (
+    <div className="rounded-xl overflow-hidden" style={{ border: "1px solid #e7e0d5" }}>
+      {/* Header */}
+      <div
+        className="flex items-center justify-between px-4 py-2.5"
+        style={{ background: "#292524" }}
+      >
+        <div className="flex items-center gap-3">
+          <div className="flex gap-1.5">
+            <span className="w-3 h-3 rounded-full" style={{ background: "#C2410C" }} />
+            <span className="w-3 h-3 rounded-full" style={{ background: "#EAB308" }} />
+            <span className="w-3 h-3 rounded-full" style={{ background: "#14532D" }} />
+          </div>
+          {(title || filename) && (
+            <span className="text-xs" style={{ color: "#a8a29e", fontFamily: "'Fira Code', monospace" }}>
+              {filename || title}
+            </span>
+          )}
+        </div>
+        <div className="flex items-center gap-2">
+          <span
+            className="text-[10px] uppercase tracking-wider px-2 py-0.5 rounded"
+            style={{ background: "#3f3f3f", color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            {language}
+          </span>
+          <button
+            onClick={handleCopy}
+            className="p-1.5 rounded-md transition-colors hover:bg-white/10"
+          >
+            {copied ? (
+              <Check className="w-3.5 h-3.5 text-green-400" />
+            ) : (
+              <Copy className="w-3.5 h-3.5" style={{ color: "#a8a29e" }} />
+            )}
+          </button>
+        </div>
+      </div>
+
+      {/* Code body */}
+      <div
+        className="px-5 py-4 overflow-x-auto"
+        style={{ background: "#1c1917" }}
+      >
+        <pre className="text-sm leading-relaxed" style={{ fontFamily: "'Fira Code', monospace" }}>
+          <code style={{ color: "#e7e5e4" }}>{code}</code>
+        </pre>
+      </div>
+    </div>
+  );
+}

+ 378 - 0
client/src/components/DashboardLayout.tsx

@@ -0,0 +1,378 @@
+import { useAuth } from "@/_core/hooks/useAuth";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarHeader,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+  SidebarTrigger,
+  useSidebar,
+} from "@/components/ui/sidebar";
+import { getLoginUrl } from "@/const";
+import { useIsMobile } from "@/hooks/useMobile";
+import {
+  Headphones, LogOut, PanelLeft, MessageSquare,
+  GitBranch, Users, Home, Shield,
+  Play, BarChart3, Database, Plug,
+} from "lucide-react";
+import { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
+import { useLocation } from "wouter";
+import { DashboardLayoutSkeleton } from "./DashboardLayoutSkeleton";
+import { Button } from "./ui/button";
+
+/* ─── Role badge styling ─── */
+const ROLE_STYLES: Record<string, { bg: string; text: string; label: string }> = {
+  admin: { bg: "#14532D18", text: "#14532D", label: "Admin" },
+  agent: { bg: "#0369a118", text: "#0369a1", label: "Agent" },
+  user: { bg: "#78716C18", text: "#78716C", label: "User" },
+};
+
+/* ─── Menu items definition ─── */
+interface MenuItem {
+  icon: React.ComponentType<{ className?: string }>;
+  label: string;
+  path: string;
+  roles: string[]; // which roles can see this item
+}
+
+const ALL_MENU_ITEMS: MenuItem[] = [
+  { icon: Home, label: "Home", path: "/", roles: ["admin", "agent", "user"] },
+  { icon: MessageSquare, label: "Conversations", path: "/dashboard", roles: ["admin", "agent"] },
+  { icon: Play, label: "Playground", path: "/playground", roles: ["admin", "agent"] },
+  { icon: GitBranch, label: "Workflow Designer", path: "/workflow-designer", roles: ["admin"] },
+  { icon: BarChart3, label: "Analytics", path: "/analytics", roles: ["admin"] },
+  { icon: Database, label: "Data Sources", path: "/data-sources", roles: ["admin"] },
+  { icon: Users, label: "User Management", path: "/dashboard/users", roles: ["admin"] },
+];
+
+const SIDEBAR_WIDTH_KEY = "sidebar-width";
+const DEFAULT_WIDTH = 260;
+const MIN_WIDTH = 200;
+const MAX_WIDTH = 400;
+
+export default function DashboardLayout({
+  children,
+  requiredRole,
+}: {
+  children: React.ReactNode;
+  requiredRole?: "agent" | "admin";
+}) {
+  const [sidebarWidth, setSidebarWidth] = useState(() => {
+    const saved = localStorage.getItem(SIDEBAR_WIDTH_KEY);
+    return saved ? parseInt(saved, 10) : DEFAULT_WIDTH;
+  });
+  const { loading, user } = useAuth();
+
+  useEffect(() => {
+    localStorage.setItem(SIDEBAR_WIDTH_KEY, sidebarWidth.toString());
+  }, [sidebarWidth]);
+
+  if (loading) {
+    return <DashboardLayoutSkeleton />;
+  }
+
+  if (!user) {
+    // Redirect to login page with return URL
+    const currentPath = window.location.pathname;
+    window.location.href = `/login?returnTo=${encodeURIComponent(currentPath)}`;
+    return <DashboardLayoutSkeleton />;
+  }
+
+  // Check role-based access
+  if (requiredRole) {
+    const hasAccess =
+      requiredRole === "admin"
+        ? user.role === "admin"
+        : user.role === "admin" || user.role === "agent";
+
+    if (!hasAccess) {
+      return (
+        <div
+          className="flex items-center justify-center min-h-screen"
+          style={{ background: "#FFFBEB" }}
+        >
+          <div className="flex flex-col items-center gap-6 p-8 max-w-md w-full rounded-2xl" style={{ background: "#fff", border: "1px solid #e7e0d5" }}>
+            <div className="w-14 h-14 rounded-xl flex items-center justify-center" style={{ background: "#C2410C18", color: "#C2410C" }}>
+              <Shield className="w-7 h-7" />
+            </div>
+            <div className="flex flex-col items-center gap-2">
+              <h1
+                className="text-xl font-bold text-center"
+                style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+              >
+                Access Restricted
+              </h1>
+              <p
+                className="text-sm text-center max-w-sm"
+                style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+              >
+                {requiredRole === "admin"
+                  ? "This section requires admin privileges. Contact your administrator to request access."
+                  : "This section requires agent or admin privileges. Contact your administrator to request access."}
+              </p>
+              <div className="mt-2">
+                <span
+                  className="text-xs font-medium px-3 py-1 rounded-full"
+                  style={{
+                    background: ROLE_STYLES[user.role]?.bg || ROLE_STYLES.user.bg,
+                    color: ROLE_STYLES[user.role]?.text || ROLE_STYLES.user.text,
+                  }}
+                >
+                  Your role: {ROLE_STYLES[user.role]?.label || "User"}
+                </span>
+              </div>
+            </div>
+            <Button
+              onClick={() => { window.location.href = "/"; }}
+              variant="outline"
+              className="w-full"
+              style={{ borderColor: "#e7e0d5" }}
+            >
+              Back to Home
+            </Button>
+          </div>
+        </div>
+      );
+    }
+  }
+
+  return (
+    <SidebarProvider
+      style={
+        {
+          "--sidebar-width": `${sidebarWidth}px`,
+        } as CSSProperties
+      }
+    >
+      <DashboardLayoutContent setSidebarWidth={setSidebarWidth}>
+        {children}
+      </DashboardLayoutContent>
+    </SidebarProvider>
+  );
+}
+
+type DashboardLayoutContentProps = {
+  children: React.ReactNode;
+  setSidebarWidth: (width: number) => void;
+};
+
+function DashboardLayoutContent({
+  children,
+  setSidebarWidth,
+}: DashboardLayoutContentProps) {
+  const { user, logout } = useAuth();
+  const [location, setLocation] = useLocation();
+  const { state, toggleSidebar } = useSidebar();
+  const isCollapsed = state === "collapsed";
+  const [isResizing, setIsResizing] = useState(false);
+  const sidebarRef = useRef<HTMLDivElement>(null);
+  const isMobile = useIsMobile();
+
+  // Filter menu items based on user role
+  const menuItems = useMemo(() => {
+    if (!user) return [];
+    return ALL_MENU_ITEMS.filter(item => item.roles.includes(user.role));
+  }, [user]);
+
+  const activeMenuItem = menuItems.find(item => item.path === location);
+  const roleStyle = ROLE_STYLES[user?.role || "user"] || ROLE_STYLES.user;
+
+  useEffect(() => {
+    if (isCollapsed) {
+      setIsResizing(false);
+    }
+  }, [isCollapsed]);
+
+  useEffect(() => {
+    const handleMouseMove = (e: MouseEvent) => {
+      if (!isResizing) return;
+      const sidebarLeft =
+        sidebarRef.current?.getBoundingClientRect().left ?? 0;
+      const newWidth = e.clientX - sidebarLeft;
+      if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
+        setSidebarWidth(newWidth);
+      }
+    };
+
+    const handleMouseUp = () => {
+      setIsResizing(false);
+    };
+
+    if (isResizing) {
+      document.addEventListener("mousemove", handleMouseMove);
+      document.addEventListener("mouseup", handleMouseUp);
+      document.body.style.cursor = "col-resize";
+      document.body.style.userSelect = "none";
+    }
+
+    return () => {
+      document.removeEventListener("mousemove", handleMouseMove);
+      document.removeEventListener("mouseup", handleMouseUp);
+      document.body.style.cursor = "";
+      document.body.style.userSelect = "";
+    };
+  }, [isResizing, setSidebarWidth]);
+
+  return (
+    <>
+      <div className="relative" ref={sidebarRef}>
+        <Sidebar
+          collapsible="icon"
+          className="border-r-0"
+          disableTransition={isResizing}
+        >
+          <SidebarHeader className="h-16 justify-center">
+            <div className="flex items-center gap-3 px-2 transition-all w-full">
+              <button
+                onClick={toggleSidebar}
+                className="h-8 w-8 flex items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
+                style={{ background: "#14532D", color: "#fff" }}
+                aria-label="Toggle navigation"
+              >
+                <PanelLeft className="h-4 w-4" />
+              </button>
+              {!isCollapsed ? (
+                <div className="flex items-center gap-2 min-w-0">
+                  <span
+                    className="font-bold tracking-tight truncate text-sm"
+                    style={{ fontFamily: "'Playfair Display', serif", color: "#14532D" }}
+                  >
+                    Homelegance
+                  </span>
+                </div>
+              ) : null}
+            </div>
+          </SidebarHeader>
+
+          <SidebarContent className="gap-0">
+            <SidebarMenu className="px-2 py-1">
+              {menuItems.map(item => {
+                const isActive = location === item.path;
+                return (
+                  <SidebarMenuItem key={item.path}>
+                    <SidebarMenuButton
+                      isActive={isActive}
+                      onClick={() => setLocation(item.path)}
+                      tooltip={item.label}
+                      className="h-10 transition-all font-normal"
+                    >
+                      <item.icon
+                        className={`h-4 w-4`}
+                      />
+                      <span
+                        className="text-sm"
+                        style={{
+                          fontFamily: "'Source Sans 3', sans-serif",
+                          color: isActive ? "#14532D" : "#57534e",
+                          fontWeight: isActive ? 600 : 400,
+                        }}
+                      >
+                        {item.label}
+                      </span>
+                    </SidebarMenuButton>
+                  </SidebarMenuItem>
+                );
+              })}
+            </SidebarMenu>
+          </SidebarContent>
+
+          <SidebarFooter className="p-3">
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <button className="flex items-center gap-3 rounded-lg px-1 py-1 hover:bg-accent/50 transition-colors w-full text-left group-data-[collapsible=icon]:justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-ring">
+                  <Avatar className="h-9 w-9 border shrink-0">
+                    <AvatarFallback
+                      className="text-xs font-medium"
+                      style={{ background: roleStyle.bg, color: roleStyle.text }}
+                    >
+                      {user?.name?.charAt(0).toUpperCase() || "?"}
+                    </AvatarFallback>
+                  </Avatar>
+                  <div className="flex-1 min-w-0 group-data-[collapsible=icon]:hidden">
+                    <div className="flex items-center gap-2">
+                      <p className="text-sm font-medium truncate leading-none">
+                        {user?.name || "-"}
+                      </p>
+                      <span
+                        className="text-[10px] font-medium px-1.5 py-0.5 rounded-full shrink-0"
+                        style={{ background: roleStyle.bg, color: roleStyle.text }}
+                      >
+                        {roleStyle.label}
+                      </span>
+                    </div>
+                    <p className="text-xs text-muted-foreground truncate mt-1">
+                      {user?.email || "-"}
+                    </p>
+                  </div>
+                </button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="end" className="w-52">
+                <div className="px-2 py-1.5">
+                  <p className="text-xs text-muted-foreground">Signed in as</p>
+                  <p className="text-sm font-medium truncate">{user?.name || user?.email}</p>
+                  <span
+                    className="text-[10px] font-medium px-1.5 py-0.5 rounded-full inline-block mt-1"
+                    style={{ background: roleStyle.bg, color: roleStyle.text }}
+                  >
+                    {roleStyle.label}
+                  </span>
+                </div>
+                <DropdownMenuSeparator />
+                <DropdownMenuItem
+                  onClick={logout}
+                  className="cursor-pointer text-destructive focus:text-destructive"
+                >
+                  <LogOut className="mr-2 h-4 w-4" />
+                  <span>Sign out</span>
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </SidebarFooter>
+        </Sidebar>
+        <div
+          className={`absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-primary/20 transition-colors ${isCollapsed ? "hidden" : ""}`}
+          onMouseDown={() => {
+            if (isCollapsed) return;
+            setIsResizing(true);
+          }}
+          style={{ zIndex: 50 }}
+        />
+      </div>
+
+      <SidebarInset>
+        {isMobile && (
+          <div className="flex border-b h-14 items-center justify-between bg-background/95 px-2 backdrop-blur supports-[backdrop-filter]:backdrop-blur sticky top-0 z-40">
+            <div className="flex items-center gap-2">
+              <SidebarTrigger className="h-9 w-9 rounded-lg bg-background" />
+              <div className="flex items-center gap-3">
+                <div className="flex flex-col gap-1">
+                  <span
+                    className="tracking-tight text-foreground text-sm"
+                    style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+                  >
+                    {activeMenuItem?.label ?? "Dashboard"}
+                  </span>
+                </div>
+              </div>
+            </div>
+          </div>
+        )}
+        <main className="flex-1 p-4" style={{ background: "#FFFBEB" }}>
+          {children}
+        </main>
+      </SidebarInset>
+    </>
+  );
+}

+ 46 - 0
client/src/components/DashboardLayoutSkeleton.tsx

@@ -0,0 +1,46 @@
+import { Skeleton } from './ui/skeleton';
+
+export function DashboardLayoutSkeleton() {
+  return (
+    <div className="flex min-h-screen bg-background">
+      {/* Sidebar skeleton */}
+      <div className="w-[280px] border-r border-border bg-background p-4 space-y-6">
+        {/* Logo area */}
+        <div className="flex items-center gap-3 px-2">
+          <Skeleton className="h-8 w-8 rounded-md" />
+          <Skeleton className="h-4 w-24" />
+        </div>
+
+        {/* Menu items */}
+        <div className="space-y-2 px-2">
+          <Skeleton className="h-10 w-full rounded-lg" />
+          <Skeleton className="h-10 w-full rounded-lg" />
+          <Skeleton className="h-10 w-full rounded-lg" />
+        </div>
+
+        {/* User profile area at bottom */}
+        <div className="absolute bottom-4 left-4 right-4">
+          <div className="flex items-center gap-3 px-1">
+            <Skeleton className="h-9 w-9 rounded-full" />
+            <div className="flex-1 space-y-2">
+              <Skeleton className="h-3 w-20" />
+              <Skeleton className="h-2 w-32" />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* Main content skeleton */}
+      <div className="flex-1 p-4 space-y-4">
+        {/* Content blocks */}
+        <Skeleton className="h-12 w-48 rounded-lg" />
+        <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+          <Skeleton className="h-32 rounded-xl" />
+          <Skeleton className="h-32 rounded-xl" />
+          <Skeleton className="h-32 rounded-xl" />
+        </div>
+        <Skeleton className="h-64 rounded-xl" />
+      </div>
+    </div>
+  );
+}

+ 62 - 0
client/src/components/ErrorBoundary.tsx

@@ -0,0 +1,62 @@
+import { cn } from "@/lib/utils";
+import { AlertTriangle, RotateCcw } from "lucide-react";
+import { Component, ReactNode } from "react";
+
+interface Props {
+  children: ReactNode;
+}
+
+interface State {
+  hasError: boolean;
+  error: Error | null;
+}
+
+class ErrorBoundary extends Component<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { hasError: false, error: null };
+  }
+
+  static getDerivedStateFromError(error: Error): State {
+    return { hasError: true, error };
+  }
+
+  render() {
+    if (this.state.hasError) {
+      return (
+        <div className="flex items-center justify-center min-h-screen p-8 bg-background">
+          <div className="flex flex-col items-center w-full max-w-2xl p-8">
+            <AlertTriangle
+              size={48}
+              className="text-destructive mb-6 flex-shrink-0"
+            />
+
+            <h2 className="text-xl mb-4">An unexpected error occurred.</h2>
+
+            <div className="p-4 w-full rounded bg-muted overflow-auto mb-6">
+              <pre className="text-sm text-muted-foreground whitespace-break-spaces">
+                {this.state.error?.stack}
+              </pre>
+            </div>
+
+            <button
+              onClick={() => window.location.reload()}
+              className={cn(
+                "flex items-center gap-2 px-4 py-2 rounded-lg",
+                "bg-primary text-primary-foreground",
+                "hover:opacity-90 cursor-pointer"
+              )}
+            >
+              <RotateCcw size={16} />
+              Reload Page
+            </button>
+          </div>
+        </div>
+      );
+    }
+
+    return this.props.children;
+  }
+}
+
+export default ErrorBoundary;

+ 89 - 0
client/src/components/ManusDialog.tsx

@@ -0,0 +1,89 @@
+import { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogTitle,
+} from "@/components/ui/dialog";
+
+interface ManusDialogProps {
+  title?: string;
+  logo?: string;
+  open?: boolean;
+  onLogin: () => void;
+  onOpenChange?: (open: boolean) => void;
+  onClose?: () => void;
+}
+
+export function ManusDialog({
+  title,
+  logo,
+  open = false,
+  onLogin,
+  onOpenChange,
+  onClose,
+}: ManusDialogProps) {
+  const [internalOpen, setInternalOpen] = useState(open);
+
+  useEffect(() => {
+    if (!onOpenChange) {
+      setInternalOpen(open);
+    }
+  }, [open, onOpenChange]);
+
+  const handleOpenChange = (nextOpen: boolean) => {
+    if (onOpenChange) {
+      onOpenChange(nextOpen);
+    } else {
+      setInternalOpen(nextOpen);
+    }
+
+    if (!nextOpen) {
+      onClose?.();
+    }
+  };
+
+  return (
+    <Dialog
+      open={onOpenChange ? open : internalOpen}
+      onOpenChange={handleOpenChange}
+    >
+      <DialogContent className="py-5 bg-[#f8f8f7] rounded-[20px] w-[400px] shadow-[0px_4px_11px_0px_rgba(0,0,0,0.08)] border border-[rgba(0,0,0,0.08)] backdrop-blur-2xl p-0 gap-0 text-center">
+        <div className="flex flex-col items-center gap-2 p-5 pt-12">
+          {logo ? (
+            <div className="w-16 h-16 bg-white rounded-xl border border-[rgba(0,0,0,0.08)] flex items-center justify-center">
+              <img
+                src={logo}
+                alt="Dialog graphic"
+                className="w-10 h-10 rounded-md"
+              />
+            </div>
+          ) : null}
+
+          {/* Title and subtitle */}
+          {title ? (
+            <DialogTitle className="text-xl font-semibold text-[#34322d] leading-[26px] tracking-[-0.44px]">
+              {title}
+            </DialogTitle>
+          ) : null}
+          <DialogDescription className="text-sm text-[#858481] leading-5 tracking-[-0.154px]">
+            Please login with Manus to continue
+          </DialogDescription>
+        </div>
+
+        <DialogFooter className="px-5 py-5">
+          {/* Login button */}
+          <Button
+            onClick={onLogin}
+            className="w-full h-10 bg-[#1a1a19] hover:bg-[#1a1a19]/90 text-white rounded-[10px] text-sm font-medium leading-5 tracking-[-0.154px]"
+          >
+            Login with Manus
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 155 - 0
client/src/components/Map.tsx

@@ -0,0 +1,155 @@
+/**
+ * GOOGLE MAPS FRONTEND INTEGRATION - ESSENTIAL GUIDE
+ *
+ * USAGE FROM PARENT COMPONENT:
+ * ======
+ *
+ * const mapRef = useRef<google.maps.Map | null>(null);
+ *
+ * <MapView
+ *   initialCenter={{ lat: 40.7128, lng: -74.0060 }}
+ *   initialZoom={15}
+ *   onMapReady={(map) => {
+ *     mapRef.current = map; // Store to control map from parent anytime, google map itself is in charge of the re-rendering, not react state.
+ * </MapView>
+ *
+ * ======
+ * Available Libraries and Core Features:
+ * -------------------------------
+ * 📍 MARKER (from `marker` library)
+ * - Attaches to map using { map, position }
+ * new google.maps.marker.AdvancedMarkerElement({
+ *   map,
+ *   position: { lat: 37.7749, lng: -122.4194 },
+ *   title: "San Francisco",
+ * });
+ *
+ * -------------------------------
+ * 🏢 PLACES (from `places` library)
+ * - Does not attach directly to map; use data with your map manually.
+ * const place = new google.maps.places.Place({ id: PLACE_ID });
+ * await place.fetchFields({ fields: ["displayName", "location"] });
+ * map.setCenter(place.location);
+ * new google.maps.marker.AdvancedMarkerElement({ map, position: place.location });
+ *
+ * -------------------------------
+ * 🧭 GEOCODER (from `geocoding` library)
+ * - Standalone service; manually apply results to map.
+ * const geocoder = new google.maps.Geocoder();
+ * geocoder.geocode({ address: "New York" }, (results, status) => {
+ *   if (status === "OK" && results[0]) {
+ *     map.setCenter(results[0].geometry.location);
+ *     new google.maps.marker.AdvancedMarkerElement({
+ *       map,
+ *       position: results[0].geometry.location,
+ *     });
+ *   }
+ * });
+ *
+ * -------------------------------
+ * 📐 GEOMETRY (from `geometry` library)
+ * - Pure utility functions; not attached to map.
+ * const dist = google.maps.geometry.spherical.computeDistanceBetween(p1, p2);
+ *
+ * -------------------------------
+ * 🛣️ ROUTES (from `routes` library)
+ * - Combines DirectionsService (standalone) + DirectionsRenderer (map-attached)
+ * const directionsService = new google.maps.DirectionsService();
+ * const directionsRenderer = new google.maps.DirectionsRenderer({ map });
+ * directionsService.route(
+ *   { origin, destination, travelMode: "DRIVING" },
+ *   (res, status) => status === "OK" && directionsRenderer.setDirections(res)
+ * );
+ *
+ * -------------------------------
+ * 🌦️ MAP LAYERS (attach directly to map)
+ * - new google.maps.TrafficLayer().setMap(map);
+ * - new google.maps.TransitLayer().setMap(map);
+ * - new google.maps.BicyclingLayer().setMap(map);
+ *
+ * -------------------------------
+ * ✅ SUMMARY
+ * - “map-attached” → AdvancedMarkerElement, DirectionsRenderer, Layers.
+ * - “standalone” → Geocoder, DirectionsService, DistanceMatrixService, ElevationService.
+ * - “data-only” → Place, Geometry utilities.
+ */
+
+/// <reference types="@types/google.maps" />
+
+import { useEffect, useRef } from "react";
+import { usePersistFn } from "@/hooks/usePersistFn";
+import { cn } from "@/lib/utils";
+
+declare global {
+  interface Window {
+    google?: typeof google;
+  }
+}
+
+const API_KEY = import.meta.env.VITE_FRONTEND_FORGE_API_KEY;
+const FORGE_BASE_URL =
+  import.meta.env.VITE_FRONTEND_FORGE_API_URL ||
+  "https://forge.butterfly-effect.dev";
+const MAPS_PROXY_URL = `${FORGE_BASE_URL}/v1/maps/proxy`;
+
+function loadMapScript() {
+  return new Promise(resolve => {
+    const script = document.createElement("script");
+    script.src = `${MAPS_PROXY_URL}/maps/api/js?key=${API_KEY}&v=weekly&libraries=marker,places,geocoding,geometry`;
+    script.async = true;
+    script.crossOrigin = "anonymous";
+    script.onload = () => {
+      resolve(null);
+      script.remove(); // Clean up immediately
+    };
+    script.onerror = () => {
+      console.error("Failed to load Google Maps script");
+    };
+    document.head.appendChild(script);
+  });
+}
+
+interface MapViewProps {
+  className?: string;
+  initialCenter?: google.maps.LatLngLiteral;
+  initialZoom?: number;
+  onMapReady?: (map: google.maps.Map) => void;
+}
+
+export function MapView({
+  className,
+  initialCenter = { lat: 37.7749, lng: -122.4194 },
+  initialZoom = 12,
+  onMapReady,
+}: MapViewProps) {
+  const mapContainer = useRef<HTMLDivElement>(null);
+  const map = useRef<google.maps.Map | null>(null);
+
+  const init = usePersistFn(async () => {
+    await loadMapScript();
+    if (!mapContainer.current) {
+      console.error("Map container not found");
+      return;
+    }
+    map.current = new window.google.maps.Map(mapContainer.current, {
+      zoom: initialZoom,
+      center: initialCenter,
+      mapTypeControl: true,
+      fullscreenControl: true,
+      zoomControl: true,
+      streetViewControl: true,
+      mapId: "DEMO_MAP_ID",
+    });
+    if (onMapReady) {
+      onMapReady(map.current);
+    }
+  });
+
+  useEffect(() => {
+    init();
+  }, [init]);
+
+  return (
+    <div ref={mapContainer} className={cn("w-full h-[500px]", className)} />
+  );
+}

+ 254 - 0
client/src/components/WorkflowDiagram.tsx

@@ -0,0 +1,254 @@
+/**
+ * WorkflowDiagram — Interactive chatbot conversation flow diagram
+ * Design: Warm Showroom — forest green nodes, terracotta accents, cream background
+ */
+import { useState } from "react";
+import { motion } from "framer-motion";
+import {
+  MessageCircle,
+  ShoppingBag,
+  MapPin,
+  Package,
+  Shield,
+  Users,
+  Bot,
+  ArrowRight,
+  Sparkles,
+} from "lucide-react";
+
+interface FlowNode {
+  id: string;
+  label: string;
+  description: string;
+  icon: React.ReactNode;
+  color: string;
+  children?: string[];
+}
+
+const FLOW_NODES: FlowNode[] = [
+  {
+    id: "entry",
+    label: "User Visits Website",
+    description: "A visitor lands on homelegance.com. The chatbot widget appears in the bottom-right corner after a short delay, with a gentle greeting animation.",
+    icon: <MessageCircle className="w-5 h-5" />,
+    color: "#14532D",
+    children: ["greeting"],
+  },
+  {
+    id: "greeting",
+    label: "AI Greeting & Intent Detection",
+    description: "The chatbot sends a warm welcome message and presents quick-action buttons. Natural Language Processing (NLP) analyzes the user's first message to detect intent.",
+    icon: <Bot className="w-5 h-5" />,
+    color: "#14532D",
+    children: ["products", "dealer", "order", "warranty", "sales"],
+  },
+  {
+    id: "products",
+    label: "Product Discovery",
+    description: "Guides users through furniture categories (Bedroom, Dining, Seating, etc.), shows best sellers, and helps filter by collection, style, or price range. Can display product images and specs inline.",
+    icon: <ShoppingBag className="w-5 h-5" />,
+    color: "#C2410C",
+  },
+  {
+    id: "dealer",
+    label: "Dealer Locator",
+    description: "Asks for zip code or city, then queries the dealer database to find authorized Homelegance retailers nearby. Shows results with distance, contact info, and directions link.",
+    icon: <MapPin className="w-5 h-5" />,
+    color: "#C2410C",
+  },
+  {
+    id: "order",
+    label: "Order Status Tracking",
+    description: "Authenticates the dealer/retailer via order number or account email, then retrieves real-time order status from the fulfillment system. Shows shipping tracking when available.",
+    icon: <Package className="w-5 h-5" />,
+    color: "#C2410C",
+  },
+  {
+    id: "warranty",
+    label: "Parts & Warranty",
+    description: "Walks users through the warranty claim process: collects product info, purchase date, issue description, and photos. Creates a support ticket automatically.",
+    icon: <Shield className="w-5 h-5" />,
+    color: "#C2410C",
+  },
+  {
+    id: "sales",
+    label: "Live Agent Handoff",
+    description: "When the chatbot detects complex queries or the user requests human help, it seamlessly transfers the conversation to a live sales representative with full context.",
+    icon: <Users className="w-5 h-5" />,
+    color: "#C2410C",
+  },
+];
+
+export default function WorkflowDiagram() {
+  const [activeNode, setActiveNode] = useState<string>("entry");
+  const active = FLOW_NODES.find((n) => n.id === activeNode);
+
+  return (
+    <div className="w-full">
+      {/* Flow visualization */}
+      <div className="relative">
+        {/* Entry node */}
+        <div className="flex justify-center mb-6">
+          <FlowNodeCard
+            node={FLOW_NODES[0]}
+            isActive={activeNode === "entry"}
+            onClick={() => setActiveNode("entry")}
+          />
+        </div>
+
+        {/* Connector line down */}
+        <div className="flex justify-center mb-2">
+          <div className="w-0.5 h-8" style={{ background: "#14532D" }} />
+        </div>
+
+        {/* AI Processing node */}
+        <div className="flex justify-center mb-6">
+          <FlowNodeCard
+            node={FLOW_NODES[1]}
+            isActive={activeNode === "greeting"}
+            onClick={() => setActiveNode("greeting")}
+          />
+        </div>
+
+        {/* Branching connector */}
+        <div className="flex justify-center mb-2">
+          <div className="relative w-full max-w-3xl h-8">
+            <div className="absolute left-1/2 top-0 w-0.5 h-4 -translate-x-1/2" style={{ background: "#14532D" }} />
+            <div className="absolute top-4 left-[10%] right-[10%] h-0.5" style={{ background: "#14532D" }} />
+            {[10, 28, 50, 72, 90].map((pos) => (
+              <div
+                key={pos}
+                className="absolute top-4 w-0.5 h-4"
+                style={{ left: `${pos}%`, transform: "translateX(-50%)", background: "#14532D" }}
+              />
+            ))}
+          </div>
+        </div>
+
+        {/* Branch nodes */}
+        <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
+          {FLOW_NODES.slice(2).map((node) => (
+            <FlowNodeCard
+              key={node.id}
+              node={node}
+              isActive={activeNode === node.id}
+              onClick={() => setActiveNode(node.id)}
+              compact
+            />
+          ))}
+        </div>
+      </div>
+
+      {/* Detail panel */}
+      <motion.div
+        key={activeNode}
+        initial={{ opacity: 0, y: 10 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.3 }}
+        className="mt-8 p-6 rounded-xl"
+        style={{
+          background: "#fff",
+          boxShadow: "0 4px 20px rgba(120, 113, 108, 0.1)",
+          border: "1px solid #e7e0d5",
+        }}
+      >
+        <div className="flex items-start gap-4">
+          <div
+            className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0"
+            style={{ background: active?.color + "15", color: active?.color }}
+          >
+            {active?.icon}
+          </div>
+          <div>
+            <h4
+              className="text-lg font-semibold mb-2"
+              style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+            >
+              {active?.label}
+            </h4>
+            <p className="text-sm leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+              {active?.description}
+            </p>
+            {active?.children && (
+              <div className="flex flex-wrap gap-2 mt-3">
+                <span className="text-xs font-medium" style={{ color: "#a8a29e" }}>
+                  Leads to:
+                </span>
+                {active.children.map((childId) => {
+                  const child = FLOW_NODES.find((n) => n.id === childId);
+                  return (
+                    <button
+                      key={childId}
+                      onClick={() => setActiveNode(childId)}
+                      className="flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium transition-colors"
+                      style={{
+                        background: "rgba(194, 65, 12, 0.08)",
+                        color: "#C2410C",
+                      }}
+                    >
+                      {child?.label}
+                      <ArrowRight className="w-3 h-3" />
+                    </button>
+                  );
+                })}
+              </div>
+            )}
+          </div>
+        </div>
+      </motion.div>
+    </div>
+  );
+}
+
+function FlowNodeCard({
+  node,
+  isActive,
+  onClick,
+  compact,
+}: {
+  node: FlowNode;
+  isActive: boolean;
+  onClick: () => void;
+  compact?: boolean;
+}) {
+  return (
+    <motion.button
+      whileHover={{ scale: 1.03, y: -2 }}
+      whileTap={{ scale: 0.98 }}
+      onClick={onClick}
+      className={`relative rounded-xl transition-all ${compact ? "p-3 w-full" : "px-6 py-4"}`}
+      style={{
+        background: isActive ? node.color : "#fff",
+        color: isActive ? "#fff" : node.color,
+        boxShadow: isActive
+          ? `0 8px 24px ${node.color}30`
+          : "0 2px 8px rgba(120, 113, 108, 0.1)",
+        border: `1.5px solid ${isActive ? node.color : "#e7e0d5"}`,
+      }}
+    >
+      <div className={`flex items-center ${compact ? "flex-col gap-2" : "gap-3"}`}>
+        <div
+          className={`${compact ? "w-9 h-9" : "w-10 h-10"} rounded-lg flex items-center justify-center`}
+          style={{
+            background: isActive ? "rgba(255,255,255,0.2)" : node.color + "12",
+          }}
+        >
+          {node.icon}
+        </div>
+        <span
+          className={`font-semibold ${compact ? "text-xs text-center leading-tight" : "text-sm"}`}
+          style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          {node.label}
+        </span>
+      </div>
+      {isActive && (
+        <motion.div
+          layoutId="activeIndicator"
+          className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full"
+          style={{ background: "#C2410C" }}
+        />
+      )}
+    </motion.button>
+  );
+}

+ 64 - 0
client/src/components/ui/accordion.tsx

@@ -0,0 +1,64 @@
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDownIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Accordion({
+  ...props
+}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
+  return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
+}
+
+function AccordionItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
+  return (
+    <AccordionPrimitive.Item
+      data-slot="accordion-item"
+      className={cn("border-b last:border-b-0", className)}
+      {...props}
+    />
+  );
+}
+
+function AccordionTrigger({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
+  return (
+    <AccordionPrimitive.Header className="flex">
+      <AccordionPrimitive.Trigger
+        data-slot="accordion-trigger"
+        className={cn(
+          "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
+      </AccordionPrimitive.Trigger>
+    </AccordionPrimitive.Header>
+  );
+}
+
+function AccordionContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
+  return (
+    <AccordionPrimitive.Content
+      data-slot="accordion-content"
+      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
+      {...props}
+    >
+      <div className={cn("pt-0 pb-4", className)}>{children}</div>
+    </AccordionPrimitive.Content>
+  );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

+ 155 - 0
client/src/components/ui/alert-dialog.tsx

@@ -0,0 +1,155 @@
+import * as React from "react";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+function AlertDialog({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
+}
+
+function AlertDialogTrigger({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  );
+}
+
+function AlertDialogPortal({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  );
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
+  return (
+    <AlertDialogPrimitive.Overlay
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Content
+        data-slot="alert-dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  );
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn("text-lg font-semibold", className)}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
+  return (
+    <AlertDialogPrimitive.Action
+      className={cn(buttonVariants(), className)}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogCancel({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
+  return (
+    <AlertDialogPrimitive.Cancel
+      className={cn(buttonVariants({ variant: "outline" }), className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  AlertDialog,
+  AlertDialogPortal,
+  AlertDialogOverlay,
+  AlertDialogTrigger,
+  AlertDialogContent,
+  AlertDialogHeader,
+  AlertDialogFooter,
+  AlertDialogTitle,
+  AlertDialogDescription,
+  AlertDialogAction,
+  AlertDialogCancel,
+};

+ 66 - 0
client/src/components/ui/alert.tsx

@@ -0,0 +1,66 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+  "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+  {
+    variants: {
+      variant: {
+        default: "bg-card text-card-foreground",
+        destructive:
+          "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+);
+
+function Alert({
+  className,
+  variant,
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
+  return (
+    <div
+      data-slot="alert"
+      role="alert"
+      className={cn(alertVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-title"
+      className={cn(
+        "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDescription({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-description"
+      className={cn(
+        "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Alert, AlertTitle, AlertDescription };

+ 9 - 0
client/src/components/ui/aspect-ratio.tsx

@@ -0,0 +1,9 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
+
+function AspectRatio({
+  ...props
+}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
+  return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
+}
+
+export { AspectRatio };

+ 51 - 0
client/src/components/ui/avatar.tsx

@@ -0,0 +1,51 @@
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "@/lib/utils";
+
+function Avatar({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
+  return (
+    <AvatarPrimitive.Root
+      data-slot="avatar"
+      className={cn(
+        "relative flex size-8 shrink-0 overflow-hidden rounded-full",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarImage({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
+  return (
+    <AvatarPrimitive.Image
+      data-slot="avatar-image"
+      className={cn("aspect-square size-full", className)}
+      {...props}
+    />
+  );
+}
+
+function AvatarFallback({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
+  return (
+    <AvatarPrimitive.Fallback
+      data-slot="avatar-fallback"
+      className={cn(
+        "bg-muted flex size-full items-center justify-center rounded-full",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Avatar, AvatarImage, AvatarFallback };

+ 46 - 0
client/src/components/ui/badge.tsx

@@ -0,0 +1,46 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+  "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+  {
+    variants: {
+      variant: {
+        default:
+          "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+        secondary:
+          "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+        destructive:
+          "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+);
+
+function Badge({
+  className,
+  variant,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"span"> &
+  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "span";
+
+  return (
+    <Comp
+      data-slot="badge"
+      className={cn(badgeVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+export { Badge, badgeVariants };

+ 109 - 0
client/src/components/ui/breadcrumb.tsx

@@ -0,0 +1,109 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+  return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+  return (
+    <ol
+      data-slot="breadcrumb-list"
+      className={cn(
+        "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="breadcrumb-item"
+      className={cn("inline-flex items-center gap-1.5", className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbLink({
+  asChild,
+  className,
+  ...props
+}: React.ComponentProps<"a"> & {
+  asChild?: boolean;
+}) {
+  const Comp = asChild ? Slot : "a";
+
+  return (
+    <Comp
+      data-slot="breadcrumb-link"
+      className={cn("hover:text-foreground transition-colors", className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="breadcrumb-page"
+      role="link"
+      aria-disabled="true"
+      aria-current="page"
+      className={cn("text-foreground font-normal", className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbSeparator({
+  children,
+  className,
+  ...props
+}: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="breadcrumb-separator"
+      role="presentation"
+      aria-hidden="true"
+      className={cn("[&>svg]:size-3.5", className)}
+      {...props}
+    >
+      {children ?? <ChevronRight />}
+    </li>
+  );
+}
+
+function BreadcrumbEllipsis({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="breadcrumb-ellipsis"
+      role="presentation"
+      aria-hidden="true"
+      className={cn("flex size-9 items-center justify-center", className)}
+      {...props}
+    >
+      <MoreHorizontal className="size-4" />
+      <span className="sr-only">More</span>
+    </span>
+  );
+}
+
+export {
+  Breadcrumb,
+  BreadcrumbList,
+  BreadcrumbItem,
+  BreadcrumbLink,
+  BreadcrumbPage,
+  BreadcrumbSeparator,
+  BreadcrumbEllipsis,
+};

+ 83 - 0
client/src/components/ui/button-group.tsx

@@ -0,0 +1,83 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
+
+const buttonGroupVariants = cva(
+  "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+  {
+    variants: {
+      orientation: {
+        horizontal:
+          "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+        vertical:
+          "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+      },
+    },
+    defaultVariants: {
+      orientation: "horizontal",
+    },
+  }
+);
+
+function ButtonGroup({
+  className,
+  orientation,
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="button-group"
+      data-orientation={orientation}
+      className={cn(buttonGroupVariants({ orientation }), className)}
+      {...props}
+    />
+  );
+}
+
+function ButtonGroupText({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"div"> & {
+  asChild?: boolean;
+}) {
+  const Comp = asChild ? Slot : "div";
+
+  return (
+    <Comp
+      className={cn(
+        "bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ButtonGroupSeparator({
+  className,
+  orientation = "vertical",
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="button-group-separator"
+      orientation={orientation}
+      className={cn(
+        "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  ButtonGroup,
+  ButtonGroupSeparator,
+  ButtonGroupText,
+  buttonGroupVariants,
+};

+ 60 - 0
client/src/components/ui/button.tsx

@@ -0,0 +1,60 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default: "bg-primary text-primary-foreground hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
+        secondary:
+          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+        ghost:
+          "hover:bg-accent dark:hover:bg-accent/50",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
+        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+        icon: "size-9",
+        "icon-sm": "size-8",
+        "icon-lg": "size-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+);
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean;
+  }) {
+  const Comp = asChild ? Slot : "button";
+
+  return (
+    <Comp
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+}
+
+export { Button, buttonVariants };

+ 211 - 0
client/src/components/ui/calendar.tsx

@@ -0,0 +1,211 @@
+import * as React from "react";
+import {
+  ChevronDownIcon,
+  ChevronLeftIcon,
+  ChevronRightIcon,
+} from "lucide-react";
+import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
+
+import { cn } from "@/lib/utils";
+import { Button, buttonVariants } from "@/components/ui/button";
+
+function Calendar({
+  className,
+  classNames,
+  showOutsideDays = true,
+  captionLayout = "label",
+  buttonVariant = "ghost",
+  formatters,
+  components,
+  ...props
+}: React.ComponentProps<typeof DayPicker> & {
+  buttonVariant?: React.ComponentProps<typeof Button>["variant"];
+}) {
+  const defaultClassNames = getDefaultClassNames();
+
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn(
+        "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
+        String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
+        String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+        className
+      )}
+      captionLayout={captionLayout}
+      formatters={{
+        formatMonthDropdown: date =>
+          date.toLocaleString("default", { month: "short" }),
+        ...formatters,
+      }}
+      classNames={{
+        root: cn("w-fit", defaultClassNames.root),
+        months: cn(
+          "flex gap-4 flex-col md:flex-row relative",
+          defaultClassNames.months
+        ),
+        month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
+        nav: cn(
+          "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
+          defaultClassNames.nav
+        ),
+        button_previous: cn(
+          buttonVariants({ variant: buttonVariant }),
+          "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+          defaultClassNames.button_previous
+        ),
+        button_next: cn(
+          buttonVariants({ variant: buttonVariant }),
+          "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+          defaultClassNames.button_next
+        ),
+        month_caption: cn(
+          "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
+          defaultClassNames.month_caption
+        ),
+        dropdowns: cn(
+          "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
+          defaultClassNames.dropdowns
+        ),
+        dropdown_root: cn(
+          "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
+          defaultClassNames.dropdown_root
+        ),
+        dropdown: cn(
+          "absolute bg-popover inset-0 opacity-0",
+          defaultClassNames.dropdown
+        ),
+        caption_label: cn(
+          "select-none font-medium",
+          captionLayout === "label"
+            ? "text-sm"
+            : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
+          defaultClassNames.caption_label
+        ),
+        table: "w-full border-collapse",
+        weekdays: cn("flex", defaultClassNames.weekdays),
+        weekday: cn(
+          "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
+          defaultClassNames.weekday
+        ),
+        week: cn("flex w-full mt-2", defaultClassNames.week),
+        week_number_header: cn(
+          "select-none w-(--cell-size)",
+          defaultClassNames.week_number_header
+        ),
+        week_number: cn(
+          "text-[0.8rem] select-none text-muted-foreground",
+          defaultClassNames.week_number
+        ),
+        day: cn(
+          "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
+          defaultClassNames.day
+        ),
+        range_start: cn(
+          "rounded-l-md bg-accent",
+          defaultClassNames.range_start
+        ),
+        range_middle: cn("rounded-none", defaultClassNames.range_middle),
+        range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
+        today: cn(
+          "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+          defaultClassNames.today
+        ),
+        outside: cn(
+          "text-muted-foreground aria-selected:text-muted-foreground",
+          defaultClassNames.outside
+        ),
+        disabled: cn(
+          "text-muted-foreground opacity-50",
+          defaultClassNames.disabled
+        ),
+        hidden: cn("invisible", defaultClassNames.hidden),
+        ...classNames,
+      }}
+      components={{
+        Root: ({ className, rootRef, ...props }) => {
+          return (
+            <div
+              data-slot="calendar"
+              ref={rootRef}
+              className={cn(className)}
+              {...props}
+            />
+          );
+        },
+        Chevron: ({ className, orientation, ...props }) => {
+          if (orientation === "left") {
+            return (
+              <ChevronLeftIcon className={cn("size-4", className)} {...props} />
+            );
+          }
+
+          if (orientation === "right") {
+            return (
+              <ChevronRightIcon
+                className={cn("size-4", className)}
+                {...props}
+              />
+            );
+          }
+
+          return (
+            <ChevronDownIcon className={cn("size-4", className)} {...props} />
+          );
+        },
+        DayButton: CalendarDayButton,
+        WeekNumber: ({ children, ...props }) => {
+          return (
+            <td {...props}>
+              <div className="flex size-(--cell-size) items-center justify-center text-center">
+                {children}
+              </div>
+            </td>
+          );
+        },
+        ...components,
+      }}
+      {...props}
+    />
+  );
+}
+
+function CalendarDayButton({
+  className,
+  day,
+  modifiers,
+  ...props
+}: React.ComponentProps<typeof DayButton>) {
+  const defaultClassNames = getDefaultClassNames();
+
+  const ref = React.useRef<HTMLButtonElement>(null);
+  React.useEffect(() => {
+    if (modifiers.focused) ref.current?.focus();
+  }, [modifiers.focused]);
+
+  return (
+    <Button
+      ref={ref}
+      variant="ghost"
+      size="icon"
+      data-day={day.date.toLocaleDateString()}
+      data-selected-single={
+        modifiers.selected &&
+        !modifiers.range_start &&
+        !modifiers.range_end &&
+        !modifiers.range_middle
+      }
+      data-range-start={modifiers.range_start}
+      data-range-end={modifiers.range_end}
+      data-range-middle={modifiers.range_middle}
+      className={cn(
+        "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
+        defaultClassNames.day,
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Calendar, CalendarDayButton };

+ 92 - 0
client/src/components/ui/card.tsx

@@ -0,0 +1,92 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card"
+      className={cn(
+        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-header"
+      className={cn(
+        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-title"
+      className={cn("leading-none font-semibold", className)}
+      {...props}
+    />
+  );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-action"
+      className={cn(
+        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-content"
+      className={cn("px-6", className)}
+      {...props}
+    />
+  );
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-footer"
+      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Card,
+  CardHeader,
+  CardFooter,
+  CardTitle,
+  CardAction,
+  CardDescription,
+  CardContent,
+};

+ 239 - 0
client/src/components/ui/carousel.tsx

@@ -0,0 +1,239 @@
+import * as React from "react";
+import useEmblaCarousel, {
+  type UseEmblaCarouselType,
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+  opts?: CarouselOptions;
+  plugins?: CarouselPlugin;
+  orientation?: "horizontal" | "vertical";
+  setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+  carouselRef: ReturnType<typeof useEmblaCarousel>[0];
+  api: ReturnType<typeof useEmblaCarousel>[1];
+  scrollPrev: () => void;
+  scrollNext: () => void;
+  canScrollPrev: boolean;
+  canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext<CarouselContextProps | null>(null);
+
+function useCarousel() {
+  const context = React.useContext(CarouselContext);
+
+  if (!context) {
+    throw new Error("useCarousel must be used within a <Carousel />");
+  }
+
+  return context;
+}
+
+function Carousel({
+  orientation = "horizontal",
+  opts,
+  setApi,
+  plugins,
+  className,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+  const [carouselRef, api] = useEmblaCarousel(
+    {
+      ...opts,
+      axis: orientation === "horizontal" ? "x" : "y",
+    },
+    plugins
+  );
+  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+  const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+  const onSelect = React.useCallback((api: CarouselApi) => {
+    if (!api) return;
+    setCanScrollPrev(api.canScrollPrev());
+    setCanScrollNext(api.canScrollNext());
+  }, []);
+
+  const scrollPrev = React.useCallback(() => {
+    api?.scrollPrev();
+  }, [api]);
+
+  const scrollNext = React.useCallback(() => {
+    api?.scrollNext();
+  }, [api]);
+
+  const handleKeyDown = React.useCallback(
+    (event: React.KeyboardEvent<HTMLDivElement>) => {
+      if (event.key === "ArrowLeft") {
+        event.preventDefault();
+        scrollPrev();
+      } else if (event.key === "ArrowRight") {
+        event.preventDefault();
+        scrollNext();
+      }
+    },
+    [scrollPrev, scrollNext]
+  );
+
+  React.useEffect(() => {
+    if (!api || !setApi) return;
+    setApi(api);
+  }, [api, setApi]);
+
+  React.useEffect(() => {
+    if (!api) return;
+    onSelect(api);
+    api.on("reInit", onSelect);
+    api.on("select", onSelect);
+
+    return () => {
+      api?.off("select", onSelect);
+    };
+  }, [api, onSelect]);
+
+  return (
+    <CarouselContext.Provider
+      value={{
+        carouselRef,
+        api: api,
+        opts,
+        orientation:
+          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+        scrollPrev,
+        scrollNext,
+        canScrollPrev,
+        canScrollNext,
+      }}
+    >
+      <div
+        onKeyDownCapture={handleKeyDown}
+        className={cn("relative", className)}
+        role="region"
+        aria-roledescription="carousel"
+        data-slot="carousel"
+        {...props}
+      >
+        {children}
+      </div>
+    </CarouselContext.Provider>
+  );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+  const { carouselRef, orientation } = useCarousel();
+
+  return (
+    <div
+      ref={carouselRef}
+      className="overflow-hidden"
+      data-slot="carousel-content"
+    >
+      <div
+        className={cn(
+          "flex",
+          orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
+          className
+        )}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+  const { orientation } = useCarousel();
+
+  return (
+    <div
+      role="group"
+      aria-roledescription="slide"
+      data-slot="carousel-item"
+      className={cn(
+        "min-w-0 shrink-0 grow-0 basis-full",
+        orientation === "horizontal" ? "pl-4" : "pt-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CarouselPrevious({
+  className,
+  variant = "outline",
+  size = "icon",
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+  return (
+    <Button
+      data-slot="carousel-previous"
+      variant={variant}
+      size={size}
+      className={cn(
+        "absolute size-8 rounded-full",
+        orientation === "horizontal"
+          ? "top-1/2 -left-12 -translate-y-1/2"
+          : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
+        className
+      )}
+      disabled={!canScrollPrev}
+      onClick={scrollPrev}
+      {...props}
+    >
+      <ArrowLeft />
+      <span className="sr-only">Previous slide</span>
+    </Button>
+  );
+}
+
+function CarouselNext({
+  className,
+  variant = "outline",
+  size = "icon",
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+  return (
+    <Button
+      data-slot="carousel-next"
+      variant={variant}
+      size={size}
+      className={cn(
+        "absolute size-8 rounded-full",
+        orientation === "horizontal"
+          ? "top-1/2 -right-12 -translate-y-1/2"
+          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
+        className
+      )}
+      disabled={!canScrollNext}
+      onClick={scrollNext}
+      {...props}
+    >
+      <ArrowRight />
+      <span className="sr-only">Next slide</span>
+    </Button>
+  );
+}
+
+export {
+  type CarouselApi,
+  Carousel,
+  CarouselContent,
+  CarouselItem,
+  CarouselPrevious,
+  CarouselNext,
+};

+ 355 - 0
client/src/components/ui/chart.tsx

@@ -0,0 +1,355 @@
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+  [k in string]: {
+    label?: React.ReactNode;
+    icon?: React.ComponentType;
+  } & (
+    | { color?: string; theme?: never }
+    | { color?: never; theme: Record<keyof typeof THEMES, string> }
+  );
+};
+
+type ChartContextProps = {
+  config: ChartConfig;
+};
+
+const ChartContext = React.createContext<ChartContextProps | null>(null);
+
+function useChart() {
+  const context = React.useContext(ChartContext);
+
+  if (!context) {
+    throw new Error("useChart must be used within a <ChartContainer />");
+  }
+
+  return context;
+}
+
+function ChartContainer({
+  id,
+  className,
+  children,
+  config,
+  ...props
+}: React.ComponentProps<"div"> & {
+  config: ChartConfig;
+  children: React.ComponentProps<
+    typeof RechartsPrimitive.ResponsiveContainer
+  >["children"];
+}) {
+  const uniqueId = React.useId();
+  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+  return (
+    <ChartContext.Provider value={{ config }}>
+      <div
+        data-slot="chart"
+        data-chart={chartId}
+        className={cn(
+          "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
+          className
+        )}
+        {...props}
+      >
+        <ChartStyle id={chartId} config={config} />
+        <RechartsPrimitive.ResponsiveContainer>
+          {children}
+        </RechartsPrimitive.ResponsiveContainer>
+      </div>
+    </ChartContext.Provider>
+  );
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+  const colorConfig = Object.entries(config).filter(
+    ([, config]) => config.theme || config.color
+  );
+
+  if (!colorConfig.length) {
+    return null;
+  }
+
+  return (
+    <style
+      dangerouslySetInnerHTML={{
+        __html: Object.entries(THEMES)
+          .map(
+            ([theme, prefix]) => `
+${prefix} [data-chart=${id}] {
+${colorConfig
+  .map(([key, itemConfig]) => {
+    const color =
+      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
+      itemConfig.color;
+    return color ? `  --color-${key}: ${color};` : null;
+  })
+  .join("\n")}
+}
+`
+          )
+          .join("\n"),
+      }}
+    />
+  );
+};
+
+const ChartTooltip = RechartsPrimitive.Tooltip;
+
+function ChartTooltipContent({
+  active,
+  payload,
+  className,
+  indicator = "dot",
+  hideLabel = false,
+  hideIndicator = false,
+  label,
+  labelFormatter,
+  labelClassName,
+  formatter,
+  color,
+  nameKey,
+  labelKey,
+}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
+  React.ComponentProps<"div"> & {
+    hideLabel?: boolean;
+    hideIndicator?: boolean;
+    indicator?: "line" | "dot" | "dashed";
+    nameKey?: string;
+    labelKey?: string;
+  }) {
+  const { config } = useChart();
+
+  const tooltipLabel = React.useMemo(() => {
+    if (hideLabel || !payload?.length) {
+      return null;
+    }
+
+    const [item] = payload;
+    const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
+    const itemConfig = getPayloadConfigFromPayload(config, item, key);
+    const value =
+      !labelKey && typeof label === "string"
+        ? config[label as keyof typeof config]?.label || label
+        : itemConfig?.label;
+
+    if (labelFormatter) {
+      return (
+        <div className={cn("font-medium", labelClassName)}>
+          {labelFormatter(value, payload)}
+        </div>
+      );
+    }
+
+    if (!value) {
+      return null;
+    }
+
+    return <div className={cn("font-medium", labelClassName)}>{value}</div>;
+  }, [
+    label,
+    labelFormatter,
+    payload,
+    hideLabel,
+    labelClassName,
+    config,
+    labelKey,
+  ]);
+
+  if (!active || !payload?.length) {
+    return null;
+  }
+
+  const nestLabel = payload.length === 1 && indicator !== "dot";
+
+  return (
+    <div
+      className={cn(
+        "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
+        className
+      )}
+    >
+      {!nestLabel ? tooltipLabel : null}
+      <div className="grid gap-1.5">
+        {payload
+          .filter(item => item.type !== "none")
+          .map((item, index) => {
+            const key = `${nameKey || item.name || item.dataKey || "value"}`;
+            const itemConfig = getPayloadConfigFromPayload(config, item, key);
+            const indicatorColor = color || item.payload.fill || item.color;
+
+            return (
+              <div
+                key={item.dataKey}
+                className={cn(
+                  "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
+                  indicator === "dot" && "items-center"
+                )}
+              >
+                {formatter && item?.value !== undefined && item.name ? (
+                  formatter(item.value, item.name, item, index, item.payload)
+                ) : (
+                  <>
+                    {itemConfig?.icon ? (
+                      <itemConfig.icon />
+                    ) : (
+                      !hideIndicator && (
+                        <div
+                          className={cn(
+                            "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
+                            {
+                              "h-2.5 w-2.5": indicator === "dot",
+                              "w-1": indicator === "line",
+                              "w-0 border-[1.5px] border-dashed bg-transparent":
+                                indicator === "dashed",
+                              "my-0.5": nestLabel && indicator === "dashed",
+                            }
+                          )}
+                          style={
+                            {
+                              "--color-bg": indicatorColor,
+                              "--color-border": indicatorColor,
+                            } as React.CSSProperties
+                          }
+                        />
+                      )
+                    )}
+                    <div
+                      className={cn(
+                        "flex flex-1 justify-between leading-none",
+                        nestLabel ? "items-end" : "items-center"
+                      )}
+                    >
+                      <div className="grid gap-1.5">
+                        {nestLabel ? tooltipLabel : null}
+                        <span className="text-muted-foreground">
+                          {itemConfig?.label || item.name}
+                        </span>
+                      </div>
+                      {item.value && (
+                        <span className="text-foreground font-mono font-medium tabular-nums">
+                          {item.value.toLocaleString()}
+                        </span>
+                      )}
+                    </div>
+                  </>
+                )}
+              </div>
+            );
+          })}
+      </div>
+    </div>
+  );
+}
+
+const ChartLegend = RechartsPrimitive.Legend;
+
+function ChartLegendContent({
+  className,
+  hideIcon = false,
+  payload,
+  verticalAlign = "bottom",
+  nameKey,
+}: React.ComponentProps<"div"> &
+  Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
+    hideIcon?: boolean;
+    nameKey?: string;
+  }) {
+  const { config } = useChart();
+
+  if (!payload?.length) {
+    return null;
+  }
+
+  return (
+    <div
+      className={cn(
+        "flex items-center justify-center gap-4",
+        verticalAlign === "top" ? "pb-3" : "pt-3",
+        className
+      )}
+    >
+      {payload
+        .filter(item => item.type !== "none")
+        .map(item => {
+          const key = `${nameKey || item.dataKey || "value"}`;
+          const itemConfig = getPayloadConfigFromPayload(config, item, key);
+
+          return (
+            <div
+              key={item.value}
+              className={cn(
+                "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
+              )}
+            >
+              {itemConfig?.icon && !hideIcon ? (
+                <itemConfig.icon />
+              ) : (
+                <div
+                  className="h-2 w-2 shrink-0 rounded-[2px]"
+                  style={{
+                    backgroundColor: item.color,
+                  }}
+                />
+              )}
+              {itemConfig?.label}
+            </div>
+          );
+        })}
+    </div>
+  );
+}
+
+// Helper to extract item config from a payload.
+function getPayloadConfigFromPayload(
+  config: ChartConfig,
+  payload: unknown,
+  key: string
+) {
+  if (typeof payload !== "object" || payload === null) {
+    return undefined;
+  }
+
+  const payloadPayload =
+    "payload" in payload &&
+    typeof payload.payload === "object" &&
+    payload.payload !== null
+      ? payload.payload
+      : undefined;
+
+  let configLabelKey: string = key;
+
+  if (
+    key in payload &&
+    typeof payload[key as keyof typeof payload] === "string"
+  ) {
+    configLabelKey = payload[key as keyof typeof payload] as string;
+  } else if (
+    payloadPayload &&
+    key in payloadPayload &&
+    typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
+  ) {
+    configLabelKey = payloadPayload[
+      key as keyof typeof payloadPayload
+    ] as string;
+  }
+
+  return configLabelKey in config
+    ? config[configLabelKey]
+    : config[key as keyof typeof config];
+}
+
+export {
+  ChartContainer,
+  ChartTooltip,
+  ChartTooltipContent,
+  ChartLegend,
+  ChartLegendContent,
+  ChartStyle,
+};

+ 30 - 0
client/src/components/ui/checkbox.tsx

@@ -0,0 +1,30 @@
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { CheckIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Checkbox({
+  className,
+  ...props
+}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
+  return (
+    <CheckboxPrimitive.Root
+      data-slot="checkbox"
+      className={cn(
+        "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+        className
+      )}
+      {...props}
+    >
+      <CheckboxPrimitive.Indicator
+        data-slot="checkbox-indicator"
+        className="flex items-center justify-center text-current transition-none"
+      >
+        <CheckIcon className="size-3.5" />
+      </CheckboxPrimitive.Indicator>
+    </CheckboxPrimitive.Root>
+  );
+}
+
+export { Checkbox };

+ 31 - 0
client/src/components/ui/collapsible.tsx

@@ -0,0 +1,31 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
+
+function Collapsible({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
+  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
+}
+
+function CollapsibleTrigger({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
+  return (
+    <CollapsiblePrimitive.CollapsibleTrigger
+      data-slot="collapsible-trigger"
+      {...props}
+    />
+  );
+}
+
+function CollapsibleContent({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
+  return (
+    <CollapsiblePrimitive.CollapsibleContent
+      data-slot="collapsible-content"
+      {...props}
+    />
+  );
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };

+ 184 - 0
client/src/components/ui/command.tsx

@@ -0,0 +1,184 @@
+"use client";
+
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { SearchIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+
+function Command({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive>) {
+  return (
+    <CommandPrimitive
+      data-slot="command"
+      className={cn(
+        "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandDialog({
+  title = "Command Palette",
+  description = "Search for a command to run...",
+  children,
+  className,
+  showCloseButton = true,
+  ...props
+}: React.ComponentProps<typeof Dialog> & {
+  title?: string;
+  description?: string;
+  className?: string;
+  showCloseButton?: boolean;
+}) {
+  return (
+    <Dialog {...props}>
+      <DialogHeader className="sr-only">
+        <DialogTitle>{title}</DialogTitle>
+        <DialogDescription>{description}</DialogDescription>
+      </DialogHeader>
+      <DialogContent
+        className={cn("overflow-hidden p-0", className)}
+        showCloseButton={showCloseButton}
+      >
+        <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+          {children}
+        </Command>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+function CommandInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Input>) {
+  return (
+    <div
+      data-slot="command-input-wrapper"
+      className="flex h-9 items-center gap-2 border-b px-3"
+    >
+      <SearchIcon className="size-4 shrink-0 opacity-50" />
+      <CommandPrimitive.Input
+        data-slot="command-input"
+        className={cn(
+          "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
+          className
+        )}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function CommandList({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.List>) {
+  return (
+    <CommandPrimitive.List
+      data-slot="command-list"
+      className={cn(
+        "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandEmpty({
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
+  return (
+    <CommandPrimitive.Empty
+      data-slot="command-empty"
+      className="py-6 text-center text-sm"
+      {...props}
+    />
+  );
+}
+
+function CommandGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Group>) {
+  return (
+    <CommandPrimitive.Group
+      data-slot="command-group"
+      className={cn(
+        "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
+  return (
+    <CommandPrimitive.Separator
+      data-slot="command-separator"
+      className={cn("bg-border -mx-1 h-px", className)}
+      {...props}
+    />
+  );
+}
+
+function CommandItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Item>) {
+  return (
+    <CommandPrimitive.Item
+      data-slot="command-item"
+      className={cn(
+        "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="command-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Command,
+  CommandDialog,
+  CommandInput,
+  CommandList,
+  CommandEmpty,
+  CommandGroup,
+  CommandItem,
+  CommandShortcut,
+  CommandSeparator,
+};

+ 250 - 0
client/src/components/ui/context-menu.tsx

@@ -0,0 +1,250 @@
+import * as React from "react";
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function ContextMenu({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
+  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
+}
+
+function ContextMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
+  return (
+    <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
+  );
+}
+
+function ContextMenuGroup({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
+  return (
+    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
+  );
+}
+
+function ContextMenuPortal({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
+  return (
+    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
+  );
+}
+
+function ContextMenuSub({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
+  return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
+}
+
+function ContextMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
+  return (
+    <ContextMenuPrimitive.RadioGroup
+      data-slot="context-menu-radio-group"
+      {...props}
+    />
+  );
+}
+
+function ContextMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.SubTrigger
+      data-slot="context-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto" />
+    </ContextMenuPrimitive.SubTrigger>
+  );
+}
+
+function ContextMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
+  return (
+    <ContextMenuPrimitive.SubContent
+      data-slot="context-menu-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
+  return (
+    <ContextMenuPrimitive.Portal>
+      <ContextMenuPrimitive.Content
+        data-slot="context-menu-content"
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </ContextMenuPrimitive.Portal>
+  );
+}
+
+function ContextMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
+  inset?: boolean;
+  variant?: "default" | "destructive";
+}) {
+  return (
+    <ContextMenuPrimitive.Item
+      data-slot="context-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
+  return (
+    <ContextMenuPrimitive.CheckboxItem
+      data-slot="context-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <ContextMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </ContextMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.CheckboxItem>
+  );
+}
+
+function ContextMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
+  return (
+    <ContextMenuPrimitive.RadioItem
+      data-slot="context-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <ContextMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </ContextMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.RadioItem>
+  );
+}
+
+function ContextMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.Label
+      data-slot="context-menu-label"
+      data-inset={inset}
+      className={cn(
+        "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
+  return (
+    <ContextMenuPrimitive.Separator
+      data-slot="context-menu-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="context-menu-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  ContextMenu,
+  ContextMenuTrigger,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuCheckboxItem,
+  ContextMenuRadioItem,
+  ContextMenuLabel,
+  ContextMenuSeparator,
+  ContextMenuShortcut,
+  ContextMenuGroup,
+  ContextMenuPortal,
+  ContextMenuSub,
+  ContextMenuSubContent,
+  ContextMenuSubTrigger,
+  ContextMenuRadioGroup,
+};

+ 209 - 0
client/src/components/ui/dialog.tsx

@@ -0,0 +1,209 @@
+import { cn } from "@/lib/utils";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+import * as React from "react";
+
+// Context to track composition state across dialog children
+const DialogCompositionContext = React.createContext<{
+  isComposing: () => boolean;
+  setComposing: (composing: boolean) => void;
+  justEndedComposing: () => boolean;
+  markCompositionEnd: () => void;
+}>({
+  isComposing: () => false,
+  setComposing: () => {},
+  justEndedComposing: () => false,
+  markCompositionEnd: () => {},
+});
+
+export const useDialogComposition = () =>
+  React.useContext(DialogCompositionContext);
+
+function Dialog({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  const composingRef = React.useRef(false);
+  const justEndedRef = React.useRef(false);
+  const endTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  const contextValue = React.useMemo(
+    () => ({
+      isComposing: () => composingRef.current,
+      setComposing: (composing: boolean) => {
+        composingRef.current = composing;
+      },
+      justEndedComposing: () => justEndedRef.current,
+      markCompositionEnd: () => {
+        justEndedRef.current = true;
+        if (endTimerRef.current) {
+          clearTimeout(endTimerRef.current);
+        }
+        endTimerRef.current = setTimeout(() => {
+          justEndedRef.current = false;
+        }, 150);
+      },
+    }),
+    []
+  );
+
+  return (
+    <DialogCompositionContext.Provider value={contextValue}>
+      <DialogPrimitive.Root data-slot="dialog" {...props} />
+    </DialogCompositionContext.Provider>
+  );
+}
+
+function DialogTrigger({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
+}
+
+function DialogPortal({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
+}
+
+function DialogClose({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+DialogOverlay.displayName = "DialogOverlay";
+
+function DialogContent({
+  className,
+  children,
+  showCloseButton = true,
+  onEscapeKeyDown,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Content> & {
+  showCloseButton?: boolean;
+}) {
+  const { isComposing } = useDialogComposition();
+
+  const handleEscapeKeyDown = React.useCallback(
+    (e: KeyboardEvent) => {
+      // Check both the native isComposing property and our context state
+      // This handles Safari's timing issues with composition events
+      const isCurrentlyComposing = (e as any).isComposing || isComposing();
+
+      // If IME is composing, prevent dialog from closing
+      if (isCurrentlyComposing) {
+        e.preventDefault();
+        return;
+      }
+
+      // Call user's onEscapeKeyDown if provided
+      onEscapeKeyDown?.(e);
+    },
+    [isComposing, onEscapeKeyDown]
+  );
+
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        onEscapeKeyDown={handleEscapeKeyDown}
+        {...props}
+      >
+        {children}
+        {showCloseButton && (
+          <DialogPrimitive.Close
+            data-slot="dialog-close"
+            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
+          >
+            <XIcon />
+            <span className="sr-only">Close</span>
+          </DialogPrimitive.Close>
+        )}
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn("text-lg leading-none font-semibold", className)}
+      {...props}
+    />
+  );
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger
+};
+

+ 133 - 0
client/src/components/ui/drawer.tsx

@@ -0,0 +1,133 @@
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
+
+import { cn } from "@/lib/utils";
+
+function Drawer({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
+  return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
+}
+
+function DrawerTrigger({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
+  return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
+}
+
+function DrawerPortal({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
+  return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
+}
+
+function DrawerClose({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
+  return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
+}
+
+function DrawerOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
+  return (
+    <DrawerPrimitive.Overlay
+      data-slot="drawer-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
+  return (
+    <DrawerPortal data-slot="drawer-portal">
+      <DrawerOverlay />
+      <DrawerPrimitive.Content
+        data-slot="drawer-content"
+        className={cn(
+          "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
+          "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
+          "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
+          "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
+          "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
+          className
+        )}
+        {...props}
+      >
+        <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
+        {children}
+      </DrawerPrimitive.Content>
+    </DrawerPortal>
+  );
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="drawer-header"
+      className={cn(
+        "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="drawer-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  );
+}
+
+function DrawerTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
+  return (
+    <DrawerPrimitive.Title
+      data-slot="drawer-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  );
+}
+
+function DrawerDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
+  return (
+    <DrawerPrimitive.Description
+      data-slot="drawer-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Drawer,
+  DrawerPortal,
+  DrawerOverlay,
+  DrawerTrigger,
+  DrawerClose,
+  DrawerContent,
+  DrawerHeader,
+  DrawerFooter,
+  DrawerTitle,
+  DrawerDescription,
+};

+ 255 - 0
client/src/components/ui/dropdown-menu.tsx

@@ -0,0 +1,255 @@
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function DropdownMenu({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
+}
+
+function DropdownMenuPortal({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+  return (
+    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+  );
+}
+
+function DropdownMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+  return (
+    <DropdownMenuPrimitive.Trigger
+      data-slot="dropdown-menu-trigger"
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuContent({
+  className,
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+  return (
+    <DropdownMenuPrimitive.Portal>
+      <DropdownMenuPrimitive.Content
+        data-slot="dropdown-menu-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </DropdownMenuPrimitive.Portal>
+  );
+}
+
+function DropdownMenuGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+  return (
+    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+  );
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+  inset?: boolean;
+  variant?: "default" | "destructive";
+}) {
+  return (
+    <DropdownMenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+  return (
+    <DropdownMenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.CheckboxItem>
+  );
+}
+
+function DropdownMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+  return (
+    <DropdownMenuPrimitive.RadioGroup
+      data-slot="dropdown-menu-radio-group"
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+  return (
+    <DropdownMenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.RadioItem>
+  );
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+  inset?: boolean;
+}) {
+  return (
+    <DropdownMenuPrimitive.Label
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+  return (
+    <DropdownMenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuSub({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+  inset?: boolean;
+}) {
+  return (
+    <DropdownMenuPrimitive.SubTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto size-4" />
+    </DropdownMenuPrimitive.SubTrigger>
+  );
+}
+
+function DropdownMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+  return (
+    <DropdownMenuPrimitive.SubContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+};

+ 104 - 0
client/src/components/ui/empty.tsx

@@ -0,0 +1,104 @@
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+function Empty({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="empty"
+      className={cn(
+        "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="empty-header"
+      className={cn(
+        "flex max-w-sm flex-col items-center gap-2 text-center",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+const emptyMediaVariants = cva(
+  "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+);
+
+function EmptyMedia({
+  className,
+  variant = "default",
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
+  return (
+    <div
+      data-slot="empty-icon"
+      data-variant={variant}
+      className={cn(emptyMediaVariants({ variant, className }))}
+      {...props}
+    />
+  );
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="empty-title"
+      className={cn("text-lg font-medium tracking-tight", className)}
+      {...props}
+    />
+  );
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
+  return (
+    <div
+      data-slot="empty-description"
+      className={cn(
+        "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="empty-content"
+      className={cn(
+        "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Empty,
+  EmptyHeader,
+  EmptyTitle,
+  EmptyDescription,
+  EmptyContent,
+  EmptyMedia,
+};

+ 242 - 0
client/src/components/ui/field.tsx

@@ -0,0 +1,242 @@
+import { useMemo } from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+
+function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+  return (
+    <fieldset
+      data-slot="field-set"
+      className={cn(
+        "flex flex-col gap-6",
+        "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldLegend({
+  className,
+  variant = "legend",
+  ...props
+}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
+  return (
+    <legend
+      data-slot="field-legend"
+      data-variant={variant}
+      className={cn(
+        "mb-3 font-medium",
+        "data-[variant=legend]:text-base",
+        "data-[variant=label]:text-sm",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="field-group"
+      className={cn(
+        "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+const fieldVariants = cva(
+  "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
+  {
+    variants: {
+      orientation: {
+        vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
+        horizontal: [
+          "flex-row items-center",
+          "[&>[data-slot=field-label]]:flex-auto",
+          "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+        ],
+        responsive: [
+          "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
+          "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
+          "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+        ],
+      },
+    },
+    defaultVariants: {
+      orientation: "vertical",
+    },
+  }
+);
+
+function Field({
+  className,
+  orientation = "vertical",
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="field"
+      data-orientation={orientation}
+      className={cn(fieldVariants({ orientation }), className)}
+      {...props}
+    />
+  );
+}
+
+function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="field-content"
+      className={cn(
+        "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof Label>) {
+  return (
+    <Label
+      data-slot="field-label"
+      className={cn(
+        "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
+        "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
+        "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="field-label"
+      className={cn(
+        "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
+  return (
+    <p
+      data-slot="field-description"
+      className={cn(
+        "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
+        "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
+        "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldSeparator({
+  children,
+  className,
+  ...props
+}: React.ComponentProps<"div"> & {
+  children?: React.ReactNode;
+}) {
+  return (
+    <div
+      data-slot="field-separator"
+      data-content={!!children}
+      className={cn(
+        "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
+        className
+      )}
+      {...props}
+    >
+      <Separator className="absolute inset-0 top-1/2" />
+      {children && (
+        <span
+          className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
+          data-slot="field-separator-content"
+        >
+          {children}
+        </span>
+      )}
+    </div>
+  );
+}
+
+function FieldError({
+  className,
+  children,
+  errors,
+  ...props
+}: React.ComponentProps<"div"> & {
+  errors?: Array<{ message?: string } | undefined>;
+}) {
+  const content = useMemo(() => {
+    if (children) {
+      return children;
+    }
+
+    if (!errors) {
+      return null;
+    }
+
+    if (errors?.length === 1 && errors[0]?.message) {
+      return errors[0].message;
+    }
+
+    return (
+      <ul className="ml-4 flex list-disc flex-col gap-1">
+        {errors.map(
+          (error, index) =>
+            error?.message && <li key={index}>{error.message}</li>
+        )}
+      </ul>
+    );
+  }, [children, errors]);
+
+  if (!content) {
+    return null;
+  }
+
+  return (
+    <div
+      role="alert"
+      data-slot="field-error"
+      className={cn("text-destructive text-sm font-normal", className)}
+      {...props}
+    >
+      {content}
+    </div>
+  );
+}
+
+export {
+  Field,
+  FieldLabel,
+  FieldDescription,
+  FieldError,
+  FieldGroup,
+  FieldLegend,
+  FieldSeparator,
+  FieldSet,
+  FieldContent,
+  FieldTitle,
+};

+ 168 - 0
client/src/components/ui/form.tsx

@@ -0,0 +1,168 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import {
+  Controller,
+  FormProvider,
+  useFormContext,
+  useFormState,
+  type ControllerProps,
+  type FieldPath,
+  type FieldValues,
+} from "react-hook-form";
+
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+  name: TName;
+};
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+  {} as FormFieldContextValue
+);
+
+const FormField = <
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+  ...props
+}: ControllerProps<TFieldValues, TName>) => {
+  return (
+    <FormFieldContext.Provider value={{ name: props.name }}>
+      <Controller {...props} />
+    </FormFieldContext.Provider>
+  );
+};
+
+const useFormField = () => {
+  const fieldContext = React.useContext(FormFieldContext);
+  const itemContext = React.useContext(FormItemContext);
+  const { getFieldState } = useFormContext();
+  const formState = useFormState({ name: fieldContext.name });
+  const fieldState = getFieldState(fieldContext.name, formState);
+
+  if (!fieldContext) {
+    throw new Error("useFormField should be used within <FormField>");
+  }
+
+  const { id } = itemContext;
+
+  return {
+    id,
+    name: fieldContext.name,
+    formItemId: `${id}-form-item`,
+    formDescriptionId: `${id}-form-item-description`,
+    formMessageId: `${id}-form-item-message`,
+    ...fieldState,
+  };
+};
+
+type FormItemContextValue = {
+  id: string;
+};
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+  {} as FormItemContextValue
+);
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+  const id = React.useId();
+
+  return (
+    <FormItemContext.Provider value={{ id }}>
+      <div
+        data-slot="form-item"
+        className={cn("grid gap-2", className)}
+        {...props}
+      />
+    </FormItemContext.Provider>
+  );
+}
+
+function FormLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  const { error, formItemId } = useFormField();
+
+  return (
+    <Label
+      data-slot="form-label"
+      data-error={!!error}
+      className={cn("data-[error=true]:text-destructive", className)}
+      htmlFor={formItemId}
+      {...props}
+    />
+  );
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+  const { error, formItemId, formDescriptionId, formMessageId } =
+    useFormField();
+
+  return (
+    <Slot
+      data-slot="form-control"
+      id={formItemId}
+      aria-describedby={
+        !error
+          ? `${formDescriptionId}`
+          : `${formDescriptionId} ${formMessageId}`
+      }
+      aria-invalid={!!error}
+      {...props}
+    />
+  );
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+  const { formDescriptionId } = useFormField();
+
+  return (
+    <p
+      data-slot="form-description"
+      id={formDescriptionId}
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+  const { error, formMessageId } = useFormField();
+  const body = error ? String(error?.message ?? "") : props.children;
+
+  if (!body) {
+    return null;
+  }
+
+  return (
+    <p
+      data-slot="form-message"
+      id={formMessageId}
+      className={cn("text-destructive text-sm", className)}
+      {...props}
+    >
+      {body}
+    </p>
+  );
+}
+
+export {
+  useFormField,
+  Form,
+  FormItem,
+  FormLabel,
+  FormControl,
+  FormDescription,
+  FormMessage,
+  FormField,
+};

+ 42 - 0
client/src/components/ui/hover-card.tsx

@@ -0,0 +1,42 @@
+import * as React from "react";
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
+
+import { cn } from "@/lib/utils";
+
+function HoverCard({
+  ...props
+}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
+  return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
+}
+
+function HoverCardTrigger({
+  ...props
+}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
+  return (
+    <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
+  );
+}
+
+function HoverCardContent({
+  className,
+  align = "center",
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
+  return (
+    <HoverCardPrimitive.Portal data-slot="hover-card-portal">
+      <HoverCardPrimitive.Content
+        data-slot="hover-card-content"
+        align={align}
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+          className
+        )}
+        {...props}
+      />
+    </HoverCardPrimitive.Portal>
+  );
+}
+
+export { HoverCard, HoverCardTrigger, HoverCardContent };

+ 168 - 0
client/src/components/ui/input-group.tsx

@@ -0,0 +1,168 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+
+function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="input-group"
+      role="group"
+      className={cn(
+        "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
+        "h-9 min-w-0 has-[>textarea]:h-auto",
+
+        // Variants based on alignment.
+        "has-[>[data-align=inline-start]]:[&>input]:pl-2",
+        "has-[>[data-align=inline-end]]:[&>input]:pr-2",
+        "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
+        "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
+
+        // Focus state.
+        "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
+
+        // Error state.
+        "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
+
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+const inputGroupAddonVariants = cva(
+  "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
+  {
+    variants: {
+      align: {
+        "inline-start":
+          "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
+        "inline-end":
+          "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
+        "block-start":
+          "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
+        "block-end":
+          "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
+      },
+    },
+    defaultVariants: {
+      align: "inline-start",
+    },
+  }
+);
+
+function InputGroupAddon({
+  className,
+  align = "inline-start",
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="input-group-addon"
+      data-align={align}
+      className={cn(inputGroupAddonVariants({ align }), className)}
+      onClick={e => {
+        if ((e.target as HTMLElement).closest("button")) {
+          return;
+        }
+        e.currentTarget.parentElement?.querySelector("input")?.focus();
+      }}
+      {...props}
+    />
+  );
+}
+
+const inputGroupButtonVariants = cva(
+  "text-sm shadow-none flex gap-2 items-center",
+  {
+    variants: {
+      size: {
+        xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
+        sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
+        "icon-xs":
+          "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
+        "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+      },
+    },
+    defaultVariants: {
+      size: "xs",
+    },
+  }
+);
+
+function InputGroupButton({
+  className,
+  type = "button",
+  variant = "ghost",
+  size = "xs",
+  ...props
+}: Omit<React.ComponentProps<typeof Button>, "size"> &
+  VariantProps<typeof inputGroupButtonVariants>) {
+  return (
+    <Button
+      type={type}
+      data-size={size}
+      variant={variant}
+      className={cn(inputGroupButtonVariants({ size }), className)}
+      {...props}
+    />
+  );
+}
+
+function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
+  return (
+    <span
+      className={cn(
+        "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function InputGroupInput({
+  className,
+  ...props
+}: React.ComponentProps<"input">) {
+  return (
+    <Input
+      data-slot="input-group-control"
+      className={cn(
+        "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function InputGroupTextarea({
+  className,
+  ...props
+}: React.ComponentProps<"textarea">) {
+  return (
+    <Textarea
+      data-slot="input-group-control"
+      className={cn(
+        "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  InputGroup,
+  InputGroupAddon,
+  InputGroupButton,
+  InputGroupText,
+  InputGroupInput,
+  InputGroupTextarea,
+};

+ 75 - 0
client/src/components/ui/input-otp.tsx

@@ -0,0 +1,75 @@
+import * as React from "react";
+import { OTPInput, OTPInputContext } from "input-otp";
+import { MinusIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function InputOTP({
+  className,
+  containerClassName,
+  ...props
+}: React.ComponentProps<typeof OTPInput> & {
+  containerClassName?: string;
+}) {
+  return (
+    <OTPInput
+      data-slot="input-otp"
+      containerClassName={cn(
+        "flex items-center gap-2 has-disabled:opacity-50",
+        containerClassName
+      )}
+      className={cn("disabled:cursor-not-allowed", className)}
+      {...props}
+    />
+  );
+}
+
+function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="input-otp-group"
+      className={cn("flex items-center", className)}
+      {...props}
+    />
+  );
+}
+
+function InputOTPSlot({
+  index,
+  className,
+  ...props
+}: React.ComponentProps<"div"> & {
+  index: number;
+}) {
+  const inputOTPContext = React.useContext(OTPInputContext);
+  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
+
+  return (
+    <div
+      data-slot="input-otp-slot"
+      data-active={isActive}
+      className={cn(
+        "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
+        className
+      )}
+      {...props}
+    >
+      {char}
+      {hasFakeCaret && (
+        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
+          <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
+        </div>
+      )}
+    </div>
+  );
+}
+
+function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
+  return (
+    <div data-slot="input-otp-separator" role="separator" {...props}>
+      <MinusIcon />
+    </div>
+  );
+}
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

+ 70 - 0
client/src/components/ui/input.tsx

@@ -0,0 +1,70 @@
+import { useDialogComposition } from "@/components/ui/dialog";
+import { useComposition } from "@/hooks/useComposition";
+import { cn } from "@/lib/utils";
+import * as React from "react";
+
+function Input({
+  className,
+  type,
+  onKeyDown,
+  onCompositionStart,
+  onCompositionEnd,
+  ...props
+}: React.ComponentProps<"input">) {
+  // Get dialog composition context if available (will be no-op if not inside Dialog)
+  const dialogComposition = useDialogComposition();
+
+  // Add composition event handlers to support input method editor (IME) for CJK languages.
+  const {
+    onCompositionStart: handleCompositionStart,
+    onCompositionEnd: handleCompositionEnd,
+    onKeyDown: handleKeyDown,
+  } = useComposition<HTMLInputElement>({
+    onKeyDown: (e) => {
+      // Check if this is an Enter key that should be blocked
+      const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
+
+      // If Enter key is pressed while composing or just after composition ended,
+      // don't call the user's onKeyDown (this blocks the business logic)
+      if (e.key === "Enter" && isComposing) {
+        return;
+      }
+
+      // Otherwise, call the user's onKeyDown
+      onKeyDown?.(e);
+    },
+    onCompositionStart: e => {
+      dialogComposition.setComposing(true);
+      onCompositionStart?.(e);
+    },
+    onCompositionEnd: e => {
+      // Mark that composition just ended - this helps handle the Enter key that confirms input
+      dialogComposition.markCompositionEnd();
+      // Delay setting composing to false to handle Safari's event order
+      // In Safari, compositionEnd fires before the ESC keydown event
+      setTimeout(() => {
+        dialogComposition.setComposing(false);
+      }, 100);
+      onCompositionEnd?.(e);
+    },
+  });
+
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+        className
+      )}
+      onCompositionStart={handleCompositionStart}
+      onCompositionEnd={handleCompositionEnd}
+      onKeyDown={handleKeyDown}
+      {...props}
+    />
+  );
+}
+
+export { Input };

+ 193 - 0
client/src/components/ui/item.tsx

@@ -0,0 +1,193 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
+
+function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      role="list"
+      data-slot="item-group"
+      className={cn("group/item-group flex flex-col", className)}
+      {...props}
+    />
+  );
+}
+
+function ItemSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="item-separator"
+      orientation="horizontal"
+      className={cn("my-0", className)}
+      {...props}
+    />
+  );
+}
+
+const itemVariants = cva(
+  "group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        outline: "border-border",
+        muted: "bg-muted/50",
+      },
+      size: {
+        default: "p-4 gap-4 ",
+        sm: "py-3 px-4 gap-2.5",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+);
+
+function Item({
+  className,
+  variant = "default",
+  size = "default",
+  asChild = false,
+  ...props
+}: React.ComponentProps<"div"> &
+  VariantProps<typeof itemVariants> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "div";
+  return (
+    <Comp
+      data-slot="item"
+      data-variant={variant}
+      data-size={size}
+      className={cn(itemVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+}
+
+const itemMediaVariants = cva(
+  "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
+        image:
+          "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+);
+
+function ItemMedia({
+  className,
+  variant = "default",
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
+  return (
+    <div
+      data-slot="item-media"
+      data-variant={variant}
+      className={cn(itemMediaVariants({ variant, className }))}
+      {...props}
+    />
+  );
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="item-content"
+      className={cn(
+        "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="item-title"
+      className={cn(
+        "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
+  return (
+    <p
+      data-slot="item-description"
+      className={cn(
+        "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
+        "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="item-actions"
+      className={cn("flex items-center gap-2", className)}
+      {...props}
+    />
+  );
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="item-header"
+      className={cn(
+        "flex basis-full items-center justify-between gap-2",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="item-footer"
+      className={cn(
+        "flex basis-full items-center justify-between gap-2",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Item,
+  ItemMedia,
+  ItemContent,
+  ItemActions,
+  ItemGroup,
+  ItemSeparator,
+  ItemTitle,
+  ItemDescription,
+  ItemHeader,
+  ItemFooter,
+};

+ 28 - 0
client/src/components/ui/kbd.tsx

@@ -0,0 +1,28 @@
+import { cn } from "@/lib/utils";
+
+function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
+  return (
+    <kbd
+      data-slot="kbd"
+      className={cn(
+        "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
+        "[&_svg:not([class*='size-'])]:size-3",
+        "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <kbd
+      data-slot="kbd-group"
+      className={cn("inline-flex items-center gap-1", className)}
+      {...props}
+    />
+  );
+}
+
+export { Kbd, KbdGroup };

+ 22 - 0
client/src/components/ui/label.tsx

@@ -0,0 +1,22 @@
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+
+import { cn } from "@/lib/utils";
+
+function Label({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  return (
+    <LabelPrimitive.Root
+      data-slot="label"
+      className={cn(
+        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Label };

+ 274 - 0
client/src/components/ui/menubar.tsx

@@ -0,0 +1,274 @@
+import * as React from "react";
+import * as MenubarPrimitive from "@radix-ui/react-menubar";
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Menubar({
+  className,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
+  return (
+    <MenubarPrimitive.Root
+      data-slot="menubar"
+      className={cn(
+        "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarMenu({
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
+  return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
+}
+
+function MenubarGroup({
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
+  return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
+}
+
+function MenubarPortal({
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
+  return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
+}
+
+function MenubarRadioGroup({
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
+  return (
+    <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
+  );
+}
+
+function MenubarTrigger({
+  className,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
+  return (
+    <MenubarPrimitive.Trigger
+      data-slot="menubar-trigger"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarContent({
+  className,
+  align = "start",
+  alignOffset = -4,
+  sideOffset = 8,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
+  return (
+    <MenubarPortal>
+      <MenubarPrimitive.Content
+        data-slot="menubar-content"
+        align={align}
+        alignOffset={alignOffset}
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </MenubarPortal>
+  );
+}
+
+function MenubarItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
+  inset?: boolean;
+  variant?: "default" | "destructive";
+}) {
+  return (
+    <MenubarPrimitive.Item
+      data-slot="menubar-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
+  return (
+    <MenubarPrimitive.CheckboxItem
+      data-slot="menubar-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <MenubarPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </MenubarPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </MenubarPrimitive.CheckboxItem>
+  );
+}
+
+function MenubarRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
+  return (
+    <MenubarPrimitive.RadioItem
+      data-slot="menubar-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <MenubarPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </MenubarPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </MenubarPrimitive.RadioItem>
+  );
+}
+
+function MenubarLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
+  inset?: boolean;
+}) {
+  return (
+    <MenubarPrimitive.Label
+      data-slot="menubar-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
+  return (
+    <MenubarPrimitive.Separator
+      data-slot="menubar-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  );
+}
+
+function MenubarShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="menubar-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarSub({
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
+  return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
+}
+
+function MenubarSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
+  inset?: boolean;
+}) {
+  return (
+    <MenubarPrimitive.SubTrigger
+      data-slot="menubar-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto h-4 w-4" />
+    </MenubarPrimitive.SubTrigger>
+  );
+}
+
+function MenubarSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
+  return (
+    <MenubarPrimitive.SubContent
+      data-slot="menubar-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Menubar,
+  MenubarPortal,
+  MenubarMenu,
+  MenubarTrigger,
+  MenubarContent,
+  MenubarGroup,
+  MenubarSeparator,
+  MenubarLabel,
+  MenubarItem,
+  MenubarShortcut,
+  MenubarCheckboxItem,
+  MenubarRadioGroup,
+  MenubarRadioItem,
+  MenubarSub,
+  MenubarSubTrigger,
+  MenubarSubContent,
+};

+ 168 - 0
client/src/components/ui/navigation-menu.tsx

@@ -0,0 +1,168 @@
+import * as React from "react";
+import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
+import { cva } from "class-variance-authority";
+import { ChevronDownIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function NavigationMenu({
+  className,
+  children,
+  viewport = true,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
+  viewport?: boolean;
+}) {
+  return (
+    <NavigationMenuPrimitive.Root
+      data-slot="navigation-menu"
+      data-viewport={viewport}
+      className={cn(
+        "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      {viewport && <NavigationMenuViewport />}
+    </NavigationMenuPrimitive.Root>
+  );
+}
+
+function NavigationMenuList({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
+  return (
+    <NavigationMenuPrimitive.List
+      data-slot="navigation-menu-list"
+      className={cn(
+        "group flex flex-1 list-none items-center justify-center gap-1",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
+  return (
+    <NavigationMenuPrimitive.Item
+      data-slot="navigation-menu-item"
+      className={cn("relative", className)}
+      {...props}
+    />
+  );
+}
+
+const navigationMenuTriggerStyle = cva(
+  "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
+);
+
+function NavigationMenuTrigger({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
+  return (
+    <NavigationMenuPrimitive.Trigger
+      data-slot="navigation-menu-trigger"
+      className={cn(navigationMenuTriggerStyle(), "group", className)}
+      {...props}
+    >
+      {children}{" "}
+      <ChevronDownIcon
+        className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
+        aria-hidden="true"
+      />
+    </NavigationMenuPrimitive.Trigger>
+  );
+}
+
+function NavigationMenuContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
+  return (
+    <NavigationMenuPrimitive.Content
+      data-slot="navigation-menu-content"
+      className={cn(
+        "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
+        "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuViewport({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
+  return (
+    <div
+      className={cn(
+        "absolute top-full left-0 isolate z-50 flex justify-center"
+      )}
+    >
+      <NavigationMenuPrimitive.Viewport
+        data-slot="navigation-menu-viewport"
+        className={cn(
+          "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
+          className
+        )}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function NavigationMenuLink({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
+  return (
+    <NavigationMenuPrimitive.Link
+      data-slot="navigation-menu-link"
+      className={cn(
+        "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuIndicator({
+  className,
+  ...props
+}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
+  return (
+    <NavigationMenuPrimitive.Indicator
+      data-slot="navigation-menu-indicator"
+      className={cn(
+        "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
+        className
+      )}
+      {...props}
+    >
+      <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
+    </NavigationMenuPrimitive.Indicator>
+  );
+}
+
+export {
+  NavigationMenu,
+  NavigationMenuList,
+  NavigationMenuItem,
+  NavigationMenuContent,
+  NavigationMenuTrigger,
+  NavigationMenuLink,
+  NavigationMenuIndicator,
+  NavigationMenuViewport,
+  navigationMenuTriggerStyle,
+};

+ 127 - 0
client/src/components/ui/pagination.tsx

@@ -0,0 +1,127 @@
+import * as React from "react";
+import {
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  MoreHorizontalIcon,
+} from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button, buttonVariants } from "@/components/ui/button";
+
+function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
+  return (
+    <nav
+      role="navigation"
+      aria-label="pagination"
+      data-slot="pagination"
+      className={cn("mx-auto flex w-full justify-center", className)}
+      {...props}
+    />
+  );
+}
+
+function PaginationContent({
+  className,
+  ...props
+}: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="pagination-content"
+      className={cn("flex flex-row items-center gap-1", className)}
+      {...props}
+    />
+  );
+}
+
+function PaginationItem({ ...props }: React.ComponentProps<"li">) {
+  return <li data-slot="pagination-item" {...props} />;
+}
+
+type PaginationLinkProps = {
+  isActive?: boolean;
+} & Pick<React.ComponentProps<typeof Button>, "size"> &
+  React.ComponentProps<"a">;
+
+function PaginationLink({
+  className,
+  isActive,
+  size = "icon",
+  ...props
+}: PaginationLinkProps) {
+  return (
+    <a
+      aria-current={isActive ? "page" : undefined}
+      data-slot="pagination-link"
+      data-active={isActive}
+      className={cn(
+        buttonVariants({
+          variant: isActive ? "outline" : "ghost",
+          size,
+        }),
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function PaginationPrevious({
+  className,
+  ...props
+}: React.ComponentProps<typeof PaginationLink>) {
+  return (
+    <PaginationLink
+      aria-label="Go to previous page"
+      size="default"
+      className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
+      {...props}
+    >
+      <ChevronLeftIcon />
+      <span className="hidden sm:block">Previous</span>
+    </PaginationLink>
+  );
+}
+
+function PaginationNext({
+  className,
+  ...props
+}: React.ComponentProps<typeof PaginationLink>) {
+  return (
+    <PaginationLink
+      aria-label="Go to next page"
+      size="default"
+      className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
+      {...props}
+    >
+      <span className="hidden sm:block">Next</span>
+      <ChevronRightIcon />
+    </PaginationLink>
+  );
+}
+
+function PaginationEllipsis({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      aria-hidden
+      data-slot="pagination-ellipsis"
+      className={cn("flex size-9 items-center justify-center", className)}
+      {...props}
+    >
+      <MoreHorizontalIcon className="size-4" />
+      <span className="sr-only">More pages</span>
+    </span>
+  );
+}
+
+export {
+  Pagination,
+  PaginationContent,
+  PaginationLink,
+  PaginationItem,
+  PaginationPrevious,
+  PaginationNext,
+  PaginationEllipsis,
+};

+ 46 - 0
client/src/components/ui/popover.tsx

@@ -0,0 +1,46 @@
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/utils";
+
+function Popover({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
+  return <PopoverPrimitive.Root data-slot="popover" {...props} />;
+}
+
+function PopoverTrigger({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
+  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
+}
+
+function PopoverContent({
+  className,
+  align = "center",
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
+  return (
+    <PopoverPrimitive.Portal>
+      <PopoverPrimitive.Content
+        data-slot="popover-content"
+        align={align}
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+          className
+        )}
+        {...props}
+      />
+    </PopoverPrimitive.Portal>
+  );
+}
+
+function PopoverAnchor({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
+  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

+ 29 - 0
client/src/components/ui/progress.tsx

@@ -0,0 +1,29 @@
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+
+import { cn } from "@/lib/utils";
+
+function Progress({
+  className,
+  value,
+  ...props
+}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
+  return (
+    <ProgressPrimitive.Root
+      data-slot="progress"
+      className={cn(
+        "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
+        className
+      )}
+      {...props}
+    >
+      <ProgressPrimitive.Indicator
+        data-slot="progress-indicator"
+        className="bg-primary h-full w-full flex-1 transition-all"
+        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+      />
+    </ProgressPrimitive.Root>
+  );
+}
+
+export { Progress };

+ 43 - 0
client/src/components/ui/radio-group.tsx

@@ -0,0 +1,43 @@
+import * as React from "react";
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
+import { CircleIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function RadioGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
+  return (
+    <RadioGroupPrimitive.Root
+      data-slot="radio-group"
+      className={cn("grid gap-3", className)}
+      {...props}
+    />
+  );
+}
+
+function RadioGroupItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
+  return (
+    <RadioGroupPrimitive.Item
+      data-slot="radio-group-item"
+      className={cn(
+        "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+        className
+      )}
+      {...props}
+    >
+      <RadioGroupPrimitive.Indicator
+        data-slot="radio-group-indicator"
+        className="relative flex items-center justify-center"
+      >
+        <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
+      </RadioGroupPrimitive.Indicator>
+    </RadioGroupPrimitive.Item>
+  );
+}
+
+export { RadioGroup, RadioGroupItem };

+ 54 - 0
client/src/components/ui/resizable.tsx

@@ -0,0 +1,54 @@
+import * as React from "react";
+import { GripVerticalIcon } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+function ResizablePanelGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
+  return (
+    <ResizablePrimitive.PanelGroup
+      data-slot="resizable-panel-group"
+      className={cn(
+        "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function ResizablePanel({
+  ...props
+}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
+  return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
+}
+
+function ResizableHandle({
+  withHandle,
+  className,
+  ...props
+}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
+  withHandle?: boolean;
+}) {
+  return (
+    <ResizablePrimitive.PanelResizeHandle
+      data-slot="resizable-handle"
+      className={cn(
+        "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
+        className
+      )}
+      {...props}
+    >
+      {withHandle && (
+        <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
+          <GripVerticalIcon className="size-2.5" />
+        </div>
+      )}
+    </ResizablePrimitive.PanelResizeHandle>
+  );
+}
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

+ 56 - 0
client/src/components/ui/scroll-area.tsx

@@ -0,0 +1,56 @@
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "@/lib/utils";
+
+function ScrollArea({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
+  return (
+    <ScrollAreaPrimitive.Root
+      data-slot="scroll-area"
+      className={cn("relative", className)}
+      {...props}
+    >
+      <ScrollAreaPrimitive.Viewport
+        data-slot="scroll-area-viewport"
+        className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
+      >
+        {children}
+      </ScrollAreaPrimitive.Viewport>
+      <ScrollBar />
+      <ScrollAreaPrimitive.Corner />
+    </ScrollAreaPrimitive.Root>
+  );
+}
+
+function ScrollBar({
+  className,
+  orientation = "vertical",
+  ...props
+}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
+  return (
+    <ScrollAreaPrimitive.ScrollAreaScrollbar
+      data-slot="scroll-area-scrollbar"
+      orientation={orientation}
+      className={cn(
+        "flex touch-none p-px transition-colors select-none",
+        orientation === "vertical" &&
+          "h-full w-2.5 border-l border-l-transparent",
+        orientation === "horizontal" &&
+          "h-2.5 flex-col border-t border-t-transparent",
+        className
+      )}
+      {...props}
+    >
+      <ScrollAreaPrimitive.ScrollAreaThumb
+        data-slot="scroll-area-thumb"
+        className="bg-border relative flex-1 rounded-full"
+      />
+    </ScrollAreaPrimitive.ScrollAreaScrollbar>
+  );
+}
+
+export { ScrollArea, ScrollBar };

+ 185 - 0
client/src/components/ui/select.tsx

@@ -0,0 +1,185 @@
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Select({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Root>) {
+  return <SelectPrimitive.Root data-slot="select" {...props} />;
+}
+
+function SelectGroup({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Group>) {
+  return <SelectPrimitive.Group data-slot="select-group" {...props} />;
+}
+
+function SelectValue({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Value>) {
+  return <SelectPrimitive.Value data-slot="select-value" {...props} />;
+}
+
+function SelectTrigger({
+  className,
+  size = "default",
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
+  size?: "sm" | "default";
+}) {
+  return (
+    <SelectPrimitive.Trigger
+      data-slot="select-trigger"
+      data-size={size}
+      className={cn(
+        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <SelectPrimitive.Icon asChild>
+        <ChevronDownIcon className="size-4 opacity-50" />
+      </SelectPrimitive.Icon>
+    </SelectPrimitive.Trigger>
+  );
+}
+
+function SelectContent({
+  className,
+  children,
+  position = "popper",
+  align = "center",
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+  return (
+    <SelectPrimitive.Portal>
+      <SelectPrimitive.Content
+        data-slot="select-content"
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
+          position === "popper" &&
+            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+          className
+        )}
+        position={position}
+        align={align}
+        {...props}
+      >
+        <SelectScrollUpButton />
+        <SelectPrimitive.Viewport
+          className={cn(
+            "p-1",
+            position === "popper" &&
+              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+          )}
+        >
+          {children}
+        </SelectPrimitive.Viewport>
+        <SelectScrollDownButton />
+      </SelectPrimitive.Content>
+    </SelectPrimitive.Portal>
+  );
+}
+
+function SelectLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Label>) {
+  return (
+    <SelectPrimitive.Label
+      data-slot="select-label"
+      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
+      {...props}
+    />
+  );
+}
+
+function SelectItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Item>) {
+  return (
+    <SelectPrimitive.Item
+      data-slot="select-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+        className
+      )}
+      {...props}
+    >
+      <span className="absolute right-2 flex size-3.5 items-center justify-center">
+        <SelectPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </SelectPrimitive.ItemIndicator>
+      </span>
+      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+    </SelectPrimitive.Item>
+  );
+}
+
+function SelectSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+  return (
+    <SelectPrimitive.Separator
+      data-slot="select-separator"
+      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  );
+}
+
+function SelectScrollUpButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+  return (
+    <SelectPrimitive.ScrollUpButton
+      data-slot="select-scroll-up-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronUpIcon className="size-4" />
+    </SelectPrimitive.ScrollUpButton>
+  );
+}
+
+function SelectScrollDownButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+  return (
+    <SelectPrimitive.ScrollDownButton
+      data-slot="select-scroll-down-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronDownIcon className="size-4" />
+    </SelectPrimitive.ScrollDownButton>
+  );
+}
+
+export {
+  Select,
+  SelectContent,
+  SelectGroup,
+  SelectItem,
+  SelectLabel,
+  SelectScrollDownButton,
+  SelectScrollUpButton,
+  SelectSeparator,
+  SelectTrigger,
+  SelectValue,
+};

+ 26 - 0
client/src/components/ui/separator.tsx

@@ -0,0 +1,26 @@
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "@/lib/utils";
+
+function Separator({
+  className,
+  orientation = "horizontal",
+  decorative = true,
+  ...props
+}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+  return (
+    <SeparatorPrimitive.Root
+      data-slot="separator"
+      decorative={decorative}
+      orientation={orientation}
+      className={cn(
+        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Separator };

+ 139 - 0
client/src/components/ui/sheet.tsx

@@ -0,0 +1,139 @@
+"use client";
+
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />;
+}
+
+function SheetTrigger({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
+}
+
+function SheetClose({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Close>) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
+}
+
+function SheetPortal({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
+}
+
+function SheetOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
+  return (
+    <SheetPrimitive.Overlay
+      data-slot="sheet-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetContent({
+  className,
+  children,
+  side = "right",
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Content> & {
+  side?: "top" | "right" | "bottom" | "left";
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Content
+        data-slot="sheet-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+          side === "right" &&
+            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
+          side === "left" &&
+            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
+          side === "top" &&
+            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
+          side === "bottom" &&
+            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+          <XIcon className="size-4" />
+          <span className="sr-only">Close</span>
+        </SheetPrimitive.Close>
+      </SheetPrimitive.Content>
+    </SheetPortal>
+  );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn("flex flex-col gap-1.5 p-4", className)}
+      {...props}
+    />
+  );
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  );
+}
+
+function SheetTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Title>) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  );
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Description>) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+};

+ 734 - 0
client/src/components/ui/sidebar.tsx

@@ -0,0 +1,734 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+} from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useIsMobile } from "@/hooks/useMobile";
+import { cn } from "@/lib/utils";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
+import * as React from "react";
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
+
+type SidebarContextProps = {
+  state: "expanded" | "collapsed";
+  open: boolean;
+  setOpen: (open: boolean) => void;
+  openMobile: boolean;
+  setOpenMobile: (open: boolean) => void;
+  isMobile: boolean;
+  toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext<SidebarContextProps | null>(null);
+
+function useSidebar() {
+  const context = React.useContext(SidebarContext);
+  if (!context) {
+    throw new Error("useSidebar must be used within a SidebarProvider.");
+  }
+
+  return context;
+}
+
+function SidebarProvider({
+  defaultOpen = true,
+  open: openProp,
+  onOpenChange: setOpenProp,
+  className,
+  style,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  defaultOpen?: boolean;
+  open?: boolean;
+  onOpenChange?: (open: boolean) => void;
+}) {
+  const isMobile = useIsMobile();
+  const [openMobile, setOpenMobile] = React.useState(false);
+
+  // This is the internal state of the sidebar.
+  // We use openProp and setOpenProp for control from outside the component.
+  const [_open, _setOpen] = React.useState(defaultOpen);
+  const open = openProp ?? _open;
+  const setOpen = React.useCallback(
+    (value: boolean | ((value: boolean) => boolean)) => {
+      const openState = typeof value === "function" ? value(open) : value;
+      if (setOpenProp) {
+        setOpenProp(openState);
+      } else {
+        _setOpen(openState);
+      }
+
+      // This sets the cookie to keep the sidebar state.
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+    },
+    [setOpenProp, open]
+  );
+
+  // Helper to toggle the sidebar.
+  const toggleSidebar = React.useCallback(() => {
+    return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open);
+  }, [isMobile, setOpen, setOpenMobile]);
+
+  // Adds a keyboard shortcut to toggle the sidebar.
+  React.useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (
+        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+        (event.metaKey || event.ctrlKey)
+      ) {
+        event.preventDefault();
+        toggleSidebar();
+      }
+    };
+
+    window.addEventListener("keydown", handleKeyDown);
+    return () => window.removeEventListener("keydown", handleKeyDown);
+  }, [toggleSidebar]);
+
+  // We add a state so that we can do data-state="expanded" or "collapsed".
+  // This makes it easier to style the sidebar with Tailwind classes.
+  const state = open ? "expanded" : "collapsed";
+
+  const contextValue = React.useMemo<SidebarContextProps>(
+    () => ({
+      state,
+      open,
+      setOpen,
+      isMobile,
+      openMobile,
+      setOpenMobile,
+      toggleSidebar,
+    }),
+    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+  );
+
+  return (
+    <SidebarContext.Provider value={contextValue}>
+      <TooltipProvider delayDuration={0}>
+        <div
+          data-slot="sidebar-wrapper"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH,
+              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
+              ...style,
+            } as React.CSSProperties
+          }
+          className={cn(
+            "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+            className
+          )}
+          {...props}
+        >
+          {children}
+        </div>
+      </TooltipProvider>
+    </SidebarContext.Provider>
+  );
+}
+
+function Sidebar({
+  side = "left",
+  variant = "sidebar",
+  collapsible = "offcanvas",
+  disableTransition = false,
+  className,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  side?: "left" | "right";
+  variant?: "sidebar" | "floating" | "inset";
+  collapsible?: "offcanvas" | "icon" | "none";
+  disableTransition?: boolean;
+}) {
+  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+  if (collapsible === "none") {
+    return (
+      <div
+        data-slot="sidebar"
+        className={cn(
+          "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
+          className
+        )}
+        {...props}
+      >
+        {children}
+      </div>
+    );
+  }
+
+  if (isMobile) {
+    return (
+      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+        <SheetContent
+          data-sidebar="sidebar"
+          data-slot="sidebar"
+          data-mobile="true"
+          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+            } as React.CSSProperties
+          }
+          side={side}
+        >
+          <SheetHeader className="sr-only">
+            <SheetTitle>Sidebar</SheetTitle>
+            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+          </SheetHeader>
+          <div className="flex h-full w-full flex-col">{children}</div>
+        </SheetContent>
+      </Sheet>
+    );
+  }
+
+  return (
+    <div
+      className="group peer text-sidebar-foreground hidden md:block"
+      data-state={state}
+      data-collapsible={state === "collapsed" ? collapsible : ""}
+      data-variant={variant}
+      data-side={side}
+      data-slot="sidebar"
+    >
+      {/* This is what handles the sidebar gap on desktop */}
+      <div
+        data-slot="sidebar-gap"
+        className={cn(
+          "relative w-(--sidebar-width) bg-transparent",
+          disableTransition
+            ? "transition-none"
+            : "transition-[width] duration-200 ease-linear",
+          "group-data-[collapsible=offcanvas]:w-0",
+          "group-data-[side=right]:rotate-180",
+          variant === "floating" || variant === "inset"
+            ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
+        )}
+      />
+      <div
+        data-slot="sidebar-container"
+        className={cn(
+          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex",
+          disableTransition
+            ? "transition-none"
+            : "transition-[left,right,width] duration-200 ease-linear",
+          side === "left"
+            ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
+            : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
+          // Adjust the padding for floating and inset variants.
+          variant === "floating" || variant === "inset"
+            ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
+          className
+        )}
+        {...props}
+      >
+        <div
+          data-sidebar="sidebar"
+          data-slot="sidebar-inner"
+          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+        >
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function SidebarTrigger({
+  className,
+  onClick,
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { toggleSidebar } = useSidebar();
+
+  return (
+    <Button
+      data-sidebar="trigger"
+      data-slot="sidebar-trigger"
+      variant="ghost"
+      size="icon"
+      className={cn("size-7", className)}
+      onClick={event => {
+        onClick?.(event);
+        toggleSidebar();
+      }}
+      {...props}
+    >
+      <PanelLeftIcon />
+      <span className="sr-only">Toggle Sidebar</span>
+    </Button>
+  );
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+  const { toggleSidebar } = useSidebar();
+
+  return (
+    <button
+      data-sidebar="rail"
+      data-slot="sidebar-rail"
+      aria-label="Toggle Sidebar"
+      tabIndex={-1}
+      onClick={toggleSidebar}
+      title="Toggle Sidebar"
+      className={cn(
+        "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+        "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
+        "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
+        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
+        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+  return (
+    <main
+      data-slot="sidebar-inset"
+      className={cn(
+        "bg-background relative flex w-full flex-1 flex-col",
+        "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof Input>) {
+  return (
+    <Input
+      data-slot="sidebar-input"
+      data-sidebar="input"
+      className={cn("bg-background h-8 w-full shadow-none", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-header"
+      data-sidebar="header"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-footer"
+      data-sidebar="footer"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="sidebar-separator"
+      data-sidebar="separator"
+      className={cn("bg-sidebar-border mx-2 w-auto", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-content"
+      data-sidebar="content"
+      className={cn(
+        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group"
+      data-sidebar="group"
+      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroupLabel({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "div";
+
+  return (
+    <Comp
+      data-slot="sidebar-group-label"
+      data-sidebar="group-label"
+      className={cn(
+        "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroupAction({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "button";
+
+  return (
+    <Comp
+      data-slot="sidebar-group-action"
+      data-sidebar="group-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroupContent({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group-content"
+      data-sidebar="group-content"
+      className={cn("w-full text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu"
+      data-sidebar="menu"
+      className={cn("flex w-full min-w-0 flex-col gap-1", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-item"
+      data-sidebar="menu-item"
+      className={cn("group/menu-item relative", className)}
+      {...props}
+    />
+  );
+}
+
+const sidebarMenuButtonVariants = cva(
+  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+        outline:
+          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+      },
+      size: {
+        default: "h-8 text-sm",
+        sm: "h-7 text-xs",
+        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+);
+
+function SidebarMenuButton({
+  asChild = false,
+  isActive = false,
+  variant = "default",
+  size = "default",
+  tooltip,
+  className,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean;
+  isActive?: boolean;
+  tooltip?: string | React.ComponentProps<typeof TooltipContent>;
+} & VariantProps<typeof sidebarMenuButtonVariants>) {
+  const Comp = asChild ? Slot : "button";
+  const { isMobile, state } = useSidebar();
+
+  const button = (
+    <Comp
+      data-slot="sidebar-menu-button"
+      data-sidebar="menu-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
+      {...props}
+    />
+  );
+
+  if (!tooltip) {
+    return button;
+  }
+
+  if (typeof tooltip === "string") {
+    tooltip = {
+      children: tooltip,
+    };
+  }
+
+  return (
+    <Tooltip>
+      <TooltipTrigger asChild>{button}</TooltipTrigger>
+      <TooltipContent
+        side="right"
+        align="center"
+        hidden={state !== "collapsed" || isMobile}
+        {...tooltip}
+      />
+    </Tooltip>
+  );
+}
+
+function SidebarMenuAction({
+  className,
+  asChild = false,
+  showOnHover = false,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean;
+  showOnHover?: boolean;
+}) {
+  const Comp = asChild ? Slot : "button";
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-action"
+      data-sidebar="menu-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        showOnHover &&
+          "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuBadge({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-menu-badge"
+      data-sidebar="menu-badge"
+      className={cn(
+        "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
+        "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSkeleton({
+  className,
+  showIcon = false,
+  ...props
+}: React.ComponentProps<"div"> & {
+  showIcon?: boolean;
+}) {
+  // Random width between 50 to 90%.
+  const width = React.useMemo(() => {
+    return `${Math.floor(Math.random() * 40) + 50}%`;
+  }, []);
+
+  return (
+    <div
+      data-slot="sidebar-menu-skeleton"
+      data-sidebar="menu-skeleton"
+      className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
+      {...props}
+    >
+      {showIcon && (
+        <Skeleton
+          className="size-4 rounded-md"
+          data-sidebar="menu-skeleton-icon"
+        />
+      )}
+      <Skeleton
+        className="h-4 max-w-(--skeleton-width) flex-1"
+        data-sidebar="menu-skeleton-text"
+        style={
+          {
+            "--skeleton-width": width,
+          } as React.CSSProperties
+        }
+      />
+    </div>
+  );
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu-sub"
+      data-sidebar="menu-sub"
+      className={cn(
+        "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSubItem({
+  className,
+  ...props
+}: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-sub-item"
+      data-sidebar="menu-sub-item"
+      className={cn("group/menu-sub-item relative", className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSubButton({
+  asChild = false,
+  size = "md",
+  isActive = false,
+  className,
+  ...props
+}: React.ComponentProps<"a"> & {
+  asChild?: boolean;
+  size?: "sm" | "md";
+  isActive?: boolean;
+}) {
+  const Comp = asChild ? Slot : "a";
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-sub-button"
+      data-sidebar="menu-sub-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+        "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+        size === "sm" && "text-xs",
+        size === "md" && "text-sm",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarGroupAction,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarHeader,
+  SidebarInput,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuAction,
+  SidebarMenuBadge,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSkeleton,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+  SidebarRail,
+  SidebarSeparator,
+  SidebarTrigger,
+  useSidebar
+};
+

+ 13 - 0
client/src/components/ui/skeleton.tsx

@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="skeleton"
+      className={cn("bg-accent animate-pulse rounded-md", className)}
+      {...props}
+    />
+  );
+}
+
+export { Skeleton };

+ 61 - 0
client/src/components/ui/slider.tsx

@@ -0,0 +1,61 @@
+import * as React from "react";
+import * as SliderPrimitive from "@radix-ui/react-slider";
+
+import { cn } from "@/lib/utils";
+
+function Slider({
+  className,
+  defaultValue,
+  value,
+  min = 0,
+  max = 100,
+  ...props
+}: React.ComponentProps<typeof SliderPrimitive.Root>) {
+  const _values = React.useMemo(
+    () =>
+      Array.isArray(value)
+        ? value
+        : Array.isArray(defaultValue)
+          ? defaultValue
+          : [min, max],
+    [value, defaultValue, min, max]
+  );
+
+  return (
+    <SliderPrimitive.Root
+      data-slot="slider"
+      defaultValue={defaultValue}
+      value={value}
+      min={min}
+      max={max}
+      className={cn(
+        "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
+        className
+      )}
+      {...props}
+    >
+      <SliderPrimitive.Track
+        data-slot="slider-track"
+        className={cn(
+          "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
+        )}
+      >
+        <SliderPrimitive.Range
+          data-slot="slider-range"
+          className={cn(
+            "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
+          )}
+        />
+      </SliderPrimitive.Track>
+      {Array.from({ length: _values.length }, (_, index) => (
+        <SliderPrimitive.Thumb
+          data-slot="slider-thumb"
+          key={index}
+          className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
+        />
+      ))}
+    </SliderPrimitive.Root>
+  );
+}
+
+export { Slider };

+ 23 - 0
client/src/components/ui/sonner.tsx

@@ -0,0 +1,23 @@
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+
+const Toaster = ({ ...props }: ToasterProps) => {
+  const { theme = "system" } = useTheme();
+
+  return (
+    <Sonner
+      theme={theme as ToasterProps["theme"]}
+      className="toaster group"
+      style={
+        {
+          "--normal-bg": "var(--popover)",
+          "--normal-text": "var(--popover-foreground)",
+          "--normal-border": "var(--border)",
+        } as React.CSSProperties
+      }
+      {...props}
+    />
+  );
+};
+
+export { Toaster };

+ 16 - 0
client/src/components/ui/spinner.tsx

@@ -0,0 +1,16 @@
+import { Loader2Icon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
+  return (
+    <Loader2Icon
+      role="status"
+      aria-label="Loading"
+      className={cn("size-4 animate-spin", className)}
+      {...props}
+    />
+  );
+}
+
+export { Spinner };

+ 29 - 0
client/src/components/ui/switch.tsx

@@ -0,0 +1,29 @@
+import * as React from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/utils";
+
+function Switch({
+  className,
+  ...props
+}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
+  return (
+    <SwitchPrimitive.Root
+      data-slot="switch"
+      className={cn(
+        "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+        className
+      )}
+      {...props}
+    >
+      <SwitchPrimitive.Thumb
+        data-slot="switch-thumb"
+        className={cn(
+          "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
+        )}
+      />
+    </SwitchPrimitive.Root>
+  );
+}
+
+export { Switch };

+ 114 - 0
client/src/components/ui/table.tsx

@@ -0,0 +1,114 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Table({ className, ...props }: React.ComponentProps<"table">) {
+  return (
+    <div
+      data-slot="table-container"
+      className="relative w-full overflow-x-auto"
+    >
+      <table
+        data-slot="table"
+        className={cn("w-full caption-bottom text-sm", className)}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+  return (
+    <thead
+      data-slot="table-header"
+      className={cn("[&_tr]:border-b", className)}
+      {...props}
+    />
+  );
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+  return (
+    <tbody
+      data-slot="table-body"
+      className={cn("[&_tr:last-child]:border-0", className)}
+      {...props}
+    />
+  );
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+  return (
+    <tfoot
+      data-slot="table-footer"
+      className={cn(
+        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+  return (
+    <tr
+      data-slot="table-row"
+      className={cn(
+        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+  return (
+    <th
+      data-slot="table-head"
+      className={cn(
+        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+  return (
+    <td
+      data-slot="table-cell"
+      className={cn(
+        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableCaption({
+  className,
+  ...props
+}: React.ComponentProps<"caption">) {
+  return (
+    <caption
+      data-slot="table-caption"
+      className={cn("text-muted-foreground mt-4 text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Table,
+  TableHeader,
+  TableBody,
+  TableFooter,
+  TableHead,
+  TableRow,
+  TableCell,
+  TableCaption,
+};

+ 64 - 0
client/src/components/ui/tabs.tsx

@@ -0,0 +1,64 @@
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+
+import { cn } from "@/lib/utils";
+
+function Tabs({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Root>) {
+  return (
+    <TabsPrimitive.Root
+      data-slot="tabs"
+      className={cn("flex flex-col gap-2", className)}
+      {...props}
+    />
+  );
+}
+
+function TabsList({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.List>) {
+  return (
+    <TabsPrimitive.List
+      data-slot="tabs-list"
+      className={cn(
+        "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TabsTrigger({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
+  return (
+    <TabsPrimitive.Trigger
+      data-slot="tabs-trigger"
+      className={cn(
+        "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  );
+}
+
+function TabsContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Content>) {
+  return (
+    <TabsPrimitive.Content
+      data-slot="tabs-content"
+      className={cn("flex-1 outline-none", className)}
+      {...props}
+    />
+  );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };

+ 67 - 0
client/src/components/ui/textarea.tsx

@@ -0,0 +1,67 @@
+import { useDialogComposition } from "@/components/ui/dialog";
+import { useComposition } from "@/hooks/useComposition";
+import { cn } from "@/lib/utils";
+import * as React from "react";
+
+function Textarea({
+  className,
+  onKeyDown,
+  onCompositionStart,
+  onCompositionEnd,
+  ...props
+}: React.ComponentProps<"textarea">) {
+  // Get dialog composition context if available (will be no-op if not inside Dialog)
+  const dialogComposition = useDialogComposition();
+
+  // Add composition event handlers to support input method editor (IME) for CJK languages.
+  const {
+    onCompositionStart: handleCompositionStart,
+    onCompositionEnd: handleCompositionEnd,
+    onKeyDown: handleKeyDown,
+  } = useComposition<HTMLTextAreaElement>({
+    onKeyDown: (e) => {
+      // Check if this is an Enter key that should be blocked
+      const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
+
+      // If Enter key is pressed while composing or just after composition ended,
+      // don't call the user's onKeyDown (this blocks the business logic)
+      // Note: For textarea, Shift+Enter should still work for newlines
+      if (e.key === "Enter" && !e.shiftKey && isComposing) {
+        return;
+      }
+
+      // Otherwise, call the user's onKeyDown
+      onKeyDown?.(e);
+    },
+    onCompositionStart: e => {
+      dialogComposition.setComposing(true);
+      onCompositionStart?.(e);
+    },
+    onCompositionEnd: e => {
+      // Mark that composition just ended - this helps handle the Enter key that confirms input
+      dialogComposition.markCompositionEnd();
+      // Delay setting composing to false to handle Safari's event order
+      // In Safari, compositionEnd fires before the ESC keydown event
+      setTimeout(() => {
+        dialogComposition.setComposing(false);
+      }, 100);
+      onCompositionEnd?.(e);
+    },
+  });
+
+  return (
+    <textarea
+      data-slot="textarea"
+      className={cn(
+        "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        className
+      )}
+      onCompositionStart={handleCompositionStart}
+      onCompositionEnd={handleCompositionEnd}
+      onKeyDown={handleKeyDown}
+      {...props}
+    />
+  );
+}
+
+export { Textarea };

+ 73 - 0
client/src/components/ui/toggle-group.tsx

@@ -0,0 +1,73 @@
+"use client";
+
+import * as React from "react";
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import { type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { toggleVariants } from "@/components/ui/toggle";
+
+const ToggleGroupContext = React.createContext<
+  VariantProps<typeof toggleVariants>
+>({
+  size: "default",
+  variant: "default",
+});
+
+function ToggleGroup({
+  className,
+  variant,
+  size,
+  children,
+  ...props
+}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
+  VariantProps<typeof toggleVariants>) {
+  return (
+    <ToggleGroupPrimitive.Root
+      data-slot="toggle-group"
+      data-variant={variant}
+      data-size={size}
+      className={cn(
+        "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
+        className
+      )}
+      {...props}
+    >
+      <ToggleGroupContext.Provider value={{ variant, size }}>
+        {children}
+      </ToggleGroupContext.Provider>
+    </ToggleGroupPrimitive.Root>
+  );
+}
+
+function ToggleGroupItem({
+  className,
+  children,
+  variant,
+  size,
+  ...props
+}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
+  VariantProps<typeof toggleVariants>) {
+  const context = React.useContext(ToggleGroupContext);
+
+  return (
+    <ToggleGroupPrimitive.Item
+      data-slot="toggle-group-item"
+      data-variant={context.variant || variant}
+      data-size={context.size || size}
+      className={cn(
+        toggleVariants({
+          variant: context.variant || variant,
+          size: context.size || size,
+        }),
+        "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
+        className
+      )}
+      {...props}
+    >
+      {children}
+    </ToggleGroupPrimitive.Item>
+  );
+}
+
+export { ToggleGroup, ToggleGroupItem };

+ 45 - 0
client/src/components/ui/toggle.tsx

@@ -0,0 +1,45 @@
+import * as React from "react";
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const toggleVariants = cva(
+  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        outline:
+          "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+      },
+      size: {
+        default: "h-9 px-2 min-w-9",
+        sm: "h-8 px-1.5 min-w-8",
+        lg: "h-10 px-2.5 min-w-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+);
+
+function Toggle({
+  className,
+  variant,
+  size,
+  ...props
+}: React.ComponentProps<typeof TogglePrimitive.Root> &
+  VariantProps<typeof toggleVariants>) {
+  return (
+    <TogglePrimitive.Root
+      data-slot="toggle"
+      className={cn(toggleVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+}
+
+export { Toggle, toggleVariants };

+ 59 - 0
client/src/components/ui/tooltip.tsx

@@ -0,0 +1,59 @@
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+  delayDuration = 0,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delayDuration={delayDuration}
+      {...props}
+    />
+  );
+}
+
+function Tooltip({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return (
+    <TooltipProvider>
+      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+    </TooltipProvider>
+  );
+}
+
+function TooltipTrigger({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
+}
+
+function TooltipContent({
+  className,
+  sideOffset = 0,
+  children,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Content
+        data-slot="tooltip-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+      </TooltipPrimitive.Content>
+    </TooltipPrimitive.Portal>
+  );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

+ 21 - 0
client/src/const.ts

@@ -0,0 +1,21 @@
+export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
+
+// Generate login URL at runtime so redirect URI reflects the current origin.
+export const getLoginUrl = (returnPath?: string) => {
+  const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
+  const appId = import.meta.env.VITE_APP_ID;
+  const redirectUri = `${window.location.origin}/api/oauth/callback`;
+  // Encode both the redirect URI and optional return path in state
+  const statePayload = returnPath
+    ? JSON.stringify({ redirectUri, returnPath })
+    : redirectUri;
+  const state = btoa(statePayload);
+
+  const url = new URL(`${oauthPortalUrl}/app-auth`);
+  url.searchParams.set("appId", appId);
+  url.searchParams.set("redirectUri", redirectUri);
+  url.searchParams.set("state", state);
+  url.searchParams.set("type", "signIn");
+
+  return url.toString();
+};

+ 64 - 0
client/src/contexts/ThemeContext.tsx

@@ -0,0 +1,64 @@
+import React, { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "light" | "dark";
+
+interface ThemeContextType {
+  theme: Theme;
+  toggleTheme?: () => void;
+  switchable: boolean;
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+interface ThemeProviderProps {
+  children: React.ReactNode;
+  defaultTheme?: Theme;
+  switchable?: boolean;
+}
+
+export function ThemeProvider({
+  children,
+  defaultTheme = "light",
+  switchable = false,
+}: ThemeProviderProps) {
+  const [theme, setTheme] = useState<Theme>(() => {
+    if (switchable) {
+      const stored = localStorage.getItem("theme");
+      return (stored as Theme) || defaultTheme;
+    }
+    return defaultTheme;
+  });
+
+  useEffect(() => {
+    const root = document.documentElement;
+    if (theme === "dark") {
+      root.classList.add("dark");
+    } else {
+      root.classList.remove("dark");
+    }
+
+    if (switchable) {
+      localStorage.setItem("theme", theme);
+    }
+  }, [theme, switchable]);
+
+  const toggleTheme = switchable
+    ? () => {
+        setTheme(prev => (prev === "light" ? "dark" : "light"));
+      }
+    : undefined;
+
+  return (
+    <ThemeContext.Provider value={{ theme, toggleTheme, switchable }}>
+      {children}
+    </ThemeContext.Provider>
+  );
+}
+
+export function useTheme() {
+  const context = useContext(ThemeContext);
+  if (!context) {
+    throw new Error("useTheme must be used within ThemeProvider");
+  }
+  return context;
+}

+ 81 - 0
client/src/hooks/useComposition.ts

@@ -0,0 +1,81 @@
+import { useRef } from "react";
+import { usePersistFn } from "./usePersistFn";
+
+export interface UseCompositionReturn<
+  T extends HTMLInputElement | HTMLTextAreaElement,
+> {
+  onCompositionStart: React.CompositionEventHandler<T>;
+  onCompositionEnd: React.CompositionEventHandler<T>;
+  onKeyDown: React.KeyboardEventHandler<T>;
+  isComposing: () => boolean;
+}
+
+export interface UseCompositionOptions<
+  T extends HTMLInputElement | HTMLTextAreaElement,
+> {
+  onKeyDown?: React.KeyboardEventHandler<T>;
+  onCompositionStart?: React.CompositionEventHandler<T>;
+  onCompositionEnd?: React.CompositionEventHandler<T>;
+}
+
+type TimerResponse = ReturnType<typeof setTimeout>;
+
+export function useComposition<
+  T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement,
+>(options: UseCompositionOptions<T> = {}): UseCompositionReturn<T> {
+  const {
+    onKeyDown: originalOnKeyDown,
+    onCompositionStart: originalOnCompositionStart,
+    onCompositionEnd: originalOnCompositionEnd,
+  } = options;
+
+  const c = useRef(false);
+  const timer = useRef<TimerResponse | null>(null);
+  const timer2 = useRef<TimerResponse | null>(null);
+
+  const onCompositionStart = usePersistFn((e: React.CompositionEvent<T>) => {
+    if (timer.current) {
+      clearTimeout(timer.current);
+      timer.current = null;
+    }
+    if (timer2.current) {
+      clearTimeout(timer2.current);
+      timer2.current = null;
+    }
+    c.current = true;
+    originalOnCompositionStart?.(e);
+  });
+
+  const onCompositionEnd = usePersistFn((e: React.CompositionEvent<T>) => {
+    // 使用两层 setTimeout 来处理 Safari 浏览器中 compositionEnd 先于 onKeyDown 触发的问题
+    timer.current = setTimeout(() => {
+      timer2.current = setTimeout(() => {
+        c.current = false;
+      });
+    });
+    originalOnCompositionEnd?.(e);
+  });
+
+  const onKeyDown = usePersistFn((e: React.KeyboardEvent<T>) => {
+    // 在 composition 状态下,阻止 ESC 和 Enter(非 shift+Enter)事件的冒泡
+    if (
+      c.current &&
+      (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey))
+    ) {
+      e.stopPropagation();
+      return;
+    }
+    originalOnKeyDown?.(e);
+  });
+
+  const isComposing = usePersistFn(() => {
+    return c.current;
+  });
+
+  return {
+    onCompositionStart,
+    onCompositionEnd,
+    onKeyDown,
+    isComposing,
+  };
+}

+ 21 - 0
client/src/hooks/useMobile.tsx

@@ -0,0 +1,21 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
+    undefined
+  );
+
+  React.useEffect(() => {
+    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+    const onChange = () => {
+      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+    };
+    mql.addEventListener("change", onChange);
+    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+    return () => mql.removeEventListener("change", onChange);
+  }, []);
+
+  return !!isMobile;
+}

+ 20 - 0
client/src/hooks/usePersistFn.ts

@@ -0,0 +1,20 @@
+import { useRef } from "react";
+
+type noop = (...args: any[]) => any;
+
+/**
+ * usePersistFn instead of useCallback to reduce cognitive load
+ */
+export function usePersistFn<T extends noop>(fn: T) {
+  const fnRef = useRef<T>(fn);
+  fnRef.current = fn;
+
+  const persistFn = useRef<T>(null);
+  if (!persistFn.current) {
+    persistFn.current = function (this: unknown, ...args) {
+      return fnRef.current!.apply(this, args);
+    } as T;
+  }
+
+  return persistFn.current!;
+}

+ 150 - 0
client/src/index.css

@@ -0,0 +1,150 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-card: var(--card);
+  --color-card-foreground: var(--card-foreground);
+  --color-popover: var(--popover);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-primary: var(--primary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-secondary: var(--secondary);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-muted: var(--muted);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-accent: var(--accent);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-destructive: var(--destructive);
+  --color-destructive-foreground: var(--destructive-foreground);
+  --color-border: var(--border);
+  --color-input: var(--input);
+  --color-ring: var(--ring);
+  --color-chart-1: var(--chart-1);
+  --color-chart-2: var(--chart-2);
+  --color-chart-3: var(--chart-3);
+  --color-chart-4: var(--chart-4);
+  --color-chart-5: var(--chart-5);
+  --color-sidebar: var(--sidebar);
+  --color-sidebar-foreground: var(--sidebar-foreground);
+  --color-sidebar-primary: var(--sidebar-primary);
+  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+  --color-sidebar-accent: var(--sidebar-accent);
+  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+  --color-sidebar-border: var(--sidebar-border);
+  --color-sidebar-ring: var(--sidebar-ring);
+  --font-display: 'Playfair Display', Georgia, serif;
+  --font-body: 'Source Sans 3', system-ui, sans-serif;
+  --font-mono: 'Fira Code', monospace;
+  --color-forest: oklch(0.30 0.07 155);
+  --color-forest-light: oklch(0.40 0.07 155);
+  --color-taupe: oklch(0.55 0.02 60);
+  --color-taupe-light: oklch(0.75 0.02 60);
+  --color-cream: oklch(0.97 0.01 90);
+  --color-cream-dark: oklch(0.93 0.01 90);
+  --color-terracotta: oklch(0.55 0.15 35);
+  --color-terracotta-light: oklch(0.65 0.12 35);
+  --color-linen: oklch(0.95 0.008 80);
+}
+
+:root {
+  --primary: oklch(0.30 0.07 155);
+  --primary-foreground: oklch(0.97 0.01 90);
+  --sidebar-primary: oklch(0.30 0.07 155);
+  --sidebar-primary-foreground: oklch(0.97 0.01 90);
+  --chart-1: oklch(0.30 0.07 155);
+  --chart-2: oklch(0.55 0.15 35);
+  --chart-3: oklch(0.55 0.02 60);
+  --chart-4: oklch(0.40 0.07 155);
+  --chart-5: oklch(0.65 0.12 35);
+  --radius: 0.5rem;
+  --background: oklch(0.97 0.01 90);
+  --foreground: oklch(0.22 0.02 60);
+  --card: oklch(0.99 0.005 90);
+  --card-foreground: oklch(0.22 0.02 60);
+  --popover: oklch(0.99 0.005 90);
+  --popover-foreground: oklch(0.22 0.02 60);
+  --secondary: oklch(0.95 0.008 80);
+  --secondary-foreground: oklch(0.35 0.02 60);
+  --muted: oklch(0.93 0.01 90);
+  --muted-foreground: oklch(0.50 0.02 60);
+  --accent: oklch(0.93 0.01 90);
+  --accent-foreground: oklch(0.22 0.02 60);
+  --destructive: oklch(0.55 0.15 35);
+  --destructive-foreground: oklch(0.97 0.01 90);
+  --border: oklch(0.88 0.01 80);
+  --input: oklch(0.88 0.01 80);
+  --ring: oklch(0.30 0.07 155);
+  --sidebar: oklch(0.97 0.01 90);
+  --sidebar-foreground: oklch(0.22 0.02 60);
+  --sidebar-accent: oklch(0.93 0.01 90);
+  --sidebar-accent-foreground: oklch(0.22 0.02 60);
+  --sidebar-border: oklch(0.88 0.01 80);
+  --sidebar-ring: oklch(0.30 0.07 155);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+  body {
+    @apply bg-background text-foreground;
+    font-family: 'Source Sans 3', system-ui, sans-serif;
+    font-size: 17px;
+    line-height: 1.75;
+  }
+  h1, h2, h3, h4, h5, h6 {
+    font-family: 'Playfair Display', Georgia, serif;
+  }
+  code, pre {
+    font-family: 'Fira Code', monospace;
+  }
+  button:not(:disabled),
+  [role="button"]:not([aria-disabled="true"]),
+  [type="button"]:not(:disabled),
+  [type="submit"]:not(:disabled),
+  [type="reset"]:not(:disabled),
+  a[href],
+  select:not(:disabled),
+  input[type="checkbox"]:not(:disabled),
+  input[type="radio"]:not(:disabled) {
+    @apply cursor-pointer;
+  }
+}
+
+@layer components {
+  .container {
+    width: 100%;
+    margin-left: auto;
+    margin-right: auto;
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+
+  .flex {
+    min-height: 0;
+    min-width: 0;
+  }
+
+  @media (min-width: 640px) {
+    .container {
+      padding-left: 1.5rem;
+      padding-right: 1.5rem;
+    }
+  }
+
+  @media (min-width: 1024px) {
+    .container {
+      padding-left: 2rem;
+      padding-right: 2rem;
+      max-width: 1280px;
+    }
+  }
+}

+ 4 - 0
client/src/lib/trpc.ts

@@ -0,0 +1,4 @@
+import { createTRPCReact } from "@trpc/react-query";
+import type { AppRouter } from "../../../server/routers";
+
+export const trpc = createTRPCReact<AppRouter>();

+ 6 - 0
client/src/lib/utils.ts

@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}

+ 63 - 0
client/src/main.tsx

@@ -0,0 +1,63 @@
+import { trpc } from "@/lib/trpc";
+import { UNAUTHED_ERR_MSG } from '@shared/const';
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { httpBatchLink, TRPCClientError } from "@trpc/client";
+import { createRoot } from "react-dom/client";
+import superjson from "superjson";
+import App from "./App";
+import { getLoginUrl } from "./const";
+import "./index.css";
+
+const queryClient = new QueryClient();
+
+const redirectToLoginIfUnauthorized = (error: unknown) => {
+  if (!(error instanceof TRPCClientError)) return;
+  if (typeof window === "undefined") return;
+
+  const isUnauthorized = error.message === UNAUTHED_ERR_MSG;
+
+  if (!isUnauthorized) return;
+
+  // Redirect to our custom login page instead of OAuth
+  const currentPath = window.location.pathname;
+  window.location.href = `/login?returnTo=${encodeURIComponent(currentPath)}`;
+};
+
+queryClient.getQueryCache().subscribe(event => {
+  if (event.type === "updated" && event.action.type === "error") {
+    const error = event.query.state.error;
+    redirectToLoginIfUnauthorized(error);
+    console.error("[API Query Error]", error);
+  }
+});
+
+queryClient.getMutationCache().subscribe(event => {
+  if (event.type === "updated" && event.action.type === "error") {
+    const error = event.mutation.state.error;
+    redirectToLoginIfUnauthorized(error);
+    console.error("[API Mutation Error]", error);
+  }
+});
+
+const trpcClient = trpc.createClient({
+  links: [
+    httpBatchLink({
+      url: "/api/trpc",
+      transformer: superjson,
+      fetch(input, init) {
+        return globalThis.fetch(input, {
+          ...(init ?? {}),
+          credentials: "include",
+        });
+      },
+    }),
+  ],
+});
+
+createRoot(document.getElementById("root")!).render(
+  <trpc.Provider client={trpcClient} queryClient={queryClient}>
+    <QueryClientProvider client={queryClient}>
+      <App />
+    </QueryClientProvider>
+  </trpc.Provider>
+);

+ 213 - 0
client/src/pages/AcceptInvite.tsx

@@ -0,0 +1,213 @@
+/**
+ * Accept Invitation Page — Public page where invited users accept their invitation
+ */
+import { useState, useEffect } from "react";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { getLoginUrl } from "@/const";
+import { toast } from "sonner";
+import {
+  CheckCircle2, XCircle, Clock, Mail, Shield,
+  Crown, Headphones, User, LogIn, Loader2,
+} from "lucide-react";
+import { useRoute, useLocation } from "wouter";
+
+const ROLE_LABELS: Record<string, { icon: React.ReactNode; label: string; description: string; color: string }> = {
+  admin: {
+    icon: <Crown className="w-5 h-5" />,
+    label: "Admin",
+    description: "Full access to conversations, workflow designer, and user management",
+    color: "#14532D",
+  },
+  agent: {
+    icon: <Headphones className="w-5 h-5" />,
+    label: "Agent",
+    description: "Access to the agent dashboard to monitor and reply to conversations",
+    color: "#0369a1",
+  },
+  user: {
+    icon: <User className="w-5 h-5" />,
+    label: "User",
+    description: "Basic access to the platform",
+    color: "#78716C",
+  },
+};
+
+export default function AcceptInvite() {
+  const [, params] = useRoute("/invite/:token");
+  const [, navigate] = useLocation();
+  const token = params?.token || "";
+  const { user, isAuthenticated, loading: authLoading } = useAuth();
+  const [accepted, setAccepted] = useState(false);
+
+  const { data: validation, isLoading: validating, error: validationError } = trpc.invitations.validate.useQuery(
+    { token },
+    { enabled: !!token }
+  );
+
+  const acceptMutation = trpc.invitations.accept.useMutation({
+    onSuccess: (data) => {
+      setAccepted(true);
+      toast.success(`Welcome! You've been assigned the ${data.role} role.`);
+    },
+    onError: (error) => {
+      toast.error("Failed to accept invitation", { description: error.message });
+    },
+  });
+
+  const handleAccept = () => {
+    if (token) {
+      acceptMutation.mutate({ token });
+    }
+  };
+
+  const handleGoToDashboard = () => {
+    navigate("/dashboard");
+  };
+
+  // Loading state
+  if (authLoading || validating) {
+    return (
+      <div className="min-h-screen flex items-center justify-center" style={{ background: "#FFFBEB" }}>
+        <div className="text-center">
+          <Loader2 className="w-8 h-8 animate-spin mx-auto" style={{ color: "#14532D" }} />
+          <p className="text-sm mt-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Validating invitation...
+          </p>
+        </div>
+      </div>
+    );
+  }
+
+  // Invalid or expired invitation
+  if (validation && !validation.valid) {
+    return (
+      <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
+        <div className="max-w-md w-full p-8 rounded-2xl text-center" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
+          <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#dc262614" }}>
+            <XCircle className="w-8 h-8" style={{ color: "#dc2626" }} />
+          </div>
+          <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+            Invalid Invitation
+          </h1>
+          <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            {validation.reason}
+          </p>
+          <Button onClick={() => navigate("/")} variant="outline" style={{ borderColor: "#e7e0d5" }}>
+            Go to Home
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+  // Successfully accepted
+  if (accepted) {
+    return (
+      <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
+        <div className="max-w-md w-full p-8 rounded-2xl text-center" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
+          <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#16a34a14" }}>
+            <CheckCircle2 className="w-8 h-8" style={{ color: "#16a34a" }} />
+          </div>
+          <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+            Welcome Aboard!
+          </h1>
+          <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Your invitation has been accepted. You now have access to the Homelegance chatbot system.
+          </p>
+          <Button onClick={handleGoToDashboard} className="text-white" style={{ background: "#14532D" }}>
+            Go to Dashboard
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+  // Valid invitation — show details
+  if (validation && validation.valid) {
+    const roleConfig = ROLE_LABELS[validation.role] || ROLE_LABELS.user;
+
+    return (
+      <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
+        <div className="max-w-md w-full p-8 rounded-2xl" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
+          <div className="text-center mb-6">
+            <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#14532D14" }}>
+              <Mail className="w-8 h-8" style={{ color: "#14532D" }} />
+            </div>
+            <h1 className="text-2xl font-bold mb-1" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+              You're Invited!
+            </h1>
+            <p className="text-sm" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+              <strong>{validation.invitedBy}</strong> has invited you to join the Homelegance chatbot team.
+            </p>
+          </div>
+
+          {/* Invitation Details */}
+          <div className="space-y-3 mb-6">
+            <div className="p-4 rounded-xl" style={{ background: roleConfig.color + "08", border: `1px solid ${roleConfig.color}20` }}>
+              <div className="flex items-center gap-3">
+                <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: roleConfig.color + "18", color: roleConfig.color }}>
+                  {roleConfig.icon}
+                </div>
+                <div>
+                  <div className="text-sm font-semibold" style={{ color: roleConfig.color }}>{roleConfig.label} Role</div>
+                  <div className="text-xs" style={{ color: "#78716C" }}>{roleConfig.description}</div>
+                </div>
+              </div>
+            </div>
+
+            <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
+              <Mail className="w-3.5 h-3.5" />
+              <span>Invited: <strong>{validation.email}</strong></span>
+            </div>
+
+            <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
+              <Clock className="w-3.5 h-3.5" />
+              <span>Expires: {validation.expiresAt ? new Date(validation.expiresAt).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" }) : "—"}</span>
+            </div>
+
+            {validation.message && (
+              <div className="p-3 rounded-lg" style={{ background: "#f5f0e8" }}>
+                <p className="text-sm italic" style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}>
+                  "{validation.message}"
+                </p>
+              </div>
+            )}
+          </div>
+
+          {/* Action */}
+          {isAuthenticated ? (
+            <Button
+              onClick={handleAccept}
+              disabled={acceptMutation.isPending}
+              className="w-full text-white"
+              style={{ background: "#14532D" }}
+            >
+              {acceptMutation.isPending ? (
+                <><Loader2 className="w-4 h-4 animate-spin mr-2" /> Accepting...</>
+              ) : (
+                <><CheckCircle2 className="w-4 h-4 mr-2" /> Accept Invitation</>
+              )}
+            </Button>
+          ) : (
+            <div className="space-y-3">
+              <p className="text-xs text-center" style={{ color: "#a8a29e" }}>
+                You need to sign in first to accept this invitation.
+              </p>
+              <a
+                href={`/login?returnTo=${encodeURIComponent(`/invite/${token}`)}`}
+                className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-xl text-sm font-semibold text-white transition-colors"
+                style={{ background: "#14532D" }}
+              >
+                <LogIn className="w-4 h-4" /> Sign In to Accept
+              </a>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+
+  return null;
+}

+ 981 - 0
client/src/pages/AgentDashboard.tsx

@@ -0,0 +1,981 @@
+/**
+ * Agent Dashboard — Conversations view (wrapped by DashboardLayout)
+ * Redesigned for large-volume conversation management with
+ * search, filters (status, agent, date), pagination, sorting, and bulk actions.
+ */
+import { useState, useRef, useEffect, useMemo, useCallback } from "react";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  MessageSquare, CheckCircle2, AlertTriangle, Clock,
+  Send, Bot, User, Headphones, ArrowLeft, ArrowUpDown,
+  BarChart3, X, Search, Filter, ChevronLeft, ChevronRight,
+  Calendar, UserCheck, Trash2, MoreHorizontal, MessageCircle,
+  Sparkles, Bell, BellRing,
+} from "lucide-react";
+import { Streamdown } from "streamdown";
+import { toast } from "sonner";
+
+/* ─── Bot name constant ─── */
+const BOT_NAME = "Ellie";
+
+/* ─── Status badge colors ─── */
+const STATUS_STYLES: Record<string, { bg: string; text: string; label: string; dot: string }> = {
+  active: { bg: "#14532D12", text: "#14532D", label: "Active", dot: "#22c55e" },
+  escalated: { bg: "#C2410C12", text: "#C2410C", label: "Escalated", dot: "#f97316" },
+  resolved: { bg: "#0369a112", text: "#0369a1", label: "Resolved", dot: "#3b82f6" },
+  closed: { bg: "#78716C12", text: "#78716C", label: "Closed", dot: "#a8a29e" },
+};
+
+/* ─── Stats Card ─── */
+function StatCard({ icon, label, value, color, isActive, onClick }: {
+  icon: React.ReactNode; label: string; value: number; color: string;
+  isActive?: boolean; onClick?: () => void;
+}) {
+  return (
+    <button
+      onClick={onClick}
+      className="p-3 rounded-xl border transition-all text-left w-full"
+      style={{
+        background: isActive ? color + "08" : "#fff",
+        borderColor: isActive ? color + "40" : "#e7e0d5",
+        boxShadow: isActive ? `0 0 0 1px ${color}20` : undefined,
+      }}
+    >
+      <div className="flex items-center gap-2.5">
+        <div className="w-9 h-9 rounded-lg flex items-center justify-center" style={{ background: color + "14", color }}>
+          {icon}
+        </div>
+        <div>
+          <div className="text-xl font-bold" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{value}</div>
+          <div className="text-[11px]" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>{label}</div>
+        </div>
+      </div>
+    </button>
+  );
+}
+
+/* ─── Conversation Row (table-style for large volumes) ─── */
+function ConversationRow({
+  conversation,
+  isSelected,
+  isChecked,
+  onClick,
+  onCheck,
+}: {
+  conversation: any;
+  isSelected: boolean;
+  isChecked: boolean;
+  onClick: () => void;
+  onCheck: (checked: boolean) => void;
+}) {
+  const style = STATUS_STYLES[conversation.status] || STATUS_STYLES.active;
+  const createdDate = new Date(conversation.createdAt);
+  const updatedDate = new Date(conversation.updatedAt);
+  const isToday = new Date().toDateString() === updatedDate.toDateString();
+
+  return (
+    <div
+      className="flex items-center gap-3 px-3 py-2.5 border-b transition-all cursor-pointer hover:bg-stone-50"
+      style={{
+        background: isSelected ? "#14532D06" : isChecked ? "#fef3c7" : "transparent",
+        borderColor: "#f5f0e8",
+      }}
+    >
+      {/* Checkbox */}
+      <input
+        type="checkbox"
+        checked={isChecked}
+        onChange={(e) => { e.stopPropagation(); onCheck(e.target.checked); }}
+        className="w-4 h-4 rounded border-stone-300 accent-green-800 shrink-0"
+      />
+
+      {/* Main content — clickable */}
+      <div className="flex-1 min-w-0 flex items-center gap-3" onClick={onClick}>
+        {/* Status dot + visitor */}
+        <div className="flex items-center gap-2 min-w-0 w-36 shrink-0">
+          <div className="w-2 h-2 rounded-full shrink-0" style={{ background: style.dot }} />
+          <span className="text-sm font-medium truncate" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+            {conversation.visitorName || "Visitor"}
+          </span>
+        </div>
+
+        {/* Customer ID */}
+        <div className="w-28 shrink-0 hidden lg:block">
+          <span className="text-xs font-mono truncate block" style={{ color: conversation.customerId ? "#14532D" : "#d6d3d1" }}>
+            {conversation.customerId || "—"}
+          </span>
+        </div>
+
+        {/* Sales Rep */}
+        <div className="w-28 shrink-0 hidden lg:block">
+          <span className="text-xs truncate block" style={{ color: conversation.salesRep ? "#78716C" : "#d6d3d1" }}>
+            {conversation.salesRep || "—"}
+          </span>
+        </div>
+
+        {/* Agent Name */}
+        <div className="w-28 shrink-0 hidden lg:block">
+          <span className="text-xs truncate block" style={{ color: conversation.agentName ? "#0369a1" : "#d6d3d1" }}>
+            {conversation.agentName || "—"}
+          </span>
+        </div>
+
+        {/* Status badge */}
+        <div className="w-24 shrink-0">
+          <span
+            className="text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wider"
+            style={{ background: style.bg, color: style.text }}
+          >
+            {style.label}
+          </span>
+        </div>
+
+        {/* Message count */}
+        <div className="w-14 shrink-0 flex items-center gap-1">
+          <MessageCircle className="w-3 h-3" style={{ color: "#a8a29e" }} />
+          <span className="text-xs" style={{ color: "#78716C" }}>{conversation.messageCount || 0}</span>
+        </div>
+
+        {/* Email / session */}
+        <div className="flex-1 min-w-0 hidden xl:block">
+          <span className="text-xs truncate block" style={{ color: "#a8a29e" }}>
+            {conversation.visitorEmail || `#${conversation.sessionId?.slice(0, 10)}`}
+          </span>
+        </div>
+
+        {/* Date */}
+        <div className="w-28 shrink-0 text-right hidden md:block">
+          <span className="text-xs" style={{ color: "#78716C" }}>
+            {isToday
+              ? updatedDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
+              : updatedDate.toLocaleDateString([], { month: "short", day: "numeric" })}
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Message Bubble ─── */
+function MessageBubble({ message }: { message: any }) {
+  const isVisitor = message.sender === "visitor";
+  const isBot = message.sender === "bot";
+
+  return (
+    <div className={`flex gap-2 ${isVisitor ? "justify-end" : "justify-start"}`}>
+      {!isVisitor && (
+        <div
+          className="w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-1"
+          style={{ background: isBot ? "#14532D" : "#C2410C", color: "#fff" }}
+        >
+          {isBot ? <Sparkles className="w-3.5 h-3.5" /> : <Headphones className="w-3.5 h-3.5" />}
+        </div>
+      )}
+      <div
+        className="max-w-[75%] px-3.5 py-2.5 rounded-xl text-sm leading-relaxed"
+        style={{
+          background: isVisitor ? "#14532D" : "#f5f0e8",
+          color: isVisitor ? "#fff" : "#292524",
+          fontFamily: "'Source Sans 3', sans-serif",
+          borderBottomRightRadius: isVisitor ? "4px" : undefined,
+          borderBottomLeftRadius: !isVisitor ? "4px" : undefined,
+        }}
+      >
+        {!isVisitor && (
+          <div className="text-[10px] font-semibold mb-1" style={{ color: isBot ? "#14532D" : "#C2410C" }}>
+            {isBot ? BOT_NAME : (message.metadata?.agentName || "Agent")}
+          </div>
+        )}
+        {!isVisitor ? (
+          <div className="prose prose-sm max-w-none" style={{ color: "inherit" }}>
+            <Streamdown>{message.content}</Streamdown>
+          </div>
+        ) : (
+          message.content
+        )}
+        <div className="text-[10px] mt-1 opacity-60">
+          {new Date(message.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
+        </div>
+      </div>
+      {isVisitor && (
+        <div className="w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-1" style={{ background: "#78716C20", color: "#78716C" }}>
+          <User className="w-3.5 h-3.5" />
+        </div>
+      )}
+    </div>
+  );
+}
+
+/* ─── Chat Panel ─── */
+function ChatPanel({
+  conversationId,
+  conversation,
+  onBack,
+}: {
+  conversationId: number;
+  conversation: any;
+  onBack: () => void;
+}) {
+  const [replyText, setReplyText] = useState("");
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+  const utils = trpc.useUtils();
+
+  const { data: messagesData, isLoading } = trpc.agent.messages.useQuery(
+    { conversationId },
+    { refetchInterval: 3000 }
+  );
+
+  const replyMutation = trpc.agent.reply.useMutation({
+    onSuccess: () => {
+      setReplyText("");
+      utils.agent.messages.invalidate({ conversationId });
+      utils.agent.conversationsAdvanced.invalidate();
+    },
+  });
+
+  const statusMutation = trpc.agent.updateStatus.useMutation({
+    onSuccess: () => {
+      utils.agent.conversationsAdvanced.invalidate();
+      utils.agent.stats.invalidate();
+    },
+  });
+
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [messagesData]);
+
+  const handleSend = () => {
+    if (!replyText.trim()) return;
+    replyMutation.mutate({ conversationId, content: replyText.trim() });
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter" && !e.shiftKey) {
+      e.preventDefault();
+      handleSend();
+    }
+  };
+
+  const style = STATUS_STYLES[conversation?.status] || STATUS_STYLES.active;
+
+  return (
+    <div className="flex flex-col h-full">
+      {/* Header */}
+      <div className="px-4 py-3 border-b flex items-center justify-between" style={{ borderColor: "#e7e0d5" }}>
+        <div className="flex items-center gap-3">
+          <button onClick={onBack} className="p-1.5 rounded-lg hover:bg-black/5 transition-colors">
+            <ArrowLeft className="w-4 h-4" style={{ color: "#78716C" }} />
+          </button>
+          <div>
+            <div className="flex items-center gap-2">
+              <span className="text-sm font-bold" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+                {conversation?.visitorName || "Visitor"}
+              </span>
+              <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wider" style={{ background: style.bg, color: style.text }}>
+                {style.label}
+              </span>
+            </div>
+            <div className="flex items-center gap-3 flex-wrap">
+              <span className="text-xs" style={{ color: "#a8a29e" }}>
+                {conversation?.visitorEmail || `Session: ${conversation?.sessionId?.slice(0, 12)}`}
+                {conversation?.assignedAgentId && " · Assigned"}
+              </span>
+              {conversation?.customerId && (
+                <span className="text-xs font-mono px-1.5 py-0.5 rounded" style={{ background: "#14532D0a", color: "#14532D" }}>
+                  ID: {conversation.customerId}
+                </span>
+              )}
+              {conversation?.salesRep && (
+                <span className="text-xs px-1.5 py-0.5 rounded" style={{ background: "#C2410C0a", color: "#C2410C" }}>
+                  Rep: {conversation.salesRep}
+                </span>
+              )}
+              {conversation?.agentName && (
+                <span className="text-xs px-1.5 py-0.5 rounded" style={{ background: "#0369a10a", color: "#0369a1" }}>
+                  Agent: {conversation.agentName}
+                </span>
+              )}
+            </div>
+          </div>
+        </div>
+        <div className="flex items-center gap-2">
+          {conversation?.status !== "resolved" && (
+            <Button
+              size="sm" variant="outline"
+              onClick={() => statusMutation.mutate({ conversationId, status: "resolved" })}
+              className="text-xs" style={{ borderColor: "#14532D40", color: "#14532D" }}
+            >
+              <CheckCircle2 className="w-3 h-3 mr-1" /> Resolve
+            </Button>
+          )}
+          {conversation?.status !== "closed" && (
+            <Button
+              size="sm" variant="outline"
+              onClick={() => statusMutation.mutate({ conversationId, status: "closed" })}
+              className="text-xs" style={{ borderColor: "#78716C40", color: "#78716C" }}
+            >
+              <X className="w-3 h-3 mr-1" /> Close
+            </Button>
+          )}
+        </div>
+      </div>
+
+      {/* Messages */}
+      <ScrollArea className="flex-1 p-4">
+        <div className="space-y-3">
+          {isLoading ? (
+            <div className="text-center py-8 text-sm" style={{ color: "#a8a29e" }}>Loading messages...</div>
+          ) : (
+            messagesData?.map((msg: any) => <MessageBubble key={msg.id} message={msg} />)
+          )}
+          <div ref={messagesEndRef} />
+        </div>
+      </ScrollArea>
+
+      {/* Reply input */}
+      {conversation?.status !== "closed" && (
+        <div className="p-4 border-t" style={{ borderColor: "#e7e0d5" }}>
+          <div className="flex gap-2">
+            <Textarea
+              value={replyText}
+              onChange={(e) => setReplyText(e.target.value)}
+              onKeyDown={handleKeyDown}
+              placeholder="Type your reply... (Enter to send, Shift+Enter for new line)"
+              className="resize-none text-sm"
+              rows={2}
+              style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+            />
+            <Button
+              onClick={handleSend}
+              disabled={!replyText.trim() || replyMutation.isPending}
+              className="self-end text-white"
+              style={{ background: "#14532D" }}
+            >
+              <Send className="w-4 h-4" />
+            </Button>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
+
+/* ─── Pagination Controls ─── */
+function PaginationBar({
+  page, totalPages, total, pageSize, onPageChange,
+}: {
+  page: number; totalPages: number; total: number; pageSize: number;
+  onPageChange: (p: number) => void;
+}) {
+  const startItem = (page - 1) * pageSize + 1;
+  const endItem = Math.min(page * pageSize, total);
+
+  // Generate page numbers to show
+  const pages: (number | "...")[] = [];
+  if (totalPages <= 7) {
+    for (let i = 1; i <= totalPages; i++) pages.push(i);
+  } else {
+    pages.push(1);
+    if (page > 3) pages.push("...");
+    for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) {
+      pages.push(i);
+    }
+    if (page < totalPages - 2) pages.push("...");
+    pages.push(totalPages);
+  }
+
+  return (
+    <div className="flex items-center justify-between px-3 py-2.5 border-t" style={{ borderColor: "#e7e0d5" }}>
+      <span className="text-xs" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>
+        Showing {startItem}–{endItem} of {total} conversations
+      </span>
+      <div className="flex items-center gap-1">
+        <button
+          onClick={() => onPageChange(page - 1)}
+          disabled={page <= 1}
+          className="w-7 h-7 rounded flex items-center justify-center disabled:opacity-30 hover:bg-stone-100 transition-colors"
+        >
+          <ChevronLeft className="w-4 h-4" style={{ color: "#78716C" }} />
+        </button>
+        {pages.map((p, i) =>
+          p === "..." ? (
+            <span key={`e${i}`} className="w-7 h-7 flex items-center justify-center text-xs" style={{ color: "#a8a29e" }}>...</span>
+          ) : (
+            <button
+              key={p}
+              onClick={() => onPageChange(p)}
+              className="w-7 h-7 rounded text-xs font-medium flex items-center justify-center transition-colors"
+              style={{
+                background: p === page ? "#14532D" : "transparent",
+                color: p === page ? "#fff" : "#78716C",
+              }}
+            >
+              {p}
+            </button>
+          )
+        )}
+        <button
+          onClick={() => onPageChange(page + 1)}
+          disabled={page >= totalPages}
+          className="w-7 h-7 rounded flex items-center justify-center disabled:opacity-30 hover:bg-stone-100 transition-colors"
+        >
+          <ChevronRight className="w-4 h-4" style={{ color: "#78716C" }} />
+        </button>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Main Conversations View ─── */
+export default function AgentDashboard() {
+  const { user, isAuthenticated } = useAuth();
+  const utils = trpc.useUtils();
+
+  // Filters state
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(20);
+  const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
+  const [searchQuery, setSearchQuery] = useState("");
+  const [debouncedSearch, setDebouncedSearch] = useState("");
+  const [agentFilter, setAgentFilter] = useState<number | undefined>(undefined);
+  const [dateFrom, setDateFrom] = useState("");
+  const [dateTo, setDateTo] = useState("");
+  const [sortBy, setSortBy] = useState<"updated" | "created" | "visitor" | "status" | "customerId" | "salesRep" | "agent">("updated");
+  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
+  const [showFilters, setShowFilters] = useState(false);
+
+  // Selection state
+  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
+  const [selectedConversationId, setSelectedConversationId] = useState<number | null>(null);
+
+  // Notification state
+  const [notificationsEnabled, setNotificationsEnabled] = useState(false);
+  const [prevMessageCounts, setPrevMessageCounts] = useState<Record<number, number>>({});
+  const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>(
+    typeof window !== "undefined" && "Notification" in window ? Notification.permission : "default"
+  );
+
+  // Request browser notification permission
+  const requestNotificationPermission = useCallback(async () => {
+    if (!("Notification" in window)) {
+      toast.error("Browser notifications are not supported");
+      return;
+    }
+    const permission = await Notification.requestPermission();
+    setNotificationPermission(permission);
+    if (permission === "granted") {
+      setNotificationsEnabled(true);
+      toast.success("Notifications enabled", { description: "You'll be notified when customers send new messages" });
+    } else if (permission === "denied") {
+      toast.error("Notifications blocked", { description: "Please enable notifications in your browser settings" });
+    }
+  }, []);
+
+  // Toggle notifications
+  const toggleNotifications = useCallback(() => {
+    if (notificationsEnabled) {
+      setNotificationsEnabled(false);
+      toast.info("Notifications paused");
+    } else {
+      if (notificationPermission === "granted") {
+        setNotificationsEnabled(true);
+        toast.success("Notifications enabled");
+      } else {
+        requestNotificationPermission();
+      }
+    }
+  }, [notificationsEnabled, notificationPermission, requestNotificationPermission]);
+
+  // Debounce search
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setDebouncedSearch(searchQuery);
+      setPage(1);
+    }, 300);
+    return () => clearTimeout(timer);
+  }, [searchQuery]);
+
+  // Reset page when filters change
+  useEffect(() => { setPage(1); }, [statusFilter, agentFilter, dateFrom, dateTo, sortBy, sortOrder]);
+
+  // Stabilize query input
+  const queryInput = useMemo(() => ({
+    page,
+    pageSize,
+    status: statusFilter,
+    search: debouncedSearch || undefined,
+    agentId: agentFilter,
+    dateFrom: dateFrom || undefined,
+    dateTo: dateTo || undefined,
+    sortBy,
+    sortOrder,
+  }), [page, pageSize, statusFilter, debouncedSearch, agentFilter, dateFrom, dateTo, sortBy, sortOrder]);
+
+  const { data: stats } = trpc.agent.stats.useQuery(undefined, {
+    refetchInterval: 10000,
+    enabled: isAuthenticated,
+  });
+
+  const { data: convData, isLoading: convsLoading } = trpc.agent.conversationsAdvanced.useQuery(
+    queryInput,
+    { refetchInterval: 5000, enabled: isAuthenticated }
+  );
+
+  const { data: agentsList } = trpc.agent.agents.useQuery(undefined, {
+    enabled: isAuthenticated,
+  });
+
+  const selectedConversation = useMemo(
+    () => convData?.conversations?.find((c: any) => c.id === selectedConversationId),
+    [convData, selectedConversationId]
+  );
+
+  // Detect new customer messages and trigger notifications
+  useEffect(() => {
+    if (!notificationsEnabled || !convData?.conversations?.length) return;
+
+    const currentCounts: Record<number, number> = {};
+    for (const conv of convData.conversations) {
+      currentCounts[conv.id] = conv.messageCount || 0;
+    }
+
+    // Only check if we have previous data (skip first load)
+    if (Object.keys(prevMessageCounts).length > 0) {
+      for (const conv of convData.conversations) {
+        const prevCount = prevMessageCounts[conv.id] || 0;
+        const currentCount = conv.messageCount || 0;
+        if (currentCount > prevCount && conv.status !== "closed" && conv.status !== "resolved") {
+          const newMsgCount = currentCount - prevCount;
+          // In-app toast notification
+          toast.info(
+            `New message from ${conv.visitorName || "Visitor"}`,
+            {
+              description: `${newMsgCount} new message${newMsgCount > 1 ? "s" : ""} in conversation #${conv.id}`,
+              action: {
+                label: "View",
+                onClick: () => setSelectedConversationId(conv.id),
+              },
+              duration: 8000,
+            }
+          );
+
+          // Browser notification (only when tab is not focused)
+          if (notificationPermission === "granted" && document.hidden) {
+            try {
+              const notification = new Notification(
+                `New message from ${conv.visitorName || "Visitor"}`,
+                {
+                  body: `${newMsgCount} new message${newMsgCount > 1 ? "s" : ""} in conversation #${conv.id}`,
+                  icon: "/favicon.ico",
+                  tag: `conv-${conv.id}`,
+                }
+              );
+              notification.onclick = () => {
+                window.focus();
+                setSelectedConversationId(conv.id);
+                notification.close();
+              };
+            } catch (e) {
+              // Browser notification failed silently
+            }
+          }
+        }
+      }
+    }
+
+    setPrevMessageCounts(currentCounts);
+  }, [convData, notificationsEnabled, notificationPermission]); // eslint-disable-line react-hooks/exhaustive-deps
+
+  // Bulk actions
+  const bulkStatusMutation = trpc.agent.bulkUpdateStatus.useMutation({
+    onSuccess: (data) => {
+      toast.success(`Updated ${data.updated} conversations`);
+      setSelectedIds(new Set());
+      utils.agent.conversationsAdvanced.invalidate();
+      utils.agent.stats.invalidate();
+    },
+  });
+
+  const deleteMutation = trpc.agent.deleteConversations.useMutation({
+    onSuccess: (data) => {
+      toast.success(`Deleted ${data.deleted} conversations`);
+      setSelectedIds(new Set());
+      setSelectedConversationId(null);
+      utils.agent.conversationsAdvanced.invalidate();
+      utils.agent.stats.invalidate();
+    },
+    onError: (err) => {
+      toast.error(err.message || "Failed to delete");
+    },
+  });
+
+  const toggleSelectAll = useCallback(() => {
+    if (!convData?.conversations) return;
+    const allIds = convData.conversations.map((c: any) => c.id);
+    const allSelected = allIds.every((id: number) => selectedIds.has(id));
+    if (allSelected) {
+      setSelectedIds(new Set());
+    } else {
+      setSelectedIds(new Set(allIds));
+    }
+  }, [convData, selectedIds]);
+
+  const toggleSelect = useCallback((id: number, checked: boolean) => {
+    setSelectedIds(prev => {
+      const next = new Set(prev);
+      if (checked) next.add(id); else next.delete(id);
+      return next;
+    });
+  }, []);
+
+  const clearFilters = () => {
+    setStatusFilter(undefined);
+    setSearchQuery("");
+    setAgentFilter(undefined);
+    setDateFrom("");
+    setDateTo("");
+    setSortBy("updated");
+    setSortOrder("desc");
+    setPage(1);
+  };
+
+  const hasActiveFilters = statusFilter || debouncedSearch || agentFilter || dateFrom || dateTo;
+
+  const toggleSort = (field: typeof sortBy) => {
+    if (sortBy === field) {
+      setSortOrder(prev => prev === "desc" ? "asc" : "desc");
+    } else {
+      setSortBy(field);
+      setSortOrder("desc");
+    }
+  };
+
+  // If a conversation is selected, show the chat panel
+  if (selectedConversationId && selectedConversation) {
+    return (
+      <div className="h-[calc(100vh-4rem)]">
+        <div className="h-full rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+          <ChatPanel
+            conversationId={selectedConversationId}
+            conversation={selectedConversation}
+            onBack={() => setSelectedConversationId(null)}
+          />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      {/* Header */}
+      <div className="flex items-start justify-between">
+        <div>
+          <h1 className="text-2xl font-bold" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+            Conversations
+          </h1>
+          <p className="text-sm mt-1" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Monitor and respond to customer chatbot conversations with {BOT_NAME}.
+          </p>
+        </div>
+        <button
+          onClick={toggleNotifications}
+          className="flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all"
+          style={{
+            borderColor: notificationsEnabled ? "#14532D40" : "#e7e0d5",
+            background: notificationsEnabled ? "#14532D08" : "transparent",
+            color: notificationsEnabled ? "#14532D" : "#78716C",
+            fontFamily: "'Source Sans 3', sans-serif",
+          }}
+          title={notificationsEnabled ? "Notifications are on — click to pause" : "Enable notifications for new customer messages"}
+        >
+          {notificationsEnabled ? (
+            <BellRing className="w-4 h-4" />
+          ) : (
+            <Bell className="w-4 h-4" />
+          )}
+          <span className="hidden sm:inline">
+            {notificationsEnabled ? "Notifications On" : "Enable Notifications"}
+          </span>
+        </button>
+      </div>
+
+      {/* Stats row — clickable to filter */}
+      <div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
+        <StatCard
+          icon={<MessageSquare className="w-4 h-4" />} label="Total Chats" value={stats?.total || 0} color="#14532D"
+          isActive={!statusFilter} onClick={() => setStatusFilter(undefined)}
+        />
+        <StatCard
+          icon={<Clock className="w-4 h-4" />} label="Active" value={stats?.active || 0} color="#0369a1"
+          isActive={statusFilter === "active"} onClick={() => setStatusFilter(statusFilter === "active" ? undefined : "active")}
+        />
+        <StatCard
+          icon={<AlertTriangle className="w-4 h-4" />} label="Escalated" value={stats?.escalated || 0} color="#C2410C"
+          isActive={statusFilter === "escalated"} onClick={() => setStatusFilter(statusFilter === "escalated" ? undefined : "escalated")}
+        />
+        <StatCard
+          icon={<CheckCircle2 className="w-4 h-4" />} label="Resolved" value={stats?.resolved || 0} color="#14532D"
+          isActive={statusFilter === "resolved"} onClick={() => setStatusFilter(statusFilter === "resolved" ? undefined : "resolved")}
+        />
+        <StatCard
+          icon={<BarChart3 className="w-4 h-4" />} label="Closed" value={stats?.closed || 0} color="#78716C"
+          isActive={statusFilter === "closed"} onClick={() => setStatusFilter(statusFilter === "closed" ? undefined : "closed")}
+        />
+      </div>
+
+      {/* Search & Filter toolbar */}
+      <div className="rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <div className="p-3 flex items-center gap-3 flex-wrap">
+          {/* Search */}
+          <div className="flex-1 min-w-[200px] relative">
+            <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: "#a8a29e" }} />
+            <input
+              type="text"
+              value={searchQuery}
+              onChange={(e) => setSearchQuery(e.target.value)}
+              placeholder="Search by name, email, customer ID, sales rep..."
+              className="w-full pl-9 pr-3 py-2 rounded-lg text-sm border"
+              style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif", background: "#fafaf9" }}
+            />
+          </div>
+
+          {/* Toggle filters */}
+          <Button
+            variant="outline" size="sm"
+            onClick={() => setShowFilters(!showFilters)}
+            className="text-xs gap-1.5"
+            style={{ borderColor: hasActiveFilters ? "#14532D40" : "#e7e0d5", color: hasActiveFilters ? "#14532D" : "#78716C" }}
+          >
+            <Filter className="w-3.5 h-3.5" />
+            Filters
+            {hasActiveFilters && (
+              <span className="w-4 h-4 rounded-full text-[10px] flex items-center justify-center text-white" style={{ background: "#14532D" }}>
+                {[statusFilter, agentFilter, dateFrom, dateTo].filter(Boolean).length}
+              </span>
+            )}
+          </Button>
+
+          {/* Sort */}
+          <Select value={sortBy} onValueChange={(v) => setSortBy(v as any)}>
+            <SelectTrigger className="w-[140px] text-xs h-8" style={{ borderColor: "#e7e0d5" }}>
+              <ArrowUpDown className="w-3 h-3 mr-1" style={{ color: "#a8a29e" }} />
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="updated">Last Activity</SelectItem>
+              <SelectItem value="created">Date Created</SelectItem>
+              <SelectItem value="visitor">Visitor Name</SelectItem>
+              <SelectItem value="status">Status</SelectItem>
+              <SelectItem value="customerId">Customer ID</SelectItem>
+              <SelectItem value="salesRep">Sales Rep</SelectItem>
+              <SelectItem value="agent">Agent Name</SelectItem>
+            </SelectContent>
+          </Select>
+
+          <button
+            onClick={() => setSortOrder(prev => prev === "desc" ? "asc" : "desc")}
+            className="w-8 h-8 rounded-lg flex items-center justify-center border hover:bg-stone-50 transition-colors"
+            style={{ borderColor: "#e7e0d5" }}
+            title={sortOrder === "desc" ? "Newest first" : "Oldest first"}
+          >
+            <ArrowUpDown className="w-3.5 h-3.5" style={{ color: "#78716C", transform: sortOrder === "asc" ? "scaleY(-1)" : undefined }} />
+          </button>
+        </div>
+
+        {/* Expanded filters */}
+        {showFilters && (
+          <div className="px-3 pb-3 flex items-center gap-3 flex-wrap border-t pt-3" style={{ borderColor: "#f5f0e8" }}>
+            {/* Agent filter */}
+            <div className="flex items-center gap-2">
+              <UserCheck className="w-3.5 h-3.5" style={{ color: "#a8a29e" }} />
+              <Select
+                value={agentFilter?.toString() || "all"}
+                onValueChange={(v) => setAgentFilter(v === "all" ? undefined : Number(v))}
+              >
+                <SelectTrigger className="w-[160px] text-xs h-8" style={{ borderColor: "#e7e0d5" }}>
+                  <SelectValue placeholder="All Agents" />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="all">All Agents</SelectItem>
+                  {agentsList?.map((agent: any) => (
+                    <SelectItem key={agent.id} value={agent.id.toString()}>
+                      {agent.name || agent.email || `Agent #${agent.id}`}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+            </div>
+
+            {/* Date range */}
+            <div className="flex items-center gap-2">
+              <Calendar className="w-3.5 h-3.5" style={{ color: "#a8a29e" }} />
+              <input
+                type="date"
+                value={dateFrom}
+                onChange={(e) => setDateFrom(e.target.value)}
+                className="px-2 py-1 rounded-lg text-xs border h-8"
+                style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                placeholder="From"
+              />
+              <span className="text-xs" style={{ color: "#a8a29e" }}>to</span>
+              <input
+                type="date"
+                value={dateTo}
+                onChange={(e) => setDateTo(e.target.value)}
+                className="px-2 py-1 rounded-lg text-xs border h-8"
+                style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                placeholder="To"
+              />
+            </div>
+
+            {hasActiveFilters && (
+              <button
+                onClick={clearFilters}
+                className="text-xs font-medium px-2 py-1 rounded-lg hover:bg-stone-100 transition-colors"
+                style={{ color: "#C2410C" }}
+              >
+                Clear all filters
+              </button>
+            )}
+          </div>
+        )}
+
+        {/* Bulk actions bar */}
+        {selectedIds.size > 0 && (
+          <div className="px-3 pb-3 flex items-center gap-3 border-t pt-3" style={{ borderColor: "#f5f0e8" }}>
+            <span className="text-xs font-medium" style={{ color: "#14532D" }}>
+              {selectedIds.size} selected
+            </span>
+            <Button
+              size="sm" variant="outline" className="text-xs h-7"
+              style={{ borderColor: "#14532D40", color: "#14532D" }}
+              onClick={() => bulkStatusMutation.mutate({ conversationIds: Array.from(selectedIds), status: "resolved" })}
+            >
+              <CheckCircle2 className="w-3 h-3 mr-1" /> Resolve
+            </Button>
+            <Button
+              size="sm" variant="outline" className="text-xs h-7"
+              style={{ borderColor: "#78716C40", color: "#78716C" }}
+              onClick={() => bulkStatusMutation.mutate({ conversationIds: Array.from(selectedIds), status: "closed" })}
+            >
+              <X className="w-3 h-3 mr-1" /> Close
+            </Button>
+            {user?.role === "admin" && (
+              <Button
+                size="sm" variant="outline" className="text-xs h-7"
+                style={{ borderColor: "#dc262640", color: "#dc2626" }}
+                onClick={() => {
+                  if (confirm(`Delete ${selectedIds.size} conversation(s)? This cannot be undone.`)) {
+                    deleteMutation.mutate({ conversationIds: Array.from(selectedIds) });
+                  }
+                }}
+              >
+                <Trash2 className="w-3 h-3 mr-1" /> Delete
+              </Button>
+            )}
+            <button
+              onClick={() => setSelectedIds(new Set())}
+              className="text-xs ml-auto" style={{ color: "#a8a29e" }}
+            >
+              Deselect all
+            </button>
+          </div>
+        )}
+
+        {/* Table header */}
+        <div className="flex items-center gap-3 px-3 py-2 border-t text-[11px] font-semibold uppercase tracking-wider" style={{ borderColor: "#f5f0e8", color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>
+          <input
+            type="checkbox"
+            checked={convData?.conversations?.length ? convData.conversations.every((c: any) => selectedIds.has(c.id)) : false}
+            onChange={toggleSelectAll}
+            className="w-4 h-4 rounded border-stone-300 accent-green-800 shrink-0"
+          />
+          <button onClick={() => toggleSort("visitor")} className="w-36 shrink-0 text-left flex items-center gap-1 hover:text-stone-600">
+            Visitor {sortBy === "visitor" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+          <button onClick={() => toggleSort("customerId")} className="w-28 shrink-0 text-left flex items-center gap-1 hover:text-stone-600 hidden lg:flex">
+            Customer ID {sortBy === "customerId" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+          <button onClick={() => toggleSort("salesRep")} className="w-28 shrink-0 text-left flex items-center gap-1 hover:text-stone-600 hidden lg:flex">
+            Sales Rep {sortBy === "salesRep" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+          <button onClick={() => toggleSort("agent")} className="w-28 shrink-0 text-left flex items-center gap-1 hover:text-stone-600 hidden lg:flex">
+            Agent {sortBy === "agent" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+          <button onClick={() => toggleSort("status")} className="w-24 shrink-0 text-left flex items-center gap-1 hover:text-stone-600">
+            Status {sortBy === "status" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+          <span className="w-14 shrink-0">Msgs</span>
+          <span className="flex-1 hidden xl:block">Contact</span>
+          <button onClick={() => toggleSort("updated")} className="w-28 shrink-0 text-right flex items-center gap-1 justify-end hover:text-stone-600 hidden md:flex">
+            Last Active {sortBy === "updated" && <ArrowUpDown className="w-3 h-3" />}
+          </button>
+        </div>
+
+        {/* Conversation rows */}
+        <div className="min-h-[200px]">
+          {convsLoading ? (
+            <div className="text-center py-12 text-sm" style={{ color: "#a8a29e" }}>
+              <MessageSquare className="w-6 h-6 mx-auto mb-2 animate-pulse" style={{ color: "#d6d3d1" }} />
+              Loading conversations...
+            </div>
+          ) : !convData?.conversations?.length ? (
+            <div className="text-center py-12">
+              <MessageSquare className="w-10 h-10 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+              <p className="text-sm font-medium" style={{ color: "#78716C" }}>
+                {hasActiveFilters ? "No conversations match your filters" : "No conversations yet"}
+              </p>
+              <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>
+                {hasActiveFilters
+                  ? "Try adjusting your search or filter criteria"
+                  : `Conversations will appear here when visitors chat with ${BOT_NAME}`}
+              </p>
+              {hasActiveFilters && (
+                <button
+                  onClick={clearFilters}
+                  className="mt-3 text-xs font-medium px-3 py-1.5 rounded-lg"
+                  style={{ background: "#14532D12", color: "#14532D" }}
+                >
+                  Clear all filters
+                </button>
+              )}
+            </div>
+          ) : (
+            convData.conversations.map((conv: any) => (
+              <ConversationRow
+                key={conv.id}
+                conversation={conv}
+                isSelected={conv.id === selectedConversationId}
+                isChecked={selectedIds.has(conv.id)}
+                onClick={() => setSelectedConversationId(conv.id)}
+                onCheck={(checked) => toggleSelect(conv.id, checked)}
+              />
+            ))
+          )}
+        </div>
+
+        {/* Pagination */}
+        {convData && convData.totalPages > 1 && (
+          <PaginationBar
+            page={convData.page}
+            totalPages={convData.totalPages}
+            total={convData.total}
+            pageSize={convData.pageSize}
+            onPageChange={setPage}
+          />
+        )}
+      </div>
+    </div>
+  );
+}

+ 305 - 0
client/src/pages/Analytics.tsx

@@ -0,0 +1,305 @@
+/**
+ * Analytics — Track chatbot resolution rate, interactions, and category breakdown
+ * Design: Warm Showroom palette with clean dashboard cards
+ */
+import { useState, useMemo } from "react";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import {
+  BarChart3, TrendingUp, CheckCircle2, AlertTriangle,
+  Users, MessageSquare, Bot, Headphones, ThumbsUp, ThumbsDown,
+  Package, Truck, RotateCw, XCircle, ArrowUpRight, ArrowDownRight,
+  Calendar, RefreshCw, MousePointerClick,
+} from "lucide-react";
+
+/* ─── Stat Card ─── */
+function StatCard({
+  label, value, icon, color, subtext, trend,
+}: {
+  label: string; value: string | number; icon: React.ReactNode; color: string;
+  subtext?: string; trend?: { value: number; positive: boolean };
+}) {
+  return (
+    <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+      <div className="flex items-start justify-between">
+        <div>
+          <p className="text-[11px] font-medium uppercase tracking-wider" style={{ color: "#a8a29e" }}>{label}</p>
+          <p className="text-2xl font-bold mt-1" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{value}</p>
+          {subtext && <p className="text-[11px] mt-0.5" style={{ color: "#78716C" }}>{subtext}</p>}
+          {trend && (
+            <div className="flex items-center gap-1 mt-1">
+              {trend.positive ? <ArrowUpRight className="w-3 h-3" style={{ color: "#14532D" }} /> : <ArrowDownRight className="w-3 h-3" style={{ color: "#dc2626" }} />}
+              <span className="text-[10px] font-medium" style={{ color: trend.positive ? "#14532D" : "#dc2626" }}>
+                {trend.value}% vs last period
+              </span>
+            </div>
+          )}
+        </div>
+        <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: `${color}14`, color }}>
+          {icon}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Category Bar ─── */
+function CategoryBar({ category, count, resolved, total, icon, color }: {
+  category: string; count: number; resolved: number; total: number;
+  icon: React.ReactNode; color: string;
+}) {
+  const pct = total > 0 ? Math.round((count / total) * 100) : 0;
+  const resPct = count > 0 ? Math.round((resolved / count) * 100) : 0;
+
+  return (
+    <div className="p-3 rounded-lg border" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
+      <div className="flex items-center justify-between mb-2">
+        <div className="flex items-center gap-2">
+          <div className="w-7 h-7 rounded-md flex items-center justify-center" style={{ background: `${color}14`, color }}>
+            {icon}
+          </div>
+          <div>
+            <span className="text-sm font-semibold capitalize" style={{ color: "#292524" }}>{category}</span>
+            <span className="text-[10px] ml-2" style={{ color: "#a8a29e" }}>{count} interactions</span>
+          </div>
+        </div>
+        <span className="text-xs font-bold" style={{ color }}>{pct}%</span>
+      </div>
+      {/* Progress bar */}
+      <div className="h-2 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
+        <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: color }} />
+      </div>
+      <div className="flex items-center justify-between mt-1.5">
+        <span className="text-[10px]" style={{ color: "#a8a29e" }}>Resolution rate</span>
+        <span className="text-[10px] font-medium" style={{ color: resPct >= 70 ? "#14532D" : resPct >= 40 ? "#ca8a04" : "#dc2626" }}>
+          {resPct}%
+        </span>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Resolution Donut ─── */
+function ResolutionDonut({ botRate, agentRate, escalatedRate, abandonedRate }: {
+  botRate: number; agentRate: number; escalatedRate: number; abandonedRate: number;
+}) {
+  const total = botRate + agentRate + escalatedRate + abandonedRate;
+  const segments = [
+    { label: "Bot Resolved", pct: botRate, color: "#14532D" },
+    { label: "Agent Resolved", pct: agentRate, color: "#0369a1" },
+    { label: "Escalated", pct: escalatedRate, color: "#ca8a04" },
+    { label: "Abandoned", pct: abandonedRate, color: "#dc2626" },
+  ];
+
+  return (
+    <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+      <h3 className="text-xs font-bold uppercase tracking-wider mb-4" style={{ color: "#78716C" }}>Resolution Breakdown</h3>
+      <div className="flex items-center gap-6">
+        {/* Simple visual bars */}
+        <div className="flex-1 space-y-2">
+          {segments.map(seg => (
+            <div key={seg.label}>
+              <div className="flex items-center justify-between mb-0.5">
+                <span className="text-[11px] font-medium" style={{ color: "#292524" }}>{seg.label}</span>
+                <span className="text-[11px] font-bold" style={{ color: seg.color }}>{seg.pct}%</span>
+              </div>
+              <div className="h-2.5 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
+                <div className="h-full rounded-full transition-all" style={{ width: `${seg.pct}%`, background: seg.color }} />
+              </div>
+            </div>
+          ))}
+        </div>
+        {/* Overall score */}
+        <div className="text-center shrink-0">
+          <div
+            className="w-20 h-20 rounded-full flex items-center justify-center border-4"
+            style={{ borderColor: "#14532D", background: "#14532D08" }}
+          >
+            <div>
+              <div className="text-xl font-bold" style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}>
+                {botRate + agentRate}%
+              </div>
+              <div className="text-[8px] uppercase tracking-wider" style={{ color: "#78716C" }}>Resolved</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default function Analytics() {
+  const [dateRange, setDateRange] = useState<"7d" | "30d" | "90d" | "all">("30d");
+
+  const dateFilters = useMemo(() => {
+    const now = new Date();
+    if (dateRange === "all") return {};
+    const days = dateRange === "7d" ? 7 : dateRange === "30d" ? 30 : 90;
+    const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
+    return { startDate: start.toISOString(), endDate: now.toISOString() };
+  }, [dateRange]);
+
+  const { data: summary, isLoading, refetch } = trpc.analytics.summary.useQuery(dateFilters);
+
+  const categoryIcons: Record<string, { icon: React.ReactNode; color: string }> = {
+    orders: { icon: <Package className="w-3.5 h-3.5" />, color: "#14532D" },
+    shipping: { icon: <Truck className="w-3.5 h-3.5" />, color: "#0369a1" },
+    returning: { icon: <RotateCw className="w-3.5 h-3.5" />, color: "#ca8a04" },
+    cancelling: { icon: <XCircle className="w-3.5 h-3.5" />, color: "#dc2626" },
+  };
+
+  // Compute rates for donut
+  const totalOutcomes = summary ? (summary.resolvedByBot + summary.resolvedByAgent + summary.escalated + summary.abandoned) : 0;
+  const botRate = totalOutcomes > 0 ? Math.round((summary!.resolvedByBot / totalOutcomes) * 100) : 0;
+  const agentRate = totalOutcomes > 0 ? Math.round((summary!.resolvedByAgent / totalOutcomes) * 100) : 0;
+  const escalatedRate = totalOutcomes > 0 ? Math.round((summary!.escalated / totalOutcomes) * 100) : 0;
+  const abandonedRate = totalOutcomes > 0 ? Math.round((summary!.abandoned / totalOutcomes) * 100) : 0;
+
+  return (
+    <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
+      {/* Top toolbar */}
+      <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <div className="flex items-center gap-3">
+          <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#ca8a04" }}>
+            <BarChart3 className="w-3.5 h-3.5 text-white" />
+          </div>
+          <span className="text-sm font-bold" style={{ color: "#ca8a04", fontFamily: "'Playfair Display', serif" }}>Analytics</span>
+        </div>
+        <div className="flex items-center gap-2">
+          {/* Date range selector */}
+          <div className="flex gap-0.5 p-0.5 rounded-lg" style={{ background: "#f5f0e8" }}>
+            {(["7d", "30d", "90d", "all"] as const).map(range => (
+              <button
+                key={range}
+                onClick={() => setDateRange(range)}
+                className="px-2.5 py-1 rounded-md text-[11px] font-medium transition-colors"
+                style={{
+                  background: dateRange === range ? "#fff" : "transparent",
+                  color: dateRange === range ? "#292524" : "#a8a29e",
+                  boxShadow: dateRange === range ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
+                }}
+              >
+                {range === "all" ? "All" : range}
+              </button>
+            ))}
+          </div>
+          <Button onClick={() => refetch()} variant="outline" size="sm" className="text-xs">
+            <RefreshCw className="w-3 h-3 mr-1" /> Refresh
+          </Button>
+        </div>
+      </div>
+
+      {/* Content */}
+      <div className="flex-1 overflow-y-auto p-4 space-y-4" style={{ background: "#FFFBEB" }}>
+        {isLoading ? (
+          <div className="flex items-center justify-center py-20">
+            <RefreshCw className="w-6 h-6 animate-spin" style={{ color: "#ca8a04" }} />
+            <span className="ml-3 text-sm" style={{ color: "#78716C" }}>Loading analytics...</span>
+          </div>
+        ) : !summary ? (
+          <div className="text-center py-20">
+            <BarChart3 className="w-12 h-12 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+            <p className="text-sm" style={{ color: "#78716C" }}>No analytics data available yet</p>
+            <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Data will appear as customers interact with the chatbot</p>
+          </div>
+        ) : (
+          <>
+            {/* Top-level stats */}
+            <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
+              <StatCard
+                label="Total Sessions"
+                value={summary.totalSessions}
+                icon={<Users className="w-5 h-5" />}
+                color="#14532D"
+                subtext={`${summary.totalEvents} total events`}
+              />
+              <StatCard
+                label="Resolution Rate"
+                value={`${summary.resolutionRate}%`}
+                icon={<CheckCircle2 className="w-5 h-5" />}
+                color={summary.resolutionRate >= 70 ? "#14532D" : summary.resolutionRate >= 40 ? "#ca8a04" : "#dc2626"}
+                subtext={`${summary.resolvedByBot + summary.resolvedByAgent} resolved`}
+              />
+              <StatCard
+                label="Bot Resolution"
+                value={`${summary.botResolutionRate}%`}
+                icon={<Bot className="w-5 h-5" />}
+                color="#0369a1"
+                subtext={`${summary.resolvedByBot} by bot`}
+              />
+              <StatCard
+                label="Escalated"
+                value={summary.escalated}
+                icon={<Headphones className="w-5 h-5" />}
+                color="#C2410C"
+                subtext={`${summary.abandoned} abandoned`}
+              />
+            </div>
+
+            {/* Second row */}
+            <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
+              <StatCard
+                label="Messages Sent"
+                value={summary.messagesSent}
+                icon={<MessageSquare className="w-5 h-5" />}
+                color="#7c3aed"
+              />
+              <StatCard
+                label="Messages Received"
+                value={summary.messagesReceived}
+                icon={<MessageSquare className="w-5 h-5" />}
+                color="#059669"
+              />
+              <StatCard
+                label="Button Clicks"
+                value={summary.buttonClicks}
+                icon={<MousePointerClick className="w-5 h-5" />}
+                color="#ca8a04"
+              />
+              <StatCard
+                label="Feedback"
+                value={`${summary.positiveFeedback}/${summary.negativeFeedback}`}
+                icon={summary.positiveFeedback >= summary.negativeFeedback ? <ThumbsUp className="w-5 h-5" /> : <ThumbsDown className="w-5 h-5" />}
+                color={summary.positiveFeedback >= summary.negativeFeedback ? "#14532D" : "#dc2626"}
+                subtext="Positive / Negative"
+              />
+            </div>
+
+            {/* Resolution breakdown */}
+            {totalOutcomes > 0 && (
+              <ResolutionDonut
+                botRate={botRate}
+                agentRate={agentRate}
+                escalatedRate={escalatedRate}
+                abandonedRate={abandonedRate}
+              />
+            )}
+
+            {/* Category breakdown */}
+            <div>
+              <h3 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "#78716C" }}>
+                Category Breakdown
+              </h3>
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
+                {summary.categoryBreakdown.map(cat => {
+                  const catConfig = categoryIcons[cat.category] || { icon: <Package className="w-3.5 h-3.5" />, color: "#78716C" };
+                  return (
+                    <CategoryBar
+                      key={cat.category}
+                      category={cat.category}
+                      count={cat.count}
+                      resolved={cat.resolved}
+                      total={summary.totalEvents}
+                      icon={catConfig.icon}
+                      color={catConfig.color}
+                    />
+                  );
+                })}
+              </div>
+            </div>
+          </>
+        )}
+      </div>
+    </div>
+  );
+}

+ 1437 - 0
client/src/pages/ComponentShowcase.tsx

@@ -0,0 +1,1437 @@
+import {
+  Accordion,
+  AccordionContent,
+  AccordionItem,
+  AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { AspectRatio } from "@/components/ui/aspect-ratio";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Badge } from "@/components/ui/badge";
+import {
+  Breadcrumb,
+  BreadcrumbItem,
+  BreadcrumbLink,
+  BreadcrumbList,
+  BreadcrumbPage,
+  BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { Button } from "@/components/ui/button";
+import { Calendar } from "@/components/ui/calendar";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardFooter,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import {
+  Carousel,
+  CarouselContent,
+  CarouselItem,
+  CarouselNext,
+  CarouselPrevious,
+} from "@/components/ui/carousel";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+} from "@/components/ui/command";
+import {
+  ContextMenu,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuTrigger,
+} from "@/components/ui/context-menu";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
+import {
+  Drawer,
+  DrawerClose,
+  DrawerContent,
+  DrawerDescription,
+  DrawerFooter,
+  DrawerHeader,
+  DrawerTitle,
+  DrawerTrigger,
+} from "@/components/ui/drawer";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuLabel,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  HoverCard,
+  HoverCardContent,
+  HoverCardTrigger,
+} from "@/components/ui/hover-card";
+import { Input } from "@/components/ui/input";
+import {
+  InputOTP,
+  InputOTPGroup,
+  InputOTPSlot,
+} from "@/components/ui/input-otp";
+import { Label } from "@/components/ui/label";
+import {
+  Menubar,
+  MenubarContent,
+  MenubarItem,
+  MenubarMenu,
+  MenubarSeparator,
+  MenubarTrigger,
+} from "@/components/ui/menubar";
+import {
+  Pagination,
+  PaginationContent,
+  PaginationItem,
+  PaginationLink,
+  PaginationNext,
+  PaginationPrevious,
+} from "@/components/ui/pagination";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
+import { Progress } from "@/components/ui/progress";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import {
+  ResizableHandle,
+  ResizablePanel,
+  ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+import {
+  Table,
+  TableBody,
+  TableCaption,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import { Toggle } from "@/components/ui/toggle";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useTheme } from "@/contexts/ThemeContext";
+import { format } from "date-fns";
+import { zhCN } from "date-fns/locale";
+import {
+  AlertCircle,
+  CalendarIcon,
+  Check,
+  Clock,
+  Moon,
+  Sun,
+  X,
+} from "lucide-react";
+import { useState } from "react";
+import { toast as sonnerToast } from "sonner";
+import { AIChatBox, type Message } from "@/components/AIChatBox";
+
+export default function ComponentsShowcase() {
+  const { theme, toggleTheme } = useTheme();
+  const [date, setDate] = useState<Date | undefined>(new Date());
+  const [datePickerDate, setDatePickerDate] = useState<Date>();
+  const [selectedFruits, setSelectedFruits] = useState<string[]>([]);
+  const [progress, setProgress] = useState(33);
+  const [currentPage, setCurrentPage] = useState(2);
+  const [openCombobox, setOpenCombobox] = useState(false);
+  const [selectedFramework, setSelectedFramework] = useState("");
+  const [selectedMonth, setSelectedMonth] = useState("");
+  const [selectedYear, setSelectedYear] = useState("");
+  const [dialogInput, setDialogInput] = useState("");
+  const [dialogOpen, setDialogOpen] = useState(false);
+
+  // AI ChatBox demo state
+  const [chatMessages, setChatMessages] = useState<Message[]>([
+    { role: "system", content: "You are a helpful assistant." },
+  ]);
+  const [isChatLoading, setIsChatLoading] = useState(false);
+
+  const handleDialogSubmit = () => {
+    console.log("Dialog submitted with value:", dialogInput);
+    sonnerToast.success("Submitted successfully", {
+      description: `Input: ${dialogInput}`,
+    });
+    setDialogInput("");
+    setDialogOpen(false);
+  };
+
+  const handleDialogKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === "Enter" && !e.nativeEvent.isComposing) {
+      e.preventDefault();
+      handleDialogSubmit();
+    }
+  };
+
+  const handleChatSend = (content: string) => {
+    // Add user message
+    const newMessages: Message[] = [...chatMessages, { role: "user", content }];
+    setChatMessages(newMessages);
+
+    // Simulate AI response with delay
+    setIsChatLoading(true);
+    setTimeout(() => {
+      const aiResponse: Message = {
+        role: "assistant",
+        content: `This is a **demo response**. In a real app, you would call a tRPC mutation here:\n\n\`\`\`typescript\nconst chatMutation = trpc.ai.chat.useMutation({\n  onSuccess: (response) => {\n    setChatMessages(prev => [...prev, {\n      role: "assistant",\n      content: response.choices[0].message.content\n    }]);\n  }\n});\n\nchatMutation.mutate({ messages: newMessages });\n\`\`\`\n\nYour message was: "${content}"`,
+      };
+      setChatMessages([...newMessages, aiResponse]);
+      setIsChatLoading(false);
+    }, 1500);
+  };
+
+  return (
+    <div className="min-h-screen bg-background text-foreground">
+      <main className="container max-w-6xl mx-auto">
+        <div className="space-y-2 justify-between flex">
+          <h2 className="text-3xl font-bold tracking-tight mb-6">
+            Shadcn/ui Component Library
+          </h2>
+          <Button variant="outline" size="icon" onClick={toggleTheme}>
+            {theme === "light" ? (
+              <Moon className="h-5 w-5" />
+            ) : (
+              <Sun className="h-5 w-5" />
+            )}
+          </Button>
+        </div>
+
+        <div className="space-y-12">
+          {/* Text Colors Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Text Colors</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                  <div className="space-y-3">
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Foreground (Default)
+                      </p>
+                      <p className="text-foreground text-lg">
+                        Default text color for main content
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Muted Foreground
+                      </p>
+                      <p className="text-muted-foreground text-lg">
+                        Muted text for secondary information
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Primary
+                      </p>
+                      <p className="text-primary text-lg font-medium">
+                        Primary brand color text
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Secondary Foreground
+                      </p>
+                      <p className="text-secondary-foreground text-lg">
+                        Secondary action text color
+                      </p>
+                    </div>
+                  </div>
+                  <div className="space-y-3">
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Accent Foreground
+                      </p>
+                      <p className="text-accent-foreground text-lg">
+                        Accent text for emphasis
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Destructive
+                      </p>
+                      <p className="text-destructive text-lg font-medium">
+                        Error or destructive action text
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Card Foreground
+                      </p>
+                      <p className="text-card-foreground text-lg">
+                        Text color on card backgrounds
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground mb-1">
+                        Popover Foreground
+                      </p>
+                      <p className="text-popover-foreground text-lg">
+                        Text color in popovers
+                      </p>
+                    </div>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Color Combinations Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Color Combinations</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                  <div className="bg-primary text-primary-foreground rounded-lg p-4">
+                    <p className="font-medium mb-1">Primary</p>
+                    <p className="text-sm opacity-90">
+                      Primary background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-secondary text-secondary-foreground rounded-lg p-4">
+                    <p className="font-medium mb-1">Secondary</p>
+                    <p className="text-sm opacity-90">
+                      Secondary background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-muted text-muted-foreground rounded-lg p-4">
+                    <p className="font-medium mb-1">Muted</p>
+                    <p className="text-sm opacity-90">
+                      Muted background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-accent text-accent-foreground rounded-lg p-4">
+                    <p className="font-medium mb-1">Accent</p>
+                    <p className="text-sm opacity-90">
+                      Accent background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-destructive text-destructive-foreground rounded-lg p-4">
+                    <p className="font-medium mb-1">Destructive</p>
+                    <p className="text-sm opacity-90">
+                      Destructive background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-card text-card-foreground rounded-lg p-4 border">
+                    <p className="font-medium mb-1">Card</p>
+                    <p className="text-sm opacity-90">
+                      Card background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-popover text-popover-foreground rounded-lg p-4 border">
+                    <p className="font-medium mb-1">Popover</p>
+                    <p className="text-sm opacity-90">
+                      Popover background with foreground text
+                    </p>
+                  </div>
+                  <div className="bg-background text-foreground rounded-lg p-4 border">
+                    <p className="font-medium mb-1">Background</p>
+                    <p className="text-sm opacity-90">
+                      Default background with foreground text
+                    </p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Buttons Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Buttons</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="flex flex-wrap gap-4">
+                  <Button>Default</Button>
+                  <Button variant="secondary">Secondary</Button>
+                  <Button variant="destructive">Destructive</Button>
+                  <Button variant="outline">Outline</Button>
+                  <Button variant="ghost">Ghost</Button>
+                  <Button variant="link">Link</Button>
+                  <Button size="sm">Small</Button>
+                  <Button size="lg">Large</Button>
+                  <Button size="icon">
+                    <Check className="h-4 w-4" />
+                  </Button>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Form Inputs Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Form Inputs</h3>
+            <Card>
+              <CardContent className="pt-6 space-y-6">
+                <div className="space-y-2">
+                  <Label htmlFor="email">Email</Label>
+                  <Input id="email" type="email" placeholder="Email" />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="message">Message</Label>
+                  <Textarea
+                    id="message"
+                    placeholder="Type your message here."
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label>Select</Label>
+                  <Select>
+                    <SelectTrigger>
+                      <SelectValue placeholder="Select a fruit" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="apple">Apple</SelectItem>
+                      <SelectItem value="banana">Banana</SelectItem>
+                      <SelectItem value="orange">Orange</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+                <div className="flex items-center space-x-2">
+                  <Checkbox id="terms" />
+                  <Label htmlFor="terms">Accept terms and conditions</Label>
+                </div>
+                <div className="flex items-center space-x-2">
+                  <Switch id="airplane-mode" />
+                  <Label htmlFor="airplane-mode">Airplane Mode</Label>
+                </div>
+                <div className="space-y-2">
+                  <Label>Radio Group</Label>
+                  <RadioGroup defaultValue="option-one">
+                    <div className="flex items-center space-x-2">
+                      <RadioGroupItem value="option-one" id="option-one" />
+                      <Label htmlFor="option-one">Option One</Label>
+                    </div>
+                    <div className="flex items-center space-x-2">
+                      <RadioGroupItem value="option-two" id="option-two" />
+                      <Label htmlFor="option-two">Option Two</Label>
+                    </div>
+                  </RadioGroup>
+                </div>
+                <div className="space-y-2">
+                  <Label>Slider</Label>
+                  <Slider defaultValue={[50]} max={100} step={1} />
+                </div>
+                <div className="space-y-2">
+                  <Label>Input OTP</Label>
+                  <InputOTP maxLength={6}>
+                    <InputOTPGroup>
+                      <InputOTPSlot index={0} />
+                      <InputOTPSlot index={1} />
+                      <InputOTPSlot index={2} />
+                      <InputOTPSlot index={3} />
+                      <InputOTPSlot index={4} />
+                      <InputOTPSlot index={5} />
+                    </InputOTPGroup>
+                  </InputOTP>
+                </div>
+                <div className="space-y-2">
+                  <Label>Date Time Picker</Label>
+                  <Popover>
+                    <PopoverTrigger asChild>
+                      <Button
+                        variant="outline"
+                        className={`w-full justify-start text-left font-normal ${
+                          !datePickerDate && "text-muted-foreground"
+                        }`}
+                      >
+                        <CalendarIcon className="mr-2 h-4 w-4" />
+                        {datePickerDate ? (
+                          format(datePickerDate, "PPP HH:mm", { locale: zhCN })
+                        ) : (
+                          <span>Select date and time</span>
+                        )}
+                      </Button>
+                    </PopoverTrigger>
+                    <PopoverContent className="w-auto p-0" align="start">
+                      <div className="p-3 space-y-3">
+                        <Calendar
+                          mode="single"
+                          selected={datePickerDate}
+                          onSelect={setDatePickerDate}
+                        />
+                        <div className="border-t pt-3 space-y-2">
+                          <Label className="flex items-center gap-2">
+                            <Clock className="h-4 w-4" />
+                            Time
+                          </Label>
+                          <div className="flex gap-2">
+                            <Input
+                              type="time"
+                              value={
+                                datePickerDate
+                                  ? format(datePickerDate, "HH:mm")
+                                  : "00:00"
+                              }
+                              onChange={e => {
+                                const [hours, minutes] =
+                                  e.target.value.split(":");
+                                const newDate = datePickerDate
+                                  ? new Date(datePickerDate)
+                                  : new Date();
+                                newDate.setHours(parseInt(hours));
+                                newDate.setMinutes(parseInt(minutes));
+                                setDatePickerDate(newDate);
+                              }}
+                            />
+                          </div>
+                        </div>
+                      </div>
+                    </PopoverContent>
+                  </Popover>
+                  {datePickerDate && (
+                    <p className="text-sm text-muted-foreground">
+                      Selected:{" "}
+                      {format(datePickerDate, "yyyy/MM/dd  HH:mm", {
+                        locale: zhCN,
+                      })}
+                    </p>
+                  )}
+                </div>
+                <div className="space-y-2">
+                  <Label>Searchable Dropdown</Label>
+                  <Popover open={openCombobox} onOpenChange={setOpenCombobox}>
+                    <PopoverTrigger asChild>
+                      <Button
+                        variant="outline"
+                        role="combobox"
+                        aria-expanded={openCombobox}
+                        className="w-full justify-between"
+                      >
+                        {selectedFramework
+                          ? [
+                              { value: "react", label: "React" },
+                              { value: "vue", label: "Vue" },
+                              { value: "angular", label: "Angular" },
+                              { value: "svelte", label: "Svelte" },
+                              { value: "nextjs", label: "Next.js" },
+                              { value: "nuxt", label: "Nuxt" },
+                              { value: "remix", label: "Remix" },
+                            ].find(fw => fw.value === selectedFramework)?.label
+                          : "Select framework..."}
+                        <CalendarIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                      </Button>
+                    </PopoverTrigger>
+                    <PopoverContent className="w-full p-0">
+                      <Command>
+                        <CommandInput placeholder="Search frameworks..." />
+                        <CommandList>
+                          <CommandEmpty>No framework found</CommandEmpty>
+                          <CommandGroup>
+                            {[
+                              { value: "react", label: "React" },
+                              { value: "vue", label: "Vue" },
+                              { value: "angular", label: "Angular" },
+                              { value: "svelte", label: "Svelte" },
+                              { value: "nextjs", label: "Next.js" },
+                              { value: "nuxt", label: "Nuxt" },
+                              { value: "remix", label: "Remix" },
+                            ].map(framework => (
+                              <CommandItem
+                                key={framework.value}
+                                value={framework.value}
+                                onSelect={currentValue => {
+                                  setSelectedFramework(
+                                    currentValue === selectedFramework
+                                      ? ""
+                                      : currentValue
+                                  );
+                                  setOpenCombobox(false);
+                                }}
+                              >
+                                <Check
+                                  className={`mr-2 h-4 w-4 ${
+                                    selectedFramework === framework.value
+                                      ? "opacity-100"
+                                      : "opacity-0"
+                                  }`}
+                                />
+                                {framework.label}
+                              </CommandItem>
+                            ))}
+                          </CommandGroup>
+                        </CommandList>
+                      </Command>
+                    </PopoverContent>
+                  </Popover>
+                  {selectedFramework && (
+                    <p className="text-sm text-muted-foreground">
+                      Selected:{" "}
+                      {
+                        [
+                          { value: "react", label: "React" },
+                          { value: "vue", label: "Vue" },
+                          { value: "angular", label: "Angular" },
+                          { value: "svelte", label: "Svelte" },
+                          { value: "nextjs", label: "Next.js" },
+                          { value: "nuxt", label: "Nuxt" },
+                          { value: "remix", label: "Remix" },
+                        ].find(fw => fw.value === selectedFramework)?.label
+                      }
+                    </p>
+                  )}
+                </div>
+                <div className="space-y-2">
+                  <div className="grid grid-cols-2 gap-4">
+                    <div className="space-y-2">
+                      <Label htmlFor="month" className="text-sm font-medium">
+                        Month
+                      </Label>
+                      <Select
+                        value={selectedMonth}
+                        onValueChange={setSelectedMonth}
+                      >
+                        <SelectTrigger id="month">
+                          <SelectValue placeholder="MM" />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {Array.from({ length: 12 }, (_, i) => i + 1).map(
+                            month => (
+                              <SelectItem
+                                key={month}
+                                value={month.toString().padStart(2, "0")}
+                              >
+                                {month.toString().padStart(2, "0")}
+                              </SelectItem>
+                            )
+                          )}
+                        </SelectContent>
+                      </Select>
+                    </div>
+                    <div className="space-y-2">
+                      <Label htmlFor="year" className="text-sm font-medium">
+                        Year
+                      </Label>
+                      <Select
+                        value={selectedYear}
+                        onValueChange={setSelectedYear}
+                      >
+                        <SelectTrigger id="year">
+                          <SelectValue placeholder="YYYY" />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {Array.from(
+                            { length: 10 },
+                            (_, i) => new Date().getFullYear() - 5 + i
+                          ).map(year => (
+                            <SelectItem key={year} value={year.toString()}>
+                              {year}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    </div>
+                  </div>
+                  {selectedMonth && selectedYear && (
+                    <p className="text-sm text-muted-foreground">
+                      Selected: {selectedYear}/{selectedMonth}/
+                    </p>
+                  )}
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Data Display Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Data Display</h3>
+            <Card>
+              <CardContent className="pt-6 space-y-6">
+                <div className="space-y-2">
+                  <Label>Badges</Label>
+                  <div className="flex flex-wrap gap-2">
+                    <Badge>Default</Badge>
+                    <Badge variant="secondary">Secondary</Badge>
+                    <Badge variant="destructive">Destructive</Badge>
+                    <Badge variant="outline">Outline</Badge>
+                  </div>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Avatar</Label>
+                  <div className="flex gap-4">
+                    <Avatar>
+                      <AvatarImage src="https://github.com/shadcn.png" />
+                      <AvatarFallback>CN</AvatarFallback>
+                    </Avatar>
+                    <Avatar>
+                      <AvatarFallback>AB</AvatarFallback>
+                    </Avatar>
+                  </div>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Progress</Label>
+                  <Progress value={progress} />
+                  <div className="flex gap-2">
+                    <Button
+                      size="sm"
+                      onClick={() => setProgress(Math.max(0, progress - 10))}
+                    >
+                      -10
+                    </Button>
+                    <Button
+                      size="sm"
+                      onClick={() => setProgress(Math.min(100, progress + 10))}
+                    >
+                      +10
+                    </Button>
+                  </div>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Skeleton</Label>
+                  <div className="space-y-2">
+                    <Skeleton className="h-4 w-full" />
+                    <Skeleton className="h-4 w-3/4" />
+                    <Skeleton className="h-4 w-1/2" />
+                  </div>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Pagination</Label>
+                  <Pagination>
+                    <PaginationContent>
+                      <PaginationItem>
+                        <PaginationPrevious
+                          href="#"
+                          onClick={e => {
+                            e.preventDefault();
+                            setCurrentPage(Math.max(1, currentPage - 1));
+                          }}
+                        />
+                      </PaginationItem>
+                      {[1, 2, 3, 4, 5].map(page => (
+                        <PaginationItem key={page}>
+                          <PaginationLink
+                            href="#"
+                            isActive={currentPage === page}
+                            onClick={e => {
+                              e.preventDefault();
+                              setCurrentPage(page);
+                            }}
+                          >
+                            {page}
+                          </PaginationLink>
+                        </PaginationItem>
+                      ))}
+                      <PaginationItem>
+                        <PaginationNext
+                          href="#"
+                          onClick={e => {
+                            e.preventDefault();
+                            setCurrentPage(Math.min(5, currentPage + 1));
+                          }}
+                        />
+                      </PaginationItem>
+                    </PaginationContent>
+                  </Pagination>
+                  <p className="text-sm text-muted-foreground text-center">
+                    Current page: {currentPage}
+                  </p>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Table</Label>
+                  <Table>
+                    <TableCaption>A list of your recent invoices.</TableCaption>
+                    <TableHeader>
+                      <TableRow>
+                        <TableHead className="w-[100px]">Invoice</TableHead>
+                        <TableHead>Status</TableHead>
+                        <TableHead>Method</TableHead>
+                        <TableHead className="text-right">Amount</TableHead>
+                      </TableRow>
+                    </TableHeader>
+                    <TableBody>
+                      <TableRow>
+                        <TableCell className="font-medium">INV001</TableCell>
+                        <TableCell>Paid</TableCell>
+                        <TableCell>Credit Card</TableCell>
+                        <TableCell className="text-right">$250.00</TableCell>
+                      </TableRow>
+                      <TableRow>
+                        <TableCell className="font-medium">INV002</TableCell>
+                        <TableCell>Pending</TableCell>
+                        <TableCell>PayPal</TableCell>
+                        <TableCell className="text-right">$150.00</TableCell>
+                      </TableRow>
+                      <TableRow>
+                        <TableCell className="font-medium">INV003</TableCell>
+                        <TableCell>Unpaid</TableCell>
+                        <TableCell>Bank Transfer</TableCell>
+                        <TableCell className="text-right">$350.00</TableCell>
+                      </TableRow>
+                    </TableBody>
+                  </Table>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Menubar</Label>
+                  <Menubar>
+                    <MenubarMenu>
+                      <MenubarTrigger>File</MenubarTrigger>
+                      <MenubarContent>
+                        <MenubarItem>New Tab</MenubarItem>
+                        <MenubarItem>New Window</MenubarItem>
+                        <MenubarSeparator />
+                        <MenubarItem>Share</MenubarItem>
+                        <MenubarSeparator />
+                        <MenubarItem>Print</MenubarItem>
+                      </MenubarContent>
+                    </MenubarMenu>
+                    <MenubarMenu>
+                      <MenubarTrigger>Edit</MenubarTrigger>
+                      <MenubarContent>
+                        <MenubarItem>Undo</MenubarItem>
+                        <MenubarItem>Redo</MenubarItem>
+                      </MenubarContent>
+                    </MenubarMenu>
+                    <MenubarMenu>
+                      <MenubarTrigger>View</MenubarTrigger>
+                      <MenubarContent>
+                        <MenubarItem>Reload</MenubarItem>
+                        <MenubarItem>Force Reload</MenubarItem>
+                      </MenubarContent>
+                    </MenubarMenu>
+                  </Menubar>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Breadcrumb</Label>
+                  <Breadcrumb>
+                    <BreadcrumbList>
+                      <BreadcrumbItem>
+                        <BreadcrumbLink href="/">Home</BreadcrumbLink>
+                      </BreadcrumbItem>
+                      <BreadcrumbSeparator />
+                      <BreadcrumbItem>
+                        <BreadcrumbLink href="/components">
+                          Components
+                        </BreadcrumbLink>
+                      </BreadcrumbItem>
+                      <BreadcrumbSeparator />
+                      <BreadcrumbItem>
+                        <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
+                      </BreadcrumbItem>
+                    </BreadcrumbList>
+                  </Breadcrumb>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Alerts Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Alerts</h3>
+            <div className="space-y-4">
+              <Alert>
+                <AlertCircle className="h-4 w-4" />
+                <AlertTitle>Heads up!</AlertTitle>
+                <AlertDescription>
+                  You can add components to your app using the cli.
+                </AlertDescription>
+              </Alert>
+              <Alert variant="destructive">
+                <X className="h-4 w-4" />
+                <AlertTitle>Error</AlertTitle>
+                <AlertDescription>
+                  Your session has expired. Please log in again.
+                </AlertDescription>
+              </Alert>
+            </div>
+          </section>
+
+          {/* Tabs Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Tabs</h3>
+            <Tabs defaultValue="account" className="w-full">
+              <TabsList className="grid w-full grid-cols-3">
+                <TabsTrigger value="account">Account</TabsTrigger>
+                <TabsTrigger value="password">Password</TabsTrigger>
+                <TabsTrigger value="settings">Settings</TabsTrigger>
+              </TabsList>
+              <TabsContent value="account">
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Account</CardTitle>
+                    <CardDescription>
+                      Make changes to your account here.
+                    </CardDescription>
+                  </CardHeader>
+                  <CardContent className="space-y-2">
+                    <div className="space-y-1">
+                      <Label htmlFor="name">Name</Label>
+                      <Input id="name" defaultValue="Pedro Duarte" />
+                    </div>
+                  </CardContent>
+                  <CardFooter>
+                    <Button>Save changes</Button>
+                  </CardFooter>
+                </Card>
+              </TabsContent>
+              <TabsContent value="password">
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Password</CardTitle>
+                    <CardDescription>
+                      Change your password here.
+                    </CardDescription>
+                  </CardHeader>
+                  <CardContent className="space-y-2">
+                    <div className="space-y-1">
+                      <Label htmlFor="current">Current password</Label>
+                      <Input id="current" type="password" />
+                    </div>
+                    <div className="space-y-1">
+                      <Label htmlFor="new">New password</Label>
+                      <Input id="new" type="password" />
+                    </div>
+                  </CardContent>
+                  <CardFooter>
+                    <Button>Save password</Button>
+                  </CardFooter>
+                </Card>
+              </TabsContent>
+              <TabsContent value="settings">
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Settings</CardTitle>
+                    <CardDescription>
+                      Manage your settings here.
+                    </CardDescription>
+                  </CardHeader>
+                  <CardContent>
+                    <p className="text-sm text-muted-foreground">
+                      Settings content goes here.
+                    </p>
+                  </CardContent>
+                </Card>
+              </TabsContent>
+            </Tabs>
+          </section>
+
+          {/* Accordion Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Accordion</h3>
+            <Accordion type="single" collapsible className="w-full">
+              <AccordionItem value="item-1">
+                <AccordionTrigger>Is it accessible?</AccordionTrigger>
+                <AccordionContent>
+                  Yes. It adheres to the WAI-ARIA design pattern.
+                </AccordionContent>
+              </AccordionItem>
+              <AccordionItem value="item-2">
+                <AccordionTrigger>Is it styled?</AccordionTrigger>
+                <AccordionContent>
+                  Yes. It comes with default styles that matches the other
+                  components' aesthetic.
+                </AccordionContent>
+              </AccordionItem>
+              <AccordionItem value="item-3">
+                <AccordionTrigger>Is it animated?</AccordionTrigger>
+                <AccordionContent>
+                  Yes. It's animated by default, but you can disable it if you
+                  prefer.
+                </AccordionContent>
+              </AccordionItem>
+            </Accordion>
+          </section>
+
+          {/* Collapsible Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Collapsible</h3>
+            <Collapsible>
+              <Card>
+                <CardHeader>
+                  <CollapsibleTrigger asChild>
+                    <Button variant="ghost" className="w-full justify-between">
+                      <CardTitle>@peduarte starred 3 repositories</CardTitle>
+                    </Button>
+                  </CollapsibleTrigger>
+                </CardHeader>
+                <CollapsibleContent>
+                  <CardContent>
+                    <div className="space-y-2">
+                      <div className="rounded-md border px-4 py-3 font-mono text-sm">
+                        @radix-ui/primitives
+                      </div>
+                      <div className="rounded-md border px-4 py-3 font-mono text-sm">
+                        @radix-ui/colors
+                      </div>
+                      <div className="rounded-md border px-4 py-3 font-mono text-sm">
+                        @stitches/react
+                      </div>
+                    </div>
+                  </CardContent>
+                </CollapsibleContent>
+              </Card>
+            </Collapsible>
+          </section>
+
+          {/* Dialog, Sheet, Drawer Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Overlays</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="flex flex-wrap gap-4">
+                  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+                    <DialogTrigger asChild>
+                      <Button variant="outline">Open Dialog</Button>
+                    </DialogTrigger>
+                    <DialogContent>
+                      <DialogHeader>
+                        <DialogTitle>Test Input</DialogTitle>
+                        <DialogDescription>
+                          Enter some text below. Press Enter to submit (IME composition supported).
+                        </DialogDescription>
+                      </DialogHeader>
+                      <div className="space-y-4 py-4">
+                        <div className="space-y-2">
+                          <Label htmlFor="dialog-input">Input</Label>
+                          <Input
+                            id="dialog-input"
+                            placeholder="Type something..."
+                            value={dialogInput}
+                            onChange={(e) => setDialogInput(e.target.value)}
+                            onKeyDown={handleDialogKeyDown}
+                            autoFocus
+                          />
+                        </div>
+                      </div>
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="outline"
+                          onClick={() => setDialogOpen(false)}
+                        >
+                          Cancel
+                        </Button>
+                        <Button onClick={handleDialogSubmit}>Submit</Button>
+                      </div>
+                    </DialogContent>
+                  </Dialog>
+
+                  <Sheet>
+                    <SheetTrigger asChild>
+                      <Button variant="outline">Open Sheet</Button>
+                    </SheetTrigger>
+                    <SheetContent>
+                      <SheetHeader>
+                        <SheetTitle>Edit profile</SheetTitle>
+                        <SheetDescription>
+                          Make changes to your profile here. Click save when
+                          you're done.
+                        </SheetDescription>
+                      </SheetHeader>
+                    </SheetContent>
+                  </Sheet>
+
+                  <Drawer>
+                    <DrawerTrigger asChild>
+                      <Button variant="outline">Open Drawer</Button>
+                    </DrawerTrigger>
+                    <DrawerContent>
+                      <DrawerHeader>
+                        <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+                        <DrawerDescription>
+                          This action cannot be undone.
+                        </DrawerDescription>
+                      </DrawerHeader>
+                      <DrawerFooter>
+                        <Button>Submit</Button>
+                        <DrawerClose asChild>
+                          <Button variant="outline">Cancel</Button>
+                        </DrawerClose>
+                      </DrawerFooter>
+                    </DrawerContent>
+                  </Drawer>
+
+                  <Popover>
+                    <PopoverTrigger asChild>
+                      <Button variant="outline">Open Popover</Button>
+                    </PopoverTrigger>
+                    <PopoverContent>
+                      <div className="space-y-2">
+                        <h4 className="font-medium leading-none">Dimensions</h4>
+                        <p className="text-sm text-muted-foreground">
+                          Set the dimensions for the layer.
+                        </p>
+                      </div>
+                    </PopoverContent>
+                  </Popover>
+
+                  <Tooltip>
+                    <TooltipTrigger asChild>
+                      <Button variant="outline">Hover me</Button>
+                    </TooltipTrigger>
+                    <TooltipContent>
+                      <p>Add to library</p>
+                    </TooltipContent>
+                  </Tooltip>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Menus Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Menus</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="flex flex-wrap gap-4">
+                  <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                      <Button variant="outline">Dropdown Menu</Button>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent>
+                      <DropdownMenuLabel>My Account</DropdownMenuLabel>
+                      <DropdownMenuSeparator />
+                      <DropdownMenuItem>Profile</DropdownMenuItem>
+                      <DropdownMenuItem>Billing</DropdownMenuItem>
+                      <DropdownMenuItem>Team</DropdownMenuItem>
+                      <DropdownMenuItem>Subscription</DropdownMenuItem>
+                    </DropdownMenuContent>
+                  </DropdownMenu>
+
+                  <ContextMenu>
+                    <ContextMenuTrigger asChild>
+                      <Button variant="outline">Right Click Me</Button>
+                    </ContextMenuTrigger>
+                    <ContextMenuContent>
+                      <ContextMenuItem>Profile</ContextMenuItem>
+                      <ContextMenuItem>Billing</ContextMenuItem>
+                      <ContextMenuItem>Team</ContextMenuItem>
+                      <ContextMenuItem>Subscription</ContextMenuItem>
+                    </ContextMenuContent>
+                  </ContextMenu>
+
+                  <HoverCard>
+                    <HoverCardTrigger asChild>
+                      <Button variant="outline">Hover Card</Button>
+                    </HoverCardTrigger>
+                    <HoverCardContent>
+                      <div className="space-y-2">
+                        <h4 className="text-sm font-semibold">@nextjs</h4>
+                        <p className="text-sm">
+                          The React Framework – created and maintained by
+                          @vercel.
+                        </p>
+                      </div>
+                    </HoverCardContent>
+                  </HoverCard>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Calendar Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Calendar</h3>
+            <Card>
+              <CardContent className="pt-6 flex justify-center">
+                <Calendar
+                  mode="single"
+                  selected={date}
+                  onSelect={setDate}
+                  className="rounded-md border"
+                />
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Carousel Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Carousel</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <Carousel className="w-full max-w-xs mx-auto">
+                  <CarouselContent>
+                    {Array.from({ length: 5 }).map((_, index) => (
+                      <CarouselItem key={index}>
+                        <div className="p-1">
+                          <Card>
+                            <CardContent className="flex aspect-square items-center justify-center p-6">
+                              <span className="text-4xl font-semibold">
+                                {index + 1}
+                              </span>
+                            </CardContent>
+                          </Card>
+                        </div>
+                      </CarouselItem>
+                    ))}
+                  </CarouselContent>
+                  <CarouselPrevious />
+                  <CarouselNext />
+                </Carousel>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Toggle Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Toggle</h3>
+            <Card>
+              <CardContent className="pt-6 space-y-4">
+                <div className="space-y-2">
+                  <Label>Toggle</Label>
+                  <div className="flex gap-2">
+                    <Toggle aria-label="Toggle italic">
+                      <span className="font-bold">B</span>
+                    </Toggle>
+                    <Toggle aria-label="Toggle italic">
+                      <span className="italic">I</span>
+                    </Toggle>
+                    <Toggle aria-label="Toggle underline">
+                      <span className="underline">U</span>
+                    </Toggle>
+                  </div>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Toggle Group</Label>
+                  <ToggleGroup type="multiple">
+                    <ToggleGroupItem value="bold" aria-label="Toggle bold">
+                      <span className="font-bold">B</span>
+                    </ToggleGroupItem>
+                    <ToggleGroupItem value="italic" aria-label="Toggle italic">
+                      <span className="italic">I</span>
+                    </ToggleGroupItem>
+                    <ToggleGroupItem
+                      value="underline"
+                      aria-label="Toggle underline"
+                    >
+                      <span className="underline">U</span>
+                    </ToggleGroupItem>
+                  </ToggleGroup>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Aspect Ratio & Scroll Area Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Layout Components</h3>
+            <Card>
+              <CardContent className="pt-6 space-y-6">
+                <div className="space-y-2">
+                  <Label>Aspect Ratio (16/9)</Label>
+                  <AspectRatio ratio={16 / 9} className="bg-muted">
+                    <div className="flex h-full items-center justify-center">
+                      <p className="text-muted-foreground">16:9 Aspect Ratio</p>
+                    </div>
+                  </AspectRatio>
+                </div>
+                <Separator />
+                <div className="space-y-2">
+                  <Label>Scroll Area</Label>
+                  <ScrollArea className="h-[200px] w-full rounded-md border overflow-hidden">
+                    <div className="p-4">
+                      <div className="space-y-4">
+                        {Array.from({ length: 20 }).map((_, i) => (
+                          <div key={i} className="text-sm">
+                            Item {i + 1}: This is a scrollable content area
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  </ScrollArea>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Resizable Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Resizable Panels</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <ResizablePanelGroup
+                  direction="horizontal"
+                  className="min-h-[200px] rounded-lg border"
+                >
+                  <ResizablePanel defaultSize={50}>
+                    <div className="flex h-full items-center justify-center p-6">
+                      <span className="font-semibold">Panel One</span>
+                    </div>
+                  </ResizablePanel>
+                  <ResizableHandle />
+                  <ResizablePanel defaultSize={50}>
+                    <div className="flex h-full items-center justify-center p-6">
+                      <span className="font-semibold">Panel Two</span>
+                    </div>
+                  </ResizablePanel>
+                </ResizablePanelGroup>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* Toast Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">Toast</h3>
+            <Card>
+              <CardContent className="pt-6 space-y-4">
+                <div className="space-y-2">
+                  <Label>Sonner Toast</Label>
+                  <div className="flex flex-wrap gap-2">
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        sonnerToast.success("Operation successful", {
+                          description: "Your changes have been saved",
+                        });
+                      }}
+                    >
+                      Success
+                    </Button>
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        sonnerToast.error("Operation failed", {
+                          description:
+                            "Cannot complete operation, please try again",
+                        });
+                      }}
+                    >
+                      Error
+                    </Button>
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        sonnerToast.info("Information", {
+                          description: "This is an information message",
+                        });
+                      }}
+                    >
+                      Info
+                    </Button>
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        sonnerToast.warning("Warning", {
+                          description:
+                            "Please note the impact of this operation",
+                        });
+                      }}
+                    >
+                      Warning
+                    </Button>
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        sonnerToast.loading("Loading", {
+                          description: "Please wait",
+                        });
+                      }}
+                    >
+                      Loading
+                    </Button>
+                    <Button
+                      variant="outline"
+                      onClick={() => {
+                        const promise = new Promise(resolve =>
+                          setTimeout(resolve, 2000)
+                        );
+                        sonnerToast.promise(promise, {
+                          loading: "Processing...",
+                          success: "Processing complete!",
+                          error: "Processing failed",
+                        });
+                      }}
+                    >
+                      Promise
+                    </Button>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+
+          {/* AI ChatBox Section */}
+          <section className="space-y-4">
+            <h3 className="text-2xl font-semibold">AI ChatBox</h3>
+            <Card>
+              <CardContent className="pt-6">
+                <div className="space-y-4">
+                  <div className="text-sm text-muted-foreground">
+                    <p>
+                      A ready-to-use chat interface component that integrates with the LLM system.
+                      Features markdown rendering, auto-scrolling, and loading states.
+                    </p>
+                    <p className="mt-2">
+                      This is a demo with simulated responses. In a real app, you'd connect it to a tRPC mutation.
+                    </p>
+                  </div>
+                  <AIChatBox
+                    messages={chatMessages}
+                    onSendMessage={handleChatSend}
+                    isLoading={isChatLoading}
+                    placeholder="Try sending a message..."
+                    height="500px"
+                    emptyStateMessage="How can I help you today?"
+                    suggestedPrompts={[
+                      "What is React?",
+                      "Explain TypeScript",
+                      "How to use tRPC?",
+                      "Best practices for web development",
+                    ]}
+                  />
+                </div>
+              </CardContent>
+            </Card>
+          </section>
+        </div>
+      </main>
+
+      <footer className="border-t py-6 mt-12">
+        <div className="container text-center text-sm text-muted-foreground">
+          <p>Shadcn/ui Component Showcase</p>
+        </div>
+      </footer>
+    </div>
+  );
+}

+ 652 - 0
client/src/pages/DataSources.tsx

@@ -0,0 +1,652 @@
+/**
+ * Data Sources — Lyro-inspired AI Agent knowledge base & API connections
+ * Manage URL imports, file uploads, Q&A pairs, and external API connections
+ */
+import { useState } from "react";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { toast } from "sonner";
+import {
+  Database, Globe, FileText, MessageCircle, Plug,
+  Plus, Trash2, RefreshCw, CheckCircle2, AlertTriangle,
+  XCircle, ExternalLink, Play, Settings, ChevronRight,
+  Package, Truck, RotateCw, Search, Zap,
+  ArrowRight, Code2, TestTube,
+} from "lucide-react";
+
+/* ─── Action Templates (Lyro-style) ─── */
+const ACTION_TEMPLATES = [
+  {
+    id: "check_order",
+    name: "Check Order Status",
+    description: "Look up order status by order number or customer ID",
+    category: "orders",
+    icon: <Package className="w-4 h-4" />,
+    color: "#14532D",
+    method: "GET" as const,
+    endpoint: "/api/orders/{orderId}/status",
+    inputVars: [
+      { name: "orderId", type: "string", description: "Order number", required: true },
+    ],
+    outputVars: [
+      { name: "status", type: "string", description: "Current order status" },
+      { name: "estimatedDelivery", type: "string", description: "Estimated delivery date" },
+      { name: "trackingNumber", type: "string", description: "Shipping tracking number" },
+    ],
+  },
+  {
+    id: "track_shipment",
+    name: "Track Shipment",
+    description: "Get real-time shipping tracking information",
+    category: "shipping",
+    icon: <Truck className="w-4 h-4" />,
+    color: "#0369a1",
+    method: "GET" as const,
+    endpoint: "/api/shipping/{trackingNumber}",
+    inputVars: [
+      { name: "trackingNumber", type: "string", description: "Tracking number", required: true },
+    ],
+    outputVars: [
+      { name: "carrier", type: "string", description: "Shipping carrier" },
+      { name: "currentLocation", type: "string", description: "Current package location" },
+      { name: "estimatedArrival", type: "string", description: "ETA" },
+      { name: "events", type: "array", description: "Tracking event history" },
+    ],
+  },
+  {
+    id: "return_request",
+    name: "Submit Return Request",
+    description: "Initiate a return for an order item",
+    category: "returning",
+    icon: <RotateCw className="w-4 h-4" />,
+    color: "#ca8a04",
+    method: "POST" as const,
+    endpoint: "/api/returns",
+    inputVars: [
+      { name: "orderId", type: "string", description: "Original order number", required: true },
+      { name: "itemId", type: "string", description: "Item to return", required: true },
+      { name: "reason", type: "string", description: "Return reason", required: true },
+    ],
+    outputVars: [
+      { name: "returnId", type: "string", description: "Return authorization number" },
+      { name: "returnLabel", type: "string", description: "Shipping label URL" },
+      { name: "refundEstimate", type: "number", description: "Estimated refund amount" },
+    ],
+  },
+  {
+    id: "cancel_order",
+    name: "Cancel Order",
+    description: "Cancel an order that hasn't shipped yet",
+    category: "cancelling",
+    icon: <XCircle className="w-4 h-4" />,
+    color: "#dc2626",
+    method: "POST" as const,
+    endpoint: "/api/orders/{orderId}/cancel",
+    inputVars: [
+      { name: "orderId", type: "string", description: "Order to cancel", required: true },
+      { name: "reason", type: "string", description: "Cancellation reason", required: false },
+    ],
+    outputVars: [
+      { name: "cancelled", type: "boolean", description: "Whether cancellation succeeded" },
+      { name: "refundAmount", type: "number", description: "Refund amount" },
+      { name: "message", type: "string", description: "Status message" },
+    ],
+  },
+  {
+    id: "customer_lookup",
+    name: "Customer Lookup",
+    description: "Look up customer account details from CRM",
+    category: "customer",
+    icon: <Search className="w-4 h-4" />,
+    color: "#7c3aed",
+    method: "GET" as const,
+    endpoint: "/api/crm/customers/{customerId}",
+    inputVars: [
+      { name: "customerId", type: "string", description: "Customer ID or email", required: true },
+    ],
+    outputVars: [
+      { name: "name", type: "string", description: "Customer name" },
+      { name: "email", type: "string", description: "Email address" },
+      { name: "accountType", type: "string", description: "Account type (dealer/retail)" },
+      { name: "territory", type: "string", description: "Sales territory" },
+      { name: "assignedRep", type: "string", description: "Assigned sales representative" },
+    ],
+  },
+];
+
+/* ─── Tab type ─── */
+type TabId = "sources" | "connections" | "templates";
+
+/* ─── Source type config ─── */
+const SOURCE_TYPES = {
+  url: { icon: <Globe className="w-4 h-4" />, color: "#0369a1", label: "URL Import" },
+  file: { icon: <FileText className="w-4 h-4" />, color: "#7c3aed", label: "File Upload" },
+  qa_pair: { icon: <MessageCircle className="w-4 h-4" />, color: "#14532D", label: "Q&A Pair" },
+  api: { icon: <Plug className="w-4 h-4" />, color: "#ca8a04", label: "API Connection" },
+};
+
+const STATUS_STYLES: Record<string, { bg: string; text: string; icon: React.ReactNode }> = {
+  active: { bg: "#14532D14", text: "#14532D", icon: <CheckCircle2 className="w-3 h-3" /> },
+  inactive: { bg: "#78716C14", text: "#78716C", icon: <XCircle className="w-3 h-3" /> },
+  syncing: { bg: "#0369a114", text: "#0369a1", icon: <RefreshCw className="w-3 h-3 animate-spin" /> },
+  error: { bg: "#dc262614", text: "#dc2626", icon: <AlertTriangle className="w-3 h-3" /> },
+};
+
+export default function DataSources() {
+  const [activeTab, setActiveTab] = useState<TabId>("sources");
+  const [showAddSource, setShowAddSource] = useState(false);
+  const [showAddConnection, setShowAddConnection] = useState(false);
+  const [newSource, setNewSource] = useState<{ name: string; type: "url" | "file" | "qa_pair" | "api"; config: any }>({ name: "", type: "url", config: {} });
+  const [newConnection, setNewConnection] = useState<{
+    name: string; description: string; category: string; method: "GET" | "POST" | "PUT" | "DELETE";
+    endpoint: string; inputVariables: any[]; outputVariables: any[];
+  }>({
+    name: "", description: "", category: "", method: "GET",
+    endpoint: "", inputVariables: [], outputVariables: [],
+  });
+
+  const { data: sources, isLoading: loadingSources } = trpc.dataSources.list.useQuery();
+  const { data: connections, isLoading: loadingConnections } = trpc.apiConnections.list.useQuery();
+
+  const utils = trpc.useUtils();
+
+  const createSource = trpc.dataSources.create.useMutation({
+    onSuccess: () => {
+      toast.success("Data source added");
+      utils.dataSources.list.invalidate();
+      setShowAddSource(false);
+      setNewSource({ name: "", type: "url", config: {} });
+    },
+    onError: (err) => toast.error(err.message),
+  });
+
+  const deleteSource = trpc.dataSources.delete.useMutation({
+    onSuccess: () => {
+      toast.success("Data source removed");
+      utils.dataSources.list.invalidate();
+    },
+  });
+
+  const createConnection = trpc.apiConnections.create.useMutation({
+    onSuccess: () => {
+      toast.success("API connection created");
+      utils.apiConnections.list.invalidate();
+      setShowAddConnection(false);
+      setNewConnection({ name: "", description: "", category: "", method: "GET", endpoint: "", inputVariables: [], outputVariables: [] });
+    },
+    onError: (err) => toast.error(err.message),
+  });
+
+  const deleteConnection = trpc.apiConnections.delete.useMutation({
+    onSuccess: () => {
+      toast.success("API connection removed");
+      utils.apiConnections.list.invalidate();
+    },
+  });
+
+  const testConnection = trpc.apiConnections.test.useMutation({
+    onSuccess: (data) => {
+      if (data.success) {
+        toast.success(`${data.message} (${data.responseTime}ms)`);
+      } else {
+        toast.error(data.message);
+      }
+    },
+  });
+
+  const handleUseTemplate = (template: typeof ACTION_TEMPLATES[0]) => {
+    setNewConnection({
+      name: template.name,
+      description: template.description,
+      category: template.category,
+      method: template.method,
+      endpoint: template.endpoint,
+      inputVariables: template.inputVars,
+      outputVariables: template.outputVars,
+    });
+    setShowAddConnection(true);
+    setActiveTab("connections");
+  };
+
+  const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
+    { id: "sources", label: "Data Sources", icon: <Database className="w-3.5 h-3.5" /> },
+    { id: "connections", label: "API Connections", icon: <Plug className="w-3.5 h-3.5" /> },
+    { id: "templates", label: "Action Templates", icon: <Zap className="w-3.5 h-3.5" /> },
+  ];
+
+  return (
+    <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
+      {/* Top toolbar */}
+      <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <div className="flex items-center gap-3">
+          <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#059669" }}>
+            <Database className="w-3.5 h-3.5 text-white" />
+          </div>
+          <span className="text-sm font-bold" style={{ color: "#059669", fontFamily: "'Playfair Display', serif" }}>Knowledge & Data</span>
+        </div>
+        <div className="flex items-center gap-2">
+          {activeTab === "sources" && (
+            <Button onClick={() => setShowAddSource(true)} size="sm" className="text-xs text-white" style={{ background: "#059669" }}>
+              <Plus className="w-3 h-3 mr-1" /> Add Source
+            </Button>
+          )}
+          {activeTab === "connections" && (
+            <Button onClick={() => setShowAddConnection(true)} size="sm" className="text-xs text-white" style={{ background: "#059669" }}>
+              <Plus className="w-3 h-3 mr-1" /> Add Connection
+            </Button>
+          )}
+        </div>
+      </div>
+
+      {/* Tabs */}
+      <div className="border-b px-4 flex gap-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        {tabs.map(tab => (
+          <button
+            key={tab.id}
+            onClick={() => setActiveTab(tab.id)}
+            className="flex items-center gap-1.5 px-4 py-2.5 text-xs font-medium transition-colors border-b-2"
+            style={{
+              color: activeTab === tab.id ? "#059669" : "#a8a29e",
+              borderColor: activeTab === tab.id ? "#059669" : "transparent",
+            }}
+          >
+            {tab.icon} {tab.label}
+          </button>
+        ))}
+      </div>
+
+      {/* Content */}
+      <div className="flex-1 overflow-y-auto p-4" style={{ background: "#FFFBEB" }}>
+        {/* ─── Data Sources Tab ─── */}
+        {activeTab === "sources" && (
+          <div className="space-y-3">
+            {/* Add source form */}
+            {showAddSource && (
+              <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                <h3 className="text-sm font-semibold mb-3" style={{ color: "#292524" }}>Add Data Source</h3>
+                <div className="space-y-3">
+                  <div className="grid grid-cols-4 gap-2">
+                    {(Object.entries(SOURCE_TYPES) as [string, any][]).map(([key, config]) => (
+                      <button
+                        key={key}
+                        onClick={() => setNewSource(prev => ({ ...prev, type: key as any }))}
+                        className="p-2 rounded-lg border text-center transition-colors"
+                        style={{
+                          borderColor: newSource.type === key ? config.color : "#e7e0d5",
+                          background: newSource.type === key ? `${config.color}08` : "#fff",
+                        }}
+                      >
+                        <div className="flex justify-center mb-1" style={{ color: config.color }}>{config.icon}</div>
+                        <span className="text-[10px] font-medium" style={{ color: newSource.type === key ? config.color : "#78716C" }}>{config.label}</span>
+                      </button>
+                    ))}
+                  </div>
+                  <input
+                    type="text"
+                    value={newSource.name}
+                    onChange={(e) => setNewSource(prev => ({ ...prev, name: e.target.value }))}
+                    placeholder="Source name"
+                    className="w-full px-3 py-2 text-sm rounded-lg border"
+                    style={{ borderColor: "#e7e0d5" }}
+                  />
+                  {newSource.type === "url" && (
+                    <input
+                      type="url"
+                      value={newSource.config.url || ""}
+                      onChange={(e) => setNewSource(prev => ({ ...prev, config: { ...prev.config, url: e.target.value } }))}
+                      placeholder="https://www.homelegance.com/faq"
+                      className="w-full px-3 py-2 text-sm rounded-lg border"
+                      style={{ borderColor: "#e7e0d5" }}
+                    />
+                  )}
+                  {newSource.type === "qa_pair" && (
+                    <>
+                      <input
+                        type="text"
+                        value={newSource.config.question || ""}
+                        onChange={(e) => setNewSource(prev => ({ ...prev, config: { ...prev.config, question: e.target.value } }))}
+                        placeholder="Question"
+                        className="w-full px-3 py-2 text-sm rounded-lg border"
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                      <textarea
+                        value={newSource.config.answer || ""}
+                        onChange={(e) => setNewSource(prev => ({ ...prev, config: { ...prev.config, answer: e.target.value } }))}
+                        placeholder="Answer"
+                        className="w-full px-3 py-2 text-sm rounded-lg border resize-none"
+                        rows={3}
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </>
+                  )}
+                  <div className="flex gap-2 justify-end">
+                    <Button variant="outline" size="sm" onClick={() => setShowAddSource(false)}>Cancel</Button>
+                    <Button
+                      size="sm"
+                      className="text-white"
+                      style={{ background: "#059669" }}
+                      onClick={() => createSource.mutate(newSource)}
+                      disabled={!newSource.name || createSource.isPending}
+                    >
+                      {createSource.isPending ? "Adding..." : "Add Source"}
+                    </Button>
+                  </div>
+                </div>
+              </div>
+            )}
+
+            {/* Sources list */}
+            {loadingSources ? (
+              <div className="text-center py-12">
+                <RefreshCw className="w-5 h-5 animate-spin mx-auto" style={{ color: "#a8a29e" }} />
+              </div>
+            ) : !sources || sources.length === 0 ? (
+              <div className="text-center py-12">
+                <Database className="w-10 h-10 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+                <p className="text-sm" style={{ color: "#78716C" }}>No data sources yet</p>
+                <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Add URLs, files, or Q&A pairs to train your chatbot</p>
+              </div>
+            ) : (
+              sources.map((source: any) => {
+                const typeConfig = SOURCE_TYPES[source.type as keyof typeof SOURCE_TYPES] || SOURCE_TYPES.url;
+                const statusStyle = STATUS_STYLES[source.status] || STATUS_STYLES.active;
+                return (
+                  <div key={source.id} className="p-3 rounded-xl border flex items-center gap-3" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                    <div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0" style={{ background: `${typeConfig.color}14`, color: typeConfig.color }}>
+                      {typeConfig.icon}
+                    </div>
+                    <div className="flex-1 min-w-0">
+                      <div className="flex items-center gap-2">
+                        <span className="text-sm font-semibold truncate" style={{ color: "#292524" }}>{source.name}</span>
+                        <span className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[9px] font-medium" style={{ background: statusStyle.bg, color: statusStyle.text }}>
+                          {statusStyle.icon} {source.status}
+                        </span>
+                      </div>
+                      <div className="flex items-center gap-2 mt-0.5">
+                        <span className="text-[10px]" style={{ color: "#a8a29e" }}>{typeConfig.label}</span>
+                        <span className="text-[10px]" style={{ color: "#a8a29e" }}>· {source.itemCount} items</span>
+                      </div>
+                    </div>
+                    <button
+                      onClick={() => deleteSource.mutate({ id: source.id })}
+                      className="p-1.5 rounded-lg hover:bg-red-50 transition-colors"
+                    >
+                      <Trash2 className="w-3.5 h-3.5" style={{ color: "#dc2626" }} />
+                    </button>
+                  </div>
+                );
+              })
+            )}
+          </div>
+        )}
+
+        {/* ─── API Connections Tab ─── */}
+        {activeTab === "connections" && (
+          <div className="space-y-3">
+            {/* Add connection form */}
+            {showAddConnection && (
+              <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                <h3 className="text-sm font-semibold mb-3" style={{ color: "#292524" }}>
+                  {newConnection.name ? `Configure: ${newConnection.name}` : "Add API Connection"}
+                </h3>
+                <div className="space-y-3">
+                  <div className="grid grid-cols-2 gap-3">
+                    <div>
+                      <label className="text-[11px] font-medium" style={{ color: "#78716C" }}>Name</label>
+                      <input
+                        type="text"
+                        value={newConnection.name}
+                        onChange={(e) => setNewConnection(prev => ({ ...prev, name: e.target.value }))}
+                        placeholder="e.g., Check Order Status"
+                        className="mt-1 w-full px-3 py-2 text-sm rounded-lg border"
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </div>
+                    <div>
+                      <label className="text-[11px] font-medium" style={{ color: "#78716C" }}>Category</label>
+                      <select
+                        value={newConnection.category}
+                        onChange={(e) => setNewConnection(prev => ({ ...prev, category: e.target.value }))}
+                        className="mt-1 w-full px-3 py-2 text-sm rounded-lg border"
+                        style={{ borderColor: "#e7e0d5" }}
+                      >
+                        <option value="">Select category</option>
+                        <option value="orders">Orders</option>
+                        <option value="shipping">Shipping</option>
+                        <option value="returning">Returning</option>
+                        <option value="cancelling">Cancelling</option>
+                        <option value="customer">Customer</option>
+                      </select>
+                    </div>
+                  </div>
+                  <div>
+                    <label className="text-[11px] font-medium" style={{ color: "#78716C" }}>Description</label>
+                    <input
+                      type="text"
+                      value={newConnection.description}
+                      onChange={(e) => setNewConnection(prev => ({ ...prev, description: e.target.value }))}
+                      placeholder="What does this API do?"
+                      className="mt-1 w-full px-3 py-2 text-sm rounded-lg border"
+                      style={{ borderColor: "#e7e0d5" }}
+                    />
+                  </div>
+                  <div className="grid grid-cols-4 gap-3">
+                    <div>
+                      <label className="text-[11px] font-medium" style={{ color: "#78716C" }}>Method</label>
+                      <select
+                        value={newConnection.method}
+                        onChange={(e) => setNewConnection(prev => ({ ...prev, method: e.target.value as any }))}
+                        className="mt-1 w-full px-3 py-2 text-sm rounded-lg border"
+                        style={{ borderColor: "#e7e0d5" }}
+                      >
+                        <option value="GET">GET</option>
+                        <option value="POST">POST</option>
+                        <option value="PUT">PUT</option>
+                        <option value="DELETE">DELETE</option>
+                      </select>
+                    </div>
+                    <div className="col-span-3">
+                      <label className="text-[11px] font-medium" style={{ color: "#78716C" }}>Endpoint</label>
+                      <input
+                        type="text"
+                        value={newConnection.endpoint}
+                        onChange={(e) => setNewConnection(prev => ({ ...prev, endpoint: e.target.value }))}
+                        placeholder="/api/orders/{orderId}/status"
+                        className="mt-1 w-full px-3 py-2 text-sm rounded-lg border font-mono"
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </div>
+                  </div>
+
+                  {/* Input variables preview */}
+                  {newConnection.inputVariables.length > 0 && (
+                    <div className="p-2 rounded-lg" style={{ background: "#f5f0e8" }}>
+                      <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#78716C" }}>Input Variables</span>
+                      <div className="mt-1 space-y-0.5">
+                        {newConnection.inputVariables.map((v: any, i: number) => (
+                          <div key={i} className="flex items-center gap-2 text-[11px]">
+                            <Code2 className="w-3 h-3" style={{ color: "#0369a1" }} />
+                            <span className="font-mono font-medium" style={{ color: "#292524" }}>{v.name}</span>
+                            <span style={{ color: "#a8a29e" }}>({v.type})</span>
+                            {v.required && <span className="text-[9px] px-1 rounded" style={{ background: "#dc262614", color: "#dc2626" }}>required</span>}
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+
+                  {/* Output variables preview */}
+                  {newConnection.outputVariables.length > 0 && (
+                    <div className="p-2 rounded-lg" style={{ background: "#f5f0e8" }}>
+                      <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#78716C" }}>Output Variables</span>
+                      <div className="mt-1 space-y-0.5">
+                        {newConnection.outputVariables.map((v: any, i: number) => (
+                          <div key={i} className="flex items-center gap-2 text-[11px]">
+                            <ArrowRight className="w-3 h-3" style={{ color: "#14532D" }} />
+                            <span className="font-mono font-medium" style={{ color: "#292524" }}>{v.name}</span>
+                            <span style={{ color: "#a8a29e" }}>({v.type})</span>
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+
+                  <div className="flex gap-2 justify-end">
+                    <Button variant="outline" size="sm" onClick={() => { setShowAddConnection(false); setNewConnection({ name: "", description: "", category: "", method: "GET", endpoint: "", inputVariables: [], outputVariables: [] }); }}>
+                      Cancel
+                    </Button>
+                    <Button
+                      size="sm"
+                      className="text-white"
+                      style={{ background: "#059669" }}
+                      onClick={() => createConnection.mutate(newConnection)}
+                      disabled={!newConnection.name || !newConnection.endpoint || createConnection.isPending}
+                    >
+                      {createConnection.isPending ? "Creating..." : "Create Connection"}
+                    </Button>
+                  </div>
+                </div>
+              </div>
+            )}
+
+            {/* Connections list */}
+            {loadingConnections ? (
+              <div className="text-center py-12">
+                <RefreshCw className="w-5 h-5 animate-spin mx-auto" style={{ color: "#a8a29e" }} />
+              </div>
+            ) : !connections || connections.length === 0 ? (
+              <div className="text-center py-12">
+                <Plug className="w-10 h-10 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+                <p className="text-sm" style={{ color: "#78716C" }}>No API connections yet</p>
+                <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>
+                  Use Action Templates to quickly set up connections for orders, shipping, returns, and more
+                </p>
+              </div>
+            ) : (
+              connections.map((conn: any) => (
+                <div key={conn.id} className="p-3 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                  <div className="flex items-center gap-3">
+                    <div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0" style={{ background: "#ca8a0414", color: "#ca8a04" }}>
+                      <Plug className="w-4 h-4" />
+                    </div>
+                    <div className="flex-1 min-w-0">
+                      <div className="flex items-center gap-2">
+                        <span className="text-sm font-semibold truncate" style={{ color: "#292524" }}>{conn.name}</span>
+                        <span className="text-[9px] px-1.5 py-0.5 rounded font-mono font-medium" style={{ background: "#f5f0e8", color: "#78716C" }}>
+                          {conn.method}
+                        </span>
+                        {conn.isActive ? (
+                          <span className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[9px] font-medium" style={{ background: "#14532D14", color: "#14532D" }}>
+                            <CheckCircle2 className="w-2.5 h-2.5" /> Active
+                          </span>
+                        ) : (
+                          <span className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[9px] font-medium" style={{ background: "#78716C14", color: "#78716C" }}>
+                            Inactive
+                          </span>
+                        )}
+                      </div>
+                      <p className="text-[10px] font-mono truncate mt-0.5" style={{ color: "#a8a29e" }}>{conn.endpoint}</p>
+                      {conn.description && <p className="text-[10px] mt-0.5 truncate" style={{ color: "#78716C" }}>{conn.description}</p>}
+                    </div>
+                    <div className="flex items-center gap-1 shrink-0">
+                      <button
+                        onClick={() => testConnection.mutate({ id: conn.id })}
+                        disabled={testConnection.isPending}
+                        className="p-1.5 rounded-lg hover:bg-green-50 transition-colors"
+                        title="Test connection"
+                      >
+                        <TestTube className="w-3.5 h-3.5" style={{ color: "#059669" }} />
+                      </button>
+                      <button
+                        onClick={() => deleteConnection.mutate({ id: conn.id })}
+                        className="p-1.5 rounded-lg hover:bg-red-50 transition-colors"
+                      >
+                        <Trash2 className="w-3.5 h-3.5" style={{ color: "#dc2626" }} />
+                      </button>
+                    </div>
+                  </div>
+                  {conn.executionCount > 0 && (
+                    <div className="mt-2 pt-2 border-t flex items-center gap-3" style={{ borderColor: "#f5f0e8" }}>
+                      <span className="text-[10px]" style={{ color: "#a8a29e" }}>{conn.executionCount} executions</span>
+                      {conn.lastExecutedAt && (
+                        <span className="text-[10px]" style={{ color: "#a8a29e" }}>
+                          Last: {new Date(conn.lastExecutedAt).toLocaleDateString()}
+                        </span>
+                      )}
+                    </div>
+                  )}
+                </div>
+              ))
+            )}
+          </div>
+        )}
+
+        {/* ─── Action Templates Tab ─── */}
+        {activeTab === "templates" && (
+          <div className="space-y-3">
+            <div className="p-3 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+              <p className="text-xs" style={{ color: "#78716C" }}>
+                Action Templates provide pre-configured API connection patterns for common chatbot operations.
+                Click "Use Template" to create a connection with pre-filled settings that you can customize for your specific API endpoints.
+              </p>
+            </div>
+
+            {ACTION_TEMPLATES.map(template => (
+              <div key={template.id} className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                <div className="flex items-start gap-3">
+                  <div className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style={{ background: `${template.color}14`, color: template.color }}>
+                    {template.icon}
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center gap-2">
+                      <span className="text-sm font-semibold" style={{ color: "#292524" }}>{template.name}</span>
+                      <span className="text-[9px] px-1.5 py-0.5 rounded font-mono font-medium" style={{ background: "#f5f0e8", color: "#78716C" }}>
+                        {template.method}
+                      </span>
+                      <span className="text-[9px] px-1.5 py-0.5 rounded-full capitalize" style={{ background: `${template.color}14`, color: template.color }}>
+                        {template.category}
+                      </span>
+                    </div>
+                    <p className="text-[11px] mt-0.5" style={{ color: "#78716C" }}>{template.description}</p>
+                    <p className="text-[10px] font-mono mt-1" style={{ color: "#a8a29e" }}>{template.endpoint}</p>
+
+                    <div className="flex gap-4 mt-2">
+                      <div>
+                        <span className="text-[9px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Inputs</span>
+                        <div className="flex flex-wrap gap-1 mt-0.5">
+                          {template.inputVars.map(v => (
+                            <span key={v.name} className="text-[9px] px-1.5 py-0.5 rounded font-mono" style={{ background: "#0369a108", color: "#0369a1", border: "1px solid #0369a120" }}>
+                              {v.name}{v.required ? "*" : ""}
+                            </span>
+                          ))}
+                        </div>
+                      </div>
+                      <div>
+                        <span className="text-[9px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Outputs</span>
+                        <div className="flex flex-wrap gap-1 mt-0.5">
+                          {template.outputVars.map(v => (
+                            <span key={v.name} className="text-[9px] px-1.5 py-0.5 rounded font-mono" style={{ background: "#14532D08", color: "#14532D", border: "1px solid #14532D20" }}>
+                              {v.name}
+                            </span>
+                          ))}
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                  <Button
+                    onClick={() => handleUseTemplate(template)}
+                    size="sm"
+                    className="text-xs text-white shrink-0"
+                    style={{ background: template.color }}
+                  >
+                    <Zap className="w-3 h-3 mr-1" /> Use Template
+                  </Button>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}

+ 274 - 0
client/src/pages/ForgotPassword.tsx

@@ -0,0 +1,274 @@
+import { useState } from "react";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import {
+  Bot,
+  Loader2,
+  Mail,
+  AlertCircle,
+  ArrowLeft,
+  CheckCircle2,
+  Copy,
+  ExternalLink,
+} from "lucide-react";
+import { Link } from "wouter";
+
+export default function ForgotPassword() {
+  const [email, setEmail] = useState("");
+  const [error, setError] = useState("");
+  const [sent, setSent] = useState(false);
+  const [resetToken, setResetToken] = useState<string | null>(null);
+  const [copied, setCopied] = useState(false);
+
+  const forgotMutation = trpc.auth.forgotPassword.useMutation({
+    onSuccess: (data) => {
+      setSent(true);
+      // In demo mode, the token is returned for testing
+      if (data.resetToken) {
+        setResetToken(data.resetToken);
+      }
+    },
+    onError: (err) => {
+      setError(err.message || "Something went wrong");
+    },
+  });
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+    if (!email) {
+      setError("Please enter your email address");
+      return;
+    }
+    forgotMutation.mutate({ email });
+  };
+
+  const copyResetLink = () => {
+    if (resetToken) {
+      const link = `${window.location.origin}/reset-password/${resetToken}`;
+      navigator.clipboard.writeText(link);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    }
+  };
+
+  const isLoading = forgotMutation.isPending;
+
+  return (
+    <div
+      className="min-h-screen flex items-center justify-center px-4 py-12 relative"
+      style={{ background: "linear-gradient(135deg, #FFFBEB 0%, #f5f0e8 50%, #e8e0d4 100%)" }}
+    >
+      <div className="absolute top-0 left-0 w-full h-1" style={{ background: "#14532D" }} />
+      <div
+        className="absolute top-32 right-32 w-48 h-48 rounded-full opacity-10"
+        style={{ background: "#C2410C", filter: "blur(60px)" }}
+      />
+
+      <div className="w-full max-w-md relative z-10">
+        <Link
+          href="/login"
+          className="inline-flex items-center gap-1.5 text-sm mb-6 transition-colors hover:opacity-80"
+          style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          <ArrowLeft className="w-4 h-4" />
+          Back to sign in
+        </Link>
+
+        <Card
+          className="border-0 shadow-xl"
+          style={{ background: "#fff", boxShadow: "0 20px 60px rgba(120, 113, 108, 0.12)" }}
+        >
+          <CardHeader className="text-center pb-2">
+            <div className="flex justify-center mb-4">
+              <div
+                className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                style={{
+                  background: sent
+                    ? "linear-gradient(135deg, #16a34a 0%, #15803d 100%)"
+                    : "linear-gradient(135deg, #C2410C 0%, #ea580c 100%)",
+                  boxShadow: sent
+                    ? "0 4px 16px rgba(22, 163, 74, 0.3)"
+                    : "0 4px 16px rgba(194, 65, 12, 0.3)",
+                }}
+              >
+                {sent ? (
+                  <CheckCircle2 className="w-7 h-7 text-white" />
+                ) : (
+                  <Mail className="w-7 h-7 text-white" />
+                )}
+              </div>
+            </div>
+            <CardTitle
+              className="text-2xl"
+              style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+            >
+              {sent ? "Check Your Email" : "Forgot Password?"}
+            </CardTitle>
+            <CardDescription
+              className="text-sm mt-1"
+              style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+            >
+              {sent
+                ? "We've sent password reset instructions to your email"
+                : "Enter your email and we'll send you a reset link"}
+            </CardDescription>
+          </CardHeader>
+
+          <CardContent className="pt-4">
+            {sent ? (
+              <div className="space-y-4">
+                <div
+                  className="p-4 rounded-lg text-sm leading-relaxed"
+                  style={{
+                    background: "#F0FDF4",
+                    color: "#166534",
+                    border: "1px solid #BBF7D0",
+                    fontFamily: "'Source Sans 3', sans-serif",
+                  }}
+                >
+                  <p className="font-medium mb-1">Reset instructions sent!</p>
+                  <p className="text-xs" style={{ color: "#15803d" }}>
+                    If an account exists for <strong>{email}</strong>, you'll receive an email with a link to reset your password. The link expires in 1 hour.
+                  </p>
+                </div>
+
+                {/* Demo mode: show the reset link directly */}
+                {resetToken && (
+                  <div
+                    className="p-4 rounded-lg text-sm"
+                    style={{
+                      background: "#FFFBEB",
+                      border: "1px solid #FDE68A",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                    }}
+                  >
+                    <p className="font-medium mb-2" style={{ color: "#92400E" }}>
+                      Demo Mode — Reset Link
+                    </p>
+                    <p className="text-xs mb-3" style={{ color: "#a16207" }}>
+                      In production, this link would be sent via email. For testing, you can use it directly:
+                    </p>
+                    <div className="flex gap-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        className="text-xs flex-1"
+                        style={{ borderColor: "#FDE68A", color: "#92400E" }}
+                        onClick={copyResetLink}
+                      >
+                        <Copy className="w-3 h-3 mr-1" />
+                        {copied ? "Copied!" : "Copy Link"}
+                      </Button>
+                      <Link href={`/reset-password/${resetToken}`}>
+                        <Button
+                          size="sm"
+                          className="text-xs"
+                          style={{ background: "#C2410C" }}
+                        >
+                          <ExternalLink className="w-3 h-3 mr-1" />
+                          Open Reset Page
+                        </Button>
+                      </Link>
+                    </div>
+                  </div>
+                )}
+
+                <div className="text-center pt-2">
+                  <p
+                    className="text-xs mb-3"
+                    style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}
+                  >
+                    Didn't receive the email? Check your spam folder or
+                  </p>
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    className="text-xs"
+                    style={{ borderColor: "#e7e0d5", color: "#57534e" }}
+                    onClick={() => {
+                      setSent(false);
+                      setResetToken(null);
+                    }}
+                  >
+                    Try again
+                  </Button>
+                </div>
+              </div>
+            ) : (
+              <form onSubmit={handleSubmit} className="space-y-4">
+                {error && (
+                  <div
+                    className="flex items-center gap-2 p-3 rounded-lg text-sm"
+                    style={{
+                      background: "#FEF2F2",
+                      color: "#DC2626",
+                      border: "1px solid #FECACA",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                    }}
+                  >
+                    <AlertCircle className="w-4 h-4 shrink-0" />
+                    {error}
+                  </div>
+                )}
+
+                <div className="space-y-2">
+                  <Label
+                    htmlFor="email"
+                    className="text-sm font-medium"
+                    style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                  >
+                    Email Address
+                  </Label>
+                  <div className="relative">
+                    <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                    <Input
+                      id="email"
+                      type="email"
+                      placeholder="you@company.com"
+                      value={email}
+                      onChange={(e) => setEmail(e.target.value)}
+                      className="pl-10 h-11"
+                      style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                      disabled={isLoading}
+                      autoComplete="email"
+                    />
+                  </div>
+                </div>
+
+                <Button
+                  type="submit"
+                  className="w-full h-11 text-sm font-semibold"
+                  style={{
+                    background: "linear-gradient(135deg, #C2410C 0%, #ea580c 100%)",
+                    fontFamily: "'Source Sans 3', sans-serif",
+                    boxShadow: "0 2px 8px rgba(194, 65, 12, 0.25)",
+                  }}
+                  disabled={isLoading}
+                >
+                  {isLoading ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      Sending...
+                    </>
+                  ) : (
+                    "Send Reset Link"
+                  )}
+                </Button>
+              </form>
+            )}
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 854 - 0
client/src/pages/Home.tsx

@@ -0,0 +1,854 @@
+/**
+ * Home Page — Homelegance Chatbot Integration Demo
+ * Design: Warm Showroom — Scandinavian warmth, editorial magazine layout
+ * Playfair Display headings, Source Sans 3 body, forest green + terracotta + cream palette
+ */
+import { useRef } from "react";
+import { motion, useInView } from "framer-motion";
+import {
+  MessageCircle,
+  ArrowDown,
+  Zap,
+  Clock,
+  TrendingUp,
+  Shield,
+  Code2,
+  Layers,
+  GitBranch,
+  CheckCircle2,
+  ExternalLink,
+  ChevronRight,
+  Bot,
+  Workflow,
+  Server,
+  Puzzle,
+} from "lucide-react";
+import ChatbotWidgetLive from "@/components/ChatbotWidgetLive";
+import WorkflowDiagram from "@/components/WorkflowDiagram";
+import ArchitectureDiagram from "@/components/ArchitectureDiagram";
+import CodeSnippet from "@/components/CodeSnippet";
+
+/* ─── Asset URLs ─── */
+const HERO_IMG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663460016926/j3fMicxd96oQ6qWqevDJt5/hero-showroom-3kpaMXDd6czasp7Qgkfo5b.webp";
+const WORKFLOW_BG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663460016926/j3fMicxd96oQ6qWqevDJt5/workflow-bg-fRHbG9vamH9g2W8W8BrAkm.webp";
+const CHATBOT_ILLUS = "https://d2xsxph8kpxj0f.cloudfront.net/310519663460016926/j3fMicxd96oQ6qWqevDJt5/chatbot-illustration-BFAPtQzws8tEXvTD9tFm6G.webp";
+const INTEGRATION_IMG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663460016926/j3fMicxd96oQ6qWqevDJt5/integration-hero-ARBCtGWzCjUdh76eghxiim.webp";
+const ARCH_BG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663460016926/j3fMicxd96oQ6qWqevDJt5/architecture-bg-k6ZMJihrNCrLkF6CwRB4Jj.webp";
+
+/* ─── Code Snippets ─── */
+const EMBED_CODE = `<!-- Add before closing </body> tag -->
+<script>
+  (function() {
+    var s = document.createElement('script');
+    s.src = 'https://cdn.homelegance.com/chatbot/v2/widget.js';
+    s.async = true;
+    s.dataset.siteId = 'YOUR_SITE_ID';
+    s.dataset.position = 'bottom-right';
+    s.dataset.theme = 'forest';
+    s.dataset.greeting = 'Welcome to Homelegance!';
+    document.body.appendChild(s);
+  })();
+</script>`;
+
+const CONFIG_CODE = `// chatbot-config.js
+export default {
+  // Branding
+  brandName: "Homelegance",
+  primaryColor: "#14532D",
+  accentColor: "#C2410C",
+  logoUrl: "/assets/logo.svg",
+
+  // Behavior
+  autoOpen: false,
+  openDelay: 5000,        // ms before showing greeting
+  position: "bottom-right",
+
+  // AI Configuration
+  model: "gpt-4o-mini",
+  temperature: 0.7,
+  systemPrompt: \`You are a helpful Homelegance
+    furniture assistant. Help dealers find
+    products, check orders, and answer FAQs.
+    Always be warm and professional.\`,
+
+  // Integrations
+  webhookUrl: "https://api.homelegance.com/chatbot/webhook",
+  crmIntegration: "salesforce",
+  analyticsId: "UA-XXXXXXXX-X",
+
+  // Escalation Rules
+  escalation: {
+    maxBotTurns: 8,
+    sentimentThreshold: -0.3,
+    keywords: ["speak to human", "representative"],
+    transferMessage: "Connecting you with a team member..."
+  }
+};`;
+
+const WEBHOOK_CODE = `// server/chatbot-webhook.js
+import express from 'express';
+const router = express.Router();
+
+router.post('/webhook', async (req, res) => {
+  const { event, sessionId, message, metadata } = req.body;
+
+  switch (event) {
+    case 'message.received':
+      // Process incoming user message
+      const intent = await detectIntent(message.text);
+      const response = await generateResponse(intent, metadata);
+      return res.json({ reply: response });
+
+    case 'escalation.requested':
+      // Route to live agent queue
+      await notifyAgentTeam(sessionId, metadata);
+      return res.json({ status: 'queued' });
+
+    case 'session.ended':
+      // Log conversation for analytics
+      await saveConversationLog(sessionId);
+      return res.json({ status: 'logged' });
+
+    default:
+      return res.json({ status: 'ok' });
+  }
+});
+
+export default router;`;
+
+const API_CODE = `// Example: Product Search API Integration
+async function searchProducts(query, filters) {
+  const response = await fetch(
+    'https://api.homelegance.com/v2/products/search',
+    {
+      method: 'POST',
+      headers: {
+        'Authorization': 'Bearer YOUR_API_KEY',
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        query,
+        filters: {
+          category: filters.category,
+          collection: filters.collection,
+          priceRange: filters.priceRange,
+          inStock: true
+        },
+        limit: 5,
+        include: ['images', 'pricing', 'availability']
+      })
+    }
+  );
+  return response.json();
+}`;
+
+/* ─── Section wrapper with scroll animation ─── */
+function AnimatedSection({
+  children,
+  className = "",
+  id,
+  style,
+}: {
+  children: React.ReactNode;
+  className?: string;
+  id?: string;
+  style?: React.CSSProperties;
+}) {
+  const ref = useRef(null);
+  const isInView = useInView(ref, { once: true, margin: "-80px" });
+  return (
+    <motion.section
+      ref={ref}
+      id={id}
+      initial={{ opacity: 0, y: 40 }}
+      animate={isInView ? { opacity: 1, y: 0 } : {}}
+      transition={{ duration: 0.6, ease: "easeOut" }}
+      className={className}
+      style={style}
+    >
+      {children}
+    </motion.section>
+  );
+}
+
+/* ─── Navigation ─── */
+function Nav() {
+  const links = [
+    { label: "Workflow", href: "#workflow" },
+    { label: "Architecture", href: "#architecture" },
+    { label: "Integration", href: "#integration" },
+    { label: "Benefits", href: "#benefits" },
+
+  ];
+
+  return (
+    <nav
+      className="fixed top-0 left-0 right-0 z-40 backdrop-blur-md border-b"
+      style={{ background: "rgba(255, 251, 235, 0.88)", borderColor: "#e7e0d5" }}
+    >
+      <div className="container flex items-center justify-between h-16">
+        <div className="flex items-center gap-3">
+          <div
+            className="w-9 h-9 rounded-lg flex items-center justify-center"
+            style={{ background: "#14532D" }}
+          >
+            <Bot className="w-5 h-5 text-white" />
+          </div>
+          <div>
+            <span
+              className="text-sm font-bold tracking-tight"
+              style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}
+            >
+              Homelegance
+            </span>
+            <span
+              className="text-xs block -mt-0.5"
+              style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}
+            >
+              Chatbot Integration
+            </span>
+          </div>
+        </div>
+        <div className="hidden md:flex items-center gap-1">
+          {links.map((link) => (
+            <a
+              key={link.href}
+              href={link.href}
+              className="px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-black/5"
+              style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+            >
+              {link.label}
+            </a>
+          ))}
+          <div className="w-px h-5 mx-1" style={{ background: "#e7e0d5" }} />
+          <a
+            href="/login"
+            className="px-3 py-2 rounded-lg text-sm font-semibold transition-colors"
+            style={{
+              background: "#14532D",
+              color: "#fff",
+              fontFamily: "'Source Sans 3', sans-serif",
+              borderRadius: "8px",
+              padding: "6px 16px",
+            }}
+          >
+            Sign In
+          </a>
+        </div>
+      </div>
+    </nav>
+  );
+}
+
+/* ─── Hero Section ─── */
+function HeroSection() {
+  return (
+    <section className="relative min-h-[92vh] flex items-center pt-16 overflow-hidden">
+      {/* Background image */}
+      <div className="absolute inset-0">
+        <img
+          src={HERO_IMG}
+          alt="Modern furniture showroom"
+          className="w-full h-full object-cover"
+        />
+        <div
+          className="absolute inset-0"
+          style={{
+            background: "linear-gradient(135deg, rgba(20, 83, 45, 0.82) 0%, rgba(20, 83, 45, 0.65) 40%, rgba(194, 65, 12, 0.3) 100%)",
+          }}
+        />
+      </div>
+
+      <div className="container relative z-10">
+        <div className="max-w-2xl">
+          <motion.div
+            initial={{ opacity: 0, y: 20 }}
+            animate={{ opacity: 1, y: 0 }}
+            transition={{ delay: 0.2 }}
+            className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-8"
+            style={{ background: "rgba(255,255,255,0.15)", backdropFilter: "blur(8px)" }}
+          >
+            <Zap className="w-4 h-4 text-amber-300" />
+            <span className="text-sm text-white/90" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+              AI-Powered Customer Experience
+            </span>
+          </motion.div>
+
+          <motion.h1
+            initial={{ opacity: 0, y: 30 }}
+            animate={{ opacity: 1, y: 0 }}
+            transition={{ delay: 0.3 }}
+            className="text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight mb-6"
+            style={{ fontFamily: "'Playfair Display', serif", color: "#fff" }}
+          >
+            Chatbot Workflow
+            <br />
+            <span style={{ color: "#fbbf24" }}>& Integration Guide</span>
+          </motion.h1>
+
+          <motion.p
+            initial={{ opacity: 0, y: 30 }}
+            animate={{ opacity: 1, y: 0 }}
+            transition={{ delay: 0.4 }}
+            className="text-lg mb-10 max-w-xl leading-relaxed"
+            style={{ color: "rgba(255,255,255,0.85)", fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            A comprehensive guide to designing, building, and integrating an
+            AI-powered chatbot for homelegance.com — from conversation workflows
+            to production deployment.
+          </motion.p>
+
+          <motion.div
+            initial={{ opacity: 0, y: 30 }}
+            animate={{ opacity: 1, y: 0 }}
+            transition={{ delay: 0.5 }}
+            className="flex flex-wrap gap-4"
+          >
+            <a
+              href="#workflow"
+              className="inline-flex items-center gap-2 px-6 py-3.5 rounded-xl text-sm font-semibold transition-all hover:shadow-lg"
+              style={{
+                background: "#fff",
+                color: "#14532D",
+                fontFamily: "'Source Sans 3', sans-serif",
+                boxShadow: "0 4px 16px rgba(0,0,0,0.1)",
+              }}
+            >
+              <Workflow className="w-4 h-4" />
+              Explore Workflow
+            </a>
+            <a
+              href="#integration"
+              className="inline-flex items-center gap-2 px-6 py-3.5 rounded-xl text-sm font-semibold transition-all hover:bg-white/20"
+              style={{
+                background: "rgba(255,255,255,0.12)",
+                color: "#fff",
+                border: "1px solid rgba(255,255,255,0.25)",
+                fontFamily: "'Source Sans 3', sans-serif",
+              }}
+            >
+              <Code2 className="w-4 h-4" />
+              Integration Code
+            </a>
+          </motion.div>
+        </div>
+      </div>
+
+      {/* Scroll indicator */}
+      <motion.div
+        animate={{ y: [0, 8, 0] }}
+        transition={{ repeat: Infinity, duration: 2 }}
+        className="absolute bottom-8 left-1/2 -translate-x-1/2"
+      >
+        <ArrowDown className="w-5 h-5 text-white/50" />
+      </motion.div>
+    </section>
+  );
+}
+
+/* ─── Stats Bar ─── */
+function StatsBar() {
+  const stats = [
+    { value: "73%", label: "Queries Resolved by Bot", icon: <CheckCircle2 className="w-5 h-5" /> },
+    { value: "< 2s", label: "Average Response Time", icon: <Clock className="w-5 h-5" /> },
+    { value: "24/7", label: "Availability", icon: <Shield className="w-5 h-5" /> },
+    { value: "40%", label: "Support Cost Reduction", icon: <TrendingUp className="w-5 h-5" /> },
+  ];
+
+  return (
+    <div style={{ background: "#14532D" }}>
+      <div className="container py-10">
+        <div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
+          {stats.map((stat, i) => (
+            <motion.div
+              key={stat.label}
+              initial={{ opacity: 0, y: 20 }}
+              whileInView={{ opacity: 1, y: 0 }}
+              viewport={{ once: true }}
+              transition={{ delay: i * 0.1 }}
+              className="text-center"
+            >
+              <div className="flex justify-center mb-2">
+                <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: "rgba(255,255,255,0.12)", color: "#fbbf24" }}>
+                  {stat.icon}
+                </div>
+              </div>
+              <div className="text-2xl sm:text-3xl font-bold text-white mb-1" style={{ fontFamily: "'Playfair Display', serif" }}>
+                {stat.value}
+              </div>
+              <div className="text-xs text-green-200/70" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+                {stat.label}
+              </div>
+            </motion.div>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Workflow Section ─── */
+function WorkflowSection() {
+  return (
+    <AnimatedSection
+      id="workflow"
+      className="py-20 lg:py-28 relative"
+    >
+      <div
+        className="absolute inset-0 opacity-30"
+        style={{
+          backgroundImage: `url(${WORKFLOW_BG})`,
+          backgroundSize: "cover",
+          backgroundPosition: "center",
+        }}
+      />
+      <div className="container relative z-10">
+        <div className="max-w-2xl mx-auto text-center mb-14">
+          <div
+            className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-5"
+            style={{ background: "#14532D12", color: "#14532D" }}
+          >
+            <GitBranch className="w-4 h-4" />
+            <span className="text-xs font-semibold uppercase tracking-wider" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+              Conversation Flow
+            </span>
+          </div>
+          <h2
+            className="text-3xl sm:text-4xl font-bold mb-4"
+            style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+          >
+            Chatbot Workflow Design
+          </h2>
+          <p className="text-base leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            The chatbot follows a structured conversation flow that routes users to the right
+            information based on their intent. Click on any node below to see how each branch works.
+          </p>
+        </div>
+
+        <WorkflowDiagram />
+      </div>
+    </AnimatedSection>
+  );
+}
+
+/* ─── Architecture Section ─── */
+function ArchitectureSection() {
+  return (
+    <AnimatedSection
+      id="architecture"
+      className="py-20 lg:py-28"
+      style={{ background: "#f5f0e8" }}
+    >
+      <div className="container">
+        <div className="grid lg:grid-cols-2 gap-12 items-start">
+          {/* Left: text */}
+          <div>
+            <div
+              className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-5"
+              style={{ background: "#C2410C12", color: "#C2410C" }}
+            >
+              <Server className="w-4 h-4" />
+              <span className="text-xs font-semibold uppercase tracking-wider" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+                System Architecture
+              </span>
+            </div>
+            <h2
+              className="text-3xl sm:text-4xl font-bold mb-4"
+              style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+            >
+              How It All Connects
+            </h2>
+            <p className="text-base leading-relaxed mb-8" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+              The chatbot architecture is designed as a layered system. The frontend widget communicates
+              through a secure API gateway to the AI processing layer, which connects to Homelegance's
+              existing CRM and fulfillment systems.
+            </p>
+
+            <div className="space-y-4">
+              {[
+                {
+                  title: "Client Layer",
+                  desc: "The chat widget is embedded via a lightweight JavaScript SDK that loads asynchronously. It handles the UI, message rendering, and WebSocket connection for real-time communication.",
+                },
+                {
+                  title: "Middleware",
+                  desc: "An API gateway handles authentication, rate limiting, and request routing. Webhooks process events like new messages, escalation requests, and session endings.",
+                },
+                {
+                  title: "AI & Logic",
+                  desc: "The NLP engine uses a fine-tuned language model to detect user intent and extract entities (product names, order numbers, locations). The dialog manager maintains conversation state.",
+                },
+                {
+                  title: "Integrations",
+                  desc: "The chatbot connects to Homelegance's CRM for order data, the product catalog API for search, and the dealer database for location queries. Live agent handoff uses the existing support queue.",
+                },
+              ].map((item, i) => (
+                <motion.div
+                  key={item.title}
+                  initial={{ opacity: 0, x: -20 }}
+                  whileInView={{ opacity: 1, x: 0 }}
+                  viewport={{ once: true }}
+                  transition={{ delay: i * 0.1 }}
+                  className="p-4 rounded-xl"
+                  style={{ background: "#fff", border: "1px solid #e7e0d5" }}
+                >
+                  <h4 className="text-sm font-bold mb-1" style={{ color: "#14532D", fontFamily: "'Source Sans 3', sans-serif" }}>
+                    {item.title}
+                  </h4>
+                  <p className="text-sm leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+                    {item.desc}
+                  </p>
+                </motion.div>
+              ))}
+            </div>
+          </div>
+
+          {/* Right: diagram */}
+          <div className="lg:sticky lg:top-24">
+            <div className="p-6 rounded-2xl" style={{ background: "#FFFBEB", border: "1px solid #e7e0d5" }}>
+              <ArchitectureDiagram />
+            </div>
+          </div>
+        </div>
+      </div>
+    </AnimatedSection>
+  );
+}
+
+/* ─── Integration Guide Section ─── */
+function IntegrationSection() {
+  const steps = [
+    {
+      step: "01",
+      title: "Embed the Widget",
+      description: "Add a single script tag to your website's HTML. The widget loads asynchronously and won't affect page performance. Configure it with your site ID and preferred settings.",
+      code: EMBED_CODE,
+      language: "html",
+      filename: "index.html",
+    },
+    {
+      step: "02",
+      title: "Configure Behavior",
+      description: "Customize the chatbot's appearance, AI model, escalation rules, and integration endpoints through a configuration file. This controls everything from colors to conversation flow.",
+      code: CONFIG_CODE,
+      language: "javascript",
+      filename: "chatbot-config.js",
+    },
+    {
+      step: "03",
+      title: "Set Up Webhooks",
+      description: "Create server-side webhook handlers to process chatbot events. This is where you connect to your CRM, order management system, and live agent queue.",
+      code: WEBHOOK_CODE,
+      language: "javascript",
+      filename: "server/chatbot-webhook.js",
+    },
+    {
+      step: "04",
+      title: "Connect Product APIs",
+      description: "Integrate the chatbot with Homelegance's product catalog API to enable real-time product search, availability checking, and personalized recommendations within conversations.",
+      code: API_CODE,
+      language: "javascript",
+      filename: "api/product-search.js",
+    },
+  ];
+
+  return (
+    <AnimatedSection id="integration" className="py-20 lg:py-28">
+      <div className="container">
+        <div className="max-w-2xl mx-auto text-center mb-14">
+          <div
+            className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-5"
+            style={{ background: "#14532D12", color: "#14532D" }}
+          >
+            <Code2 className="w-4 h-4" />
+            <span className="text-xs font-semibold uppercase tracking-wider" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+              Step-by-Step Guide
+            </span>
+          </div>
+          <h2
+            className="text-3xl sm:text-4xl font-bold mb-4"
+            style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+          >
+            Integration Guide
+          </h2>
+          <p className="text-base leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Follow these four steps to integrate the chatbot into homelegance.com.
+            Each step includes production-ready code that you can customize to your needs.
+          </p>
+        </div>
+
+        {/* Steps */}
+        <div className="max-w-4xl mx-auto space-y-16">
+          {steps.map((step, i) => (
+            <motion.div
+              key={step.step}
+              initial={{ opacity: 0, y: 30 }}
+              whileInView={{ opacity: 1, y: 0 }}
+              viewport={{ once: true }}
+              transition={{ delay: 0.1 }}
+              className="relative"
+            >
+              {/* Step number + connector */}
+              <div className="flex items-start gap-6">
+                <div className="shrink-0 hidden sm:flex flex-col items-center">
+                  <div
+                    className="w-14 h-14 rounded-2xl flex items-center justify-center text-lg font-bold"
+                    style={{
+                      background: "#14532D",
+                      color: "#fff",
+                      fontFamily: "'Playfair Display', serif",
+                      boxShadow: "0 4px 16px rgba(20, 83, 45, 0.25)",
+                    }}
+                  >
+                    {step.step}
+                  </div>
+                  {i < steps.length - 1 && (
+                    <div className="w-0.5 h-16 mt-3" style={{ background: "#e7e0d5" }} />
+                  )}
+                </div>
+
+                <div className="flex-1">
+                  <h3
+                    className="text-xl sm:text-2xl font-bold mb-3"
+                    style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+                  >
+                    <span className="sm:hidden mr-2" style={{ color: "#C2410C" }}>{step.step}.</span>
+                    {step.title}
+                  </h3>
+                  <p className="text-sm leading-relaxed mb-5" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+                    {step.description}
+                  </p>
+                  <CodeSnippet
+                    code={step.code}
+                    language={step.language}
+                    filename={step.filename}
+                  />
+                </div>
+              </div>
+            </motion.div>
+          ))}
+        </div>
+      </div>
+    </AnimatedSection>
+  );
+}
+
+/* ─── Benefits Section ─── */
+function BenefitsSection() {
+  const benefits = [
+    {
+      icon: <Clock className="w-6 h-6" />,
+      title: "24/7 Availability",
+      description: "The chatbot never sleeps. Dealers and retailers across time zones can get instant answers to product questions, check order status, and find information at any hour.",
+    },
+    {
+      icon: <TrendingUp className="w-6 h-6" />,
+      title: "Increased Engagement",
+      description: "Interactive conversation drives deeper product exploration. Dealers discover collections they might have missed, leading to larger and more diverse orders.",
+    },
+    {
+      icon: <Zap className="w-6 h-6" />,
+      title: "Faster Resolution",
+      description: "Common queries like order status, dealer location, and product specs are answered instantly. This frees up sales reps to focus on complex, high-value conversations.",
+    },
+    {
+      icon: <Shield className="w-6 h-6" />,
+      title: "Consistent Experience",
+      description: "Every interaction follows the same quality standards. The chatbot delivers accurate product information, current pricing, and up-to-date availability data every time.",
+    },
+    {
+      icon: <Layers className="w-6 h-6" />,
+      title: "Scalable Support",
+      description: "Handle hundreds of simultaneous conversations without adding headcount. The chatbot scales with your business during peak seasons like market weeks and promotions.",
+    },
+    {
+      icon: <Puzzle className="w-6 h-6" />,
+      title: "Seamless Integration",
+      description: "Connects with existing CRM, ERP, and order management systems. No need to replace current tools — the chatbot enhances what you already have.",
+    },
+  ];
+
+  return (
+    <AnimatedSection
+      id="benefits"
+      className="py-20 lg:py-28"
+    >
+      <div
+        className="absolute inset-0 opacity-5"
+        style={{
+          backgroundImage: `url(${ARCH_BG})`,
+          backgroundSize: "cover",
+        }}
+      />
+      <div className="container relative z-10">
+        <div className="max-w-2xl mx-auto text-center mb-14">
+          <div
+            className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-5"
+            style={{ background: "#C2410C12", color: "#C2410C" }}
+          >
+            <Zap className="w-4 h-4" />
+            <span className="text-xs font-semibold uppercase tracking-wider" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+              Why a Chatbot
+            </span>
+          </div>
+          <h2
+            className="text-3xl sm:text-4xl font-bold mb-4"
+            style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+          >
+            Business Benefits
+          </h2>
+          <p className="text-base leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            An AI chatbot transforms how Homelegance connects with its dealer network,
+            delivering measurable improvements across customer experience and operational efficiency.
+          </p>
+        </div>
+
+        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
+          {benefits.map((benefit, i) => (
+            <motion.div
+              key={benefit.title}
+              initial={{ opacity: 0, y: 20 }}
+              whileInView={{ opacity: 1, y: 0 }}
+              viewport={{ once: true }}
+              transition={{ delay: i * 0.08 }}
+              whileHover={{ y: -4, boxShadow: "0 12px 32px rgba(120, 113, 108, 0.12)" }}
+              className="p-6 rounded-xl transition-all"
+              style={{
+                background: "#fff",
+                border: "1px solid #e7e0d5",
+                boxShadow: "0 2px 8px rgba(120, 113, 108, 0.06)",
+              }}
+            >
+              <div
+                className="w-12 h-12 rounded-xl flex items-center justify-center mb-4"
+                style={{ background: "#14532D12", color: "#14532D" }}
+              >
+                {benefit.icon}
+              </div>
+              <h3
+                className="text-lg font-bold mb-2"
+                style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+              >
+                {benefit.title}
+              </h3>
+              <p className="text-sm leading-relaxed" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+                {benefit.description}
+              </p>
+            </motion.div>
+          ))}
+        </div>
+      </div>
+    </AnimatedSection>
+  );
+}
+
+/* ─── CTA Section ─── */
+function CTASection() {
+  return (
+    <section className="relative py-20 overflow-hidden">
+      <div className="absolute inset-0">
+        <img
+          src={INTEGRATION_IMG}
+          alt="Developer workspace"
+          className="w-full h-full object-cover"
+        />
+        <div
+          className="absolute inset-0"
+          style={{
+            background: "linear-gradient(135deg, rgba(20, 83, 45, 0.88) 0%, rgba(20, 83, 45, 0.75) 100%)",
+          }}
+        />
+      </div>
+      <div className="container relative z-10 text-center">
+        <motion.div
+          initial={{ opacity: 0, y: 20 }}
+          whileInView={{ opacity: 1, y: 0 }}
+          viewport={{ once: true }}
+        >
+          <h2
+            className="text-3xl sm:text-4xl font-bold mb-4 text-white"
+            style={{ fontFamily: "'Playfair Display', serif" }}
+          >
+            Ready to Get Started?
+          </h2>
+          <p
+            className="text-base mb-8 max-w-lg mx-auto"
+            style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            Try the live chatbot demo by clicking the green chat icon in the bottom-right corner.
+            Experience the conversation flow firsthand.
+          </p>
+          <div className="flex flex-wrap justify-center gap-4">
+            <a
+              href="#workflow"
+              className="inline-flex items-center gap-2 px-6 py-3.5 rounded-xl text-sm font-semibold transition-all"
+              style={{
+                background: "#fff",
+                color: "#14532D",
+                fontFamily: "'Source Sans 3', sans-serif",
+                boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
+              }}
+            >
+              Review Workflow
+              <ChevronRight className="w-4 h-4" />
+            </a>
+            <a
+              href="#integration"
+              className="inline-flex items-center gap-2 px-6 py-3.5 rounded-xl text-sm font-semibold transition-all"
+              style={{
+                background: "rgba(255,255,255,0.12)",
+                color: "#fff",
+                border: "1px solid rgba(255,255,255,0.25)",
+                fontFamily: "'Source Sans 3', sans-serif",
+              }}
+            >
+              View Integration Code
+              <ExternalLink className="w-4 h-4" />
+            </a>
+          </div>
+        </motion.div>
+      </div>
+    </section>
+  );
+}
+
+/* ─── Footer ─── */
+function Footer() {
+  return (
+    <footer className="py-8 border-t" style={{ borderColor: "#e7e0d5", background: "#FFFBEB" }}>
+      <div className="container">
+        <div className="flex flex-col sm:flex-row items-center justify-between gap-4">
+          <div className="flex items-center gap-2">
+            <div className="w-7 h-7 rounded-md flex items-center justify-center" style={{ background: "#14532D" }}>
+              <Bot className="w-4 h-4 text-white" />
+            </div>
+            <span className="text-sm font-semibold" style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}>
+              Homelegance Chatbot Demo
+            </span>
+          </div>
+          <p className="text-xs" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Chatbot integration concept for homelegance.com
+          </p>
+        </div>
+      </div>
+    </footer>
+  );
+}
+
+/* ─── Main Page ─── */
+export default function Home() {
+  return (
+    <div className="min-h-screen" style={{ background: "#FFFBEB" }}>
+      <Nav />
+      <HeroSection />
+      <StatsBar />
+      <WorkflowSection />
+      <ArchitectureSection />
+      <IntegrationSection />
+      <BenefitsSection />
+      <CTASection />
+      <Footer />
+      <ChatbotWidgetLive />
+    </div>
+  );
+}

+ 298 - 0
client/src/pages/Login.tsx

@@ -0,0 +1,298 @@
+import { useState } from "react";
+import { useLocation } from "wouter";
+import { trpc } from "@/lib/trpc";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { getLoginUrl } from "@/const";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardFooter,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import {
+  Bot,
+  Eye,
+  EyeOff,
+  Loader2,
+  Mail,
+  Lock,
+  AlertCircle,
+  ArrowLeft,
+} from "lucide-react";
+import { Link } from "wouter";
+
+export default function Login() {
+  const [, navigate] = useLocation();
+  const { isAuthenticated, loading: authLoading } = useAuth();
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [showPassword, setShowPassword] = useState(false);
+  const [error, setError] = useState("");
+
+  const loginMutation = trpc.auth.login.useMutation({
+    onSuccess: () => {
+      // Redirect to dashboard after login
+      const params = new URLSearchParams(window.location.search);
+      const returnTo = params.get("returnTo") || "/dashboard";
+      window.location.href = returnTo;
+    },
+    onError: (err) => {
+      setError(err.message || "Invalid email or password");
+    },
+  });
+
+  // Redirect if already logged in
+  if (!authLoading && isAuthenticated) {
+    window.location.href = "/dashboard";
+    return null;
+  }
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+    if (!email || !password) {
+      setError("Please fill in all fields");
+      return;
+    }
+    loginMutation.mutate({ email, password });
+  };
+
+  const isLoading = loginMutation.isPending;
+
+  return (
+    <div
+      className="min-h-screen flex items-center justify-center px-4 py-12 relative"
+      style={{ background: "linear-gradient(135deg, #FFFBEB 0%, #f5f0e8 50%, #e8e0d4 100%)" }}
+    >
+      {/* Decorative elements */}
+      <div className="absolute top-0 left-0 w-full h-1" style={{ background: "#14532D" }} />
+      <div
+        className="absolute top-20 right-20 w-64 h-64 rounded-full opacity-10"
+        style={{ background: "#14532D", filter: "blur(80px)" }}
+      />
+      <div
+        className="absolute bottom-20 left-20 w-48 h-48 rounded-full opacity-10"
+        style={{ background: "#C2410C", filter: "blur(60px)" }}
+      />
+
+      <div className="w-full max-w-md relative z-10">
+        {/* Back to home */}
+        <Link
+          href="/"
+          className="inline-flex items-center gap-1.5 text-sm mb-6 transition-colors hover:opacity-80"
+          style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          <ArrowLeft className="w-4 h-4" />
+          Back to home
+        </Link>
+
+        <Card
+          className="border-0 shadow-xl"
+          style={{ background: "#fff", boxShadow: "0 20px 60px rgba(120, 113, 108, 0.12)" }}
+        >
+          <CardHeader className="text-center pb-2">
+            {/* Logo */}
+            <div className="flex justify-center mb-4">
+              <div
+                className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                style={{
+                  background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                  boxShadow: "0 4px 16px rgba(20, 83, 45, 0.3)",
+                }}
+              >
+                <Bot className="w-7 h-7 text-white" />
+              </div>
+            </div>
+            <CardTitle
+              className="text-2xl"
+              style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+            >
+              Welcome Back
+            </CardTitle>
+            <CardDescription
+              className="text-sm mt-1"
+              style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+            >
+              Sign in to access the Homelegance Dashboard
+            </CardDescription>
+          </CardHeader>
+
+          <CardContent className="pt-4">
+            <form onSubmit={handleSubmit} className="space-y-4">
+              {error && (
+                <div
+                  className="flex items-center gap-2 p-3 rounded-lg text-sm"
+                  style={{
+                    background: "#FEF2F2",
+                    color: "#DC2626",
+                    border: "1px solid #FECACA",
+                    fontFamily: "'Source Sans 3', sans-serif",
+                  }}
+                >
+                  <AlertCircle className="w-4 h-4 shrink-0" />
+                  {error}
+                </div>
+              )}
+
+              <div className="space-y-2">
+                <Label
+                  htmlFor="email"
+                  className="text-sm font-medium"
+                  style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                >
+                  Email Address
+                </Label>
+                <div className="relative">
+                  <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="email"
+                    type="email"
+                    placeholder="you@company.com"
+                    value={email}
+                    onChange={(e) => setEmail(e.target.value)}
+                    className="pl-10 h-11"
+                    style={{
+                      borderColor: "#e7e0d5",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                    }}
+                    disabled={isLoading}
+                    autoComplete="email"
+                  />
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <div className="flex items-center justify-between">
+                  <Label
+                    htmlFor="password"
+                    className="text-sm font-medium"
+                    style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                  >
+                    Password
+                  </Label>
+                  <Link
+                    href="/forgot-password"
+                    className="text-xs font-medium transition-colors hover:opacity-80"
+                    style={{ color: "#C2410C", fontFamily: "'Source Sans 3', sans-serif" }}
+                  >
+                    Forgot your password?
+                  </Link>
+                </div>
+                <div className="relative">
+                  <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="password"
+                    type={showPassword ? "text" : "password"}
+                    placeholder="Enter your password"
+                    value={password}
+                    onChange={(e) => setPassword(e.target.value)}
+                    className="pl-10 pr-10 h-11"
+                    style={{
+                      borderColor: "#e7e0d5",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                    }}
+                    disabled={isLoading}
+                    autoComplete="current-password"
+                  />
+                  <button
+                    type="button"
+                    onClick={() => setShowPassword(!showPassword)}
+                    className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
+                  >
+                    {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
+                  </button>
+                </div>
+              </div>
+
+              <Button
+                type="submit"
+                className="w-full h-11 text-sm font-semibold"
+                style={{
+                  background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                  fontFamily: "'Source Sans 3', sans-serif",
+                  boxShadow: "0 2px 8px rgba(20, 83, 45, 0.25)",
+                }}
+                disabled={isLoading}
+              >
+                {isLoading ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    Signing in...
+                  </>
+                ) : (
+                  "Sign In"
+                )}
+              </Button>
+            </form>
+
+            {/* Divider */}
+            <div className="relative my-6">
+              <Separator style={{ background: "#e7e0d5" }} />
+              <span
+                className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 px-3 text-xs"
+                style={{
+                  background: "#fff",
+                  color: "#a8a29e",
+                  fontFamily: "'Source Sans 3', sans-serif",
+                }}
+              >
+                or continue with
+              </span>
+            </div>
+
+            {/* OAuth login */}
+            <Button
+              variant="outline"
+              className="w-full h-11 text-sm font-medium"
+              style={{
+                borderColor: "#e7e0d5",
+                color: "#57534e",
+                fontFamily: "'Source Sans 3', sans-serif",
+              }}
+              onClick={() => {
+                window.location.href = getLoginUrl("/dashboard");
+              }}
+              disabled={isLoading}
+            >
+              <svg className="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="#14532D" opacity="0.2"/>
+                <path d="M12 6a3 3 0 100 6 3 3 0 000-6zm0 8c-3.31 0-6 1.34-6 3v1h12v-1c0-1.66-2.69-3-6-3z" fill="#14532D"/>
+              </svg>
+              Sign in with Manus Account
+            </Button>
+          </CardContent>
+
+          <CardFooter className="justify-center pb-6">
+            <p
+              className="text-sm"
+              style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+            >
+              Don't have an account?{" "}
+              <Link
+                href="/register"
+                className="font-semibold transition-colors hover:opacity-80"
+                style={{ color: "#14532D" }}
+              >
+                Create one
+              </Link>
+            </p>
+          </CardFooter>
+        </Card>
+
+        {/* Footer */}
+        <p
+          className="text-center text-xs mt-6"
+          style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          Homelegance Chatbot Administration Portal
+        </p>
+      </div>
+    </div>
+  );
+}

+ 52 - 0
client/src/pages/NotFound.tsx

@@ -0,0 +1,52 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { AlertCircle, Home } from "lucide-react";
+import { useLocation } from "wouter";
+
+export default function NotFound() {
+  const [, setLocation] = useLocation();
+
+  const handleGoHome = () => {
+    setLocation("/");
+  };
+
+  return (
+    <div className="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
+      <Card className="w-full max-w-lg mx-4 shadow-lg border-0 bg-white/80 backdrop-blur-sm">
+        <CardContent className="pt-8 pb-8 text-center">
+          <div className="flex justify-center mb-6">
+            <div className="relative">
+              <div className="absolute inset-0 bg-red-100 rounded-full animate-pulse" />
+              <AlertCircle className="relative h-16 w-16 text-red-500" />
+            </div>
+          </div>
+
+          <h1 className="text-4xl font-bold text-slate-900 mb-2">404</h1>
+
+          <h2 className="text-xl font-semibold text-slate-700 mb-4">
+            Page Not Found
+          </h2>
+
+          <p className="text-slate-600 mb-8 leading-relaxed">
+            Sorry, the page you are looking for doesn't exist.
+            <br />
+            It may have been moved or deleted.
+          </p>
+
+          <div
+            id="not-found-button-group"
+            className="flex flex-col sm:flex-row gap-3 justify-center"
+          >
+            <Button
+              onClick={handleGoHome}
+              className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg"
+            >
+              <Home className="w-4 h-4 mr-2" />
+              Go Home
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 445 - 0
client/src/pages/Playground.tsx

@@ -0,0 +1,445 @@
+/**
+ * Playground — Test the chatbot in a simulated customer view
+ * Features: Interactive chatbot preview, flow selection, execution path tracking, reset
+ */
+import { useState, useRef, useEffect, useMemo } from "react";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { toast } from "sonner";
+import {
+  Play, RotateCcw, Sparkles, User, Headphones,
+  Send, Bot, Loader2, Package, Truck, RotateCw,
+  XCircle, ChevronRight, Eye, MessageCircle,
+} from "lucide-react";
+import { Streamdown } from "streamdown";
+
+/* ─── Quick-reply options matching the greeting ─── */
+const GREETING_OPTIONS = [
+  { id: "orders", label: "Orders", icon: <Package className="w-3.5 h-3.5" />, color: "#14532D" },
+  { id: "shipping", label: "Shipping", icon: <Truck className="w-3.5 h-3.5" />, color: "#0369a1" },
+  { id: "returning", label: "Returning", icon: <RotateCw className="w-3.5 h-3.5" />, color: "#ca8a04" },
+  { id: "cancelling", label: "Cancelling", icon: <XCircle className="w-3.5 h-3.5" />, color: "#dc2626" },
+];
+
+interface TestMessage {
+  id: number;
+  sender: "visitor" | "bot" | "agent" | "system";
+  content: string;
+  timestamp: string;
+  flowStep?: string;
+}
+
+export default function Playground() {
+  const [sessionId, setSessionId] = useState<string | null>(null);
+  const [messages, setMessages] = useState<TestMessage[]>([]);
+  const [inputText, setInputText] = useState("");
+  const [isTyping, setIsTyping] = useState(false);
+  const [showGreeting, setShowGreeting] = useState(true);
+  const [selectedFlow, setSelectedFlow] = useState<string | null>(null);
+  const [flowPath, setFlowPath] = useState<string[]>([]);
+  const [testMode, setTestMode] = useState<"interactive" | "flow">("interactive");
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  const startSession = trpc.chat.startSession.useMutation({
+    onSuccess: (data) => {
+      setSessionId(data.sessionId);
+    },
+  });
+
+  const { data: serverMessages } = trpc.chat.getMessages.useQuery(
+    { sessionId: sessionId || "" },
+    { enabled: !!sessionId, refetchInterval: 2000 }
+  );
+
+  const sendMessage = trpc.chat.sendMessage.useMutation({
+    onSuccess: () => setIsTyping(false),
+    onError: () => setIsTyping(false),
+  });
+
+  const trackEvent = trpc.analytics.track.useMutation();
+
+  // Sync server messages
+  useEffect(() => {
+    if (serverMessages?.messages) {
+      const mapped: TestMessage[] = serverMessages.messages.map((m: any) => ({
+        id: m.id,
+        sender: m.sender,
+        content: m.content,
+        timestamp: m.createdAt?.toString() || new Date().toISOString(),
+      }));
+      setMessages(mapped);
+      if (mapped.length > 0) setShowGreeting(false);
+    }
+  }, [serverMessages]);
+
+  // Auto-scroll
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [messages, isTyping]);
+
+  const handleStartTest = () => {
+    setMessages([]);
+    setShowGreeting(true);
+    setSelectedFlow(null);
+    setFlowPath([]);
+    setSessionId(null);
+    startSession.mutate({});
+  };
+
+  const handleReset = () => {
+    setMessages([]);
+    setShowGreeting(true);
+    setSelectedFlow(null);
+    setFlowPath([]);
+    setSessionId(null);
+    setInputText("");
+    setIsTyping(false);
+  };
+
+  const handleOptionClick = (optionId: string) => {
+    if (!sessionId) return;
+    setShowGreeting(false);
+    setSelectedFlow(optionId);
+    setFlowPath(prev => [...prev, `Greeting → ${optionId}`]);
+
+    trackEvent.mutate({
+      sessionId,
+      eventType: "button_clicked",
+      category: optionId,
+      metadata: { source: "playground", button: optionId },
+    });
+
+    // Send the option as a message
+    const text = `I need help with ${optionId}`;
+    setIsTyping(true);
+    setMessages(prev => [...prev, {
+      id: Date.now(),
+      sender: "visitor",
+      content: text,
+      timestamp: new Date().toISOString(),
+      flowStep: `User selected: ${optionId}`,
+    }]);
+    sendMessage.mutate({ sessionId, content: text });
+  };
+
+  const handleSend = () => {
+    if (!inputText.trim() || !sessionId || isTyping) return;
+    const text = inputText.trim();
+    setInputText("");
+    setIsTyping(true);
+    setShowGreeting(false);
+
+    setMessages(prev => [...prev, {
+      id: Date.now(),
+      sender: "visitor",
+      content: text,
+      timestamp: new Date().toISOString(),
+    }]);
+
+    sendMessage.mutate({ sessionId, content: text });
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter" && !e.shiftKey) {
+      e.preventDefault();
+      handleSend();
+    }
+  };
+
+  return (
+    <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
+      {/* Top toolbar */}
+      <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <div className="flex items-center gap-3">
+          <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#7c3aed" }}>
+            <Play className="w-3.5 h-3.5 text-white" />
+          </div>
+          <span className="text-sm font-bold" style={{ color: "#7c3aed", fontFamily: "'Playfair Display', serif" }}>Playground</span>
+          {sessionId && (
+            <span className="text-[10px] px-2 py-0.5 rounded-full font-medium" style={{ background: "#14532D14", color: "#14532D" }}>
+              Session active
+            </span>
+          )}
+        </div>
+        <div className="flex items-center gap-2">
+          <Button onClick={handleReset} variant="outline" size="sm" className="text-xs">
+            <RotateCcw className="w-3 h-3 mr-1" /> Reset
+          </Button>
+          <Button onClick={handleStartTest} size="sm" className="text-xs text-white" style={{ background: "#7c3aed" }}>
+            <Play className="w-3 h-3 mr-1" /> {sessionId ? "Restart" : "Start Test"}
+          </Button>
+        </div>
+      </div>
+
+      <div className="flex-1 flex min-h-0">
+        {/* Left panel — Flow execution path */}
+        <div className="w-64 shrink-0 border-r overflow-y-auto p-3 space-y-3" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+          <h3 className="text-xs font-bold uppercase tracking-wider" style={{ color: "#78716C" }}>
+            Test Controls
+          </h3>
+
+          {/* Mode selector */}
+          <div className="space-y-1">
+            <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Mode</span>
+            <div className="flex gap-1">
+              {(["interactive", "flow"] as const).map(mode => (
+                <button
+                  key={mode}
+                  onClick={() => setTestMode(mode)}
+                  className="flex-1 px-2 py-1.5 rounded-lg text-[11px] font-medium capitalize transition-colors"
+                  style={{
+                    background: testMode === mode ? "#7c3aed14" : "transparent",
+                    color: testMode === mode ? "#7c3aed" : "#a8a29e",
+                    border: testMode === mode ? "1px solid #7c3aed30" : "1px solid transparent",
+                  }}
+                >
+                  {mode}
+                </button>
+              ))}
+            </div>
+          </div>
+
+          {/* Quick flow test */}
+          {testMode === "flow" && (
+            <div className="space-y-1.5">
+              <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Test Flow</span>
+              {GREETING_OPTIONS.map(opt => (
+                <button
+                  key={opt.id}
+                  onClick={() => {
+                    if (sessionId) handleOptionClick(opt.id);
+                    else toast.info("Start a test session first");
+                  }}
+                  className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-left hover:bg-black/5 transition-colors"
+                >
+                  <div className="w-6 h-6 rounded-md flex items-center justify-center shrink-0" style={{ background: `${opt.color}14`, color: opt.color }}>
+                    {opt.icon}
+                  </div>
+                  <span className="text-[11px] font-medium" style={{ color: "#292524" }}>{opt.label}</span>
+                </button>
+              ))}
+            </div>
+          )}
+
+          {/* Execution path */}
+          <div className="space-y-1.5">
+            <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Execution Path</span>
+            {flowPath.length === 0 ? (
+              <p className="text-[11px] italic" style={{ color: "#d6d3d1" }}>No flow steps recorded yet</p>
+            ) : (
+              <div className="space-y-1">
+                {flowPath.map((step, i) => (
+                  <div key={i} className="flex items-center gap-1.5">
+                    <div className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold" style={{ background: "#7c3aed14", color: "#7c3aed" }}>
+                      {i + 1}
+                    </div>
+                    <span className="text-[10px]" style={{ color: "#78716C" }}>{step}</span>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+
+          {/* Session info */}
+          {sessionId && (
+            <div className="p-2 rounded-lg" style={{ background: "#f5f0e8" }}>
+              <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Session</span>
+              <p className="text-[10px] font-mono mt-0.5 truncate" style={{ color: "#78716C" }}>{sessionId}</p>
+              <p className="text-[10px] mt-1" style={{ color: "#a8a29e" }}>
+                {messages.length} messages · {selectedFlow ? `Flow: ${selectedFlow}` : "No flow selected"}
+              </p>
+            </div>
+          )}
+        </div>
+
+        {/* Center — Chat preview (simulated customer view) */}
+        <div className="flex-1 flex items-center justify-center p-6" style={{ background: "#f5f0e8" }}>
+          <div
+            className="w-[400px] max-w-full rounded-2xl shadow-2xl overflow-hidden flex flex-col"
+            style={{
+              height: "580px",
+              background: "#FFFBEB",
+              border: "1px solid #e7e0d5",
+              boxShadow: "0 20px 60px rgba(0,0,0,0.12)",
+            }}
+          >
+            {/* Chat header */}
+            <div className="px-4 py-3 flex items-center justify-between" style={{ background: "linear-gradient(135deg, #14532D 0%, #166534 100%)" }}>
+              <div className="flex items-center gap-2.5">
+                <div className="w-9 h-9 rounded-full flex items-center justify-center" style={{ background: "rgba(255,255,255,0.15)" }}>
+                  <Sparkles className="w-4.5 h-4.5 text-white" />
+                </div>
+                <div>
+                  <div className="text-sm font-semibold text-white" style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+                    Ellie
+                  </div>
+                  <div className="flex items-center gap-1">
+                    <span className="w-1.5 h-1.5 rounded-full bg-green-400" />
+                    <span className="text-[10px] text-green-200">Always happy to help</span>
+                  </div>
+                </div>
+              </div>
+              <div className="flex items-center gap-1">
+                <span className="text-[9px] px-2 py-0.5 rounded-full font-medium" style={{ background: "rgba(255,255,255,0.15)", color: "#fff" }}>
+                  TEST MODE
+                </span>
+              </div>
+            </div>
+
+            {/* Messages area */}
+            <div className="flex-1 overflow-y-auto p-3 space-y-3">
+              {!sessionId ? (
+                <div className="flex flex-col items-center justify-center h-full text-center">
+                  <div className="w-14 h-14 rounded-full flex items-center justify-center mb-3" style={{ background: "#14532D14" }}>
+                    <MessageCircle className="w-7 h-7" style={{ color: "#14532D" }} />
+                  </div>
+                  <p className="text-sm font-medium" style={{ color: "#292524" }}>Ready to test</p>
+                  <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Click "Start Test" to begin a chatbot session</p>
+                </div>
+              ) : (
+                <>
+                  {/* Greeting with options */}
+                  {showGreeting && messages.length === 0 && (
+                    <div className="space-y-3">
+                      <div className="flex gap-2">
+                        <div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1" style={{ background: "#14532D", color: "#fff" }}>
+                          <Sparkles className="w-3 h-3" />
+                        </div>
+                        <div className="px-3 py-2 rounded-xl text-[13px] leading-relaxed" style={{ background: "#f5f0e8", color: "#292524", borderBottomLeftRadius: "4px" }}>
+                          <span role="img" aria-label="wave">👋</span> What can we help you with today?
+                        </div>
+                      </div>
+                      <div className="flex flex-wrap gap-2 justify-center px-4">
+                        {GREETING_OPTIONS.map(opt => (
+                          <button
+                            key={opt.id}
+                            onClick={() => handleOptionClick(opt.id)}
+                            className="px-4 py-2 rounded-full text-[12px] font-medium transition-all hover:shadow-md"
+                            style={{
+                              background: "#fff",
+                              color: "#0369a1",
+                              border: "1.5px solid #0369a130",
+                            }}
+                          >
+                            {opt.label}
+                          </button>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+
+                  {/* Message list */}
+                  {messages.map((msg) => {
+                    const isVisitor = msg.sender === "visitor";
+                    const isBot = msg.sender === "bot";
+                    return (
+                      <div key={msg.id} className={`flex gap-2 ${isVisitor ? "justify-end" : "justify-start"}`}>
+                        {!isVisitor && (
+                          <div
+                            className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1"
+                            style={{ background: isBot ? "#14532D" : "#C2410C", color: "#fff" }}
+                          >
+                            {isBot ? <Sparkles className="w-3 h-3" /> : <Headphones className="w-3 h-3" />}
+                          </div>
+                        )}
+                        <div
+                          className="max-w-[80%] px-3 py-2 rounded-xl text-[13px] leading-relaxed"
+                          style={{
+                            background: isVisitor ? "#14532D" : "#f5f0e8",
+                            color: isVisitor ? "#fff" : "#292524",
+                            fontFamily: "'Source Sans 3', sans-serif",
+                            borderBottomRightRadius: isVisitor ? "4px" : undefined,
+                            borderBottomLeftRadius: !isVisitor ? "4px" : undefined,
+                          }}
+                        >
+                          {isBot || msg.sender === "agent" ? (
+                            <div className="prose prose-sm max-w-none" style={{ color: "inherit" }}>
+                              <Streamdown>{msg.content}</Streamdown>
+                            </div>
+                          ) : (
+                            msg.content
+                          )}
+                        </div>
+                        {isVisitor && (
+                          <div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-1" style={{ background: "#78716C20", color: "#78716C" }}>
+                            <User className="w-3 h-3" />
+                          </div>
+                        )}
+                      </div>
+                    );
+                  })}
+
+                  {/* Typing indicator */}
+                  {isTyping && (
+                    <div className="flex gap-2 items-start">
+                      <div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0" style={{ background: "#14532D", color: "#fff" }}>
+                        <Sparkles className="w-3 h-3" />
+                      </div>
+                      <div className="px-3 py-2 rounded-xl" style={{ background: "#f5f0e8" }}>
+                        <div className="flex gap-1">
+                          <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "0ms" }} />
+                          <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "150ms" }} />
+                          <span className="w-1.5 h-1.5 rounded-full animate-bounce" style={{ background: "#a8a29e", animationDelay: "300ms" }} />
+                        </div>
+                      </div>
+                    </div>
+                  )}
+
+                  {/* Greeting options after messages if still in greeting state */}
+                  {showGreeting && messages.length > 0 && (
+                    <div className="flex flex-wrap gap-2 justify-center px-4">
+                      {GREETING_OPTIONS.map(opt => (
+                        <button
+                          key={opt.id}
+                          onClick={() => handleOptionClick(opt.id)}
+                          className="px-4 py-2 rounded-full text-[12px] font-medium transition-all hover:shadow-md"
+                          style={{ background: "#fff", color: "#0369a1", border: "1.5px solid #0369a130" }}
+                        >
+                          {opt.label}
+                        </button>
+                      ))}
+                    </div>
+                  )}
+                </>
+              )}
+              <div ref={messagesEndRef} />
+            </div>
+
+            {/* Input area */}
+            <div className="p-3 border-t" style={{ borderColor: "#e7e0d5" }}>
+              {showGreeting && sessionId && messages.length === 0 ? (
+                <p className="text-center text-[11px] py-1" style={{ color: "#a8a29e" }}>
+                  Hit the buttons to respond
+                </p>
+              ) : (
+                <div className="flex gap-2">
+                  <input
+                    type="text"
+                    value={inputText}
+                    onChange={(e) => setInputText(e.target.value)}
+                    onKeyDown={handleKeyDown}
+                    placeholder="Type your message..."
+                    className="flex-1 px-3 py-2 rounded-xl text-sm border"
+                    style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif", background: "#fff" }}
+                    disabled={isTyping || !sessionId}
+                  />
+                  <button
+                    onClick={handleSend}
+                    disabled={!inputText.trim() || isTyping || !sessionId}
+                    className="w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-40"
+                    style={{ background: "#0369a1", color: "#fff" }}
+                  >
+                    <Send className="w-4 h-4" />
+                  </button>
+                </div>
+              )}
+              <div className="text-center mt-2">
+                <span className="text-[10px]" style={{ color: "#d6d3d1" }}>Powered by Ellie · Homelegance AI</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 323 - 0
client/src/pages/Register.tsx

@@ -0,0 +1,323 @@
+import { useState } from "react";
+import { useLocation } from "wouter";
+import { trpc } from "@/lib/trpc";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardFooter,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import {
+  Bot,
+  Eye,
+  EyeOff,
+  Loader2,
+  Mail,
+  Lock,
+  User,
+  AlertCircle,
+  ArrowLeft,
+  CheckCircle2,
+} from "lucide-react";
+import { Link } from "wouter";
+
+export default function Register() {
+  const [, navigate] = useLocation();
+  const { isAuthenticated, loading: authLoading } = useAuth();
+  const [name, setName] = useState("");
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+  const [showPassword, setShowPassword] = useState(false);
+  const [error, setError] = useState("");
+
+  const registerMutation = trpc.auth.register.useMutation({
+    onSuccess: () => {
+      window.location.href = "/dashboard";
+    },
+    onError: (err) => {
+      setError(err.message || "Registration failed");
+    },
+  });
+
+  // Redirect if already logged in
+  if (!authLoading && isAuthenticated) {
+    window.location.href = "/dashboard";
+    return null;
+  }
+
+  const passwordChecks = [
+    { label: "At least 8 characters", met: password.length >= 8 },
+    { label: "Contains a number", met: /\d/.test(password) },
+    { label: "Contains uppercase letter", met: /[A-Z]/.test(password) },
+  ];
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+
+    if (!name || !email || !password || !confirmPassword) {
+      setError("Please fill in all fields");
+      return;
+    }
+    if (password !== confirmPassword) {
+      setError("Passwords do not match");
+      return;
+    }
+    if (password.length < 8) {
+      setError("Password must be at least 8 characters");
+      return;
+    }
+
+    registerMutation.mutate({ email, password, name });
+  };
+
+  const isLoading = registerMutation.isPending;
+
+  return (
+    <div
+      className="min-h-screen flex items-center justify-center px-4 py-12 relative"
+      style={{ background: "linear-gradient(135deg, #FFFBEB 0%, #f5f0e8 50%, #e8e0d4 100%)" }}
+    >
+      {/* Decorative elements */}
+      <div className="absolute top-0 left-0 w-full h-1" style={{ background: "#14532D" }} />
+      <div
+        className="absolute top-20 left-20 w-64 h-64 rounded-full opacity-10"
+        style={{ background: "#C2410C", filter: "blur(80px)" }}
+      />
+      <div
+        className="absolute bottom-20 right-20 w-48 h-48 rounded-full opacity-10"
+        style={{ background: "#14532D", filter: "blur(60px)" }}
+      />
+
+      <div className="w-full max-w-md relative z-10">
+        {/* Back to home */}
+        <Link
+          href="/"
+          className="inline-flex items-center gap-1.5 text-sm mb-6 transition-colors hover:opacity-80"
+          style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          <ArrowLeft className="w-4 h-4" />
+          Back to home
+        </Link>
+
+        <Card
+          className="border-0 shadow-xl"
+          style={{ background: "#fff", boxShadow: "0 20px 60px rgba(120, 113, 108, 0.12)" }}
+        >
+          <CardHeader className="text-center pb-2">
+            <div className="flex justify-center mb-4">
+              <div
+                className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                style={{
+                  background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                  boxShadow: "0 4px 16px rgba(20, 83, 45, 0.3)",
+                }}
+              >
+                <Bot className="w-7 h-7 text-white" />
+              </div>
+            </div>
+            <CardTitle
+              className="text-2xl"
+              style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+            >
+              Create Account
+            </CardTitle>
+            <CardDescription
+              className="text-sm mt-1"
+              style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+            >
+              Join the Homelegance team to manage the chatbot
+            </CardDescription>
+          </CardHeader>
+
+          <CardContent className="pt-4">
+            <form onSubmit={handleSubmit} className="space-y-4">
+              {error && (
+                <div
+                  className="flex items-center gap-2 p-3 rounded-lg text-sm"
+                  style={{
+                    background: "#FEF2F2",
+                    color: "#DC2626",
+                    border: "1px solid #FECACA",
+                    fontFamily: "'Source Sans 3', sans-serif",
+                  }}
+                >
+                  <AlertCircle className="w-4 h-4 shrink-0" />
+                  {error}
+                </div>
+              )}
+
+              <div className="space-y-2">
+                <Label
+                  htmlFor="name"
+                  className="text-sm font-medium"
+                  style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                >
+                  Full Name
+                </Label>
+                <div className="relative">
+                  <User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="name"
+                    type="text"
+                    placeholder="John Smith"
+                    value={name}
+                    onChange={(e) => setName(e.target.value)}
+                    className="pl-10 h-11"
+                    style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                    disabled={isLoading}
+                    autoComplete="name"
+                  />
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <Label
+                  htmlFor="email"
+                  className="text-sm font-medium"
+                  style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                >
+                  Email Address
+                </Label>
+                <div className="relative">
+                  <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="email"
+                    type="email"
+                    placeholder="you@company.com"
+                    value={email}
+                    onChange={(e) => setEmail(e.target.value)}
+                    className="pl-10 h-11"
+                    style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                    disabled={isLoading}
+                    autoComplete="email"
+                  />
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <Label
+                  htmlFor="password"
+                  className="text-sm font-medium"
+                  style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                >
+                  Password
+                </Label>
+                <div className="relative">
+                  <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="password"
+                    type={showPassword ? "text" : "password"}
+                    placeholder="Create a strong password"
+                    value={password}
+                    onChange={(e) => setPassword(e.target.value)}
+                    className="pl-10 pr-10 h-11"
+                    style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                    disabled={isLoading}
+                    autoComplete="new-password"
+                  />
+                  <button
+                    type="button"
+                    onClick={() => setShowPassword(!showPassword)}
+                    className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
+                  >
+                    {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
+                  </button>
+                </div>
+
+                {/* Password strength indicators */}
+                {password.length > 0 && (
+                  <div className="space-y-1 mt-2">
+                    {passwordChecks.map((check) => (
+                      <div
+                        key={check.label}
+                        className="flex items-center gap-2 text-xs"
+                        style={{
+                          color: check.met ? "#16a34a" : "#a8a29e",
+                          fontFamily: "'Source Sans 3', sans-serif",
+                        }}
+                      >
+                        <CheckCircle2
+                          className="w-3.5 h-3.5"
+                          style={{ opacity: check.met ? 1 : 0.4 }}
+                        />
+                        {check.label}
+                      </div>
+                    ))}
+                  </div>
+                )}
+              </div>
+
+              <div className="space-y-2">
+                <Label
+                  htmlFor="confirmPassword"
+                  className="text-sm font-medium"
+                  style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                >
+                  Confirm Password
+                </Label>
+                <div className="relative">
+                  <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                  <Input
+                    id="confirmPassword"
+                    type="password"
+                    placeholder="Confirm your password"
+                    value={confirmPassword}
+                    onChange={(e) => setConfirmPassword(e.target.value)}
+                    className="pl-10 h-11"
+                    style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                    disabled={isLoading}
+                    autoComplete="new-password"
+                  />
+                </div>
+              </div>
+
+              <Button
+                type="submit"
+                className="w-full h-11 text-sm font-semibold"
+                style={{
+                  background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                  fontFamily: "'Source Sans 3', sans-serif",
+                  boxShadow: "0 2px 8px rgba(20, 83, 45, 0.25)",
+                }}
+                disabled={isLoading}
+              >
+                {isLoading ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    Creating account...
+                  </>
+                ) : (
+                  "Create Account"
+                )}
+              </Button>
+            </form>
+          </CardContent>
+
+          <CardFooter className="justify-center pb-6">
+            <p
+              className="text-sm"
+              style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+            >
+              Already have an account?{" "}
+              <Link
+                href="/login"
+                className="font-semibold transition-colors hover:opacity-80"
+                style={{ color: "#14532D" }}
+              >
+                Sign in
+              </Link>
+            </p>
+          </CardFooter>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 362 - 0
client/src/pages/ResetPassword.tsx

@@ -0,0 +1,362 @@
+import { useState } from "react";
+import { useParams } from "wouter";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import {
+  Loader2,
+  Lock,
+  Eye,
+  EyeOff,
+  AlertCircle,
+  ArrowLeft,
+  CheckCircle2,
+  ShieldAlert,
+} from "lucide-react";
+import { Link } from "wouter";
+
+export default function ResetPassword() {
+  const params = useParams<{ token: string }>();
+  const token = params.token || "";
+
+  const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+  const [showPassword, setShowPassword] = useState(false);
+  const [error, setError] = useState("");
+  const [success, setSuccess] = useState(false);
+
+  // Validate the token
+  const validateQuery = trpc.auth.validateResetToken.useQuery(
+    { token },
+    { enabled: !!token, retry: false }
+  );
+
+  const resetMutation = trpc.auth.resetPassword.useMutation({
+    onSuccess: () => {
+      setSuccess(true);
+    },
+    onError: (err) => {
+      setError(err.message || "Failed to reset password");
+    },
+  });
+
+  const passwordChecks = [
+    { label: "At least 8 characters", met: password.length >= 8 },
+    { label: "Contains a number", met: /\d/.test(password) },
+    { label: "Contains uppercase letter", met: /[A-Z]/.test(password) },
+  ];
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+
+    if (!password || !confirmPassword) {
+      setError("Please fill in all fields");
+      return;
+    }
+    if (password !== confirmPassword) {
+      setError("Passwords do not match");
+      return;
+    }
+    if (password.length < 8) {
+      setError("Password must be at least 8 characters");
+      return;
+    }
+
+    resetMutation.mutate({ token, newPassword: password });
+  };
+
+  const isLoading = resetMutation.isPending;
+  const isValidating = validateQuery.isLoading;
+  const isValid = validateQuery.data?.valid;
+  const validationReason = validateQuery.data && !validateQuery.data.valid
+    ? validateQuery.data.reason
+    : null;
+
+  return (
+    <div
+      className="min-h-screen flex items-center justify-center px-4 py-12 relative"
+      style={{ background: "linear-gradient(135deg, #FFFBEB 0%, #f5f0e8 50%, #e8e0d4 100%)" }}
+    >
+      <div className="absolute top-0 left-0 w-full h-1" style={{ background: "#14532D" }} />
+
+      <div className="w-full max-w-md relative z-10">
+        <Link
+          href="/login"
+          className="inline-flex items-center gap-1.5 text-sm mb-6 transition-colors hover:opacity-80"
+          style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          <ArrowLeft className="w-4 h-4" />
+          Back to sign in
+        </Link>
+
+        <Card
+          className="border-0 shadow-xl"
+          style={{ background: "#fff", boxShadow: "0 20px 60px rgba(120, 113, 108, 0.12)" }}
+        >
+          {/* Loading state */}
+          {isValidating && (
+            <>
+              <CardHeader className="text-center pb-2">
+                <div className="flex justify-center mb-4">
+                  <Loader2 className="w-10 h-10 animate-spin" style={{ color: "#14532D" }} />
+                </div>
+                <CardTitle
+                  className="text-xl"
+                  style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+                >
+                  Validating Reset Link...
+                </CardTitle>
+              </CardHeader>
+            </>
+          )}
+
+          {/* Invalid token */}
+          {!isValidating && !isValid && !success && (
+            <>
+              <CardHeader className="text-center pb-2">
+                <div className="flex justify-center mb-4">
+                  <div
+                    className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                    style={{
+                      background: "linear-gradient(135deg, #DC2626 0%, #EF4444 100%)",
+                      boxShadow: "0 4px 16px rgba(220, 38, 38, 0.3)",
+                    }}
+                  >
+                    <ShieldAlert className="w-7 h-7 text-white" />
+                  </div>
+                </div>
+                <CardTitle
+                  className="text-2xl"
+                  style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+                >
+                  Invalid Reset Link
+                </CardTitle>
+                <CardDescription
+                  className="text-sm mt-1"
+                  style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+                >
+                  {validationReason || "This password reset link is not valid"}
+                </CardDescription>
+              </CardHeader>
+              <CardContent className="pt-4 text-center">
+                <Link href="/forgot-password">
+                  <Button
+                    className="text-sm font-semibold"
+                    style={{
+                      background: "linear-gradient(135deg, #C2410C 0%, #ea580c 100%)",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                    }}
+                  >
+                    Request a New Reset Link
+                  </Button>
+                </Link>
+              </CardContent>
+            </>
+          )}
+
+          {/* Success state */}
+          {success && (
+            <>
+              <CardHeader className="text-center pb-2">
+                <div className="flex justify-center mb-4">
+                  <div
+                    className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                    style={{
+                      background: "linear-gradient(135deg, #16a34a 0%, #15803d 100%)",
+                      boxShadow: "0 4px 16px rgba(22, 163, 74, 0.3)",
+                    }}
+                  >
+                    <CheckCircle2 className="w-7 h-7 text-white" />
+                  </div>
+                </div>
+                <CardTitle
+                  className="text-2xl"
+                  style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+                >
+                  Password Reset!
+                </CardTitle>
+                <CardDescription
+                  className="text-sm mt-1"
+                  style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+                >
+                  Your password has been successfully updated
+                </CardDescription>
+              </CardHeader>
+              <CardContent className="pt-4 text-center">
+                <Link href="/login">
+                  <Button
+                    className="text-sm font-semibold"
+                    style={{
+                      background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                      boxShadow: "0 2px 8px rgba(20, 83, 45, 0.25)",
+                    }}
+                  >
+                    Sign In with New Password
+                  </Button>
+                </Link>
+              </CardContent>
+            </>
+          )}
+
+          {/* Reset form */}
+          {!isValidating && isValid && !success && (
+            <>
+              <CardHeader className="text-center pb-2">
+                <div className="flex justify-center mb-4">
+                  <div
+                    className="w-14 h-14 rounded-2xl flex items-center justify-center"
+                    style={{
+                      background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                      boxShadow: "0 4px 16px rgba(20, 83, 45, 0.3)",
+                    }}
+                  >
+                    <Lock className="w-7 h-7 text-white" />
+                  </div>
+                </div>
+                <CardTitle
+                  className="text-2xl"
+                  style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}
+                >
+                  Set New Password
+                </CardTitle>
+                <CardDescription
+                  className="text-sm mt-1"
+                  style={{ fontFamily: "'Source Sans 3', sans-serif", color: "#78716C" }}
+                >
+                  {validateQuery.data?.valid && "email" in validateQuery.data
+                    ? `Enter a new password for ${validateQuery.data.email}`
+                    : "Choose a strong password for your account"}
+                </CardDescription>
+              </CardHeader>
+
+              <CardContent className="pt-4">
+                <form onSubmit={handleSubmit} className="space-y-4">
+                  {error && (
+                    <div
+                      className="flex items-center gap-2 p-3 rounded-lg text-sm"
+                      style={{
+                        background: "#FEF2F2",
+                        color: "#DC2626",
+                        border: "1px solid #FECACA",
+                        fontFamily: "'Source Sans 3', sans-serif",
+                      }}
+                    >
+                      <AlertCircle className="w-4 h-4 shrink-0" />
+                      {error}
+                    </div>
+                  )}
+
+                  <div className="space-y-2">
+                    <Label
+                      htmlFor="password"
+                      className="text-sm font-medium"
+                      style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                    >
+                      New Password
+                    </Label>
+                    <div className="relative">
+                      <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                      <Input
+                        id="password"
+                        type={showPassword ? "text" : "password"}
+                        placeholder="Enter new password"
+                        value={password}
+                        onChange={(e) => setPassword(e.target.value)}
+                        className="pl-10 pr-10 h-11"
+                        style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                        disabled={isLoading}
+                        autoComplete="new-password"
+                      />
+                      <button
+                        type="button"
+                        onClick={() => setShowPassword(!showPassword)}
+                        className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
+                      >
+                        {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
+                      </button>
+                    </div>
+
+                    {password.length > 0 && (
+                      <div className="space-y-1 mt-2">
+                        {passwordChecks.map((check) => (
+                          <div
+                            key={check.label}
+                            className="flex items-center gap-2 text-xs"
+                            style={{
+                              color: check.met ? "#16a34a" : "#a8a29e",
+                              fontFamily: "'Source Sans 3', sans-serif",
+                            }}
+                          >
+                            <CheckCircle2
+                              className="w-3.5 h-3.5"
+                              style={{ opacity: check.met ? 1 : 0.4 }}
+                            />
+                            {check.label}
+                          </div>
+                        ))}
+                      </div>
+                    )}
+                  </div>
+
+                  <div className="space-y-2">
+                    <Label
+                      htmlFor="confirmPassword"
+                      className="text-sm font-medium"
+                      style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}
+                    >
+                      Confirm New Password
+                    </Label>
+                    <div className="relative">
+                      <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
+                      <Input
+                        id="confirmPassword"
+                        type="password"
+                        placeholder="Confirm new password"
+                        value={confirmPassword}
+                        onChange={(e) => setConfirmPassword(e.target.value)}
+                        className="pl-10 h-11"
+                        style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                        disabled={isLoading}
+                        autoComplete="new-password"
+                      />
+                    </div>
+                  </div>
+
+                  <Button
+                    type="submit"
+                    className="w-full h-11 text-sm font-semibold"
+                    style={{
+                      background: "linear-gradient(135deg, #14532D 0%, #166534 100%)",
+                      fontFamily: "'Source Sans 3', sans-serif",
+                      boxShadow: "0 2px 8px rgba(20, 83, 45, 0.25)",
+                    }}
+                    disabled={isLoading}
+                  >
+                    {isLoading ? (
+                      <>
+                        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                        Resetting password...
+                      </>
+                    ) : (
+                      "Reset Password"
+                    )}
+                  </Button>
+                </form>
+              </CardContent>
+            </>
+          )}
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 978 - 0
client/src/pages/UserManagement.tsx

@@ -0,0 +1,978 @@
+/**
+ * User Management — Admin module for managing users, invitations, and activity
+ * Design: Warm Showroom palette, tabbed layout
+ */
+import { useState, useMemo, useCallback } from "react";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { toast } from "sonner";
+import {
+  Users, Shield, Headphones, User, Search, Trash2,
+  CheckCircle2, AlertTriangle, Crown, Mail, Send,
+  Clock, XCircle, RefreshCw, Download, History,
+  MoreHorizontal, UserPlus, CheckSquare, Square,
+  ArrowUpDown, Eye, Copy, ExternalLink,
+} from "lucide-react";
+
+/* ─── Role config ─── */
+const ROLE_CONFIG: Record<string, {
+  icon: React.ReactNode;
+  color: string;
+  bg: string;
+  label: string;
+  description: string;
+}> = {
+  admin: {
+    icon: <Crown className="w-3.5 h-3.5" />,
+    color: "#14532D",
+    bg: "#14532D18",
+    label: "Admin",
+    description: "Full access: conversations, workflow designer, user management",
+  },
+  agent: {
+    icon: <Headphones className="w-3.5 h-3.5" />,
+    color: "#0369a1",
+    bg: "#0369a118",
+    label: "Agent",
+    description: "Can view and reply to conversations in the agent dashboard",
+  },
+  user: {
+    icon: <User className="w-3.5 h-3.5" />,
+    color: "#78716C",
+    bg: "#78716C18",
+    label: "User",
+    description: "Basic access only, no dashboard or workflow access",
+  },
+};
+
+const INVITE_STATUS_CONFIG: Record<string, { color: string; bg: string; icon: React.ReactNode; label: string }> = {
+  pending: { color: "#d97706", bg: "#d9770618", icon: <Clock className="w-3 h-3" />, label: "Pending" },
+  accepted: { color: "#16a34a", bg: "#16a34a18", icon: <CheckCircle2 className="w-3 h-3" />, label: "Accepted" },
+  expired: { color: "#78716C", bg: "#78716C18", icon: <Clock className="w-3 h-3" />, label: "Expired" },
+  revoked: { color: "#dc2626", bg: "#dc262618", icon: <XCircle className="w-3 h-3" />, label: "Revoked" },
+};
+
+const AUDIT_ACTION_LABELS: Record<string, { label: string; color: string }> = {
+  role_change: { label: "Role Changed", color: "#0369a1" },
+  user_deleted: { label: "User Deleted", color: "#dc2626" },
+  bulk_role_change: { label: "Bulk Role Change", color: "#7c3aed" },
+  bulk_delete: { label: "Bulk Delete", color: "#dc2626" },
+  invitation_sent: { label: "Invitation Sent", color: "#16a34a" },
+  invitation_resent: { label: "Invitation Resent", color: "#d97706" },
+  invitation_revoked: { label: "Invitation Revoked", color: "#dc2626" },
+  invitation_accepted: { label: "Invitation Accepted", color: "#16a34a" },
+};
+
+/* ─── Role Badge ─── */
+function RoleBadge({ role }: { role: string }) {
+  const config = ROLE_CONFIG[role] || ROLE_CONFIG.user;
+  return (
+    <span
+      className="inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full"
+      style={{ background: config.bg, color: config.color }}
+    >
+      {config.icon}
+      {config.label}
+    </span>
+  );
+}
+
+function InviteStatusBadge({ status }: { status: string }) {
+  const config = INVITE_STATUS_CONFIG[status] || INVITE_STATUS_CONFIG.pending;
+  return (
+    <span
+      className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full"
+      style={{ background: config.bg, color: config.color }}
+    >
+      {config.icon}
+      {config.label}
+    </span>
+  );
+}
+
+/* ─── Stats Cards ─── */
+function RoleStats({ users, invitations }: { users: any[]; invitations: any[] }) {
+  const counts = useMemo(() => {
+    const c = { total: users.length, admin: 0, agent: 0, user: 0, pendingInvites: 0 };
+    users.forEach(u => {
+      if (u.role in c) c[u.role as keyof typeof c]++;
+    });
+    c.pendingInvites = invitations.filter(i => i.status === "pending").length;
+    return c;
+  }, [users, invitations]);
+
+  const stats = [
+    { label: "Total Users", value: counts.total, icon: <Users className="w-5 h-5" />, iconBg: "#14532D14", iconColor: "#14532D" },
+    { label: "Admins", value: counts.admin, icon: <Crown className="w-5 h-5" />, iconBg: "#14532D18", iconColor: "#14532D" },
+    { label: "Agents", value: counts.agent, icon: <Headphones className="w-5 h-5" />, iconBg: "#0369a118", iconColor: "#0369a1" },
+    { label: "Pending Invites", value: counts.pendingInvites, icon: <Mail className="w-5 h-5" />, iconBg: "#d9770618", iconColor: "#d97706" },
+  ];
+
+  return (
+    <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
+      {stats.map(s => (
+        <div key={s.label} className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+          <div className="flex items-center gap-3">
+            <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: s.iconBg, color: s.iconColor }}>
+              {s.icon}
+            </div>
+            <div>
+              <div className="text-2xl font-bold" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{s.value}</div>
+              <div className="text-xs" style={{ color: "#a8a29e", fontFamily: "'Source Sans 3', sans-serif" }}>{s.label}</div>
+            </div>
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+/* ─── Send Invitation Dialog ─── */
+function SendInviteDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
+  const [email, setEmail] = useState("");
+  const [role, setRole] = useState<string>("agent");
+  const [message, setMessage] = useState("");
+  const utils = trpc.useUtils();
+
+  const sendMutation = trpc.invitations.send.useMutation({
+    onSuccess: (data) => {
+      toast.success("Invitation sent!", {
+        description: `Invitation sent to ${data.email} as ${ROLE_CONFIG[data.role]?.label || data.role}.`,
+      });
+      utils.invitations.list.invalidate();
+      utils.auditLogs.list.invalidate();
+      setEmail("");
+      setRole("agent");
+      setMessage("");
+      onOpenChange(false);
+    },
+    onError: (error) => {
+      toast.error("Failed to send invitation", { description: error.message });
+    },
+  });
+
+  const handleSend = () => {
+    if (!email.trim()) { toast.error("Email is required"); return; }
+    sendMutation.mutate({ email: email.trim(), role: role as "user" | "agent" | "admin", message: message.trim() || undefined });
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent style={{ borderColor: "#e7e0d5" }}>
+        <DialogHeader>
+          <DialogTitle style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+            Send Invitation
+          </DialogTitle>
+          <DialogDescription style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+            Invite a new team member to join the Homelegance chatbot system. They will receive a link to accept the invitation.
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-4 py-2">
+          <div>
+            <label className="text-sm font-medium block mb-1.5" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+              Email Address *
+            </label>
+            <input
+              type="email"
+              placeholder="colleague@homelegance.com"
+              value={email}
+              onChange={(e) => setEmail(e.target.value)}
+              className="w-full px-3 py-2.5 rounded-xl border text-sm"
+              style={{ borderColor: "#e7e0d5", background: "#fff", fontFamily: "'Source Sans 3', sans-serif", color: "#292524" }}
+            />
+          </div>
+
+          <div>
+            <label className="text-sm font-medium block mb-1.5" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+              Role
+            </label>
+            <Select value={role} onValueChange={setRole}>
+              <SelectTrigger style={{ borderColor: "#e7e0d5", background: "#fff" }}>
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="admin">
+                  <div className="flex items-center gap-2">
+                    <Crown className="w-3.5 h-3.5" style={{ color: "#14532D" }} />
+                    <span>Admin — Full access</span>
+                  </div>
+                </SelectItem>
+                <SelectItem value="agent">
+                  <div className="flex items-center gap-2">
+                    <Headphones className="w-3.5 h-3.5" style={{ color: "#0369a1" }} />
+                    <span>Agent — Dashboard access</span>
+                  </div>
+                </SelectItem>
+                <SelectItem value="user">
+                  <div className="flex items-center gap-2">
+                    <User className="w-3.5 h-3.5" style={{ color: "#78716C" }} />
+                    <span>User — Basic access</span>
+                  </div>
+                </SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+
+          <div>
+            <label className="text-sm font-medium block mb-1.5" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+              Personal Message (optional)
+            </label>
+            <textarea
+              placeholder="Welcome to the team! We'd love to have you help manage our chatbot..."
+              value={message}
+              onChange={(e) => setMessage(e.target.value)}
+              rows={3}
+              className="w-full px-3 py-2.5 rounded-xl border text-sm resize-none"
+              style={{ borderColor: "#e7e0d5", background: "#fff", fontFamily: "'Source Sans 3', sans-serif", color: "#292524" }}
+            />
+          </div>
+
+          <div className="p-3 rounded-lg" style={{ background: "#14532D08", border: "1px solid #14532D18" }}>
+            <p className="text-xs" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+              The invitation link will be valid for <strong>7 days</strong>. The invitee will need to sign in to accept the invitation and receive their assigned role.
+            </p>
+          </div>
+        </div>
+
+        <DialogFooter>
+          <Button variant="outline" onClick={() => onOpenChange(false)} style={{ borderColor: "#e7e0d5" }}>
+            Cancel
+          </Button>
+          <Button
+            onClick={handleSend}
+            disabled={sendMutation.isPending || !email.trim()}
+            className="text-white"
+            style={{ background: "#14532D" }}
+          >
+            {sendMutation.isPending ? (
+              <><RefreshCw className="w-4 h-4 animate-spin mr-2" /> Sending...</>
+            ) : (
+              <><Send className="w-4 h-4 mr-2" /> Send Invitation</>
+            )}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+/* ─── Users Tab ─── */
+function UsersTab() {
+  const { user: currentUser } = useAuth();
+  const [searchQuery, setSearchQuery] = useState("");
+  const [roleFilter, setRoleFilter] = useState<string>("all");
+  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
+  const [confirmDialog, setConfirmDialog] = useState<{
+    open: boolean; type: "role" | "delete" | "bulkRole" | "bulkDelete";
+    userId?: number; userName?: string; currentRole?: string; newRole?: string;
+  }>({ open: false, type: "role" });
+
+  const utils = trpc.useUtils();
+  const { data: usersList, isLoading } = trpc.users.list.useQuery();
+
+  const updateRoleMutation = trpc.users.updateRole.useMutation({
+    onSuccess: (data) => {
+      toast.success(`Role updated to ${ROLE_CONFIG[data.role]?.label || data.role}`);
+      utils.users.list.invalidate();
+      utils.auditLogs.list.invalidate();
+      setConfirmDialog(prev => ({ ...prev, open: false }));
+    },
+    onError: (error) => toast.error("Failed to update role", { description: error.message }),
+  });
+
+  const deleteMutation = trpc.users.delete.useMutation({
+    onSuccess: () => {
+      toast.success("User deleted successfully");
+      utils.users.list.invalidate();
+      utils.auditLogs.list.invalidate();
+      setConfirmDialog(prev => ({ ...prev, open: false }));
+    },
+    onError: (error) => toast.error("Failed to delete user", { description: error.message }),
+  });
+
+  const bulkRoleMutation = trpc.users.bulkUpdateRole.useMutation({
+    onSuccess: (results) => {
+      const successes = results.filter(r => r.success).length;
+      toast.success(`${successes} user(s) updated`);
+      utils.users.list.invalidate();
+      utils.auditLogs.list.invalidate();
+      setSelectedIds(new Set());
+      setConfirmDialog(prev => ({ ...prev, open: false }));
+    },
+    onError: (error) => toast.error("Bulk update failed", { description: error.message }),
+  });
+
+  const bulkDeleteMutation = trpc.users.bulkDelete.useMutation({
+    onSuccess: (results) => {
+      const successes = results.filter(r => r.success).length;
+      toast.success(`${successes} user(s) deleted`);
+      utils.users.list.invalidate();
+      utils.auditLogs.list.invalidate();
+      setSelectedIds(new Set());
+      setConfirmDialog(prev => ({ ...prev, open: false }));
+    },
+    onError: (error) => toast.error("Bulk delete failed", { description: error.message }),
+  });
+
+  const exportCsvQuery = trpc.users.exportCsv.useQuery(undefined, { enabled: false });
+
+  const filteredUsers = useMemo(() => {
+    if (!usersList) return [];
+    return usersList.filter((u: any) => {
+      const matchesSearch = !searchQuery ||
+        u.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+        u.email?.toLowerCase().includes(searchQuery.toLowerCase());
+      const matchesRole = roleFilter === "all" || u.role === roleFilter;
+      return matchesSearch && matchesRole;
+    });
+  }, [usersList, searchQuery, roleFilter]);
+
+  const toggleSelect = (id: number) => {
+    setSelectedIds(prev => {
+      const next = new Set(prev);
+      if (next.has(id)) next.delete(id); else next.add(id);
+      return next;
+    });
+  };
+
+  const toggleSelectAll = () => {
+    if (selectedIds.size === filteredUsers.filter((u: any) => u.id !== currentUser?.id).length) {
+      setSelectedIds(new Set());
+    } else {
+      setSelectedIds(new Set(filteredUsers.filter((u: any) => u.id !== currentUser?.id).map((u: any) => u.id)));
+    }
+  };
+
+  const handleExportCsv = async () => {
+    const result = await exportCsvQuery.refetch();
+    if (result.data) {
+      const blob = new Blob([result.data.csv], { type: "text/csv" });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = `homelegance-users-${new Date().toISOString().split("T")[0]}.csv`;
+      a.click();
+      URL.revokeObjectURL(url);
+      toast.success(`Exported ${result.data.count} users`);
+    }
+  };
+
+  const confirmAction = () => {
+    const d = confirmDialog;
+    if (d.type === "role" && d.userId && d.newRole) {
+      updateRoleMutation.mutate({ userId: d.userId, role: d.newRole as "user" | "agent" | "admin" });
+    } else if (d.type === "delete" && d.userId) {
+      deleteMutation.mutate({ userId: d.userId });
+    } else if (d.type === "bulkRole" && d.newRole) {
+      bulkRoleMutation.mutate({ userIds: Array.from(selectedIds), role: d.newRole as "user" | "agent" | "admin" });
+    } else if (d.type === "bulkDelete") {
+      bulkDeleteMutation.mutate({ userIds: Array.from(selectedIds) });
+    }
+  };
+
+  const isPending = updateRoleMutation.isPending || deleteMutation.isPending || bulkRoleMutation.isPending || bulkDeleteMutation.isPending;
+
+  return (
+    <div className="space-y-4">
+      {/* Toolbar */}
+      <div className="flex flex-col sm:flex-row gap-3">
+        <div className="relative flex-1">
+          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: "#a8a29e" }} />
+          <input
+            type="text"
+            placeholder="Search by name or email..."
+            value={searchQuery}
+            onChange={(e) => setSearchQuery(e.target.value)}
+            className="w-full pl-10 pr-4 py-2.5 rounded-xl border text-sm"
+            style={{ borderColor: "#e7e0d5", background: "#fff", fontFamily: "'Source Sans 3', sans-serif", color: "#292524" }}
+          />
+        </div>
+        <Select value={roleFilter} onValueChange={setRoleFilter}>
+          <SelectTrigger className="w-full sm:w-40" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
+            <SelectValue placeholder="Filter by role" />
+          </SelectTrigger>
+          <SelectContent>
+            <SelectItem value="all">All Roles</SelectItem>
+            <SelectItem value="admin">Admin</SelectItem>
+            <SelectItem value="agent">Agent</SelectItem>
+            <SelectItem value="user">User</SelectItem>
+          </SelectContent>
+        </Select>
+        <Button variant="outline" onClick={handleExportCsv} className="gap-2" style={{ borderColor: "#e7e0d5" }}>
+          <Download className="w-4 h-4" /> Export CSV
+        </Button>
+      </div>
+
+      {/* Bulk Actions Bar */}
+      {selectedIds.size > 0 && (
+        <div className="flex items-center gap-3 p-3 rounded-xl border" style={{ background: "#14532D08", borderColor: "#14532D30" }}>
+          <span className="text-sm font-medium" style={{ color: "#14532D" }}>
+            {selectedIds.size} selected
+          </span>
+          <div className="flex gap-2 ml-auto">
+            <Select onValueChange={(role) => setConfirmDialog({ open: true, type: "bulkRole", newRole: role })}>
+              <SelectTrigger className="w-40 h-8 text-xs" style={{ borderColor: "#14532D30", background: "#fff" }}>
+                <SelectValue placeholder="Change role to..." />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="admin">Admin</SelectItem>
+                <SelectItem value="agent">Agent</SelectItem>
+                <SelectItem value="user">User</SelectItem>
+              </SelectContent>
+            </Select>
+            <Button
+              variant="outline"
+              size="sm"
+              className="text-red-600 border-red-200 hover:bg-red-50"
+              onClick={() => setConfirmDialog({ open: true, type: "bulkDelete" })}
+            >
+              <Trash2 className="w-3.5 h-3.5 mr-1" /> Delete
+            </Button>
+            <Button variant="ghost" size="sm" onClick={() => setSelectedIds(new Set())}>
+              Clear
+            </Button>
+          </div>
+        </div>
+      )}
+
+      {/* Users Table */}
+      <div className="rounded-xl border overflow-hidden" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <ScrollArea className="max-h-[500px]">
+          <table className="w-full">
+            <thead>
+              <tr style={{ background: "#f5f0e8" }}>
+                <th className="text-left px-3 py-3 w-10">
+                  <button onClick={toggleSelectAll} className="flex items-center justify-center">
+                    {selectedIds.size > 0 && selectedIds.size === filteredUsers.filter((u: any) => u.id !== currentUser?.id).length ? (
+                      <CheckSquare className="w-4 h-4" style={{ color: "#14532D" }} />
+                    ) : (
+                      <Square className="w-4 h-4" style={{ color: "#a8a29e" }} />
+                    )}
+                  </button>
+                </th>
+                <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>User</th>
+                <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Role</th>
+                <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Joined</th>
+                <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Last Active</th>
+                <th className="text-right text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={6} className="text-center py-12">
+                    <div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
+                    <p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading users...</p>
+                  </td>
+                </tr>
+              ) : filteredUsers.length === 0 ? (
+                <tr>
+                  <td colSpan={6} className="text-center py-12">
+                    <Users className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
+                    <p className="text-sm" style={{ color: "#a8a29e" }}>No users found</p>
+                  </td>
+                </tr>
+              ) : (
+                filteredUsers.map((u: any) => {
+                  const isSelf = u.id === currentUser?.id;
+                  const isSelected = selectedIds.has(u.id);
+                  return (
+                    <tr key={u.id} className="border-t transition-colors hover:bg-black/[0.02]" style={{ borderColor: "#e7e0d5", background: isSelected ? "#14532D06" : undefined }}>
+                      <td className="px-3 py-3">
+                        {isSelf ? (
+                          <div className="w-4 h-4" />
+                        ) : (
+                          <button onClick={() => toggleSelect(u.id)} className="flex items-center justify-center">
+                            {isSelected ? (
+                              <CheckSquare className="w-4 h-4" style={{ color: "#14532D" }} />
+                            ) : (
+                              <Square className="w-4 h-4" style={{ color: "#d6d3d1" }} />
+                            )}
+                          </button>
+                        )}
+                      </td>
+                      <td className="px-4 py-3">
+                        <div className="flex items-center gap-3">
+                          <div
+                            className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-xs font-medium"
+                            style={{ background: ROLE_CONFIG[u.role]?.bg || "#78716C18", color: ROLE_CONFIG[u.role]?.color || "#78716C" }}
+                          >
+                            {u.name?.charAt(0).toUpperCase() || "?"}
+                          </div>
+                          <div>
+                            <div className="flex items-center gap-2">
+                              <span className="text-sm font-medium" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+                                {u.name || "Unnamed"}
+                              </span>
+                              {isSelf && (
+                                <span className="text-[10px] px-1.5 py-0.5 rounded-full font-medium" style={{ background: "#14532D14", color: "#14532D" }}>You</span>
+                              )}
+                            </div>
+                            <span className="text-xs" style={{ color: "#a8a29e" }}>{u.email || "No email"}</span>
+                          </div>
+                        </div>
+                      </td>
+                      <td className="px-4 py-3"><RoleBadge role={u.role} /></td>
+                      <td className="px-4 py-3 hidden md:table-cell">
+                        <span className="text-xs" style={{ color: "#78716C" }}>
+                          {u.createdAt ? new Date(u.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—"}
+                        </span>
+                      </td>
+                      <td className="px-4 py-3 hidden md:table-cell">
+                        <span className="text-xs" style={{ color: "#78716C" }}>
+                          {u.lastSignedIn ? new Date(u.lastSignedIn).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "Never"}
+                        </span>
+                      </td>
+                      <td className="px-4 py-3 text-right">
+                        {isSelf ? (
+                          <span className="text-xs italic" style={{ color: "#a8a29e" }}>Cannot edit self</span>
+                        ) : (
+                          <div className="flex items-center gap-1 justify-end">
+                            <Select
+                              value={u.role}
+                              onValueChange={(newRole) => setConfirmDialog({ open: true, type: "role", userId: u.id, userName: u.name || "this user", currentRole: u.role, newRole })}
+                            >
+                              <SelectTrigger className="w-28 h-8 text-xs" style={{ borderColor: "#e7e0d5" }}>
+                                <SelectValue />
+                              </SelectTrigger>
+                              <SelectContent>
+                                <SelectItem value="admin"><div className="flex items-center gap-2"><Crown className="w-3 h-3" style={{ color: "#14532D" }} /> Admin</div></SelectItem>
+                                <SelectItem value="agent"><div className="flex items-center gap-2"><Headphones className="w-3 h-3" style={{ color: "#0369a1" }} /> Agent</div></SelectItem>
+                                <SelectItem value="user"><div className="flex items-center gap-2"><User className="w-3 h-3" style={{ color: "#78716C" }} /> User</div></SelectItem>
+                              </SelectContent>
+                            </Select>
+                            <Button
+                              variant="ghost"
+                              size="sm"
+                              className="h-8 w-8 p-0 text-red-400 hover:text-red-600 hover:bg-red-50"
+                              onClick={() => setConfirmDialog({ open: true, type: "delete", userId: u.id, userName: u.name || u.email || "this user" })}
+                            >
+                              <Trash2 className="w-3.5 h-3.5" />
+                            </Button>
+                          </div>
+                        )}
+                      </td>
+                    </tr>
+                  );
+                })
+              )}
+            </tbody>
+          </table>
+        </ScrollArea>
+      </div>
+
+      {/* Confirmation Dialog */}
+      <Dialog open={confirmDialog.open} onOpenChange={(open) => setConfirmDialog(prev => ({ ...prev, open }))}>
+        <DialogContent style={{ borderColor: "#e7e0d5" }}>
+          <DialogHeader>
+            <DialogTitle style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+              {confirmDialog.type === "delete" ? "Delete User" :
+               confirmDialog.type === "bulkDelete" ? `Delete ${selectedIds.size} Users` :
+               confirmDialog.type === "bulkRole" ? `Change Role for ${selectedIds.size} Users` :
+               "Confirm Role Change"}
+            </DialogTitle>
+            <DialogDescription style={{ fontFamily: "'Source Sans 3', sans-serif" }}>
+              {confirmDialog.type === "delete" ? (
+                <>Are you sure you want to delete <strong>{confirmDialog.userName}</strong>? This action cannot be undone.</>
+              ) : confirmDialog.type === "bulkDelete" ? (
+                <>Are you sure you want to delete <strong>{selectedIds.size} users</strong>? This action cannot be undone.</>
+              ) : confirmDialog.type === "bulkRole" ? (
+                <>Change the role of <strong>{selectedIds.size} users</strong> to <RoleBadge role={confirmDialog.newRole || "user"} />?</>
+              ) : (
+                <>Change the role for <strong>{confirmDialog.userName}</strong>.</>
+              )}
+            </DialogDescription>
+          </DialogHeader>
+
+          {confirmDialog.type === "role" && (
+            <div className="space-y-3 py-2">
+              <div className="flex items-center gap-3">
+                <span className="text-sm" style={{ color: "#78716C", minWidth: 50 }}>From:</span>
+                <RoleBadge role={confirmDialog.currentRole || "user"} />
+              </div>
+              <div className="flex items-center gap-3">
+                <span className="text-sm" style={{ color: "#78716C", minWidth: 50 }}>To:</span>
+                <RoleBadge role={confirmDialog.newRole || "user"} />
+              </div>
+            </div>
+          )}
+
+          {(confirmDialog.type === "delete" || confirmDialog.type === "bulkDelete") && (
+            <div className="flex items-start gap-2 p-3 rounded-lg" style={{ background: "#dc262610", border: "1px solid #dc262630" }}>
+              <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" style={{ color: "#dc2626" }} />
+              <p className="text-xs" style={{ color: "#dc2626" }}>
+                This will permanently remove the user(s) and their access. This cannot be undone.
+              </p>
+            </div>
+          )}
+
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setConfirmDialog(prev => ({ ...prev, open: false }))} style={{ borderColor: "#e7e0d5" }}>
+              Cancel
+            </Button>
+            <Button
+              onClick={confirmAction}
+              disabled={isPending}
+              className="text-white"
+              style={{ background: confirmDialog.type === "delete" || confirmDialog.type === "bulkDelete" ? "#dc2626" : "#14532D" }}
+            >
+              {isPending ? "Processing..." : confirmDialog.type === "delete" || confirmDialog.type === "bulkDelete" ? "Delete" : "Confirm"}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}
+
+/* ─── Invitations Tab ─── */
+function InvitationsTab() {
+  const [statusFilter, setStatusFilter] = useState<string>("all");
+  const utils = trpc.useUtils();
+  const { data: invitationsList, isLoading } = trpc.invitations.list.useQuery();
+
+  const resendMutation = trpc.invitations.resend.useMutation({
+    onSuccess: () => {
+      toast.success("Invitation resent with a new link");
+      utils.invitations.list.invalidate();
+      utils.auditLogs.list.invalidate();
+    },
+    onError: (error) => toast.error("Failed to resend", { description: error.message }),
+  });
+
+  const revokeMutation = trpc.invitations.revoke.useMutation({
+    onSuccess: () => {
+      toast.success("Invitation revoked");
+      utils.invitations.list.invalidate();
+      utils.auditLogs.list.invalidate();
+    },
+    onError: (error) => toast.error("Failed to revoke", { description: error.message }),
+  });
+
+  const filteredInvitations = useMemo(() => {
+    if (!invitationsList) return [];
+    if (statusFilter === "all") return invitationsList;
+    return invitationsList.filter((i: any) => i.status === statusFilter);
+  }, [invitationsList, statusFilter]);
+
+  const copyInviteLink = (token: string) => {
+    const link = `${window.location.origin}/invite/${token}`;
+    navigator.clipboard.writeText(link);
+    toast.success("Invite link copied to clipboard");
+  };
+
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center gap-3">
+        <Select value={statusFilter} onValueChange={setStatusFilter}>
+          <SelectTrigger className="w-40" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
+            <SelectValue placeholder="Filter status" />
+          </SelectTrigger>
+          <SelectContent>
+            <SelectItem value="all">All Status</SelectItem>
+            <SelectItem value="pending">Pending</SelectItem>
+            <SelectItem value="accepted">Accepted</SelectItem>
+            <SelectItem value="expired">Expired</SelectItem>
+            <SelectItem value="revoked">Revoked</SelectItem>
+          </SelectContent>
+        </Select>
+        <span className="text-sm" style={{ color: "#a8a29e" }}>
+          {filteredInvitations.length} invitation{filteredInvitations.length !== 1 ? "s" : ""}
+        </span>
+      </div>
+
+      <div className="rounded-xl border overflow-hidden" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <ScrollArea className="max-h-[500px]">
+          <table className="w-full">
+            <thead>
+              <tr style={{ background: "#f5f0e8" }}>
+                <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Email</th>
+                <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Role</th>
+                <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Status</th>
+                <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Sent By</th>
+                <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Expires</th>
+                <th className="text-right text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={6} className="text-center py-12">
+                    <div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
+                    <p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading invitations...</p>
+                  </td>
+                </tr>
+              ) : filteredInvitations.length === 0 ? (
+                <tr>
+                  <td colSpan={6} className="text-center py-12">
+                    <Mail className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
+                    <p className="text-sm" style={{ color: "#a8a29e" }}>No invitations found</p>
+                  </td>
+                </tr>
+              ) : (
+                filteredInvitations.map((inv: any) => (
+                  <tr key={inv.id} className="border-t transition-colors hover:bg-black/[0.02]" style={{ borderColor: "#e7e0d5" }}>
+                    <td className="px-4 py-3">
+                      <div className="flex items-center gap-2">
+                        <Mail className="w-4 h-4 shrink-0" style={{ color: "#a8a29e" }} />
+                        <div>
+                          <span className="text-sm font-medium block" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+                            {inv.email}
+                          </span>
+                          {inv.message && (
+                            <span className="text-xs block truncate max-w-[200px]" style={{ color: "#a8a29e" }}>
+                              "{inv.message}"
+                            </span>
+                          )}
+                        </div>
+                      </div>
+                    </td>
+                    <td className="px-4 py-3"><RoleBadge role={inv.role} /></td>
+                    <td className="px-4 py-3"><InviteStatusBadge status={inv.status} /></td>
+                    <td className="px-4 py-3 hidden md:table-cell">
+                      <span className="text-xs" style={{ color: "#78716C" }}>{inv.invitedByName || "—"}</span>
+                    </td>
+                    <td className="px-4 py-3 hidden md:table-cell">
+                      <span className="text-xs" style={{ color: inv.status === "pending" && new Date(inv.expiresAt) < new Date() ? "#dc2626" : "#78716C" }}>
+                        {inv.expiresAt ? new Date(inv.expiresAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—"}
+                      </span>
+                    </td>
+                    <td className="px-4 py-3 text-right">
+                      <div className="flex items-center gap-1 justify-end">
+                        {inv.status === "pending" && (
+                          <>
+                            <Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={() => copyInviteLink(inv.token)}>
+                              <Copy className="w-3 h-3" /> Copy Link
+                            </Button>
+                            <Button
+                              variant="ghost" size="sm" className="h-7 text-xs gap-1"
+                              onClick={() => resendMutation.mutate({ invitationId: inv.id })}
+                              disabled={resendMutation.isPending}
+                            >
+                              <RefreshCw className="w-3 h-3" /> Resend
+                            </Button>
+                            <Button
+                              variant="ghost" size="sm" className="h-7 text-xs gap-1 text-red-500 hover:text-red-700"
+                              onClick={() => revokeMutation.mutate({ invitationId: inv.id })}
+                              disabled={revokeMutation.isPending}
+                            >
+                              <XCircle className="w-3 h-3" /> Revoke
+                            </Button>
+                          </>
+                        )}
+                        {(inv.status === "expired") && (
+                          <Button
+                            variant="ghost" size="sm" className="h-7 text-xs gap-1"
+                            onClick={() => resendMutation.mutate({ invitationId: inv.id })}
+                            disabled={resendMutation.isPending}
+                          >
+                            <RefreshCw className="w-3 h-3" /> Resend
+                          </Button>
+                        )}
+                        {inv.status === "accepted" && (
+                          <span className="text-xs" style={{ color: "#16a34a" }}>
+                            {inv.acceptedAt ? `Accepted ${new Date(inv.acceptedAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}` : "Accepted"}
+                          </span>
+                        )}
+                        {inv.status === "revoked" && (
+                          <span className="text-xs italic" style={{ color: "#a8a29e" }}>Revoked</span>
+                        )}
+                      </div>
+                    </td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </ScrollArea>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Activity Log Tab ─── */
+function ActivityLogTab() {
+  const { data: logs, isLoading } = trpc.auditLogs.list.useQuery({ limit: 100 });
+
+  return (
+    <div className="space-y-4">
+      <div className="rounded-xl border overflow-hidden" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <ScrollArea className="max-h-[600px]">
+          {isLoading ? (
+            <div className="text-center py-12">
+              <div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
+              <p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading activity...</p>
+            </div>
+          ) : !logs || logs.length === 0 ? (
+            <div className="text-center py-12">
+              <History className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
+              <p className="text-sm" style={{ color: "#a8a29e" }}>No activity recorded yet</p>
+            </div>
+          ) : (
+            <div className="divide-y" style={{ borderColor: "#e7e0d5" }}>
+              {logs.map((log: any) => {
+                const actionConfig = AUDIT_ACTION_LABELS[log.action] || { label: log.action, color: "#78716C" };
+                const details = log.details as Record<string, any> | null;
+                return (
+                  <div key={log.id} className="px-4 py-3 hover:bg-black/[0.02] transition-colors">
+                    <div className="flex items-start gap-3">
+                      <div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 mt-0.5" style={{ background: actionConfig.color + "14", color: actionConfig.color }}>
+                        <History className="w-4 h-4" />
+                      </div>
+                      <div className="flex-1 min-w-0">
+                        <div className="flex items-center gap-2 flex-wrap">
+                          <span className="text-xs font-semibold px-2 py-0.5 rounded-full" style={{ background: actionConfig.color + "14", color: actionConfig.color }}>
+                            {actionConfig.label}
+                          </span>
+                          <span className="text-xs" style={{ color: "#a8a29e" }}>
+                            {log.createdAt ? new Date(log.createdAt).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""}
+                          </span>
+                        </div>
+                        <p className="text-sm mt-1" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+                          <strong>{log.actorName || "System"}</strong>
+                          {log.action === "role_change" && details && (
+                            <> changed <strong>{log.targetName}</strong>'s role from {details.previousRole} to {details.newRole}</>
+                          )}
+                          {log.action === "user_deleted" && (
+                            <> deleted user <strong>{log.targetName}</strong></>
+                          )}
+                          {log.action === "invitation_sent" && details && (
+                            <> invited <strong>{log.targetName}</strong> as {details.role}</>
+                          )}
+                          {log.action === "invitation_resent" && (
+                            <> resent invitation to <strong>{log.targetName}</strong></>
+                          )}
+                          {log.action === "invitation_revoked" && (
+                            <> revoked invitation for <strong>{log.targetName}</strong></>
+                          )}
+                          {log.action === "invitation_accepted" && (
+                            <> accepted invitation for <strong>{log.targetName}</strong></>
+                          )}
+                          {log.action === "bulk_role_change" && details && (
+                            <> changed {details.userIds?.length} users to {details.newRole}</>
+                          )}
+                          {log.action === "bulk_delete" && details && (
+                            <> deleted {details.userIds?.length} users</>
+                          )}
+                        </p>
+                      </div>
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          )}
+        </ScrollArea>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Main Component ─── */
+export default function UserManagement() {
+  const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
+  const { data: usersList } = trpc.users.list.useQuery();
+  const { data: invitationsList } = trpc.invitations.list.useQuery();
+
+  return (
+    <div className="space-y-6">
+      {/* Header */}
+      <div className="flex items-start justify-between">
+        <div>
+          <h1 className="text-2xl font-bold" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
+            User Management
+          </h1>
+          <p className="text-sm mt-1" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
+            Manage users, send invitations, and track team activity.
+          </p>
+        </div>
+        <Button onClick={() => setInviteDialogOpen(true)} className="text-white gap-2" style={{ background: "#14532D" }}>
+          <UserPlus className="w-4 h-4" /> Invite Member
+        </Button>
+      </div>
+
+      {/* Stats */}
+      <RoleStats users={usersList || []} invitations={invitationsList || []} />
+
+      {/* Role Legend */}
+      <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <h3 className="text-sm font-semibold mb-3" style={{ color: "#292524", fontFamily: "'Source Sans 3', sans-serif" }}>
+          Role Permissions
+        </h3>
+        <div className="grid sm:grid-cols-3 gap-3">
+          {Object.entries(ROLE_CONFIG).map(([key, config]) => (
+            <div key={key} className="flex items-start gap-3 p-3 rounded-lg" style={{ background: config.bg }}>
+              <div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 mt-0.5" style={{ background: config.color + "20", color: config.color }}>
+                {config.icon}
+              </div>
+              <div>
+                <div className="text-sm font-semibold" style={{ color: config.color }}>{config.label}</div>
+                <div className="text-xs mt-0.5" style={{ color: "#78716C" }}>{config.description}</div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Tabs */}
+      <Tabs defaultValue="users" className="w-full">
+        <TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0 h-auto" style={{ borderColor: "#e7e0d5" }}>
+          <TabsTrigger
+            value="users"
+            className="rounded-none border-b-2 border-transparent data-[state=active]:border-[#14532D] data-[state=active]:text-[#14532D] data-[state=active]:shadow-none px-4 py-2.5 text-sm"
+            style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            <Users className="w-4 h-4 mr-2" /> Users
+          </TabsTrigger>
+          <TabsTrigger
+            value="invitations"
+            className="rounded-none border-b-2 border-transparent data-[state=active]:border-[#14532D] data-[state=active]:text-[#14532D] data-[state=active]:shadow-none px-4 py-2.5 text-sm"
+            style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            <Mail className="w-4 h-4 mr-2" /> Invitations
+          </TabsTrigger>
+          <TabsTrigger
+            value="activity"
+            className="rounded-none border-b-2 border-transparent data-[state=active]:border-[#14532D] data-[state=active]:text-[#14532D] data-[state=active]:shadow-none px-4 py-2.5 text-sm"
+            style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+          >
+            <History className="w-4 h-4 mr-2" /> Activity Log
+          </TabsTrigger>
+        </TabsList>
+
+        <TabsContent value="users" className="mt-4">
+          <UsersTab />
+        </TabsContent>
+        <TabsContent value="invitations" className="mt-4">
+          <InvitationsTab />
+        </TabsContent>
+        <TabsContent value="activity" className="mt-4">
+          <ActivityLogTab />
+        </TabsContent>
+      </Tabs>
+
+      {/* Send Invitation Dialog */}
+      <SendInviteDialog open={inviteDialogOpen} onOpenChange={setInviteDialogOpen} />
+    </div>
+  );
+}

+ 945 - 0
client/src/pages/WorkflowDesigner.tsx

@@ -0,0 +1,945 @@
+/**
+ * Workflow Designer — Tidio-inspired flow builder with categories & sub-categories
+ * Categories: Support Flows, Leads Flows, Sales Flows, Others
+ * Sub-categories: Deflect, Self Service, Orders, Shipping, Returning, Cancelling
+ */
+import { useState, useRef, useCallback, useEffect, useMemo } from "react";
+import { useAuth } from "@/_core/hooks/useAuth";
+import { trpc } from "@/lib/trpc";
+import { Button } from "@/components/ui/button";
+import { toast } from "sonner";
+import {
+  GitBranch, Zap, Save, Trash2, X, Square,
+  MessageCircle, Bot, Users, HelpCircle,
+  Database, ShoppingCart, ShieldAlert, Sparkles,
+  Check, Clock, XCircle, RefreshCw, ChevronDown, ChevronUp,
+  UserCheck, FileText, AlertTriangle, Plus,
+  Headphones, TrendingUp, Megaphone, MoreHorizontal,
+  Package, Truck, RotateCw, ArrowRight, Layers,
+  Play, Eye, Copy, ChevronRight, FolderOpen,
+  Shield, Filter, Search, Settings,
+} from "lucide-react";
+
+/* ─── Types ─── */
+type NodeType = "greeting" | "intent" | "response" | "condition" | "escalation" | "action" | "end" | "customer_data" | "sales_order" | "guardrail";
+
+interface FlowNode {
+  id: string;
+  type: NodeType;
+  label: string;
+  config: Record<string, any>;
+  x: number;
+  y: number;
+}
+
+interface FlowEdge {
+  id: string;
+  sourceId: string;
+  targetId: string;
+  label?: string;
+}
+
+/* ─── Flow Category & Template Types ─── */
+interface FlowSubCategory {
+  id: string;
+  label: string;
+  icon: React.ReactNode;
+  color: string;
+  description: string;
+}
+
+interface FlowCategory {
+  id: string;
+  label: string;
+  icon: React.ReactNode;
+  color: string;
+  description: string;
+  subCategories: FlowSubCategory[];
+}
+
+interface FlowTemplate {
+  id: string;
+  name: string;
+  description: string;
+  category: string;
+  subCategory: string;
+  tags: string[];
+  nodes: FlowNode[];
+  edges: FlowEdge[];
+  isActive: boolean;
+}
+
+/* ─── Node type config ─── */
+const NODE_TYPES: Record<NodeType, { icon: React.ReactNode; color: string; bgColor: string; description: string }> = {
+  greeting: { icon: <MessageCircle className="w-4 h-4" />, color: "#14532D", bgColor: "#14532D14", description: "Welcome message with quick-reply buttons" },
+  intent: { icon: <HelpCircle className="w-4 h-4" />, color: "#0369a1", bgColor: "#0369a114", description: "Detect user intent from message" },
+  response: { icon: <Bot className="w-4 h-4" />, color: "#7c3aed", bgColor: "#7c3aed14", description: "Send a response to the user" },
+  condition: { icon: <GitBranch className="w-4 h-4" />, color: "#ca8a04", bgColor: "#ca8a0414", description: "Branch based on a condition" },
+  escalation: { icon: <Users className="w-4 h-4" />, color: "#C2410C", bgColor: "#C2410C14", description: "Transfer to a human agent" },
+  action: { icon: <Zap className="w-4 h-4" />, color: "#059669", bgColor: "#05966914", description: "Perform an action (API call)" },
+  end: { icon: <Square className="w-4 h-4" />, color: "#78716C", bgColor: "#78716C14", description: "End the conversation flow" },
+  customer_data: { icon: <Database className="w-4 h-4" />, color: "#7c3aed", bgColor: "#7c3aed14", description: "Look up customer data from CRM" },
+  sales_order: { icon: <ShoppingCart className="w-4 h-4" />, color: "#0891b2", bgColor: "#0891b214", description: "Query sales order data" },
+  guardrail: { icon: <ShieldAlert className="w-4 h-4" />, color: "#dc2626", bgColor: "#dc262614", description: "Block sensitive topics" },
+};
+
+/* ─── Flow Categories (Tidio-inspired) ─── */
+const FLOW_CATEGORIES: FlowCategory[] = [
+  {
+    id: "support",
+    label: "Support Flows",
+    icon: <Headphones className="w-5 h-5" />,
+    color: "#14532D",
+    description: "Handle customer support inquiries and resolve issues",
+    subCategories: [
+      { id: "orders", label: "Orders", icon: <Package className="w-4 h-4" />, color: "#14532D", description: "Order status, tracking, modifications" },
+      { id: "shipping", label: "Shipping", icon: <Truck className="w-4 h-4" />, color: "#0369a1", description: "Shipping inquiries, delivery updates" },
+      { id: "returning", label: "Returning", icon: <RotateCw className="w-4 h-4" />, color: "#ca8a04", description: "Return requests, refund processing" },
+      { id: "cancelling", label: "Cancelling", icon: <XCircle className="w-4 h-4" />, color: "#dc2626", description: "Order cancellation requests" },
+      { id: "deflect", label: "Deflect", icon: <Shield className="w-4 h-4" />, color: "#7c3aed", description: "Deflect common questions with self-service" },
+      { id: "self_service", label: "Self Service", icon: <Search className="w-4 h-4" />, color: "#059669", description: "Enable customers to solve issues themselves" },
+    ],
+  },
+  {
+    id: "leads",
+    label: "Leads Flows",
+    icon: <TrendingUp className="w-5 h-5" />,
+    color: "#0369a1",
+    description: "Capture and qualify leads from website visitors",
+    subCategories: [
+      { id: "capture", label: "Lead Capture", icon: <UserCheck className="w-4 h-4" />, color: "#0369a1", description: "Collect visitor information" },
+      { id: "qualify", label: "Qualify", icon: <Filter className="w-4 h-4" />, color: "#7c3aed", description: "Qualify leads based on criteria" },
+      { id: "nurture", label: "Nurture", icon: <MessageCircle className="w-4 h-4" />, color: "#059669", description: "Engage and nurture leads" },
+    ],
+  },
+  {
+    id: "sales",
+    label: "Sales Flows",
+    icon: <Megaphone className="w-5 h-5" />,
+    color: "#ca8a04",
+    description: "Drive sales through guided product discovery",
+    subCategories: [
+      { id: "product_search", label: "Product Search", icon: <Search className="w-4 h-4" />, color: "#ca8a04", description: "Help customers find products" },
+      { id: "recommendations", label: "Recommendations", icon: <Sparkles className="w-4 h-4" />, color: "#7c3aed", description: "AI-powered product suggestions" },
+      { id: "dealer_locator", label: "Dealer Locator", icon: <Users className="w-4 h-4" />, color: "#14532D", description: "Find nearby dealers" },
+    ],
+  },
+  {
+    id: "others",
+    label: "Others",
+    icon: <MoreHorizontal className="w-5 h-5" />,
+    color: "#78716C",
+    description: "Custom flows and miscellaneous automations",
+    subCategories: [
+      { id: "faq", label: "FAQ", icon: <HelpCircle className="w-4 h-4" />, color: "#78716C", description: "Frequently asked questions" },
+      { id: "feedback", label: "Feedback", icon: <FileText className="w-4 h-4" />, color: "#0891b2", description: "Collect customer feedback" },
+      { id: "custom", label: "Custom", icon: <Settings className="w-4 h-4" />, color: "#a8a29e", description: "Build your own flow" },
+    ],
+  },
+];
+
+/* ─── Pre-built Flow Templates ─── */
+const FLOW_TEMPLATES: FlowTemplate[] = [
+  {
+    id: "order_status",
+    name: "Check Order Status",
+    description: "Let customers check their order status by providing an order number",
+    category: "support",
+    subCategory: "orders",
+    tags: ["popular", "self-service"],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Greeting", config: { message: "👋 What can we help you with today?", quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"], detectCustomerId: true, customerIdSource: "session" }, x: 300, y: 50 },
+      { id: "g2", type: "guardrail", label: "Content Filter", config: { blockedTopics: ["overall revenue", "profit margin", "internal pricing", "employee data"], blockedMessage: "I'm sorry, I can't share that information." }, x: 600, y: 50 },
+      { id: "i1", type: "intent", label: "Detect Intent", config: { intents: ["order_status", "track_order", "modify_order"] }, x: 300, y: 180 },
+      { id: "cd1", type: "customer_data", label: "Customer Lookup", config: { apiEndpoint: "/api/crm/customer", lookupField: "customerId", returnFields: ["name", "email", "accountType"] }, x: 100, y: 310 },
+      { id: "so1", type: "sales_order", label: "Order Lookup", config: { apiEndpoint: "/api/orders", lookupField: "orderId", returnFields: ["status", "estimatedDelivery", "trackingNumber"] }, x: 500, y: 310 },
+      { id: "r1", type: "response", label: "Order Status Reply", config: { message: "Your order {{orderId}} is currently {{status}}. Estimated delivery: {{estimatedDelivery}}" }, x: 300, y: 440 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 560 },
+    ],
+    edges: [
+      { id: "e_g1_i1", sourceId: "g1", targetId: "i1" },
+      { id: "e_i1_cd1", sourceId: "i1", targetId: "cd1", label: "order_status" },
+      { id: "e_i1_so1", sourceId: "i1", targetId: "so1", label: "track_order" },
+      { id: "e_cd1_r1", sourceId: "cd1", targetId: "r1" },
+      { id: "e_so1_r1", sourceId: "so1", targetId: "r1" },
+      { id: "e_r1_e1", sourceId: "r1", targetId: "e1" },
+    ],
+  },
+  {
+    id: "shipping_tracking",
+    name: "Track Shipment",
+    description: "Help customers track their shipment with real-time updates",
+    category: "support",
+    subCategory: "shipping",
+    tags: ["popular"],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Greeting", config: { message: "👋 What can we help you with today?", quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"], detectCustomerId: true, customerIdSource: "session" }, x: 300, y: 50 },
+      { id: "r1", type: "response", label: "Ask Tracking #", config: { message: "Please provide your tracking number and I'll look up the latest status." }, x: 300, y: 180 },
+      { id: "a1", type: "action", label: "Track Shipment API", config: { apiEndpoint: "/api/shipping/track", method: "GET" }, x: 300, y: 310 },
+      { id: "r2", type: "response", label: "Tracking Result", config: { message: "Your package is currently {{currentLocation}}. Estimated arrival: {{estimatedArrival}}" }, x: 300, y: 440 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 560 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "r1" },
+      { id: "e2", sourceId: "r1", targetId: "a1" },
+      { id: "e3", sourceId: "a1", targetId: "r2" },
+      { id: "e4", sourceId: "r2", targetId: "e1" },
+    ],
+  },
+  {
+    id: "return_request",
+    name: "Submit Return Request",
+    description: "Guide customers through the return process step by step",
+    category: "support",
+    subCategory: "returning",
+    tags: ["self-service"],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Greeting", config: { message: "👋 What can we help you with today?", quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"], detectCustomerId: true, customerIdSource: "session" }, x: 300, y: 50 },
+      { id: "r1", type: "response", label: "Ask Order #", config: { message: "I'll help you with your return. Please provide your order number." }, x: 300, y: 180 },
+      { id: "cd1", type: "customer_data", label: "Verify Customer", config: { apiEndpoint: "/api/crm/customer", lookupField: "customerId" }, x: 100, y: 310 },
+      { id: "c1", type: "condition", label: "Return Eligible?", config: { condition: "order.daysOld <= 30 && order.status === 'delivered'" }, x: 300, y: 310 },
+      { id: "a1", type: "action", label: "Create Return", config: { apiEndpoint: "/api/returns", method: "POST" }, x: 150, y: 440 },
+      { id: "r2", type: "response", label: "Not Eligible", config: { message: "Unfortunately, this order is not eligible for return. Would you like to speak with an agent?" }, x: 450, y: 440 },
+      { id: "esc1", type: "escalation", label: "Transfer to Agent", config: { priority: "normal", department: "returns" }, x: 450, y: 560 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 560 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "r1" },
+      { id: "e2", sourceId: "r1", targetId: "c1" },
+      { id: "e3", sourceId: "c1", targetId: "a1", label: "eligible" },
+      { id: "e4", sourceId: "c1", targetId: "r2", label: "not eligible" },
+      { id: "e5", sourceId: "r2", targetId: "esc1" },
+      { id: "e6", sourceId: "a1", targetId: "e1" },
+    ],
+  },
+  {
+    id: "cancel_order",
+    name: "Cancel Order",
+    description: "Process order cancellation requests with eligibility checks",
+    category: "support",
+    subCategory: "cancelling",
+    tags: [],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Greeting", config: { message: "👋 What can we help you with today?", quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"], detectCustomerId: true, customerIdSource: "session" }, x: 300, y: 50 },
+      { id: "r1", type: "response", label: "Ask Order #", config: { message: "I can help you cancel an order. Please provide the order number." }, x: 300, y: 180 },
+      { id: "so1", type: "sales_order", label: "Check Order", config: { apiEndpoint: "/api/orders", lookupField: "orderId" }, x: 300, y: 310 },
+      { id: "c1", type: "condition", label: "Can Cancel?", config: { condition: "order.status === 'pending' || order.status === 'processing'" }, x: 300, y: 440 },
+      { id: "a1", type: "action", label: "Cancel Order", config: { apiEndpoint: "/api/orders/cancel", method: "POST" }, x: 150, y: 560 },
+      { id: "r2", type: "response", label: "Cannot Cancel", config: { message: "This order has already shipped and cannot be cancelled. Would you like to initiate a return instead?" }, x: 450, y: 560 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 680 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "r1" },
+      { id: "e2", sourceId: "r1", targetId: "so1" },
+      { id: "e3", sourceId: "so1", targetId: "c1" },
+      { id: "e4", sourceId: "c1", targetId: "a1", label: "yes" },
+      { id: "e5", sourceId: "c1", targetId: "r2", label: "no" },
+      { id: "e6", sourceId: "a1", targetId: "e1" },
+    ],
+  },
+  {
+    id: "deflect_faq",
+    name: "FAQ Deflection",
+    description: "Answer common questions automatically to reduce agent workload",
+    category: "support",
+    subCategory: "deflect",
+    tags: ["popular"],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Greeting", config: { message: "👋 What can we help you with today?", quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"], detectCustomerId: true, customerIdSource: "session" }, x: 300, y: 50 },
+      { id: "gr1", type: "guardrail", label: "Content Filter", config: { blockedTopics: ["overall revenue", "profit margin", "internal pricing"] }, x: 600, y: 50 },
+      { id: "i1", type: "intent", label: "Detect FAQ", config: { intents: ["warranty_info", "delivery_time", "payment_methods", "store_hours"] }, x: 300, y: 180 },
+      { id: "r1", type: "response", label: "FAQ Answer", config: { message: "Here's the information you requested: {{faqAnswer}}" }, x: 300, y: 310 },
+      { id: "r2", type: "response", label: "Ask Satisfaction", config: { message: "Did that answer your question?" }, x: 300, y: 440 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 560 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "i1" },
+      { id: "e2", sourceId: "i1", targetId: "r1" },
+      { id: "e3", sourceId: "r1", targetId: "r2" },
+      { id: "e4", sourceId: "r2", targetId: "e1" },
+    ],
+  },
+  {
+    id: "lead_capture",
+    name: "Lead Capture",
+    description: "Capture visitor information for follow-up by sales team",
+    category: "leads",
+    subCategory: "capture",
+    tags: [],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Welcome", config: { message: "Welcome to Homelegance! Are you a dealer or looking for furniture for your home?" }, x: 300, y: 50 },
+      { id: "r1", type: "response", label: "Ask Contact", config: { message: "I'd love to help! Could you share your name and email so our team can follow up?" }, x: 300, y: 180 },
+      { id: "a1", type: "action", label: "Save Lead", config: { apiEndpoint: "/api/leads", method: "POST" }, x: 300, y: 310 },
+      { id: "r2", type: "response", label: "Confirm", config: { message: "Thank you! A member of our team will reach out shortly." }, x: 300, y: 440 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 560 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "r1" },
+      { id: "e2", sourceId: "r1", targetId: "a1" },
+      { id: "e3", sourceId: "a1", targetId: "r2" },
+      { id: "e4", sourceId: "r2", targetId: "e1" },
+    ],
+  },
+  {
+    id: "product_finder",
+    name: "Product Finder",
+    description: "Guide customers to find the right furniture with smart questions",
+    category: "sales",
+    subCategory: "product_search",
+    tags: ["popular"],
+    isActive: false,
+    nodes: [
+      { id: "g1", type: "greeting", label: "Welcome", config: { message: "Looking for furniture? I can help you find the perfect piece!" }, x: 300, y: 50 },
+      { id: "r1", type: "response", label: "Ask Category", config: { message: "What type of furniture are you looking for? (Bedroom, Living Room, Dining, etc.)" }, x: 300, y: 180 },
+      { id: "r2", type: "response", label: "Ask Style", config: { message: "What style do you prefer? (Modern, Traditional, Transitional)" }, x: 300, y: 310 },
+      { id: "a1", type: "action", label: "Search Products", config: { apiEndpoint: "/api/products/search", method: "POST" }, x: 300, y: 440 },
+      { id: "r3", type: "response", label: "Show Results", config: { message: "Here are some options that match your preferences:" }, x: 300, y: 560 },
+      { id: "e1", type: "end", label: "End", config: {}, x: 300, y: 680 },
+    ],
+    edges: [
+      { id: "e1", sourceId: "g1", targetId: "r1" },
+      { id: "e2", sourceId: "r1", targetId: "r2" },
+      { id: "e3", sourceId: "r2", targetId: "a1" },
+      { id: "e4", sourceId: "a1", targetId: "r3" },
+      { id: "e5", sourceId: "r3", targetId: "e1" },
+    ],
+  },
+];
+
+/* ─── View modes ─── */
+type ViewMode = "gallery" | "editor";
+
+/* ─── Flow Node Component ─── */
+function FlowNodeCard({
+  node, isSelected, onSelect, onDragStart,
+}: {
+  node: FlowNode; isSelected: boolean;
+  onSelect: () => void; onDragStart: (e: React.MouseEvent) => void;
+}) {
+  const typeConfig = NODE_TYPES[node.type];
+  return (
+    <div
+      className="absolute cursor-move select-none"
+      style={{ left: node.x, top: node.y, zIndex: isSelected ? 20 : 10 }}
+      onMouseDown={(e) => { e.stopPropagation(); onDragStart(e); onSelect(); }}
+    >
+      <div
+        className="w-44 rounded-xl border-2 shadow-sm transition-shadow hover:shadow-md overflow-hidden"
+        style={{
+          borderColor: isSelected ? typeConfig.color : "#e7e0d5",
+          background: "#fff",
+          boxShadow: isSelected ? `0 0 0 2px ${typeConfig.color}30` : undefined,
+        }}
+      >
+        <div className="px-3 py-2 flex items-center gap-2" style={{ background: typeConfig.bgColor }}>
+          <div style={{ color: typeConfig.color }}>{typeConfig.icon}</div>
+          <span className="text-[11px] font-bold truncate" style={{ color: typeConfig.color }}>{node.label}</span>
+        </div>
+        <div className="px-3 py-1.5">
+          <span className="text-[10px] capitalize" style={{ color: "#a8a29e" }}>{node.type.replace("_", " ")}</span>
+        </div>
+      </div>
+      {/* Connection dots */}
+      <div className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full border-2 bg-white" style={{ borderColor: typeConfig.color }} />
+      <div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full border-2 bg-white" style={{ borderColor: typeConfig.color }} />
+    </div>
+  );
+}
+
+/* ─── Edge SVG ─── */
+function FlowEdgeLine({ edge, nodes }: { edge: FlowEdge; nodes: FlowNode[] }) {
+  const source = nodes.find(n => n.id === edge.sourceId);
+  const target = nodes.find(n => n.id === edge.targetId);
+  if (!source || !target) return null;
+
+  const x1 = source.x + 88;
+  const y1 = source.y + 58;
+  const x2 = target.x + 88;
+  const y2 = target.y;
+  const midY = (y1 + y2) / 2;
+
+  return (
+    <g>
+      <path
+        d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`}
+        fill="none"
+        stroke="#d6d3d1"
+        strokeWidth={2}
+      />
+      {edge.label && (
+        <text
+          x={(x1 + x2) / 2}
+          y={midY - 6}
+          textAnchor="middle"
+          className="text-[9px] fill-stone-400"
+          style={{ fontFamily: "'Source Sans 3', sans-serif" }}
+        >
+          {edge.label}
+        </text>
+      )}
+    </g>
+  );
+}
+
+/* ─── AI Suggestions Panel ─── */
+function AISuggestionsPanel() {
+  const { data: suggestions, isLoading } = trpc.workflow.getSuggestions.useQuery({ workflowId: "default" });
+  const generateMut = trpc.workflow.generateSuggestions.useMutation({
+    onSuccess: () => toast.success("Suggestions generated"),
+    onError: (err) => toast.error(err.message),
+  });
+  const reviewMut = trpc.workflow.reviewSuggestion.useMutation({
+    onSuccess: () => toast.success("Suggestion updated"),
+  });
+
+  const pending = suggestions?.filter((s: any) => s.status === "pending" || s.status === "waiting") || [];
+
+  return (
+    <div className="p-3 space-y-2">
+      <div className="flex items-center justify-between">
+        <span className="text-[10px] font-bold uppercase tracking-wider" style={{ color: "#78716C" }}>AI Suggestions</span>
+        <button
+          onClick={() => generateMut.mutate({ workflowId: "default" })}
+          disabled={generateMut.isPending}
+          className="text-[10px] font-medium px-2 py-0.5 rounded-md transition-colors"
+          style={{ color: "#7c3aed", background: "#7c3aed14" }}
+        >
+          {generateMut.isPending ? "Analyzing..." : "Generate"}
+        </button>
+      </div>
+      {pending.length === 0 ? (
+        <p className="text-[10px] italic" style={{ color: "#d6d3d1" }}>No pending suggestions</p>
+      ) : (
+        pending.map((s: any) => (
+          <div key={s.id} className="p-2 rounded-lg border" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
+            <div className="flex items-center gap-1.5">
+              <Sparkles className="w-3 h-3" style={{ color: "#7c3aed" }} />
+              <span className="text-[11px] font-medium truncate" style={{ color: "#292524" }}>{s.label}</span>
+            </div>
+            <p className="text-[9px] mt-0.5" style={{ color: "#a8a29e" }}>{s.description}</p>
+            <div className="flex gap-1 mt-1.5">
+              <button
+                onClick={() => reviewMut.mutate({ suggestionId: s.id, status: "approved" })}
+                className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-medium"
+                style={{ background: "#14532D14", color: "#14532D" }}
+              >
+                <Check className="w-2.5 h-2.5" /> Approve
+              </button>
+              <button
+                onClick={() => reviewMut.mutate({ suggestionId: s.id, status: "waiting" })}
+                className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-medium"
+                style={{ background: "#ca8a0414", color: "#ca8a04" }}
+              >
+                <Clock className="w-2.5 h-2.5" /> Wait
+              </button>
+              <button
+                onClick={() => reviewMut.mutate({ suggestionId: s.id, status: "declined" })}
+                className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-medium"
+                style={{ background: "#dc262614", color: "#dc2626" }}
+              >
+                <XCircle className="w-2.5 h-2.5" /> Decline
+              </button>
+            </div>
+          </div>
+        ))
+      )}
+    </div>
+  );
+}
+
+/* ─── Main Component ─── */
+export default function WorkflowDesigner() {
+  const { user } = useAuth();
+  const [viewMode, setViewMode] = useState<ViewMode>("gallery");
+  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
+  const [selectedSubCategory, setSelectedSubCategory] = useState<string | null>(null);
+  const [activeTemplate, setActiveTemplate] = useState<FlowTemplate | null>(null);
+
+  // Editor state
+  const [nodes, setNodes] = useState<FlowNode[]>([]);
+  const [edges, setEdges] = useState<FlowEdge[]>([]);
+  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
+  const [dragState, setDragState] = useState<{ nodeId: string; offsetX: number; offsetY: number } | null>(null);
+  const canvasRef = useRef<HTMLDivElement>(null);
+
+  const saveWorkflow = trpc.workflow.save.useMutation({
+    onSuccess: () => toast.success("Workflow saved"),
+    onError: (err) => toast.error(err.message),
+  });
+
+  // Load workflow from server
+  const { data: savedWorkflow } = trpc.workflow.load.useQuery({ workflowId: "default" });
+
+  // Drag handlers
+  const handleDragStart = useCallback((nodeId: string, e: React.MouseEvent) => {
+    const node = nodes.find(n => n.id === nodeId);
+    if (!node) return;
+    setDragState({ nodeId, offsetX: e.clientX - node.x, offsetY: e.clientY - node.y });
+  }, [nodes]);
+
+  const handleMouseMove = useCallback((e: React.MouseEvent) => {
+    if (!dragState) return;
+    setNodes(prev => prev.map(n =>
+      n.id === dragState.nodeId ? { ...n, x: e.clientX - dragState.offsetX, y: e.clientY - dragState.offsetY } : n
+    ));
+  }, [dragState]);
+
+  const handleMouseUp = useCallback(() => setDragState(null), []);
+
+  const addNode = (type: NodeType) => {
+    const newNode: FlowNode = {
+      id: `n_${Date.now()}`,
+      type,
+      label: type.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase()),
+      config: type === "greeting" ? {
+        message: "👋 What can we help you with today?",
+        quickReplies: ["Orders", "Shipping", "Returning", "Cancelling"],
+        detectCustomerId: true,
+        customerIdSource: "session",
+      } : type === "guardrail" ? {
+        blockedTopics: ["overall revenue", "profit margin", "internal pricing", "employee data"],
+        blockedMessage: "I'm sorry, I can't share that information.",
+      } : {},
+      x: 300 + Math.random() * 100,
+      y: 100 + nodes.length * 80,
+    };
+    setNodes(prev => [...prev, newNode]);
+    setSelectedNodeId(newNode.id);
+  };
+
+  const deleteNode = (nodeId: string) => {
+    setNodes(prev => prev.filter(n => n.id !== nodeId));
+    setEdges(prev => prev.filter(e => e.sourceId !== nodeId && e.targetId !== nodeId));
+    if (selectedNodeId === nodeId) setSelectedNodeId(null);
+  };
+
+  const workflowId = activeTemplate?.id || "default";
+
+  const handleSave = () => {
+    saveWorkflow.mutate({
+      workflowId,
+      nodes: nodes.map(n => ({
+        workflowId,
+        nodeId: n.id,
+        type: n.type,
+        label: n.label,
+        config: n.config,
+        positionX: n.x,
+        positionY: n.y,
+      })),
+      edges: edges.map(e => ({
+        workflowId,
+        sourceNodeId: e.sourceId,
+        targetNodeId: e.targetId,
+        label: e.label,
+      })),
+    });
+  };
+
+  const loadTemplate = (template: FlowTemplate) => {
+    setNodes(template.nodes);
+    setEdges(template.edges);
+    setActiveTemplate(template);
+    setViewMode("editor");
+  };
+
+  // Filter templates
+  const filteredTemplates = useMemo(() => {
+    let templates = FLOW_TEMPLATES;
+    if (selectedCategory) templates = templates.filter(t => t.category === selectedCategory);
+    if (selectedSubCategory) templates = templates.filter(t => t.subCategory === selectedSubCategory);
+    return templates;
+  }, [selectedCategory, selectedSubCategory]);
+
+  const selectedCategoryObj = FLOW_CATEGORIES.find(c => c.id === selectedCategory);
+
+  return (
+    <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
+      {/* Top toolbar */}
+      <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+        <div className="flex items-center gap-3">
+          <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#14532D" }}>
+            <GitBranch className="w-3.5 h-3.5 text-white" />
+          </div>
+          <span className="text-sm font-bold" style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}>Workflow Designer</span>
+          {activeTemplate && viewMode === "editor" && (
+            <>
+              <ChevronRight className="w-3 h-3" style={{ color: "#a8a29e" }} />
+              <span className="text-xs font-medium" style={{ color: "#78716C" }}>{activeTemplate.name}</span>
+            </>
+          )}
+        </div>
+        <div className="flex items-center gap-2">
+          {viewMode === "editor" && (
+            <>
+              <Button onClick={() => { setViewMode("gallery"); setActiveTemplate(null); }} variant="outline" size="sm" className="text-xs">
+                <FolderOpen className="w-3 h-3 mr-1" /> Templates
+              </Button>
+              <Button onClick={handleSave} size="sm" className="text-xs text-white" style={{ background: "#14532D" }} disabled={saveWorkflow.isPending}>
+                <Save className="w-3 h-3 mr-1" /> {saveWorkflow.isPending ? "Saving..." : "Save"}
+              </Button>
+            </>
+          )}
+          {viewMode === "gallery" && (
+            <Button onClick={() => { setNodes([]); setEdges([]); setActiveTemplate(null); setViewMode("editor"); }} size="sm" className="text-xs text-white" style={{ background: "#14532D" }}>
+              <Plus className="w-3 h-3 mr-1" /> New Flow
+            </Button>
+          )}
+        </div>
+      </div>
+
+      {/* ─── Gallery View ─── */}
+      {viewMode === "gallery" && (
+        <div className="flex-1 flex min-h-0">
+          {/* Left sidebar — Categories */}
+          <div className="w-56 shrink-0 border-r overflow-y-auto" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+            <div className="p-3">
+              <h3 className="text-[10px] font-bold uppercase tracking-wider mb-2" style={{ color: "#a8a29e" }}>Categories</h3>
+              <button
+                onClick={() => { setSelectedCategory(null); setSelectedSubCategory(null); }}
+                className="w-full flex items-center gap-2 px-2 py-2 rounded-lg text-left mb-1 transition-colors"
+                style={{
+                  background: !selectedCategory ? "#14532D0A" : "transparent",
+                  color: !selectedCategory ? "#14532D" : "#78716C",
+                }}
+              >
+                <Layers className="w-4 h-4" />
+                <span className="text-[12px] font-medium">All Flows</span>
+                <span className="ml-auto text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: "#f5f0e8", color: "#a8a29e" }}>
+                  {FLOW_TEMPLATES.length}
+                </span>
+              </button>
+
+              {FLOW_CATEGORIES.map(cat => (
+                <div key={cat.id}>
+                  <button
+                    onClick={() => { setSelectedCategory(cat.id); setSelectedSubCategory(null); }}
+                    className="w-full flex items-center gap-2 px-2 py-2 rounded-lg text-left mb-0.5 transition-colors"
+                    style={{
+                      background: selectedCategory === cat.id ? `${cat.color}0A` : "transparent",
+                      color: selectedCategory === cat.id ? cat.color : "#78716C",
+                    }}
+                  >
+                    <div style={{ color: cat.color }}>{cat.icon}</div>
+                    <span className="text-[12px] font-medium">{cat.label}</span>
+                    <span className="ml-auto text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: "#f5f0e8", color: "#a8a29e" }}>
+                      {FLOW_TEMPLATES.filter(t => t.category === cat.id).length}
+                    </span>
+                  </button>
+
+                  {/* Sub-categories */}
+                  {selectedCategory === cat.id && (
+                    <div className="ml-4 mb-1 space-y-0.5">
+                      {cat.subCategories.map(sub => (
+                        <button
+                          key={sub.id}
+                          onClick={() => setSelectedSubCategory(selectedSubCategory === sub.id ? null : sub.id)}
+                          className="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-md text-left transition-colors"
+                          style={{
+                            background: selectedSubCategory === sub.id ? `${sub.color}0A` : "transparent",
+                            color: selectedSubCategory === sub.id ? sub.color : "#a8a29e",
+                          }}
+                        >
+                          <div style={{ color: sub.color }}>{sub.icon}</div>
+                          <span className="text-[11px] font-medium">{sub.label}</span>
+                        </button>
+                      ))}
+                    </div>
+                  )}
+                </div>
+              ))}
+            </div>
+          </div>
+
+          {/* Right — Template cards */}
+          <div className="flex-1 overflow-y-auto p-4" style={{ background: "#FFFBEB" }}>
+            {/* Category header */}
+            {selectedCategoryObj && (
+              <div className="mb-4 p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                <div className="flex items-center gap-3">
+                  <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: `${selectedCategoryObj.color}14`, color: selectedCategoryObj.color }}>
+                    {selectedCategoryObj.icon}
+                  </div>
+                  <div>
+                    <h2 className="text-base font-bold" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{selectedCategoryObj.label}</h2>
+                    <p className="text-xs" style={{ color: "#78716C" }}>{selectedCategoryObj.description}</p>
+                  </div>
+                </div>
+              </div>
+            )}
+
+            {/* Template grid */}
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
+              {filteredTemplates.map(template => {
+                const cat = FLOW_CATEGORIES.find(c => c.id === template.category);
+                const sub = cat?.subCategories.find(s => s.id === template.subCategory);
+                return (
+                  <div key={template.id} className="p-4 rounded-xl border transition-shadow hover:shadow-md" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                    <div className="flex items-start gap-3">
+                      <div className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style={{ background: sub ? `${sub.color}14` : "#f5f0e8", color: sub?.color || "#78716C" }}>
+                        {sub?.icon || <GitBranch className="w-4 h-4" />}
+                      </div>
+                      <div className="flex-1 min-w-0">
+                        <h3 className="text-sm font-semibold" style={{ color: "#292524" }}>{template.name}</h3>
+                        <p className="text-[11px] mt-0.5" style={{ color: "#78716C" }}>{template.description}</p>
+                        <div className="flex items-center gap-1.5 mt-2">
+                          {sub && (
+                            <span className="text-[9px] px-1.5 py-0.5 rounded-full capitalize" style={{ background: `${sub.color}14`, color: sub.color }}>
+                              {sub.label}
+                            </span>
+                          )}
+                          {template.tags.map(tag => (
+                            <span key={tag} className="text-[9px] px-1.5 py-0.5 rounded-full capitalize" style={{ background: "#f5f0e8", color: "#a8a29e" }}>
+                              {tag}
+                            </span>
+                          ))}
+                          <span className="text-[9px]" style={{ color: "#d6d3d1" }}>{template.nodes.length} nodes</span>
+                        </div>
+                      </div>
+                    </div>
+                    <div className="flex gap-2 mt-3 pt-3 border-t" style={{ borderColor: "#f5f0e8" }}>
+                      <Button onClick={() => loadTemplate(template)} size="sm" className="flex-1 text-xs text-white" style={{ background: sub?.color || "#14532D" }}>
+                        <Play className="w-3 h-3 mr-1" /> Use Template
+                      </Button>
+                      <Button onClick={() => { setActiveTemplate(template); setViewMode("editor"); setNodes(template.nodes); setEdges(template.edges); }} variant="outline" size="sm" className="text-xs">
+                        <Eye className="w-3 h-3 mr-1" /> Preview
+                      </Button>
+                    </div>
+                  </div>
+                );
+              })}
+
+              {filteredTemplates.length === 0 && (
+                <div className="col-span-2 text-center py-12">
+                  <FolderOpen className="w-10 h-10 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+                  <p className="text-sm" style={{ color: "#78716C" }}>No templates in this category yet</p>
+                  <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Create a new flow or select a different category</p>
+                </div>
+              )}
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* ─── Editor View ─── */}
+      {viewMode === "editor" && (
+        <div className="flex-1 flex min-h-0">
+          {/* Left sidebar — Node palette & AI suggestions */}
+          <div className="w-56 shrink-0 border-r overflow-y-auto" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+            <div className="p-3">
+              <h3 className="text-[10px] font-bold uppercase tracking-wider mb-2" style={{ color: "#a8a29e" }}>Add Node</h3>
+              <div className="space-y-1">
+                {(Object.entries(NODE_TYPES) as [NodeType, typeof NODE_TYPES[NodeType]][]).map(([type, config]) => (
+                  <button
+                    key={type}
+                    onClick={() => addNode(type)}
+                    className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-left hover:bg-black/5 transition-colors"
+                  >
+                    <div className="w-6 h-6 rounded-md flex items-center justify-center" style={{ background: config.bgColor, color: config.color }}>
+                      {config.icon}
+                    </div>
+                    <div className="min-w-0">
+                      <span className="text-[11px] font-medium capitalize block" style={{ color: "#292524" }}>{type.replace("_", " ")}</span>
+                    </div>
+                  </button>
+                ))}
+              </div>
+            </div>
+
+            {/* AI Suggestions */}
+            <div className="border-t" style={{ borderColor: "#e7e0d5" }}>
+              <AISuggestionsPanel />
+            </div>
+          </div>
+
+          {/* Canvas */}
+          <div
+            ref={canvasRef}
+            className="flex-1 relative overflow-auto cursor-crosshair"
+            style={{ background: "#FFFBEB", backgroundImage: "radial-gradient(circle, #e7e0d5 1px, transparent 1px)", backgroundSize: "24px 24px" }}
+            onMouseMove={handleMouseMove}
+            onMouseUp={handleMouseUp}
+            onMouseLeave={handleMouseUp}
+            onClick={() => setSelectedNodeId(null)}
+          >
+            {/* Edges SVG */}
+            <svg className="absolute inset-0 w-full h-full pointer-events-none" style={{ minWidth: 1200, minHeight: 800 }}>
+              {edges.map(edge => (
+                <FlowEdgeLine key={edge.id} edge={edge} nodes={nodes} />
+              ))}
+            </svg>
+
+            {/* Nodes */}
+            {nodes.map(node => (
+              <FlowNodeCard
+                key={node.id}
+                node={node}
+                isSelected={selectedNodeId === node.id}
+                onSelect={() => setSelectedNodeId(node.id)}
+                onDragStart={(e) => handleDragStart(node.id, e)}
+              />
+            ))}
+
+            {nodes.length === 0 && (
+              <div className="absolute inset-0 flex items-center justify-center">
+                <div className="text-center">
+                  <GitBranch className="w-12 h-12 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
+                  <p className="text-sm" style={{ color: "#78716C" }}>Empty canvas</p>
+                  <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Add nodes from the sidebar or load a template</p>
+                </div>
+              </div>
+            )}
+          </div>
+
+          {/* Right sidebar — Node properties */}
+          {selectedNodeId && (() => {
+            const node = nodes.find(n => n.id === selectedNodeId);
+            if (!node) return null;
+            const typeConfig = NODE_TYPES[node.type];
+            return (
+              <div className="w-64 shrink-0 border-l overflow-y-auto" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
+                <div className="p-3 border-b flex items-center justify-between" style={{ borderColor: "#e7e0d5" }}>
+                  <div className="flex items-center gap-2">
+                    <div style={{ color: typeConfig.color }}>{typeConfig.icon}</div>
+                    <span className="text-xs font-bold" style={{ color: "#292524" }}>{node.label}</span>
+                  </div>
+                  <button onClick={() => deleteNode(node.id)} className="p-1 rounded hover:bg-red-50">
+                    <Trash2 className="w-3 h-3" style={{ color: "#dc2626" }} />
+                  </button>
+                </div>
+                <div className="p-3 space-y-3">
+                  {/* Label */}
+                  <div>
+                    <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Label</label>
+                    <input
+                      type="text"
+                      value={node.label}
+                      onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, label: e.target.value } : n))}
+                      className="mt-1 w-full px-2 py-1.5 text-xs rounded-lg border"
+                      style={{ borderColor: "#e7e0d5" }}
+                    />
+                  </div>
+
+                  {/* Greeting-specific: Quick Replies */}
+                  {node.type === "greeting" && (
+                    <>
+                      <div>
+                        <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Message</label>
+                        <textarea
+                          value={node.config.message || ""}
+                          onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, message: e.target.value } } : n))}
+                          className="mt-1 w-full px-2 py-1.5 text-xs rounded-lg border resize-none"
+                          rows={2}
+                          style={{ borderColor: "#e7e0d5" }}
+                        />
+                      </div>
+                      <div>
+                        <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Quick Reply Buttons</label>
+                        <div className="mt-1 space-y-1">
+                          {(node.config.quickReplies || []).map((reply: string, i: number) => (
+                            <div key={i} className="flex items-center gap-1">
+                              <input
+                                type="text"
+                                value={reply}
+                                onChange={(e) => {
+                                  const updated = [...(node.config.quickReplies || [])];
+                                  updated[i] = e.target.value;
+                                  setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, quickReplies: updated } } : n));
+                                }}
+                                className="flex-1 px-2 py-1 text-[11px] rounded border"
+                                style={{ borderColor: "#e7e0d5" }}
+                              />
+                              <button
+                                onClick={() => {
+                                  const updated = (node.config.quickReplies || []).filter((_: any, idx: number) => idx !== i);
+                                  setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, quickReplies: updated } } : n));
+                                }}
+                                className="p-0.5 rounded hover:bg-red-50"
+                              >
+                                <X className="w-3 h-3" style={{ color: "#dc2626" }} />
+                              </button>
+                            </div>
+                          ))}
+                          <button
+                            onClick={() => {
+                              const updated = [...(node.config.quickReplies || []), "New Option"];
+                              setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, quickReplies: updated } } : n));
+                            }}
+                            className="text-[10px] font-medium px-2 py-1 rounded-md"
+                            style={{ color: "#14532D", background: "#14532D14" }}
+                          >
+                            + Add Button
+                          </button>
+                        </div>
+                      </div>
+                      <div className="flex items-center gap-2">
+                        <input
+                          type="checkbox"
+                          checked={node.config.detectCustomerId || false}
+                          onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, detectCustomerId: e.target.checked } } : n))}
+                          className="rounded"
+                        />
+                        <span className="text-[11px]" style={{ color: "#292524" }}>Auto-detect Customer ID</span>
+                      </div>
+                    </>
+                  )}
+
+                  {/* Guardrail-specific */}
+                  {node.type === "guardrail" && (
+                    <div>
+                      <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Blocked Topics</label>
+                      <textarea
+                        value={(node.config.blockedTopics || []).join("\n")}
+                        onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, blockedTopics: e.target.value.split("\n").filter(Boolean) } } : n))}
+                        className="mt-1 w-full px-2 py-1.5 text-xs rounded-lg border resize-none"
+                        rows={4}
+                        placeholder="One topic per line"
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </div>
+                  )}
+
+                  {/* Response message */}
+                  {(node.type === "response" || node.type === "escalation") && (
+                    <div>
+                      <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>Message</label>
+                      <textarea
+                        value={node.config.message || ""}
+                        onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, message: e.target.value } } : n))}
+                        className="mt-1 w-full px-2 py-1.5 text-xs rounded-lg border resize-none"
+                        rows={3}
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </div>
+                  )}
+
+                  {/* API endpoint for data nodes */}
+                  {(node.type === "customer_data" || node.type === "sales_order" || node.type === "action") && (
+                    <div>
+                      <label className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: "#a8a29e" }}>API Endpoint</label>
+                      <input
+                        type="text"
+                        value={node.config.apiEndpoint || ""}
+                        onChange={(e) => setNodes(prev => prev.map(n => n.id === node.id ? { ...n, config: { ...n.config, apiEndpoint: e.target.value } } : n))}
+                        className="mt-1 w-full px-2 py-1.5 text-xs rounded-lg border font-mono"
+                        style={{ borderColor: "#e7e0d5" }}
+                      />
+                    </div>
+                  )}
+
+                  <p className="text-[10px] italic" style={{ color: "#d6d3d1" }}>{typeConfig.description}</p>
+                </div>
+              </div>
+            );
+          })()}
+        </div>
+      )}
+    </div>
+  );
+}

+ 19 - 0
components.json

@@ -0,0 +1,19 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": false,
+  "tsx": true,
+  "tailwind": {
+    "css": "client/src/index.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  }
+}

+ 15 - 0
drizzle.config.ts

@@ -0,0 +1,15 @@
+import { defineConfig } from "drizzle-kit";
+
+const connectionString = process.env.DATABASE_URL;
+if (!connectionString) {
+  throw new Error("DATABASE_URL is required to run drizzle commands");
+}
+
+export default defineConfig({
+  schema: "./drizzle/schema.ts",
+  out: "./drizzle",
+  dialect: "postgresql",
+  dbCredentials: {
+    url: connectionString,
+  },
+});

+ 13 - 0
drizzle/0000_stormy_crystal.sql

@@ -0,0 +1,13 @@
+CREATE TABLE `users` (
+	`id` int AUTO_INCREMENT NOT NULL,
+	`openId` varchar(64) NOT NULL,
+	`name` text,
+	`email` varchar(320),
+	`loginMethod` varchar(64),
+	`role` enum('user','admin') NOT NULL DEFAULT 'user',
+	`createdAt` timestamp NOT NULL DEFAULT (now()),
+	`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
+	`lastSignedIn` timestamp NOT NULL DEFAULT (now()),
+	CONSTRAINT `users_id` PRIMARY KEY(`id`),
+	CONSTRAINT `users_openId_unique` UNIQUE(`openId`)
+);

+ 48 - 0
drizzle/0001_huge_sway.sql

@@ -0,0 +1,48 @@
+CREATE TABLE `conversations` (
+	`id` int AUTO_INCREMENT NOT NULL,
+	`sessionId` varchar(64) NOT NULL,
+	`visitorName` varchar(255),
+	`visitorEmail` varchar(320),
+	`status` enum('active','escalated','resolved','closed') NOT NULL DEFAULT 'active',
+	`assignedAgentId` int,
+	`metadata` json,
+	`createdAt` timestamp NOT NULL DEFAULT (now()),
+	`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
+	CONSTRAINT `conversations_id` PRIMARY KEY(`id`),
+	CONSTRAINT `conversations_sessionId_unique` UNIQUE(`sessionId`)
+);
+--> statement-breakpoint
+CREATE TABLE `messages` (
+	`id` int AUTO_INCREMENT NOT NULL,
+	`conversationId` int NOT NULL,
+	`sender` enum('visitor','bot','agent') NOT NULL,
+	`content` text NOT NULL,
+	`metadata` json,
+	`createdAt` timestamp NOT NULL DEFAULT (now()),
+	CONSTRAINT `messages_id` PRIMARY KEY(`id`)
+);
+--> statement-breakpoint
+CREATE TABLE `workflow_edges` (
+	`id` int AUTO_INCREMENT NOT NULL,
+	`workflowId` varchar(64) NOT NULL,
+	`sourceNodeId` varchar(64) NOT NULL,
+	`targetNodeId` varchar(64) NOT NULL,
+	`label` varchar(255),
+	`condition` json,
+	`createdAt` timestamp NOT NULL DEFAULT (now()),
+	CONSTRAINT `workflow_edges_id` PRIMARY KEY(`id`)
+);
+--> statement-breakpoint
+CREATE TABLE `workflow_nodes` (
+	`id` int AUTO_INCREMENT NOT NULL,
+	`workflowId` varchar(64) NOT NULL,
+	`nodeId` varchar(64) NOT NULL,
+	`type` enum('greeting','intent','response','condition','escalation','action','end') NOT NULL,
+	`label` varchar(255) NOT NULL,
+	`config` json,
+	`positionX` int NOT NULL DEFAULT 0,
+	`positionY` int NOT NULL DEFAULT 0,
+	`createdAt` timestamp NOT NULL DEFAULT (now()),
+	`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
+	CONSTRAINT `workflow_nodes_id` PRIMARY KEY(`id`)
+);

Some files were not shown because too many files changed in this diff