Skip to main content
AI operations can take a few seconds to complete. Rather than leaving users staring at a spinner, streaming lets you show real-time progress as the Chart Maker Agent works through your request.

Why streaming matters

When a user asks the agent to “convert this to a stacked bar chart with custom colors”, there’s processing happening behind the scenes. With streaming, you can:
  • Show a progress bar that fills as the operation progresses
  • Display status messages like “Analyzing chart structure…” or “Applying color theme…”
  • Let users cancel mid-operation if they change their mind
  • Create a more responsive experience that feels faster, even when it isn’t

Getting started with streaming

Instead of generateGraph(), use generateGraphStream(). It returns an async iterator that yields events as they arrive:
import { GraphyAiSdk } from '@graphysdk/ai';

const ai = new GraphyAiSdk({
  apiKey: <GRAPHY_API_KEY>,
  baseUrl: 'https://api.graphy.app',
});

const stream = await ai.generateGraphStream({
  config: myConfig,
  userPrompt: 'Add a trend line',
});

for await (const event of stream) {
  if (event.type === 'progress') {
    console.log(`${event.percentage}% - ${event.message}`);
  }

  if (event.type === 'complete') {
    setConfig(event.data.config);
  }

  if (event.type === 'error') {
    console.error('Generation failed:', event.error);
  }
}
The SDK emits events with a type property (‘progress’, ‘complete’, or ‘error’) that you can use to handle each case.

Understanding events

The stream emits three types of events. You’ll typically receive several progress events, followed by either a complete or error event.

Progress events

These arrive periodically during processing. Use them to update your UI:
interface ProgressEvent {
  type: 'progress';
  percentage: number;      // 0-100
  message?: string;        // "Analyzing data structure..."
  metadata?: Record<string, unknown>;
}
The percentage gives you a number for progress bars. The message provides human-readable status text you can display directly to users.

Complete events

When the operation succeeds, you’ll receive exactly one complete event with the result:
interface CompleteEvent<ResponseType> {
  type: 'complete';
  data: ResponseType;
}

Error events

If something goes wrong during processing, you’ll receive an error event:
interface ErrorEvent {
  type: 'error';
  error: string;
  code?: string;
  retryable?: boolean;  // true for transient failures
}
Check retryable to decide whether to offer a “Try again” option to users.

Letting users cancel

Long-running operations should be cancellable. Pass an AbortSignal to let users bail out:
const controller = new AbortController();

const stream = await ai.generateGraphStream(
  { config, userPrompt: 'Create a complex heatmap' },
  controller.signal
);

// User clicks cancel
cancelButton.onclick = () => controller.abort();

try {
  for await (const event of stream) {
    // Handle events...
  }
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('User cancelled the operation');
  }
}

React pattern

In React, store the controller in a ref so you can cancel from anywhere. Don’t forget to clean up when the component unmounts:
import { useState, useRef, useEffect } from 'react';
import { GraphyAiSdk } from '@graphysdk/ai';

function ChartEditor({ config, onUpdate }) {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  const handleGenerate = async (prompt: string) => {
    // Cancel any in-flight request
    abortRef.current?.abort();
    abortRef.current = new AbortController();

    setIsLoading(true);
    setProgress(0);

    try {
      const stream = await ai.generateGraphStream(
        { config, userPrompt: prompt },
        abortRef.current.signal
      );

      for await (const event of stream) {
        if (event.type === 'progress') {
          setProgress(event.percentage);
          setStatus(event.message || '');
        }

        if (event.type === 'complete') {
          onUpdate(event.data.config);
        }
      }
    } catch (error) {
      // Ignore abort errors - user cancelled intentionally
      if (error.name !== 'AbortError') {
        console.error('Generation failed:', error);
      }
    } finally {
      setIsLoading(false);
    }
  };

  // Clean up on unmount
  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  return (
    <div>
      {isLoading && (
        <>
          <progress value={progress} max={100} />
          <span>{status}</span>
          <button onClick={() => abortRef.current?.abort()}>
            Cancel
          </button>
        </>
      )}
    </div>
  );
}

The simpler alternative

If you just need basic progress updates without the full streaming API, use the onProgress callback with the regular generateGraph() method:
const result = await ai.generateGraph(
  { config, userPrompt: 'Add a trend line' },
  (progress) => {
    console.log(`${progress.percentage}% - ${progress.message}`);
  }
);

// Result returned when complete
console.log(result.config);
This is simpler but gives you less control. Here’s how they compare:
StreamingCallback
Progress updatesYesYes
Typed error eventsYesNo (throws instead)
Fine-grained controlYesLimited
Code complexityMoreLess
Use streaming when you need the extra control. Use callbacks when you just want progress without the overhead.

Handling errors gracefully

Errors can happen at two levels: as events within the stream, or as exceptions when the connection fails.
try {
  const stream = await ai.generateGraphStream({ config, userPrompt });

  for await (const event of stream) {
    if (event.type === 'error') {
      // Error from the API - check if we can retry
      if (event.retryable) {
        showRetryButton();
      } else {
        showError(event.error);
      }
      return;
    }

    // Handle progress and complete events...
  }
} catch (error) {
  // Network failure, timeout, or abort
  if (error.name === 'AbortError') {
    // User cancelled - nothing to do
    return;
  }
  showError('Connection failed. Please try again.');
}
The distinction matters for UX: API errors often have helpful messages to show users, while network errors usually just need a generic “try again” prompt.