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 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
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
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
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:
@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:
@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.