ComponentsAI

Ask bar

The one floating way into Flo and search on every page. It opens a search that asks Flo when nothing matches, and carries the review count and the mic.

Playground

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 { AskBar } from '@/ui/ask-bar';<AskBar placeholder="Ask Flo or search" />

Default

Centred at the foot of every page, 360px wide. It is drawn as the Flo composer is: white, round, with the composer's hairline and soft lift. The count shows only when changes wait and links to Tasks › To review. The mic dictates to Flo.

import { useState } from 'react';
import { AskBar } from '@/ui/ask-bar';
import { toast } from '@/ui/toast';

export default function AskBarDefault() {
  const [listening, setListening] = useState(false);
  return (
    <AskBar
      count={8}
      reviewHref="#to-review"
      onOpen={() => toast.add({ title: 'Search opens here' })}
      onDictate={() => setListening((on) => !on)}
      listening={listening}
    />
  );
}

Nothing waiting

No count when nothing waits. Hover turns the edge to the input grey and the text to ink in 160ms.

import { AskBar } from '@/ui/ask-bar';

export default function AskBarNothingWaiting() {
  return <AskBar onOpen={() => {}} onDictate={() => {}} />;
}

Listening

While the mic is on it is pressed: grey fill, red icon. The words go to the Flo panel.

import { AskBar } from '@/ui/ask-bar';

export default function AskBarListening() {
  return <AskBar onOpen={() => {}} onDictate={() => {}} listening />;
}

Folded to a tab

Within 24px of a page's end the pill folds to an "Ask Flo" tab, so the last row stays readable. Click or drag up to open.

import { AskBar } from '@/ui/ask-bar';

export default function AskBarTab() {
  return <AskBar dock="tab" onOpen={() => {}} />;
}

Inline composer

Where Flo edits what is on screen, such as the CV review and the template screen, the inline composer is the Flo composer with no files: the same island, so the bar and the composer read as one family. Dictated words land in the box to check before sending; while Flo works, Send shows a spinner.

import { useState } from 'react';
import { FloComposer } from '@/ui/flo-composer';

export default function AskBarInlineComposer() {
  const [busy, setBusy] = useState(false);
  const [listening, setListening] = useState<number | null>(null);
  return (
    <div className="w-full max-w-130">
      <FloComposer
        placeholder="Tell Flo what to change in this CV"
        working={busy}
        voice={{ listening: listening !== null, startedAt: listening ?? undefined, onToggle: () => setListening((at) => (at ? null : Date.now())) }}
        onSend={() => {
          setListening(null);
          setBusy(true);
          setTimeout(() => setBusy(false), 1500);
        }}
      />
    </div>
  );
}

Morph into the composer

Click the pill, or press Replay: the pill grows into the Flo composer, and Esc or Send folds it back. It is one view transition, so the box and its content move together; press Reduced motion to see the fallback.

import { useRef, useState } from 'react';
import { AskBar } from '@/ui/ask-bar';
import { FloComposer } from '@/ui/flo-composer';
import { morph } from '@/ui/motion';

export default function AskBarMorph() {
  const [open, setOpen] = useState(false);
  const stage = useRef<HTMLDivElement>(null);
  const toggle = () => morph(() => setOpen((shown) => !shown), stage.current);
  return (
    <div ref={stage} className="flex h-56 w-full flex-col items-center justify-end">
      {open ? (
        <FloComposer
          ref={(form) => form?.querySelector('textarea')?.focus()}
          className="w-full max-w-120 [view-transition-name:ask-bar]"
          onKeyDown={(event) => event.key === 'Escape' && toggle()}
          onSend={toggle}
        />
      ) : (
        <AskBar className="[view-transition-name:ask-bar]" onOpen={toggle} />
      )}
      <button type="button" hidden data-replay onClick={toggle} />
    </div>
  );
}

On this page