initial commit

This commit is contained in:
2026-08-26 15:01:15 +05:30
parent aa42d9ee81
commit cf4f4ecd9c
87 changed files with 12814 additions and 2328 deletions

View File

@@ -1,11 +1,11 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { ArrowUp } from 'lucide-react';
import { LuArrowUp } from 'react-icons/lu';
import { HStack } from './shim';
import { VStack } from './shim';
import { Text } from './shim';
import { ChatDictationButton, useChatDictation } from './shim';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Text } from '@astryxdesign/core/Text';
import { ChatDictationButton, useChatDictation } from '@astryxdesign/core/Chat';
// ==============================|| Doormile AI — composer ||============================== //
//
@@ -136,7 +136,7 @@ const AIComposer = ({ value, onChange, onSubmit, isBusy, placeholder }) => {
disabled={!canSend}
aria-label={isBusy ? 'Waiting for the current answer' : 'Send message'}
>
<ArrowUp size={16} strokeWidth={2.4} aria-hidden="true" />
<LuArrowUp size={16} strokeWidth={2.4} aria-hidden="true" />
</button>
</HStack>
</VStack>

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { VStack } from './shim';
import { HStack } from './shim';
import { Text } from './shim';
import { Button } from './shim';
import { Selector } from './shim';
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 { Selector } from '@astryxdesign/core/Selector';
// ==============================|| Doormile AI — one dropdown step ||============================== //
//

View File

@@ -1,14 +1,14 @@
import { memo, useState } from 'react';
import PropTypes from 'prop-types';
import { Copy } from 'lucide-react';
import { ChevronDown } from 'lucide-react';
import { AiOutlineCopy as CopyOutlined } from 'react-icons/ai';
import { LuChevronDown } from 'react-icons/lu';
import { HStack } from './shim';
import { VStack } from './shim';
import { Text } from './shim';
import { IconButton } from './shim';
import { Button } from './shim';
import { ChatToolCalls } from './shim';
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 { ChatToolCalls } from '@astryxdesign/core/Chat';
import { Spark, Metric, StatGrid, StateBlock, AnswerList } from './AIParts';
import AIFlowStep from './AIFlowStep';
@@ -51,7 +51,7 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
variant="ghost"
label="Copy answer"
tooltip="Copy answer"
icon={<Copy />}
icon={<CopyOutlined />}
onClick={() => onCopy(message)}
/>
</HStack>
@@ -96,7 +96,7 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
<span>
{showSources ? 'Hide' : 'Sources'} ({sourceCount})
</span>
<ChevronDown size={11} aria-hidden="true" />
<LuChevronDown size={11} aria-hidden="true" />
</button>
</HStack>
{showSources && <ChatToolCalls calls={message.sourceCalls} />}

View File

@@ -1,22 +1,20 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import '../DoormileAI.css';
import PropTypes from 'prop-types';
import { useLocation } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
import { X as CloseOutlined, MoreHorizontal as MoreOutlined } from 'lucide-react';
import { ChevronDown as LuChevronDown, PanelRightOpen as LuPanelRightOpen, PanelRightClose as LuPanelRightClose, History as LuHistory, Trash2 as LuTrash2, MessageSquare as LuMessageSquare } from 'lucide-react';
import { AiOutlineClose as CloseOutlined, AiOutlineMore as MoreOutlined } from 'react-icons/ai';
import { LuChevronDown, LuPanelRightOpen, LuPanelRightClose, LuHistory, LuTrash2, LuMessageSquare } from 'react-icons/lu';
import { HStack } from './shim';
import { VStack } from './shim';
import { Text } from './shim';
import { IconButton } from './shim';
import { DropdownMenu } from './shim';
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 { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
import { toast } from '@/components/ui/use-toast';
const OpenToast = ({message, tone}) => toast({description: message, variant: tone === 'error' ? 'destructive' : 'default'});
const STATUS = { error: 'error', success: 'success', warning: 'warning', info: 'info', muted: 'muted' };
import { OpenToast } from 'components/third-party/OpenToast';
import { STATUS } from 'themes/dt/tokens';
import { answerQuestion, FOLLOW_UP_SUGGESTIONS } from '@/lib/assistant/intents';
import { executeCreateCustomer, buildCustomerPayload } from '@/lib/assistant/actions';
import { executeCreateOrder, buildOrderPayload, validateOrderDraft } from '@/lib/assistant/orderActions';
@@ -228,17 +226,8 @@ const AIPanel = ({ isOpen, onClose }) => {
useEffect(() => {
if (isOpen) {
setIsMounted(true);
// One frame between mounting and showing, so the slide-in has a
// from-state to animate out of. rAF is the right clock for that, but a
// hidden or heavily throttled document never runs the callback at all —
// and a panel that refuses to open is worse than one that opens without
// its animation, so a timer backs it up.
const raf = requestAnimationFrame(() => setIsShown(true));
const fallback = setTimeout(() => setIsShown(true), 50);
return () => {
cancelAnimationFrame(raf);
clearTimeout(fallback);
};
return () => cancelAnimationFrame(raf);
}
setIsShown(false);
const timer = setTimeout(() => setIsMounted(false), ANIMATION_MS);
@@ -275,14 +264,15 @@ const AIPanel = ({ isOpen, onClose }) => {
}, [isWide]);
// ---- anchor the dock to the real header height ----
// The dock starts where the top nav ends. Measured rather than assumed, and
// re-measured on resize, so the seam stays closed if the header ever changes
// height — a hard-coded number leaves a sliver of page visible above the
// panel the moment the bar grows.
// The dock starts where the top nav ends. `--appshell-header-height` looks
// like the right token for that and Dispatch.css already reads it, but
// Astryx does not actually publish it — verified in the browser, where it
// resolves to nothing and the fallback wins. The header is 57px, not the 64
// a fallback would guess, so trusting it left a 7px sliver of page visible
// above the panel.
//
// AdminLayout marks its bar `data-app-header`; that attribute exists purely
// as this hook. The selector used to be `.astryx-layout-header`, a class this
// app does not render at all, so the measurement silently never ran.
// Measured instead, and re-measured on resize, so the seam stays closed if
// the header ever changes height.
useEffect(() => {
if (!isMounted) return undefined;
const header = document.querySelector('[data-app-header]');
@@ -1394,25 +1384,22 @@ const AIPanel = ({ isOpen, onClose }) => {
// shrink and scroll. See the [data-compact] rule in DoormileAI.css.
const isCompactStrip = hasThread;
if (!isMounted || !isShown) return null;
return createPortal(
<>
<div
className="dai-scrim"
data-open={isShown}
onClick={onClose}
aria-hidden="true"
/>
<HStack className="dai-root dai-scrim" data-open={isShown} padding={0} gap={0} onClick={onClose} aria-hidden="true" />
<VStack
as="aside"
ref={panelRef}
className="dai-root dai-panel"
data-open={isShown}
data-state={isShown ? 'open' : 'closed'}
gap={0}
padding={0}
/* `complementary`, not `dialog`. A dialog promises modality — focus is
trapped, the rest of the page is inert, and screen readers announce
it as blocking. None of that is true any more: the page beside it
stays live and operable, so announcing it as a dialog would be a
lie to exactly the users who cannot see that it is docked. */
role="complementary"
aria-label="Doormile AI — Operations Copilot"
tabIndex={-1}

View File

@@ -1,13 +1,25 @@
import PropTypes from 'prop-types';
import { AlertTriangle, Info, Sparkles } from 'lucide-react';
import { AiOutlineWarning as WarningOutlined, AiOutlineInfoCircle as InfoCircleOutlined } from 'react-icons/ai';
import { HStack } from './shim';
import { VStack } from './shim';
import { Text } from './shim';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Text } from '@astryxdesign/core/Text';
import doormileMark from 'assets/images/doormile-mark.png';
// ==============================|| Doormile AI — shared primitives ||============================== //
//
// Small presentational pieces shared by the panel's states. Kept in one file
// because each is a handful of lines and they are only ever used together.
// The AI mark — a soft gradient orb, deliberately not a robot face.
// The Doormile D. One component behind the assistant's identity, so the
// header, every reply, the welcome screen and the thinking state can't drift
// apart — they all render this. The mark is RGBA with a transparent ground, so
// it sits on the panel surface rather than needing a coloured chip behind it.
export const Spark = ({ size = 'sm' }) => (
<HStack className="dai-spark" data-size={size} padding={0} gap={0} aria-hidden="true">
<Sparkles className="text-brand w-4 h-4" />
<img src={doormileMark} alt="" />
</HStack>
);
@@ -87,8 +99,8 @@ StatGrid.propTypes = {
// silently dropped), and the rehydrated "element" would then crash the render
// on the next panel open.
const STATE_ICONS = {
warning: <AlertTriangle />,
info: <Info />
warning: <WarningOutlined />,
info: <InfoCircleOutlined />
};
// A full list, rendered readably. Replaces the "…and 4 more" truncation that

View File

@@ -1,12 +1,12 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
import { VStack } from './shim';
import { HStack } from './shim';
import { Text } from './shim';
import { Button } from './shim';
import { TextArea } from './shim';
import { FileInput } from './shim';
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 { TextArea } from '@astryxdesign/core/TextArea';
import { FileInput } from '@astryxdesign/core/FileInput';
import { parseBulkRows, BULK_MAX } from '@/lib/assistant/bulkOrderActions';
import { parseBulkFile, templateCsv, downloadCsv } from '@/lib/assistant/bulkFile';

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { HStack } from './shim';
import { VStack } from './shim';
import { Text } from './shim';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Text } from '@astryxdesign/core/Text';
import { CHIP_LABELS } from './pageContext';

View File

@@ -1,5 +1,8 @@
import { useCallback, useRef, useState } from 'react';
import { Bot } from 'lucide-react';
import { Tooltip } from '@astryxdesign/core/Tooltip';
import doormileMark from 'assets/images/doormile-mark.png';
import AIPanel from './AIPanel';
import '../DoormileAI.css';
@@ -20,17 +23,19 @@ const DoormileAITrigger = () => {
return (
<>
<button
ref={buttonRef}
type="button"
className="dai-root dai-trigger inline-flex items-center justify-center rounded-md p-2 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
data-active={isOpen}
aria-label="Open Doormile AI"
aria-expanded={isOpen}
onClick={() => (isOpen ? close() : setIsOpen(true))}
>
<Bot className="dai-trigger-mark text-slate-700" aria-hidden="true" />
</button>
<Tooltip content="Doormile AI">
<button
ref={buttonRef}
type="button"
className="dai-root dai-trigger"
data-active={isOpen}
aria-label="Open Doormile AI"
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} />
</>
);

View File

@@ -1,19 +1,19 @@
import {
Clock3 as LuClock3,
Package as LuPackage,
Bike as LuBike,
Truck as LuTruck,
Building2 as LuBuilding2,
CircleDot as LuCircleDot,
Banknote as LuBanknote,
Users as LuUsers,
Layers as LuLayers,
TimerOff as LuTimerOff,
UserPlus as LuUserPlus,
PackagePlus as LuPackagePlus,
ListPlus as LuListPlus,
Repeat as LuRepeat
} from 'lucide-react';
LuClock3,
LuPackage,
LuBike,
LuTruck,
LuBuilding2,
LuCircleDot,
LuBanknote,
LuUsers,
LuLayers,
LuTimerOff,
LuUserPlus,
LuPackagePlus,
LuListPlus,
LuRepeat
} from 'react-icons/lu';
// ==============================|| Doormile AI — page context ||============================== //
//

View File

@@ -1,139 +0,0 @@
import React from 'react';
import { DropdownMenu as KrowDropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
export const HStack = ({ as: Component = 'div', children, className = '', gap = 0, padding = 0, vAlign, justify, wrap, style, ...props }) => {
return (
<Component
className={`flex flex-row ${
vAlign === 'start' ? 'items-start' : vAlign === 'end' ? 'items-end' : vAlign === 'center' ? 'items-center' : 'items-stretch'
} ${
justify === 'between' ? 'justify-between' : justify === 'end' ? 'justify-end' : justify === 'center' ? 'justify-center' : 'justify-start'
} ${wrap === 'wrap' ? 'flex-wrap' : ''} ${className}`}
style={{ gap: `${gap * 0.5}rem`, padding: typeof padding === 'number' && padding > 0 ? `${padding * 0.5}rem` : undefined, ...style }}
{...props}
>
{children}
</Component>
);
};
export const VStack = ({ as: Component = 'div', children, className = '', gap = 0, padding = 0, align, justify, style, ...props }) => {
return (
<Component
className={`flex flex-col ${
align === 'start' ? 'items-start' : align === 'end' ? 'items-end' : align === 'center' ? 'items-center' : 'items-stretch'
} ${
justify === 'between' ? 'justify-between' : justify === 'end' ? 'justify-end' : justify === 'center' ? 'justify-center' : 'justify-start'
} ${className}`}
style={{ gap: `${gap * 0.5}rem`, padding: typeof padding === 'number' && padding > 0 ? `${padding * 0.5}rem` : undefined, ...style }}
{...props}
>
{children}
</Component>
);
};
export const Text = ({ as: Component = 'span', children, className = '', style, ...props }) => {
return (
<Component className={`text-sm ${className}`} style={style} {...props}>
{children}
</Component>
);
};
export const IconButton = ({ children, className = '', icon, onClick, tooltip, label, size, variant, isIconOnly, ...props }) => {
return (
<button
className={`inline-flex items-center justify-center rounded-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors ${
size === 'sm' ? 'p-1.5' : 'p-2'
} ${isIconOnly ? 'aspect-square' : ''} ${className}`}
onClick={onClick}
title={tooltip || label}
aria-label={label}
{...props}
>
{icon || children}
</button>
);
};
export const DropdownMenu = ({ button, items }) => (
<KrowDropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton {...button} />
</DropdownMenuTrigger>
<DropdownMenuContent>
{items?.map((it, i) => (
<DropdownMenuItem key={i} onClick={it.onClick}>
{it.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</KrowDropdownMenu>
);
export const Button = ({ children, onClick, className = '' }) => (
<button
onClick={onClick}
className={`px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 ${className}`}
>
{children}
</button>
);
export const Selector = ({ value, onChange, options = [], placeholder }) => (
<select
value={value}
onChange={onChange}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="" disabled>
{placeholder || 'Select...'}
</option>
{options.map((o) => (
<option key={o.value || o} value={o.value || o}>
{o.label || o}
</option>
))}
</select>
);
export const TextArea = ({ value, onChange, placeholder, rows = 3, className = '' }) => (
<textarea
value={value}
onChange={onChange}
placeholder={placeholder}
rows={rows}
className={`block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${className}`}
/>
);
export const FileInput = ({ onChange, accept, className = '' }) => (
<input
type="file"
onChange={onChange}
accept={accept}
className={`block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 ${className}`}
/>
);
export const ChatToolCalls = ({ calls }) => (
<div className="text-xs text-gray-500 bg-gray-50 p-2 rounded">
{calls?.map((c, i) => (
<div key={i} className="mb-1">
<strong>{c.tool || 'Tool'}</strong>
<pre className="mt-1 overflow-x-auto text-[10px]">{JSON.stringify(c.args, null, 2)}</pre>
</div>
))}
</div>
);
export const useChatDictation = () => {
return { isDictating: false, startDictation: () => {}, stopDictation: () => {}, toggleDictation: () => {} };
};
export const ChatDictationButton = ({ isDictating, onToggle }) => (
<button type="button" onClick={onToggle} className="p-2 text-gray-500 hover:text-blue-500" title="Dictation unavailable">
🎤
</button>
);