From a7e60ee5dd0b1c1a6eefd8663a0189e4c9d48768 Mon Sep 17 00:00:00 2001 From: Rezwana Karim <126201034+rezwana-karim@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:35:23 +0600 Subject: [PATCH 1/3] Add StormPilot chat UI, settings and routes Introduce StormPilot feature: add chat UI component, preferences form, pages for /stormpilot and /settings/stormpilot, and supporting lib (src/lib/stormpilot.ts). Protect /stormpilot in middleware and add sidebar navigation items. Implement streaming message handling, session management, model/capabilities loading, attachment upload and web-research context with graceful fallback. Also add generated audit artifacts (COMPREHENSIVE_API_INVENTORY_GENERATED.md and COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md) documenting API inventory and analysis. --- COMPREHENSIVE_API_INVENTORY_GENERATED.md | 344 ++++++ ...E_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md | 256 ++++ middleware.ts | 1 + src/app/settings/stormpilot/page.tsx | 51 + src/app/stormpilot/page.tsx | 50 + src/components/app-sidebar.tsx | 12 + .../stormpilot/stormpilot-chat-app.tsx | 1054 +++++++++++++++++ .../stormpilot-preferences-form.tsx | 328 +++++ src/lib/stormpilot.ts | 76 ++ 9 files changed, 2172 insertions(+) create mode 100644 COMPREHENSIVE_API_INVENTORY_GENERATED.md create mode 100644 COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md create mode 100644 src/app/settings/stormpilot/page.tsx create mode 100644 src/app/stormpilot/page.tsx create mode 100644 src/components/stormpilot/stormpilot-chat-app.tsx create mode 100644 src/components/stormpilot/stormpilot-preferences-form.tsx create mode 100644 src/lib/stormpilot.ts diff --git a/COMPREHENSIVE_API_INVENTORY_GENERATED.md b/COMPREHENSIVE_API_INVENTORY_GENERATED.md new file mode 100644 index 00000000..1697bd6e --- /dev/null +++ b/COMPREHENSIVE_API_INVENTORY_GENERATED.md @@ -0,0 +1,344 @@ +# API Inventory Snapshot + +Generated: 2026-03-19T03:40:57.9674518+06:00 +Total route handlers: 256 + +## Routes by domain +- admin: 31 routes +- integrations: 28 routes +- chat: 25 routes +- stores: 23 routes +- subscriptions: 16 routes +- shipping: 13 routes +- orders: 10 routes +- store: 9 routes +- webhooks: 8 routes +- products: 6 routes +- landing-pages: 6 routes +- inventory: 5 routes +- notifications: 5 routes +- analytics: 5 routes +- subscription: 4 routes +- checkout: 4 routes +- payments: 4 routes +- cart: 4 routes +- customers: 3 routes +- reviews: 3 routes +- coupons: 3 routes +- categories: 3 routes +- wishlist: 2 routes +- auth: 2 routes +- settings: 2 routes +- organizations: 2 routes +- brands: 2 routes +- media: 2 routes +- gdpr: 2 routes +- v1: 2 routes +- emails: 2 routes +- attributes: 2 routes +- store-staff: 2 routes +- tracking: 1 routes +- themes: 1 routes +- users: 1 routes +- subscription-plans: 1 routes +- webhook: 1 routes +- billing: 1 routes +- audit-logs: 1 routes +- search: 1 routes +- cron: 1 routes +- product-attributes: 1 routes +- permissions: 1 routes +- health: 1 routes +- fulfillments: 1 routes +- demo: 1 routes +- store-requests: 1 routes +- csrf-token: 1 routes + +## Chat + Ollama related routes +- [POST] /api/chat/actions/parse (src/app/api/chat/actions/parse/route.ts) +- [POST] /api/chat/assistant (src/app/api/chat/assistant/route.ts) +- [GET] /api/chat/capabilities (src/app/api/chat/capabilities/route.ts) +- [POST] /api/chat/embed (src/app/api/chat/embed/route.ts) +- [POST] /api/chat/generate (src/app/api/chat/generate/route.ts) +- [DELETE,GET] /api/chat/history (src/app/api/chat/history/route.ts) +- [POST] /api/chat/image-generate (src/app/api/chat/image-generate/route.ts) +- [DELETE,GET] /api/chat/messages (src/app/api/chat/messages/route.ts) +- [GET] /api/chat/models (src/app/api/chat/models/route.ts) +- [GET] /api/chat/models/[name] (src/app/api/chat/models/[name]/route.ts) +- [POST] /api/chat/models/manage (src/app/api/chat/models/manage/route.ts) +- [GET] /api/chat/models/running (src/app/api/chat/models/running/route.ts) +- [POST] /api/chat/ollama (src/app/api/chat/ollama/route.ts) +- [POST] /api/chat/openai/v1/chat/completions (src/app/api/chat/openai/v1/chat/completions/route.ts) +- [POST] /api/chat/openai/v1/embeddings (src/app/api/chat/openai/v1/embeddings/route.ts) +- [POST] /api/chat/openai/v1/images/generations (src/app/api/chat/openai/v1/images/generations/route.ts) +- [GET] /api/chat/openai/v1/models (src/app/api/chat/openai/v1/models/route.ts) +- [GET] /api/chat/openai/v1/models/[name] (src/app/api/chat/openai/v1/models/[name]/route.ts) +- [POST] /api/chat/semantic-search/products (src/app/api/chat/semantic-search/products/route.ts) +- [GET,POST] /api/chat/sessions (src/app/api/chat/sessions/route.ts) +- [DELETE,PATCH] /api/chat/sessions/[sessionId] (src/app/api/chat/sessions/[sessionId]/route.ts) +- [POST] /api/chat/tools/execute (src/app/api/chat/tools/execute/route.ts) +- [GET] /api/chat/usage (src/app/api/chat/usage/route.ts) +- [POST] /api/chat/webfetch (src/app/api/chat/webfetch/route.ts) +- [POST] /api/chat/websearch (src/app/api/chat/websearch/route.ts) +- [GET,PUT] /api/settings/ai (src/app/api/settings/ai/route.ts) +- [POST] /api/settings/ai/test (src/app/api/settings/ai/test/route.ts) +- [UNKNOWN] /api/v1/chat/completions (src/app/api/v1/chat/completions/route.ts) +- [GET] /api/v1/models (src/app/api/v1/models/route.ts) + +## Full route list +- [UNKNOWN] /api/admin/activity (src/app/api/admin/activity/route.ts) +- [GET] /api/admin/activity/export (src/app/api/admin/activity/export/route.ts) +- [GET] /api/admin/activity/platform (src/app/api/admin/activity/platform/route.ts) +- [GET] /api/admin/analytics (src/app/api/admin/analytics/route.ts) +- [POST] /api/admin/fix-broken-trials (src/app/api/admin/fix-broken-trials/route.ts) +- [GET,POST] /api/admin/plans (src/app/api/admin/plans/route.ts) +- [DELETE,GET,PATCH] /api/admin/plans/[id] (src/app/api/admin/plans/[id]/route.ts) +- [UNKNOWN] /api/admin/reports (src/app/api/admin/reports/route.ts) +- [GET] /api/admin/revenue (src/app/api/admin/revenue/route.ts) +- [UNKNOWN] /api/admin/role-requests (src/app/api/admin/role-requests/route.ts) +- [GET] /api/admin/role-requests/[id] (src/app/api/admin/role-requests/[id]/route.ts) +- [POST] /api/admin/role-requests/[id]/approve (src/app/api/admin/role-requests/[id]/approve/route.ts) +- [POST] /api/admin/role-requests/[id]/reject (src/app/api/admin/role-requests/[id]/reject/route.ts) +- [POST] /api/admin/role-requests/[id]/request-modification (src/app/api/admin/role-requests/[id]/request-modification/route.ts) +- [GET] /api/admin/setup-payment-configs (src/app/api/admin/setup-payment-configs/route.ts) +- [UNKNOWN] /api/admin/stats (src/app/api/admin/stats/route.ts) +- [GET] /api/admin/store-requests (src/app/api/admin/store-requests/route.ts) +- [UNKNOWN] /api/admin/store-requests/[id]/approve (src/app/api/admin/store-requests/[id]/approve/route.ts) +- [UNKNOWN] /api/admin/store-requests/[id]/reject (src/app/api/admin/store-requests/[id]/reject/route.ts) +- [UNKNOWN] /api/admin/stores (src/app/api/admin/stores/route.ts) +- [DELETE,GET,POST] /api/admin/stores/[storeId]/pathao/configure (src/app/api/admin/stores/[storeId]/pathao/configure/route.ts) +- [POST] /api/admin/stores/[storeId]/pathao/test (src/app/api/admin/stores/[storeId]/pathao/test/route.ts) +- [GET,POST] /api/admin/subscriptions (src/app/api/admin/subscriptions/route.ts) +- [GET] /api/admin/subscriptions/export (src/app/api/admin/subscriptions/export/route.ts) +- [UNKNOWN] /api/admin/system (src/app/api/admin/system/route.ts) +- [UNKNOWN] /api/admin/users (src/app/api/admin/users/route.ts) +- [UNKNOWN] /api/admin/users/[id] (src/app/api/admin/users/[id]/route.ts) +- [UNKNOWN] /api/admin/users/[id]/approve (src/app/api/admin/users/[id]/approve/route.ts) +- [UNKNOWN] /api/admin/users/[id]/reject (src/app/api/admin/users/[id]/reject/route.ts) +- [UNKNOWN] /api/admin/users/[id]/suspend (src/app/api/admin/users/[id]/suspend/route.ts) +- [UNKNOWN] /api/admin/users/pending (src/app/api/admin/users/pending/route.ts) +- [UNKNOWN] /api/analytics/customers (src/app/api/analytics/customers/route.ts) +- [UNKNOWN] /api/analytics/dashboard (src/app/api/analytics/dashboard/route.ts) +- [UNKNOWN] /api/analytics/products/top (src/app/api/analytics/products/top/route.ts) +- [UNKNOWN] /api/analytics/revenue (src/app/api/analytics/revenue/route.ts) +- [UNKNOWN] /api/analytics/sales (src/app/api/analytics/sales/route.ts) +- [UNKNOWN] /api/attributes (src/app/api/attributes/route.ts) +- [UNKNOWN] /api/attributes/[id] (src/app/api/attributes/[id]/route.ts) +- [UNKNOWN] /api/audit-logs (src/app/api/audit-logs/route.ts) +- [UNKNOWN] /api/auth/[...nextauth] (src/app/api/auth/[...nextauth]/route.ts) +- [UNKNOWN] /api/auth/signup (src/app/api/auth/signup/route.ts) +- [GET] /api/billing/history (src/app/api/billing/history/route.ts) +- [UNKNOWN] /api/brands (src/app/api/brands/route.ts) +- [UNKNOWN] /api/brands/[slug] (src/app/api/brands/[slug]/route.ts) +- [UNKNOWN] /api/cart (src/app/api/cart/route.ts) +- [UNKNOWN] /api/cart/[id] (src/app/api/cart/[id]/route.ts) +- [UNKNOWN] /api/cart/count (src/app/api/cart/count/route.ts) +- [UNKNOWN] /api/cart/validate (src/app/api/cart/validate/route.ts) +- [UNKNOWN] /api/categories (src/app/api/categories/route.ts) +- [UNKNOWN] /api/categories/[slug] (src/app/api/categories/[slug]/route.ts) +- [UNKNOWN] /api/categories/tree (src/app/api/categories/tree/route.ts) +- [POST] /api/chat/actions/parse (src/app/api/chat/actions/parse/route.ts) +- [POST] /api/chat/assistant (src/app/api/chat/assistant/route.ts) +- [GET] /api/chat/capabilities (src/app/api/chat/capabilities/route.ts) +- [POST] /api/chat/embed (src/app/api/chat/embed/route.ts) +- [POST] /api/chat/generate (src/app/api/chat/generate/route.ts) +- [DELETE,GET] /api/chat/history (src/app/api/chat/history/route.ts) +- [POST] /api/chat/image-generate (src/app/api/chat/image-generate/route.ts) +- [DELETE,GET] /api/chat/messages (src/app/api/chat/messages/route.ts) +- [GET] /api/chat/models (src/app/api/chat/models/route.ts) +- [GET] /api/chat/models/[name] (src/app/api/chat/models/[name]/route.ts) +- [POST] /api/chat/models/manage (src/app/api/chat/models/manage/route.ts) +- [GET] /api/chat/models/running (src/app/api/chat/models/running/route.ts) +- [POST] /api/chat/ollama (src/app/api/chat/ollama/route.ts) +- [POST] /api/chat/openai/v1/chat/completions (src/app/api/chat/openai/v1/chat/completions/route.ts) +- [POST] /api/chat/openai/v1/embeddings (src/app/api/chat/openai/v1/embeddings/route.ts) +- [POST] /api/chat/openai/v1/images/generations (src/app/api/chat/openai/v1/images/generations/route.ts) +- [GET] /api/chat/openai/v1/models (src/app/api/chat/openai/v1/models/route.ts) +- [GET] /api/chat/openai/v1/models/[name] (src/app/api/chat/openai/v1/models/[name]/route.ts) +- [POST] /api/chat/semantic-search/products (src/app/api/chat/semantic-search/products/route.ts) +- [GET,POST] /api/chat/sessions (src/app/api/chat/sessions/route.ts) +- [DELETE,PATCH] /api/chat/sessions/[sessionId] (src/app/api/chat/sessions/[sessionId]/route.ts) +- [POST] /api/chat/tools/execute (src/app/api/chat/tools/execute/route.ts) +- [GET] /api/chat/usage (src/app/api/chat/usage/route.ts) +- [POST] /api/chat/webfetch (src/app/api/chat/webfetch/route.ts) +- [POST] /api/chat/websearch (src/app/api/chat/websearch/route.ts) +- [UNKNOWN] /api/checkout/complete (src/app/api/checkout/complete/route.ts) +- [UNKNOWN] /api/checkout/payment-intent (src/app/api/checkout/payment-intent/route.ts) +- [UNKNOWN] /api/checkout/shipping (src/app/api/checkout/shipping/route.ts) +- [UNKNOWN] /api/checkout/validate (src/app/api/checkout/validate/route.ts) +- [UNKNOWN] /api/coupons (src/app/api/coupons/route.ts) +- [UNKNOWN] /api/coupons/[id] (src/app/api/coupons/[id]/route.ts) +- [UNKNOWN] /api/coupons/validate (src/app/api/coupons/validate/route.ts) +- [POST] /api/cron/subscriptions (src/app/api/cron/subscriptions/route.ts) +- [UNKNOWN] /api/csrf-token (src/app/api/csrf-token/route.ts) +- [UNKNOWN] /api/customers (src/app/api/customers/route.ts) +- [UNKNOWN] /api/customers/[id] (src/app/api/customers/[id]/route.ts) +- [UNKNOWN] /api/customers/export (src/app/api/customers/export/route.ts) +- [POST] /api/demo/create-store (src/app/api/demo/create-store/route.ts) +- [UNKNOWN] /api/emails/send (src/app/api/emails/send/route.ts) +- [UNKNOWN] /api/emails/templates (src/app/api/emails/templates/route.ts) +- [UNKNOWN] /api/fulfillments/[fulfillmentId] (src/app/api/fulfillments/[fulfillmentId]/route.ts) +- [UNKNOWN] /api/gdpr/delete (src/app/api/gdpr/delete/route.ts) +- [UNKNOWN] /api/gdpr/export (src/app/api/gdpr/export/route.ts) +- [GET] /api/health (src/app/api/health/route.ts) +- [UNKNOWN] /api/integrations (src/app/api/integrations/route.ts) +- [UNKNOWN] /api/integrations/[id] (src/app/api/integrations/[id]/route.ts) +- [GET] /api/integrations/facebook/analytics (src/app/api/integrations/facebook/analytics/route.ts) +- [POST] /api/integrations/facebook/catalog (src/app/api/integrations/facebook/catalog/route.ts) +- [GET] /api/integrations/facebook/checkout (src/app/api/integrations/facebook/checkout/route.ts) +- [GET,POST,PUT] /api/integrations/facebook/conversions (src/app/api/integrations/facebook/conversions/route.ts) +- [GET,POST] /api/integrations/facebook/conversions/retry (src/app/api/integrations/facebook/conversions/retry/route.ts) +- [POST] /api/integrations/facebook/disconnect (src/app/api/integrations/facebook/disconnect/route.ts) +- [GET] /api/integrations/facebook/feed (src/app/api/integrations/facebook/feed/route.ts) +- [GET,POST] /api/integrations/facebook/messages (src/app/api/integrations/facebook/messages/route.ts) +- [GET] /api/integrations/facebook/messages/[conversationId] (src/app/api/integrations/facebook/messages/[conversationId]/route.ts) +- [PATCH] /api/integrations/facebook/messages/[conversationId]/read (src/app/api/integrations/facebook/messages/[conversationId]/read/route.ts) +- [GET] /api/integrations/facebook/oauth/callback (src/app/api/integrations/facebook/oauth/callback/route.ts) +- [GET] /api/integrations/facebook/oauth/connect (src/app/api/integrations/facebook/oauth/connect/route.ts) +- [GET,POST] /api/integrations/facebook/orders (src/app/api/integrations/facebook/orders/route.ts) +- [POST] /api/integrations/facebook/orders/[orderId]/sync-cancellation (src/app/api/integrations/facebook/orders/[orderId]/sync-cancellation/route.ts) +- [POST] /api/integrations/facebook/orders/[orderId]/sync-refund (src/app/api/integrations/facebook/orders/[orderId]/sync-refund/route.ts) +- [POST] /api/integrations/facebook/orders/[orderId]/sync-shipment (src/app/api/integrations/facebook/orders/[orderId]/sync-shipment/route.ts) +- [POST] /api/integrations/facebook/orders/[orderId]/sync-status (src/app/api/integrations/facebook/orders/[orderId]/sync-status/route.ts) +- [GET,POST] /api/integrations/facebook/orders/poll (src/app/api/integrations/facebook/orders/poll/route.ts) +- [POST] /api/integrations/facebook/orders/sync (src/app/api/integrations/facebook/orders/sync/route.ts) +- [GET,POST] /api/integrations/facebook/products/batch-status (src/app/api/integrations/facebook/products/batch-status/route.ts) +- [POST] /api/integrations/facebook/products/sync (src/app/api/integrations/facebook/products/sync/route.ts) +- [GET,PATCH] /api/integrations/facebook/settings (src/app/api/integrations/facebook/settings/route.ts) +- [GET,POST] /api/integrations/facebook/status (src/app/api/integrations/facebook/status/route.ts) +- [POST] /api/integrations/facebook/webhooks/subscribe (src/app/api/integrations/facebook/webhooks/subscribe/route.ts) +- [DELETE,GET,POST] /api/integrations/sslcommerz (src/app/api/integrations/sslcommerz/route.ts) +- [POST] /api/integrations/sslcommerz/test (src/app/api/integrations/sslcommerz/test/route.ts) +- [UNKNOWN] /api/inventory (src/app/api/inventory/route.ts) +- [UNKNOWN] /api/inventory/adjust (src/app/api/inventory/adjust/route.ts) +- [UNKNOWN] /api/inventory/bulk (src/app/api/inventory/bulk/route.ts) +- [UNKNOWN] /api/inventory/history (src/app/api/inventory/history/route.ts) +- [UNKNOWN] /api/inventory/low-stock (src/app/api/inventory/low-stock/route.ts) +- [GET,POST] /api/landing-pages (src/app/api/landing-pages/route.ts) +- [DELETE,GET,PATCH] /api/landing-pages/[id] (src/app/api/landing-pages/[id]/route.ts) +- [POST] /api/landing-pages/[id]/duplicate (src/app/api/landing-pages/[id]/duplicate/route.ts) +- [DELETE,POST] /api/landing-pages/[id]/publish (src/app/api/landing-pages/[id]/publish/route.ts) +- [POST] /api/landing-pages/[id]/track (src/app/api/landing-pages/[id]/track/route.ts) +- [GET] /api/landing-pages/templates (src/app/api/landing-pages/templates/route.ts) +- [DELETE,GET] /api/media/list (src/app/api/media/list/route.ts) +- [POST] /api/media/upload (src/app/api/media/upload/route.ts) +- [UNKNOWN] /api/notifications (src/app/api/notifications/route.ts) +- [UNKNOWN] /api/notifications/[id] (src/app/api/notifications/[id]/route.ts) +- [UNKNOWN] /api/notifications/[id]/read (src/app/api/notifications/[id]/read/route.ts) +- [UNKNOWN] /api/notifications/mark-all-read (src/app/api/notifications/mark-all-read/route.ts) +- [UNKNOWN] /api/notifications/read (src/app/api/notifications/read/route.ts) +- [GET,POST] /api/orders (src/app/api/orders/route.ts) +- [DELETE,GET,PATCH] /api/orders/[id] (src/app/api/orders/[id]/route.ts) +- [UNKNOWN] /api/orders/[id]/cancel (src/app/api/orders/[id]/cancel/route.ts) +- [UNKNOWN] /api/orders/[id]/fulfillments (src/app/api/orders/[id]/fulfillments/route.ts) +- [UNKNOWN] /api/orders/[id]/invoice (src/app/api/orders/[id]/invoice/route.ts) +- [UNKNOWN] /api/orders/[id]/refund (src/app/api/orders/[id]/refund/route.ts) +- [UNKNOWN] /api/orders/[id]/status (src/app/api/orders/[id]/status/route.ts) +- [GET] /api/orders/check-updates (src/app/api/orders/check-updates/route.ts) +- [GET] /api/orders/stream (src/app/api/orders/stream/route.ts) +- [POST] /api/orders/track (src/app/api/orders/track/route.ts) +- [UNKNOWN] /api/organizations (src/app/api/organizations/route.ts) +- [UNKNOWN] /api/organizations/[slug]/invite (src/app/api/organizations/[slug]/invite/route.ts) +- [GET,POST] /api/payments/configurations (src/app/api/payments/configurations/route.ts) +- [POST] /api/payments/configurations/toggle (src/app/api/payments/configurations/toggle/route.ts) +- [POST] /api/payments/sslcommerz/initiate (src/app/api/payments/sslcommerz/initiate/route.ts) +- [GET] /api/payments/transactions (src/app/api/payments/transactions/route.ts) +- [UNKNOWN] /api/permissions (src/app/api/permissions/route.ts) +- [UNKNOWN] /api/product-attributes (src/app/api/product-attributes/route.ts) +- [UNKNOWN] /api/products (src/app/api/products/route.ts) +- [UNKNOWN] /api/products/[id] (src/app/api/products/[id]/route.ts) +- [UNKNOWN] /api/products/[id]/reviews (src/app/api/products/[id]/reviews/route.ts) +- [UNKNOWN] /api/products/[id]/store (src/app/api/products/[id]/store/route.ts) +- [UNKNOWN] /api/products/import (src/app/api/products/import/route.ts) +- [UNKNOWN] /api/products/upload (src/app/api/products/upload/route.ts) +- [UNKNOWN] /api/reviews (src/app/api/reviews/route.ts) +- [UNKNOWN] /api/reviews/[id] (src/app/api/reviews/[id]/route.ts) +- [UNKNOWN] /api/reviews/[id]/approve (src/app/api/reviews/[id]/approve/route.ts) +- [UNKNOWN] /api/search (src/app/api/search/route.ts) +- [GET,PUT] /api/settings/ai (src/app/api/settings/ai/route.ts) +- [POST] /api/settings/ai/test (src/app/api/settings/ai/test/route.ts) +- [GET] /api/shipping/pathao/areas/[zoneId] (src/app/api/shipping/pathao/areas/[zoneId]/route.ts) +- [GET] /api/shipping/pathao/auth (src/app/api/shipping/pathao/auth/route.ts) +- [POST] /api/shipping/pathao/calculate-price (src/app/api/shipping/pathao/calculate-price/route.ts) +- [GET] /api/shipping/pathao/cities (src/app/api/shipping/pathao/cities/route.ts) +- [POST] /api/shipping/pathao/create (src/app/api/shipping/pathao/create/route.ts) +- [GET] /api/shipping/pathao/label/[consignmentId] (src/app/api/shipping/pathao/label/[consignmentId]/route.ts) +- [POST] /api/shipping/pathao/price (src/app/api/shipping/pathao/price/route.ts) +- [GET] /api/shipping/pathao/shipments (src/app/api/shipping/pathao/shipments/route.ts) +- [GET] /api/shipping/pathao/stores (src/app/api/shipping/pathao/stores/route.ts) +- [GET] /api/shipping/pathao/track (src/app/api/shipping/pathao/track/route.ts) +- [GET] /api/shipping/pathao/track/[consignmentId] (src/app/api/shipping/pathao/track/[consignmentId]/route.ts) +- [GET] /api/shipping/pathao/zones/[cityId] (src/app/api/shipping/pathao/zones/[cityId]/route.ts) +- [UNKNOWN] /api/shipping/rates (src/app/api/shipping/rates/route.ts) +- [UNKNOWN] /api/store-requests (src/app/api/store-requests/route.ts) +- [UNKNOWN] /api/store-staff (src/app/api/store-staff/route.ts) +- [UNKNOWN] /api/store-staff/[id] (src/app/api/store-staff/[id]/route.ts) +- [GET] /api/store/[slug] (src/app/api/store/[slug]/route.ts) +- [POST] /api/store/[slug]/cart/validate (src/app/api/store/[slug]/cart/validate/route.ts) +- [POST] /api/store/[slug]/coupons/validate (src/app/api/store/[slug]/coupons/validate/route.ts) +- [UNKNOWN] /api/store/[slug]/orders (src/app/api/store/[slug]/orders/route.ts) +- [GET] /api/store/[slug]/orders/[orderId] (src/app/api/store/[slug]/orders/[orderId]/route.ts) +- [GET] /api/store/[slug]/orders/[orderId]/invoice (src/app/api/store/[slug]/orders/[orderId]/invoice/route.ts) +- [GET,POST] /api/store/[slug]/orders/[orderId]/verify-payment (src/app/api/store/[slug]/orders/[orderId]/verify-payment/route.ts) +- [GET] /api/store/[slug]/orders/track (src/app/api/store/[slug]/orders/track/route.ts) +- [GET] /api/store/[slug]/payment-methods (src/app/api/store/[slug]/payment-methods/route.ts) +- [UNKNOWN] /api/stores (src/app/api/stores/route.ts) +- [UNKNOWN] /api/stores/[id] (src/app/api/stores/[id]/route.ts) +- [GET] /api/stores/[id]/custom-roles (src/app/api/stores/[id]/custom-roles/route.ts) +- [UNKNOWN] /api/stores/[id]/domain (src/app/api/stores/[id]/domain/route.ts) +- [UNKNOWN] /api/stores/[id]/domain/verify (src/app/api/stores/[id]/domain/verify/route.ts) +- [GET] /api/stores/[id]/manifest (src/app/api/stores/[id]/manifest/route.ts) +- [GET,PATCH] /api/stores/[id]/pathao/settings (src/app/api/stores/[id]/pathao/settings/route.ts) +- [GET] /api/stores/[id]/pwa (src/app/api/stores/[id]/pwa/route.ts) +- [UNKNOWN] /api/stores/[id]/role-requests (src/app/api/stores/[id]/role-requests/route.ts) +- [UNKNOWN] /api/stores/[id]/role-requests/[requestId] (src/app/api/stores/[id]/role-requests/[requestId]/route.ts) +- [UNKNOWN] /api/stores/[id]/settings (src/app/api/stores/[id]/settings/route.ts) +- [UNKNOWN] /api/stores/[id]/staff (src/app/api/stores/[id]/staff/route.ts) +- [UNKNOWN] /api/stores/[id]/staff/[staffId] (src/app/api/stores/[id]/staff/[staffId]/route.ts) +- [UNKNOWN] /api/stores/[id]/staff/accept-invite (src/app/api/stores/[id]/staff/accept-invite/route.ts) +- [UNKNOWN] /api/stores/[id]/stats (src/app/api/stores/[id]/stats/route.ts) +- [GET,PATCH,PUT] /api/stores/[id]/storefront (src/app/api/stores/[id]/storefront/route.ts) +- [DELETE,GET,PUT] /api/stores/[id]/storefront/draft (src/app/api/stores/[id]/storefront/draft/route.ts) +- [POST] /api/stores/[id]/storefront/publish (src/app/api/stores/[id]/storefront/publish/route.ts) +- [GET,POST] /api/stores/[id]/storefront/versions (src/app/api/stores/[id]/storefront/versions/route.ts) +- [GET] /api/stores/[id]/sw (src/app/api/stores/[id]/sw/route.ts) +- [UNKNOWN] /api/stores/[id]/theme (src/app/api/stores/[id]/theme/route.ts) +- [GET,PATCH] /api/stores/current/pathao-config (src/app/api/stores/current/pathao-config/route.ts) +- [UNKNOWN] /api/stores/lookup (src/app/api/stores/lookup/route.ts) +- [GET] /api/subscription-plans (src/app/api/subscription-plans/route.ts) +- [POST] /api/subscription/extend-grace-period (src/app/api/subscription/extend-grace-period/route.ts) +- [GET] /api/subscription/grace-period-status (src/app/api/subscription/grace-period-status/route.ts) +- [GET] /api/subscription/plans (src/app/api/subscription/plans/route.ts) +- [GET] /api/subscription/trial-status (src/app/api/subscription/trial-status/route.ts) +- [UNKNOWN] /api/subscriptions (src/app/api/subscriptions/route.ts) +- [UNKNOWN] /api/subscriptions/[id] (src/app/api/subscriptions/[id]/route.ts) +- [PATCH] /api/subscriptions/cancel (src/app/api/subscriptions/cancel/route.ts) +- [GET] /api/subscriptions/current (src/app/api/subscriptions/current/route.ts) +- [POST] /api/subscriptions/downgrade (src/app/api/subscriptions/downgrade/route.ts) +- [POST] /api/subscriptions/init-trial (src/app/api/subscriptions/init-trial/route.ts) +- [GET] /api/subscriptions/plans (src/app/api/subscriptions/plans/route.ts) +- [POST] /api/subscriptions/renew (src/app/api/subscriptions/renew/route.ts) +- [GET,POST] /api/subscriptions/sslcommerz/cancel (src/app/api/subscriptions/sslcommerz/cancel/route.ts) +- [GET,POST] /api/subscriptions/sslcommerz/fail (src/app/api/subscriptions/sslcommerz/fail/route.ts) +- [POST] /api/subscriptions/sslcommerz/ipn (src/app/api/subscriptions/sslcommerz/ipn/route.ts) +- [GET,POST] /api/subscriptions/sslcommerz/success (src/app/api/subscriptions/sslcommerz/success/route.ts) +- [UNKNOWN] /api/subscriptions/status (src/app/api/subscriptions/status/route.ts) +- [UNKNOWN] /api/subscriptions/subscribe (src/app/api/subscriptions/subscribe/route.ts) +- [POST] /api/subscriptions/upgrade (src/app/api/subscriptions/upgrade/route.ts) +- [POST] /api/subscriptions/webhook (src/app/api/subscriptions/webhook/route.ts) +- [UNKNOWN] /api/themes (src/app/api/themes/route.ts) +- [GET,POST] /api/tracking (src/app/api/tracking/route.ts) +- [UNKNOWN] /api/users/[id]/profile (src/app/api/users/[id]/profile/route.ts) +- [UNKNOWN] /api/v1/chat/completions (src/app/api/v1/chat/completions/route.ts) +- [GET] /api/v1/models (src/app/api/v1/models/route.ts) +- [POST] /api/webhook/payment (src/app/api/webhook/payment/route.ts) +- [UNKNOWN] /api/webhooks (src/app/api/webhooks/route.ts) +- [UNKNOWN] /api/webhooks/[id] (src/app/api/webhooks/[id]/route.ts) +- [GET,POST] /api/webhooks/facebook (src/app/api/webhooks/facebook/route.ts) +- [POST] /api/webhooks/pathao (src/app/api/webhooks/pathao/route.ts) +- [GET,POST] /api/webhooks/sslcommerz/cancel (src/app/api/webhooks/sslcommerz/cancel/route.ts) +- [GET,POST] /api/webhooks/sslcommerz/fail (src/app/api/webhooks/sslcommerz/fail/route.ts) +- [POST] /api/webhooks/sslcommerz/ipn (src/app/api/webhooks/sslcommerz/ipn/route.ts) +- [GET,POST] /api/webhooks/sslcommerz/success (src/app/api/webhooks/sslcommerz/success/route.ts) +- [UNKNOWN] /api/wishlist (src/app/api/wishlist/route.ts) +- [UNKNOWN] /api/wishlist/[id] (src/app/api/wishlist/[id]/route.ts) diff --git a/COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md b/COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md new file mode 100644 index 00000000..0d24614b --- /dev/null +++ b/COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md @@ -0,0 +1,256 @@ +# Comprehensive Review Analysis — StormCom API, Ollama Chat, and StormPilot UX + +Date: 2026-03-19 +Branch: `copilot/build-cloud-chat-interface` + +## 1) Scope & Method + +This review covered: + +1. **Project-wide API landscape** (all `src/app/api/**/route.ts` handlers) +2. **Ollama AI chat backend** (schema, routes, security, tenancy, rate limit, OpenAI compatibility) +3. **Official Ollama Cloud/API docs online** (docs.ollama.com via web fetch) +4. **New UX/UI design and implementation** for a separate chat experience named **StormPilot** +5. **Browser validation** with business-owner style workflows (login, session management, prompting, settings, attachments) + +--- + +## 2) Project-wide API Audit Summary + +Generated inventory file: `COMPREHENSIVE_API_INVENTORY_GENERATED.md` + +### Key inventory numbers +- **Total route handlers:** 256 +- Highest-volume domains: + - `admin` (31) + - `integrations` (28) + - `chat` (25) + - `stores` (23) + - `subscriptions` (16) + +### API architecture observations +- Large modular API surface organized by business domains (admin, commerce, subscriptions, stores, integrations, chat). +- Strong use of Next.js App Router route handlers with clear domain folders. +- Chat domain is a substantial subsystem, not a thin wrapper. + +### Security pattern observations (cross-cutting) +- Broad use of session-authenticated routes (`getServerSession(authOptions)` patterns). +- Route-level permission/ownership checks exist in many domains. +- Multi-tenant patterns are present across store/org-aware routes. + +> Full endpoint list is intentionally generated into `COMPREHENSIVE_API_INVENTORY_GENERATED.md` for traceability and future audits. + +--- + +## 3) Ollama Chat System — DB Schema Deep Dive + +Primary schema file: `prisma/schema.prisma` + +### Core AI/Chat models + +#### `OllamaConfig` +Stores user/org/store scoped model runtime configuration: +- `userId`, `organizationId`, `storeId` scope +- `host`, `model`, `temperature`, `systemPrompt` +- `apiKeyEncrypted` (encrypted at rest) +- `isCloudMode`, `isActive` +- usage/test metadata (`lastTestAt`, `lastUsedAt`) + +#### `ChatSession` +Conversation container with tenancy & lifecycle metadata: +- `userId`, `organizationId`, `storeId` +- `title`, `summary` +- `isArchived`, `isPinned` +- `lastMessageAt`, `totalMessages` +- `metadata` JSON + +#### `ChatMessage` +Per-message records with inference telemetry: +- `role` (`USER`, `ASSISTANT`, `SYSTEM`, `TOOL`) +- `content`, optional `thinking` +- `model`, token metrics (`promptTokens`, `completionTokens`, `totalTokens`) +- `executionTimeMs`, `toolCalls` JSON +- linked `attachments` + +#### `ChatAttachment` +Attachment metadata (uploaded/generated/external) linked to `ChatMessage`. + +#### `ChatUsageLog` +Usage/accounting table with model/action/type/token/cost dimensions. + +### Schema strengths +- Good normalized separation of config/session/message/attachment/usage. +- Strong basis for observability and cost tracking. +- Multi-tenant identifiers are embedded into session-level records. + +--- + +## 4) Ollama Chat API Deep Dive (In-Project) + +### Core chat routes +- `/api/chat/ollama` — primary chat endpoint (streaming NDJSON, sessions, attachments, model routing) +- `/api/chat/sessions` and `/api/chat/sessions/[sessionId]` — session lifecycle +- `/api/chat/messages` — message retrieval and cleanup +- `/api/chat/history` — compatibility/history operations +- `/api/chat/usage` — usage stats + +### Capability & model routes +- `/api/chat/models` +- `/api/chat/models/[name]` +- `/api/chat/models/running` +- `/api/chat/models/manage` +- `/api/chat/capabilities` + +### Advanced/feature routes +- `/api/chat/embed` +- `/api/chat/image-generate` +- `/api/chat/semantic-search/products` +- `/api/chat/tools/execute` +- `/api/chat/websearch` +- `/api/chat/webfetch` +- `/api/chat/actions/parse` + +### OpenAI compatibility routes +- `/api/chat/openai/v1/chat/completions` +- `/api/chat/openai/v1/embeddings` +- `/api/chat/openai/v1/images/generations` +- `/api/chat/openai/v1/models` +- `/api/chat/openai/v1/models/[name]` +- plus wrappers: + - `/api/v1/chat/completions` + - `/api/v1/models` + +### Security/tenancy/reliability controls found +- Session auth checks on chat routes. +- Tenant resolution via `resolveChatTenantContext(...)`. +- Session ownership and tenant checks in message/session operations. +- Rate limiting in primary chat flow (`enforceChatRateLimit`). +- Input sanitization pattern in `ollama` route. +- Attachment validation (count/type/size) in `ollama` route. + +### Notable implementation details +- `src/lib/ollama.ts` centralizes client construction and config resolution (DB → env fallback). +- OpenAI-compatible endpoints are implemented server-side with model translation. + +--- + +## 5) Official Ollama Cloud/API Documentation Audit (Online) + +Sources fetched from docs.ollama.com included: +- API overview (`/api`) +- Authentication (`/api/authentication`) +- chat/generate/embed/tags/show/ps/create/pull/push/copy/delete pages +- OpenAI compatibility (`/openai`) +- capabilities pages (tool-calling, structured outputs, web-search/fetch) + +### Confirmed endpoint families (official docs) +- Native API patterns: + - `POST /api/chat` + - `POST /api/generate` + - `POST /api/embed` + - `GET /api/tags` + - `POST /api/show` + - model management endpoints for create/pull/push/copy/delete +- OpenAI compatibility patterns: + - `/v1/chat/completions` + - `/v1/embeddings` + - `/v1/models` (and model details) +- Cloud auth patterns documented with API key usage. + +### Practical finding from live app +- Web-search helper route can return upstream `502` in current environment. +- StormPilot now handles this gracefully with explicit fallback messaging and anti-hallucination instruction. + +--- + +## 6) StormPilot UX Strategy (Business Owner Lens) + +Design principle: **Keep main chat simple and action-oriented; move tuning/config to dedicated settings page.** + +### Main interface goals +- Fast operation prompts for store owners (sales snapshot, low stock, delayed orders, coupon performance) +- Session continuity for recurring operational conversations +- Attachment-first support for notes/docs/images +- Clean streaming response UX with reasoning visibility + +### Separate settings goals +- Keep advanced AI host/key/model setup in existing `/settings/ai` +- Keep StormPilot interaction preferences in dedicated `/settings/stormpilot` + +### Responsive strategy +- **Mobile**: single-column chat + slide-out sessions sheet +- **Tablet**: compact single-column with prominent controls +- **Desktop**: grid layout designed for session pane + chat pane behavior + +### UX flow artifact (FigJam) +- [StormPilot UX Flow Diagram](https://www.figma.com/online-whiteboard/create-diagram/1c0df189-2bbe-466a-a88a-0383f0858ed7?utm_source=other&utm_content=edit_in_figjam&oai_id=&request_id=b8c9c899-8770-4744-bfb3-5e1494439539) + +--- + +## 7) Implemented Changes + +### New files +- `src/lib/stormpilot.ts` +- `src/components/stormpilot/stormpilot-chat-app.tsx` +- `src/components/stormpilot/stormpilot-preferences-form.tsx` +- `src/app/stormpilot/page.tsx` +- `src/app/settings/stormpilot/page.tsx` +- `COMPREHENSIVE_API_INVENTORY_GENERATED.md` +- `COMPREHENSIVE_STORMPILOT_OLLAMA_ANALYSIS_2026-03-19.md` + +### Updated files +- `src/components/app-sidebar.tsx` + - Added nav items for StormPilot and StormPilot Settings +- `middleware.ts` + - Added `/stormpilot` to protected paths + +### Behavior implemented in StormPilot +- Session list + archive/delete operations +- Message history loading and streaming response rendering +- Attachment upload flow to `/api/chat/ollama` +- Quick business-owner prompts +- Model selector + capabilities badges +- Web research mode (pre-fetch context via `/api/chat/websearch`) +- Graceful fallback when websearch fails +- Anti-hallucination prompt guard when live web context unavailable + +--- + +## 8) Browser Validation Findings & Fixes + +### Validated flows +- Login and route protection +- StormPilot load + session persistence +- Prompt send and response rendering +- Settings save and effect on chat UI (web research toggle) +- Attachment upload and analysis flow +- Mobile session drawer behavior + +### Issues found and fixed during validation +1. **Streaming race condition** on first-session message could hide assistant placeholder. + - **Fix**: prevent message reload during active send stream and improve scroll container handling. +2. **Websearch upstream failures (502)** caused degraded experience. + - **Fix**: explicit status feedback + anti-hallucination fallback constraint in outbound prompt. + +--- + +## 9) Remaining Recommended Enhancements + +1. **True source-cited web research mode** + - If `/api/chat/websearch` fails frequently, add retry/backoff and provider health diagnostics in UI. +2. **Attachment content extraction transparency** + - Surface parsed snippets/metadata in UI so users know what model actually received. +3. **Desktop pane verification in broader viewport contexts** + - Due tooling viewport constraints, complete final visual QA on full browser window with real monitor width. +4. **Optional stop-generation control** + - Add AbortController cancel button for long-running streams. + +--- + +## 10) Validation Status + +- Type-check: ✅ +- Build: ✅ +- Changed-file lint: ✅ +- Browser UX validation: ✅ (with issues found and fixed) + diff --git a/middleware.ts b/middleware.ts index 30e5ffa8..47dfe551 100644 --- a/middleware.ts +++ b/middleware.ts @@ -308,6 +308,7 @@ export default async function middleware(request: NextRequest) { "/projects", "/products", "/chat", + "/stormpilot", ]; const isProtectedPath = protectedPaths.some((path) => diff --git a/src/app/settings/stormpilot/page.tsx b/src/app/settings/stormpilot/page.tsx new file mode 100644 index 00000000..b9a950a0 --- /dev/null +++ b/src/app/settings/stormpilot/page.tsx @@ -0,0 +1,51 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { AppSidebar } from "@/components/app-sidebar"; +import { SiteHeader } from "@/components/site-header"; +import { StormPilotPreferencesForm } from "@/components/stormpilot/stormpilot-preferences-form"; +import { authOptions } from "@/lib/auth"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; + +export const metadata = { + title: "StormPilot Settings", + description: "Configure StormPilot experience for desktop, tablet, and mobile workflows.", +}; + +export default async function StormPilotSettingsPage() { + const session = await getServerSession(authOptions); + if (!session?.user) { + redirect("/login"); + } + + return ( + + + + +
+
+
+
+
+

