/**
* BillSectionSummaryButton Component
*
* AI-powered summary button for bill sections.
* Generates plain-language explanations and displays them in the chat widget.
* Summaries are cached globally to save API tokens.
*/
'use client';
import { useState, useCallback } from 'react';
import { Sparkles } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useChatStore } from '@/lib/stores/chatStore';
export interface BillSectionSummaryButtonProps {
billNumber: string;
session: string;
billTitle: string;
sectionRef: string; // e.g., "s2.1.a"
sectionLabel: string; // e.g., "2.1(a)"
sectionText: string; // Full text of the section
parentContext?: string; // Context from parent sections for smaller provisions
locale: string;
size?: 'sm' | 'xs';
className?: string;
}
/**
* Generate a SHA-256 hash of the section content (browser-compatible)
*/
async function generateContentHash(text: string): Promise<string> {
const normalized = text.trim().toLowerCase();
const encoder = new TextEncoder();
const data = encoder.encode(normalized);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
export function BillSectionSummaryButton({
billNumber,
session,
billTitle,
sectionRef,
sectionLabel,
sectionText,
parentContext,
locale,
size = 'sm',
className = '',
}: BillSectionSummaryButtonProps) {
const { user, loading: authLoading } = useAuth();
const [isLoading, setIsLoading] = useState(false);
// Chat store actions
const toggleOpen = useChatStore((state) => state.toggleOpen);
const isOpen = useChatStore((state) => state.isOpen);
const setContext = useChatStore((state) => state.setContext);
const addMessage = useChatStore((state) => state.addMessage);
const setChatLoading = useChatStore((state) => state.setLoading);
const iconSizes = {
sm: 16,
xs: 14,
};
const iconSize = iconSizes[size];
const handleClick = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!user || isLoading) return;
setIsLoading(true);
// Open chat immediately and show loading indicator
if (!isOpen) {
toggleOpen();
}
setChatLoading(true);
try {
// Generate content hash for cache validation
const contentHash = await generateContentHash(sectionText);
// Call API to get or generate summary
const response = await fetch(
`/api/bills/${session}/${billNumber}/sections/${encodeURIComponent(sectionRef)}/summary`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sectionText,
sectionLabel,
parentContext,
billTitle,
contentHash,
}),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to generate summary');
}
const data = await response.json();
// Set bill context for the chat
setContext('bill', `${session}/${billNumber}`, {
number: billNumber,
session,
title: billTitle,
section: sectionLabel,
sectionRef,
});
// Wait for useBillChatSummary to create the conversation (it runs when chat opens)
// This prevents a race condition where both try to create a conversation
let currentConversation = useChatStore.getState().conversation;
let attempts = 0;
const maxAttempts = 20; // 2 seconds max wait
while (!currentConversation && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 100));
currentConversation = useChatStore.getState().conversation;
attempts++;
}
// Build the summary message with section context
const summaryMessage = `**${locale === 'fr' ? 'Explication de la section' : 'Section Explanation'}: ${sectionLabel}**\n\n${data.summary}`;
// Add summary as an assistant message
addMessage({
id: `section-summary-${sectionRef}-${Date.now()}`,
conversation_id: currentConversation?.id || 'temp',
role: 'assistant',
content: summaryMessage,
created_at: new Date().toISOString(),
used_byo_key: false,
});
} catch (err: unknown) {
console.error('Error generating section summary:', err);
// Show error message in the chat
const errorMessage = err instanceof Error ? err.message : 'Failed to generate summary';
const isBYOKKeyRequired = errorMessage.toLowerCase().includes('byok') &&
errorMessage.toLowerCase().includes('api key');
const isQuotaError = errorMessage.toLowerCase().includes('quota') ||
errorMessage.toLowerCase().includes('limit') ||
errorMessage.toLowerCase().includes('exceeded');
let displayMessage: string;
if (isBYOKKeyRequired) {
displayMessage = locale === 'fr'
? `**Clé API requise**\n\nVotre forfait BYOK nécessite une clé API Anthropic pour utiliser les fonctionnalités IA.\n\n[Ajouter votre clé API dans les Paramètres](/${locale}/settings)`
: `**API Key Required**\n\nYour BYOK plan requires an Anthropic API key to use AI features.\n\n[Add your API key in Settings](/${locale}/settings)`;
} else if (isQuotaError) {
displayMessage = locale === 'fr'
? `**Limite atteinte**\n\nVous avez atteint votre limite de requêtes. Veuillez passer à un forfait supérieur ou réessayer plus tard.`
: `**Quota Exceeded**\n\nYou've reached your query limit. Please upgrade your plan or try again later.`;
} else {
displayMessage = locale === 'fr'
? `**Erreur**\n\nImpossible de générer l'explication: ${errorMessage}`
: `**Error**\n\nUnable to generate explanation: ${errorMessage}`;
}
addMessage({
id: `section-summary-error-${sectionRef}-${Date.now()}`,
conversation_id: useChatStore.getState().conversation?.id || 'temp',
role: 'assistant',
content: displayMessage,
created_at: new Date().toISOString(),
used_byo_key: false,
});
} finally {
setIsLoading(false);
setChatLoading(false);
}
}, [
user,
isLoading,
sectionText,
session,
billNumber,
sectionRef,
sectionLabel,
parentContext,
billTitle,
locale,
setContext,
isOpen,
toggleOpen,
addMessage,
setChatLoading,
]);
// Don't render if not authenticated or still loading auth
if (authLoading || !user) {
return null;
}
// Don't render if there's no text content to summarize
if (!sectionText || sectionText.trim().length === 0) {
return null;
}
const title = locale === 'fr' ? 'Explication IA de cette section' : 'AI explanation of this section';
return (
<button
onClick={handleClick}
disabled={isLoading}
className={`
p-1 transition-colors
text-text-tertiary hover:text-accent-red
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
title={title}
aria-label={title}
>
<Sparkles size={iconSize} />
</button>
);
}