# Fine-tune Card

> Adjust something an agent produced with a compact panel of number, choice, and menu controls.

Source: https://ai.honestui.com/docs/components/fine-tune-card

```tsx
"use client"

import * as React from "react"

import { FineTuneCard, type FineTuneValues } from "@/components/ai/fine-tune-card"

const defaultValues: FineTuneValues = { temperature: 40, length: 300, tone: "neutral", format: "prose" }

/** Nothing is regenerated here. The values under the card are what `onValuesChange` receives. */
export function FineTuneCardDemo() {
  const [values, setValues] = React.useState(defaultValues)

  return (
    <div className="flex w-full max-w-xs min-w-0 flex-col gap-[var(--hui-space-3)]">
      <FineTuneCard
        title="Reply style"
        defaultValues={defaultValues}
        onValuesChange={setValues}
        controls={[
          { type: "number", id: "temperature", label: "Variety", min: 0, max: 100, step: 5 },
          { type: "number", id: "length", label: "Max words", min: 50, max: 1000, step: 50 },
          {
            type: "segment",
            id: "format",
            label: "Format",
            options: [
              { value: "prose", label: "Prose" },
              { value: "list", label: "List" },
            ],
          },
          {
            type: "select",
            id: "tone",
            label: "Tone",
            options: [
              { value: "neutral", label: "Neutral" },
              { value: "formal", label: "Formal" },
              { value: "friendly", label: "Friendly" },
            ],
          },
        ]}
      />
      <p
        aria-live="polite"
        className="m-0 font-mono text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-mono-mini)]"
      >
        {JSON.stringify(values)}
      </p>
    </div>
  )
}

```

## Overview [#overview]

Fine-tune Card is a small panel of controls for adjusting a result without writing another prompt: a chart's size, a reply's length, a tone. You describe the controls as data, and the card reports every value when one changes.

The card holds the values and nothing else. Regenerating or restyling the result is your application's work, in `onValuesChange`.

## Anatomy [#anatomy]

<Anatomy title="FineTuneCard">
  <AnatomyItem name="Header" description="The title, an Edited note, and Reset" />

  <AnatomyItem name="Number" description="A field that can be typed into, stepped, or scrubbed by its label" />

  <AnatomyItem name="Segment" description="A small set of choices, one always chosen" />

  <AnatomyItem name="Select" description="A menu, for longer lists" />
</Anatomy>

