Skip to main content
This guide covers four patterns for wiring AI content into the SecurAtlas Next.js app. All examples use the Supabase client and the RPCs documented in the API Reference.

Pattern 1: Display Narrative (Server Component)

Use rpc_get_or_request_narrative to fetch the cached narrative. If none exists or the cache is stale, the RPC enqueues a generation job and returns freshness: 'none' or freshness: 'stale'.
// app/(app)/t/[tenantId]/(workspace)/dashboard/_components/gap-narrative.tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';

interface NarrativeResponse {
  narrative: string | null;
  generated_at: string | null;
  provider: string | null;
  fallback_used: boolean;
  freshness: 'none' | 'fresh' | 'stale';
  pending_regen: boolean;
}

export async function GapNarrative({ tenantId }: { tenantId: string }) {
  const supabase = getSupabaseServerClient();

  const { data } = await supabase.rpc('rpc_get_or_request_narrative', {
    p_tenant_id: tenantId,
    p_force_refresh: false,
  });

  const result = data as NarrativeResponse | null;

  if (!result?.narrative) {
    return (
      <div className="text-sm text-muted-foreground">
        {result?.pending_regen
          ? 'Generating narrative...'
          : 'No narrative available yet.'}
      </div>
    );
  }

  return (
    <div className="prose prose-sm dark:prose-invert">
      <p>{result.narrative}</p>
      {result.freshness === 'stale' && (
        <p className="text-xs text-amber-500">
          Updating in background...
        </p>
      )}
    </div>
  );
}

Pattern 2: Regenerate Button (Client Component)

Call the same RPC with force_refresh: true to enqueue a new generation job regardless of cache state.
'use client';

import { useState } from 'react';
import { RefreshCw } from 'lucide-react';
import { getSupabaseBrowserClient } from '@kit/supabase/browser-client';

export function RegenerateNarrativeButton({ tenantId }: { tenantId: string }) {
  const [loading, setLoading] = useState(false);

  async function handleRegenerate() {
    setLoading(true);
    const supabase = getSupabaseBrowserClient();

    await supabase.rpc('rpc_get_or_request_narrative', {
      p_tenant_id: tenantId,
      p_force_refresh: true,
    });

    setLoading(false);
    // The narrative will appear via real-time subscription (Pattern 3)
    // or on next page load
  }

  return (
    <button
      onClick={handleRegenerate}
      disabled={loading}
      className="inline-flex items-center gap-1.5 text-xs text-muted-foreground
                 hover:text-foreground transition-colors disabled:opacity-50"
    >
      <RefreshCw className={`w-3 h-3 ${loading ? 'animate-spin' : ''}`} />
      {loading ? 'Requesting...' : 'Regenerate'}
    </button>
  );
}

Pattern 3: Real-Time Updates

Subscribe to the ai_gap_narratives table via Supabase Realtime to update the UI when a new narrative is generated.
'use client';

import { useEffect, useState } from 'react';
import { getSupabaseBrowserClient } from '@kit/supabase/browser-client';

export function useNarrativeRealtime(tenantId: string) {
  const [narrative, setNarrative] = useState<string | null>(null);
  const [generatedAt, setGeneratedAt] = useState<string | null>(null);

  useEffect(() => {
    const supabase = getSupabaseBrowserClient();

    const channel = supabase
      .channel(`narrative:${tenantId}`)
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'ai_gap_narratives',
          filter: `tenant_id=eq.${tenantId}`,
        },
        (payload) => {
          const row = payload.new as any;
          if (row?.narrative) {
            setNarrative(row.narrative);
            setGeneratedAt(row.generated_at);
          }
        },
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [tenantId]);

  return { narrative, generatedAt };
}
Realtime subscriptions require that the table has Realtime enabled in the Supabase dashboard (Database > Replication). ai_gap_narratives is already enabled.

Pattern 4: AI Health Status Badge

Display the overall health of the AI content system using rpc_get_ai_health_summary.
import { getSupabaseServerClient } from '@kit/supabase/server-client';

interface HealthSummary {
  queue_depth: number;
  active_providers: number;
  total_providers: number;
  circuits_open: number;
  last_generation_at: string | null;
}

export async function AiStatusBadge() {
  const supabase = getSupabaseServerClient();
  const { data } = await supabase.rpc('rpc_get_ai_health_summary');
  const health = data as HealthSummary | null;

  if (!health) return null;

  const isHealthy = health.circuits_open === 0 && health.active_providers > 0;

  return (
    <div className="flex items-center gap-1.5 text-xs">
      <div
        className={`w-1.5 h-1.5 rounded-full ${
          isHealthy ? 'bg-emerald-500' : 'bg-amber-500'
        }`}
      />
      <span className="text-muted-foreground">
        AI: {health.active_providers}/{health.total_providers} providers
        {health.queue_depth > 0 && ` · ${health.queue_depth} queued`}
      </span>
    </div>
  );
}