/** * The blank transcript, on every Piggy surface. * * There used to be two of these. The workspace had this one; the dock, the * sheet and the phone drawer had a second, narrower one that offered three * read openers, showed a Sparkles glyph and never once mentioned that Piggy * can write — the product's headline capability, missing from the surface most * people keep open all day. One agent gets one front door, so this is now it, * and `narrow` is what the 22rem column asks for instead of a second file. * * It has one job that the old blank transcript did not have: Piggy can write * now, and nobody will discover that by typing into a box. So the openers are * in two columns — what it can find out, and what it can get done — and the * second column says plainly that a change is proposed and waits for a person. * * Every sentence here comes from `lib/piggy-copy`. It is written once because * it was written three times and had already drifted. * * The read openers come from `piggySuggestions`, which picks them by the one * read tool this context resolves to, so every line is one Piggy can ground. * The write openers are held here because there is no equivalent table for them * yet, and they are written against the same constraint: each one is answerable * with the tools a `/piggy` turn is actually given — the workspace summary, the * record lookups, the renewals list — and none of them names a record that only * exists in the demo book. * * Pressing a write opener while Piggy is in Read only moves it to Ask first. * That is a change to a permission, so it is never silent: the card says so * before it is pressed, and the mode control in the header changes with it. Ask * first cannot write unattended — it proposes, and the Apply button is the * person — so the escalation this performs is from "no tools" to "a proposal * you must approve", which is the thing the user just asked for by pressing it. */ import { ArrowRight, PenLine, Search } from 'lucide-react'; import type { PiggyChatContext, PiggyMode } from '@pig/core'; import { piggyCopy, piggyLine } from '@/lib/piggy-copy'; import { piggySuggestions } from '@/lib/piggy-suggestions'; import { PiggyMark } from '@/components/PiggyMark'; import { Label, cn } from '@/components/ui'; /** * Openers that end in a change to the book. * * Every write tool takes a record id, and none of the tools a `/piggy` turn is * given returns one from the page context alone — so each of these is a lookup * followed by a write, and none of them names a record. Naming one would make * them land beautifully on the seeded demo book and fail on the first real * deployment, which is the opposite of the trade this file should make. * * The consequence is stated to the user rather than hidden: where the sentence * does not identify the record, Piggy asks which one instead of choosing. That * is the behaviour a CRM should have, and it is measurably what the default * model does — see the note under the column. */ const WRITE_STARTERS = [ 'Find the block furthest from break-even and log a note on its account.', 'Look up the contract renewing soonest and log a call about extending it.', 'Add a task to chase the account we have not spoken to in a month.', ]; const READ_STARTERS_SHOWN = 3; export function PiggyWorkspaceStarters({ context, mode, canWrite, onAsk, onAskWithChange, narrow = false, }: { context?: PiggyChatContext; /** Only to word the note. The escalation itself belongs to the thread. */ mode: PiggyMode; canWrite: boolean; onAsk: (text: string) => void; /** * An opener that ends in a write. The thread raises the mode first and sends * once the conversation is holding the new one — `send` reads the mode out of * the conversation, so sending in the same tick would ask for a change with * the write tools still withheld. */ onAskWithChange: (text: string) => void; /** * The surface is under ~26rem — the dock, the phone drawer, the workspace's * middle column on a phone. Stacks the two groups, halves the openers and * takes the short form of every sentence. */ narrow?: boolean; }) { /* * Two openers each on a phone, three on a desktop. * * Not a taste decision: the transcript sticks to the bottom of its * scrollport, so anything taller than the viewport opens with its own * heading scrolled off the top. Measured at 393x852 the six-opener version * overran by about 180px, which put the pig, the headline and the first * column header above the fold on the screen that is supposed to introduce * the product. */ const perGroup = narrow ? 2 : READ_STARTERS_SHOWN; const reads = piggySuggestions(context).slice(0, perGroup); const writes = WRITE_STARTERS.slice(0, perGroup); return ( // `flex-1` rather than `h-full`: the conversation viewport's content element // is sized by its children, so a percentage height resolves to nothing. // Centred where there is room to spare, and airless where there is not: on // a narrow surface this front door has to land whole above the composer at // 393x852 and in the dock's 22rem column, and it is measured to.
{/* The agent's mark in the agent's colour. Accent is identity in this product and never meaning, and this is the one place on the front door where the identity is the subject. */}

{piggyLine(piggyCopy.headline, narrow)}

{/* Narrow keeps the boundary and drops the mechanism: the headline has already said a change waits for approval, and the note under the write openers says it again where it is about to matter. Repeating it a third time in a 22rem column costs three lines the openers need to land above the composer. */}

{narrow ? piggyLine(piggyCopy.capability, true) : `${piggyCopy.capability.long} ${piggyCopy.safety.long}`}

} title={piggyLine(piggyCopy.readGroupTitle, narrow)} /* Dropped on a phone, where the two columns are stacked and every line costs: the hero above has just said the same thing, and the note that has to survive is the one about writing. */ note={narrow ? null : piggyLine(piggyCopy.readGroupNote, narrow)} > {reads.map((suggestion) => ( onAsk(suggestion)}> {suggestion} ))} } title={piggyLine(piggyCopy.writeGroupTitle, narrow)} note={piggyLine( canWrite ? mode === 'read_only' ? piggyCopy.writeGroupNote.readOnly : piggyCopy.writeGroupNote.askFirst : piggyCopy.writeGroupNote.noAccess, narrow, )} > {writes.map((suggestion) => ( onAskWithChange(suggestion)} > {suggestion} ))}
); } function StarterGroup({ icon, title, note, narrow, children, }: { icon: React.ReactNode; title: string; note: string | null; /** Measured, not guessed: see the note on the drawer's height below. */ narrow: boolean; children: React.ReactNode; }) { return ( /* * The gaps close on a narrow surface rather than the content thinning. * Measured at 393x852 the phone drawer gives this front door 458px and it * wanted 498, and the 40px it was over came out of white space rather than * out of an opener — a column of two questions is the whole point of the * second column, and one of them is not a choice. */
{children}
{note ?

{note}

: null}
); } function StarterButton({ children, onClick, disabled = false, }: { children: React.ReactNode; onClick: () => void; disabled?: boolean; }) { return ( ); }