/**
* SharedConversationEmbed - Expandable card for AI conversations shared in forum posts
*
* Features:
* - Collapsible/expandable conversation view
* - Shows question and Gordie's response
* - Fork button to continue the conversation with context
* - Visual indication it's from CanadaGPT
*/
'use client';
import { useState } from 'react';
import { ChevronDown, ChevronUp, GitFork, MessageSquare, Bot, Loader2 } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useChatStore, useChatOpen } from '@/lib/stores/chatStore';
interface ConversationSnapshot {
messages: Array<{
role: 'user' | 'assistant';
content: string;
}>;
billContext?: {
number: string;
session: string;
title?: string;
};
}
interface SharedConversationEmbedProps {
conversationSnapshot: ConversationSnapshot;
conversationId?: string;
locale?: string;
defaultExpanded?: boolean;
}
export function SharedConversationEmbed({
conversationSnapshot,
conversationId,
locale = 'en',
defaultExpanded = false,
}: SharedConversationEmbedProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [isForking, setIsForking] = useState(false);
const [isOpen, toggleOpen] = useChatOpen();
const { addMessage, createConversation } = useChatStore();
const userMessage = conversationSnapshot.messages.find(m => m.role === 'user');
const assistantMessage = conversationSnapshot.messages.find(m => m.role === 'assistant');
if (!userMessage || !assistantMessage) {
return null;
}
// Truncate text for collapsed view
const truncate = (text: string, maxLength: number) => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength).trim() + '...';
};
const handleForkConversation = async () => {
if (isForking) return;
setIsForking(true);
try {
// Build context for the new conversation
const billContext = conversationSnapshot.billContext;
const billInfo = billContext ? ` about Bill ${billContext.number}` : '';
// Create the conversation first (this also sets context)
await createConversation(
billContext
? {
type: 'bill',
id: billContext.number,
data: {
name: `Forked conversation${billInfo}`,
...billContext,
},
}
: undefined
);
// Now add the forked context message (conversation exists, so it won't be wiped)
const contextMessage = {
id: `forked-${Date.now()}`,
conversation_id: '',
role: 'assistant' as const,
content: `*Forked conversation${billInfo}*\n\n**Original question:** ${userMessage.content}\n\n**Original response:** ${assistantMessage.content}\n\n---\n\nFeel free to ask follow-up questions!`,
used_byo_key: false,
created_at: new Date().toISOString(),
billContext: conversationSnapshot.billContext,
};
addMessage(contextMessage);
// Open the chat widget if it's not already open
if (!isOpen) {
toggleOpen();
}
} finally {
setIsForking(false);
}
};
return (
<div className="my-3 bg-gradient-to-br from-gray-800/80 to-gray-900/80 border border-gray-600 rounded-lg overflow-hidden">
{/* Header - Always visible */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between p-3 hover:bg-gray-700/30 transition-colors"
>
<div className="flex items-center gap-2">
<div className="p-1.5 bg-accent-red/20 rounded-lg">
<Bot className="w-4 h-4 text-accent-red" />
</div>
<div className="text-left">
<span className="text-sm font-medium text-white">
{locale === 'fr' ? 'Conversation CanadaGPT' : 'CanadaGPT Conversation'}
</span>
{!isExpanded && (
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">
{truncate(userMessage.content, 60)}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</div>
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-gray-700">
{/* Conversation Content */}
<div className="p-4 space-y-4">
{/* User Question */}
<div className="flex gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
<MessageSquare className="w-4 h-4 text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-blue-400 mb-1">
{locale === 'fr' ? 'Question' : 'Question'}
</p>
<p className="text-sm text-gray-200 whitespace-pre-wrap">
{userMessage.content}
</p>
</div>
</div>
{/* Gordie's Response */}
<div className="flex gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-accent-red/20 flex items-center justify-center">
<Bot className="w-4 h-4 text-accent-red" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-accent-red mb-1">Gordie</p>
<div className="text-sm text-gray-200 max-h-[300px] overflow-y-auto">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-accent-red hover:underline"
target="_blank"
rel="noopener noreferrer"
/>
),
code: ({ node, inline, ...props }: any) =>
inline ? (
<code
{...props}
className="bg-gray-700 text-gray-100 px-1.5 py-0.5 rounded text-sm font-mono"
/>
) : (
<code
{...props}
className="block bg-gray-700 text-gray-100 p-3 rounded-md text-sm overflow-x-auto font-mono"
/>
),
p: ({ node, ...props }) => (
<p {...props} className="mb-2 last:mb-0 text-gray-200" />
),
ul: ({ node, ...props }) => (
<ul {...props} className="list-disc ml-4 mb-2 text-gray-200" />
),
ol: ({ node, ...props }) => (
<ol {...props} className="list-decimal ml-4 mb-2 text-gray-200" />
),
li: ({ node, ...props }) => (
<li {...props} className="text-gray-200" />
),
h1: ({ node, ...props }) => (
<h1 {...props} className="text-lg font-bold mb-2 text-gray-100" />
),
h2: ({ node, ...props }) => (
<h2 {...props} className="text-base font-semibold mb-2 text-gray-100" />
),
h3: ({ node, ...props }) => (
<h3 {...props} className="text-sm font-semibold mb-1 text-gray-100" />
),
strong: ({ node, ...props }) => (
<strong {...props} className="font-semibold text-gray-100" />
),
}}
>
{assistantMessage.content}
</ReactMarkdown>
</div>
</div>
</div>
</div>
{/* Footer with Fork Button */}
<div className="px-4 py-3 bg-gray-900/50 border-t border-gray-700 flex items-center justify-between">
<p className="text-xs text-gray-500 italic">
{locale === 'fr'
? 'Partagé depuis CanadaGPT'
: 'Shared from CanadaGPT'}
</p>
<button
onClick={(e) => {
e.stopPropagation();
handleForkConversation();
}}
disabled={isForking}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isForking ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>{locale === 'fr' ? 'Chargement...' : 'Loading...'}</span>
</>
) : (
<>
<GitFork className="w-3.5 h-3.5" />
<span>{locale === 'fr' ? 'Continuer la conversation' : 'Fork Conversation'}</span>
</>
)}
</button>
</div>
</div>
)}
</div>
);
}