Skip to content

Instantly share code, notes, and snippets.

@azizpunjani
Last active January 30, 2026 16:00
Show Gist options
  • Select an option

  • Save azizpunjani/5f3b484d6a4cc2bf708490c6172513a8 to your computer and use it in GitHub Desktop.

Select an option

Save azizpunjani/5f3b484d6a4cc2bf708490c6172513a8 to your computer and use it in GitHub Desktop.
AI Text Transform - Technical Design

AI Text Transform - Technical Design

Overview

A set of reusable AI text transformation tools that any team at Highspot can integrate into their feature.


Goals

  1. Reusable - Any team can integrate AI text transformation
  2. Simple - Dead simple API, minimal learning curve
  3. Flexible - Customize when needed, sensible defaults when not
  4. Consistent - Same UX patterns across products

Architecture

The Two Pieces

useAITextTransform (Hook)

For teams that want full control. Build your own UI, use our AI logic.

Returns:

  • transform(text, { preset: 'shorten' }) - Transform with a preset
  • transform(text, { prompt: 'Custom...' }) - Transform with custom prompt
  • result - The AI-generated text
  • busy - Loading state
  • errors - Error messages
  • cancel - Cancel in-flight request

AIRefinePanel (Component)

For teams that want quick integration. Full UI out of the box, customizable when needed.

Includes:

  • Preset buttons (Shorten, Elaborate, Make it professional) - customizable
  • Result display with label
  • Action buttons (Regenerate, Replace, Add below) - customizable

UI Designs

Inline / Popover Version

When text is selected in an editor, a popover appears with refinement options:

Inline/Popover UI

Modal / Dialog Version

Same content, different container.

Modal/Dialog UI


API Reference

useAITextTransform Hook

const {
  transform,     // (text: string, options: TransformOptions) => void
  result,        // string | null - the AI-generated text
  busy,          // boolean - loading state
  errors,        // string[] - error messages
  cancel,        // () => void - cancel in-flight request
} = useAITextTransform();

Transform Options

transform(text, { preset: 'shorten' });
transform(text, { preset: 'elaborate' });
transform(text, { preset: 'professional' });

Built-in Presets

Preset Description
shorten Condense text while preserving meaning
elaborate Expand with more detail and context
professional Rewrite in business-appropriate tone

Custom Presets

Teams can define their own presets by providing a prompt in the preset definition:

const customPresets = [
  // Built-in presets (just use the key)
  { key: 'shorten', label: 'Shorten' },
  { key: 'elaborate', label: 'Elaborate' },

  // Custom presets (provide a prompt)
  { key: 'simplify', label: 'Simplify', prompt: 'Rewrite this for a 5th grader:' },
  { key: 'spanish', label: 'Spanish', prompt: 'Translate to Spanish:' },
];

<AIRefinePanel presets={customPresets} ... />

Note: For fully custom AI interactions beyond text refinement, use useGeneralAIRequest directly.


AIRefinePanel Component

interface AIRefinePanelProps {
  // Required
  text: string;                    // The text to transform
  onReplace: (text: string) => void;  // Called when user clicks Replace
  onInsert: (text: string) => void;   // Called when user clicks Add below

  // Optional
  onCancel?: () => void;           // Called when user cancels
  presets?: Preset[];              // Custom presets (has defaults)
  children?: RenderProp;           // Custom action buttons
}

Usage Examples

Example 1: Basic Usage (Defaults)

Most common case - use all defaults:

import { AIRefinePanel } from '@highspot/ai-refine';

const MyEditor = () => {
  const [selectedText, setSelectedText] = useState('');

  return (
    <Popover>
      <AIRefinePanel
        text={selectedText}
        onReplace={(newText) => editor.replaceSelection(newText)}
        onInsert={(newText) => editor.insertBelow(newText)}
      />
    </Popover>
  );
};

What you get:

  • Shorten, Elaborate, Make it professional buttons
  • Loading state
  • Error handling
  • Regenerate, Replace, Add below actions

Example 2: In a Modal

Wrap in a modal, add original text display:

import { AIRefinePanel } from '@highspot/ai-refine';
import { Modal } from '@highspot/polar';