StormPilot Settings

+

+ Control UI/UX preferences for your AI operations workflow while keeping core Ollama + credentials in AI Settings. +

+
+ +
+
+
+
+
+
+ ); +} diff --git a/src/app/stormpilot/page.tsx b/src/app/stormpilot/page.tsx new file mode 100644 index 00000000..2bd1fc6e --- /dev/null +++ b/src/app/stormpilot/page.tsx @@ -0,0 +1,50 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { AppSidebar } from "@/components/app-sidebar"; +import { SiteHeader } from "@/components/site-header"; +import { StormPilotChatApp } from "@/components/stormpilot/stormpilot-chat-app"; +import { authOptions } from "@/lib/auth"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; + +export const metadata = { + title: "StormPilot", + description: "Business-first AI operations cockpit for StormCom stores.", +}; + +export default async function StormPilotPage() { + const session = await getServerSession(authOptions); + if (!session?.user) { + redirect("/login"); + } + + return ( + + + + +
+
+
+
+
+

StormPilot

+

+ Your responsive AI co-pilot for store operations, insights, and daily decisions. +

+
+ +
+
+
+
+
+
+ ); +} diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index acba9073..d2d68395 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -180,6 +180,12 @@ const getNavConfig = (session: { user?: { name?: string | null; email?: string | icon: IconMessageChatbot, permission: undefined, // All authenticated users }, + { + title: "StormPilot", + url: "/stormpilot", + icon: IconMessageChatbot, + permission: undefined, // All authenticated users + }, ], navClouds: [ { @@ -242,6 +248,12 @@ const getNavConfig = (session: { user?: { name?: string | null; email?: string | icon: IconFileAi, permission: undefined, // Everyone can access AI settings }, + { + title: "StormPilot Settings", + url: "/settings/stormpilot", + icon: IconMessageChatbot, + permission: undefined, + }, { title: "Notifications", url: "/dashboard/notifications", diff --git a/src/components/stormpilot/stormpilot-chat-app.tsx b/src/components/stormpilot/stormpilot-chat-app.tsx new file mode 100644 index 00000000..49b01e19 --- /dev/null +++ b/src/components/stormpilot/stormpilot-chat-app.tsx @@ -0,0 +1,1054 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { + Archive, + Bot, + ChevronRight, + FileText, + Loader2, + Paperclip, + PanelLeft, + Pin, + Plus, + RefreshCw, + Search, + Send, + Settings2, + Sparkles, + Trash2, +} from "lucide-react"; +import type { + ChatAttachmentData, + ChatCapabilityStatusData, + ChatMessageData, + ChatSessionData, + OllamaModelInfo, +} from "@/lib/chat-types"; +import { + normalizeStormPilotPreferences, + STORMPILOT_DEFAULT_PREFERENCES, + STORMPILOT_PREFERENCES_STORAGE_KEY, + type StormPilotPreferences, +} from "@/lib/stormpilot"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Textarea } from "@/components/ui/textarea"; + +type UiMessage = ChatMessageData & { + isStreaming?: boolean; + isError?: boolean; +}; + +function formatRelativeTime(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + + const now = Date.now(); + const diffMs = now - date.getTime(); + const diffMin = Math.floor(diffMs / 60_000); + + if (diffMin < 1) return "Just now"; + if (diffMin < 60) return `${diffMin}m ago`; + + const diffHr = Math.floor(diffMin / 60); + if (diffHr < 24) return `${diffHr}h ago`; + + return date.toLocaleDateString(); +} + +function formatBytes(bytes: number) { + if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + let value = bytes; + let idx = 0; + + while (value >= 1024 && idx < units.length - 1) { + value /= 1024; + idx += 1; + } + + return `${value.toFixed(value >= 10 || idx === 0 ? 0 : 1)} ${units[idx]}`; +} + +async function toJsonSafe(response: Response) { + try { + return await response.json(); + } catch { + return null; + } +} + +function readStoredPreferences(): StormPilotPreferences { + if (typeof window === "undefined") return STORMPILOT_DEFAULT_PREFERENCES; + + try { + const raw = window.localStorage.getItem(STORMPILOT_PREFERENCES_STORAGE_KEY); + if (!raw) return STORMPILOT_DEFAULT_PREFERENCES; + return normalizeStormPilotPreferences(JSON.parse(raw)); + } catch { + return STORMPILOT_DEFAULT_PREFERENCES; + } +} + +export function StormPilotChatApp() { + const [preferences, setPreferences] = React.useState( + STORMPILOT_DEFAULT_PREFERENCES, + ); + const [sessions, setSessions] = React.useState([]); + const [activeSessionId, setActiveSessionId] = React.useState(null); + const [messages, setMessages] = React.useState([]); + const [models, setModels] = React.useState([]); + const [selectedModel, setSelectedModel] = React.useState(""); + const [capabilities, setCapabilities] = React.useState(null); + + const [loadingSessions, setLoadingSessions] = React.useState(true); + const [loadingMessages, setLoadingMessages] = React.useState(false); + const [loadingModelData, setLoadingModelData] = React.useState(false); + const [isSending, setIsSending] = React.useState(false); + const [isRefreshing, setIsRefreshing] = React.useState(false); + + const [input, setInput] = React.useState(""); + const [pendingFiles, setPendingFiles] = React.useState([]); + const [statusMessage, setStatusMessage] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(null); + const [mobileSessionSheetOpen, setMobileSessionSheetOpen] = React.useState(false); + + const messageScrollContainerRef = React.useRef(null); + const isSendingRef = React.useRef(false); + const fileInputRef = React.useRef(null); + + React.useEffect(() => { + isSendingRef.current = isSending; + }, [isSending]); + + React.useEffect(() => { + const next = readStoredPreferences(); + setPreferences(next); + setSelectedModel(next.defaultModel); + }, []); + + const loadSessions = React.useCallback(async () => { + setLoadingSessions(true); + try { + const response = await fetch("/api/chat/sessions", { cache: "no-store" }); + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || "Failed to load sessions"); + } + + const data = (await response.json()) as { sessions?: ChatSessionData[] }; + const nextSessions = Array.isArray(data.sessions) ? data.sessions : []; + setSessions(nextSessions); + + setActiveSessionId((current) => { + if (current && nextSessions.some((session) => session.id === current)) { + return current; + } + + if (nextSessions.length === 0) return null; + if (!preferences.autoOpenLatestSession) return current ?? null; + + return nextSessions[0].id; + }); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : "Failed to load sessions"); + } finally { + setLoadingSessions(false); + } + }, [preferences.autoOpenLatestSession]); + + const loadMessages = React.useCallback(async (sessionId: string) => { + setLoadingMessages(true); + try { + const query = new URLSearchParams({ sessionId, limit: "100" }); + const response = await fetch(`/api/chat/messages?${query.toString()}`, { + cache: "no-store", + }); + + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || "Failed to load messages"); + } + + const data = (await response.json()) as { messages?: ChatMessageData[] }; + const rows = Array.isArray(data.messages) ? [...data.messages].reverse() : []; + setMessages(rows); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : "Failed to load messages"); + setMessages([]); + } finally { + setLoadingMessages(false); + } + }, []); + + const loadModelData = React.useCallback(async () => { + setLoadingModelData(true); + try { + const [modelsRes, capsRes] = await Promise.all([ + fetch("/api/chat/models", { cache: "no-store" }), + fetch("/api/chat/capabilities", { cache: "no-store" }), + ]); + + if (modelsRes.ok) { + const modelsData = (await modelsRes.json()) as { models?: OllamaModelInfo[] }; + const nextModels = Array.isArray(modelsData.models) ? modelsData.models : []; + setModels(nextModels); + + if (!selectedModel && nextModels.length > 0) { + const fromPrefs = preferences.defaultModel; + const preferred = + fromPrefs && nextModels.some((item) => item.name === fromPrefs) + ? fromPrefs + : nextModels[0]?.name; + + if (preferred) { + setSelectedModel(preferred); + } + } + } + + if (capsRes.ok) { + const capabilityData = (await capsRes.json()) as ChatCapabilityStatusData; + setCapabilities(capabilityData); + } + } catch { + // Keep UI functional even if model metadata fails. + } finally { + setLoadingModelData(false); + } + }, [preferences.defaultModel, selectedModel]); + + React.useEffect(() => { + void loadSessions(); + void loadModelData(); + }, [loadSessions, loadModelData]); + + React.useEffect(() => { + if (!activeSessionId) { + setMessages([]); + return; + } + + if (isSendingRef.current) { + return; + } + + void loadMessages(activeSessionId); + }, [activeSessionId, loadMessages]); + + React.useEffect(() => { + const viewport = messageScrollContainerRef.current?.querySelector( + "[data-slot='scroll-area-viewport']", + ); + if (viewport instanceof HTMLElement) { + viewport.scrollTop = viewport.scrollHeight; + } + }, [messages]); + + const updateAssistantMessage = React.useCallback((id: string, updater: (message: UiMessage) => UiMessage) => { + setMessages((current) => current.map((message) => (message.id === id ? updater(message) : message))); + }, []); + + const createSessionIfNeeded = React.useCallback( + async (titleHint: string) => { + if (activeSessionId) { + return activeSessionId; + } + + const response = await fetch("/api/chat/sessions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: titleHint.slice(0, 120) }), + }); + + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || "Failed to create a new session"); + } + + const data = (await response.json()) as { session: ChatSessionData }; + const createdSession = data.session; + + setSessions((current) => [createdSession, ...current.filter((item) => item.id !== createdSession.id)]); + setActiveSessionId(createdSession.id); + return createdSession.id; + }, + [activeSessionId], + ); + + const buildWebResearchContext = React.useCallback(async (query: string) => { + const response = await fetch("/api/chat/websearch", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, maxResults: 4 }), + }); + + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || data?.details || "Web search failed"); + } + + const payload = (await response.json()) as { + data?: { results?: Array<{ title?: string; url?: string; snippet?: string }> }; + }; + + const results = Array.isArray(payload.data?.results) ? payload.data.results : []; + if (results.length === 0) { + return ""; + } + + const lines = results.slice(0, 4).map((item, index) => { + const title = item.title || "Untitled"; + const url = item.url || "(no url)"; + const snippet = (item.snippet || "").replace(/\s+/g, " ").trim().slice(0, 240); + return `${index + 1}. ${title}\nURL: ${url}${snippet ? `\nSummary: ${snippet}` : ""}`; + }); + + return lines.join("\n\n"); + }, []); + + const parseNdjsonStream = React.useCallback( + async (response: Response, assistantMessageId: string) => { + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Could not read AI stream"); + } + + const decoder = new TextDecoder(); + let buffer = ""; + + const processLine = (line: string) => { + if (!line.trim()) return; + + let event: { type?: string; token?: string; message?: string }; + try { + event = JSON.parse(line) as { type?: string; token?: string; message?: string }; + } catch { + return; + } + + if (event.type === "content" && typeof event.token === "string") { + updateAssistantMessage(assistantMessageId, (message) => ({ + ...message, + content: `${message.content}${event.token}`, + isStreaming: true, + })); + return; + } + + if (event.type === "thinking" && typeof event.token === "string") { + updateAssistantMessage(assistantMessageId, (message) => ({ + ...message, + thinking: `${message.thinking || ""}${event.token}`, + isStreaming: true, + })); + return; + } + + if (event.type === "error") { + throw new Error(event.message || "AI service returned an error"); + } + + if (event.type === "done") { + updateAssistantMessage(assistantMessageId, (message) => ({ + ...message, + isStreaming: false, + })); + } + }; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + buffer += decoder.decode(); + break; + } + + buffer += decoder.decode(value, { stream: true }); + + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + processLine(line); + newlineIndex = buffer.indexOf("\n"); + } + } + + if (buffer.trim()) { + processLine(buffer); + } + }, + [updateAssistantMessage], + ); + + const handleSendMessage = React.useCallback(async () => { + if (isSending) return; + + const trimmed = input.trim(); + if (!trimmed && pendingFiles.length === 0) { + setStatusMessage("Type a message or attach files to continue."); + return; + } + + setErrorMessage(null); + setStatusMessage(null); + isSendingRef.current = true; + setIsSending(true); + + const filesToSend = pendingFiles.slice(0, preferences.maxAttachmentFiles); + + try { + const sessionId = await createSessionIfNeeded(trimmed || "Attachment analysis"); + + const userMessageId = `local-user-${Date.now()}`; + const assistantMessageId = `local-assistant-${Date.now() + 1}`; + const nowIso = new Date().toISOString(); + + const attachmentMetadata: ChatAttachmentData[] = filesToSend.map((file, index) => ({ + id: `pending-${Date.now()}-${index}`, + name: file.name, + size: file.size, + type: file.type || "application/octet-stream", + isImage: file.type.startsWith("image/"), + source: "upload", + })); + + const optimisticUserMessage: UiMessage = { + id: userMessageId, + role: "USER", + content: trimmed || "(Sent attachments for analysis)", + model: selectedModel || null, + sessionId, + createdAt: nowIso, + attachments: attachmentMetadata, + }; + + const optimisticAssistantMessage: UiMessage = { + id: assistantMessageId, + role: "ASSISTANT", + content: "", + thinking: "", + model: selectedModel || null, + sessionId, + createdAt: new Date().toISOString(), + isStreaming: true, + }; + + setMessages((current) => [...current, optimisticUserMessage, optimisticAssistantMessage]); + + let outboundMessage = trimmed; + if (preferences.enableResearchMode && trimmed && filesToSend.length === 0) { + try { + const context = await buildWebResearchContext(trimmed); + if (context) { + const combined = `${trimmed}\n\n[Web research context]\n${context}`; + outboundMessage = combined.slice(0, 3900); + } + } catch (searchError) { + setStatusMessage( + searchError instanceof Error + ? `Web search skipped: ${searchError.message}` + : "Web search skipped for this message.", + ); + + const safetyPrompt = + `${trimmed}\n\n` + + "[System constraint: Live web search was unavailable for this request. " + + "Do not fabricate latest events or real-time facts. If data is unknown, clearly state uncertainty.]"; + outboundMessage = safetyPrompt.slice(0, 3900); + } + } + + const formData = new FormData(); + formData.append("message", outboundMessage); + formData.append("sessionId", sessionId); + + if (selectedModel) { + formData.append("model", selectedModel); + } + + if (preferences.thinkMode !== "off") { + formData.append("think", preferences.thinkMode); + } + + for (const file of filesToSend) { + formData.append("files", file); + } + + setInput(""); + setPendingFiles([]); + + const response = await fetch("/api/chat/ollama", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || data?.details || "Failed to send message"); + } + + await parseNdjsonStream(response, assistantMessageId); + + await Promise.all([loadSessions(), loadMessages(sessionId)]); + } catch (error) { + const friendly = error instanceof Error ? error.message : "Failed to send message"; + setErrorMessage(friendly); + + setMessages((current) => { + const copy = [...current]; + const idx = [...copy].reverse().findIndex((message) => message.id.startsWith("local-assistant-")); + if (idx === -1) return copy; + + const actualIndex = copy.length - 1 - idx; + copy[actualIndex] = { + ...copy[actualIndex], + content: friendly, + isError: true, + isStreaming: false, + }; + + return copy; + }); + } finally { + isSendingRef.current = false; + setIsSending(false); + } + }, [ + buildWebResearchContext, + createSessionIfNeeded, + input, + isSending, + loadMessages, + loadSessions, + parseNdjsonStream, + pendingFiles, + preferences, + selectedModel, + ]); + + const handleSelectFiles = React.useCallback( + (event: React.ChangeEvent) => { + const picked = Array.from(event.target.files || []); + if (picked.length === 0) return; + + const valid: File[] = []; + const rejected: string[] = []; + + for (const file of picked) { + const maxBytes = preferences.maxAttachmentSizeMb * 1024 * 1024; + if (file.size > maxBytes) { + rejected.push(`${file.name} exceeds ${preferences.maxAttachmentSizeMb}MB`); + continue; + } + valid.push(file); + } + + setPendingFiles((current) => { + const merged = [...current, ...valid]; + const trimmed = merged.slice(0, preferences.maxAttachmentFiles); + return trimmed; + }); + + if (rejected.length > 0) { + setStatusMessage(rejected.join(" • ")); + } + + event.target.value = ""; + }, + [preferences.maxAttachmentFiles, preferences.maxAttachmentSizeMb], + ); + + const handleRefresh = React.useCallback(async () => { + setIsRefreshing(true); + setErrorMessage(null); + + try { + await loadSessions(); + await loadModelData(); + if (activeSessionId) { + await loadMessages(activeSessionId); + } + } finally { + setIsRefreshing(false); + } + }, [activeSessionId, loadMessages, loadModelData, loadSessions]); + + const handleArchiveSession = React.useCallback( + async (sessionId: string, hardDelete = false) => { + const shouldContinue = + !hardDelete || window.confirm("Delete this chat permanently? This cannot be undone."); + if (!shouldContinue) return; + + try { + const response = await fetch(`/api/chat/sessions/${sessionId}${hardDelete ? "?hard=1" : ""}`, { + method: "DELETE", + }); + + if (!response.ok) { + const data = await toJsonSafe(response); + throw new Error(data?.error || "Failed to update session"); + } + + if (activeSessionId === sessionId) { + setActiveSessionId(null); + setMessages([]); + } + + await loadSessions(); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : "Failed to update session"); + } + }, + [activeSessionId, loadSessions], + ); + + const canSend = !isSending && (input.trim().length > 0 || pendingFiles.length > 0); + + const quickPrompts = [ + "Give me today's sales, orders, and top 5 products snapshot.", + "Which products are low in stock and need urgent restock?", + "Summarize pending orders and delayed fulfillments.", + "Show coupon performance and recommendations for this week.", + ]; + + return ( +
+ + +
+ StormPilot Sessions + +
+ + Keep conversations organized by store operations, marketing, and support. + +
+ + +
+ {loadingSessions && sessions.length === 0 ? ( +
+ Loading sessions... +
+ ) : sessions.length === 0 ? ( +
+ No sessions yet. Start with a prompt below. +
+ ) : ( + sessions.map((session) => { + const isActive = session.id === activeSessionId; + + return ( +
+ + +
+ + +
+
+ ); + }) + )} +
+
+
+
+ +
+ + +
+
+ + + + + + + StormPilot Sessions + + Pick a session or create a new one. + + +
+ +
+ {sessions.map((session) => ( + + ))} +
+
+
+
+ + + StormPilot + + + {preferences.enableResearchMode && ( + + Web Research On + + )} +
+ +
+ + +
+
+ +
+
+ +

+ Built for daily store operations, inventory decisions, and customer support guidance. +

+
+ +
+ + +
+ +
+ +
+ + Tools {capabilities?.checks.tools.available ? "On" : "Off"} + + + OpenAI {capabilities?.checks.openAiCompat.available ? "On" : "Off"} + +
+
+
+
+
+ + + + {messages.length === 0 && !loadingMessages ? ( +
+
+ Business-owner quick prompts +
+
+ {quickPrompts.map((prompt) => ( + + ))} +
+
+ ) : ( +
+ +
+ {loadingMessages ? ( +
+ Loading chat messages... +
+ ) : ( + messages.map((message) => { + const isUser = message.role === "USER"; + const hasAttachments = Array.isArray(message.attachments) && message.attachments.length > 0; + + return ( +
+
+
+ + {isUser ? "You" : "StormPilot"} + + + {formatRelativeTime(message.createdAt)} + {message.isStreaming && ( + <> + + + + )} +
+ +

+ {message.content || (message.isStreaming ? "Generating response..." : "")} +

+ + {!isUser && message.thinking ? ( +
+ + Reasoning trace + +

+ {message.thinking} +

+
+ ) : null} + + {hasAttachments ? ( +
+ {message.attachments?.map((attachment) => ( + + + {attachment.name} + ({formatBytes(attachment.size)}) + + ))} +
+ ) : null} +
+
+ ); + }) + )} +
+
+
+ )} + +
+
+

+ Upload docs/images, ask in natural language, and get store-aware answers. +

+
+ + +
+
+ + {pendingFiles.length > 0 ? ( +
+ {pendingFiles.map((file) => ( + + {file.name} + {formatBytes(file.size)} + + + ))} +
+ ) : null} + +
+