Flo message
One turn in the Flo chat: your message in a soft bubble, Flo's reply without one under a single Flo line, with Copy and thumbs on hover and a shimmering line while Flo works.
Playground
The playground drives Flo's reply. Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { FloReply } from '@/ui/flo-message';<FloReply working="Searching candidates…"> Two changes for you to approve.</FloReply>Your message
Right-aligned in a bubble on the current fill: 16px radius, 10px by 16px padding, at most 85% of the column. It shows at once when sent. Files sent with it sit above the bubble.
import { FloUserMessage } from '@/ui/flo-message';
export default function FloMessageUser() {
return (
<div className="flex w-full max-w-90 flex-col">
<FloUserMessage>Move Maya to CV sent and email Sara her CV</FloUserMessage>
</div>
);
}Waiting and not sent
A message sent while Flo is still writing waits, dashed, and says it sends when Flo finishes. A message that didn't send stays at 70% with "Not sent" and Retry, which sends it again without retyping.
Not sent. It stays here, so nothing is lost.
import { FloUserMessage } from '@/ui/flo-message';
export default function FloMessageQueued() {
return (
<div className="flex w-full max-w-90 flex-col gap-300">
<FloUserMessage status="queued">Add her to the Halcyon shortlist too</FloUserMessage>
<FloUserMessage status="failed" onRetry={() => {}}>
Who is waiting at CV sent?
</FloUserMessage>
</div>
);
}Flo's reply
No bubble and no side icon. One line above the reply: Flo's mark, "Flo", and the steps it took, which open on a 2px rail. Then the reply's text, then its cards: here a Flo approval. The newest reply keeps Copy and the thumbs shown.
Two changes for you to approve. The email goes from your own inbox once you send it.
- Maya Chenfrom
Long listtoCV sent - Sara LeeEmail Maya's CV · draft
import { FloApproval } from '@/ui/flo-approval';
import { FloReply } from '@/ui/flo-message';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';
const later = () => new Promise<void>((resolve) => setTimeout(resolve, 800));
const text = 'Two changes for you to approve. The email goes from your own inbox once you send it.';
export default function FloMessageReply() {
return (
<FloReply
className="w-full max-w-90"
latest
steps={[{ title: 'Read Senior Data Engineer' }, { title: 'Found Maya Chen', detail: 'Long list' }, { title: 'Drafted the email to Sara Lee' }]}
copyText={text}
onFeedbackChange={() => {}}
>
<p>{text}</p>
<FloApproval onApprove={later} onDeny={later} onUndo={later} decidedAt="10:42">
<ProposedChangeRow kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen, Stage Long list to CV sent">
<ValueDiff before="Long list" after={<Lozenge phase="client">CV sent</Lozenge>} />
</ProposedChangeRow>
<ProposedChangeRow kind="email" record={<RecordChip variant="inline" type="contact" name="Sara Lee" />} summary="Sara Lee, Email Maya's CV, draft">
Email Maya's CV · draft
</ProposedChangeRow>
</FloApproval>
</FloReply>
);
}Actions on hover
On an older reply, Copy, Good reply and Bad reply fade in under it over 150ms when you hover it or focus inside it. Copy says Copied for two seconds; a thumb pressed again clears it. A record in the text is an inline record chip that opens its peek.
is at CV sent with Halcyon Bank, waiting six days.
import { FloReply } from '@/ui/flo-message';
import { RecordChip } from '@/ui/record-chip';
export default function FloMessageHoverActions() {
return (
<FloReply className="w-full max-w-90" copyText="Maya Chen is at CV sent with Halcyon Bank, waiting six days." onFeedbackChange={() => {}}>
<p>
<RecordChip variant="inline" type="candidate" name="Maya Chen" onOpen={() => {}} /> is at CV sent with Halcyon Bank, waiting six days.
</p>
</FloReply>
);
}Working
While Flo works, the line names what it is doing in words, with a lighter band sweeping it every 2s; past five seconds it adds the time so far. With reduced motion the words stay still.
import { FloReply } from '@/ui/flo-message';
export default function FloMessageWorking() {
return <FloReply className="w-full max-w-90" status="working" working="Searching candidates…" elapsed="0:12" />;
}Steps
Each thing Flo did, on a 2px rail: done steps keep a subtle check, the running one a spinner, a failed one a warning.
- Read Senior Data Engineer
- Searched candidates · 142 match
- Judging fit · 30 of 142
import { FloReply } from '@/ui/flo-message';
export default function FloMessageSteps() {
return (
<FloReply
className="w-full max-w-90"
defaultStepsOpen
steps={[
{ title: 'Read Senior Data Engineer' },
{ title: 'Searched candidates', detail: '142 match' },
{ title: 'Judging fit', detail: '30 of 142', status: 'running' },
]}
/>
);
}Stopped
A reply you stopped keeps what Flo did and says you stopped it.
I found 142 people who match so far.
You stopped this reply.
import { FloReply } from '@/ui/flo-message';
export default function FloMessageStopped() {
return (
<FloReply className="w-full max-w-90" status="stopped" steps={[{ title: 'Searched candidates', detail: '142 match' }]}>
<p>I found 142 people who match so far.</p>
</FloReply>
);
}When things go wrong
Not sent
A message that didn't send stays in the thread at 70%, so nothing typed is lost. Retry sends it again. (The second message in "Waiting and not sent" above.)
Out of time
A turn that ran out of time says so, keeps what it did above, and offers Carry on. "See what happened" opens the turn in Flo activity.
Flo ran out of time before finishing. What it did is above.
See what happenedimport { FloReply } from '@/ui/flo-message';
export default function FloMessageLimit() {
return (
<FloReply
className="w-full max-w-[25rem]"
status="limit"
steps={['Read 40 CVs', { title: 'Judged fit', detail: '28 of 40' }]}
traceHref="#activity"
onCarryOn={() => {}}
/>
);
}Flo couldn't answer
A model error or a timeout says what happened in plain words, in danger, with Retry and the turn's log. No codes.
Flo couldn't answer just now. Try again; if it keeps happening, see what happened.
See what happenedimport { FloReply } from '@/ui/flo-message';
export default function FloMessageError() {
return (
<FloReply
className="w-full max-w-[25rem]"
status="error"
error="Flo couldn't answer just now. Try again; if it keeps happening, see what happened."
traceHref="#activity"
onRetry={() => {}}
/>
);
}Flo won't do it
Some things only the member may do, such as deleting a Company for good. Flo says so in its reply and links to where the member does it. It is a normal reply, not an error.
Deleting a Company for good is yours to do, so I can't do it for you. Open it and choose Delete from its menu.
import { FloReply } from '@/ui/flo-message';
import { Link } from '@/ui/link';
export default function FloMessageRefused() {
return (
<FloReply className="w-full max-w-[25rem]" copyText="Deleting a Company for good is yours to do, so I can't do it for you." onFeedbackChange={() => {}}>
<p>Deleting a Company for good is yours to do, so I can't do it for you. Open it and choose Delete from its menu.</p>
<p>
<Link href="#halcyon-bank">Open Halcyon Bank</Link>
</p>
</FloReply>
);
}A tool failed partway
Flo shows what it got, marks the step that failed with a warning, and says what is missing and how to get it.
- Searched Halcyon Bank · 3 match
- Couldn't search Northwind · the search timed out
I found 3 people at Halcyon Bank. The Northwind search timed out, so nobody from there is in this list yet. Ask me to search Northwind again.
import { FloReply } from '@/ui/flo-message';
export default function FloMessagePartial() {
return (
<FloReply
className="w-full max-w-[25rem]"
defaultStepsOpen
steps={[
{ title: 'Searched Halcyon Bank', detail: '3 match' },
{ title: "Couldn't search Northwind", detail: 'the search timed out', status: 'error' },
]}
copyText="I found 3 people at Halcyon Bank."
onFeedbackChange={() => {}}
>
<p>I found 3 people at Halcyon Bank. The Northwind search timed out, so nobody from there is in this list yet. Ask me to search Northwind again.</p>
</FloReply>
);
}Long and non-Latin text
A long link in your message wraps rather than overflowing. Names in any script keep their characters; a chip that runs out of room ends in an ellipsis with the full name in a tooltip.
Added , with and already on the list of 1,234,567 people.
import { FloReply, FloUserMessage } from '@/ui/flo-message';
import { RecordChip } from '@/ui/record-chip';
export default function FloMessageExtremes() {
return (
<div className="flex w-full max-w-[25rem] flex-col gap-7">
<FloUserMessage>Find https://www.linkedin.com/in/nguyen-thi-minh-khai-senior-data-platform-engineer-halcyon-bank-london and add her</FloUserMessage>
<FloReply>
<p>
Added <RecordChip variant="inline" type="candidate" name="Nguyễn Thị Minh Khai" onOpen={() => {}} />, with{' '}
<RecordChip variant="inline" type="candidate" name="王小明" onOpen={() => {}} /> and{' '}
<RecordChip variant="inline" type="candidate" name="Zoë O'Brien-Smith" onOpen={() => {}} /> already on the list of 1,234,567 people.
</p>
</FloReply>
</div>
);
}On the Flo page
On the Flo page the reading type is 16/26 instead of 15/24; the rest is the same.
Three people have waited more than five days at CV sent. Maya Chen has waited longest, six days, for Halcyon Bank.
import { FloReply, FloUserMessage } from '@/ui/flo-message';
export default function FloMessagePageSize() {
return (
<div className="flex w-full max-w-180 flex-col gap-10">
<FloUserMessage size="page">Who is waiting at CV sent?</FloUserMessage>
<FloReply size="page" latest steps={[{ title: 'Read your open jobs' }]} copyText="Three people have waited more than five days at CV sent." onFeedbackChange={() => {}}>
<p>Three people have waited more than five days at CV sent. Maya Chen has waited longest, six days, for Halcyon Bank.</p>
</FloReply>
</div>
);
}Flo approval
The changes Flo proposed in one turn, for you to approve or deny together: N changes for you, Deny and the green Approve, the rows, then the outcome with Undo.
Flo thread
The Flo conversation: a column of turns that stays pinned to the newest while Flo writes, Latest once you scroll up, and What should Flo do? with suggested asks when it's empty.