// web/src/components/sidebar/LeftSidebar.tsx import React, { useEffect, useMemo, useState } from "react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Badge } from "../ui/badge"; import { SavedChatSection } from "./SavedChatSection"; import { Pencil, Check, X, MailPlus, Users, GraduationCap } from "lucide-react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog"; import type { SavedChat, Workspace, LearningMode, Language, SpaceType, GroupMember, User, SavedItem, } from "../../App"; import type { CourseDirectoryItem } from "../../lib/courseDirectory"; import clareAvatar from "../../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png"; // ✅ Theme-compatible divider (light/dark) + visibility const Divider = () => (
); type Props = { learningMode: LearningMode; language: Language; onLearningModeChange: (m: LearningMode) => void; onLanguageChange: (l: Language) => void; spaceType: SpaceType; groupMembers: GroupMember[]; user: User | null; onLogin: (u: any) => void; onLogout: () => void; isLoggedIn: boolean; onEditProfile: () => void; savedItems: SavedItem[]; recentlySavedId: string | null; onUnsave: (id: string) => void; onSave: ( content: string, type: "export" | "quiz" | "summary", saveAsChat?: boolean, format?: "pdf" | "text", workspaceId?: string ) => void; savedChats: SavedChat[]; onLoadChat: (chat: SavedChat) => void; onDeleteSavedChat: (id: string) => void; onRenameSavedChat: (id: string, newTitle: string) => void; currentWorkspaceId: string; workspaces: Workspace[]; selectedCourse: string; availableCourses: CourseDirectoryItem[]; // optional if you already wired backend onRenameGroupName?: (workspaceId: string, newName: string) => Promise | void; onRenameGroupNo?: (workspaceId: string, newNo: number) => Promise | void; }; function gmailComposeLink(email: string, subject?: string, body?: string) { const to = `to=${encodeURIComponent(email)}`; const su = subject ? `&su=${encodeURIComponent(subject)}` : ""; const bd = body ? `&body=${encodeURIComponent(body)}` : ""; return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`; } function norm(s: any) { return String(s ?? "").trim().toLowerCase(); } function pickAny(obj: any, keys: string[]) { for (const k of keys) { const v = obj?.[k]; if (v !== undefined && v !== null && String(v).trim() !== "") return v; } return undefined; } function toIntOrFallback(v: any, fb: number) { const n = Number(v); if (Number.isFinite(n) && n > 0) return Math.floor(n); return fb; } export function LeftSidebar(props: Props) { const { user, isLoggedIn, spaceType, groupMembers, savedChats, onLoadChat, onDeleteSavedChat, onRenameSavedChat, currentWorkspaceId, workspaces, selectedCourse, availableCourses, onRenameGroupName, onRenameGroupNo, } = props; const currentWorkspace = useMemo( () => workspaces.find((w) => w.id === currentWorkspaceId), [workspaces, currentWorkspaceId] ); const isMyWorkspace = useMemo(() => { const ws: any = currentWorkspace as any; if (ws?.isPersonal === true) return true; if (String(ws?.spaceType || "").toLowerCase().includes("my")) return true; if (String(ws?.type || "").toLowerCase().includes("my")) return true; if (String(ws?.kind || "").toLowerCase().includes("my")) return true; const id = String(ws?.id || "").toLowerCase(); const name = String(ws?.name || "").toLowerCase(); if (id.includes("my") || id.includes("personal")) return true; if (name.includes("my space") || name.includes("personal")) return true; return false; }, [currentWorkspace]); const isTeamSpace = useMemo(() => { if (isMyWorkspace) return false; const st = String(spaceType || "").toLowerCase(); return st === "group" || st === "team"; }, [spaceType, isMyWorkspace]); // --------- CourseInfo resolution --------- const courseInfo = useMemo((): CourseDirectoryItem | null => { const list = Array.isArray(availableCourses) ? availableCourses : []; const selRaw = (selectedCourse || "").trim(); const sel = norm(selRaw); if (sel) { const hit = list.find((c: any) => norm(c.id) === sel) || list.find((c: any) => norm(c.name) === sel); if (hit) return hit as any; } const wsCourse = (currentWorkspace as any)?.courseInfo as | { id?: string; name?: string; instructor?: any; teachingAssistant?: any } | undefined; const wsId = norm(wsCourse?.id); if (wsId) return (list.find((c: any) => norm(c.id) === wsId) ?? (wsCourse as any)) as any; const wsName = norm(wsCourse?.name); if (wsName) return (list.find((c: any) => norm(c.name) === wsName) ?? (wsCourse as any)) as any; return null; }, [availableCourses, currentWorkspace, selectedCourse]); const courseName = useMemo(() => { return (courseInfo as any)?.name ?? (selectedCourse || "Course"); }, [courseInfo, selectedCourse]); // --------- Group fields --------- const wsGroupNo = useMemo(() => { const ws: any = currentWorkspace as any; return ws?.groupNo ?? ws?.groupNumber ?? ws?.groupIndex ?? ws?.group_id ?? ws?.groupId ?? 1; }, [currentWorkspace]); const wsGroupName = useMemo(() => { const ws: any = currentWorkspace as any; return pickAny(ws, ["groupName", "name", "title"]) || "My Group"; }, [currentWorkspace]); // --------- Demo group mapping (My Space only) --------- const demoGroupMap: Record = useMemo( () => ({ "Introduction to AI": { name: "My Space", no: 1 }, "Machine Learning": { name: "Study Sprint", no: 3 }, "Data Visualization": { name: "Design Lab", no: 2 }, }), [] ); const demoGroup = useMemo(() => { const hit = demoGroupMap[courseName]; if (hit) return hit; return { name: wsGroupName || "My Group", no: toIntOrFallback(wsGroupNo, 1), }; }, [demoGroupMap, courseName, wsGroupName, wsGroupNo]); const memberCount = (groupMembers || []).length; // localStorage keys (Team space editing) const groupNameStorageKey = useMemo( () => `clare_group_name__${currentWorkspaceId}`, [currentWorkspaceId] ); const groupNoStorageKey = useMemo( () => `clare_group_no__${currentWorkspaceId}`, [currentWorkspaceId] ); // group name (Team space editable) const [groupName, setGroupName] = useState(wsGroupName); const [editingGroupName, setEditingGroupName] = useState(false); const [draftGroupName, setDraftGroupName] = useState(wsGroupName); // group no (Team space editable) const [groupNo, setGroupNo] = useState(toIntOrFallback(wsGroupNo, 1)); const [editingGroupNo, setEditingGroupNo] = useState(false); const [draftGroupNo, setDraftGroupNo] = useState(String(toIntOrFallback(wsGroupNo, 1))); // Invite dialog state const [inviteOpen, setInviteOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); const handleSendInvite = () => { if (!inviteEmail.trim()) { toast.error("Please enter an email to invite"); return; } toast.success(`Invitation sent to ${inviteEmail}`); setInviteEmail(""); setInviteOpen(false); }; // hydrate persisted editable values (Team space) useEffect(() => { const storedName = typeof window !== "undefined" ? window.localStorage.getItem(groupNameStorageKey) : null; const name = storedName && storedName.trim() ? storedName : wsGroupName; setGroupName(name); setDraftGroupName(name); setEditingGroupName(false); const storedNo = typeof window !== "undefined" ? window.localStorage.getItem(groupNoStorageKey) : null; const no = storedNo && storedNo.trim() ? toIntOrFallback(storedNo, toIntOrFallback(wsGroupNo, 1)) : toIntOrFallback(wsGroupNo, 1); setGroupNo(no); setDraftGroupNo(String(no)); setEditingGroupNo(false); }, [groupNameStorageKey, groupNoStorageKey, wsGroupName, wsGroupNo]); const saveGroupName = async () => { const next = (draftGroupName || "").trim() || "My Group"; setGroupName(next); setDraftGroupName(next); setEditingGroupName(false); if (onRenameGroupName) { try { await onRenameGroupName(currentWorkspaceId, next); return; } catch {} } try { window.localStorage.setItem(groupNameStorageKey, next); } catch {} }; const cancelGroupName = () => { setDraftGroupName(groupName); setEditingGroupName(false); }; const saveGroupNo = async () => { const nextNo = toIntOrFallback(draftGroupNo, groupNo); setGroupNo(nextNo); setDraftGroupNo(String(nextNo)); setEditingGroupNo(false); if (onRenameGroupNo) { try { await onRenameGroupNo(currentWorkspaceId, nextNo); return; } catch {} } try { window.localStorage.setItem(groupNoStorageKey, String(nextNo)); } catch {} }; const cancelGroupNo = () => { setDraftGroupNo(String(groupNo)); setEditingGroupNo(false); }; // --------- Contacts --------- const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A"; const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim(); const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A"; const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim(); return (
{/* ================= TOP (non-scroll) ================= */}
{/* ✅ removed Welcome section entirely */}
{/* Course + Group */}
{/* Course row with icon (single-color, no container/background) */}
{courseName}
{/* ===== My Space (beautified) ===== */} {!isTeamSpace ? (
Group Name: {demoGroup.name}
Group Number: {demoGroup.no}
) : ( /* ===== Team/Group (editable card) ===== */
{/* Line 1: group name editable */} {!editingGroupName ? (
{groupName || "My Group"}
) : (
setDraftGroupName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveGroupName(); if (e.key === "Escape") cancelGroupName(); }} className="h-8 w-[220px]" autoFocus />
)} {/* Line 2: Group {no}(✏️) ({count}) + Invite */}
{!editingGroupNo ? (
Group{" "} {groupNo} {" "} ({memberCount})
) : (
Group
setDraftGroupNo(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveGroupNo(); if (e.key === "Escape") cancelGroupNo(); }} className="h-8 w-[80px]" autoFocus />
({memberCount})
)}
{(groupMembers || []).map((member) => { const isAI = !!(member as any).isAI; const name = String((member as any).name || ""); const email = String((member as any).email || ""); const initials = name ? name .split(" ") .filter(Boolean) .map((n) => n[0]) .join("") .toUpperCase() : "?"; return (
{isAI ? ( Clare ) : ( {initials} )}

{name}

{isAI && ( AI )}

{email}

); })}
)}
{/* ================= MIDDLE (only scroll) ================= */}
{/* ================= BOTTOM (fixed, non-scroll) ================= */}
Instructor:  {instructorEmail ? ( {instructorName} ) : ( {instructorName} )}
TA:  {taEmail ? ( {taName} ) : ( {taName} )}
{/* Invite Dialog */} Invite member Send a quick email invite with the team details.
setInviteEmail(e.target.value)} />

An invitation email with a join link will be sent to this address.

); }