'use client';
import React, { useState, useRef, useEffect } from 'react';
import { Send, Paperclip, Loader2 } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
export default function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
role: 'assistant',
content: 'Ciao! Sono Trusty Sign, il tuo assistente specializzato in firme digitali. Posso aiutarti a:\n\n• Firmare documenti digitalmente\n• Verificare firme esistenti\n• Gestire certificati digitali\n• Consulenza su normative (eIDAS, CAD)\n\nCarica un documento o dimmi cosa ti serve!',
timestamp: new Date(),
},
]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [sessionId, setSessionId] = useState<string | null>(null);
const [versionInfo, setVersionInfo] = useState<{commit: string, message: string} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// Fetch version info on mount
useEffect(() => {
const fetchVersion = async () => {
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
const response = await fetch(`${apiUrl}/trustysign/version`);
const data = await response.json();
setVersionInfo(data);
} catch (error) {
console.error('Failed to fetch version:', error);
}
};
fetchVersion();
}, []);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const newFiles = Array.from(e.target.files);
setUploadedFiles((prev) => [...prev, ...newFiles]);
}
};
const removeFile = (index: number) => {
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleSend = async () => {
if (!inputValue.trim() && uploadedFiles.length === 0) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
const messageText = inputValue;
setInputValue('');
setIsLoading(true);
try {
// TODO: Upload files first if present
if (uploadedFiles.length > 0) {
// For now, just mention files in message
console.log('Files to upload:', uploadedFiles);
}
// Send message to backend
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
// TODO: Replace with proper authentication - for MVP testing only
const testUserEmail = 'yyi9910@infocert.it';
console.log('🚀 Sending request to:', `${apiUrl}/trustysign/chat?user_email=${encodeURIComponent(testUserEmail)}`);
console.log('📝 Message:', messageText);
const response = await fetch(`${apiUrl}/trustysign/chat?user_email=${encodeURIComponent(testUserEmail)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: messageText,
session_id: sessionId, // Use persisted session_id
}),
});
console.log('📡 Response status:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('✅ Response data:', data);
// Save session_id from response
if (data.session_id && !sessionId) {
setSessionId(data.session_id);
console.log('Session ID saved:', data.session_id);
}
// Check for attachment download
if (data.attachment) {
const att = data.attachment;
// File salvato su disco - usa endpoint download
if (att.file_path) {
// Estrai il filename dal path (es: /app/attachments/20231106_120530_document.pdf)
const filename = att.file_path.split('/').pop();
// Trigger download da endpoint API
const downloadUrl = `/trustysign/download/${filename}`;
const a = document.createElement('a');
a.href = downloadUrl;
a.download = att.name || 'attachment';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
const sizeFormatted = att.size > 1024 * 1024
? `${(att.size / 1024 / 1024).toFixed(2)} MB`
: `${(att.size / 1024).toFixed(1)} KB`;
const downloadMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `✅ Download avviato: **${att.name}** (${sizeFormatted})`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, downloadMessage]);
}
// Don't return - continue to show the final response below
}
// Check if OAuth is required
if (data.error && data.error.oauth_required) {
const oauthMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `🔐 Per accedere alle tue email e calendario, devo autenticarmi con il tuo account Microsoft.\n\n[Clicca qui per autenticarti](${data.error.login_url})\n\nDopo l'autenticazione, riprova la richiesta.`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, oauthMessage]);
} else {
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: data.response || 'Ricevuto!',
timestamp: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
}
} catch (error) {
console.error('❌ Error sending message:', error);
console.error('❌ Error stack:', error instanceof Error ? error.stack : 'No stack');
console.error('❌ Error message:', error instanceof Error ? error.message : String(error));
// Show error message to user
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `⚠️ Mi dispiace, si è verificato un errore. Riprova tra poco.\n\nDettagli tecnici: ${error instanceof Error ? error.message : String(error)}`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
setUploadedFiles([]);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div style={{
minHeight: '100vh',
background: 'var(--color-bg)',
padding: '20px'
}}>
<div style={{
maxWidth: '900px',
margin: '0 auto'
}}>
{/* Header - SAME AS BOOKING */}
<div style={{
background: 'linear-gradient(135deg, #0072ce 0%, #005a9e 100%)',
color: 'white',
padding: '30px',
borderRadius: '8px 8px 0 0'
}}>
{/* Header Logos - COPIED FROM BOOKING */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px'
}}>
<img
src="/trustysign/tinexta-logo-white.svg"
alt="Tinexta InfoCert"
style={{ height: '35px' }}
/>
<img
src="/trustysign/trusty-logo.svg"
alt="Trusty Personal Assistant"
style={{ height: '40px' }}
/>
</div>
{/* Header Content */}
<div style={{ textAlign: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', marginBottom: '5px' }}>
<h1 style={{
fontSize: '24px',
fontWeight: 600,
margin: 0
}}>
Trusty Personal Assistant
</h1>
{versionInfo && (
<span
style={{
background: 'rgba(255,255,255,0.2)',
padding: '4px 10px',
borderRadius: '12px',
fontSize: '11px',
fontFamily: 'monospace',
fontWeight: 500
}}
title={versionInfo.message}
>
{versionInfo.commit}
</span>
)}
</div>
<p style={{
fontSize: '14px',
opacity: 0.9
}}>
Il tuo assistente AI per business e firme digitali
</p>
</div>
</div>
{/* Chat Card - SAME AS BOOKING */}
<div style={{
background: 'var(--color-card)',
borderRadius: '0 0 8px 8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
padding: '30px',
minHeight: '500px',
display: 'flex',
flexDirection: 'column'
}}>
{/* Messages Area */}
<div style={{
flex: 1,
overflowY: 'auto',
marginBottom: '20px',
maxHeight: '500px'
}}>
{messages.map((msg) => (
<div
key={msg.id}
style={{
marginBottom: '20px',
padding: '20px',
borderRadius: '8px',
border: '2px solid var(--color-border)',
background: msg.role === 'assistant' ? '#f0f8ff' : 'white'
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px'
}}>
<div style={{
fontWeight: 600,
color: 'var(--color-primary-dark)',
fontSize: '14px'
}}>
{msg.role === 'assistant' ? '🤖 Trusty' : '👤 Tu'}
</div>
<div style={{
fontSize: '12px',
color: 'var(--color-text-light)'
}}>
{msg.timestamp.toLocaleTimeString('it-IT', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
<div style={{
fontSize: '14px',
lineHeight: 1.6,
color: 'var(--color-text)'
}}>
{msg.role === 'assistant' ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => (
<a
{...props}
style={{
color: 'var(--color-primary)',
textDecoration: 'underline',
fontWeight: 500
}}
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => (
<p {...props} style={{ margin: '0.5em 0' }} />
)
}}
>
{msg.content}
</ReactMarkdown>
) : (
<div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>
)}
</div>
</div>
))}
{isLoading && (
<div style={{
textAlign: 'center',
padding: '20px'
}}>
<div style={{
border: '4px solid var(--color-border)',
borderTop: '4px solid var(--color-primary)',
borderRadius: '50%',
width: '40px',
height: '40px',
animation: 'spin 1s linear infinite',
margin: '0 auto 10px'
}}></div>
<p style={{ fontSize: '14px', color: 'var(--color-text-light)' }}>
Trusty sta scrivendo...
</p>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* File Upload Chips */}
{uploadedFiles.length > 0 && (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
marginBottom: '15px'
}}>
{uploadedFiles.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
background: '#e3f2fd',
borderRadius: '20px',
fontSize: '12px',
color: 'var(--color-primary-dark)'
}}
>
<span>📎 {file.name}</span>
<button
onClick={() => removeFile(index)}
style={{
background: 'none',
border: 'none',
color: 'var(--color-error)',
cursor: 'pointer',
fontSize: '16px',
padding: 0,
lineHeight: 1
}}
>
×
</button>
</div>
))}
</div>
)}
{/* Input Area - SAME STYLE AS BOOKING BUTTONS */}
<div style={{
display: 'flex',
gap: '10px',
alignItems: 'flex-end'
}}>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
multiple
accept=".pdf,.p7m,.p7s"
style={{ display: 'none' }}
/>
<button
onClick={() => fileInputRef.current?.click()}
style={{
padding: '14px',
border: '2px solid var(--color-primary)',
borderRadius: '6px',
background: 'white',
color: 'var(--color-primary)',
cursor: 'pointer',
transition: 'all 0.3s ease',
flexShrink: 0
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--color-primary)';
e.currentTarget.style.color = 'white';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'white';
e.currentTarget.style.color = 'var(--color-primary)';
}}
>
<Paperclip size={20} />
</button>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Invia un messaggio"
rows={1}
style={{
flex: 1,
padding: '14px',
border: '2px solid var(--color-border)',
borderRadius: '6px',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'none',
minHeight: '50px',
lineHeight: 1.6
}}
/>
<button
onClick={handleSend}
disabled={isLoading || (!inputValue.trim() && uploadedFiles.length === 0)}
style={{
padding: '14px 24px',
border: 'none',
borderRadius: '6px',
background: isLoading || (!inputValue.trim() && uploadedFiles.length === 0)
? '#ccc'
: 'var(--color-primary-light)',
color: 'white',
fontSize: '16px',
fontWeight: 600,
cursor: isLoading || (!inputValue.trim() && uploadedFiles.length === 0)
? 'not-allowed'
: 'pointer',
transition: 'all 0.3s ease',
display: 'flex',
alignItems: 'center',
gap: '8px',
flexShrink: 0
}}
onMouseEnter={(e) => {
if (!isLoading && (inputValue.trim() || uploadedFiles.length > 0)) {
e.currentTarget.style.background = 'var(--color-primary)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,120,212,0.3)';
}
}}
onMouseLeave={(e) => {
if (!isLoading && (inputValue.trim() || uploadedFiles.length > 0)) {
e.currentTarget.style.background = 'var(--color-primary-light)';
e.currentTarget.style.boxShadow = 'none';
}
}}
>
{isLoading ? <Loader2 size={20} className="animate-spin" /> : <Send size={20} />}
<span>Invia</span>
</button>
</div>
</div>
{/* Footer - SAME AS BOOKING */}
<div style={{
textAlign: 'center',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid var(--color-border)',
color: 'var(--color-text-light)',
fontSize: '12px'
}}>
Powered by <strong style={{ color: 'var(--color-primary)' }}>Trusty Sign</strong><br />
Tinexta InfoCert - Digital Trust & Cybersecurity
</div>
</div>
<style jsx>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}