Skip to content

Instantly share code, notes, and snippets.

@azizpunjani
Created February 5, 2026 16:08
Show Gist options
  • Select an option

  • Save azizpunjani/28f6ae5ee6f2c4512e03bb893bfa5811 to your computer and use it in GitHub Desktop.

Select an option

Save azizpunjani/28f6ae5ee6f2c4512e03bb893bfa5811 to your computer and use it in GitHub Desktop.
AI Text Transform - Implementation Analysis (streaming, patterns, recommendations) - Updated

AI Text Transform - Implementation Analysis

Overview

Analysis of implementing the AI Text Transform feature for Highspot, including streaming capabilities.

Based on the original technical design gist.


Key Components

1. useAITextTransform (Hook)

For teams wanting full control over UI.

Returns:

  • reset - Reset state
  • cancel - Cancel in-flight request
  • errors - Error messages
  • busy - Loading state
  • result - The AI-generated text
  • transform(text, options) - Transform with preset or custom promptKey

Built-in Presets:

Preset Prompt Key
shorten ai_refine_shorten
elaborate ai_refine_elaborate
professional ai_refine_professional

2. AIRefinePanel (Component)

Full UI out of the box with:

  • Error handling & loading state
  • Result display with label
  • Action buttons (Regenerate, Replace, Add below)
  • Preset buttons (Shorten, Elaborate, Make it professional)

Streaming Support

Streaming is exposed as an explicit option, not a hidden feature flag:

const { transform, result, busy } = useAITextTransform({ streaming: true });

Rationale:

  • Components are designed for either progressive updates or single updates
  • Consumers should be aware of re-render implications (~50-200 updates vs 2)
  • Explicit opt-in is more predictable than silent behavior changes

Internal Implementation:

  • Single hook with unified API
  • Two separate code paths (transformStreaming / transformNonStreaming)
  • Shared state management (result, busy, errors, cancel, reset)

Consumer Experience:

Non-Streaming Streaming
result updates Once (null → full text) Incrementally (null → "The" → "The quick" → ...)
Re-renders ~2 ~50-200
Perceived speed Waits, then shows all Text appears immediately

Backend Status: ✅ Already Supported

The /api/v1/ai/llm/general endpoint already supports streaming via query parameters:

POST /api/v1/ai/llm/general?streaming=true&stream_format=sse

Recommended Implementation Approach

Phase 1: Ship Without Streaming (v1)

Use existing useGeneralAIRequest hook:

import { useCallback, useRef, useState } from 'react';
import { getCurrentUser } from '~/helpers/CurrentUser';
import useGeneralAIRequest from '~/features/shared/components/ai/RoutesAIServices/hooks/useGeneralAIRequest';

const PRESETS: Record<string, string> = {
  shorten: 'ai_refine_shorten',
  elaborate: 'ai_refine_elaborate',
  professional: 'ai_refine_professional',
};

type TransformOptions = {
  preset?: keyof typeof PRESETS;
  promptKey?: string;
};

export const useAITextTransform = () => {
  const currentUser = getCurrentUser();
  const [result, setResult] = useState<string | null>(null);
  const [activePreset, setActivePreset] = useState<string | null>(null);

  const { onSubmit, busy, errors, onCancel, setErrors } = useGeneralAIRequest({
    onSuccess: (response) => {
      setResult(response?.choices?.[0]?.text ?? null);
    },
  });

  const transform = useCallback(
    (text: string, options: TransformOptions) => {
      const promptKey = options.promptKey ?? PRESETS[options.preset!];
      if (!promptKey) {
        setErrors(['Invalid preset or missing promptKey']);
        return;
      }

      setResult(null);
      setActivePreset(options.preset ?? 'custom');

      onSubmit({
        domainId: currentUser.get('domain_id'),
        userId: currentUser.get('id'),
        requestProps: { prompt_key: promptKey, text },
        args: { temperature: 0.3, enable_event_logging: true },
      });
    },
    [currentUser, onSubmit, setErrors]
  );

  const reset = useCallback(() => {
    setResult(null);
    setActivePreset(null);
    setErrors([]);
  }, [setErrors]);

  return { transform, result, activePreset, busy, errors, cancel: onCancel, reset };
};

Phase 2: Add Streaming Support

Same hook API, with streaming as an explicit option:

import { useCallback, useRef, useState } from 'react';
import { getCurrentUser } from '~/helpers/CurrentUser';
import useGeneralAIRequest from '~/features/shared/components/ai/RoutesAIServices/hooks/useGeneralAIRequest';
import RoutesAIServices from '~/features/shared/components/ai/RoutesAIServices';

const PRESETS: Record<string, string> = {
  shorten: 'ai_refine_shorten',
  elaborate: 'ai_refine_elaborate',
  professional: 'ai_refine_professional',
};

type TransformOptions = {
  preset?: keyof typeof PRESETS;
  promptKey?: string;
};

type UseAITextTransformOptions = {
  streaming?: boolean;
};

