updates on the ai bot and the queries and the delivereis page
This commit is contained in:
@@ -953,40 +953,50 @@ body.dai-docked .astryx-layout-content {
|
||||
}
|
||||
}
|
||||
/* --------------------------------------------------------------------------
|
||||
Write-action confirm card
|
||||
Write-action confirm card & buttons
|
||||
-------------------------------------------------------------------------- */
|
||||
.dai-root .dai-action {
|
||||
padding: 10px 11px;
|
||||
border: 1px solid var(--dai-border-strong);
|
||||
border-radius: 5px;
|
||||
background: var(--dai-surface-alt);
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.dai-root .dai-action-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 5px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
color: #4f46e5;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
.dai-root .dai-action-summary {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
font-weight: 550;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
color: var(--dai-text);
|
||||
}
|
||||
.dai-root .dai-action-note {
|
||||
font-size: 12px;
|
||||
color: var(--dai-text-secondary);
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.dai-root .dai-ok {
|
||||
color: #047857;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dai-root .dai-err {
|
||||
color: #b91c1c;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dai-root .dai-action button {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* --------------------------------------------------------------------------
|
||||
Overflow containment
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 '@astryxdesign/core/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
|
||||
// ==============================|| Doormile AI — one dropdown step ||============================== //
|
||||
@@ -70,7 +70,16 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
|
||||
? 'This tenant has no saved business locations — add one on the Tenants page before creating an order.'
|
||||
: step.emptyHint || 'There’s nothing to choose from here.'}
|
||||
</Text>
|
||||
<Button label="Cancel" variant="secondary" size="sm" onClick={onCancel} />
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -90,19 +99,26 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
|
||||
hasSearch={options.length > 8}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
<HStack gap={1.5} padding={0}>
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button
|
||||
label="Continue"
|
||||
variant="primary"
|
||||
variant="default"
|
||||
size="sm"
|
||||
isDisabled={!value || isBusy}
|
||||
// The whole option is passed back, not just its value — steps that
|
||||
// pick a record (a location, a customer) need the record itself to
|
||||
// fill several fields at once.
|
||||
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer disabled:opacity-50"
|
||||
disabled={!value || isBusy}
|
||||
onClick={() => onChoose(value, chosen)}
|
||||
/>
|
||||
<Button label="Cancel" variant="secondary" size="sm" isDisabled={isBusy} onClick={onCancel} />
|
||||
</HStack>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
|
||||
disabled={isBusy}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ import { LuChevronDown } from 'react-icons/lu';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChatToolCalls } from '@astryxdesign/core/Chat';
|
||||
|
||||
import { Spark, Metric, StatGrid, StateBlock, AnswerList } from './AIParts';
|
||||
@@ -138,35 +137,46 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
|
||||
{['confirmCustomer', 'confirmOrder', 'confirmBulk', 'confirmAssign'].includes(message.form?.kind) && (
|
||||
<VStack className="dai-action" gap={1.5} padding={0}>
|
||||
{message.form.status === 'open' ? (
|
||||
<HStack gap={1.5} padding={0}>
|
||||
<div className="flex flex-wrap items-center gap-2 pt-1">
|
||||
<Button
|
||||
label={
|
||||
message.form.confirmLabel
|
||||
? message.form.confirmLabel
|
||||
: message.form.kind === 'confirmBulk'
|
||||
? `Create ${message.form.draft?.rows?.length || 0} orders`
|
||||
: message.form.kind === 'confirmOrder'
|
||||
? 'Create order'
|
||||
: message.form.kind === 'confirmAssign'
|
||||
? 'Assign rider'
|
||||
: 'Submit'
|
||||
}
|
||||
variant="primary"
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer"
|
||||
onClick={() => onSubmitForm(message, message.form.payload || message.form.draft)}
|
||||
/>
|
||||
<Button label="Cancel" variant="secondary" size="sm" onClick={() => onCancelAction(message)} />
|
||||
>
|
||||
{message.form.confirmLabel
|
||||
? message.form.confirmLabel
|
||||
: message.form.kind === 'confirmBulk'
|
||||
? `Create ${message.form.draft?.rows?.length || 0} orders`
|
||||
: message.form.kind === 'confirmOrder'
|
||||
? 'Create order'
|
||||
: message.form.kind === 'confirmAssign'
|
||||
? 'Assign rider'
|
||||
: 'Submit'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
|
||||
onClick={() => onCancelAction(message)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{/* A second, wider commit — used when rows were excluded by
|
||||
default but including them is a legitimate choice. */}
|
||||
{message.form.altDraft && (
|
||||
<Button
|
||||
label={message.form.altLabel || 'Include everything'}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-600 hover:bg-slate-100 font-medium text-xs px-3 h-8 rounded-lg cursor-pointer"
|
||||
onClick={() => onSubmitForm(message, message.form.altDraft)}
|
||||
/>
|
||||
>
|
||||
{message.form.altLabel || 'Include everything'}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
) : (
|
||||
<Text className={`dai-action-note${message.form.status === 'done' ? ' dai-ok' : ''}`}>
|
||||
{message.form.status === 'done'
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 '@astryxdesign/core/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TextArea } from '@astryxdesign/core/TextArea';
|
||||
import { FileInput } from '@astryxdesign/core/FileInput';
|
||||
|
||||
@@ -23,41 +23,38 @@ import { parseBulkFile, templateCsv, downloadCsv } from '@/lib/assistant/bulkFil
|
||||
//
|
||||
// What is reported before the operator can continue, and why each matters:
|
||||
// • rows read — so a truncated or half-empty sheet is visible immediately
|
||||
// • columns NOT recognised, by name — an unread price column would otherwise
|
||||
// silently re-quote every row
|
||||
// • rows repeated inside the file — the bulk endpoint has no idempotency key,
|
||||
// so a duplicate that gets through is a second real rider at one door
|
||||
|
||||
const PLACEHOLDER = `Ramesh Kumar, 9876543210, 12 Cross Cut Rd Gandhipuram, 641012, Coimbatore, 120, 2 meal boxes
|
||||
Priya N, 9876500011, 45 RS Puram, 641002, Coimbatore, 95, documents`;
|
||||
|
||||
// • unusable rows — bad phones, empty lines, missing fields; surfaced
|
||||
// up-front so the operator can fix the sheet instead of creating 9 good
|
||||
// orders and wondering where the 10th went
|
||||
// • columns ignored — confirms the resolver didn't mistake an unknown
|
||||
// column for a standard one
|
||||
const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
|
||||
const [file, setFile] = useState(null);
|
||||
const [fileParse, setFileParse] = useState(null);
|
||||
const [fileError, setFileError] = useState(null);
|
||||
const [raw, setRaw] = useState('');
|
||||
const [text, setText] = useState('');
|
||||
const [isReading, setIsReading] = useState(false);
|
||||
const [parsed, setParsed] = useState(null);
|
||||
|
||||
const pasted = raw.trim() ? parseBulkRows(raw) : null;
|
||||
// A loaded file wins; clearing it falls back to whatever is in the textarea.
|
||||
const parsed = fileParse || pasted;
|
||||
|
||||
const handleFile = async (picked) => {
|
||||
const chosen = Array.isArray(picked) ? picked[0] : picked;
|
||||
setFile(chosen || null);
|
||||
setFileError(null);
|
||||
if (!chosen) {
|
||||
setFileParse(null);
|
||||
const handlePasteChange = (val) => {
|
||||
setText(val);
|
||||
if (!val.trim()) {
|
||||
setParsed(null);
|
||||
return;
|
||||
}
|
||||
setParsed(parseBulkRows(val));
|
||||
};
|
||||
|
||||
const handleFilePicked = async (files) => {
|
||||
const file = Array.isArray(files) ? files[0] : files;
|
||||
if (!file) return;
|
||||
setIsReading(true);
|
||||
try {
|
||||
const result = await parseBulkFile(chosen);
|
||||
setFileParse(result);
|
||||
setRaw('');
|
||||
} catch (err) {
|
||||
setFileParse(null);
|
||||
setFileError(err.message || 'Couldn’t read that file.');
|
||||
const res = await parseBulkFile(file);
|
||||
setParsed(res);
|
||||
} catch {
|
||||
setParsed({
|
||||
rows: [],
|
||||
errors: [{ row: 0, reason: 'Failed to read file' }],
|
||||
ignoredColumns: []
|
||||
});
|
||||
} finally {
|
||||
setIsReading(false);
|
||||
}
|
||||
@@ -66,51 +63,46 @@ const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
|
||||
return (
|
||||
<VStack className="dai-action" gap={1.5} padding={0}>
|
||||
<FileInput
|
||||
label="Order sheet"
|
||||
isLabelHidden
|
||||
accept=".csv,.xls,.xlsx"
|
||||
value={file}
|
||||
onChange={handleFile}
|
||||
label="Upload spreadsheet (CSV, Excel)"
|
||||
accept=".csv,.xlsx,.xls,.tsv"
|
||||
onChange={handleFilePicked}
|
||||
isDisabled={isBusy || isReading}
|
||||
/>
|
||||
|
||||
{!fileParse && (
|
||||
<TextArea label="Or paste rows" rows={4} value={raw} onChange={setRaw} placeholder={PLACEHOLDER} isDisabled={isBusy} />
|
||||
)}
|
||||
<TextArea
|
||||
label="Or paste rows"
|
||||
placeholder="Paste CSV / spreadsheet rows here…"
|
||||
value={text}
|
||||
rows={4}
|
||||
onChange={handlePasteChange}
|
||||
isDisabled={isBusy || isReading}
|
||||
/>
|
||||
|
||||
{isReading && <Text className="dai-action-note">Reading the file…</Text>}
|
||||
{fileError && <Text className="dai-field-err">{fileError}</Text>}
|
||||
|
||||
{parsed && (
|
||||
<VStack gap={0.5} padding={0}>
|
||||
<Text className="dai-action-note">
|
||||
{fileParse ? `${fileParse.sheetName} — ` : ''}
|
||||
{parsed.rows.length} complete row{parsed.rows.length === 1 ? '' : 's'}
|
||||
{parsed.errors.length ? ` · ${parsed.errors.length} unusable` : ''}
|
||||
✓ Read {parsed.rows.length} usable row{parsed.rows.length === 1 ? '' : 's'}
|
||||
</Text>
|
||||
|
||||
{parsed.ignoredColumns?.length > 0 && (
|
||||
<Text className="dai-action-note">Columns I didn’t recognise, so didn’t use: {parsed.ignoredColumns.join(', ')}</Text>
|
||||
)}
|
||||
|
||||
{parsed.duplicates?.length > 0 && (
|
||||
<Text className="dai-field-err">
|
||||
{parsed.duplicates.length} row{parsed.duplicates.length === 1 ? '' : 's'} repeat an earlier phone and address (
|
||||
{parsed.duplicates.map((d) => `line ${d.line}`).join(', ')}) — they’d become separate orders.
|
||||
</Text>
|
||||
{parsed.errors?.length > 0 && (
|
||||
<VStack gap={0.25} padding={0}>
|
||||
<Text className="dai-field-err">
|
||||
⚠️ {parsed.errors.length} row{parsed.errors.length === 1 ? '' : 's'} couldn’t be read:
|
||||
</Text>
|
||||
{parsed.errors.slice(0, 5).map((e, idx) => (
|
||||
<Text key={idx} className="dai-field-err">
|
||||
Row {e.row}: {e.reason}
|
||||
</Text>
|
||||
))}
|
||||
{parsed.errors.length > 5 && (
|
||||
<Text className="dai-action-note">…and {parsed.errors.length - 5} more unusable rows.</Text>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* Keyed by line AND index: one row can fail several validations, so
|
||||
`line` alone is not unique and React logged "two children with the
|
||||
same key" for every repeat — seen live as a 1/2/3 cycle. Same
|
||||
shape the list renderer in AIParts already uses. */}
|
||||
{parsed.errors.slice(0, 5).map((e, i) => (
|
||||
<Text key={`${e.line}-${i}`} className="dai-field-err">
|
||||
Line {e.line}: {e.reason}
|
||||
</Text>
|
||||
))}
|
||||
{parsed.errors.length > 5 && <Text className="dai-action-note">…and {parsed.errors.length - 5} more unusable rows.</Text>}
|
||||
|
||||
{parsed.rows.length > BULK_MAX && (
|
||||
<Text className="dai-action-note">
|
||||
Over {BULK_MAX} — I’ll send them in {Math.ceil(parsed.rows.length / BULK_MAX)} batches.
|
||||
@@ -119,23 +111,35 @@ const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
<HStack gap={1.5} padding={0}>
|
||||
<div className="flex flex-wrap items-center gap-2 pt-1">
|
||||
<Button
|
||||
label={parsed?.rows.length ? `Use these ${parsed.rows.length} rows` : 'Continue'}
|
||||
variant="primary"
|
||||
variant="default"
|
||||
size="sm"
|
||||
isDisabled={!parsed?.rows.length || isBusy || isReading}
|
||||
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer disabled:opacity-50"
|
||||
disabled={!parsed?.rows.length || isBusy || isReading}
|
||||
onClick={() => onSubmit(parsed)}
|
||||
/>
|
||||
<Button label="Cancel" variant="secondary" size="sm" isDisabled={isBusy} onClick={onCancel} />
|
||||
>
|
||||
{parsed?.rows.length ? `Use these ${parsed.rows.length} rows` : 'Continue'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
|
||||
disabled={isBusy}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
label="Template"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
tooltip="Download a blank sheet with the exact column titles I read"
|
||||
className="text-slate-600 hover:bg-slate-100 font-medium text-xs px-3 h-8 rounded-lg cursor-pointer"
|
||||
title="Download a blank sheet with the exact column titles I read"
|
||||
onClick={() => downloadCsv('doormile-bulk-orders-template.csv', templateCsv())}
|
||||
/>
|
||||
</HStack>
|
||||
>
|
||||
Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Text className="dai-action-note">
|
||||
Pasted rows: name, phone, address, pincode, city, price, description — comma, tab or pipe separated. Leave the price blank and I’ll
|
||||
|
||||
Reference in New Issue
Block a user