env changes

This commit is contained in:
2026-09-24 13:18:02 +05:30
parent 23da1872f2
commit a13f7caddc
4 changed files with 459 additions and 147 deletions

View File

@@ -8,6 +8,7 @@ import {
type AssistantAnswer,
} from '@/api/assistant';
import { useAssistantScope } from './assistantScope';
import { matchAssistantRoute } from './assistantContext';
import { ArrowUp, History, Maximize2, Minimize2, PanelRightClose } from 'lucide-react';
import {
DEFAULT_WIDTH,
@@ -17,132 +18,6 @@ import {
useAssistantWidth,
} from './assistantWidth';
/**
* Per-route context, so the panel knows which page it is sitting beside.
*
* `agent` names which assistant answers here, and its absence is meaningful: a
* route without one has no assistant yet, and the composer says so rather than
* accepting a question nothing can answer.
*
* A page is only given an agent when that agent can answer every chip on it. A
* chip that comes back "I cannot look that up" is worse than no chip — it reads
* as the assistant being broken rather than as a feature not built yet. Reports
* has no agent for exactly that reason: two of its four questions have no tool
* behind them.
*
* The agents themselves are YAML on the backend, so a new one is a file there
* and one line here.
*/
const CONTEXT: Record<
string,
{
page: string;
title: string;
greeting: string;
reading: string;
prompts: string[];
agent?: string;
}
> = {
'/nearle/stores': {
page: 'Stores',
title: 'Nearle Buddy',
greeting: 'Every tenant on the platform, and which of them have branches sitting idle.',
reading: 'Would cover the tenant directory — branch counts, status and per-tenant performance.',
prompts: [
'Which tenants have no branches?',
'Who onboarded most recently?',
'What needs attention?',
'Summarise platform growth',
],
},
'/nearle/onboard/tenant': {
page: 'Onboard tenant',
title: 'Provisioning a tenant',
greeting: 'Registering the enterprise also creates its primary Administrator account.',
reading: 'Would cover the onboarding form — required fields, category and head-office address.',
prompts: ['What does provisioning create?', 'Which fields are required?', 'What happens next?'],
},
'/nearle/catalogue': {
page: 'Global catalogue',
title: 'Stocking a store',
greeting: 'The catalogue carries a price range, not a price — the store sets the real one.',
reading: 'Would cover the global catalogue and what this tenant has already imported.',
prompts: [
'Which products are already imported?',
'Catalogue or spreadsheet?',
'What does re-importing do?',
'Which columns does the sheet need?',
],
},
/* ── Store Admin ──────────────────────────────────────────────────────── */
'/admin/console': {
agent: 'console',
page: 'Console',
title: 'Across your branches',
greeting: 'App sales, counter sales and imported bills are counted separately — they live in different ledgers.',
reading: 'Would cover every branch — revenue by channel, stock health, till status and what is waiting on you.',
prompts: [
'Which branch is underperforming?',
'Any tills not syncing?',
'What needs my approval?',
'Where is stock running out?',
],
},
'/admin/sales': {
agent: 'orders',
page: 'Sales',
title: 'Orders and deliveries',
greeting: 'An app order and a counter bill are both sales, but only one of them has a delivery.',
reading: 'Would cover orders, counter bills and delivery progress across your branches.',
prompts: [
'Why is the cancel rate high?',
'Which orders are stuck?',
'Compare online and counter sales',
'What is out for delivery?',
],
},
'/admin/inventory': {
agent: 'inventory',
page: 'Inventory',
title: 'Catalogue and stock',
greeting: 'A product in the store catalogue does not mean stock on the shelf — that takes an approved request.',
reading: 'Would cover your catalogue, per-branch stock levels and the stock requests waiting on you.',
prompts: [
'What is waiting for approval?',
'What is low or out of stock?',
'What has run out completely?',
'Which branch is waiting on most stock?',
],
},
'/admin/users': {
page: 'Users & access',
title: 'Who can sign in',
greeting: 'A till account and a console login are two different things — a cashier has no console access at all.',
reading: 'Would cover your back-office directory and the till accounts at each branch.',
prompts: [
'What is the difference between the two?',
'How do I add a cashier?',
'Why can I not delete someone?',
'Who is inactive?',
],
},
'/admin/reports': {
page: 'Reports',
title: 'Revenue, sales and stock',
greeting: 'Fast and slow movers are the two lists that change what you order next.',
reading: 'Would cover revenue by channel and branch, product performance and stock movement.',
prompts: [
'Which products are slow moving?',
'Compare my branches',
'Online versus counter revenue',
'What is my inventory worth?',
],
},
};
/** One question and what came back. `answer` and `error` are exclusive. */
interface Exchange {
question: string;
@@ -172,7 +47,11 @@ function replaceLast(thread: Exchange[], update: (entry: Exchange) => Exchange):
* model can do nothing, but a person on the Inventory page can move to Sales
* and get an answer today.
*/
function composerHint(available: boolean | null, hasAgent: boolean, isAsking: boolean): string {
export function composerHint(
available: boolean | null,
hasAgent: boolean,
isAsking: boolean,
): string {
if (isAsking) return 'Thinking…';
if (available === null) return 'Checking…';
if (available === false) return 'Not connected yet';
@@ -180,6 +59,33 @@ function composerHint(available: boolean | null, hasAgent: boolean, isAsking: bo
return 'Ask about this page';
}
/**
* The line under the composer, or nothing.
*
* This used to be one hardcoded sentence — "Not connected yet — there is no
* assistant service behind this panel." — written before the panel had an API
* behind it and never removed once it did. It rendered on every page in every
* state, so the panel told everybody Buddy was off, permanently, including on a
* deployment where Buddy was answering questions. It also flatly contradicted
* the placeholder three lines above it, which was the only part telling the
* truth.
*
* The lesson worth keeping: a status message with no condition attached is not
* a status message. Each branch here reads the same two facts the composer
* itself is disabled by, so the two cannot drift apart again.
*/
export function composerNote(available: boolean | null, hasAgent: boolean): string {
// Still asking. A sentence that appears for 200ms and is replaced reads as a
// flicker, so the space stays empty until there is something true to put in it.
if (available === null) return '';
if (available === false) return 'Not connected yet — there is no assistant service behind this panel.';
if (!hasAgent) return 'No assistant for this page yet. Try Console or Sales.';
// Buddy is working. The caveat that matters then is not about connection: it
// is that an answer is a model reading live rows, and the rows are the thing
// to act on.
return 'Buddy reads your live data. Check anything you act on.';
}
/**
* A change waiting on the person.
*
@@ -419,13 +325,6 @@ function Exchange({
* through to the fallback would then be a compile error rather than simply a
* page with no assistant, which is what it is.
*/
const FALLBACK: (typeof CONTEXT)[string] = {
page: 'Console',
title: 'Good afternoon',
greeting: 'Ask about anything on this page.',
reading: 'Would cover this page.',
prompts: ['What needs attention?', 'Summarise this page'],
};
/**
* Nearle Buddy, as a layout column.
@@ -454,8 +353,7 @@ export function AssistantPanel({
const { width, setWidth, reset } = useAssistantWidth();
const [isDragging, setDragging] = useState(false);
const key = Object.keys(CONTEXT).find((entry) => pathname.startsWith(entry));
const routeContext = (key ? CONTEXT[key] : undefined) ?? FALLBACK;
const { key, context: routeContext } = matchAssistantRoute(pathname);
/**
* The heading follows the branch in view.
@@ -839,17 +737,19 @@ export function AssistantPanel({
</button>
</form>
<p
style={{
margin: 0,
fontSize: 10,
lineHeight: 1.4,
color: 'var(--color-ink-4)',
textAlign: 'center',
}}
>
Not connected yet — there is no assistant service behind this panel.
</p>
{composerNote(available, Boolean(agent)) ? (
<p
style={{
margin: 0,
fontSize: 10,
lineHeight: 1.4,
color: 'var(--color-ink-4)',
textAlign: 'center',
}}
>
{composerNote(available, Boolean(agent))}
</p>
) : null}
</div>
</aside>
);