Kanzo UI
AI

A guided chat

The parts a conversation whose turns the host decides is built from, beside the transcript and the composer — work under way, a model in the page as it loads, a point to go back to, the steps of the work, the sources a thread rests on and the ones a message carries — and the question it puts to the person, which is @kanzo-tech/ui's.

  • Reading your files… 0 of 6
  • What is a prize's amount?It has no safe answer, so it is yours to decide.
  • What would you like to know?

    Laureate and AwardUniversity

Chat is a conversation whose turns are the model's. A guided chat is one whose turns are the host's — keasy's assistant asks which files to read, reads them, says what it found, puts the decisions it cannot take to the person and offers questions to ask — and it is drawn from Chat's parts and these, so no turn is hand-rolled:

PartForAI Elements
WorkingWork under way, said: a spinner and its wordsShimmer, Loader
ModelBadgeA model in the page, as it downloads and loadsContext
Checkpoint and its partsA point in the transcript the host can go back toCheckpoint
ChainOfThought, ChainOfThoughtStepThe steps of the work — done, under way, waiting on the reader, still to come — as a listChainOfThought
Sources and its partsWhat the thread rests on, folded above the transcript: the reader's words about the data and the sources pickedSources
PromptInputHeaderThe row above the composer's field, for the sources a message carriesPromptInputHeader

