Compare commits
4 Commits
4b2948954f
...
48dc0afe1a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48dc0afe1a | ||
|
|
f5112cea92 | ||
|
|
01f2c4a804 | ||
|
|
7ee61ace8f |
@ -388,7 +388,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Messages intégrés */}
|
{/* Messages intégrés */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Chat heightClass="h-[600px]" />
|
<Chat heightClass="h-[600px]" processes={processes} myProcesses={myProcesses} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nouveaux indicateurs de stockage */}
|
{/* Nouveaux indicateurs de stockage */}
|
||||||
|
|||||||
@ -28,20 +28,55 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { PairingProcess } from "@/lib/4nk/models/PairingProcess"
|
||||||
|
|
||||||
interface ChatProps {
|
interface ChatProps {
|
||||||
heightClass?: string
|
heightClass?: string
|
||||||
|
processes?: any
|
||||||
|
myProcesses?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Chat({ heightClass = "h-[calc(100vh-8rem)]" }: ChatProps) {
|
export default function Chat({ heightClass = "h-[calc(100vh-8rem)]", processes, myProcesses }: ChatProps) {
|
||||||
const [selectedConversation, setSelectedConversation] = useState("global")
|
const [selectedConversation, setSelectedConversation] = useState("")
|
||||||
const [newMessage, setNewMessage] = useState("")
|
const [newMessage, setNewMessage] = useState("")
|
||||||
|
const [pairingProcesses, setPairingProcesses] = useState<PairingProcess[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("user")
|
const userId = searchParams.get("user")
|
||||||
const messageType = searchParams.get("message")
|
const messageType = searchParams.get("message")
|
||||||
const groupType = searchParams.get("type")
|
const groupType = searchParams.get("type")
|
||||||
|
|
||||||
|
// Filter pairing processes when processes prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (processes && Object.keys(processes).length > 0) {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// Filter pairing processes (those with memberPublicName in publicData)
|
||||||
|
const pairingList: PairingProcess[] = []
|
||||||
|
Object.entries(processes).forEach(([processId, process]) => {
|
||||||
|
// Get the latest state
|
||||||
|
const latestState = process.states?.[process.states.length - 2] // -2 because last state is usually empty
|
||||||
|
|
||||||
|
// Check if memberPublicName field exists (even if empty) - indicates pairing process
|
||||||
|
if (latestState?.public_data?.hasOwnProperty('memberPublicName')) {
|
||||||
|
const memberPublicName = latestState.public_data.memberPublicName || `Pairing ${processId.slice(0, 8)}`
|
||||||
|
pairingList.push({
|
||||||
|
id: processId,
|
||||||
|
memberPublicName: memberPublicName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setPairingProcesses(pairingList)
|
||||||
|
setIsLoading(false)
|
||||||
|
} else {
|
||||||
|
setIsLoading(true)
|
||||||
|
setPairingProcesses([])
|
||||||
|
}
|
||||||
|
}, [processes, myProcesses])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messageType === "new") {
|
if (messageType === "new") {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@ -51,7 +86,6 @@ export default function Chat({ heightClass = "h-[calc(100vh-8rem)]" }: ChatProps
|
|||||||
setSelectedConversation(userId)
|
setSelectedConversation(userId)
|
||||||
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
||||||
sessionStorage.removeItem("newMessage")
|
sessionStorage.removeItem("newMessage")
|
||||||
showNotification("info", `Conversation ouverte avec ${data.userName}`)
|
|
||||||
}
|
}
|
||||||
} else if (groupType === "group") {
|
} else if (groupType === "group") {
|
||||||
const groupData = sessionStorage.getItem("newGroupMessage")
|
const groupData = sessionStorage.getItem("newGroupMessage")
|
||||||
@ -60,227 +94,56 @@ export default function Chat({ heightClass = "h-[calc(100vh-8rem)]" }: ChatProps
|
|||||||
setSelectedConversation("group-new")
|
setSelectedConversation("group-new")
|
||||||
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
||||||
sessionStorage.removeItem("newGroupMessage")
|
sessionStorage.removeItem("newGroupMessage")
|
||||||
showNotification("info", `Conversation de groupe créée avec ${data.users.length} utilisateur(s)`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userId, messageType, groupType])
|
}, [userId, messageType, groupType])
|
||||||
|
|
||||||
const showNotification = (type: "success" | "error" | "info", message: string) => {
|
// Create conversations array with pairing processes only
|
||||||
console.log(`${type.toUpperCase()}: ${message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversations = [
|
const conversations = [
|
||||||
{
|
...pairingProcesses.map(process => {
|
||||||
id: "global",
|
// Generate avatar from memberPublicName or processId
|
||||||
name: "My Work",
|
let avatar = "P" // Default for Pairing
|
||||||
type: "group",
|
if (process.memberPublicName && typeof process.memberPublicName === 'string' && process.memberPublicName.trim().length > 0) {
|
||||||
avatar: "MW",
|
// Use memberPublicName if not empty
|
||||||
lastMessage: "Bienvenue sur le chat de l’espace My Work",
|
avatar = process.memberPublicName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||||
lastMessageTime: "Maintenant",
|
} else {
|
||||||
unreadCount: 0,
|
// Use first 2 chars of processId if memberPublicName is empty
|
||||||
isOnline: false,
|
avatar = process.id.slice(0, 2).toUpperCase()
|
||||||
isTyping: false,
|
}
|
||||||
members: 0,
|
|
||||||
},
|
// Safe display name
|
||||||
{
|
const displayName = typeof process.memberPublicName === 'string' && process.memberPublicName.trim().length > 0
|
||||||
id: "1",
|
? process.memberPublicName
|
||||||
name: "Marie Dubois",
|
: `Membre ${process.id.slice(0, 8)}`
|
||||||
type: "direct",
|
|
||||||
avatar: "MD",
|
return {
|
||||||
lastMessage: "Parfait, merci pour la validation !",
|
id: process.id,
|
||||||
lastMessageTime: "14:32",
|
name: displayName,
|
||||||
unreadCount: 0,
|
avatar: avatar,
|
||||||
isOnline: true,
|
lastMessage: "",
|
||||||
isTyping: false,
|
lastMessageTime: "",
|
||||||
},
|
unreadCount: 0,
|
||||||
{
|
isOnline: true,
|
||||||
id: "2",
|
isTyping: false,
|
||||||
name: "Équipe Juridique",
|
pairingId: process.id
|
||||||
type: "group",
|
}
|
||||||
avatar: "EJ",
|
})
|
||||||
lastMessage: "IA DocV: Analyse terminée pour Contrat_Client_ABC.pdf",
|
|
||||||
lastMessageTime: "13:45",
|
|
||||||
unreadCount: 1,
|
|
||||||
isOnline: false,
|
|
||||||
isTyping: false,
|
|
||||||
members: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "Sophie Laurent",
|
|
||||||
type: "direct",
|
|
||||||
avatar: "SL",
|
|
||||||
lastMessage: "Pouvez-vous m'envoyer le rapport ?",
|
|
||||||
lastMessageTime: "12:20",
|
|
||||||
unreadCount: 1,
|
|
||||||
isOnline: false,
|
|
||||||
isTyping: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
name: "Direction",
|
|
||||||
type: "group",
|
|
||||||
avatar: "DIR",
|
|
||||||
lastMessage: "Réunion reportée à demain 10h",
|
|
||||||
lastMessageTime: "11:15",
|
|
||||||
unreadCount: 0,
|
|
||||||
isOnline: false,
|
|
||||||
isTyping: false,
|
|
||||||
members: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
name: "Thomas Rousseau",
|
|
||||||
type: "direct",
|
|
||||||
avatar: "TR",
|
|
||||||
lastMessage: "Merci pour l'info !",
|
|
||||||
lastMessageTime: "Hier",
|
|
||||||
unreadCount: 0,
|
|
||||||
isOnline: true,
|
|
||||||
isTyping: true,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const messages = [
|
const messages: any[] = []
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
senderId: "marie",
|
|
||||||
senderName: "Marie Dubois",
|
|
||||||
content: "Bonjour ! J'ai besoin de votre avis sur le nouveau contrat client.",
|
|
||||||
timestamp: "14:20",
|
|
||||||
type: "text",
|
|
||||||
status: "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
senderId: "me",
|
|
||||||
senderName: "Moi",
|
|
||||||
content: "Bien sûr, pouvez-vous me l'envoyer ?",
|
|
||||||
timestamp: "14:22",
|
|
||||||
type: "text",
|
|
||||||
status: "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
senderId: "marie",
|
|
||||||
senderName: "Marie Dubois",
|
|
||||||
content: "",
|
|
||||||
timestamp: "14:25",
|
|
||||||
type: "file",
|
|
||||||
fileName: "Contrat_Client_ABC.pdf",
|
|
||||||
fileSize: "2.3 MB",
|
|
||||||
status: "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
senderId: "me",
|
|
||||||
senderName: "Moi",
|
|
||||||
content:
|
|
||||||
"J'ai relu le contrat, tout me semble correct. Les clauses de confidentialité sont bien définies.",
|
|
||||||
timestamp: "14:30",
|
|
||||||
type: "text",
|
|
||||||
status: "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
senderId: "marie",
|
|
||||||
senderName: "Marie Dubois",
|
|
||||||
content: "Parfait, merci pour la validation !",
|
|
||||||
timestamp: "14:32",
|
|
||||||
type: "text",
|
|
||||||
status: "delivered",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "6",
|
|
||||||
senderId: "ai",
|
|
||||||
senderName: "IA DocV",
|
|
||||||
content: `📄 **Analyse IA du document "Contrat_Client_ABC.pdf"**
|
|
||||||
|
|
||||||
**Type de document :** PDF (2.3 MB)
|
// Filter conversations based on search query
|
||||||
**Statut :** ✅ Validé
|
const filteredConversations = conversations.filter(conversation => {
|
||||||
**Dernière modification :** Il y a 2 heures
|
if (!searchQuery.trim()) return true
|
||||||
|
|
||||||
**📊 Analyse du contenu :**
|
// Search by ID (process ID)
|
||||||
• Document juridique détecté avec haute précision
|
const matchesId = conversation.id.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
• 3 tag(s) identifié(s) : contrat, client, juridique
|
// Search by name
|
||||||
• Résumé automatique disponible
|
const matchesName = conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
• 47 pages analysées
|
|
||||||
• 12 clauses contractuelles détectées
|
return matchesId || matchesName
|
||||||
|
})
|
||||||
**🎯 Métriques de qualité :**
|
|
||||||
• Lisibilité : 92%
|
|
||||||
• Conformité juridique : 100%
|
|
||||||
• Sécurité documentaire : Maximale
|
|
||||||
• Complétude des informations : 95%
|
|
||||||
|
|
||||||
**🔍 Points clés identifiés :**
|
|
||||||
• Durée du contrat : 12 mois
|
|
||||||
• Montant total : 150 000€ HT
|
|
||||||
• Clauses de confidentialité : ✅ Présentes et conformes
|
|
||||||
• Propriété intellectuelle : ✅ Bien définie
|
|
||||||
• Conditions de résiliation : ✅ Équilibrées
|
|
||||||
|
|
||||||
**🛡️ Analyse de conformité RGPD :**
|
|
||||||
• Données personnelles : ⚠️ Détectées (coordonnées client)
|
|
||||||
• Durée de conservation : Conforme (7 ans)
|
|
||||||
• Droit à l'oubli : Applicable après expiration
|
|
||||||
• Consentement : ✅ Explicite
|
|
||||||
|
|
||||||
**⚡ Recommandations :**
|
|
||||||
• ✅ Document prêt pour signature
|
|
||||||
• 📋 Archivage permanent recommandé
|
|
||||||
• 🔄 Révision suggérée dans 11 mois
|
|
||||||
• 📧 Notification client automatique activée
|
|
||||||
|
|
||||||
**📈 Score global : 94/100**
|
|
||||||
|
|
||||||
*Analyse générée automatiquement par l'IA DocV - Fiabilité : 98%*`,
|
|
||||||
timestamp: "14:35",
|
|
||||||
type: "ai_analysis",
|
|
||||||
status: "delivered",
|
|
||||||
analysisType: "document",
|
|
||||||
documentName: "Contrat_Client_ABC.pdf",
|
|
||||||
confidence: 98,
|
|
||||||
processingTime: "2.3s",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "7",
|
|
||||||
senderId: "ai",
|
|
||||||
senderName: "IA DocV",
|
|
||||||
content: `🔍 **Analyse comparative - Dossier Contrats**
|
|
||||||
|
|
||||||
**📊 Analyse de 8 documents similaires :**
|
|
||||||
• Contrats clients : 5 documents
|
|
||||||
• Avenants : 2 documents
|
|
||||||
• Conditions générales : 1 document
|
|
||||||
|
|
||||||
**📈 Tendances identifiées :**
|
|
||||||
• Montant moyen des contrats : +15% vs trimestre précédent
|
|
||||||
• Durée moyenne : 14 mois (stable)
|
|
||||||
• Taux de renouvellement : 87% (↗️ +5%)
|
|
||||||
|
|
||||||
**⚠️ Points d'attention :**
|
|
||||||
• 2 contrats expirent dans les 30 jours
|
|
||||||
• 1 clause de révision tarifaire à activer
|
|
||||||
• Mise à jour RGPD requise sur 3 documents
|
|
||||||
|
|
||||||
**🎯 Actions recommandées :**
|
|
||||||
1. Planifier renouvellement contrats Q1 2024
|
|
||||||
2. Standardiser les clauses de confidentialité
|
|
||||||
3. Créer un modèle basé sur ce contrat (performance optimale)
|
|
||||||
|
|
||||||
*Analyse prédictive activée - Prochaine révision : 15 février 2024*`,
|
|
||||||
timestamp: "14:37",
|
|
||||||
type: "ai_analysis",
|
|
||||||
status: "delivered",
|
|
||||||
analysisType: "comparative",
|
|
||||||
confidence: 95,
|
|
||||||
processingTime: "4.1s",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const filteredConversations = conversations
|
|
||||||
|
|
||||||
const currentConversation = conversations.find((conv) => conv.id === selectedConversation)
|
const currentConversation = conversations.find((conv) => conv.id === selectedConversation)
|
||||||
|
|
||||||
@ -291,241 +154,114 @@ export default function Chat({ heightClass = "h-[calc(100vh-8rem)]" }: ChatProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case "sent":
|
|
||||||
return <Clock className="h-3 w-3 text-gray-400" />
|
|
||||||
case "delivered":
|
|
||||||
return <CheckCheck className="h-3 w-3 text-gray-400" />
|
|
||||||
case "read":
|
|
||||||
return <CheckCheck className="h-3 w-3 text-blue-500" />
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAnalysisIcon = (analysisType: string) => {
|
|
||||||
switch (analysisType) {
|
|
||||||
case "document":
|
|
||||||
return <FileText className="h-4 w-4" />
|
|
||||||
case "comparative":
|
|
||||||
return <BarChart3 className="h-4 w-4" />
|
|
||||||
case "security":
|
|
||||||
return <Shield className="h-4 w-4" />
|
|
||||||
case "performance":
|
|
||||||
return <TrendingUp className="h-4 w-4" />
|
|
||||||
default:
|
|
||||||
return <Brain className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderAIMessage = (message: any) => {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="max-w-4xl">
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
<div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center">
|
|
||||||
<Brain className="h-4 w-4 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">IA DocV</span>
|
|
||||||
<Badge className="bg-gradient-to-r from-purple-100 to-blue-100 dark:from-purple-800 dark:to-blue-800 text-purple-700 dark:text-purple-200 border-purple-200 dark:border-purple-700 text-xs">
|
|
||||||
{getAnalysisIcon(message.analysisType)}
|
|
||||||
<span className="ml-1">
|
|
||||||
{message.analysisType === "document"
|
|
||||||
? "Analyse Document"
|
|
||||||
: message.analysisType === "comparative"
|
|
||||||
? "Analyse Comparative"
|
|
||||||
: "Analyse IA"}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
{message.confidence && (
|
|
||||||
<Badge className="bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-200 border-green-200 dark:border-green-700 text-xs">
|
|
||||||
<CheckCircle className="h-3 w-3 mr-1" />
|
|
||||||
{message.confidence}% fiable
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gradient-to-r from-purple-50 to-blue-50 dark:from-purple-900 dark:to-blue-900 border border-purple-200 dark:border-purple-700 rounded-lg p-4 shadow-sm">
|
|
||||||
<div className="prose prose-sm max-w-none">
|
|
||||||
<div className="whitespace-pre-line text-gray-800 dark:text-gray-100 leading-relaxed">{message.content}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-purple-200 dark:border-purple-700">
|
|
||||||
<div className="flex items-center space-x-4 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<Zap className="h-3 w-3" />
|
|
||||||
<span>Traité en {message.processingTime}</span>
|
|
||||||
</div>
|
|
||||||
{message.documentName && (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<FileText className="h-3 w-3" />
|
|
||||||
<span>{message.documentName}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-xs text-purple-600 dark:text-purple-400">{message.timestamp}</span>
|
|
||||||
<div>{getStatusIcon(message.status)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${heightClass} flex`}>
|
<div className={`${heightClass} flex`}>
|
||||||
<div className="w-80 border-r bg-white dark:bg-gray-800 flex flex-col">
|
<div className="w-80 border-r bg-white dark:bg-gray-800 flex flex-col">
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Messages</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Messages</h2>
|
||||||
<Button size="sm">
|
<div className="relative">
|
||||||
<Plus className="h-4 w-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
</Button>
|
<Input
|
||||||
|
placeholder="Rechercher par ID ou nom..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{filteredConversations.map((conversation) => (
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<div className="text-gray-500 dark:text-gray-400">Chargement des processus de pairing...</div>
|
||||||
|
</div>
|
||||||
|
) : filteredConversations.length > 0 ? (
|
||||||
|
filteredConversations.map((conversation) => (
|
||||||
<div
|
<div
|
||||||
key={conversation.id}
|
key={conversation.id}
|
||||||
onClick={() => setSelectedConversation(conversation.id)}
|
onClick={() => setSelectedConversation(conversation.id)}
|
||||||
className={`p-4 border-b cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 ${selectedConversation === conversation.id
|
className={`p-4 border-b cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||||
? "bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400"
|
selectedConversation === conversation.id
|
||||||
: ""
|
? "bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400"
|
||||||
}`}
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
||||||
{conversation.type === "group" ? (
|
<span className="text-blue-600 dark:text-blue-400 font-medium">{conversation.avatar}</span>
|
||||||
<Users className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
) : (
|
|
||||||
<span className="text-blue-600 dark:text-blue-400 font-medium">{conversation.avatar}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{conversation.isOnline && conversation.type === "direct" && (
|
{conversation.isOnline && (
|
||||||
<Circle className="absolute -bottom-1 -right-1 h-4 w-4 text-green-500 fill-current" />
|
<Circle className="absolute -bottom-1 -right-1 h-4 w-4 text-green-500 fill-current" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 truncate">{conversation.name}</h3>
|
<h3 className="font-medium text-gray-900 dark:text-gray-100 truncate">{conversation.name}</h3>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">{conversation.lastMessageTime}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mt-1">
|
{'pairingId' in conversation && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 truncate">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 font-mono">
|
||||||
{conversation.isTyping ? (
|
ID: {conversation.pairingId.slice(0, 8)}...{conversation.pairingId.slice(-4)}
|
||||||
<span className="text-blue-600 dark:text-blue-400 italic">En train d'écrire...</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={conversation.lastMessage.includes("IA DocV:") ? "text-purple-600 dark:text-purple-400 font-medium" : ""}
|
|
||||||
>
|
|
||||||
{conversation.lastMessage}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
{conversation.unreadCount > 0 && (
|
|
||||||
<Badge
|
|
||||||
className={`text-white text-xs px-2 py-1 rounded-full ${conversation.lastMessage.includes("IA DocV:")
|
|
||||||
? "bg-purple-600 dark:bg-purple-400"
|
|
||||||
: "bg-blue-600 dark:bg-blue-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{conversation.unreadCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{conversation.type === "group" && (
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{conversation.members} membres</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
</div>
|
) : (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<Search className="h-8 w-8 mx-auto text-gray-400 dark:text-gray-500 mb-2" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Aucun membre trouvé</p>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
|
Essayez de rechercher par ID ou nom
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
{currentConversation ? (
|
{currentConversation ? (
|
||||||
<>
|
<>
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
||||||
{currentConversation.type === "group" ? (
|
<span className="text-blue-600 dark:text-blue-400 font-medium">{currentConversation.avatar}</span>
|
||||||
<Users className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
||||||
) : (
|
|
||||||
<span className="text-blue-600 dark:text-blue-400 font-medium">{currentConversation.avatar}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{currentConversation.isOnline && currentConversation.type === "direct" && (
|
|
||||||
<Circle className="absolute -bottom-1 -right-1 h-3 w-3 text-green-500 fill-current" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{currentConversation.isOnline && (
|
||||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">{currentConversation.name}</h3>
|
<Circle className="absolute -bottom-1 -right-1 h-3 w-3 text-green-500 fill-current" />
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{currentConversation.type === "group"
|
|
||||||
? `${currentConversation.members} membres`
|
|
||||||
: currentConversation.isOnline
|
|
||||||
? "En ligne"
|
|
||||||
: "Hors ligne"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<MoreHorizontal className="h-4 w-4 text-gray-900 dark:text-gray-100" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 dark:bg-gray-900">
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div key={message.id}>
|
|
||||||
{message.type === "ai_analysis" ? (
|
|
||||||
renderAIMessage(message)
|
|
||||||
) : (
|
|
||||||
<div className={`flex ${message.senderId === "me" ? "justify-end" : "justify-start"}`}>
|
|
||||||
<div
|
|
||||||
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${message.senderId === "me"
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{message.type === "text" ? (
|
|
||||||
<p className="text-sm">{message.content}</p>
|
|
||||||
) : message.type === "file" ? (
|
|
||||||
<div className="flex items-center space-x-3 p-2">
|
|
||||||
<File className="h-8 w-8 text-gray-400 dark:text-gray-500" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium">{message.fileName}</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{message.fileSize}</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between mt-1 ${message.senderId === "me" ? "text-blue-100" : "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-xs">{message.timestamp}</span>
|
|
||||||
{message.senderId === "me" && <div className="ml-2">{getStatusIcon(message.status)}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-gray-100">{currentConversation.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{currentConversation.isOnline ? "En ligne" : "Hors ligne"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<MoreHorizontal className="h-4 w-4 text-gray-900 dark:text-gray-100" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<MessageSquare className="h-12 w-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">Aucun message</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">Commencez une conversation en envoyant un message</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||||
<div className="flex items-end space-x-2">
|
<div className="flex items-end space-x-2">
|
||||||
|
|||||||
13
lib/4nk/models/PairingProcess.ts
Normal file
13
lib/4nk/models/PairingProcess.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export interface PairingProcess {
|
||||||
|
id: string;
|
||||||
|
memberPublicName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPairingProcess(data: any): data is PairingProcess {
|
||||||
|
if (typeof data !== 'object' || data === null) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof data.id === 'string' &&
|
||||||
|
typeof data.memberPublicName === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user