Installing `fine-tune-card` brings [Button](https://www.honestui.com/docs/components/button), [Number Field](https://www.honestui.com/docs/components/number-field), [Select](https://www.honestui.com/docs/components/select), and [Toggle](https://www.honestui.com/docs/components/toggle) along as registry dependencies.

## Behavior [#behavior]

### Numbers [#numbers]

A number field accepts typing, steps with the arrow keys, and scrubs when its label is dragged sideways. `min`, `max`, and `step` are enforced by the field.

### Edited and Reset [#edited-and-reset]

When any value differs from `defaultValues`, the header says Edited and offers Reset, which returns every control to its default and reports the defaults through `onValuesChange`.

## Accessibility [#accessibility]

The card is a group named by its title. Its controls are the Honest UI [Number Field](https://www.honestui.com/docs/components/number-field), [Toggle](https://www.honestui.com/docs/components/toggle) group, and [Select](https://www.honestui.com/docs/components/select), each named by its label, and they keep their own keyboard behavior. Scrubbing is an extra for pointer users. Every value can also be typed.

Edited is a polite status, so it is announced when the first change is made.

| Key                                  | Action                             |
| ------------------------------------ | ---------------------------------- |
| <kbd>Tab</kbd>                       | Move across the controls           |
| <kbd>↑</kbd> and <kbd>↓</kbd>        | Step a number field                |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Choose a segment, or open the menu |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/fine-tune-card&#x22;]" />
  

  
    
      
        Add the components it is built from.
      

      <CliBlock commands="[&#x22;button&#x22;, &#x22;number-field&#x22;, &#x22;select&#x22;, &#x22;toggle&#x22;]" />

      
        Copy and paste this file into your project.
      

      ### components/ai/fine-tune-card.tsx

```tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  NumberField,
  NumberFieldGroup,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/components/ui/number-field"
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Toggle, ToggleGroup } from "@/components/ui/toggle"

type ControlOption = { value: string; label: string }

export type FineTuneControl =
  | { type: "number"; id: string; label: string; min?: number; max?: number; step?: number }
  | { type: "select"; id: string; label: string; options: ControlOption[] }
  | { type: "segment"; id: string; label: string; options: ControlOption[] }

export type FineTuneValues = Record<string, number | string>

export type FineTuneCardLabels = {
  edited: string
  reset: string
}

export const defaultFineTuneCardLabels: FineTuneCardLabels = {
  edited: "Edited",
  reset: "Reset",
}

const smallText =
  "[font-size:var(--hui-font-size-small)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)]"

export type FineTuneCardProps = Omit<React.ComponentProps<"div">, "children" | "title"> & {
  /** What is being adjusted, such as "Chart" or "Reply style". */
  title: string
  controls: FineTuneControl[]
  /** The starting value for every control `id`. Reset returns to these. */
  defaultValues: FineTuneValues
  /** Runs with every value whenever one changes. */
  onValuesChange?: (values: FineTuneValues) => void
  labels?: Partial<FineTuneCardLabels>
}

/**
 * A compact panel of controls for adjusting something an agent produced.
 * Number fields can be typed into, stepped with the arrow keys, or scrubbed
 * by dragging their label.
 */
function FineTuneCard({
  title,
  controls,
  defaultValues,
  onValuesChange,
  labels: labelsProp,
  className,
  ...props
}: FineTuneCardProps) {
  const labels = { ...defaultFineTuneCardLabels, ...labelsProp }
  const [values, setValues] = React.useState(defaultValues)
  const titleId = React.useId()
  const isEdited = controls.some((control) => values[control.id] !== defaultValues[control.id])

  const update = (next: FineTuneValues) => {
    setValues(next)
    onValuesChange?.(next)
  }
  const setValue = (id: string, value: number | string) => update({ ...values, [id]: value })

  return (
    <div
      role="group"
      aria-labelledby={titleId}
      data-slot="fine-tune-card"
      className={cn(
        "w-full min-w-0 rounded-[var(--hui-radius-4)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-soft)]",
        smallText,
        className
      )}
      {...props}
    >
      <div className="flex min-h-[var(--hui-space-10)] items-center gap-[var(--hui-space-3)] border-b-[0.5px] border-[var(--hui-color-border-base-primary)] py-[var(--hui-space-2)] ps-[var(--hui-space-4)] pe-[var(--hui-space-2)]">
        <p id={titleId} className="m-0 min-w-0 flex-1 truncate [font-weight:var(--hui-font-weight-medium)]">
          {title}
        </p>
        <span role="status" className="shrink-0 text-[var(--hui-color-foreground-base-secondary)]">
          {isEdited ? labels.edited : ""}
        </span>
        {isEdited && (
          <Button variant="link" size="sm" onClick={() => update(defaultValues)}>
            {labels.reset}
          </Button>
        )}
      </div>

      <div className="flex flex-col gap-[var(--hui-space-4)] p-[var(--hui-space-4)]">
        {controls.map((control) => (
          <FineTuneField
            key={control.id}
            control={control}
            value={values[control.id]}
            onChange={(value) => setValue(control.id, value)}
          />
        ))}
      </div>
    </div>
  )
}

function FineTuneField({
  control,
  value,
  onChange,
}: {
  control: FineTuneControl
  value: number | string | undefined
  onChange: (value: number | string) => void
}) {
  const labelId = React.useId()

  if (control.type === "number") {
    return (
      <NumberField
        size="sm"
        className="w-full flex-row items-center justify-between gap-[var(--hui-space-4)]"
        min={control.min}
        max={control.max}
        step={control.step}
        value={typeof value === "number" ? value : null}
        onValueChange={(next) => {
          if (next !== null) onChange(next)
        }}
      >
        <NumberFieldScrubArea label={control.label} />
        <NumberFieldGroup className="w-24">
          <NumberFieldInput />
        </NumberFieldGroup>
      </NumberField>
    )
  }

  const label = (
    <span id={labelId} className="shrink-0 text-[var(--hui-color-foreground-base-secondary)]">
      {control.label}
    </span>
  )

  if (control.type === "segment") {
    return (
      <div className="flex min-w-0 flex-wrap items-center justify-between gap-[var(--hui-space-3)]">
        {label}
        <ToggleGroup
          size="sm"
          aria-labelledby={labelId}
          value={typeof value === "string" ? [value] : []}
          onValueChange={(next) => {
            // Pressing the active segment would clear it; one segment always stays chosen.
            const chosen = next[0]
            if (typeof chosen === "string") onChange(chosen)
          }}
        >
          {control.options.map((option) => (
            <Toggle key={option.value} value={option.value}>
              {option.label}
            </Toggle>
          ))}
        </ToggleGroup>
      </div>
    )
  }

  return (
    <div className="flex min-w-0 items-center justify-between gap-[var(--hui-space-4)]">
      {label}
      <Select
        items={control.options}
        value={typeof value === "string" ? value : null}
        onValueChange={(next) => {
          if (typeof next === "string") onChange(next)
        }}
      >
        <SelectTrigger size="sm" aria-labelledby={labelId} className="w-36">
          <SelectValue />
        </SelectTrigger>
        <SelectPopup>
          {control.options.map((option) => (
            <SelectItem key={option.value} value={option.value}>
              {option.label}
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
    </div>
  )
}

export { FineTuneCard }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { FineTuneCard } from "@/components/ai/fine-tune-card"

<FineTuneCard
  title="Reply style"
  defaultValues={{ length: 300, tone: "neutral" }}
  controls={[
    { type: "number", id: "length", label: "Max words", min: 50, max: 1000, step: 50 },
    {
      type: "select",
      id: "tone",
      label: "Tone",
      options: [
        { value: "neutral", label: "Neutral" },
        { value: "formal", label: "Formal" },
      ],
    },
  ]}
  onValuesChange={(values) => reply.regenerate(values)}
/>
```

`onValuesChange` runs on every change, including each step of a scrub. Debounce it if your handler starts a request.

## Examples [#examples]

### Numbers only [#numbers-only]

Two number fields. Drag a label to scrub.

```tsx
import { FineTuneCard } from "@/components/ai/fine-tune-card"

export function FineTuneCardNumbersExample() {
  return (
    <div className="w-full max-w-xs min-w-0">
      <FineTuneCard
        title="Chart"
        defaultValues={{ width: 480, height: 240 }}
        controls={[
          { type: "number", id: "width", label: "Width", min: 120, max: 960, step: 8 },
          { type: "number", id: "height", label: "Height", min: 80, max: 640, step: 8 },
        ]}
      />
    </div>
  )
}

```

## API reference [#api-reference]

### FineTuneCard [#finetunecard]

Accepts native div props beyond those listed.

| Prop             | Type                               | Default  | Description                                |
| ---------------- | ---------------------------------- | -------- | ------------------------------------------ |
| `title`          | string                             | Required | What is being adjusted.                    |
| `controls`       | `FineTuneControl[]`                | Required | The controls, in order.                    |
| `defaultValues`  | `FineTuneValues`                   | Required | The starting value for every control `id`. |
| `onValuesChange` | `(values: FineTuneValues) => void` | None     | Runs with every value when one changes.    |
| `labels`         | `Partial<FineTuneCardLabels>`      | English  | The Edited note and the Reset name.        |

### Types [#types]

| Export            | Shape                                                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `FineTuneControl` | `type` of `"number"` (with optional `min`, `max`, `step`), `"segment"`, or `"select"` (each with `options`), plus `id` and `label` |
| `FineTuneValues`  | A number or string for each control `id`                                                                                           |
