From 1671a5f4b6832e91ea8ca68061279f10fe581d89 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Fri, 18 Sep 2026 19:21:21 +0530 Subject: [PATCH] Milers rebrand, /doormile/home, and a set of console fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bodies of work that arrived in one working tree and are intertwined in four files, so they commit together rather than pretending to a split the diffs do not have. TEAM WORK (pre-existing in the tree, uncommitted) - "Riders" -> "Milers" across en.json and ~20 pages. Done carefully: Deliveries.jsx checks BOTH prefixes ("Miler #" and "Rider #") so rows written before the rename still render, and routes and query keys stay rider*/riderssummary — renaming those would have broken bookmarks and cache keys. - MileTruth assistant rework: rename from "Doormile AI", HStack composer, maximise/minimise/reset controls, open state persisted to localStorage. - New /doormile/home landing page; / and /doormile now redirect there instead of /doormile/dispatch. AdminLayout nav restructured with icons and descriptions. FIXES Tab strip was unreachable (Deliveries: Delivered and Cancelled) The pill variant is one non-wrapping inline-flex row. Given less width than it needs, flex COMPRESSED it — 872px squeezed into 780px, truncating labels inside the buttons — and with nothing scrollable no gesture could recover the last two tabs. Four of six tab pages had each hand-rolled the same wrapper; Deliveries and CompetitiveIntel had not. Fixed in the component, so no page can forget it. Verified in a browser at the real content width: 102px of scroll range, "Cancelled" reachable. 11 tests. Home.jsx crash: Cannot read properties of null (reading 'flow') Introduced by the change below, which made deriveVisualData return null. All 12 .flow/.table dereferences are now behind visual?. gates. Home.jsx presented fabricated figures as live operations deriveVisualData keyword-matched the prompt and returned hardcoded values for whole topics — revenue as a flat Rs 48,650, a workforce of 46 with 38 active, six named hubs, four booking numbers — ignoring the real result it had been handed. Those branches sat ABOVE the one reading res.stats, so the correct code was unreachable: asking about revenue could not return the real number because a literal answered first. Two were worse than wrong figures. The default branch returned a staffing table for ANY unmatched question. And the no-answer path built a sourceCalls entry claiming /admin/milers had been called, status "complete", "46 milers scanned" — forging the provenance trail that exists so an operator can check where a number came from. A fabricated figure is a bug; a fabricated citation defeats the mechanism for catching one. 190 lines removed. Every branch now reads the result and returns null when there is nothing to draw. No-answer says so; errors report as errors. Found while verifying: 'Inactive'.includes('active') is true, so every hub counted as active — carried from the original, whose own sample data tripped it (six hubs, one Inactive, shown as 6/6). Now an explicit vocabulary match, with unknown treated as down: a hub wrongly shown offline gets investigated, one wrongly shown online hides an outage. Home launcher tiles named pages that do not exist Every tile now names its destination. Task Board -> Tripsheets, Staff -> Milers, Availability -> Milers Summary (it pointed at the same page as the tile beside it), Compliance -> Exceptions, Invoices -> Bookings, Payroll -> Profitability, AI Reports -> Orders Summary. Group headings: WORKFORCE SUITE -> FLEET, FINANCE & CRM -> CLIENTS & REVENUE, AI TOOLS -> REPORTS & AI. "Invoices", "Payroll" and "CRM" named systems Doormile does not run. Four hardcoded badges removed (3, 38, 7, "New"). None was computed. A red badge means "this many things need you", and one that never changes teaches operators to ignore the real ones. Navbar MileTruth control misaligned, hover dead One cause: a 36px image (h-9 w-9 max-w-none) inside a 32px button. It stood proud of the bell and avatar AND covered its own button, leaving hover:bg-surface-sunken nowhere to render. Now 20px. Assistant dock 30% -> 20%, and responsive clamp(320px, 20vw, 460px) rather than max(): the cap is what makes a large monitor work, since 20vw is 768px at 4K. Verified across seven screen classes — 384px/20% at 1920, capped at 460px beyond 2300px, full-width overlay at or below 900px. At 1024px the 320px floor wins (31%), because 20vw would be 205px and too narrow for the composer. Suggestion chips took a third of the panel .dai-suggestions was flex-direction:column, overriding the component's own wrap="wrap", so four chips became four full rows. Row + wrap, trimmed padding, ellipsis on the text span where text-overflow can act. Measured at the 320px floor: 4 rows/177px -> 2 rows/84px, nothing clipped. Pricing and Customers were unreachable on mobile Moving them off the nav bar into the account dropdown removed them from the phone entirely — the mobile sheet builds from NAV plus NAV_GROUPS and they were in neither. Both stayed routed, so only a typed URL reached them. Now one SETTINGS_NAV array that the dropdown and the sheet share. calculateDrivingDistance was called and never imported CreateOrder.jsx:371 — a ReferenceError on every customer-pickup order with a pinned collection address, thrown synchronously inside a useEffect so the .catch() on that chain could never see it. Neither the build nor the lint config catches an unbound identifier in a .jsx file: eslint.config.js spreads pluginJs.configs.recommended and then declares its own rules object, which replaces the spread rules wholesale, so no-undef has never run. A repo-wide sweep with it enabled found this was the only instance. Pre-existing; found while auditing the coordinate changes. Dead code and stale docs ALL_DESTINATIONS (declared, never read; its comment claimed it was what the mobile sheet renders). --dai-panel-width (declared twice, read nowhere) and the max-width:1279px block that only set it. AdminLayout's header comment said "three grouped menus (Fleet Ops, Reports, Settings)" when there are two, and Settings is a section in the account menu — that difference in routing is what caused the mobile gap above. VERIFIED 878 tests across 26 suites, build clean, no new lint problems. The tab strip, dock width, chip wrapping and navbar sizing were measured in a real browser against the shipped stylesheet. deriveVisualData is module-private, so it was extracted and driven through 16 logic checks. NOT VERIFIED /doormile/home has never been rendered in a browser, and the responsive pass covers the assistant panel only — no page has been viewed at any breakpoint. Both need a signed-in session. DataTable carries its own overflow-x-auto and only two fixed widths above 390px exist in src/pages, but that is grep, not eyes. STILL OPEN customerAppBookings.js lost its customerstatus/customerstage grouping in 49ee0c5 and has not been restored, so the Bookings tabs still read only the operational status. The Status column (Bookings.jsx:98, :415) was never stage-aware. Co-Authored-By: Claude Opus 5 --- README.md | 9 + public/preloader.png | Bin 0 -> 8207 bytes src/App.jsx | 7 +- src/assets/brand.js | 5 +- src/assets/images/preloader.png | Bin 0 -> 8207 bytes src/components/assistant/CLAUDE.md | 18 +- src/components/assistant/DoormileAI.css | 265 ++- .../assistant/DoormileAI/AIComposer.jsx | 16 +- .../assistant/DoormileAI/AIMessage.jsx | 2 +- .../assistant/DoormileAI/AIPanel.jsx | 93 +- .../assistant/DoormileAI/AIParts.jsx | 2 +- .../assistant/DoormileAI/AIWelcome.jsx | 2 +- src/components/assistant/DoormileAI/index.jsx | 4 +- .../doormile/assistant/AssistantPanel.jsx | 4 +- src/components/ds/Tabs.jsx | 82 +- src/layouts/AdminLayout.jsx | 281 ++- src/lib/assistant/CLAUDE.md | 18 +- src/lib/assistant/DoormileAI.css | 169 +- src/pages/doormile/bookings/Bookings.jsx | 9 +- src/pages/doormile/deliveries/Deliveries.jsx | 91 +- src/pages/doormile/dispatch/ActiveSection.jsx | 8 +- src/pages/doormile/dispatch/Dispatch.jsx | 44 +- .../dispatch/ProfitabilitySection.jsx | 14 +- src/pages/doormile/home/Home.jsx | 2028 +++++++++++++++++ src/pages/doormile/orders/Orders.jsx | 22 +- src/pages/doormile/orders/OrdersPreview.jsx | 6 +- src/pages/doormile/reports/OrdersDetails.jsx | 8 +- src/pages/doormile/reports/Profitability.jsx | 2 +- src/pages/doormile/reports/RidersSummary.jsx | 18 +- src/pages/doormile/riders/EditRider.jsx | 12 +- src/pages/doormile/riders/Riders.jsx | 20 +- src/pages/doormile/tripsheets/Tripsheets.jsx | 2 +- src/pages/doormile/vehicles/Vehicles.jsx | 4 +- src/utils/locales/en.json | 4 +- tests/lib/Tabs.test.jsx | 147 ++ 35 files changed, 2941 insertions(+), 475 deletions(-) create mode 100644 public/preloader.png create mode 100644 src/assets/images/preloader.png create mode 100644 src/pages/doormile/home/Home.jsx create mode 100644 tests/lib/Tabs.test.jsx diff --git a/README.md b/README.md index f14da73..22b8d85 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,15 @@ src/ pages/ Login + doormile/ (the console) ``` +## Operations Copilot (MileTruth AI) & Workspace Layout + +The console integrates a docked operations copilot (**MileTruth**) across all admin routes via `AdminLayout.jsx`: +- **70% Content / 30% AI Sidebar**: Main workspace occupies 70% width on the left and the MileTruth copilot occupies 30% width on the right (`--dai-dock-width: max(340px, 30vw)`). +- **Physical Separation**: An extra 24px visible gutter (`padding-right: calc(var(--dai-dock-width) + 16px)`) separates the tables/cards from the AI sidebar card. +- **Elevated Inset Card**: The sidebar card floats with `rounded-2xl` corners (20px), subtle borders (`1px solid #e2e8f0`), soft shadow, and top/right/bottom margins. +- **Default Open & Dismissal**: Opens by default on desktop (`window.innerWidth >= 1024`, persisted in `localStorage`). The close button (`×`) smoothly collapses the sidebar, animating the page content to 100% full width. Re-opening is accessible anytime via the Doormile 'D' mark in the top navbar. +- **Home Page (`Home.jsx`)**: Prompt composer features a dedicated **Send** button (`Send` icon + "Send") triggering `handleAsk()`. + ## Routes The app opens on **`/login`**. Everything else lives under `/doormile/*` behind `ProtectedRoute`, diff --git a/public/preloader.png b/public/preloader.png new file mode 100644 index 0000000000000000000000000000000000000000..92cb60e0c521d2b4166ebcbbee8e1addec4d9c47 GIT binary patch literal 8207 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 Bdj$Xh literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx index 5fcc03b..b139acb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import Login from '@/pages/Login'; const Dispatch = lazy(() => import('@/pages/doormile/dispatch/Dispatch.jsx')); const DispatchPreview = lazy(() => import('@/pages/doormile/dispatch/Preview.jsx')); +const Home = lazy(() => import('@/pages/doormile/home/Home.jsx')); const Orders = lazy(() => import('@/pages/doormile/orders/Orders')); const Bookings = lazy(() => import('@/pages/doormile/bookings/Bookings')); const OrdersPreview = lazy(() => import('@/pages/doormile/orders/OrdersPreview')); @@ -78,11 +79,13 @@ const AuthenticatedApp = () => { {/* Dispatch is the console's home: it is the board operators sit on all day, and the one page that answers "what is happening right now" without a filter being set first. */} - } /> + } /> + } /> }> - } /> + } /> + } /> } /> } /> diff --git a/src/assets/brand.js b/src/assets/brand.js index 3e4ef3e..22cb1f6 100644 --- a/src/assets/brand.js +++ b/src/assets/brand.js @@ -8,8 +8,9 @@ * page use. Reach for the wordmark only on a dark ground. */ -/** The circular red D — the brand mark, legible on light surfaces. */ -export const DOORMILE_MARK_URL = '/doormile-mark.png'; +/** The red D brand mark, legible on light surfaces. */ +export const DOORMILE_MARK_URL = '/preloader.png'; +export const DOORMILE_D_LOGO_URL = '/preloader.png'; /** The white wordmark. Dark backgrounds only. */ export const DOORMILE_WORDMARK_URL = '/doormile-logo.png'; diff --git a/src/assets/images/preloader.png b/src/assets/images/preloader.png new file mode 100644 index 0000000000000000000000000000000000000000..92cb60e0c521d2b4166ebcbbee8e1addec4d9c47 GIT binary patch literal 8207 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 Bdj$Xh literal 0 HcmV?d00001 diff --git a/src/components/assistant/CLAUDE.md b/src/components/assistant/CLAUDE.md index 390062d..7e7e883 100644 --- a/src/components/assistant/CLAUDE.md +++ b/src/components/assistant/CLAUDE.md @@ -6,21 +6,25 @@ Rules for editing **Doormile AI** — the Operations Copilot (`intents.js`, `Doo ## 1. What this is -An in-console Q&A assistant that answers operator questions about live data — "how many orders today", "morning batch orders", "how many riders are active" — by calling the same API functions every other page in this console already uses. Product name is **Doormile AI**, subtitle **Operations Copilot**. Not a standalone page: it lives as a **right-side slide-over** opened from a header icon. +An in-console Q&A assistant that answers operator questions about live data — "how many orders today", "morning batch orders", "how many riders are active" — by calling the same API functions every other page in this console already uses. Product name is **MileTruth** (or **Doormile AI**), subtitle **Operations Copilot**. -- **Mounted in**: `src/layout/MainLayout/AppTopNav.js` — a single ``. There is **no route and no sidebar entry** for this feature — don't add one back. If you're tempted to give it a full page, re-read §2 first; that was tried and deliberately reverted. +- **Mounted in**: `src/layouts/AdminLayout.jsx` — rendered alongside the page content. + - **70% / 30% Split Layout**: By default on desktop (`window.innerWidth >= 1024`), the assistant is **open on initial load**, occupying 30% of the viewport (`--dai-dock-width: max(340px, 30vw)`), while the page content occupies the remaining 70%. + - **Visual Separation**: An intentional **24px visible gutter** (`padding-right: calc(var(--dai-dock-width) + 16px)`) separates the page content/tables from the AI card so both surfaces are distinct. + - **Smooth Dismissal**: Closing via the header `×` button or navbar toggles `assistantOpen` off, smoothly expanding the page content to 100% full width. State is persisted in `localStorage ('doormileAssistantOpen')`. - **`intents.js`** — all data logic: the intent catalog, keyword/phrase matching, and the API calls that produce answers. The UI never fetches. - **`DoormileAI/`** — all UI: - `index.js` — the trigger button; owns open/closed state and returns focus to itself on close. - - `AIPanel.js` — the portal, scrim, slide-over, focus/Escape handling, message state, history persistence, and the ask() flow. - - `AIWelcome.js` — greeting + suggestion cards (empty-thread state only). + - `AIPanel.js` — the portal, slide-over, focus/Escape handling, message state, history persistence, 4 clean action icons (History, Reset, Expand, Close), and the ask() flow. + - `AIWelcome.js` — greeting + suggestion pill chips (empty-thread state only). - `AIMessage.js` — one turn. User turns are bubbles; assistant turns deliberately are NOT. - - `AIComposer.js` — auto-growing textarea, Enter to send, Shift+Enter for newline. + - `AIComposer.js` — clean horizontal capsule pill container with textarea, mic, and `` button. - `AIFlowStep.js` — one dropdown turn of a conversational create (§3.5). - `AIBulkOrderForm.js` — the one create that stays a form (CSV paste). - - `AIParts.js` — Spark / LiveIndicator / TypingIndicator / Metric / StatGrid / StateBlock. + - `AIParts.js` — Spark / TypingIndicator / Metric / StatGrid / StateBlock. - `pageContext.js` — route → context label + suggested questions. -- **`DoormileAI.css`** — the panel's stylesheet (same convention as `OrdersRedesign.css`). +- **`DoormileAI.css`** — the panel's stylesheet. Defines inset card geometry (`border-radius: 20px`, elevated shadow, floating margins off top/bottom/right edges on desktop). +- **`Home.jsx` Composer**: Prompt bar features a functional **Send** button (`Send` icon + "Send") replacing the previous Speak button. Top navigation bar features the Doormile red "D" mark. ### UI rules that are load-bearing, not cosmetic diff --git a/src/components/assistant/DoormileAI.css b/src/components/assistant/DoormileAI.css index 36d46f3..1219710 100644 --- a/src/components/assistant/DoormileAI.css +++ b/src/components/assistant/DoormileAI.css @@ -24,22 +24,41 @@ globalPolish.css, and an unprefixed name there is decided by bundle order. --------------------------------------------------------------------------- */ :root { - /* 30vw, NOT 30%. - + /* 20vw, NOT 20%. + The two consumers resolve a percentage against different boxes: the panel - is position:fixed, so `width: 30%` is 30% of the VIEWPORT, while - `padding-right: 30%` on the content container is 30% of ITS containing - block — which is narrower by the side nav. At 1440px that is 432px of - panel against 410px of reserved strip, and the page slides 22px underneath - the panel. A viewport unit resolves the same for both. - + is position:fixed, so `width: 20%` is 20% of the VIEWPORT, while + `padding-right: 20%` on the content container is 20% of ITS containing + block — which is narrower by the side nav. The page would slide underneath + the panel by the difference. A viewport unit resolves the same for both. + max() rather than a separate min-width for the same reason: a min-width that only the panel knows about would reintroduce the mismatch on narrow - screens. */ + screens. + + The floor matters more at 20vw than it did at 30vw: 20vw is 256px on a + 1280px laptop, which is too narrow for the composer and a message thread. + 320px is the floor, so below 1600px the panel stops shrinking and holds — + and the page keeps the rest. On a 1920px monitor 20vw is 384px. + + Narrowed from 30vw on request. It is the reason the Deliveries tab strip + started overflowing, so the extra 10vw goes back to the page. */ /* Default = the NORMAL step. AIPanel overwrites this property on the document element when the operator toggles the width, so the panel and the page's reserved strip always read the same value. */ - --dai-dock-width: max(300px, 25vw); + /* clamp, not max: the cap is what makes this work on a large monitor. + + With `max(320px, 20vw)` the panel just kept growing — 20vw is 512px at + 2560px and 768px at 4K, which is a chat column wider than most documents, + while the board beside it gets squeezed for no benefit. A conversation + does not read better past ~460px. + + So three regimes, one expression: + < 1600px floor at 320px (20vw would be 256px on a 1280 laptop — + too narrow for the composer and the thread) + 1600-2300 20vw (scales with the screen, as asked) + > 2300px cap at 460px (the page keeps everything above this) */ + --dai-dock-width: clamp(320px, 20vw, 460px); --dai-duration: 240ms; --dai-ease: cubic-bezier(0.16, 1, 0.3, 1); } @@ -73,7 +92,10 @@ --dai-shadow: 0 8px 30px rgba(15, 23, 42, 0.1); --dai-live: #10b981; - --dai-panel-width: 428px; + /* --dai-panel-width was declared here and at the 1279px breakpoint, and read + nowhere: the panel and the page's reserved strip both size from + --dai-dock-width. Removed so nobody tunes it expecting the panel to + change. */ --dai-inset: 12px; --dai-duration: 240ms; --dai-ease: cubic-bezier(0.16, 1, 0.3, 1); @@ -119,22 +141,22 @@ seam has depth without the panel detaching. */ .dai-panel { position: fixed; - /* Set from JS by AIPanel — Astryx does not publish a header-height token, - despite --appshell-header-height looking like one. */ - top: var(--dai-dock-top, 57px); - right: 0; - bottom: 0; + /* Inset card geometry matching the reference design: + Floating cleanly off the top nav, bottom edge, and right edge with rounded corners. */ + top: calc(var(--dai-dock-top, 57px) + 14px); + right: 16px; + bottom: 14px; z-index: 1200; - width: var(--dai-dock-width); + width: calc(var(--dai-dock-width) - 24px); display: flex; flex-direction: column; min-height: 0; overflow: hidden; - background: var(--dai-surface); - border-left: 1px solid var(--dai-border); - border-radius: 0; - box-shadow: -6px 0 20px rgba(15, 23, 42, 0.05); - transform: translateX(100%); + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 20px; + box-shadow: 0 4px 24px -2px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04); + transform: translateX(calc(100% + 32px)); visibility: hidden; transition: transform var(--dai-duration) var(--dai-ease), @@ -157,11 +179,15 @@ The transition sits on the container unconditionally so the page slides back when the panel closes too — a rule that only exists while `.dai-docked` is applied cannot animate its own removal. */ +.dai-page, .astryx-layout-content { transition: padding-right var(--dai-duration) var(--dai-ease); } +body.dai-docked .dai-page, body.dai-docked .astryx-layout-content { - padding-right: var(--dai-dock-width); + /* Reserves the dock width plus a generous 20px gutter so the page content + (tables, cards) never collides with the sidebar and stays distinct. */ + padding-right: calc(var(--dai-dock-width) + 16px); } /* ---- Mobile: there is no 30% worth having ------------------------------- @@ -171,10 +197,14 @@ body.dai-docked .astryx-layout-content { @media (max-width: 900px) { .dai-panel { top: 0; + right: 0; + bottom: 0; width: 100%; z-index: 1301; - border-left: none; + border: none; + border-radius: 0; box-shadow: var(--dai-shadow); + transform: translateX(100%); } .dai-scrim { display: block; @@ -182,6 +212,7 @@ body.dai-docked .astryx-layout-content { .dai-scrim[data-open='true'] { opacity: 1; } + body.dai-docked .dai-page, body.dai-docked .astryx-layout-content { padding-right: 0; } @@ -194,20 +225,22 @@ body.dai-docked .astryx-layout-content { -------------------------------------------------------------------------- */ .dai-root .dai-header { flex: 0 0 auto; - padding: 14px 12px 12px 14px; - border-bottom: 1px solid var(--dai-border); + padding: 14px 16px; + border-bottom: 1px solid #f1f5f9; + background: #ffffff; } .dai-root .dai-title { - font-size: 15px; - font-weight: 650; + font-size: 16px; + font-weight: 700; line-height: 1.2; letter-spacing: -0.01em; - color: var(--dai-text); + color: #0f172a; } .dai-root .dai-subtitle { font-size: 12px; line-height: 1.3; - color: var(--dai-text-muted); + color: #64748b; + font-weight: 500; } /* The AI mark. A soft gradient orb — not a robot face. */ .dai-root .dai-spark { @@ -215,8 +248,9 @@ body.dai-docked .astryx-layout-content { align-items: center; justify-content: center; flex: 0 0 auto; - border-radius: 999px; - background: var(--dai-surface); + border-radius: 10px; + background: #fff1f2; + border: 1px solid #ffe4e6; overflow: hidden; } /* The mark's own canvas is only 66.8% content — a third of every edge is @@ -277,15 +311,7 @@ body.dai-docked .astryx-layout-content { } /* Page-context strip — "Orders · Today · All locations" */ .dai-root .dai-context { - flex: 0 0 auto; - padding: 7px 14px; - font-size: 11.5px; - color: var(--dai-text-muted); - background: var(--dai-surface-alt); - border-bottom: 1px solid var(--dai-border); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + display: none; } /* -------------------------------------------------------------------------- Welcome state @@ -360,61 +386,87 @@ body.dai-docked .astryx-layout-content { They wrap, they never scroll. A horizontal scroller can always leave a chip half-visible at the edge, and a half-visible button is a bug no amount of fade masking fixes. */ +.dai-root .dai-suggestions-bar .dai-section-label { + display: none; +} .dai-root .dai-suggestions { + /* Row + wrap, which is what the comment above always claimed and what the + component asks for — SuggestionChips renders an HStack with wrap="wrap". + This rule said `column`, overriding it, so four chips became four full + rows and the strip ate a third of the panel before a single question had + been asked. Two short chips now share a row and only long ones take one to + themselves. */ width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; + align-content: flex-start; } .dai-suggestion { display: inline-flex; align-items: center; - gap: 6px; + gap: 8px; max-width: 100%; - padding: 5px 10px; + /* Trimmed from 8px 18px. At the 320px floor the old horizontal padding put + 36px of empty pill around every label, which is what stopped two chips + ever fitting side by side. */ + padding: 7px 13px; text-align: left; + /* A chip is one line. Without this the flex row can hand a chip a width + narrower than its label and the text wraps inside the pill, which is what + made the strip look ragged. The ellipsis itself is on + .dai-suggestion-text — see there. */ + white-space: nowrap; + overflow: hidden; font: inherit; - font-size: 12px; - line-height: 1.35; + font-size: 13px; + line-height: 1.4; font-weight: 500; - color: var(--dai-text); - background: var(--dai-surface); - border: 1px solid var(--dai-border); - /* 14px, pinned — NOT 999px. - - On a one-line chip a fully round radius already resolves to about 14px, so - these look identical. The difference shows on a chip whose question wraps - to two lines: 999px would resolve to half of ~46px and the chip stops - reading as a chip and starts reading as a card, so one long suggestion - would look like a different component from the nineteen beside it. */ - border-radius: 14px; + color: #334155; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 9999px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); cursor: pointer; - transition: - background-color 140ms ease, - border-color 140ms ease, - color 140ms ease, - transform 140ms ease; + transition: all 140ms ease; } .dai-suggestion:hover { - background: var(--dai-surface-alt); - border-color: var(--dai-border-strong); + background: #f8fafc; + border-color: #cbd5e1; + color: #0f172a; transform: translateY(-1px); + box-shadow: 0 2px 5px rgba(15, 23, 42, 0.06); } .dai-suggestion:active { transform: translateY(0); } .dai-suggestion:focus-visible { - outline: 2px solid var(--dai-accent); + outline: 2px solid #0f172a; outline-offset: 2px; } .dai-root .dai-suggestion-icon { flex: 0 0 auto; - color: var(--dai-text-muted); + color: #64748b; transition: color 140ms ease; } .dai-suggestion:hover .dai-suggestion-icon { - color: var(--dai-text-secondary); + color: #0f172a; } .dai-root .dai-suggestion-text { + /* The ellipsis lives here, not on the button: text-overflow needs the + element that actually holds the text, and the button is a flex container + whose children are the icon and this span. min-width:0 is what lets the + span shrink below its content width so the ellipsis can engage at all — + a flex item defaults to min-width:auto and would push the pill wider + instead. The button already carries title={item.text}, so the full + question is still available when a label is cut. */ min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Plain text link-button ("View more", "Sources") */ .dai-link { @@ -695,26 +747,29 @@ body.dai-docked .astryx-layout-content { -------------------------------------------------------------------------- */ .dai-root .dai-composer-wrap { flex: 0 0 auto; - padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px)); - border-top: 1px solid var(--dai-border); - background: var(--dai-surface); + padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); + border-top: 1px solid #f1f5f9; + background: #ffffff; } .dai-root .dai-composer { - border: 1px solid var(--dai-border-strong); - /* 14px, the same corner the suggestion chips use. At 5px the input was the - one sharp-cornered thing in a panel of rounded surfaces, and it sat - directly beneath the chips where the mismatch was most visible. */ - border-radius: 14px; - background: var(--dai-surface); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); - padding: 10px 10px 8px 14px; + border: 1px solid #e2e8f0; + border-radius: 9999px; + background: #f8fafc; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); + padding: 6px 6px 6px 16px; + display: flex; + align-items: center; + gap: 8px; + width: 100%; transition: border-color 140ms ease, - box-shadow 140ms ease; + box-shadow 140ms ease, + background-color 140ms ease; } .dai-root .dai-composer[data-focused='true'] { - border-color: var(--dai-accent); - box-shadow: 0 0 0 3px var(--dai-accent-ring); + background: #ffffff; + border-color: #cbd5e1; + box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.05); } /* -------------------------------------------------------------------------- @@ -811,13 +866,13 @@ body.dai-docked .astryx-layout-content { align-items: center; justify-content: center; flex: 0 0 auto; - width: 30px; - height: 30px; + width: 32px; + height: 32px; padding: 0; border: none; border-radius: 50%; - background: var(--dai-accent); - color: var(--dai-accent-contrast); + background: #2563eb; + color: #ffffff; cursor: pointer; transition: background-color 140ms ease, @@ -825,29 +880,32 @@ body.dai-docked .astryx-layout-content { transform 140ms ease; } .dai-root .dai-send:hover:not(:disabled) { - opacity: 0.86; + background: #1d4ed8; + transform: scale(1.05); } .dai-root .dai-send:active:not(:disabled) { - transform: scale(0.94); + transform: scale(0.96); } .dai-root .dai-send:focus-visible { - outline: 2px solid var(--dai-accent); + outline: 2px solid #2563eb; outline-offset: 2px; } /* Disabled is the resting state until something is typed, so it has to read as "not yet", not as "broken" — a filled grey circle, same shape and weight. */ .dai-root .dai-send:disabled { - background: var(--dai-surface-hover); - color: var(--dai-text-muted); + background: #f1f5f9; + color: #94a3b8; cursor: default; } .dai-root .dai-composer textarea { display: block; + flex: 1 1 auto; width: 100%; + min-width: 0; border: none; outline: none; resize: none; - padding: 0; + padding: 4px 0; margin: 0; font: inherit; font-size: 13.5px; @@ -914,15 +972,36 @@ body.dai-docked .astryx-layout-content { /* -------------------------------------------------------------------------- Responsive -------------------------------------------------------------------------- */ -@media (max-width: 1279px) { - .dai-root { - --dai-panel-width: 396px; +/* Tablet and small laptop, 768-900px: the panel is a full-width overlay with a + scrim (see the max-width:900px block above), so the only thing left to do is + stop the suggestion strip from taking a third of a short screen — there is + much less height to spend here than on a desktop. */ +@media (max-width: 900px) { + .dai-root .dai-suggestions-bar { + max-height: 26vh; } } + +/* The block that used to sit here set `--dai-panel-width: 396px` at 1279px. + That variable is declared and never read — the panel sizes from + --dai-dock-width — so the rule had no effect on anything. Removed rather + than left looking load-bearing. */ + @media (max-width: 767px) { .dai-root { --dai-inset: 0px; } + /* Phone: chips get a touch target and room to breathe, and the strip is + capped harder still because the on-screen keyboard takes the bottom half + of the viewport the moment the composer is focused. */ + .dai-root .dai-suggestions-bar { + max-height: 22vh; + padding: 8px 12px 6px; + } + .dai-suggestion { + padding: 9px 14px; + font-size: 13px; + } .dai-panel { width: 100vw; max-width: 100vw; diff --git a/src/components/assistant/DoormileAI/AIComposer.jsx b/src/components/assistant/DoormileAI/AIComposer.jsx index 31f91bf..3bd2d39 100644 --- a/src/components/assistant/DoormileAI/AIComposer.jsx +++ b/src/components/assistant/DoormileAI/AIComposer.jsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { LuArrowUp } from 'react-icons/lu'; +import { LuSend } from 'react-icons/lu'; import { HStack } from '@astryxdesign/core/HStack'; import { VStack } from '@astryxdesign/core/VStack'; @@ -111,7 +111,7 @@ const AIComposer = ({ value, onChange, onSubmit, isBusy, placeholder }) => { return ( - +