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 0000000..92cb60e
Binary files /dev/null and b/public/preloader.png differ
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 0000000..92cb60e
Binary files /dev/null and b/src/assets/images/preloader.png differ
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 (
-
+
+
{dictation.isListening ? 'Listening — sends automatically after a short pause' : 'Enter to send · Shift + Enter for a new line'}
diff --git a/src/components/assistant/DoormileAI/AIMessage.jsx b/src/components/assistant/DoormileAI/AIMessage.jsx
index feb58aa..cde00f9 100644
--- a/src/components/assistant/DoormileAI/AIMessage.jsx
+++ b/src/components/assistant/DoormileAI/AIMessage.jsx
@@ -43,7 +43,7 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
- Doormile AI
+ MileTruth {
? nextSteps.length > 0
? nextSteps
: pageContext.suggestions.slice(0, 4)
- : [...pageContext.suggestions, ...(pageContext.more || [])];
+ : pageContext.suggestions.slice(0, 4);
// Few chips => the strip is fixed and always fully visible. Twenty => it may
// shrink and scroll. See the [data-compact] rule in DoormileAI.css.
@@ -1401,7 +1413,7 @@ const AIPanel = ({ isOpen, onClose }) => {
stays live and operable, so announcing it as a dialog would be a
lie to exactly the users who cannot see that it is docked. */
role="complementary"
- aria-label="Doormile AI — Operations Copilot"
+ aria-label="MileTruth — Operations Copilot"
tabIndex={-1}
>
{/* ---- header ---- */}
@@ -1410,30 +1422,16 @@ const AIPanel = ({ isOpen, onClose }) => {
- Doormile AI
+ MileTruthOperations Copilot
-
- {/* No `size` prop on the icon — deliberately.
-
- Its two siblings here are Ant Design icons, which render at
- 1em and so inherit the button's 16px. Pinning this one to 15
- made it a pixel smaller than the ⋯ and × beside it, which is
- exactly the kind of one-pixel mismatch that reads as "off"
- without being obviously wrong. Left unset, react-icons also
- defaults to 1em and all three track the button together.
-
- Panel icons rather than chevrons: a bare chevron next to a
- close button is ambiguous (collapse? navigate? close?),
- whereas these draw the side panel itself getting wider or
- narrower, which is literally what the control does. */}
: }
onClick={() => setView((v) => (v === 'history' ? 'chat' : 'history'))}
@@ -1441,39 +1439,32 @@ const AIPanel = ({ isOpen, onClose }) => {
: }
- onClick={() => setIsWide((v) => !v)}
+ label="New conversation"
+ tooltip="New conversation"
+ icon={}
+ onClick={newConversation}
/>
- , isIconOnly: true, variant: 'ghost', size: 'sm' }}
- items={[
- // These were both wired to clearConversation, so the safe
- // option and the destructive one did the same thing. Now
- // "New" keeps the thread and "Delete" says that it does not.
- { label: 'New conversation', onClick: newConversation },
- { label: 'Conversation history', onClick: () => setView('history') },
- { label: 'Delete this conversation', onClick: clearConversation }
- ]}
+ : }
+ onClick={() => setIsWide((v) => !v)}
/>
}
+ icon={}
onClick={onClose}
/>
- {/* ---- page context ---- */}
- {pageContext.label} · Today · All locations
-
{/* ---- conversation / welcome ---- */}
@@ -1525,7 +1516,7 @@ const AIPanel = ({ isOpen, onClose }) => {
- Doormile AI
+ MileTruth
@@ -1563,7 +1554,7 @@ const AIPanel = ({ isOpen, onClose }) => {
)}
{/* ---- composer ---- */}
-
+
>,
document.body
diff --git a/src/components/assistant/DoormileAI/AIParts.jsx b/src/components/assistant/DoormileAI/AIParts.jsx
index 93d63dd..0193b3c 100644
--- a/src/components/assistant/DoormileAI/AIParts.jsx
+++ b/src/components/assistant/DoormileAI/AIParts.jsx
@@ -38,7 +38,7 @@ LiveIndicator.propTypes = { label: PropTypes.string };
// Thinking state. Three dots plus two shimmer bars — enough to read as
// "working", not enough to be a light show.
export const TypingIndicator = () => (
-
+
diff --git a/src/components/assistant/DoormileAI/AIWelcome.jsx b/src/components/assistant/DoormileAI/AIWelcome.jsx
index 4649b26..c4e56f1 100644
--- a/src/components/assistant/DoormileAI/AIWelcome.jsx
+++ b/src/components/assistant/DoormileAI/AIWelcome.jsx
@@ -77,7 +77,7 @@ const AIWelcome = () => {
How can I help with today’s operations?
- I read live orders, riders, vehicles and hubs. Every number comes from a real call — I never estimate one.
+ I read live orders, milers, vehicles and hubs. Every number comes from a real call — I never estimate one.
);
diff --git a/src/components/assistant/DoormileAI/index.jsx b/src/components/assistant/DoormileAI/index.jsx
index 34f488c..01c4a54 100644
--- a/src/components/assistant/DoormileAI/index.jsx
+++ b/src/components/assistant/DoormileAI/index.jsx
@@ -23,13 +23,13 @@ const DoormileAITrigger = () => {
return (
<>
-
+