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 { executeAssign, executeRepeatAssign, describeRider } from '../assignActions';
|
||||||
import { startRepeatFlow, answerRepeatStep } from '../repeatFlow';
|
import { startRepeatFlow, answerRepeatStep } from '../repeatFlow';
|
||||||
import { buildRepeatRun, describeDay } from '../repeatRuns';
|
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 { Spark, LiveIndicator, TypingIndicator } from './AIParts';
|
||||||
import AIWelcome, { SuggestionChips } from './AIWelcome';
|
import AIWelcome, { SuggestionChips } from './AIWelcome';
|
||||||
import AIMessage from './AIMessage';
|
import AIMessage from './AIMessage';
|
||||||
@@ -1133,9 +1133,6 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
status: res.failed && !res.created ? 'failed' : 'done',
|
status: res.failed && !res.created ? 'failed' : 'done',
|
||||||
error: res.failed ? `${res.failed} row(s) failed` : undefined
|
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
|
// 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
|
// 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,
|
// previous rider simply stay pending, and a CSV import has none at all,
|
||||||
@@ -1145,14 +1142,23 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
: null;
|
: null;
|
||||||
if (repeatAssign?.assigned) refreshPageData();
|
if (repeatAssign?.assigned) refreshPageData();
|
||||||
|
|
||||||
// Remember exactly which ones, so "Assign order" can act on them —
|
// Which of the new orders still need a rider — the ones whose source
|
||||||
// minus any that just got a rider automatically.
|
// had none, plus any the re-assign could not place.
|
||||||
|
const failedIds = new Set((repeatAssign?.failures || []).map((f) => f.bookingid));
|
||||||
const stillUnassigned = repeatAssign
|
const stillUnassigned = repeatAssign
|
||||||
? (res.createdPairs || [])
|
? (res.createdPairs || [])
|
||||||
.filter(({ index }) => rows?.[index]?.__previousMilerUserId == null)
|
.filter(({ index, bookingid }) => rows?.[index]?.__previousMilerUserId == null || failedIds.has(bookingid))
|
||||||
.map((p) => p.bookingid)
|
.map((p) => p.bookingid)
|
||||||
: res.createdIds;
|
: res.createdIds;
|
||||||
if (stillUnassigned?.length) setLastCreated({ ids: stillUnassigned, booking: null });
|
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({
|
push({
|
||||||
sender: 'assistant',
|
sender: 'assistant',
|
||||||
text: `Created ${res.created} of ${rows.length} order${rows.length === 1 ? '' : 's'}.`,
|
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
|
// Stated plainly, including what was NOT done. An operator who
|
||||||
// believes all 13 went back to their riders will not check the
|
// believes all 13 went back to their riders will not check the
|
||||||
// three that stayed pending.
|
// 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
|
||||||
? `${repeatAssign.assigned} went straight back to yesterday's rider across ${repeatAssign.riders} rider${
|
? `${repeatAssign.assigned} went straight back to yesterday's rider across ${repeatAssign.riders} rider${
|
||||||
repeatAssign.riders === 1 ? '' : 's'
|
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,
|
: null,
|
||||||
repeatAssign?.skipped ? `${repeatAssign.skipped} had no rider yesterday and are waiting for one.` : null,
|
repeatAssign?.skipped ? `${repeatAssign.skipped} had no rider yesterday and are waiting for one.` : null,
|
||||||
repeatAssign?.failures?.length
|
repeatAssign?.failures?.length
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ const REPEAT_TEXT = "Repeat yesterday's orders";
|
|||||||
// side of the write.
|
// side of the write.
|
||||||
export const ORDER_CREATED = '__orderCreated';
|
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 = {
|
const ORDERS = {
|
||||||
label: 'Orders',
|
label: 'Orders',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
@@ -230,6 +239,8 @@ const FOLLOW_UPS = {
|
|||||||
'Create a customer': ['Create an order', 'How many customers do we have?'],
|
'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?'],
|
[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'],
|
[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 ---
|
// --- order volume ---
|
||||||
"Give me today's operations summary": [
|
"Give me today's operations summary": [
|
||||||
|
|||||||
@@ -867,25 +867,36 @@ const Deliveries = () => {
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<Tooltip content={routeTooltip}>
|
<Tooltip content={routeTooltip}>
|
||||||
<VStack gap={0.5} padding={0} minWidth={0} style={{ cursor: 'pointer' }}>
|
<VStack gap={0.5} padding={0} minWidth={0} style={{ cursor: 'pointer' }}>
|
||||||
<HStack gap={1} vAlign="center" minWidth={0}>
|
<HStack gap={1} vAlign="center" minWidth={0}>
|
||||||
<img src="/pickup-icon.png" alt="Pickup" style={{ width: 16, height: 16, flexShrink: 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'}
|
{r.pickupcustomer || 'Pickup'}
|
||||||
</Text>
|
</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}` : ''}
|
{pickupAddr ? `· ${pickupAddr}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack gap={1} vAlign="center" minWidth={0}>
|
<HStack gap={1} vAlign="center" minWidth={0}>
|
||||||
<img src="/drop-icon.png" alt="Drop" style={{ width: 17, height: 17, flexShrink: 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'}
|
{r.deliverycustomer || 'Drop'}
|
||||||
</Text>
|
</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}` : ''}
|
{dropAddr ? `· ${dropAddr}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
Reference in New Issue
Block a user