export const useAITextTransform = ({ streaming = false }: UseAITextTransformOptions = {}) => {
  const currentUser = getCurrentUser();
  const [result, setResult] = useState<string | null>(null);
  const [activePreset, setActivePreset] = useState<string | null>(null);
  const [streamingBusy, setStreamingBusy] = useState(false);
  const [streamingErrors, setStreamingErrors] = useState<string[]>([]);
  const abortRef = useRef<AbortController | null>(null);

  const { onSubmit, busy: nonStreamingBusy, errors: nonStreamingErrors, onCancel, setErrors } = useGeneralAIRequest({
    onSuccess: (response) => {
      setResult(response?.choices?.[0]?.text ?? null);
    },
  });

  const transformNonStreaming = useCallback((text: string, promptKey: string) => {
    setResult(null);
    onSubmit({
      domainId: currentUser.get('domain_id'),
      userId: currentUser.get('id'),
      requestProps: { prompt_key: promptKey, text },
      args: { temperature: 0.3, enable_event_logging: true },
    });
  }, [currentUser, onSubmit]);

  const transformStreaming = useCallback(async (text: string, promptKey: string) => {
    setResult('');
    setStreamingBusy(true);
    setStreamingErrors([]);
    abortRef.current = new AbortController();

    const url = `${RoutesAIServices.general()}?streaming=true&stream_format=sse`;

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'text/event-stream',
          'HS-CSRF': window.hs_csrf || '',
        },
        credentials: 'include',
        body: JSON.stringify({
          domain_id: currentUser.get('domain_id'),
          user_id: currentUser.get('id'),
          prompt_key: promptKey,
          text,
          args: { temperature: 0.3, enable_event_logging: true },
        }),
        signal: abortRef.current.signal,
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const events = buffer.split('\r\n\r\n');
        buffer = events.pop() || '';

        for (const event of events) {
          if (!event.trim()) continue;

          let eventName: string | undefined;
          let eventData: string | undefined;

          for (const line of event.split('\r\n')) {
            if (line.startsWith('event: ')) eventName = line.slice(7);
            if (line.startsWith('data: ')) eventData = line.slice(6);
          }

          if (eventData) {
            const parsed = JSON.parse(eventData);

            if (eventName === 'token' && parsed.content) {
              setResult(prev => (prev ?? '') + parsed.content);
            } else if (eventName === 'stream_error') {
              setStreamingErrors([parsed.error || 'Stream error']);
            }
          }
        }
      }
    } catch (err: any) {
      if (err.name !== 'AbortError') {
        setStreamingErrors([err.message || 'Unknown error']);
      }
    } finally {
      setStreamingBusy(false);
    }
  }, [currentUser]);

  const transform = useCallback((text: string, options: TransformOptions) => {
    const promptKey = options.promptKey ?? PRESETS[options.preset!];
    if (!promptKey) {
      setErrors(['Invalid preset or missing promptKey']);
      return;
    }

    setActivePreset(options.preset ?? 'custom');

    if (streaming) {
      transformStreaming(text, promptKey);
    } else {
      transformNonStreaming(text, promptKey);
    }
  }, [streaming, transformStreaming, transformNonStreaming, setErrors]);

  const cancel = useCallback(() => {
    if (streaming) {
      abortRef.current?.abort();
      setStreamingBusy(false);
    } else {
      onCancel();
    }
  }, [streaming, onCancel]);

  const reset = useCallback(() => {
    setResult(null);
    setActivePreset(null);
    setStreamingErrors([]);
    setErrors([]);
  }, [setErrors]);

  return {
    transform,
    result,
    activePreset,
    busy: streaming ? streamingBusy : nonStreamingBusy,
    errors: streaming ? streamingErrors : nonStreamingErrors,
    cancel,
    reset,
  };
};

File Structure

packages/polar-ui/src/AIRefine/
├── index.ts                     # Exports
├── useAITextTransform.ts        # The hook
├── AIRefinePanel.tsx            # The component
├── AIRefinePanel.module.scss    # Styles
├── types.ts                     # TypeScript types
├── __tests__/
│   ├── useAITextTransform.test.ts
│   └── AIRefinePanel.test.tsx
└── stories/
    └── AIRefinePanel.stories.tsx

Key Files Referenced

Backend (ai-services)

  • py/shared/shared/handlers/streaming_callback_handler.py - SSE event formatting
  • py/llmproxy/llmproxy/api/endpoints/general.py - Streaming endpoint implementation

Frontend (web/client)

  • features/training/trainingShared/modals/RolePlayScenarioModal/hooks/useGenerateScenario.ts - Example general API usage
  • features/copilot/utility/streaming/streamCopilotAnswer.ts - AG-UI streaming example
  • features/copilot/components/CopilotCenter/agent-platform/chatui/ChatUIStreamClient.ts - SSE streaming patterns
  • features/shared/components/ai/RoutesAIServices/hooks/useGeneralAIRequest.js - Existing non-streaming hook

Effort Estimate

Task Effort
Backend changes None needed - streaming already supported
Frontend hook (non-streaming) ~0.5 day
Frontend hook (streaming) ~1 day
AIRefinePanel component ~0.5 day
Testing ~0.5 day
Total ~2.5 days

JIRA Breakdown

Ticket Description Team
1 Register prompts in GrowthBook & deploy to SUs AI Services
2 Create useAITextTransform hook (non-streaming) Polar UI
3 Add streaming support to hook Polar UI
4 Create AIRefinePanel component Polar UI
5 Add Storybook stories Polar UI
6 Create Lexical AIRefinePlugin SmartPages
7 Feature flag setup SmartPages
8 E2E tests SmartPages

Open Questions

  1. Should we add more default presets? (summarize, translate, etc.)
  2. Do we need analytics/tracking built in?
  3. Should the panel support a "compact" mode for smaller spaces?

Generated from conversation analysis on Feb 5, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment