> ## 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.

# Research Synthesizer

> A multi-step research workflow with parallel analysis streams and human-in-the-loop approval

# Research Synthesizer

A research tool with a multi-stage workflow: collect sources, analyze each in parallel, synthesize findings, and wait for human approval before finalizing. The number of analysis streams is determined at runtime based on how many sources the user adds.

## Server Code

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

interface Source {
  id: string;
  content: string;
  analysis: StreamableValue<string>;
}

export default class ResearchSynthesizer extends AgenticSystem {
  @field sources: Source[] = [];
  @field synthesis = stream<string>('');
  @field stage: 'collecting' | 'analyzing' | 'synthesizing' | 'reviewing' | 'complete' = 'collecting';
  @field approval: { pending: boolean; approved?: boolean } | null = null;

  @action()
  async addSource(content: string) {
    this.sources.push({
      id: crypto.randomUUID(),
      content,
      analysis: stream<string>(''),
    });
  }

  @action()
  async removeSource(id: string) {
    this.sources = this.sources.filter(s => s.id !== id);
  }

  @action()
  async analyze() {
    this.stage = 'analyzing';

    // Analyze all sources in parallel
    await Promise.all(
      this.sources.map(source => this.analyzeSource(source))
    );

    this.stage = 'synthesizing';
    await this.synthesize();
  }

  private async analyzeSource(source: Source) {
    for await (const chunk of ai.stream(
      `Analyze this source. Extract key claims and evidence:\n\n${source.content}`
    )) {
      source.analysis.append(chunk);
    }
    source.analysis.complete();
  }

  private async synthesize() {
    const analyses = this.sources
      .map((s, i) => `Source ${i + 1}:\n${s.analysis.current}`)
      .join('\n\n---\n\n');

    for await (const chunk of ai.stream(
      `Synthesize these analyses. Identify agreements, contradictions, and gaps:\n\n${analyses}`
    )) {
      this.synthesis.append(chunk);
    }
    this.synthesis.complete();

    this.stage = 'reviewing';
    this.approval = { pending: true };
  }

  @action()
  async approve(approved: boolean) {
    this.approval = { pending: false, approved };

    if (approved) {
      this.stage = 'complete';
    } else {
      this.stage = 'collecting';
      this.synthesis.reset();
      this.approval = null;
    }
  }
}
```

### Key Patterns

**Dynamic stream arrays:** Each source contains its own `analysis` stream. When you push a source to the array, the stream syncs to clients. During analysis, each stream updates in parallel.

**Human-in-the-loop:** After synthesis, set `approval = { pending: true }` and return. The Durable Object can hibernate. When the user responds, the `approve` action continues the workflow. This approval state persists across browser sessions.

## Client Code

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

export default function ResearchUI() {
  const { sources, synthesis, stage, approval, addSource, removeSource, analyze, approve } = useSystem<ResearchSynthesizer>();

  return (
    <div className="max-w-4xl mx-auto p-6">
      <StageIndicator stage={stage} />

      <SourceInput onAdd={addSource} disabled={stage !== 'collecting'} />

      <div className="space-y-4 my-6">
        {sources.map(source => (
          <SourceCard
            key={source.id}
            source={source}
            onRemove={() => removeSource(source.id)}
            showAnalysis={stage !== 'collecting'}
          />
        ))}
      </div>

      {stage === 'collecting' && sources.length > 0 && (
        <button onClick={analyze} className="w-full py-3 bg-blue-600 text-white rounded">
          Analyze {sources.length} Sources
        </button>
      )}

      {['synthesizing', 'reviewing', 'complete'].includes(stage) && (
        <SynthesisPanel content={synthesis.current} streaming={stage === 'synthesizing'} />
      )}

      {approval?.pending && (
        <ApprovalDialog
          onApprove={() => approve(true)}
          onReject={() => approve(false)}
        />
      )}

      {stage === 'complete' && (
        <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded">
          Research complete and approved.
        </div>
      )}
    </div>
  );
}

function StageIndicator({ stage }: { stage: string }) {
  const stages = ['collecting', 'analyzing', 'synthesizing', 'reviewing', 'complete'];
  const current = stages.indexOf(stage);

  return (
    <div className="flex gap-2 mb-6">
      {stages.map((s, i) => (
        <div key={s} className={`flex-1 h-2 rounded ${i <= current ? 'bg-blue-600' : 'bg-gray-200'}`} />
      ))}
    </div>
  );
}

function SourceCard({ source, onRemove, showAnalysis }) {
  return (
    <div className="border rounded p-4">
      <div className="flex justify-between mb-2">
        <span className="text-sm text-gray-500">Source</span>
        <button onClick={onRemove} className="text-red-500 text-sm">Remove</button>
      </div>
      <p className="text-sm mb-2 line-clamp-3">{source.content}</p>
      {showAnalysis && source.analysis.current && (
        <div className="mt-3 pt-3 border-t">
          <span className="text-sm text-gray-500">Analysis</span>
          <p className="text-sm mt-1 whitespace-pre-wrap">{source.analysis.current}</p>
        </div>
      )}
    </div>
  );
}

function ApprovalDialog({ onApprove, onReject }) {
  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white rounded-lg p-6 max-w-md">
        <h3 className="text-lg font-medium mb-2">Review Synthesis</h3>
        <p className="text-gray-600 mb-4">Does this synthesis accurately represent the sources?</p>
        <div className="flex gap-3">
          <button onClick={onApprove} className="flex-1 py-2 bg-green-600 text-white rounded">Approve</button>
          <button onClick={onReject} className="flex-1 py-2 bg-gray-200 rounded">Revise</button>
        </div>
      </div>
    </div>
  );
}
```

## Running the Example

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

Add 2-3 sources, then click "Analyze Sources." Watch each source's analysis stream in parallel. When synthesis finishes, the approval dialog appears. Approve to complete, or reject to return to collection.

Key behaviors to notice:

* **Dynamic streams:** Add 5 sources and 5 parallel streams appear
* **Workflow stages:** The stage field drives which UI renders
* **Persistent approval:** Close your browser, return later—the approval dialog is still waiting
* **Clean rejection loop:** Rejecting resets to collection stage for iteration
