> ## Documentation Index
> Fetch the complete documentation index at: https://docs.idyllic.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Document Improver

> A document editor with real-time AI streaming

# Document Improver

A two-panel interface where users paste rough text on the left and watch a polished version stream into the right panel. Tokens appear as the model generates them—users see immediate progress rather than a loading spinner.

## Server Code

```typescript theme={null}
import { AgenticSystem, field, action, stream } from 'idyllic';

export default class DocImprover extends AgenticSystem {
  @field content = '';
  @field improved = stream<string>('');
  @field status: 'idle' | 'improving' = 'idle';

  @action()
  async setContent(text: string) {
    this.content = text;
  }

  @action()
  async improve() {
    this.status = 'improving';
    this.improved.reset();

    for await (const chunk of ai.stream(
      `Improve this text, making it clearer and more professional:\n\n${this.content}`
    )) {
      this.improved.append(chunk);
    }

    this.improved.complete();
    this.status = 'idle';
  }
}
```

The `content` field holds the input as a plain string. The `improved` field uses `stream<string>` because it accumulates progressively during generation—each `append()` broadcasts just that chunk, creating the token-by-token effect.

The `status` field drives loading states. While `'improving'`, the UI disables the button and shows a cursor.

## Client Code

```tsx theme={null}
import { useSystem } from '@idyllic/react';
import type { DocImprover } from '../systems/DocImprover';

export default function Editor() {
  const { content, improved, status, setContent, improve } = useSystem<DocImprover>();

  return (
    <div className="grid grid-cols-2 gap-4 p-4 h-screen">
      <div className="flex flex-col">
        <h2 className="text-lg font-medium mb-2">Original</h2>
        <textarea
          value={content}
          onChange={e => setContent(e.target.value)}
          className="flex-1 p-3 border rounded font-mono"
          placeholder="Paste your text here..."
        />
        <button
          onClick={improve}
          disabled={status === 'improving'}
          className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
        >
          {status === 'improving' ? 'Improving...' : 'Improve'}
        </button>
      </div>

      <div className="flex flex-col">
        <h2 className="text-lg font-medium mb-2">Improved</h2>
        <div className="flex-1 p-3 border rounded bg-gray-50 whitespace-pre-wrap">
          {improved.current}
          {status === 'improving' && <span className="animate-pulse">|</span>}
        </div>
      </div>
    </div>
  );
}
```

The `useSystem` hook returns reactive state that updates whenever the server broadcasts changes. The blinking cursor appears while streaming and disappears when complete.

## Running the Example

```bash theme={null}
npx idyllic dev
```

Open `http://localhost:3000`. Paste text, click Improve, watch it stream. Open a second tab—both display the same streaming output since they share state.

## Variations

### Revision History

Track previous improvements before starting each generation:

```typescript theme={null}
@field revisions: string[] = [];

@action()
async improve() {
  if (this.improved.current) {
    this.revisions.push(this.improved.current);
  }
  // ... rest of improve logic
}
```

### Multiple Styles

Parameterize the action for different tones:

```typescript theme={null}
@action()
async improve(style: 'professional' | 'casual' | 'concise') {
  const prompts = {
    professional: 'Make this more professional and formal',
    casual: 'Make this friendlier and more conversational',
    concise: 'Make this shorter without losing meaning',
  };

  for await (const chunk of ai.stream(`${prompts[style]}:\n\n${this.content}`)) {
    this.improved.append(chunk);
  }
  // ...
}
```

TypeScript ensures the client passes a valid style argument.
