updates on the changes and fix on the all the pages
This commit is contained in:
@@ -36,7 +36,8 @@ An in-console Q&A assistant that answers operator questions about live data —
|
||||
- **Selectors that style an Astryx Stack need two classes.** `padding={0}` emits a StyleX atomic at the same (0,1,0) specificity as a bare class, so `.dai-header` can lose on stylesheet order. Those rules are written `.dai-root .dai-header`. Don't "simplify" them back to one class. This never applies to `.dai-panel`/`.dai-scrim`, which carry `.dai-root` on the *same* element.
|
||||
- **The Doormile D is the assistant's identity, and `Spark` owns it.** Header, every reply, the welcome screen, the thinking state and the top-nav trigger all render `assets/images/doormile-mark.png` through that one component, so they can't drift apart. It replaced a white sparkle glyph, which is why the chip lost its gradient: the mark is red on a transparent ground and carries its own circular frame, so a coloured fill behind it fights the logo. The trigger's active state is a tinted surface for the same reason — an image can't be inverted to white the way an icon could.
|
||||
- **`--dai-accent` is the single accent knob.** It resolves to the app accent (black, root CLAUDE.md §6.2). Switching the assistant to Doormile red is one line in `DoormileAI.css`, not a hunt through components.
|
||||
- **Every page offers every suggestion.** The assistant answers about orders, riders, hubs and the rest regardless of which screen is open, so hiding a question because you're on Dispatch made it look narrower than it is. `getPageContext` appends the whole deduplicated catalog to each route's own list — the page still decides ORDER (its questions lead), not membership. `more` is retired; one flat list means one place a question can be.
|
||||
- **Each page offers ONLY its own suggestions.** `getPageContext` returns the route's context list and nothing else — no global set is appended. Asked for directly: on Orders the strip should be Orders questions, and likewise everywhere. This REVERSES the previous rule ("every page offers every suggestion"), which had already been walked back once from all twenty-one questions to a thirteen-question global set because Orders was offering "How many tenants do we have?" above the composer. Note it was barely observable before the change: every context declares at least four suggestions and the panel slices to four, so the globals mostly sat just past the cut — the behaviour was an accident of counting, not a rule. It is structural now. Dropping a question from a page's list does not remove it from the assistant: it stays typeable, stays a follow-up, and still leads on its own page.
|
||||
- **A route missing from `ROUTES` falls back to `DEFAULT_CONTEXT`**, which is the generic operations set — and that is how a page ends up offering hub questions beside a table of bookings. `/doormile/bookings` did exactly that until it was given its own context. `/doormile/app-users` and `/doormile/competitive-intel` still fall back deliberately: the intent catalog has no questions about app users or competitor branches, and inventing chips that resolve to nothing would break the rule two lines down. Give a page its own context when real intents exist for it, not before.
|
||||
- **Off-topic questions point at doormile.com, they don't get invented answers.** `aboutDoormile` is LAST in `INTENTS` so every operational intent gets first refusal, and its trigger is narrow on purpose — "how many doormile orders today" mentions the name but is an orders question. What it says is only what this console demonstrably does; nothing about the company, its coverage, pricing or history is in this app, and doormile.com is where that lives. The no-match state in `AIPanel.js` points there too.
|
||||
- **Every suggestion in `pageContext.js` must actually resolve** against `INTENTS`. A chip that returns "I can't answer that yet" is worse than no chip — check it before adding.
|
||||
|
||||
|
||||
@@ -925,50 +925,14 @@ body.dai-docked .astryx-layout-content {
|
||||
padding-inline: 2px;
|
||||
}
|
||||
/* --------------------------------------------------------------------------
|
||||
Trigger (lives in the app TopNav)
|
||||
The trigger's styles lived here and are gone with it.
|
||||
|
||||
`.dai-trigger` / `.dai-trigger-mark` styled DoormileAI/index.jsx, a trigger
|
||||
button component that nothing rendered — AdminLayout has always declared
|
||||
its own button in JSX with Tailwind classes. Component and CSS removed
|
||||
together so neither is left looking like the thing that controls the
|
||||
header button. To change that button, edit AdminLayout.
|
||||
-------------------------------------------------------------------------- */
|
||||
.dai-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
background-color 140ms ease,
|
||||
border-color 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
/* 30px box × 1.5 cancels the mark's transparent padding, so the visible D is
|
||||
30px inside the 40px button. */
|
||||
.dai-trigger-mark {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
max-width: none;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.dai-trigger:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dai-trigger:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.dai-trigger:focus-visible {
|
||||
outline: 2px solid var(--dai-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.dai-trigger[data-active='true'] {
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
border-color: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
/* --------------------------------------------------------------------------
|
||||
Responsive
|
||||
-------------------------------------------------------------------------- */
|
||||
@@ -1025,8 +989,7 @@ body.dai-docked .astryx-layout-content {
|
||||
.dai-typing span,
|
||||
.dai-shimmer,
|
||||
.dai-suggestion,
|
||||
.dai-send,
|
||||
.dai-trigger {
|
||||
.dai-send {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
@@ -38,7 +37,7 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [step.id]);
|
||||
|
||||
if (failed) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import { AiOutlineMore as MoreOutlined } from 'react-icons/ai';
|
||||
import {
|
||||
LuChevronDown,
|
||||
LuHistory,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
|
||||
import { IconButton } from '@/components/ds';
|
||||
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
@@ -36,7 +34,7 @@ import { executeAssign, executeRepeatAssign, describeRider } from '@/lib/assista
|
||||
import { startRepeatFlow, answerRepeatStep } from '@/lib/assistant/repeatFlow';
|
||||
import { buildRepeatRun, describeDay } from '@/lib/assistant/repeatRuns';
|
||||
import { getPageContext, getFollowUps, toChips, ORDER_CREATED, ORDER_CREATED_ASSIGNED } from './pageContext';
|
||||
import { Spark, LiveIndicator, TypingIndicator } from './AIParts';
|
||||
import { Spark, TypingIndicator } from './AIParts';
|
||||
import AIWelcome, { SuggestionChips } from './AIWelcome';
|
||||
import AIMessage from './AIMessage';
|
||||
import AIComposer from './AIComposer';
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TextArea } from '@astryxdesign/core/TextArea';
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||||
|
||||
import doormileMark from 'assets/images/doormile-mark.png';
|
||||
import AIPanel from './AIPanel';
|
||||
import '../DoormileAI.css';
|
||||
|
||||
// ==============================|| Doormile AI — trigger ||============================== //
|
||||
//
|
||||
// Mounted in AppTopNav. Owns only the open/closed state; everything else
|
||||
// lives in AIPanel. Focus returns here when the panel closes, so keyboard
|
||||
// users land back where they started.
|
||||
|
||||
const DoormileAITrigger = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip content="MileTruth">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
className="dai-root dai-trigger"
|
||||
data-active={isOpen}
|
||||
aria-label="Open MileTruth"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => (isOpen ? close() : setIsOpen(true))}
|
||||
>
|
||||
<img className="dai-trigger-mark" src={doormileMark} alt="" aria-hidden="true" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<AIPanel isOpen={isOpen} onClose={close} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoormileAITrigger;
|
||||
@@ -66,6 +66,9 @@ export const CHIP_LABELS = {
|
||||
'Total revenue this week': 'Revenue this week',
|
||||
'How many customers do we have?': 'Customers',
|
||||
'Assign a rider': 'Assign order',
|
||||
'How many tripsheets are dispatched?': 'Tripsheets',
|
||||
'How many open exceptions?': 'Open exceptions',
|
||||
'How many pricing rules are configured?': 'Pricing rules',
|
||||
"Repeat yesterday's orders": 'Repeat yesterday'
|
||||
};
|
||||
|
||||
@@ -167,6 +170,73 @@ const DISPATCH = {
|
||||
]
|
||||
};
|
||||
|
||||
// Linehaul and middle-mile runs.
|
||||
const TRIPSHEETS = {
|
||||
label: 'Tripsheets',
|
||||
suggestions: [
|
||||
{ icon: LuLayers, text: 'How many tripsheets are dispatched?' },
|
||||
{ icon: LuTruck, text: 'How many vehicles are available?' },
|
||||
{ icon: LuBike, text: 'How many riders are active?' },
|
||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
||||
],
|
||||
more: [{ icon: LuBuilding2, text: 'Current hub status' }]
|
||||
};
|
||||
|
||||
// The register of things that went wrong.
|
||||
const EXCEPTIONS = {
|
||||
label: 'Exceptions',
|
||||
suggestions: [
|
||||
{ icon: LuCircleDot, text: 'How many open exceptions?' },
|
||||
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
||||
{ icon: LuTimerOff, text: 'Which hubs are experiencing delays?' },
|
||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
||||
],
|
||||
more: []
|
||||
};
|
||||
|
||||
// Rate cards. Reached from the account menu rather than the bar, but it is a
|
||||
// real page and deserves its own questions like any other.
|
||||
const PRICING = {
|
||||
label: 'Pricing',
|
||||
suggestions: [
|
||||
{ icon: LuBanknote, text: 'How many pricing rules are configured?' },
|
||||
{ icon: LuBanknote, text: 'Total revenue today' },
|
||||
{ icon: LuUsers, text: 'How many tenants do we have?' },
|
||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
||||
],
|
||||
more: [{ icon: LuBanknote, text: 'Total revenue this week' }]
|
||||
};
|
||||
|
||||
// Bookings — what customers raised in the app, before it becomes an order.
|
||||
//
|
||||
// This page was not in ROUTES at all, so it fell through to DEFAULT_CONTEXT
|
||||
// and offered "How many riders are active?" and "Current hub status" beside a
|
||||
// table of B2C bookings. It is a main nav destination, so that was the most
|
||||
// visible instance of the problem this change fixes.
|
||||
//
|
||||
// No create actions: a booking here originates in the customer app, not in
|
||||
// this console, so "Create an order" would offer to make a different kind of
|
||||
// thing than the page shows.
|
||||
//
|
||||
// One honest limitation: these intents count ALL bookings, not only the
|
||||
// customer-app ones this page lists. They are the right questions to ask from
|
||||
// here and every one of them resolves, but the number will not match the row
|
||||
// count when the page is filtered to a source.
|
||||
const BOOKINGS = {
|
||||
label: 'Bookings',
|
||||
suggestions: [
|
||||
{ icon: LuPackage, text: 'How many orders today?' },
|
||||
{ icon: LuCircleDot, text: 'How many pending orders today?' },
|
||||
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
||||
],
|
||||
more: [
|
||||
{ icon: LuPackage, text: 'How many delivered orders today?' },
|
||||
{ icon: LuPackage, text: 'How many cancelled orders today?' },
|
||||
{ icon: LuClock3, text: 'Orders today vs yesterday' }
|
||||
]
|
||||
};
|
||||
|
||||
const TENANTS = {
|
||||
label: 'Tenants',
|
||||
suggestions: [
|
||||
@@ -283,6 +353,7 @@ const FOLLOW_UPS = {
|
||||
const ROUTES = [
|
||||
['/doormile/control-x', DISPATCH],
|
||||
['/doormile/deliveries', { ...ORDERS, label: 'Deliveries' }],
|
||||
['/doormile/bookings', BOOKINGS],
|
||||
['/doormile/orders', ORDERS],
|
||||
['/doormile/milers', RIDERS],
|
||||
['/doormile/riders', RIDERS],
|
||||
@@ -303,9 +374,19 @@ const ROUTES = [
|
||||
more: [{ icon: LuUsers, text: 'How many tenants do we have?' }]
|
||||
}
|
||||
],
|
||||
['/doormile/tripsheets', { ...DEFAULT_CONTEXT, label: 'Tripsheets' }],
|
||||
['/doormile/exceptions', { ...DEFAULT_CONTEXT, label: 'Exceptions' }],
|
||||
['/doormile/pricing', { ...DEFAULT_CONTEXT, label: 'Pricing' }]
|
||||
// These three were `{ ...DEFAULT_CONTEXT, label: '…' }` — a page-specific
|
||||
// NAME over the generic operations questions, so Tripsheets offered "Active
|
||||
// riders" and "Hub status" and nothing about tripsheets. The label made it
|
||||
// look intentional.
|
||||
//
|
||||
// Each now leads with its own resource. The phrasings come from the intents'
|
||||
// own documented examples (tripsheetStatus, exceptionStatus, pricingCount)
|
||||
// and were checked against their `match` predicates before being added here,
|
||||
// per the rule at the top of this file. None contains an order/booking word,
|
||||
// so none is shadowed by the order intents that run earlier in the catalog.
|
||||
['/doormile/tripsheets', TRIPSHEETS],
|
||||
['/doormile/exceptions', EXCEPTIONS],
|
||||
['/doormile/pricing', PRICING]
|
||||
];
|
||||
|
||||
// ---- every question, from every page ---------------------------------------
|
||||
@@ -322,8 +403,9 @@ const ALL_CONTEXTS = [ORDERS, RIDERS, VEHICLES, HUBS, DISPATCH, TENANTS, REPORTS
|
||||
|
||||
// Every question declared anywhere. Two jobs: it is the icon index behind
|
||||
// QUESTION_BY_TEXT (so a follow-up naming a question that is no longer global
|
||||
// still renders with the right icon), and it is the pool GLOBAL_SUGGESTIONS
|
||||
// filters. Do not narrow it — narrow GLOBAL_TEXTS instead.
|
||||
// still renders with the right icon). It no longer feeds a global suggestion
|
||||
// pool — see the note below. Do not narrow it: a question dropped from here
|
||||
// loses its icon everywhere, including as a follow-up.
|
||||
const EVERY_QUESTION = (() => {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
@@ -338,44 +420,38 @@ const EVERY_QUESTION = (() => {
|
||||
})();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The questions that ride along on EVERY page.
|
||||
// There is no longer a set of questions that rides along on every page.
|
||||
//
|
||||
// This used to be EVERY_QUESTION — all twenty-one, everywhere — which meant the
|
||||
// Orders page offered "How many tenants do we have?" and "Current hub status"
|
||||
// above the composer, questions an operator working orders is not asking. The
|
||||
// strip is a shortcut, and a shortcut that lists everything is a menu.
|
||||
// GLOBAL_TEXTS / GLOBAL_SUGGESTIONS lived here: a thirteen-question daily
|
||||
// operating picture appended to every context. It had already been narrowed
|
||||
// once, from all twenty-one, because the Orders page was offering "How many
|
||||
// tenants do we have?" above the composer. It is gone entirely now — a page
|
||||
// offers its own questions and nothing else.
|
||||
//
|
||||
// What is left is the daily operating picture: what came in, what is stuck,
|
||||
// what got out, what it earned, and the three ways to put work into the system.
|
||||
//
|
||||
// Dropping a question from THIS list does not remove it from the assistant —
|
||||
// it stays typeable, it stays a follow-up, and it still leads on its own page,
|
||||
// because withGlobalQuestions puts each page's own suggestions first. Hubs
|
||||
// still opens on hub questions; they just no longer follow you to Orders.
|
||||
// EVERY_QUESTION above stays. It is still the icon index behind
|
||||
// QUESTION_BY_TEXT, so a follow-up naming a question that no longer appears
|
||||
// on any page strip still renders with the right icon.
|
||||
// ---------------------------------------------------------------------------
|
||||
const GLOBAL_TEXTS = new Set([
|
||||
"Give me today's operations summary",
|
||||
'How many orders today?',
|
||||
'Which orders are delayed?',
|
||||
'How many pending orders today?',
|
||||
'How many delivered orders today?',
|
||||
'How many cancelled orders today?',
|
||||
'Morning Batch orders today',
|
||||
'Orders today vs yesterday',
|
||||
'Total revenue today',
|
||||
'How many riders are active?',
|
||||
'Create an order',
|
||||
REPEAT_TEXT,
|
||||
'Create multiple orders'
|
||||
]);
|
||||
|
||||
const GLOBAL_SUGGESTIONS = EVERY_QUESTION.filter((q) => GLOBAL_TEXTS.has(q.text));
|
||||
|
||||
// The page's own suggestions lead; the global set follows, deduplicated.
|
||||
const withGlobalQuestions = (context) => {
|
||||
// A page offers its OWN questions, and only those.
|
||||
//
|
||||
// The global set no longer rides along. Asked for directly: on Orders the
|
||||
// strip should be Orders questions, and the same on every other page.
|
||||
//
|
||||
// Worth being precise about what changed, because it is easy to read this as
|
||||
// a no-op. It very nearly was: every context happens to declare at least four
|
||||
// own suggestions and the panel slices to four, so in practice the globals sat
|
||||
// just past the cut on most pages. That made the current behaviour an accident
|
||||
// of counting rather than a rule — declare a context with three suggestions
|
||||
// and a hub question would surface on it. Now it cannot, whatever the counts.
|
||||
//
|
||||
// Dropping a question from a page's list does not remove it from the
|
||||
// assistant. It stays typeable, it stays a follow-up, and it still leads on
|
||||
// its own page. The strip is a shortcut, not the index.
|
||||
const ownQuestionsOnly = (context) => {
|
||||
const seen = new Set();
|
||||
const suggestions = [];
|
||||
[...(context.suggestions || []), ...(context.more || []), ...GLOBAL_SUGGESTIONS].forEach((q) => {
|
||||
[...(context.suggestions || []), ...(context.more || [])].forEach((q) => {
|
||||
if (seen.has(q.text)) return;
|
||||
seen.add(q.text);
|
||||
suggestions.push(q);
|
||||
@@ -385,7 +461,7 @@ const withGlobalQuestions = (context) => {
|
||||
|
||||
export const getPageContext = (pathname = '') => {
|
||||
const match = ROUTES.find(([prefix]) => pathname.startsWith(prefix));
|
||||
return withGlobalQuestions(match ? match[1] : DEFAULT_CONTEXT);
|
||||
return ownQuestionsOnly(match ? match[1] : DEFAULT_CONTEXT);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -191,10 +191,10 @@ export const executeRepeatAssign = async (createdPairs, rows) => {
|
||||
// Sequential on purpose. These are writes against real dispatch records, and
|
||||
// firing a burst of them concurrently makes a partial failure much harder to
|
||||
// report accurately — which order did not land, and to whom.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
for (const t of targets) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
await assignMilerToBooking(t.bookingid, { mileruserid: Number(t.mileruserid) });
|
||||
assigned += 1;
|
||||
assignedRiders.add(String(t.mileruserid));
|
||||
@@ -218,12 +218,12 @@ export const executeRepeatAssign = async (createdPairs, rows) => {
|
||||
// drops back should feel one buzz, not ten.
|
||||
let notified = 0;
|
||||
if (lookup) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
for (const userid of assignedRiders) {
|
||||
const rider = lookup.byUserId.get(userid);
|
||||
if (rider?.milerprofileid) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
await notifyRider(rider.milerprofileid);
|
||||
notified += 1;
|
||||
} catch {
|
||||
|
||||
@@ -124,18 +124,18 @@ export const resolveBulkRows = async (rows, { pickup, tenantid, cache, onProgres
|
||||
|
||||
if (hasCoords(row)) {
|
||||
located.push(row);
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
const key = cacheKey(row);
|
||||
if (cache?.has(key)) {
|
||||
located.push({ ...row, ...cache.get(key) });
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
onProgress?.({ phase: 'locate', done: i, total: rows.length, current: row.deliveryaddress });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const place = await geocodeAddress(`${row.deliveryaddress} ${row.deliverypincode}`).catch(() => null);
|
||||
const found = {
|
||||
deliverylatitude: place?.geometry?.location?.lat?.(),
|
||||
@@ -147,7 +147,7 @@ export const resolveBulkRows = async (rows, { pickup, tenantid, cache, onProgres
|
||||
|
||||
// Only wait after a real request. A cache hit or a sheet coordinate costs
|
||||
// nothing, which is what makes a re-run fast.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
if (i < rows.length - 1) await sleep(GEOCODE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,15 +128,15 @@ export const priceBulkRows = async (rows, pickup, tenantid, { onProgress, should
|
||||
|
||||
if (String(row.finalprice ?? '') !== '') {
|
||||
out.push(row);
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!match) {
|
||||
out.push({ ...row, priceError: 'no pricing configured for this tenant' });
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const km = await calculateDrivingDistance(
|
||||
{ latitude: pickup?.latitude, longitude: pickup?.longitude },
|
||||
{ latitude: row.deliverylatitude, longitude: row.deliverylongitude }
|
||||
@@ -144,7 +144,7 @@ export const priceBulkRows = async (rows, pickup, tenantid, { onProgress, should
|
||||
|
||||
if (km == null) {
|
||||
out.push({ ...row, priceError: 'could not measure the distance' });
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
const total = calculateTotalCharge(km, match.baseprice, match.priceperkm, match.basedistance);
|
||||
@@ -209,7 +209,7 @@ export const executeCreateBulk = async (rows, shared) => {
|
||||
const batch = batches[b];
|
||||
const label = batches.length > 1 ? ` (batch ${b + 1}/${batches.length})` : '';
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const res = await createExpressBookingBulk(batch);
|
||||
// Three shapes, most-nested first. The live endpoint returns
|
||||
// { data: { results: [ { index, success, bookingid, bookingno } ] } }
|
||||
@@ -236,7 +236,7 @@ export const executeCreateBulk = async (rows, shared) => {
|
||||
status: 'error',
|
||||
errorMessage: res.message || 'Rejected'
|
||||
});
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export const advance = (flow) => {
|
||||
const already = s.field === 'name' ? draft.firstname : draft[s.field];
|
||||
if (already) {
|
||||
step += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
return { flow: { ...flow, step }, ask: s.ask, done: false };
|
||||
|
||||
@@ -37,18 +37,18 @@ export const advanceFlow = async (steps, flow) => {
|
||||
const s = steps[step];
|
||||
if (!applicable(s, draft) || draft[s.id] !== undefined) {
|
||||
step += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s.auto) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const auto = await s.auto(draft);
|
||||
if (auto?.patch) draft = { ...draft, ...auto.patch };
|
||||
if (auto?.value !== undefined) {
|
||||
draft = s.apply(draft, auto.value);
|
||||
step += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
continue;
|
||||
}
|
||||
return { flow: { ...flow, step, draft }, step: s, ask: auto?.ask || s.ask, done: false };
|
||||
|
||||
@@ -386,7 +386,7 @@ const fetchBookingsInRange = async (start, end) => {
|
||||
let lastPageFetched = 1;
|
||||
|
||||
for (let page = 2; page <= budget && !stoppedEarly; page += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const next = await getBookingsPageCached(page);
|
||||
lastPageFetched = page;
|
||||
if (!next.rows.length) {
|
||||
@@ -2126,7 +2126,7 @@ const matchAndRun = async (text) => {
|
||||
for (const intent of INTENTS) {
|
||||
const params = intent.match(text);
|
||||
if (!params) continue;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const result = await intent.run(params);
|
||||
if (result) return { ...result, intentId: intent.id, params };
|
||||
}
|
||||
@@ -2194,7 +2194,7 @@ const answerMultiPart = async (text) => {
|
||||
if (segments.length < 2) return null;
|
||||
const results = [];
|
||||
for (const segment of segments) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const r = await matchAndRun(segment);
|
||||
if (r) results.push(r);
|
||||
}
|
||||
@@ -2243,7 +2243,7 @@ export async function answerQuestion(text, context = {}) {
|
||||
// WHAT. Embeddings are good at the former and unreliable at the latter.
|
||||
const params = intent?.match(normalized);
|
||||
if (intent && params) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const result = await intent.run(params);
|
||||
if (result) return { ...result, intentId: intent.id, params, routing: routed };
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export const buildRepeatRun = async (day, { onProgress, shouldStop } = {}) => {
|
||||
if (shouldStop?.()) break;
|
||||
const row = candidates[i];
|
||||
onProgress?.({ phase: 'price', done: i, total: candidates.length, current: row.customer_name });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
const [out] = await priceBulkRows([row], row.__pickup, row.tenantid);
|
||||
priced.push(out);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user