Dike logoDocs

In this guide, we'll build a production-ready Arabic Legal AI Chatbot in React/TypeScript that streams grounded legal answers, renders interactive citation hovercards, and handles weak grounding gracefully.

Rendering diagram...

1. Requesting Streaming Reasoning

When the user asks a question, open an SSE connection to POST /v1/reason:

// services/dike-chat.ts
export interface Citation {
  marker: number;
  canonical_id: string;
  article_no: string;
  citation_string: string;
  snippet: string;
  verified: boolean;
}

export interface ChatStreamCallbacks {
  onToken: (token: string) => void;
  onCitations: (citations: Citation[], isWeak: boolean) => void;
  onError: (err: Error) => void;
}

export async function streamLegalAnswer(
  question: string,
  callbacks: ChatStreamCallbacks
) {
  try {
    const res = await fetch('https://api.dike.it.com/v1/reason', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_DIKE_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        question,
        filters: { jurisdiction: 'EG', status: ['in_force'] },
        stream: true,
        language: 'ar',
      }),
    });

    const isWeak = res.headers.get('X-Dike-Grounding') === 'weak';
    const reader = res.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 lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (line.startsWith('data: ') && !line.includes('[DONE]')) {
          const payload = JSON.parse(line.slice(6));
          if (payload.token) callbacks.onToken(payload.token);
        } else if (line.startsWith('event: citations')) {
          // Next line contains citations payload
        }
      }
    }
  } catch (err: any) {
    callbacks.onError(err);
  }
}

2. Rendering Interactive Citation Badges (React)

Replace plain [1] markers in the generated text with clickable interactive badges that display source snippets:

// components/GroundedMessage.tsx
import React, { useState } from 'react';
import type { Citation } from '../services/dike-chat';

export function GroundedMessage({
  text,
  citations,
  isWeakGrounding,
}: {
  text: string;
  citations: Citation[];
  isWeakGrounding?: boolean;
}) {
  const [activeCitation, setActiveCitation] = useState<Citation | null>(null);

  // Split text by citation markers like [1], [2]
  const parts = text.split(/(\[\d+\])/g);

  return (
    <div className="space-y-4 rounded-xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-950">
      {/* Weak Grounding Warning Banner */}
      {isWeakGrounding && (
        <div className="flex items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/40 dark:text-amber-300">
          <span className="font-semibold">تنبيه:</span>
          <span>لم يعثر النظام على سند قانوني كافٍ في المصادر المفهرسة لدعم هذه الإجابة بشكل كامل.</span>
        </div>
      )}

      {/* Message Text with Interactive Footnotes */}
      <div className="leading-relaxed text-slate-800 dark:text-slate-200" dir="rtl">
        {parts.map((part, index) => {
          const match = part.match(/^\[(\d+)\]$/);
          if (match) {
            const markerNum = parseInt(match[1], 10);
            const citation = citations.find((c) => c.marker === markerNum);

            if (!citation) return <span key={index}>{part}</span>;

            return (
              <button
                key={index}
                onClick={() => setActiveCitation(citation)}
                className="mx-1 inline-flex items-center rounded bg-amber-100 px-1.5 py-0.5 text-xs font-bold text-amber-900 transition hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-300"
                title={citation.citation_string}
              >
                [{markerNum}]
              </button>
            );
          }
          return <span key={index}>{part}</span>;
        })}
      </div>

      {/* Active Citation Modal / Card */}
      {activeCitation && (
        <div className="mt-4 rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900" dir="rtl">
          <div className="flex items-center justify-between">
            <span className="font-semibold text-slate-900 dark:text-white">
              {activeCitation.citation_string}
            </span>
            <span className="text-xs text-emerald-600 dark:text-emerald-400">✓ موثق ومطابق</span>
          </div>
          <p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
            "{activeCitation.snippet}"
          </p>
          <a
            href={`/docs/api-reference/document?id=${activeCitation.canonical_id}`}
            className="mt-2 inline-block text-xs text-amber-700 hover:underline dark:text-amber-400"
          >
            عرض النص الكامل للقانون &larr;
          </a>
        </div>
      )}
    </div>
  );
}

Next Steps

On this page