updates on the issue and the another thing updates on the ai optimization
This commit is contained in:
88
src/components/assistant/DoormileAI/AIAddressStep.jsx
Normal file
88
src/components/assistant/DoormileAI/AIAddressStep.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import AddressAutocomplete from '@/components/doormile/AddressAutocomplete';
|
||||
|
||||
// ==============================|| Doormile AI — the address turn ||============================== //
|
||||
//
|
||||
// The address step, as a real search field rather than a line typed into the
|
||||
// chat box.
|
||||
//
|
||||
// It used to be an ordinary `text` step: the assistant asked for "the full
|
||||
// address" and the operator typed one into the composer, which was then
|
||||
// geocoded blind. Two things were wrong with that. They got no suggestions, so
|
||||
// an area or street had to be remembered and spelled the way the geocoder
|
||||
// wanted; and when it missed, all that came back was "I couldn't find that
|
||||
// address", with no sight of what it HAD found.
|
||||
//
|
||||
// This is the same `AddressAutocomplete` the Create Order page uses, so the
|
||||
// two surfaces resolve an address the same way and cannot drift.
|
||||
//
|
||||
// Picking from the list also hands the flow the coordinates the operator
|
||||
// actually chose, rather than whatever a second geocode of the same words
|
||||
// returns — see the `place` branch in orderFlow's resolve. That matters: the
|
||||
// dispatch optimiser routes on coordinates, and re-geocoding a label can
|
||||
// legitimately return a different place.
|
||||
//
|
||||
// Typing free text and pressing the composer still works. This is an extra way
|
||||
// in, not a replacement for one.
|
||||
const AIAddressStep = ({ step, isBusy, onChoose, onCancel }) => {
|
||||
const [text, setText] = useState('');
|
||||
const [picked, setPicked] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="w-full border border-slate-200/90 rounded-2xl bg-white p-3.5 sm:p-4 mb-3.5 shadow-3xs space-y-3">
|
||||
<div className="text-xs font-bold text-slate-800">{step.ask}</div>
|
||||
|
||||
<AddressAutocomplete
|
||||
id={`ai-address-${step.id}`}
|
||||
value={text}
|
||||
disabled={isBusy}
|
||||
onChange={(v) => {
|
||||
setText(v);
|
||||
// Editing after a pick invalidates it. Those coordinates belonged to
|
||||
// the old selection and must not ride along with new words — that is
|
||||
// how a booking ends up located somewhere nobody chose.
|
||||
setPicked(null);
|
||||
}}
|
||||
onPlaceSelected={(place) => {
|
||||
setPicked(place);
|
||||
setText(place?.formatted_address || place?.name || '');
|
||||
}}
|
||||
/>
|
||||
|
||||
{picked && (
|
||||
<div className="text-[11px] text-emerald-700 font-medium">
|
||||
Location set — {picked.city || picked.suburb || 'coordinates captured'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy || !text.trim()}
|
||||
onClick={() => onChoose(text.trim(), picked ? { place: picked } : undefined)}
|
||||
className="px-4 py-2 bg-brand hover:bg-brand-dark text-white font-semibold text-xs rounded-xl shadow-xs active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed transition-all cursor-pointer"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-3 py-2 text-slate-500 hover:text-slate-800 text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AIAddressStep.propTypes = {
|
||||
step: PropTypes.shape({ id: PropTypes.string, ask: PropTypes.string }).isRequired,
|
||||
isBusy: PropTypes.bool,
|
||||
onChoose: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AIAddressStep;
|
||||
@@ -12,6 +12,7 @@ import { IconButton } from '@/components/ds';
|
||||
|
||||
import { Spark, Metric, StatGrid, StateBlock, AnswerList } from './AIParts';
|
||||
import AIFlowStep from './AIFlowStep';
|
||||
import AIAddressStep from './AIAddressStep';
|
||||
import AIRowsStep from './AIRowsStep';
|
||||
import { failedRowsCsv, downloadCsv } from '@/lib/assistant/bulkFile';
|
||||
|
||||
@@ -127,6 +128,19 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* The address turn. Without this branch the step passed the gate in
|
||||
AIPanel and nothing drew it — the question appeared with an empty
|
||||
space beneath it, which is worse than the free-text version it
|
||||
replaced. */}
|
||||
{message.flowStep?.type === 'address' && !message.flowStep.__answered && (
|
||||
<AIAddressStep
|
||||
step={message.flowStep}
|
||||
isBusy={message.flowStep.__answered}
|
||||
onChoose={(value, option) => onChooseStep(message, value, option)}
|
||||
onCancel={() => onCancelAction(message)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.flowStep?.type === 'select' && (
|
||||
<AIFlowStep
|
||||
step={message.flowStep}
|
||||
|
||||
@@ -400,7 +400,11 @@ const AIPanel = ({ isOpen, onClose }) => {
|
||||
text: next.ask || step.ask,
|
||||
// A `select` or `rows` step carries the step definition so AIMessage
|
||||
// can render its control; a `text` step is answered in the composer.
|
||||
flowStep: step.type === 'select' || step.type === 'rows' ? step : undefined,
|
||||
// 'address' joins the list so the sidebar gets the same search
|
||||
// field as Home. A step type absent here falls through to the plain
|
||||
// composer, which is how the address step ended up asking for a full
|
||||
// address as free text.
|
||||
flowStep: ['select', 'rows', 'address'].includes(step.type) ? step : undefined,
|
||||
flowDraft: next.flow.draft
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -2090,30 +2090,6 @@ const INTENTS = [
|
||||
|
||||
const INTENTS_BY_ID = Object.fromEntries(INTENTS.map((i) => [i.id, i]));
|
||||
|
||||
// Which intent the ordered catalog reaches for a question, WITHOUT running it.
|
||||
//
|
||||
// Exported for the page-suggestion test. The catalog is ordered and the first
|
||||
// match wins, so "something matches this chip" and "the right thing matches
|
||||
// this chip" are different questions — ROADMAP.md B1/B2 are two shipped cases
|
||||
// where they had different answers.
|
||||
//
|
||||
// Covers the deterministic pass only. answerQuestion tries semantic routing
|
||||
// and the multi-part splitter ahead of this, both of which are additive: they
|
||||
// can route a question the catalog would miss, never away from one it hits.
|
||||
export const resolveIntentId = (text) => {
|
||||
const normalized = correctTypos(text);
|
||||
for (const intent of INTENTS) {
|
||||
let params = null;
|
||||
try {
|
||||
params = intent.match(normalized);
|
||||
} catch {
|
||||
params = null;
|
||||
}
|
||||
if (params) return intent.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const SUPPORTED_QUESTIONS = INTENTS.map((i) => i.label);
|
||||
|
||||
// Clean, directly-askable example phrasings — for "recommended question"
|
||||
|
||||
Reference in New Issue
Block a user