A question put to the person is @kanzo-tech/ui's Question, which knows no model either: a decision's options are data, and drawing them is a form. What a model offers is @kanzo-tech/ui's Suggestions, which knows no model: each Suggestion's description is its rationale, in the tooltip, and pending (SuggestionsProps) holds room for the pills still arriving, in skeleton, the strip aria-busy until they land. Chat and Assist draw their offers with it. Everything else a turn carries is @kanzo-tech/ui too: a figure is a Stat, a callout an Alert, a bar a Progress (under Working's words, its value), a heading inside a block an Eyebrow, a file tree a TreeView with its checkboxes, a failure a Problem.

A decision is the question part

A Task's Decision is Question's shape — its question the prompt, each option one of its choices — and the guided chat draws it in a turn with @kanzo-tech/ui's part, the same part a Questionnaire asks several of in a row.

import { Question, QuestionChoice, QuestionChoices, QuestionDescription, QuestionPrompt } from "@kanzo-tech/ui";

<Question name={decision.id} value={chosen} onValueChange={(d) => choose(decision.id, d.value)}>
  <QuestionPrompt>{decision.question}</QuestionPrompt>
  <QuestionDescription>It has no safe answer, so it is yours to decide.</QuestionDescription>
  <QuestionChoices>
    {decision.options.map((option) => (
      <QuestionChoice key={option.id} value={option.id}>
        {option.label}
      </QuestionChoice>
    ))}
  </QuestionChoices>
</Question>

A pick is the answer, in one press. Each choice reports itself through onValueChange as it is picked, so there is no Submit after it, and the arrow keys move between the choices and choose as they move — Ark's radio group, a <label> card around each radio. A decision whose answer is typed — a threshold — takes QuestionInput in place of the choices, and the answer is still a value, never parsed out of the composer's text.

Working

<Working>Thinking…</Working>
<Working value={(done / total) * 100}>Reading your files… {done} of {total}</Working>
<Working value={null}>Making your graph…</Working>

A role="status" with a spinner and the words for what is under way, so the words are announced as they change. Chat draws one where the answer will be. Given a value, @kanzo-tech/ui's Progress sits under the words — Ark's progressbar, named by its value — and without one there is no bar, which is the common case.

WorkingPropsType
valuenumber | null0–100 draws the bar at that value; null draws it indeterminate; left out, no bar

ModelBadge

A model the page fetches before it answers, as a badge: Preparing the model · 42% while it downloads and loads, Model ready, Model failed. Pressed, it opens the detail — the bar and the engine's own words, or the failure as a Problem with a retry. AI Elements' Context is the shape: a small trigger, the detail behind it. It reads a ModelLoad — localModelLoad's, or the host's own for a model fetched some other way.

const load = useSyncExternalStore((l) => watchLocalModel(MODEL, l), () => localModelLoad(MODEL));
<ModelBadge load={load} onRetry={() => void loadLocalModel(MODEL)} />

A popover rather than Context's hover card, because a failure carries a retry and what a hover card holds cannot be reached by the keyboard or on touch. onRetry is offered for a failure the device can recover from, and not for llm/unavailable — no WebGPU, or no model served — which a second try ends the same way.

ModelBadgePropsType
loadModelLoadWhere the load stands
onRetry() => voidStarts a failed load again
describeProblemProps["describe"]The host's words for a failure's code
translationsPartial<ModelBadgeTranslations>The badge's four words, the two details and retry

Checkpoint

  • Chart the contracts by year.
  • A bar per year, counting contracts.
  • Preview 1
  • Split each bar by region.
  • Each bar is now stacked by region.
  • Preview 2
  • Make it a line per region instead.
  • One line per region, by year.
  • Preview 3

A point in the conversation the host can go back to — each preview keasy's assistant draws — as a thin rule across the transcript: a mark, the label, Restore, and the rule to the end of the row. AI Elements' Checkpoint in its composition and its look, and v0's and Lovable's version restore in what it is for.

import { Checkpoint, CheckpointIcon, CheckpointTrigger } from "@kanzo-tech/ai";

<MessageList>
  {turns.map((turn) => (
    <Fragment key={turn.id}>
      <Message role={turn.role}>…</Message>
      {turn.preview && (
        <Checkpoint>
          <CheckpointIcon />
          <span>{turn.preview}</span>
          <CheckpointTrigger onClick={() => restore(turn.id)} />
        </Checkpoint>
      )}
    </Fragment>
  ))}
</MessageList>
Part
CheckpointThe row: its children, then the rule. An <li>, an item of the MessageList beside the turns; asChild for a transcript that is not one
CheckpointIconThe mark, a bookmark unless given another glyph as its child; hidden from assistive tech
CheckpointTriggerThe way back: a quiet Button, Restore unless given words of its own. onClick is the restore
CheckpointTriggerPropsType
translationsPartial<CheckpointTranslations>restore, the trigger's word

The rest are Button's.

It restores nothing. What going back means is the host's, and so is the thread afterwards. Keasy's — and AI Elements' own example — cuts the thread: the turns after the checkpoint go, the preview it marks is current again, and the latest checkpoint carries no trigger because there is nowhere to go back to. A host that keeps the cut turns as a branch draws them itself; that is MessageBranch, which is not admitted (The references).

No tooltip on the trigger. AI Elements' takes one, saying what restoring does; a host that wants the sentence gives the trigger it as its words, which a reader on touch can also read.

ChainOfThought

What I am doing
  1. Done: Read your files
    6 files, 4 of them tables
  2. In progress: Match the columns to the questions
  3. Waiting for you: Choose what identifies a prize
    Two columns could; it is yours to say
  4. Not started: Write the program
  5. Not started: Build your graph

The steps of the work, all visible: Read your files done, Match the columns under way, Choose what identifies a prize waiting on the reader, Write the program and the rest still to come. Each row is a line with a mark, an optional detail under it and an optional action at its end. AI Elements' ChainOfThought and ChainOfThoughtStep in their names and props — label, description, status, icon — for the host's steps rather than the model's reasoning, which is Reasoning.

import { ChainOfThought, ChainOfThoughtStep } from "@kanzo-tech/ai";
import { Button, Eyebrow } from "@kanzo-tech/ui";

<Eyebrow>What I am doing</Eyebrow>
<ChainOfThought>
  <ChainOfThoughtStep label="Read your files" description="6 files" />
  <ChainOfThoughtStep label="Match the columns to the questions" status="active" />
  <ChainOfThoughtStep label="Choose what identifies a prize" status="waiting" />
  <ChainOfThoughtStep
    action={<Button size="sm" variant="link">Open in the editor</Button>}
    label="Write the program"
    status="pending"
  />
</ChainOfThought>
ChainOfThoughtStepPropsType
labelReactNodeThe step's line
descriptionReactNodeUnder the label, quieter
statusChainOfThoughtStatus: "complete" | "active" | "waiting" | "pending"Defaults to complete. waiting is the step that is the reader's to take; the other three are AI Elements'
iconComponentType<{ className?: string }>A glyph in place of the status's own
actionReactNodeTrailing the row: the step's one verb
translationsPartial<ChainOfThoughtTranslations>The four statuses, said to assistive tech

children are drawn under the detail, for a body of the host's own. A row's mark is paint, so the status is also said in words, ahead of the label, and the step that is the work's present — active or waiting — is aria-current="step". It is an <ol>: the order is the meaning.

Sources

Grants the guild awarded between 2015 and 2024, by region.

Warehouse · 2Shared drive · 1

What the thread rests on, folded above the transcript: Sources, a pill with how many, and — open — the reader's own words about the data and a pill per source. AI Elements' Sources, SourcesTrigger and SourcesContent, which sit under an answer, drawn once above the whole thread.

import { Source, Sources, SourcesContent, SourcesTrigger } from "@kanzo-tech/ai";

<Sources defaultOpen>
  <SourcesTrigger count={sources.length} />
  <SourcesContent>
    <p className="basis-full">{about}</p>
    {sources.map((source) => (
      <Source key={source.id}>{source.name} · {source.files}</Source>
    ))}
  </SourcesContent>
</Sources>
Part
Sources@kanzo-tech/ui's Collapsible, closed unless defaultOpen
SourcesTriggerThe row that folds it: Sources (or its children), the count as a pill, the chevron. SourcesTriggerProps adds count and translations (SourcesTranslations: sources)
SourcesContentWhat is inside, wrapping: a child that should have a line of its own takes basis-full
SourceOne pill; its words are its children. A source that opens somewhere is asChild over the host's link

Two places for sources, two jobs. The pills in PromptInputHeader are what the next message is asked over, and they change; Sources is what the thread has been about, read-only, with the reader's words beside it.

PromptInputHeader

The row above the composer's field, for what the message will carry. Keasy puts a pill there per connection the question is asked over; pressed, a pill opens @kanzo-tech/ui's Menu with a MenuCheckboxItem per file, so the reader picks what the question reads without leaving the composer. The pills and the menu are the host's: the header is the place, and it wraps, so a row of sources in a narrow panel takes a second line.

import { PromptInput, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar } from "@kanzo-tech/ai";
import { InputGroupButton, Menu, MenuCheckboxItem, MenuContent, MenuTrigger } from "@kanzo-tech/ui";

<PromptInput onSubmit={send}>
  <PromptInputHeader>
    {connections.map((connection) => (
      <Menu closeOnSelect={false} key={connection.id}>
        <MenuTrigger asChild>
          <InputGroupButton pill variant="outline">{connection.name} · {chosen(connection).length}</InputGroupButton>
        </MenuTrigger>
        <MenuContent>
          {connection.files.map((file) => (
            <MenuCheckboxItem checked={isChosen(file)} key={file} onCheckedChange={(on) => choose(file, on)} value={file}>
              {file}
            </MenuCheckboxItem>
          ))}
        </MenuContent>
      </Menu>
    ))}
  </PromptInputHeader>
  <PromptInputTextarea placeholder="Ask about these files…" />
  <PromptInputToolbar>
    <PromptInputSubmit />
  </PromptInputToolbar>
</PromptInput>

A press on the row outside a pill puts the caret in the field, as the toolbar's does. No attachment cards: AI Elements fills this row with Attachments, a card per file sent, and a source here is a connection the question reads rather than a file uploaded with it. Chat's context stays beside Send, in the toolbar: it is one read-only pill, and a row of its own would cost the side panel a line (The references has what would move it).

Held by packages/ui/src/composites/question.test.tsx, "is a radio group named by its prompt, a choice per radio, outside any questionnaire", "answers with the choice picked, in one press, and draws the answer it is given", "moves between the choices with the arrow keys, choosing as it moves"; packages/ai/src/working.test.tsx, "is one status saying what is under way, its spinner hidden so it is not announced twice" and "draws a bar under the words given a value, indeterminate given null"; packages/ai/src/model-badge.test.tsx, "opens a failure as a Problem, with a retry only where the device can recover"; packages/ai/src/checkpoint.test.tsx, "is an item of the transcript between two turns, its rule hidden as paint", "restores nothing itself: the trigger is a button that hands the press to the host"; packages/ai/src/prompt-input.test.tsx, "carries the host's pills above the field, each opening what it holds"; packages/ai/src/chain-of-thought.test.tsx, "is an ordered list with every step visible, the future ones too" and "says each status in words, since its mark is paint, and marks the present step"; packages/ai/src/sources.test.tsx, "is a trigger naming the block and counting its sources, closed until pressed". Why these and not the rest of AI Elements is The references.

On this page