const MyModal = ({ isOpen, selectedText, onClose }) => {
  return (
    <Modal open={isOpen} onClose={onClose}>
      <Modal.Header>Refine Text</Modal.Header>

      <Modal.Body>
        {/* Show original - this is YOUR responsibility */}
        <div className="original">
          <label>Original</label>
          <p>{selectedText}</p>
        </div>

        {/* The panel */}
        <AIRefinePanel
          text={selectedText}
          onReplace={(newText) => {
            replaceText(newText);
            onClose();
          }}
          onInsert={(newText) => {
            insertText(newText);
            onClose();
          }}
        />
      </Modal.Body>
    </Modal>
  );
};

Example 3: Custom Presets

Add your own transformation options:

<AIRefinePanel
  text={selectedText}
  presets={[
    { key: 'shorten', label: 'Shorten' },
    { key: 'elaborate', label: 'Elaborate' },
    { key: 'simplify', label: 'Simplify', prompt: 'Rewrite for a 5th grader:' },
    { key: 'translate', label: 'Spanish', prompt: 'Translate to Spanish:' },
  ]}
  onReplace={...}
  onInsert={...}
/>

Example 4: Custom Action Buttons

Override the default buttons:

<AIRefinePanel text={selectedText}>
  {({ result, regenerate }) => (
    <div className="my-actions">
      <Button onClick={regenerate}>Try again</Button>
      <Button onClick={() => copyToClipboard(result)}>Copy</Button>
      <Button variant="primary" onClick={() => insertText(result)}>
        Insert
      </Button>
    </div>
  )}
</AIRefinePanel>

Example 5: Full Control with Hook Only

Build completely custom UI:

import { useAITextTransform } from '@highspot/ai-refine';

const MyCustomUI = ({ selectedText }) => {
  const { transform, result, busy, errors } = useAITextTransform();

  return (
    <div className="my-custom-design">
      <h3>AI Magic</h3>

      <div className="buttons">
        <MyFancyButton
          onClick={() => transform(selectedText, { preset: 'shorten' })}
          loading={busy}
        >
          Make it shorter
        </MyFancyButton>

        <MyFancyButton
          onClick={() => transform(selectedText, { prompt: 'Add emojis:' })}
          loading={busy}
        >
          Add emojis
        </MyFancyButton>
      </div>

      {errors.length > 0 && <MyErrorBanner errors={errors} />}

      {result && (
        <MyResultCard>
          <p>{result}</p>
          <MyFancyButton onClick={() => applyResult(result)}>
            Use this
          </MyFancyButton>
        </MyResultCard>
      )}
    </div>
  );
};

Integration Guide

For SmartPages (Lexical Editor)

Create a Lexical plugin that wraps AIRefinePanel:

// AIRefinePlugin.tsx
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useFloating } from '@floating-ui/react';
import { AIRefinePanel } from '@highspot/ai-refine';

export const AIRefinePlugin = () => {
  const [editor] = useLexicalComposerContext();
  const [selectedText, setSelectedText] = useState('');
  const [isVisible, setIsVisible] = useState(false);

  // Track selection...
  // Position popover...

  if (!isVisible) return null;

  return createPortal(
    <div ref={refs.setFloating} style={floatingStyles}>
      <AIRefinePanel
        text={selectedText}
        onReplace={(newText) => {
          editor.update(() => {
            const selection = $getSelection();
            selection.insertText(newText);
          });
          setIsVisible(false);
        }}
        onInsert={(newText) => {
          editor.update(() => {
            // Insert below logic
          });
          setIsVisible(false);
        }}
        onCancel={() => setIsVisible(false)}
      />
    </div>,
    document.body
  );
};

Technical Details

How It Works Under the Hood

Flow Diagram

API Endpoint

Uses the existing AI services infrastructure:

  • Endpoint: /api/v1/ai/llm/general
  • Method: POST
  • Auth: User session (domain_id, user_id)

No Streaming (v1)

The API returns the full response at once (not streamed). This keeps things simple:

  • Show loading spinner
  • Response arrives
  • Show result

Streaming can be added later if needed.


Decision Log

