// 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) */}
{/* ===== 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 ? (

) : (
{initials}
)}
);
})}
)}
{/* ================= MIDDLE (only scroll) ================= */}
{/* ================= BOTTOM (fixed, non-scroll) ================= */}
TA:
{taEmail ? (
{taName}
) : (
{taName}
)}
{/* Invite Dialog */}
);
}