updates on the ai bot and the deliveries page design

This commit is contained in:
2026-08-21 17:24:54 +05:30
parent 414ba6dbf2
commit 0f8510a3ea
3 changed files with 51 additions and 12 deletions

View File

@@ -26,7 +26,7 @@ import { startAssignFlow, answerAssignStep } from '../assignFlow';
import { executeAssign, executeRepeatAssign, describeRider } from '../assignActions';
import { startRepeatFlow, answerRepeatStep } from '../repeatFlow';
import { buildRepeatRun, describeDay } from '../repeatRuns';
import { getPageContext, getFollowUps, toChips, ORDER_CREATED } from './pageContext';
import { getPageContext, getFollowUps, toChips, ORDER_CREATED, ORDER_CREATED_ASSIGNED } from './pageContext';
import { Spark, LiveIndicator, TypingIndicator } from './AIParts';
import AIWelcome, { SuggestionChips } from './AIWelcome';
import AIMessage from './AIMessage';
@@ -1133,9 +1133,6 @@ const AIPanel = ({ isOpen, onClose }) => {
status: res.failed && !res.created ? 'failed' : 'done',
error: res.failed ? `${res.failed} row(s) failed` : undefined
});
// Partial success still means orders exist and need riders. Only a run
// where nothing landed leaves nothing to assign.
if (res.created > 0) setOutcomeKey(ORDER_CREATED);
// A repeat carries yesterday's rider on each row, so the new copies go
// straight back to whoever ran them — no second question. Rows with no
// previous rider simply stay pending, and a CSV import has none at all,
@@ -1145,14 +1142,23 @@ const AIPanel = ({ isOpen, onClose }) => {
: null;
if (repeatAssign?.assigned) refreshPageData();
// Remember exactly which ones, so "Assign order" can act on them —
// minus any that just got a rider automatically.
// Which of the new orders still need a rider — the ones whose source
// had none, plus any the re-assign could not place.
const failedIds = new Set((repeatAssign?.failures || []).map((f) => f.bookingid));
const stillUnassigned = repeatAssign
? (res.createdPairs || [])
.filter(({ index }) => rows?.[index]?.__previousMilerUserId == null)
.filter(({ index, bookingid }) => rows?.[index]?.__previousMilerUserId == null || failedIds.has(bookingid))
.map((p) => p.bookingid)
: res.createdIds;
if (stillUnassigned?.length) setLastCreated({ ids: stillUnassigned, booking: null });
// Set AFTER the assign, because which follow-ups make sense depends on
// whether anything is still waiting for a rider. A run where every
// order was placed automatically must not then suggest "Assign order" —
// that offers finished work as a pending step.
if (res.created > 0) {
setOutcomeKey(repeatAssign?.assigned && !stillUnassigned.length ? ORDER_CREATED_ASSIGNED : ORDER_CREATED);
}
push({
sender: 'assistant',
text: `Created ${res.created} of ${rows.length} order${rows.length === 1 ? '' : 's'}.`,
@@ -1170,10 +1176,21 @@ const AIPanel = ({ isOpen, onClose }) => {
// Stated plainly, including what was NOT done. An operator who
// believes all 13 went back to their riders will not check the
// three that stayed pending.
// Assignment and notification are reported separately, because
// they fail separately. This line used to end "notified
// automatically" on the strength of `assigned` alone — so a notify
// returning 500 (seen live) was still announced as a delivered
// push. Same mistake executeAssign already had; it does not get to
// survive here just because the wording is friendlier.
repeatAssign?.assigned
? `${repeatAssign.assigned} went straight back to yesterday's rider across ${repeatAssign.riders} rider${
repeatAssign.riders === 1 ? '' : 's'
}, notified automatically.`
}.`
: null,
repeatAssign?.assigned
? repeatAssign.notified === repeatAssign.riders
? `${repeatAssign.notified === 1 ? 'That rider was' : 'Those riders were'} notified.`
: `${repeatAssign.riders - repeatAssign.notified} of ${repeatAssign.riders} could not be notified — tell them directly. The orders ARE assigned.`
: null,
repeatAssign?.skipped ? `${repeatAssign.skipped} had no rider yesterday and are waiting for one.` : null,
repeatAssign?.failures?.length

View File

@@ -88,6 +88,15 @@ const REPEAT_TEXT = "Repeat yesterday's orders";
// side of the write.
export const ORDER_CREATED = '__orderCreated';
// The same write, but with every new order already given a rider — a repeat run
// that handed each copy back to whoever ran it yesterday.
//
// It needs its own key purely so ASSIGN_TEXT can be left out. Offering "Assign
// order" after the bot has just assigned them all presents finished work as a
// pending step, and an operator who takes the suggestion is walked into
// reassigning orders that are already out.
export const ORDER_CREATED_ASSIGNED = '__orderCreatedAssigned';
const ORDERS = {
label: 'Orders',
suggestions: [
@@ -230,6 +239,8 @@ const FOLLOW_UPS = {
'Create a customer': ['Create an order', 'How many customers do we have?'],
[ASSIGN_TEXT]: ['How many pending orders today?', 'Which orders are delayed?', 'How many riders are active?'],
[ORDER_CREATED]: [ASSIGN_TEXT, 'How many pending orders today?', 'Which orders are delayed?', 'Create an order'],
// No ASSIGN_TEXT: there is nothing left to assign.
[ORDER_CREATED_ASSIGNED]: ['How many riders are active?', 'How many pending orders today?', 'Which orders are delayed?', 'Create an order'],
// --- order volume ---
"Give me today's operations summary": [

View File

@@ -867,25 +867,36 @@ const Deliveries = () => {
</div>
);
// hasTruncateTooltip={false} on every Text in this cell.
//
// Astryx's Text turns its own truncation tooltip ON by default as
// soon as maxLines > 0, so each of these four lines was offering to
// re-show its own clipped string on hover. Nested inside the route
// Tooltip, that means hovering the address pops a bare address over
// the pickup/drop card that already contains it — two tooltips
// racing to explain the same cell.
//
// The route tooltip carries the full pickup AND drop address, so
// nothing is lost by silencing the per-line ones.
return (
<Tooltip content={routeTooltip}>
<VStack gap={0.5} padding={0} minWidth={0} style={{ cursor: 'pointer' }}>
<HStack gap={1} vAlign="center" minWidth={0}>
<img src="/pickup-icon.png" alt="Pickup" style={{ width: 16, height: 16, flexShrink: 0 }} />
<Text weight="semibold" maxLines={1} style={{ fontSize: 12, flexShrink: 0 }}>
<Text weight="semibold" maxLines={1} hasTruncateTooltip={false} style={{ fontSize: 12, flexShrink: 0 }}>
{r.pickupcustomer || 'Pickup'}
</Text>
<Text type="supporting" color="secondary" maxLines={1} style={{ fontSize: 11 }}>
<Text type="supporting" color="secondary" maxLines={1} hasTruncateTooltip={false} style={{ fontSize: 11 }}>
{pickupAddr ? `· ${pickupAddr}` : ''}
</Text>
</HStack>
<HStack gap={1} vAlign="center" minWidth={0}>
<img src="/drop-icon.png" alt="Drop" style={{ width: 17, height: 17, flexShrink: 0 }} />
<Text weight="semibold" maxLines={1} style={{ fontSize: 12, flexShrink: 0 }}>
<Text weight="semibold" maxLines={1} hasTruncateTooltip={false} style={{ fontSize: 12, flexShrink: 0 }}>
{r.deliverycustomer || 'Drop'}
</Text>
<Text type="supporting" color="secondary" maxLines={1} style={{ fontSize: 11 }}>
<Text type="supporting" color="secondary" maxLines={1} hasTruncateTooltip={false} style={{ fontSize: 11 }}>
{dropAddr ? `· ${dropAddr}` : ''}
</Text>
</HStack>