import { ReactNode } from 'react'; import { AssistantModeConfig } from './conversation'; import { AssistantMode, PageContext } from './types'; /** * What a product tells the shared assistant about ITSELF. * * ★★ Deliberately tiny, and it should stay that way. The obvious design here is * a broad capabilities object — transport, RBAC, export, artifact download, * quota, branding — and it would have been wrong. Every one of those turned out * to be either already shared or shareable once looked at: * * - `downloadAssistantArtifact` already exists in `./api`; the product's copy * was a five-line adapter dropping a vestigial argument. * - the conversation export service had ZERO product references. * - modes, RBAC and quota are consumed by the product's own orchestrator, * which composes this library rather than living inside it. * * What genuinely varies by product, and cannot be derived, is two things: WHICH * MODES it offers, and WHAT IT CALLS THE PAGE the user is on. A field is only * added here when a shared component cannot work without it and no product can * supply it by any other means. An interface that grows to fit one product is * how HealthyBowl's shape ends up baked into every other product's library. */ export interface AssistantProductCapabilities { /** * The modes this product offers, in display order. * * fe-libs owns the SHAPE (`AssistantModeConfig`); the product owns the LIST. * FluidGrids offers "Build a node", HealthyBowl offers "Take action", and * neither should be able to render the other's. */ modes: readonly AssistantModeConfig[]; /** * A human label for where the user is right now — "Crop planning", "Workflow * editor" — or null when there is nothing useful to say. * * Optional because it is genuinely optional: a product with no route model * simply has no answer, and the empty state drops the sentence rather than * inventing one. Resolving a route to an entity is deep product knowledge and * a wrong answer is worse than none. */ describeCurrentPage?: () => string | null; /** * Where the user is, as the AGENT's context — structured, not a label. * * Resolving a route to an entity is deep product knowledge and a wrong entity * is worse than none, so a product with no route model omits this and the turn * simply carries no page context. The same field already exists on * `SandboxAssistantConfig` for the transport; this is the panel's half. */ gatherPageContext?: () => PageContext; /** * The product's system preamble, wrapped around what the user typed. * * Irreducible: HealthyBowl tells the agent it is an agronomy assistant; * another product would say something else entirely. Any shared default is * one product's voice imposed on the rest, so when absent the user's text is * sent unwrapped. */ buildPrompt?: (mode: AssistantMode, content: string, languageName?: string, isExplicitChoice?: boolean, dictated?: boolean) => string; /** * Record what a turn cost, against the product's own billing. * * ★ Irreducible because the MUTATION differs — `reportHealthyBowlAiTokenUsage` * against the transport's `reportAiTokenUsage`. Different backend paths * writing different rows, so defaulting to either silently bills the wrong * ledger. Absent means the product does not meter, which is a real choice. */ reportTokenUsage?: (usage: { workspaceId: string; sessionId: string; messageId?: string; model?: string; tokens: { input: number; output: number; }; }) => Promise; /** * Where the assistant's files land in the workspace file library. * * ★★ Irreducible, and unusually load-bearing: these are real paths holding * real uploads. A wrong value does not fail loudly — it quietly starts a * SECOND folder beside the first, so yesterday's attachments are still there * and no longer reachable from the panel. * * Shared code cannot derive it. HealthyBowl's is `/HealthyBowl/AI Assistant/…` * — capitalised as a display name — while its product id is `healthybowl`; * deriving from the id would rename the folder and orphan everything in it. * * Omitted, it falls back to `//AI Assistant/…`, which is correct for * a product that has never stored anything and wrong only for one that * already has files elsewhere. That product knows, and says so here. */ storageFolders?: { attachments: string; artifacts: string; }; } export interface AssistantProductValue extends AssistantProductCapabilities { /** * Product id, e.g. `healthybowl`. The SAME value the transport sends as * `AI_ASSISTANT_PRODUCT` and the prefix for `.assistant.*` keys, so * "which product is this?" has exactly one answer rather than two that drift. */ product: string; } export interface AssistantProductProviderProps extends AssistantProductCapabilities { product: string; children: ReactNode; } /** * Mount once, above the assistant. Everything below can ask what product it is * running inside and what that product offers. */ export declare function AssistantProductProvider({ product, modes, describeCurrentPage, gatherPageContext, buildPrompt, reportTokenUsage, storageFolders, children, }: AssistantProductProviderProps): import("react/jsx-runtime").JSX.Element; /** The product in force, or null when no provider is mounted. */ export declare function useAssistantProductOrNull(): AssistantProductValue | null; /** * The product in force. * * ★ Returns an empty descriptor rather than throwing when no provider is * mounted. A missing provider is a configuration mistake, but a throw inside * render is a white screen, and a mode selector with nothing in it is a visible, * survivable failure that says exactly what is wrong. `useAssistantTr` complains * to the console on the same condition, so it is still findable. */ export declare function useAssistantProduct(): AssistantProductValue; /** * The mode a product would show for an id, falling back to its first. * * Shared because two components need exactly this and got it slightly * differently: a persisted mode can name something the product no longer offers * — a flag turned off, or a conversation restored from another build — and * `undefined` there renders an empty panel rather than a usable one. */ export declare function resolveModeConfig(modes: readonly AssistantModeConfig[], mode: string | undefined): AssistantModeConfig | undefined; /** * Where this product's assistant files belong, with its override applied. * * ★ ONE resolver, used by BOTH call sites — the attachment upload and the * artifact save button. Two independent copies of this default is exactly how * the two folders drift apart, and a file written to the wrong one is invisible * rather than missing. */ export declare function useAssistantStorageFolders(): { attachments: string; artifacts: string; }; /** * The product's label for the page the user is on — LIVE, not frozen at mount. * * ★★ One implementation, two consumers. The header polled and the empty state * did not: it was `useMemo(() => describeCurrentPage?.(), [describeCurrentPage])`, * and `describeCurrentPage` is a stable module-level function, so that memo * never re-ran. Open the panel on Crops, navigate to Livestock, and the header * said "Context: Livestock" while the empty state four lines below still said * "You're viewing Crops" — the same freeze the header was fixed for once * already, surviving in the component next to it. * * ★ Not `useLocation()`. This is exported for a HOST app to mount, and a * consumer outside a Router would get a thrown hook rather than a stale label. * Polling is router-agnostic and catches pushState, which `popstate` alone * misses entirely. */ export declare function useAssistantPageLabel(active?: boolean): string | null; //# sourceMappingURL=product.d.ts.map