+ {messages.map((message, index) => {
+ const isCurrentUser = message.user_id === currentUserId;
+ const showAvatar = index === 0 || messages[index - 1].user_id !== message.user_id;
+ const isAI = message.is_ai_response;
+
+ return (
+
+ {showAvatar ? (
+
+ {isAI ? (
+
+
+
+ ) : (
+
+ {message.user?.name.charAt(0).toUpperCase()}
+
+ )}
+
+ ) : (
+
+ )}
+
+
+ {showAvatar && (
+
+
+ {isAI ? 'Grimlock AI' : message.user?.name}
+
+
+ {formatTime(message.created_at)}
+
+ {!isAI && message.user?.role && (
+
+ {message.user.role}
+
+ )}
+
+ )}
+
+ {message.content}
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/src/components/sidebar/Sidebar.tsx b/frontend/src/components/sidebar/Sidebar.tsx
new file mode 100644
index 0000000..95d1b20
--- /dev/null
+++ b/frontend/src/components/sidebar/Sidebar.tsx
@@ -0,0 +1,166 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { Hash, Plus, MessageSquare, Settings, LogOut, ChevronDown, ChevronRight } from 'lucide-react';
+import { useAuthStore } from '@/stores/useAuthStore';
+import { useChannelStore } from '@/stores/useChannelStore';
+import { useMessageStore } from '@/stores/useMessageStore';
+
+export default function Sidebar() {
+ const router = useRouter();
+ const { user, logout } = useAuthStore();
+ const { channels, createChannel } = useChannelStore();
+ const { conversations } = useMessageStore();
+
+ const [showChannels, setShowChannels] = useState(true);
+ const [showDMs, setShowDMs] = useState(true);
+ const [isCreating, setIsCreating] = useState(false);
+
+ const handleCreateChannel = async () => {
+ const name = prompt('Channel name:');
+ if (!name) return;
+
+ const description = prompt('Description (optional):');
+ const isPrivate = confirm('Make this channel private?');
+
+ try {
+ const channel = await createChannel({ name, description: description || undefined, is_private: isPrivate });
+ router.push(`/channels/${channel.id}`);
+ } catch (error) {
+ alert('Failed to create channel');
+ }
+ };
+
+ const handleLogout = async () => {
+ await logout();
+ router.push('/login');
+ };
+
+ return (
+
+ {/* Header */}
+
+
Grimlock
+
+
+
+ {/* Scrollable content */}
+
+ {/* Channels Section */}
+
+
+
+ {showChannels && (
+
+ {channels.map((channel) => (
+
router.push(`/channels/${channel.id}`)}
+ className="w-full flex items-center gap-2 px-4 py-1.5 hover:bg-gray-700 rounded text-sm group"
+ >
+
+ {channel.name}
+ {channel.member_count && (
+ {channel.member_count}
+ )}
+
+ ))}
+ {channels.length === 0 && (
+
+ No channels yet
+
+ )}
+
+ )}
+
+
+ {/* Direct Messages Section */}
+
+
setShowDMs(!showDMs)}
+ className="w-full flex items-center justify-between px-2 py-1 hover:bg-gray-700 rounded text-sm"
+ >
+
+ {showDMs ? : }
+ Direct Messages
+
+
+
+ {showDMs && (
+
+ {conversations.map((conv) => (
+
router.push(`/dms/${conv.user.id}`)}
+ className="w-full flex items-center gap-2 px-4 py-1.5 hover:bg-gray-700 rounded text-sm group"
+ >
+
+
+ {conv.user.is_online && (
+
+ )}
+
+ {conv.user.name}
+ {conv.unread_count > 0 && (
+
+ {conv.unread_count}
+
+ )}
+
+ ))}
+ {conversations.length === 0 && (
+
+ No conversations yet
+
+ )}
+
+ )}
+
+
+
+ {/* User info footer */}
+
+
+
+ {user?.name.charAt(0).toUpperCase()}
+
+
+
{user?.name}
+
{user?.role}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/stores/useAuthStore.ts b/frontend/src/stores/useAuthStore.ts
new file mode 100644
index 0000000..1ff009c
--- /dev/null
+++ b/frontend/src/stores/useAuthStore.ts
@@ -0,0 +1,102 @@
+import { create } from 'zustand';
+import { apiClient } from '@/lib/api';
+import { wsClient } from '@/lib/socket';
+import type { User, LoginRequest, RegisterRequest } from '@/lib/types';
+
+interface AuthState {
+ user: User | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ error: string | null;
+
+ login: (data: LoginRequest) => Promise