Decision Rationale
2 exports only (hook + panel) Simplicity. Teams use panel for quick integration, hook for full control.
Presets as prop, not hardcoded Teams can customize without forking.
Children render prop for actions Default buttons work for most, custom when needed.
No "Original" in panel Modal vs inline have different needs. Consumer handles this.
Non-streaming v1 Simpler. Responses are short anyway.
Frontend prompts (not registered) Faster iteration, no backend dependency for v1.

File Structure

features/shared/components/ai/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

JIRA Breakdown

Ticket Description Team
1 Create useAITextTransform hook Polar UI
2 Create AIRefinePanel component Polar UI
3 Add Storybook stories Polar UI
4 Create Lexical AIRefinePlugin SmartPages
5 Feature flag setup SmartPages
6 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?

Appendix: Full Code

useAITextTransform.ts

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

const PRESETS: Record<string, string> = {
  shorten: 'Shorten this text while preserving meaning. Return only the result:',
  elaborate: 'Elaborate with more detail. Return only the result:',
  professional: 'Rewrite in a professional tone. Return only the result:',
};

interface TransformOptions {
  preset?: string;
  prompt?: 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 promptText = options.prompt ?? PRESETS[options.preset!];

      if (!promptText) {
        setErrors(['Provide preset or custom prompt']);
        return;
      }

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

      onSubmit({
        domainId: currentUser.get('domain_id'),
        userId: currentUser.get('id'),
        requestProps: { prompt_text: `${promptText}\n\n${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,
  };
};

AIRefinePanel.tsx

import { useState } from 'react';
import { useAITextTransform } from './useAITextTransform';
import styles from './AIRefinePanel.module.scss';

const DEFAULT_PRESETS = [
  { key: 'shorten', label: 'Shorten' },
  { key: 'elaborate', label: 'Elaborate' },
  { key: 'professional', label: 'Make it professional' },
];

const LABELS: Record<string, string> = {
  shorten: 'Shortened',
  elaborate: 'Elaborated',
  professional: 'Professional',
};

interface Preset {
  key: string;
  label: string;
  prompt?: string;
}

interface Props {
  text: string;
  presets?: Preset[];
  onReplace: (text: string) => void;
  onInsert: (text: string) => void;
  onCancel?: () => void;
  children?: (props: { result: string; regenerate: () => void }) => React.ReactNode;
}

export const AIRefinePanel = ({
  text,
  presets = DEFAULT_PRESETS,
  onReplace,
  onInsert,
  onCancel,
  children,
}: Props) => {
  const { transform, result, activePreset, busy, errors, reset } = useAITextTransform();
  const [lastPreset, setLastPreset] = useState<Preset | null>(null);

  const handleSelect = (preset: Preset) => {
    setLastPreset(preset);
    transform(text, preset.prompt ? { prompt: preset.prompt } : { preset: preset.key });
  };

  const handleRegenerate = () => {
    if (lastPreset) handleSelect(lastPreset);
  };

  return (
    <div className={styles.panel}>
      {/* Header */}
      <div className={styles.header}>
        <span className={styles.icon}></span>
        <span>Refine text</span>
      </div>

      {/* Preset Buttons */}
      <div className={styles.menu}>
        {presets.map((preset) => (
          <button
            key={preset.key}
            className={styles.presetButton}
            onClick={() => handleSelect(preset)}
            disabled={busy}
          >
            {preset.label}
          </button>
        ))}
      </div>

      {/* Loading */}
      {busy && <div className={styles.loading}>Processing...</div>}

      {/* Errors */}
      {errors.length > 0 && (
        <div className={styles.error}>{errors.join(', ')}</div>
      )}

      {/* Result */}
      {result && (
        <div className={styles.result}>
          <span className={styles.label}>
            {LABELS[activePreset!] || activePreset}
          </span>
          <p className={styles.text}>{result}</p>

          {/* Actions */}
          {children ? (
            children({ result, regenerate: handleRegenerate })
          ) : (
            <div className={styles.actions}>
              <button onClick={handleRegenerate}>Regenerate</button>
              <div className={styles.rightActions}>
                <button onClick={() => onReplace(result)}>Replace</button>
                <button
                  className={styles.primary}
                  onClick={() => onInsert(result)}
                >
                  Add below
                </button>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment