# Text Processing Panel

Text Processing Panel [#text-processing-panel]

The **Text Processing Panel** is a two-column (stacked on mobile) single-input → single-output NLP shell. The left column is a labeled textarea with a live word count and an optional [`CharLimitIndicator`](/docs/input-controls/char-limit-indicator); the right column is a result pane (spinner while processing, pre-wrap text otherwise, or an empty slot) with a copy-with-feedback button. A run / cancel / clear toolbar sits below, and an optional `header` slot renders above both columns — drop a [`SegmentedModePicker`](/docs/input-controls/segmented-mode-picker) or [`LanguagePairSelector`](/docs/input-controls/language-pair-selector) there.

It is layout-only. Own the inference in your app by wiring the toolbar to any text-in/text-out hook — `useSummarize`, `useTranslate`, `useFillMask`, `useAnswerQuestion`, or `useGenerateText` — and passing `result` / `isProcessing` back in.

**When to use it:** the paste → process → copy layout that recurs across summarization, translation, QA-context, and generation utilities.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';
import { useSummarize } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import { TextProcessingPanel } from '@/components/text-processing-panel';
import { SegmentedModePicker } from '@/registry/localmode/input-controls/segmented-mode-picker/segmented-mode-picker';

const LENGTHS = {
  short: { maxLength: 60 },
  medium: { maxLength: 130 },
  long: { maxLength: 250 },
} as const;

const SAMPLE =
  'LocalMode is a privacy-first toolkit for running machine-learning models entirely in the browser. ' +
  'Everything from embeddings and vector search to LLM chat and image processing works offline after the ' +
  'initial model download. No servers, no API keys, and your data never leaves the device.';

/**
 * Demo for TextProcessingPanel, used by the docs live preview. The shell drives
 * a real `useSummarize` flow; a SegmentedModePicker header slot selects the
 * summary length. The model downloads on the first run (Run-gated), then
 * spinner → result → copy works end to end, plus cancel and clear.
 */
export default function TextProcessingPanelDemo() {
  const [text, setText] = useState(SAMPLE);
  const [length, setLength] = useState<keyof typeof LENGTHS>('medium');

  const { data, isLoading, error, execute, cancel, reset } = useSummarize({
    model: transformers.summarizer('Xenova/distilbart-cnn-6-6'),
  });

  return (
    <TextProcessingPanel
      value={text}
      onChange={setText}
      result={data?.summary}
      isProcessing={isLoading}
      error={error?.message}
      onRun={() => execute({ text, maxLength: LENGTHS[length].maxLength })}
      onCancel={cancel}
      onClear={reset}
      inputLabel="Article"
      resultLabel="Summary"
      runLabel="Summarize"
      header={
        <SegmentedModePicker
          aria-label="Summary length"
          items={[
            { id: 'short', label: 'Short' },
            { id: 'medium', label: 'Medium' },
            { id: 'long', label: 'Long' },
          ]}
          selectedId={length}
          onSelect={setLength}
        />
      }
    />
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/input-controls/text-processing-panel
```

Dependencies [#dependencies]

* **Data source:** layout-only — renders the `result` / `isProcessing` you pass and emits `onRun` / `onCancel` / `onClear` callbacks, so it works with any text-in/text-out backend. Recommended LocalMode producers: `useSummarize` / `useTranslate` / `useFillMask` / `useAnswerQuestion` / `useGenerateText` (optional).
* `CharLimitIndicator` — composed for the optional limit ring (installed automatically as a registry dependency)
* `lucide-react` — icons

Files installed [#files-installed]

* `text-processing-panel.tsx` — the component
* `char-limit-indicator.tsx` — composed for the optional limit ring (registry dependency)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**TextProcessingPanel**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | **Required.** Input text (controlled). |
| `onChange` | `function` | — | **Required.** Fired with the new input text on every edit. |
| `result` | `string` | — | The processed result text (rendered pre-wrap in the result pane). |
| `isProcessing` | `boolean` | — | Whether a run is in progress (shows a spinner in the result pane). |
| `error` | `string` | — | Error message to surface under the result pane. |
| `onRun` | `function` | — | **Required.** Fired when the user activates Run. |
| `onCancel` | `function` | — | Fired when the user activates Cancel during processing. |
| `onClear` | `function` | — | Fired when the user activates Clear (reset input + result). |
| `inputLabel` | `string` | — | Label above the input column. |
| `resultLabel` | `string` | — | Label above the result column. |
| `placeholder` | `string` | — | Placeholder for the input textarea. |
| `maxLength` | `number` | — | When set, shows a {@link CharLimitIndicator} under the input. |
| `runLabel` | `string` | — | Run button text. |
| `header` | `ReactNode` | — | Optional header slot rendered above the two columns (tab bar, mode picker, language selector). |
| `emptyState` | `ReactNode` | — | Content shown in the result pane when there is no result yet. |

Examples [#examples]

Summarize [#summarize]

```tsx
import { useState } from 'react';
import { useSummarize } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import { TextProcessingPanel } from '@/components/text-processing-panel';

export function Summarizer() {
  const [text, setText] = useState('');
  const { data, isLoading, execute, cancel, reset } = useSummarize({
    model: transformers.summarizer('Xenova/distilbart-cnn-6-6'),
  });

  return (
    <TextProcessingPanel
      value={text}
      onChange={setText}
      result={data?.summary}
      isProcessing={isLoading}
      onRun={() => execute({ text })}
      onCancel={cancel}
      onClear={reset}
      inputLabel="Article"
      resultLabel="Summary"
      runLabel="Summarize"
    />
  );
}
```

Translate (header slot) [#translate-header-slot]

```tsx
const { data, isLoading, execute } = useTranslate({ model });
<TextProcessingPanel
  value={text}
  onChange={setText}
  result={data?.translation}
  isProcessing={isLoading}
  onRun={() => execute({ text, sourceLanguage: src, targetLanguage: tgt })}
  header={<LanguagePairSelector {...pairProps} />}
/>
```

Customization [#customization]

Pass `maxLength` to show the `CharLimitIndicator` under the input. The result pane renders `result` pre-wrap; for richer output (lists, predictions) drop your own node into `emptyState` or format `result` before passing it in. Keep the inference in your app — the panel stays a layout shell with slots. Everything is theme-driven via shadcn/ui CSS variables.

Accessibility [#accessibility]

The input textarea has no rendered `<label>` element, so it takes its &#x2A;*accessible name from `inputLabel`** — the same string shown above the column is also wired as the textarea's `aria-label`. Give each panel a descriptive `inputLabel` ("Text to summarize", "Source text") and `getByLabel(inputLabel)` resolves the field uniquely across panels on the same page. The Copy button on the result pane is labelled `"Copy result"`.