updates on the ai bot and the deliveries page design
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user