import React, { useState, useEffect, useRef, useCallback } from 'react';
import { formatTime } from '@/utils/date';
import { ResizablePanel } from '@/components/ui/ResizablePanel';
import { useUIStore } from '@/store/uiStore';
import { useCommStore } from '@/store/commStore';
import { useTranslation } from 'react-i18next';
import { useUsers, useGroups } from '@/api/hooks';
import { Avatar } from '@/components/ui/Avatar';
import {
listConversations,
getMessages,
sendMessage,
markRead,
createConversation,
} from '@/api/comm';
import { useCommWebSocket } from '@/hooks/useCommWebSocket';
import type { Conversation, Message } from '@/store/commStore';
import { BlockRenderer } from '@/components/comm/blocks';
import { Bell, Bookmark, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Send, Users } from 'lucide-react';
// ─── Icons (identical to AISidebar) ───
const robotIcon = (className: string) => (
);
const bellIcon = (className: string) => (
);
const bulbIcon = (className: string) => (
);
const teamIcon = (className: string) => (
);
const chatBubbleIcon = (className: string) => (
);
const chevronRightIcon = (
);
const sendIcon = (
);
const pinIcon = (
);
// ─── Types ───
type SidebarView = 'conversations' | 'team';
interface QuickAccessDef {
key: string;
label: string;
icon: (cls: string) => React.ReactNode;
testId: string;
view: SidebarView;
filterPinned?: string;
}
// ─── Team Panel (reused from AISidebar) ───
function TeamPanel({ onStartDirectChat }: { onStartDirectChat: (userId: string, userName: string) => void }) {
const { data: usersData, isLoading: usersLoading } = useUsers();
const { data: groupsData, isLoading: groupsLoading } = useGroups();
const users: any[] = usersData?.items || [];
const groups: any[] = groupsData?.items || [];
return (
Mitarbeiter
{usersLoading ? (
Laden...
) : users.length === 0 ? (
Keine Mitarbeiter
) : (
{users.map((u) => (
))}
)}
Gruppen
{groupsLoading ? (
Laden...
) : groups.length === 0 ? (
Keine Gruppen
) : (
{groups.map((g) => (
{g.name}
{g.description &&
{g.description}
}
))}
)}
);
}
// ─── Conversation List Item ───
function ConversationListItem({
conv,
isActive,
onClick,
}: {
conv: Conversation;
isActive: boolean;
onClick: () => void;
}) {
const senderIcon =
conv.last_msg_sender_type === 'ai' ? '🤖' : conv.last_msg_sender_type === 'system' ? '🔔' : '👤';
return (
);
}
// ─── Message Feed ───
function MessageFeed({
messages,
loading,
}: {
messages: Message[];
loading: boolean;
}) {
const scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
if (loading) {
return (
Laden...
);
}
if (messages.length === 0) {
return (
Keine Nachrichten
);
}
return (
{messages.map((msg) => {
const isAI = msg.sender_type === 'ai';
const isSystem = msg.sender_type === 'system';
const senderIcon = isAI ? '🤖' : isSystem ? '🔔' : '👤';
const senderName = isAI ? 'KI' : isSystem ? 'System' : 'Benutzer';
return (
{senderIcon}
{senderName}
{msg.content}
{/* Render rich content blocks via BlockRenderer */}
{msg.blocks && msg.blocks.length > 0 && (
)}
{msg.created_at && (
{formatTime(msg.created_at)}
)}
);
})}
);
}
// ─── Message Input ───
function MessageInput({
onSend,
disabled,
}: {
onSend: (text: string) => void;
disabled: boolean;
}) {
const [text, setText] = useState('');
const handleSend = () => {
if (!text.trim() || disabled) return;
onSend(text.trim());
setText('');
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
setText(e.target.value)}
onKeyDown={handleKeyDown}
disabled={disabled}
placeholder={disabled ? 'Schreibgeschützt' : 'Nachricht eingeben...'}
className="flex-1 px-3 py-2 text-sm rounded-lg border border-secondary-200 bg-white text-secondary-800 placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-secondary-50 disabled:text-secondary-400"
data-testid="message-input"
/>
);
}
// ─── Main MessageSidebar Component ───
export function MessageSidebar() {
const { t } = useTranslation();
const {
messageSidebarCollapsed,
toggleMessageSidebar,
} = useUIStore();
const {
conversations,
activeConversationId,
messages,
loading,
setConversations,
setActiveConversation,
setMessages,
setLoading,
setUnread,
} = useCommStore();
const [currentView, setCurrentView] = useState('conversations');
const [messagesLoading, setMessagesLoading] = useState(false);
const [error, setError] = useState(null);
// Connect WebSocket
useCommWebSocket();
// Load conversations on mount
useEffect(() => {
loadConversations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadConversations = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await listConversations();
const convs = res.items || [];
setConversations(convs);
// Auto-select first pinned conversation or first conversation
if (convs.length > 0 && !activeConversationId) {
const pinned = convs.find((c) => c.is_pinned);
const first = pinned || convs[0];
setActiveConversation(first.id);
loadMessages(first.id);
}
} catch (e) {
console.error('Failed to load conversations:', e);
setError('Konversationen konnten nicht geladen werden');
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadMessages = useCallback(async (convId: string) => {
setMessagesLoading(true);
try {
const res = await getMessages(convId);
setMessages(convId, res.items || []);
// Mark as read
await markRead(convId);
setUnread(convId, 0);
} catch (e) {
console.error('Failed to load messages:', e);
setMessages(convId, []);
} finally {
setMessagesLoading(false);
}
}, []);
const handleSelectConversation = useCallback(
(convId: string) => {
setActiveConversation(convId);
loadMessages(convId);
},
[loadMessages],
);
const handleSendMessage = useCallback(
async (text: string) => {
if (!activeConversationId) return;
try {
await sendMessage(activeConversationId, {
content: text,
content_format: 'text',
});
// The message will arrive via WebSocket, but also reload to be safe
loadMessages(activeConversationId);
} catch (e) {
console.error('Failed to send message:', e);
setError('Nachricht konnte nicht gesendet werden');
}
},
[activeConversationId, loadMessages],
);
const handleStartDirectChat = useCallback(
async (userId: string, userName: string) => {
try {
const conv = await createConversation({
title: userName,
participant_ids: [userId],
is_direct: true,
});
// Reload conversations to include the new one
await loadConversations();
if (conv && conv.id) {
handleSelectConversation(conv.id);
}
} catch (e) {
console.error('Failed to start direct chat:', e);
setError('Direkt-Chat konnte nicht gestartet werden');
}
},
[loadConversations, handleSelectConversation],
);
// Quick access buttons (same icons as AISidebar tabs)
const quickAccess: QuickAccessDef[] = [
{ key: 'assistant', label: 'Assistent', icon: robotIcon, testId: 'msg-sidebar-qa-assistant', view: 'conversations', filterPinned: 'assistant' },
{ key: 'liveai', label: 'Live KI', icon: bulbIcon, testId: 'msg-sidebar-qa-liveai', view: 'conversations', filterPinned: 'liveai' },
{ key: 'system', label: t('topbar.notifications'), icon: bellIcon, testId: 'msg-sidebar-qa-system', view: 'conversations', filterPinned: 'system' },
{ key: 'team', label: 'Team', icon: teamIcon, testId: 'msg-sidebar-qa-team', view: 'team' },
{ key: 'all', label: 'Chat', icon: chatBubbleIcon, testId: 'msg-sidebar-qa-all', view: 'conversations' },
];
// Pinned conversations (system rooms)
const pinnedConversations = conversations.filter((c) => c.is_pinned);
const normalConversations = conversations.filter((c) => !c.is_pinned);
// Active conversation object
const activeConv = conversations.find((c) => c.id === activeConversationId) || null;
const activeMessages = activeConversationId ? messages[activeConversationId] || [] : [];
const isSystemLocked =
activeConv?.is_locked && activeConv?.locked_by === 'system_notif';
// Determine which pinned conv to select when a quick-access button is clicked
const handleQuickAccess = (qa: QuickAccessDef) => {
setCurrentView(qa.view);
if (qa.view === 'conversations' && qa.filterPinned) {
// Find pinned conversation matching the filter by title
const filter = qa.filterPinned;
const titleMap: Record = {
assistant: ['Assistent', 'assistent', 'assistant'],
liveai: ['Live KI', 'live ki', 'liveai', 'proactive'],
system: ['System', 'system'],
};
const matchTitles = titleMap[filter] || [filter];
const match = pinnedConversations.find((c) =>
matchTitles.some((t) => c.title?.toLowerCase() === t.toLowerCase()),
);
if (match) {
handleSelectConversation(match.id);
return;
}
// Fallback: if no pinned match, just show conversations list
}
if (qa.view === 'conversations' && !qa.filterPinned) {
// 'all' — just show the list, select first if none active
if (!activeConversationId && conversations.length > 0) {
handleSelectConversation(conversations[0].id);
}
}
};
// ─── Collapsed: narrow icon strip (desktop only) ───
if (messageSidebarCollapsed) {
return (
{quickAccess.map((qa) => (
))}
);
}
// ─── Icon Strip (expanded) ───
const iconStrip = (
{quickAccess.map((qa) => {
const isActive =
(qa.view === 'conversations' && currentView === 'conversations') ||
(qa.view === 'team' && currentView === 'team');
return (
);
})}
);
// ─── Conversation List Panel ───
const renderConversationList = () => (
{loading && (
Laden...
)}
{error && (
{error}
)}
{!loading && conversations.length === 0 && !error && (
Keine Konversationen
)}
{/* Pinned conversations */}
{pinnedConversations.length > 0 && (
<>
{pinIcon} Angespinnt
{pinnedConversations.map((conv) => (
handleSelectConversation(conv.id)}
/>
))}
>
)}
{/* Normal conversations */}
{normalConversations.length > 0 && (
<>
Konversationen
{normalConversations.map((conv) => (
handleSelectConversation(conv.id)}
/>
))}
>
)}
);
// ─── Main Content Area ───
const renderContent = () => {
if (currentView === 'team') {
return ;
}
// conversations view: show conversation list + message feed + input
return (
{/* Conversation list (narrow, top section within panel) */}
{renderConversationList()}
{/* Message feed */}
{activeConv && (
{activeConv.is_pinned && 📌}
{activeConv.title || 'Ohne Titel'}
)}
{/* Message input (hidden for system-locked conversations) */}
{activeConv && !isSystemLocked && (
)}
{isSystemLocked && (
System-Konversation (schreibgeschützt)
)}
);
};
// ─── Expanded: Mobile = full width overlay, Desktop = resizable panel ───
return (
<>
{/* Mobile: full width overlay with back button */}
{iconStrip}
{renderContent()}
{/* Desktop: resizable panel */}
{iconStrip}
{renderContent()}
>
);
}