updates on the design and chatbot fix
This commit is contained in:
@@ -30,6 +30,10 @@ module.exports = {
|
|||||||
'\.(css|less|scss|sass)$': '<rootDir>/tests/setup/styleMock.cjs',
|
'\.(css|less|scss|sass)$': '<rootDir>/tests/setup/styleMock.cjs',
|
||||||
'\.(png|jpe?g|gif|svg|webp|woff2?|ttf|eot)$': '<rootDir>/tests/setup/fileMock.cjs',
|
'\.(png|jpe?g|gif|svg|webp|woff2?|ttf|eot)$': '<rootDir>/tests/setup/fileMock.cjs',
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
// Vite resolves these bare specifiers from src/ (jsconfig baseUrl).
|
||||||
|
// Jest needs telling, or any test importing a file that uses them fails
|
||||||
|
// to resolve rather than failing an assertion.
|
||||||
|
'^(components|pages|utils|themes|assets|lib|api)/(.*)$': '<rootDir>/src/$1/$2',
|
||||||
},
|
},
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
'src/api/doormile/**/*.js',
|
'src/api/doormile/**/*.js',
|
||||||
|
|||||||
@@ -1428,6 +1428,7 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
side="bottom"
|
||||||
label={view === 'history' ? 'Back to conversation' : 'Conversation history'}
|
label={view === 'history' ? 'Back to conversation' : 'Conversation history'}
|
||||||
tooltip={view === 'history' ? 'Back to conversation' : 'History'}
|
tooltip={view === 'history' ? 'Back to conversation' : 'History'}
|
||||||
aria-pressed={view === 'history'}
|
aria-pressed={view === 'history'}
|
||||||
@@ -1437,6 +1438,7 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
side="bottom"
|
||||||
label="New conversation"
|
label="New conversation"
|
||||||
tooltip="New conversation"
|
tooltip="New conversation"
|
||||||
icon={<LuRotateCcw />}
|
icon={<LuRotateCcw />}
|
||||||
@@ -1445,6 +1447,7 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
side="bottom"
|
||||||
label={isWide ? 'Standard width (20%)' : 'Expand width (30%)'}
|
label={isWide ? 'Standard width (20%)' : 'Expand width (30%)'}
|
||||||
tooltip={isWide ? 'Standard width (20%)' : 'Expand width (30%)'}
|
tooltip={isWide ? 'Standard width (20%)' : 'Expand width (30%)'}
|
||||||
aria-pressed={isWide}
|
aria-pressed={isWide}
|
||||||
@@ -1454,6 +1457,7 @@ const AIPanel = ({ isOpen, onClose }) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
side="bottom"
|
||||||
label="Close assistant"
|
label="Close assistant"
|
||||||
tooltip="Close assistant"
|
tooltip="Close assistant"
|
||||||
icon={<LuX />}
|
icon={<LuX />}
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ export const CHIP_LABELS = {
|
|||||||
'How many tripsheets are dispatched?': 'Tripsheets',
|
'How many tripsheets are dispatched?': 'Tripsheets',
|
||||||
'How many open exceptions?': 'Open exceptions',
|
'How many open exceptions?': 'Open exceptions',
|
||||||
'How many pricing rules are configured?': 'Pricing rules',
|
'How many pricing rules are configured?': 'Pricing rules',
|
||||||
"Repeat yesterday's orders": 'Repeat yesterday'
|
"Repeat yesterday's orders": 'Repeat yesterday',
|
||||||
|
'How many consignments do we have?': 'Consignments',
|
||||||
|
'How many app users do we have?': 'App users',
|
||||||
|
'How many partners do we have?': 'Partners',
|
||||||
|
'How many competitor branches are tracked?': 'Competitor branches',
|
||||||
|
'How many carriers do we have?': 'Carriers'
|
||||||
};
|
};
|
||||||
|
|
||||||
// The one follow-up phrasing that is not already a page suggestion. Verified
|
// The one follow-up phrasing that is not already a page suggestion. Verified
|
||||||
@@ -121,39 +126,27 @@ const ORDERS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RIDERS = {
|
const RIDERS = {
|
||||||
label: 'Riders',
|
label: 'Milers',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{ icon: LuBike, text: 'How many riders are active?' },
|
{ icon: LuBike, text: 'How many riders are active?' },
|
||||||
{ icon: LuClock3, text: "Give me today's operations summary" },
|
{ icon: LuUserPlus, text: 'Assign a rider' }
|
||||||
{ icon: LuPackage, text: 'How many orders today?' },
|
|
||||||
{ icon: LuTruck, text: 'How many vehicles are available?' }
|
|
||||||
],
|
],
|
||||||
more: [
|
more: []
|
||||||
{ icon: LuBuilding2, text: 'Current hub status' },
|
|
||||||
{ icon: LuBanknote, text: 'Total revenue today' }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VEHICLES = {
|
const VEHICLES = {
|
||||||
label: 'Vehicles',
|
label: 'Vehicles',
|
||||||
suggestions: [
|
suggestions: [{ icon: LuTruck, text: 'How many vehicles are available?' }],
|
||||||
{ icon: LuTruck, text: 'How many vehicles are available?' },
|
more: []
|
||||||
{ icon: LuBike, text: 'How many riders are active?' },
|
|
||||||
{ icon: LuBuilding2, text: 'Current hub status' },
|
|
||||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
|
||||||
],
|
|
||||||
more: [{ icon: LuPackage, text: 'How many orders today?' }]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HUBS = {
|
const HUBS = {
|
||||||
label: 'Hubs',
|
label: 'Hubs',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{ icon: LuBuilding2, text: 'Current hub status' },
|
{ icon: LuBuilding2, text: 'Current hub status' },
|
||||||
{ icon: LuTimerOff, text: 'Which hubs are experiencing delays?' },
|
{ icon: LuTimerOff, text: 'Which hubs are experiencing delays?' }
|
||||||
{ icon: LuTruck, text: 'How many vehicles are available?' },
|
|
||||||
{ icon: LuBike, text: 'How many riders are active?' }
|
|
||||||
],
|
],
|
||||||
more: [{ icon: LuPackage, text: 'How many orders today?' }]
|
more: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const DISPATCH = {
|
const DISPATCH = {
|
||||||
@@ -162,24 +155,16 @@ const DISPATCH = {
|
|||||||
{ icon: LuLayers, text: 'Morning Batch orders today' },
|
{ icon: LuLayers, text: 'Morning Batch orders today' },
|
||||||
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
||||||
{ icon: LuBike, text: 'How many riders are active?' },
|
{ icon: LuBike, text: 'How many riders are active?' },
|
||||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
{ icon: LuUserPlus, text: 'Assign a rider' }
|
||||||
],
|
],
|
||||||
more: [
|
more: []
|
||||||
{ icon: LuPackage, text: 'How many orders today?' },
|
|
||||||
{ icon: LuCircleDot, text: 'How many pending orders today?' }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Linehaul and middle-mile runs.
|
// Linehaul and middle-mile runs.
|
||||||
const TRIPSHEETS = {
|
const TRIPSHEETS = {
|
||||||
label: 'Tripsheets',
|
label: 'Tripsheets',
|
||||||
suggestions: [
|
suggestions: [{ icon: LuLayers, text: 'How many tripsheets are dispatched?' }],
|
||||||
{ icon: LuLayers, text: 'How many tripsheets are dispatched?' },
|
more: []
|
||||||
{ 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.
|
// The register of things that went wrong.
|
||||||
@@ -187,9 +172,7 @@ const EXCEPTIONS = {
|
|||||||
label: 'Exceptions',
|
label: 'Exceptions',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{ icon: LuCircleDot, text: 'How many open exceptions?' },
|
{ icon: LuCircleDot, text: 'How many open exceptions?' },
|
||||||
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
{ 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: []
|
more: []
|
||||||
};
|
};
|
||||||
@@ -198,13 +181,8 @@ const EXCEPTIONS = {
|
|||||||
// real page and deserves its own questions like any other.
|
// real page and deserves its own questions like any other.
|
||||||
const PRICING = {
|
const PRICING = {
|
||||||
label: 'Pricing',
|
label: 'Pricing',
|
||||||
suggestions: [
|
suggestions: [{ icon: LuBanknote, text: 'How many pricing rules are configured?' }],
|
||||||
{ icon: LuBanknote, text: 'How many pricing rules are configured?' },
|
more: []
|
||||||
{ 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.
|
// Bookings — what customers raised in the app, before it becomes an order.
|
||||||
@@ -227,23 +205,62 @@ const BOOKINGS = {
|
|||||||
suggestions: [
|
suggestions: [
|
||||||
{ icon: LuPackage, text: 'How many orders today?' },
|
{ icon: LuPackage, text: 'How many orders today?' },
|
||||||
{ icon: LuCircleDot, text: 'How many pending 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: LuPackage, text: 'How many cancelled orders today?' },
|
||||||
{ icon: LuClock3, text: 'Orders today vs yesterday' }
|
{ icon: LuTimerOff, text: 'Which orders are delayed?' }
|
||||||
]
|
],
|
||||||
|
more: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deliveries — an order after it has been picked up and become a consignment.
|
||||||
|
//
|
||||||
|
// This page used to be `{ ...ORDERS, label: 'Deliveries' }`: the Orders
|
||||||
|
// question set under a different name, so a table of consignments offered
|
||||||
|
// "Create an order" and "Repeat yesterday's orders". The label made it look
|
||||||
|
// deliberate, which is the same trap the Tripsheets/Exceptions/Pricing note
|
||||||
|
// below describes.
|
||||||
|
//
|
||||||
|
// Every phrasing here is checked against its intent's own `match`:
|
||||||
|
// consignmentStatus is /\bconsignments?\b/, parcelTrack wants a tracking
|
||||||
|
// number, and the delivered/delayed counts are the order intents narrowed to
|
||||||
|
// the statuses this page actually shows.
|
||||||
|
const DELIVERIES = {
|
||||||
|
label: 'Deliveries',
|
||||||
|
suggestions: [
|
||||||
|
{ icon: LuTruck, text: 'How many consignments do we have?' },
|
||||||
|
{ icon: LuPackage, text: 'How many delivered orders today?' },
|
||||||
|
{ icon: LuTimerOff, text: 'Which orders are delayed?' },
|
||||||
|
{ icon: LuBike, text: 'How many riders are active?' }
|
||||||
|
],
|
||||||
|
more: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reference data behind the console — not an operational page, so it gets the
|
||||||
|
// one question that is actually about it rather than the operations summary.
|
||||||
|
const APP_USERS = {
|
||||||
|
label: 'App users',
|
||||||
|
suggestions: [{ icon: LuUsers, text: 'How many app users do we have?' }],
|
||||||
|
more: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPETITIVE_INTEL = {
|
||||||
|
label: 'Competitive intel',
|
||||||
|
suggestions: [
|
||||||
|
{ icon: LuBuilding2, text: 'How many competitor branches are tracked?' },
|
||||||
|
// NOT "how many carrier pricing rules", which is the phrasing
|
||||||
|
// carrierPricingCount documents for itself and which never reaches it:
|
||||||
|
// pricingCount matches /\bpricing\b/ and runs 57 lines earlier, so that
|
||||||
|
// wording is answered with the general pricing-rule count. Its actual
|
||||||
|
// predicate is /\bcarriers?\b/.
|
||||||
|
{ icon: LuBanknote, text: 'How many carriers do we have?' }
|
||||||
|
],
|
||||||
|
more: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const TENANTS = {
|
const TENANTS = {
|
||||||
label: 'Tenants',
|
label: 'Tenants',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{ icon: LuUsers, text: 'How many tenants do we have?' },
|
{ icon: LuUsers, text: 'How many tenants do we have?' },
|
||||||
{ icon: LuPackage, text: 'How many orders today?' },
|
{ icon: LuUsers, text: 'How many partners do we have?' }
|
||||||
{ icon: LuBanknote, text: 'Total revenue today' },
|
|
||||||
{ icon: LuClock3, text: "Give me today's operations summary" }
|
|
||||||
],
|
],
|
||||||
more: []
|
more: []
|
||||||
};
|
};
|
||||||
@@ -352,7 +369,7 @@ const FOLLOW_UPS = {
|
|||||||
// with the wrong label.
|
// with the wrong label.
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
['/doormile/control-x', DISPATCH],
|
['/doormile/control-x', DISPATCH],
|
||||||
['/doormile/deliveries', { ...ORDERS, label: 'Deliveries' }],
|
['/doormile/deliveries', DELIVERIES],
|
||||||
['/doormile/bookings', BOOKINGS],
|
['/doormile/bookings', BOOKINGS],
|
||||||
['/doormile/orders', ORDERS],
|
['/doormile/orders', ORDERS],
|
||||||
['/doormile/milers', RIDERS],
|
['/doormile/milers', RIDERS],
|
||||||
@@ -386,7 +403,11 @@ const ROUTES = [
|
|||||||
// so none is shadowed by the order intents that run earlier in the catalog.
|
// so none is shadowed by the order intents that run earlier in the catalog.
|
||||||
['/doormile/tripsheets', TRIPSHEETS],
|
['/doormile/tripsheets', TRIPSHEETS],
|
||||||
['/doormile/exceptions', EXCEPTIONS],
|
['/doormile/exceptions', EXCEPTIONS],
|
||||||
['/doormile/pricing', PRICING]
|
['/doormile/pricing', PRICING],
|
||||||
|
// Missing from this list entirely until now, so both fell through to
|
||||||
|
// DEFAULT_CONTEXT and offered rider and hub questions over their own data.
|
||||||
|
['/doormile/app-users', APP_USERS],
|
||||||
|
['/doormile/competitive-intel', COMPETITIVE_INTEL]
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---- every question, from every page ---------------------------------------
|
// ---- every question, from every page ---------------------------------------
|
||||||
@@ -399,7 +420,20 @@ const ROUTES = [
|
|||||||
//
|
//
|
||||||
// `more` is retired: AIWelcome renders one flat list now, so a second array
|
// `more` is retired: AIWelcome renders one flat list now, so a second array
|
||||||
// would only be a second place for a question to hide.
|
// would only be a second place for a question to hide.
|
||||||
const ALL_CONTEXTS = [ORDERS, RIDERS, VEHICLES, HUBS, DISPATCH, TENANTS, REPORTS, DEFAULT_CONTEXT, ...ROUTES.map(([, c]) => c)];
|
const ALL_CONTEXTS = [
|
||||||
|
ORDERS,
|
||||||
|
RIDERS,
|
||||||
|
VEHICLES,
|
||||||
|
HUBS,
|
||||||
|
DISPATCH,
|
||||||
|
DELIVERIES,
|
||||||
|
APP_USERS,
|
||||||
|
COMPETITIVE_INTEL,
|
||||||
|
TENANTS,
|
||||||
|
REPORTS,
|
||||||
|
DEFAULT_CONTEXT,
|
||||||
|
...ROUTES.map(([, c]) => c)
|
||||||
|
];
|
||||||
|
|
||||||
// Every question declared anywhere. Two jobs: it is the icon index behind
|
// 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
|
// QUESTION_BY_TEXT (so a follow-up naming a question that is no longer global
|
||||||
|
|||||||
@@ -2090,6 +2090,30 @@ const INTENTS = [
|
|||||||
|
|
||||||
const INTENTS_BY_ID = Object.fromEntries(INTENTS.map((i) => [i.id, i]));
|
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);
|
export const SUPPORTED_QUESTIONS = INTENTS.map((i) => i.label);
|
||||||
|
|
||||||
// Clean, directly-askable example phrasings — for "recommended question"
|
// Clean, directly-askable example phrasings — for "recommended question"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-[9999] overflow-hidden rounded-lg bg-slate-900 px-2.5 py-1 text-xs font-medium text-white shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props} />
|
{...props} />
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export function AgentInsightsView() {
|
|||||||
<span>420 calls (28%)</span>
|
<span>420 calls (28%)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
|
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
|
||||||
<div className="h-full bg-indigo-600 rounded-full w-[28%]" />
|
<div className="h-full bg-slate-700 rounded-full w-[28%]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
GraduationCap,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -9,6 +8,8 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
Pencil,
|
Pencil,
|
||||||
Users,
|
Users,
|
||||||
|
Package,
|
||||||
|
Route,
|
||||||
Layers,
|
Layers,
|
||||||
Check,
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -36,32 +37,28 @@ export default function AgentScopeNavigator({
|
|||||||
const boardSkills = skills.filter((s) => s.scope === 'board');
|
const boardSkills = skills.filter((s) => s.scope === 'board');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-3xl border border-slate-200/90 shadow-2xs p-4 flex flex-col space-y-4">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-4 flex flex-col space-y-4">
|
||||||
{/* 1. Agent Profile Header + Dropdown Switcher */}
|
{/* 1. Agent Profile Header + Dropdown Switcher */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAgentMenuOpen((v) => !v)}
|
onClick={() => setAgentMenuOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between p-2 rounded-2xl hover:bg-slate-50 transition-colors text-left cursor-pointer group"
|
className="w-full flex items-center justify-between p-2 rounded-xl hover:bg-slate-50 transition-colors text-left cursor-pointer group"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="w-10 h-10 rounded-2xl bg-brand-tint border border-brand/20 text-brand flex items-center justify-center shrink-0 shadow-3xs">
|
<div className="w-9 h-9 rounded-full bg-brand-tint border border-brand/20 text-brand flex items-center justify-center shrink-0 shadow-2xs">
|
||||||
{currentAgent.iconType === 'grad' ? (
|
<Sparkles className="w-4 h-4" />
|
||||||
<GraduationCap className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<Sparkles className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-xs sm:text-sm font-bold text-slate-900 truncate font-heading group-hover:text-brand transition-colors">
|
<div className="text-xs sm:text-sm font-bold text-slate-900 truncate font-heading group-hover:text-brand transition-colors">
|
||||||
{currentAgent.name}
|
{currentAgent.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mt-0.5">
|
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
{currentAgent.badge || 'AGENT'}
|
{currentAgent.badge || 'AGENT'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className="w-4 h-4 text-slate-400 shrink-0 ml-1" />
|
<ChevronDown className="w-4 h-4 text-slate-400 shrink-0 ml-1 group-hover:text-slate-600 transition-colors" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Agent Switcher Menu */}
|
{/* Agent Switcher Menu */}
|
||||||
@@ -71,7 +68,7 @@ export default function AgentScopeNavigator({
|
|||||||
className="fixed inset-0 z-40 bg-transparent"
|
className="fixed inset-0 z-40 bg-transparent"
|
||||||
onClick={() => setAgentMenuOpen(false)}
|
onClick={() => setAgentMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-slate-200 rounded-2xl shadow-xl p-1.5 z-50 animate-in fade-in zoom-in-95">
|
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-slate-200 rounded-xl shadow-lg p-1.5 z-50 animate-in fade-in zoom-in-95">
|
||||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider px-3 py-1.5">
|
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider px-3 py-1.5">
|
||||||
Switch Agent Scope
|
Switch Agent Scope
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +81,7 @@ export default function AgentScopeNavigator({
|
|||||||
setAgentMenuOpen(false);
|
setAgentMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold text-left transition-colors cursor-pointer',
|
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-semibold text-left transition-colors cursor-pointer',
|
||||||
agent.id === activeAgentId
|
agent.id === activeAgentId
|
||||||
? 'bg-brand-tint text-brand'
|
? 'bg-brand-tint text-brand'
|
||||||
: 'text-slate-700 hover:bg-slate-50'
|
: 'text-slate-700 hover:bg-slate-50'
|
||||||
@@ -101,14 +98,14 @@ export default function AgentScopeNavigator({
|
|||||||
|
|
||||||
<div className="h-px bg-slate-100" />
|
<div className="h-px bg-slate-100" />
|
||||||
|
|
||||||
{/* 2. Hierarchical Collapsible Tree */}
|
{/* 2. Hierarchical Tree (Clean, un-cluttered style) */}
|
||||||
<div className="space-y-3 text-xs">
|
<div className="space-y-2 text-xs">
|
||||||
{/* SURFACES TREE NODE */}
|
{/* SURFACES TREE NODE */}
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSurfacesOpen((v) => !v)}
|
onClick={() => setSurfacesOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between py-1 px-1.5 text-slate-700 hover:text-slate-900 rounded-lg font-semibold cursor-pointer group"
|
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{surfacesOpen ? (
|
{surfacesOpen ? (
|
||||||
@@ -116,20 +113,20 @@ export default function AgentScopeNavigator({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
<LayoutGrid className="w-3.5 h-3.5 text-slate-500" />
|
<LayoutGrid className="w-3.5 h-3.5 text-slate-400" />
|
||||||
<span className="font-bold">Surfaces</span>
|
<span className="font-semibold text-slate-800">Surfaces</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] font-bold text-slate-400">
|
<span className="text-[11px] font-semibold text-slate-400">
|
||||||
{surfaces.length || 1}
|
{surfaces.length || 1}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{surfacesOpen && (
|
{surfacesOpen && (
|
||||||
<div className="ml-6 pl-2.5 border-l border-slate-200/80 mt-1 space-y-1">
|
<div className="pl-7 pr-1 mt-1 space-y-0.5">
|
||||||
{surfaces.map((surf) => (
|
{surfaces.map((surf) => (
|
||||||
<div
|
<div
|
||||||
key={surf.id}
|
key={surf.id}
|
||||||
className="py-1 px-2 rounded-lg text-slate-600 hover:text-slate-900 hover:bg-slate-50 text-[11px] font-medium truncate cursor-pointer"
|
className="py-1 px-2 rounded-md text-slate-600 hover:text-slate-900 hover:bg-slate-50 text-[11px] truncate cursor-pointer"
|
||||||
title={surf.description}
|
title={surf.description}
|
||||||
>
|
>
|
||||||
{surf.name}
|
{surf.name}
|
||||||
@@ -144,7 +141,7 @@ export default function AgentScopeNavigator({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSkillsOpen((v) => !v)}
|
onClick={() => setSkillsOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between py-1 px-1.5 text-slate-700 hover:text-slate-900 rounded-lg font-semibold cursor-pointer group"
|
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{skillsOpen ? (
|
{skillsOpen ? (
|
||||||
@@ -152,22 +149,22 @@ export default function AgentScopeNavigator({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
<Zap className="w-3.5 h-3.5 text-slate-500" />
|
<Zap className="w-3.5 h-3.5 text-slate-400" />
|
||||||
<span className="font-bold">Skills</span>
|
<span className="font-semibold text-slate-800">Skills</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] font-bold text-slate-400">
|
<span className="text-[11px] font-semibold text-slate-400">
|
||||||
{skills.length}
|
{skills.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{skillsOpen && (
|
{skillsOpen && (
|
||||||
<div className="ml-4 pl-2 border-l border-slate-200/80 mt-1 space-y-2">
|
<div className="pl-4 mt-1 space-y-1">
|
||||||
{/* Agent Skills Sub-node */}
|
{/* Agent Skills Sub-node */}
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAgentSkillsOpen((v) => !v)}
|
onClick={() => setAgentSkillsOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between py-1 text-[11px] font-semibold text-slate-600 hover:text-slate-900 cursor-pointer"
|
className="w-full flex items-center justify-between py-1 px-2 text-[11px] font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{agentSkillsOpen ? (
|
{agentSkillsOpen ? (
|
||||||
@@ -175,31 +172,45 @@ export default function AgentScopeNavigator({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-3 h-3 text-slate-400" />
|
<ChevronRight className="w-3 h-3 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
<Zap className="w-3 h-3 text-slate-400" />
|
<Zap className="w-3 h-3 text-brand" />
|
||||||
<span>Agent Skills</span>
|
<span className="font-medium text-slate-700">Agent Skills</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-bold">
|
<span className="text-[10px] text-slate-400 font-semibold">
|
||||||
{agentSkills.length}
|
{agentSkills.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{agentSkillsOpen && (
|
{agentSkillsOpen && (
|
||||||
<div className="ml-4 pl-2 border-l border-slate-200/80 mt-1 space-y-1">
|
<div className="pl-6 pr-1 mt-0.5 space-y-0.5">
|
||||||
{agentSkills.map((skill) => (
|
{agentSkills.map((skill) => {
|
||||||
|
const getSkillIcon = (cat) => {
|
||||||
|
switch ((cat || '').toUpperCase()) {
|
||||||
|
case 'AUTHORING':
|
||||||
|
return Pencil;
|
||||||
|
case 'WORKFORCE':
|
||||||
|
return Users;
|
||||||
|
case 'LOGISTICS':
|
||||||
|
return Package;
|
||||||
|
case 'DISPATCH':
|
||||||
|
return Route;
|
||||||
|
default:
|
||||||
|
return Zap;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const SkillIcon = getSkillIcon(skill.category);
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={skill.id}
|
key={skill.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectSkill(skill)}
|
onClick={() => onSelectSkill(skill)}
|
||||||
className="w-full text-left py-1 px-2 rounded-lg text-slate-600 hover:text-brand hover:bg-brand-tint/60 text-[11px] font-medium flex items-center gap-1.5 truncate cursor-pointer"
|
className="w-full text-left py-1.5 px-2 rounded-md text-slate-600 hover:text-brand hover:bg-brand-tint/60 text-[11px] flex items-center gap-2 truncate cursor-pointer transition-colors group"
|
||||||
>
|
>
|
||||||
{skill.category === 'AUTHORING' ? (
|
<SkillIcon className="w-3 h-3 shrink-0 text-slate-400 group-hover:text-brand transition-colors" />
|
||||||
<Pencil className="w-3 h-3 shrink-0 text-brand" />
|
|
||||||
) : (
|
|
||||||
<Users className="w-3 h-3 shrink-0 text-indigo-500" />
|
|
||||||
)}
|
|
||||||
<span className="truncate">{skill.title}</span>
|
<span className="truncate">{skill.title}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +220,7 @@ export default function AgentScopeNavigator({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBoardOpen((v) => !v)}
|
onClick={() => setBoardOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between py-1 text-[11px] font-semibold text-slate-600 hover:text-slate-900 cursor-pointer"
|
className="w-full flex items-center justify-between py-1 px-2 text-[11px] font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{boardOpen ? (
|
{boardOpen ? (
|
||||||
@@ -218,15 +229,15 @@ export default function AgentScopeNavigator({
|
|||||||
<ChevronRight className="w-3 h-3 text-slate-400" />
|
<ChevronRight className="w-3 h-3 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
<Layers className="w-3 h-3 text-slate-400" />
|
<Layers className="w-3 h-3 text-slate-400" />
|
||||||
<span>Board Skills</span>
|
<span className="font-medium text-slate-700">Board Skills</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-bold">
|
<span className="text-[10px] text-slate-400 font-semibold">
|
||||||
{boardSkills.length}
|
{boardSkills.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{boardOpen && (
|
{boardOpen && (
|
||||||
<div className="ml-4 pl-2 mt-1">
|
<div className="pl-7 pr-2 mt-0.5">
|
||||||
<p className="text-[10px] text-slate-400 leading-snug py-1">
|
<p className="text-[10px] text-slate-400 leading-snug py-1">
|
||||||
None drawing on this agent's surfaces.
|
None drawing on this agent's surfaces.
|
||||||
</p>
|
</p>
|
||||||
@@ -242,7 +253,7 @@ export default function AgentScopeNavigator({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setToolsOpen((v) => !v)}
|
onClick={() => setToolsOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between py-1 px-1.5 text-slate-700 hover:text-slate-900 rounded-lg font-semibold cursor-pointer group"
|
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{toolsOpen ? (
|
{toolsOpen ? (
|
||||||
@@ -250,31 +261,29 @@ export default function AgentScopeNavigator({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
<Wrench className="w-3.5 h-3.5 text-slate-500" />
|
<Wrench className="w-3.5 h-3.5 text-slate-400" />
|
||||||
<span className="font-bold">Tools</span>
|
<span className="font-semibold text-slate-800">Tools</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] font-bold text-slate-400">
|
<span className="text-[11px] font-semibold text-slate-400">
|
||||||
{tools.length}
|
{tools.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{toolsOpen && (
|
{toolsOpen && (
|
||||||
<div className="ml-4 pl-2 border-l border-slate-200/80 mt-1 space-y-1.5">
|
<div className="pl-6 pr-1 mt-1 space-y-0.5">
|
||||||
{tools.slice(0, 4).map((tool) => (
|
{tools.map((tool) => (
|
||||||
<button
|
<button
|
||||||
key={tool.name}
|
key={tool.name}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectTool(tool)}
|
onClick={onSelectTool}
|
||||||
className="w-full text-left py-1.5 px-2 rounded-xl text-slate-700 hover:text-brand hover:bg-brand-tint/60 transition-colors cursor-pointer group"
|
className="w-full text-left py-1 px-2 rounded-md hover:bg-slate-50 text-[11px] group cursor-pointer transition-colors block"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5 text-slate-700 group-hover:text-brand truncate font-mono text-[10.5px]">
|
||||||
<Wrench className="w-3 h-3 text-slate-400 group-hover:text-brand shrink-0" />
|
<Wrench className="w-3 h-3 shrink-0 text-slate-400 group-hover:text-brand" />
|
||||||
<span className="text-[11px] font-semibold truncate capitalize">
|
<span className="truncate">{tool.name}</span>
|
||||||
{tool.name.replace(/_/g, ' ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{tool.parentSkill && (
|
{tool.parentSkill && (
|
||||||
<div className="text-[9px] text-slate-400 font-medium pl-4 truncate">
|
<div className="text-[9.5px] text-slate-400 pl-4 truncate font-sans">
|
||||||
{tool.parentSkill}
|
{tool.parentSkill}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -118,28 +118,25 @@ export default function AgentStudio() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pb-3.5 relative transition-all cursor-pointer flex items-center gap-2 select-none',
|
'pb-3.5 -mb-px transition-all cursor-pointer flex items-center gap-2 select-none border-b-2',
|
||||||
isActive
|
isActive
|
||||||
? 'text-brand font-bold'
|
? 'border-brand text-brand font-bold'
|
||||||
: 'text-slate-500 hover:text-slate-800'
|
: 'border-transparent text-slate-500 hover:text-slate-800 hover:border-slate-300'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>{tab.label}</span>
|
<span>{tab.label}</span>
|
||||||
{badgeCount !== null && (
|
{badgeCount !== null && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1.5 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
'px-2 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-brand-tint text-brand'
|
? 'bg-brand text-white'
|
||||||
: 'bg-slate-100 text-slate-500'
|
: 'bg-slate-100 text-slate-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{badgeCount}
|
{badgeCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isActive && (
|
|
||||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand rounded-full" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Route,
|
Route,
|
||||||
Check,
|
Check,
|
||||||
Layers,
|
Layers,
|
||||||
CheckCircle2,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -24,34 +23,34 @@ export default function SkillCard({ skill, onTest, onDetails }) {
|
|||||||
icon: Pencil,
|
icon: Pencil,
|
||||||
textColor: 'text-brand',
|
textColor: 'text-brand',
|
||||||
bgColor: 'bg-brand-tint',
|
bgColor: 'bg-brand-tint',
|
||||||
borderColor: 'border-brand/30',
|
borderColor: 'border-brand/20',
|
||||||
};
|
};
|
||||||
case 'WORKFORCE':
|
case 'WORKFORCE':
|
||||||
return {
|
return {
|
||||||
icon: Users,
|
icon: Users,
|
||||||
textColor: 'text-indigo-700',
|
textColor: 'text-slate-700',
|
||||||
bgColor: 'bg-indigo-50/80',
|
bgColor: 'bg-slate-100',
|
||||||
borderColor: 'border-indigo-200/80',
|
borderColor: 'border-slate-200',
|
||||||
};
|
};
|
||||||
case 'LOGISTICS':
|
case 'LOGISTICS':
|
||||||
return {
|
return {
|
||||||
icon: Package,
|
icon: Package,
|
||||||
textColor: 'text-emerald-700',
|
textColor: 'text-emerald-700',
|
||||||
bgColor: 'bg-emerald-50/80',
|
bgColor: 'bg-emerald-50',
|
||||||
borderColor: 'border-emerald-200/80',
|
borderColor: 'border-emerald-200',
|
||||||
};
|
};
|
||||||
case 'DISPATCH':
|
case 'DISPATCH':
|
||||||
return {
|
return {
|
||||||
icon: Route,
|
icon: Route,
|
||||||
textColor: 'text-rose-700',
|
textColor: 'text-brand',
|
||||||
bgColor: 'bg-rose-50/80',
|
bgColor: 'bg-brand-tint',
|
||||||
borderColor: 'border-rose-200/80',
|
borderColor: 'border-brand/20',
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
textColor: 'text-slate-700',
|
textColor: 'text-slate-600',
|
||||||
bgColor: 'bg-slate-100',
|
bgColor: 'bg-slate-50',
|
||||||
borderColor: 'border-slate-200',
|
borderColor: 'border-slate-200',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -61,27 +60,27 @@ export default function SkillCard({ skill, onTest, onDetails }) {
|
|||||||
const CategoryIcon = meta.icon;
|
const CategoryIcon = meta.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-3xl border border-slate-200/90 shadow-2xs hover:shadow-sm hover:border-slate-300 transition-all p-5 flex flex-col justify-between relative group">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xs hover:shadow-md hover:border-slate-300 transition-all p-5 flex flex-col justify-between group">
|
||||||
{/* Top Header: Category Tag (Left) & Status Badge (Right) */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-2 mb-3.5">
|
{/* Top Header: Category Tag (Left) & Status Badge (Right) */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[11px] font-bold tracking-wider border',
|
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[10px] font-bold tracking-wider border',
|
||||||
meta.bgColor,
|
meta.bgColor,
|
||||||
meta.textColor,
|
meta.textColor,
|
||||||
meta.borderColor
|
meta.borderColor
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CategoryIcon className="w-3.5 h-3.5" />
|
<CategoryIcon className="w-3 h-3" />
|
||||||
<span>{(skill.category || 'SKILL').toUpperCase()}</span>
|
<span>{(skill.category || 'SKILL').toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold border',
|
'inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10.5px] font-semibold border',
|
||||||
isEnabled
|
isEnabled
|
||||||
? 'bg-brand-tint text-brand border-brand/30'
|
? 'bg-brand-tint/70 text-brand border-brand/20'
|
||||||
: 'bg-slate-100 text-slate-500 border-slate-200'
|
: 'bg-slate-100 text-slate-500 border-slate-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -97,27 +96,27 @@ export default function SkillCard({ skill, onTest, onDetails }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Skill Title */}
|
{/* Skill Title */}
|
||||||
<h3 className="text-base sm:text-[17px] font-bold text-slate-900 font-heading leading-snug">
|
<h3 className="text-base font-bold text-slate-900 font-heading mt-3 tracking-tight">
|
||||||
{skill.title}
|
{skill.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Skill Description */}
|
{/* Skill Description */}
|
||||||
<p className="text-xs sm:text-[13px] text-slate-600 mt-2 leading-relaxed line-clamp-3">
|
<p className="text-xs text-slate-500 mt-1.5 leading-relaxed line-clamp-2">
|
||||||
{skill.description}
|
{skill.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Prompt Trigger Pill (if present) */}
|
{/* Prompt Trigger Pill (if present) */}
|
||||||
{skill.prompt && (
|
{skill.prompt && (
|
||||||
<div className="mt-3.5 p-2.5 rounded-2xl bg-brand-tint/40 border border-brand/20 text-xs text-brand font-medium flex items-center gap-2">
|
<div className="mt-3 p-2 rounded-xl bg-brand-tint/30 border border-brand/15 text-xs text-brand flex items-center gap-2">
|
||||||
<Sparkles className="w-3.5 h-3.5 text-brand shrink-0" />
|
<Sparkles className="w-3.5 h-3.5 text-brand shrink-0" />
|
||||||
<span className="font-medium italic truncate">"{skill.prompt}"</span>
|
<span className="italic truncate font-medium">"{skill.prompt}"</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scope / Integration Tag */}
|
{/* Scope / Integration Tag */}
|
||||||
{skill.integration && (
|
{skill.integration && (
|
||||||
<div className="mt-3.5">
|
<div className="mt-3">
|
||||||
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-xl bg-slate-50 border border-slate-200/80 text-[11px] font-semibold text-slate-600">
|
<div className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-slate-50 border border-slate-200/80 text-[10.5px] font-medium text-slate-600">
|
||||||
<Layers className="w-3 h-3 text-slate-400" />
|
<Layers className="w-3 h-3 text-slate-400" />
|
||||||
<span>{skill.integration}</span>
|
<span>{skill.integration}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,23 +125,23 @@ export default function SkillCard({ skill, onTest, onDetails }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Action Bar */}
|
{/* Footer Action Bar */}
|
||||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-slate-100 text-xs font-semibold">
|
<div className="flex items-center justify-between pt-3.5 mt-3.5 border-t border-slate-100 text-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onTest(skill)}
|
onClick={() => onTest(skill)}
|
||||||
className="inline-flex items-center gap-1.5 text-brand hover:text-brand-dark transition-colors cursor-pointer group-hover:translate-x-0.5 transition-transform"
|
className="inline-flex items-center gap-1.5 text-brand hover:text-brand-dark font-bold transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<Play className="w-3.5 h-3.5 fill-current" />
|
<Play className="w-3 h-3 fill-current" />
|
||||||
<span>Test in Playground</span>
|
<span>Test in Playground</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onDetails(skill)}
|
onClick={() => onDetails(skill)}
|
||||||
className="inline-flex items-center gap-1 text-slate-500 hover:text-slate-900 transition-colors cursor-pointer"
|
className="inline-flex items-center gap-1 text-slate-500 hover:text-slate-900 font-medium transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Details</span>
|
<span>Details</span>
|
||||||
<ArrowRight className="w-3.5 h-3.5" />
|
<ArrowRight className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export default function SkillCardsGrid({
|
|||||||
const boardCount = skills.filter((s) => s.scope === 'board').length;
|
const boardCount = skills.filter((s) => s.scope === 'board').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-4">
|
||||||
{/* 1. Scope Sub-tabs (Agent Skills / Board Skills) */}
|
{/* 1. Scope Sub-tabs (Agent Skills / Board Skills) */}
|
||||||
<div className="flex items-center gap-6 border-b border-slate-200/80 pb-px text-xs sm:text-sm font-semibold">
|
<div className="flex items-center gap-8 border-b border-slate-200/80 text-xs sm:text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -69,26 +69,23 @@ export default function SkillCardsGrid({
|
|||||||
setCategoryFilter('ALL');
|
setCategoryFilter('ALL');
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pb-3 relative transition-colors cursor-pointer flex items-center gap-2',
|
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
|
||||||
scopeTab === 'agent'
|
scopeTab === 'agent'
|
||||||
? 'text-brand font-bold'
|
? 'border-brand text-brand font-bold'
|
||||||
: 'text-slate-500 hover:text-slate-800'
|
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>Agent Skills</span>
|
<span>Agent Skills</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1.5 py-0.5 rounded-full text-[11px] font-bold',
|
'px-2 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
||||||
scopeTab === 'agent'
|
scopeTab === 'agent'
|
||||||
? 'bg-brand-tint text-brand'
|
? 'bg-brand text-white'
|
||||||
: 'bg-slate-100 text-slate-500'
|
: 'bg-slate-100 text-slate-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{agentCount}
|
{agentCount}
|
||||||
</span>
|
</span>
|
||||||
{scopeTab === 'agent' && (
|
|
||||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand rounded-full" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -98,30 +95,27 @@ export default function SkillCardsGrid({
|
|||||||
setCategoryFilter('ALL');
|
setCategoryFilter('ALL');
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pb-3 relative transition-colors cursor-pointer flex items-center gap-2',
|
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
|
||||||
scopeTab === 'board'
|
scopeTab === 'board'
|
||||||
? 'text-brand font-bold'
|
? 'border-brand text-brand font-bold'
|
||||||
: 'text-slate-500 hover:text-slate-800'
|
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>Board Skills</span>
|
<span>Board Skills</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1.5 py-0.5 rounded-full text-[11px] font-bold',
|
'px-2 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
||||||
scopeTab === 'board'
|
scopeTab === 'board'
|
||||||
? 'bg-brand-tint text-brand'
|
? 'bg-brand text-white'
|
||||||
: 'bg-slate-100 text-slate-500'
|
: 'bg-slate-100 text-slate-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{boardCount}
|
{boardCount}
|
||||||
</span>
|
</span>
|
||||||
{scopeTab === 'board' && (
|
|
||||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand rounded-full" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Global Search Bar */}
|
{/* 2. Global Search Input (Sleek pill style matching reference) */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="w-4 h-4 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2 pointer-events-none" />
|
<Search className="w-4 h-4 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
@@ -129,7 +123,7 @@ export default function SkillCardsGrid({
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search skills, capabilities or pages..."
|
placeholder="Search skills, capabilities or pages..."
|
||||||
className="w-full bg-white border border-slate-200/90 rounded-2xl pl-11 pr-10 py-3 text-xs sm:text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-2 focus:ring-brand/20 shadow-2xs transition-all"
|
className="w-full h-10 bg-white border border-slate-200 rounded-full pl-10 pr-9 text-xs text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-1 focus:ring-brand/20 shadow-2xs transition-all"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
@@ -143,7 +137,7 @@ export default function SkillCardsGrid({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Category Filter Pills */}
|
{/* 3. Category Filter Pills */}
|
||||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 no-scrollbar">
|
<div className="flex items-center gap-2 overflow-x-auto pb-0.5 no-scrollbar">
|
||||||
{Object.entries(categories).map(([cat, count]) => {
|
{Object.entries(categories).map(([cat, count]) => {
|
||||||
const isSelected = categoryFilter === cat;
|
const isSelected = categoryFilter === cat;
|
||||||
const displayLabel = cat === 'ALL' ? 'All' : cat.charAt(0) + cat.slice(1).toLowerCase();
|
const displayLabel = cat === 'ALL' ? 'All' : cat.charAt(0) + cat.slice(1).toLowerCase();
|
||||||
@@ -154,17 +148,17 @@ export default function SkillCardsGrid({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCategoryFilter(cat)}
|
onClick={() => setCategoryFilter(cat)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-4 py-1.5 rounded-full text-xs font-semibold transition-all whitespace-nowrap cursor-pointer shadow-2xs flex items-center gap-1.5',
|
'px-3.5 py-1 rounded-full text-xs font-medium transition-all whitespace-nowrap cursor-pointer flex items-center gap-1.5',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-brand text-white shadow-xs'
|
? 'bg-brand text-white shadow-2xs font-semibold'
|
||||||
: 'bg-white border border-slate-200/80 text-slate-600 hover:bg-slate-50 hover:text-slate-900'
|
: 'bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:text-slate-900'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>{displayLabel}</span>
|
<span>{displayLabel}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-bold opacity-80',
|
'text-[10.5px]',
|
||||||
isSelected ? 'text-white' : 'text-slate-400'
|
isSelected ? 'text-white/90 font-bold' : 'text-slate-400'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
@@ -174,39 +168,39 @@ export default function SkillCardsGrid({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. Capabilities Count & "+ New skill" Button */}
|
{/* 4. Capabilities Count & "+ New skill" Action Row */}
|
||||||
<div className="flex items-center justify-between pt-1 text-xs text-slate-500">
|
<div className="flex items-center justify-between pt-1 text-xs text-slate-500">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-bold text-slate-800">
|
<span className="font-semibold text-slate-800">
|
||||||
{filteredSkills.length} of {scopedSkills.length} capabilities
|
{filteredSkills.length} of {scopedSkills.length} capabilities
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
for this scope ·{' '}
|
for this scope ·{' '}
|
||||||
<span className="font-bold text-brand">{enabledCount} enabled</span>
|
<span className="font-semibold text-brand">{enabledCount} enabled</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onNewSkill}
|
onClick={onNewSkill}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-white border border-slate-200/90 text-brand hover:bg-brand-tint hover:border-brand/30 font-bold text-xs shadow-2xs transition-all active:scale-95 cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-white border border-brand text-brand hover:bg-brand-tint font-semibold text-xs shadow-2xs transition-all active:scale-95 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 stroke-[2.5]" />
|
<Plus className="w-3.5 h-3.5 stroke-[2.5]" />
|
||||||
<span>New skill</span>
|
<span>New skill</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. Responsive Skill Cards Grid */}
|
{/* 5. Responsive Skill Cards Grid */}
|
||||||
{scopeTab === 'board' && filteredSkills.length === 0 ? (
|
{scopeTab === 'board' && filteredSkills.length === 0 ? (
|
||||||
<div className="p-10 rounded-3xl border border-dashed border-slate-200 bg-slate-50/50 text-center space-y-2">
|
<div className="p-10 rounded-2xl border border-dashed border-slate-200 bg-white/60 text-center space-y-2">
|
||||||
<Layers className="w-8 h-8 text-slate-400 mx-auto" />
|
<Layers className="w-7 h-7 text-slate-400 mx-auto" />
|
||||||
<div className="text-sm font-bold text-slate-700">No Board Skills Connected</div>
|
<div className="text-sm font-semibold text-slate-700">No Board Skills Connected</div>
|
||||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||||
None drawing on this agent's surfaces. Connect board skills to link live operational boards with this agent.
|
None drawing on this agent's surfaces. Connect board skills to link live operational boards with this agent.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : filteredSkills.length === 0 ? (
|
) : filteredSkills.length === 0 ? (
|
||||||
<div className="p-10 rounded-3xl border border-slate-200/80 bg-white text-center space-y-3 shadow-2xs">
|
<div className="p-10 rounded-2xl border border-slate-200 bg-white text-center space-y-3 shadow-2xs">
|
||||||
<AlertCircle className="w-7 h-7 text-slate-400 mx-auto" />
|
<AlertCircle className="w-7 h-7 text-slate-400 mx-auto" />
|
||||||
<div className="text-sm font-bold text-slate-800">No matching capabilities found</div>
|
<div className="text-sm font-semibold text-slate-800">No matching capabilities found</div>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Try adjusting your search query or selecting a different category filter.
|
Try adjusting your search query or selecting a different category filter.
|
||||||
</p>
|
</p>
|
||||||
@@ -216,13 +210,13 @@ export default function SkillCardsGrid({
|
|||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setCategoryFilter('ALL');
|
setCategoryFilter('ALL');
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-xs font-semibold text-slate-700 transition-colors cursor-pointer"
|
className="px-3.5 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-xs font-medium text-slate-700 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{filteredSkills.map((skill) => (
|
{filteredSkills.map((skill) => (
|
||||||
<SkillCard
|
<SkillCard
|
||||||
key={skill.id}
|
key={skill.id}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export const DEFAULT_SKILLS = [
|
|||||||
id: 'learning-analysis',
|
id: 'learning-analysis',
|
||||||
title: 'Learning Analysis',
|
title: 'Learning Analysis',
|
||||||
category: 'WORKFORCE',
|
category: 'WORKFORCE',
|
||||||
categoryTheme: 'indigo',
|
categoryTheme: 'slate',
|
||||||
scope: 'agent',
|
scope: 'agent',
|
||||||
integration: 'MileTruth AI',
|
integration: 'MileTruth AI',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
119
tests/assistant/pageSuggestions.test.js
Normal file
119
tests/assistant/pageSuggestions.test.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { getPageContext, CHIP_LABELS } from '@/components/assistant/DoormileAI/pageContext';
|
||||||
|
import { resolveIntentId } from '@/components/assistant/intents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every suggestion a page offers must resolve — and must resolve to the
|
||||||
|
* intent that page means.
|
||||||
|
*
|
||||||
|
* `pageContext.jsx` states the rule at the top of the file: "Every suggestion
|
||||||
|
* here MUST be a phrasing the deterministic matcher in intents.js actually
|
||||||
|
* resolves — a suggestion chip that returns 'I can't answer that yet' is worse
|
||||||
|
* than no chip." It was enforced by reading. It is now enforced here.
|
||||||
|
*
|
||||||
|
* Resolving is not enough on its own, because INTENTS is an ordered catalog
|
||||||
|
* and the FIRST match wins. A question can match the intent it was written for
|
||||||
|
* and still be answered by a different one that happens to run earlier —
|
||||||
|
* `ROADMAP.md` B1/B2 are two shipped instances of exactly that. So these tests
|
||||||
|
* assert on which intent actually wins, not merely that something does.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The route of every page that carries the assistant. Sourced from App.jsx.
|
||||||
|
const PAGES = [
|
||||||
|
'/doormile/orders',
|
||||||
|
'/doormile/bookings',
|
||||||
|
'/doormile/deliveries',
|
||||||
|
'/doormile/control-x',
|
||||||
|
'/doormile/milers',
|
||||||
|
'/doormile/vehicles',
|
||||||
|
'/doormile/hubs',
|
||||||
|
'/doormile/tenants',
|
||||||
|
'/doormile/customers',
|
||||||
|
'/doormile/tripsheets',
|
||||||
|
'/doormile/exceptions',
|
||||||
|
'/doormile/pricing',
|
||||||
|
'/doormile/app-users',
|
||||||
|
'/doormile/competitive-intel',
|
||||||
|
'/doormile/reports'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The intent the ordered catalog actually reaches for a question. */
|
||||||
|
const winningIntent = (text) => resolveIntentId(text);
|
||||||
|
|
||||||
|
const chipsFor = (route) => getPageContext(route).suggestions;
|
||||||
|
|
||||||
|
describe('every page suggestion resolves to an intent', () => {
|
||||||
|
PAGES.forEach((route) => {
|
||||||
|
it(`${route}`, () => {
|
||||||
|
const chips = chipsFor(route);
|
||||||
|
expect(chips.length).toBeGreaterThan(0);
|
||||||
|
chips.forEach((chip) => {
|
||||||
|
expect(winningIntent(chip.text)).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('a page offers its own subject, not another page\'s', () => {
|
||||||
|
// The intent id a page's questions are allowed to reach. This is the actual
|
||||||
|
// separation being asked for: on Orders, order intents; on Deliveries,
|
||||||
|
// delivery intents. Not "an intent exists" — the RIGHT one.
|
||||||
|
const ALLOWED = {
|
||||||
|
'/doormile/orders': /order|repeat|create|assign|summary|revenue|delay/i,
|
||||||
|
'/doormile/bookings': /order|delay/i,
|
||||||
|
'/doormile/deliveries': /consignment|order|delay|rider/i,
|
||||||
|
'/doormile/control-x': /order|batch|rider|assign|delay/i,
|
||||||
|
'/doormile/milers': /rider|assign/i,
|
||||||
|
'/doormile/vehicles': /vehicle/i,
|
||||||
|
'/doormile/hubs': /hub/i,
|
||||||
|
'/doormile/tenants': /tenant|partner/i,
|
||||||
|
'/doormile/customers': /customer/i,
|
||||||
|
'/doormile/tripsheets': /tripsheet/i,
|
||||||
|
'/doormile/exceptions': /exception|delay/i,
|
||||||
|
'/doormile/pricing': /pricing/i,
|
||||||
|
'/doormile/app-users': /appuser/i,
|
||||||
|
'/doormile/competitive-intel': /competitor|carrier/i
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(ALLOWED).forEach(([route, allowed]) => {
|
||||||
|
it(`${route} stays on topic`, () => {
|
||||||
|
chipsFor(route).forEach((chip) => {
|
||||||
|
const id = winningIntent(chip.text);
|
||||||
|
expect(`${route} · "${chip.text}" → ${id}`).toMatch(allowed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the two bugs this change fixes', () => {
|
||||||
|
it('Deliveries is no longer the Orders list wearing a different name', () => {
|
||||||
|
const deliveries = chipsFor('/doormile/deliveries').map((c) => c.text);
|
||||||
|
const orders = chipsFor('/doormile/orders').map((c) => c.text);
|
||||||
|
expect(deliveries).not.toEqual(orders);
|
||||||
|
// It used to be `{ ...ORDERS, label: 'Deliveries' }`, so a page of
|
||||||
|
// consignments offered "Create an order" and "Repeat yesterday's orders".
|
||||||
|
expect(deliveries).not.toContain('Create an order');
|
||||||
|
expect(deliveries).not.toContain("Repeat yesterday's orders");
|
||||||
|
expect(deliveries).toContain('How many consignments do we have?');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"carrier pricing rules" would be answered by the WRONG intent', () => {
|
||||||
|
// Kept as a live assertion rather than a comment. carrierPricingCount
|
||||||
|
// documents this exact phrasing for itself, and pricingCount matches
|
||||||
|
// /\bpricing\b/ 57 lines earlier, so the catalog never reaches it.
|
||||||
|
expect(winningIntent('How many carrier pricing rules?')).toBe('pricingCount');
|
||||||
|
// Which is why Competitive Intel asks it the other way.
|
||||||
|
expect(winningIntent('How many carriers do we have?')).toBe('carrierPricingCount');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('chips are readable in a 393px panel', () => {
|
||||||
|
it('every suggestion has a short label', () => {
|
||||||
|
const missing = [];
|
||||||
|
PAGES.forEach((route) => {
|
||||||
|
chipsFor(route).forEach((chip) => {
|
||||||
|
if (!CHIP_LABELS[chip.text]) missing.push(`${route} · ${chip.text}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user