// web/src/components/LeftSidebar.tsx import React, { useMemo, useState, useEffect } from "react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Separator } from "./ui/separator"; import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react"; import type { SavedChat, Workspace } from "../App"; // 兼容 CourseDirectoryItem / CourseInfo 两种形态(字段可能不完全一致) type AnyCourse = { id?: string; courseId?: string; name?: string; title?: string; instructor?: { name?: string; email?: string }; teachingAssistant?: { name?: string; email?: string }; ta?: { name?: string; email?: string }; }; type Props = { savedChats: SavedChat[]; onLoadChat: (chat: SavedChat) => void; onDeleteSavedChat: (id: string) => void; onRenameSavedChat: (id: string, newTitle: string) => void; currentWorkspaceId: string; workspaces: Workspace[]; selectedCourse: string; availableCourses: AnyCourse[]; }; function formatSub(ts: any) { if (!ts) return ""; try { const d = typeof ts === "string" || typeof ts === "number" ? new Date(ts) : ts; return d.toLocaleString(); } catch { return ""; } } 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 getCourseId(c: AnyCourse) { return String(c.id ?? c.courseId ?? "").trim(); } function getCourseName(c: AnyCourse) { return String(c.name ?? c.title ?? "").trim(); } function normalize(s: string) { return s.trim().toLowerCase(); } export function LeftSidebar(props: Props) { const { savedChats, onLoadChat, onDeleteSavedChat, onRenameSavedChat, currentWorkspaceId, workspaces, selectedCourse, availableCourses, } = props; const [editingId, setEditingId] = useState(null); const [draftTitle, setDraftTitle] = useState(""); const currentWorkspace = useMemo( () => workspaces.find((w) => w.id === currentWorkspaceId), [workspaces, currentWorkspaceId] ); // Debug:确认这里是否真的在运行(开发态会看到 console) useEffect(() => { // eslint-disable-next-line no-console console.log("[LeftSidebar] render", { currentWorkspaceId, selectedCourse, availableCoursesLen: availableCourses?.length ?? 0, workspaceCourseInfo: (currentWorkspace as any)?.courseInfo, }); }, [currentWorkspaceId, selectedCourse, availableCourses, currentWorkspace]); /** * 选课命中策略: * 1) 优先 selectedCourse(My Space 的 source of truth) * 2) 再用 workspace.courseInfo(group workspace 兜底) * 3) 支持 id / courseId,name / title 多字段匹配 */ const courseInfo: AnyCourse | null = useMemo(() => { const sel = String(selectedCourse || "").trim(); if (sel) { // 先按 id/courseId 精确匹配 const byId = availableCourses.find((c) => getCourseId(c) === sel); if (byId) return byId; // 再按 name/title 精确匹配(有时 selectedCourse 传的是 name) const byName = availableCourses.find((c) => getCourseName(c) === sel); if (byName) return byName; // 最后做一个宽松匹配(小写) const selN = normalize(sel); const loose = availableCourses.find( (c) => normalize(getCourseId(c)) === selN || normalize(getCourseName(c)) === selN ); if (loose) return loose; } const wsCourse = (currentWorkspace as any)?.courseInfo as AnyCourse | undefined; if (wsCourse) { const wsId = getCourseId(wsCourse); if (wsId) { const hit = availableCourses.find((c) => getCourseId(c) === wsId); return hit ?? wsCourse; } const wsName = getCourseName(wsCourse); if (wsName) { const hit = availableCourses.find((c) => getCourseName(c) === wsName); return hit ?? wsCourse; } return wsCourse; } return null; }, [availableCourses, currentWorkspace, selectedCourse]); const sortedChats = useMemo(() => { return [...savedChats].sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); }, [savedChats]); const startRename = (chat: SavedChat) => { setEditingId(chat.id); setDraftTitle(chat.title || ""); }; const cancelRename = () => { setEditingId(null); setDraftTitle(""); }; const commitRename = (id: string) => { const next = draftTitle.trim(); if (!next) return; onRenameSavedChat(id, next); cancelRename(); }; // 兼容 TA 字段不同命名:teachingAssistant / ta const instructorName = courseInfo?.instructor?.name ?? "N/A"; const instructorEmail = courseInfo?.instructor?.email?.trim() || ""; const taObj = courseInfo?.teachingAssistant ?? courseInfo?.ta; const taName = taObj?.name ?? "N/A"; const taEmail = taObj?.email?.trim() || ""; const courseTitle = getCourseName(courseInfo ?? {}) || "(No course matched)"; return (
{/* === 强制可见:用来确认你改的文件生效 === */}
LEFTSIDEBAR ACTIVE
{/* ================= Course Info(不滚动) ================= */}
{courseTitle}
{/* 如果没命中 courseInfo,直接把原因展示出来(这才是你现在缺的可观测性) */} {!courseInfo && (
courseInfo not found.
selectedCourse: {String(selectedCourse || "")}
availableCourses: {availableCourses?.length ?? 0}
currentWorkspaceId: {currentWorkspaceId}
)}
Instructor:  {instructorEmail ? ( {instructorName} ) : ( {instructorName} )}
TA:  {taEmail ? ( {taName} ) : ( {taName} )}
{/* ✅ Info / Saved Chat 分割线:固定 #ECECF1 */} {/* ================= Saved Chat Header(不滚动) ================= */}

Saved Chat

{/* ================= Saved Chat List(唯一滚动区) ================= */} {/* 用你 CSS 里的 panelScroll,保证滚动隔离生效 */}
{sortedChats.length === 0 ? (
No saved chats yet
Save conversations to view them here
) : ( sortedChats.map((chat) => { const isEditing = editingId === chat.id; const sub = formatSub(chat.timestamp); return (
{isEditing ? (
setDraftTitle(e.target.value)} autoFocus onKeyDown={(e) => { if (e.key === "Enter") commitRename(chat.id); if (e.key === "Escape") cancelRename(); }} />
) : ( )}
{!isEditing && (
)}
); }) )}
); }