SarahXia0405 commited on
Commit
2863b71
·
verified ·
1 Parent(s): b52a2ce

Update web/src/components/LeftSidebar.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/LeftSidebar.tsx +149 -83
web/src/components/LeftSidebar.tsx CHANGED
@@ -1,5 +1,5 @@
1
  // web/src/components/LeftSidebar.tsx
2
- import React, { useMemo, useState } from "react";
3
  import { Button } from "./ui/button";
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
@@ -7,13 +7,15 @@ import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
7
 
8
  import type { SavedChat, Workspace } from "../App";
9
 
10
- // 你项目里可能有 courseDirectory,也可能直接从 App availableCoursesCourseInfo[]
11
- // 所以这里不强依赖 CourseDirectoryItem 结构,做宽松兼容。
12
  type AnyCourse = {
13
- id: string;
14
- name: string;
15
- instructor?: { name: string; email: string };
16
- teachingAssistant?: { name: string; email: string };
 
 
 
17
  };
18
 
19
  type Props = {
@@ -25,10 +27,7 @@ type Props = {
25
  currentWorkspaceId: string;
26
  workspaces: Workspace[];
27
 
28
- // ✅ 当前选中的课程(My Space 用它)
29
  selectedCourse: string;
30
-
31
- // ✅ 允许两种来源:CourseInfo[] 或 CourseDirectoryItem[]
32
  availableCourses: AnyCourse[];
33
  };
34
 
@@ -49,16 +48,30 @@ function gmailComposeLink(email: string, subject?: string, body?: string) {
49
  return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`;
50
  }
51
 
52
- export function LeftSidebar({
53
- savedChats,
54
- onLoadChat,
55
- onDeleteSavedChat,
56
- onRenameSavedChat,
57
- currentWorkspaceId,
58
- workspaces,
59
- selectedCourse,
60
- availableCourses,
61
- }: Props) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  const [editingId, setEditingId] = useState<string | null>(null);
63
  const [draftTitle, setDraftTitle] = useState("");
64
 
@@ -67,27 +80,56 @@ export function LeftSidebar({
67
  [workspaces, currentWorkspaceId]
68
  );
69
 
 
 
 
 
 
 
 
 
 
 
 
70
  /**
71
- * ✅ 最关键courseInfo 一定要能在 My Space 下拿到
72
- * 优先使用 selectedCourse(你 App 里 currentCourseId),这才是 My Space 的 source of truth
73
- * group workspace 再用 workspace.courseInfo 兜底
 
74
  */
75
  const courseInfo: AnyCourse | null = useMemo(() => {
76
- const selId = (selectedCourse || "").trim();
77
- if (selId) {
78
- const hit = availableCourses.find((c) => String(c.id) === selId);
79
- if (hit) return hit;
80
- }
81
 
82
- const wsCourse = (currentWorkspace as any)?.courseInfo as AnyCourse | undefined;
83
- if (wsCourse?.id) {
84
- const hit = availableCourses.find((c) => String(c.id) === String(wsCourse.id));
85
- return hit ?? wsCourse;
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
 
88
- if (wsCourse?.name) {
89
- const hit = availableCourses.find((c) => c.name === wsCourse.name);
90
- return hit ?? wsCourse;
 
 
 
 
 
 
 
 
 
 
91
  }
92
 
93
  return null;
@@ -116,60 +158,85 @@ export function LeftSidebar({
116
  cancelRename();
117
  };
118
 
119
- // ===== Display fields (safe) =====
120
  const instructorName = courseInfo?.instructor?.name ?? "N/A";
121
  const instructorEmail = courseInfo?.instructor?.email?.trim() || "";
122
- const taName = courseInfo?.teachingAssistant?.name ?? "N/A";
123
- const taEmail = courseInfo?.teachingAssistant?.email?.trim() || "";
 
 
 
 
124
 
125
  return (
126
  <div className="h-full w-full flex flex-col min-h-0">
 
 
 
 
 
127
  {/* ================= Course Info(不滚动) ================= */}
128
- {courseInfo ? (
129
- <div className="px-4 pt-4 pb-3 flex-shrink-0 space-y-2">
130
- <div className="font-semibold text-base">{courseInfo.name}</div>
131
-
132
- <div className="text-sm text-muted-foreground">
133
- Instructor:&nbsp;
134
- {instructorEmail ? (
135
- <a
136
- href={gmailComposeLink(
137
- instructorEmail,
138
- `[Clare] Question about ${courseInfo.name}`,
139
- `Hi ${instructorName},\n\nI have a question about ${courseInfo.name}:\n\n(Write your question here)\n\nThanks,\n`
140
- )}
141
- target="_blank"
142
- rel="noopener noreferrer"
143
- className="text-primary hover:underline"
144
- >
145
- {instructorName}
146
- </a>
147
- ) : (
148
- <span className="text-muted-foreground/60">{instructorName}</span>
149
- )}
150
- </div>
151
 
152
- <div className="text-sm text-muted-foreground">
153
- TA:&nbsp;
154
- {taEmail ? (
155
- <a
156
- href={gmailComposeLink(
157
- taEmail,
158
- `[Clare] Help request for ${courseInfo.name}`,
159
- `Hi ${taName},\n\nI need help with ${courseInfo.name}:\n\n(Write your question here)\n\nThanks,\n`
160
- )}
161
- target="_blank"
162
- rel="noopener noreferrer"
163
- className="text-primary hover:underline"
164
- >
165
- {taName}
166
- </a>
167
- ) : (
168
- <span className="text-muted-foreground/60">{taName}</span>
169
- )}
170
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
172
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  {/* ✅ Info / Saved Chat 分割线:固定 #ECECF1 */}
175
  <Separator className="flex-shrink-0 bg-[#ECECF1]" />
@@ -183,7 +250,8 @@ export function LeftSidebar({
183
  <Separator className="flex-shrink-0" />
184
 
185
  {/* ================= Saved Chat List(唯一滚动区) ================= */}
186
- <div className="flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3">
 
187
  {sortedChats.length === 0 ? (
188
  <div className="text-sm text-muted-foreground text-center py-10">
189
  No saved chats yet
@@ -198,7 +266,6 @@ export function LeftSidebar({
198
  return (
199
  <div key={chat.id} className="rounded-xl border bg-card px-4 py-3">
200
  <div className="flex items-start justify-between gap-3">
201
- {/* 左侧 */}
202
  <div className="flex-1 min-w-0">
203
  {isEditing ? (
204
  <div className="space-y-2">
@@ -228,7 +295,6 @@ export function LeftSidebar({
228
  )}
229
  </div>
230
 
231
- {/* 右侧按钮 */}
232
  {!isEditing && (
233
  <div className="flex gap-2 flex-shrink-0">
234
  <Button variant="ghost" size="icon" onClick={() => startRename(chat)}>
 
1
  // web/src/components/LeftSidebar.tsx
2
+ import React, { useMemo, useState, useEffect } from "react";
3
  import { Button } from "./ui/button";
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
 
7
 
8
  import type { SavedChat, Workspace } from "../App";
9
 
10
+ // 兼容 CourseDirectoryItem / CourseInfo 两种形态字段可能不完全一致
 
11
  type AnyCourse = {
12
+ id?: string;
13
+ courseId?: string;
14
+ name?: string;
15
+ title?: string;
16
+ instructor?: { name?: string; email?: string };
17
+ teachingAssistant?: { name?: string; email?: string };
18
+ ta?: { name?: string; email?: string };
19
  };
20
 
21
  type Props = {
 
27
  currentWorkspaceId: string;
28
  workspaces: Workspace[];
29
 
 
30
  selectedCourse: string;
 
 
31
  availableCourses: AnyCourse[];
32
  };
33
 
 
48
  return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`;
49
  }
50
 
51
+ function getCourseId(c: AnyCourse) {
52
+ return String(c.id ?? c.courseId ?? "").trim();
53
+ }
54
+
55
+ function getCourseName(c: AnyCourse) {
56
+ return String(c.name ?? c.title ?? "").trim();
57
+ }
58
+
59
+ function normalize(s: string) {
60
+ return s.trim().toLowerCase();
61
+ }
62
+
63
+ export function LeftSidebar(props: Props) {
64
+ const {
65
+ savedChats,
66
+ onLoadChat,
67
+ onDeleteSavedChat,
68
+ onRenameSavedChat,
69
+ currentWorkspaceId,
70
+ workspaces,
71
+ selectedCourse,
72
+ availableCourses,
73
+ } = props;
74
+
75
  const [editingId, setEditingId] = useState<string | null>(null);
76
  const [draftTitle, setDraftTitle] = useState("");
77
 
 
80
  [workspaces, currentWorkspaceId]
81
  );
82
 
83
+ // Debug:确认这里是否真的在运行(开发态会看到 console)
84
+ useEffect(() => {
85
+ // eslint-disable-next-line no-console
86
+ console.log("[LeftSidebar] render", {
87
+ currentWorkspaceId,
88
+ selectedCourse,
89
+ availableCoursesLen: availableCourses?.length ?? 0,
90
+ workspaceCourseInfo: (currentWorkspace as any)?.courseInfo,
91
+ });
92
+ }, [currentWorkspaceId, selectedCourse, availableCourses, currentWorkspace]);
93
+
94
  /**
95
+ * 选课命中策略
96
+ * 1) 优先 selectedCourse(My Space 的 source of truth
97
+ * 2) 再用 workspace.courseInfo(group workspace 兜底
98
+ * 3) 支持 id / courseId,name / title 多字段匹配
99
  */
100
  const courseInfo: AnyCourse | null = useMemo(() => {
101
+ const sel = String(selectedCourse || "").trim();
 
 
 
 
102
 
103
+ if (sel) {
104
+ // 先按 id/courseId 精确匹配
105
+ const byId = availableCourses.find((c) => getCourseId(c) === sel);
106
+ if (byId) return byId;
107
+
108
+ // 再按 name/title 精确匹配(有时 selectedCourse 传的是 name)
109
+ const byName = availableCourses.find((c) => getCourseName(c) === sel);
110
+ if (byName) return byName;
111
+
112
+ // 最后做一个宽松匹配(小写)
113
+ const selN = normalize(sel);
114
+ const loose = availableCourses.find(
115
+ (c) => normalize(getCourseId(c)) === selN || normalize(getCourseName(c)) === selN
116
+ );
117
+ if (loose) return loose;
118
  }
119
 
120
+ const wsCourse = (currentWorkspace as any)?.courseInfo as AnyCourse | undefined;
121
+ if (wsCourse) {
122
+ const wsId = getCourseId(wsCourse);
123
+ if (wsId) {
124
+ const hit = availableCourses.find((c) => getCourseId(c) === wsId);
125
+ return hit ?? wsCourse;
126
+ }
127
+ const wsName = getCourseName(wsCourse);
128
+ if (wsName) {
129
+ const hit = availableCourses.find((c) => getCourseName(c) === wsName);
130
+ return hit ?? wsCourse;
131
+ }
132
+ return wsCourse;
133
  }
134
 
135
  return null;
 
158
  cancelRename();
159
  };
160
 
161
+ // 兼容 TA 字段不同命名:teachingAssistant / ta
162
  const instructorName = courseInfo?.instructor?.name ?? "N/A";
163
  const instructorEmail = courseInfo?.instructor?.email?.trim() || "";
164
+
165
+ const taObj = courseInfo?.teachingAssistant ?? courseInfo?.ta;
166
+ const taName = taObj?.name ?? "N/A";
167
+ const taEmail = taObj?.email?.trim() || "";
168
+
169
+ const courseTitle = getCourseName(courseInfo ?? {}) || "(No course matched)";
170
 
171
  return (
172
  <div className="h-full w-full flex flex-col min-h-0">
173
+ {/* === 强制可见:用来确认你改的文件生效 === */}
174
+ <div className="px-4 pt-2 text-xs text-red-600 flex-shrink-0">
175
+ LEFTSIDEBAR ACTIVE
176
+ </div>
177
+
178
  {/* ================= Course Info(不滚动) ================= */}
179
+ <div className="px-4 pt-3 pb-3 flex-shrink-0 space-y-2">
180
+ <div className="font-semibold text-base">{courseTitle}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ {/* 如果没命中 courseInfo,直接把原因展示出来(这才是你现在缺的可观测性) */}
183
+ {!courseInfo && (
184
+ <div className="text-xs text-muted-foreground space-y-1">
185
+ <div>
186
+ courseInfo not found.
187
+ </div>
188
+ <div>
189
+ selectedCourse: <span className="font-medium">{String(selectedCourse || "")}</span>
190
+ </div>
191
+ <div>
192
+ availableCourses: <span className="font-medium">{availableCourses?.length ?? 0}</span>
193
+ </div>
194
+ <div>
195
+ currentWorkspaceId: <span className="font-medium">{currentWorkspaceId}</span>
196
+ </div>
 
 
 
197
  </div>
198
+ )}
199
+
200
+ <div className="text-sm text-muted-foreground">
201
+ Instructor:&nbsp;
202
+ {instructorEmail ? (
203
+ <a
204
+ href={gmailComposeLink(
205
+ instructorEmail,
206
+ `[Clare] Question about ${courseTitle}`,
207
+ `Hi ${instructorName},\n\nI have a question about ${courseTitle}:\n\n(Write your question here)\n\nThanks,\n`
208
+ )}
209
+ target="_blank"
210
+ rel="noopener noreferrer"
211
+ className="text-primary hover:underline"
212
+ >
213
+ {instructorName}
214
+ </a>
215
+ ) : (
216
+ <span className="text-muted-foreground/60">{instructorName}</span>
217
+ )}
218
  </div>
219
+
220
+ <div className="text-sm text-muted-foreground">
221
+ TA:&nbsp;
222
+ {taEmail ? (
223
+ <a
224
+ href={gmailComposeLink(
225
+ taEmail,
226
+ `[Clare] Help request for ${courseTitle}`,
227
+ `Hi ${taName},\n\nI need help with ${courseTitle}:\n\n(Write your question here)\n\nThanks,\n`
228
+ )}
229
+ target="_blank"
230
+ rel="noopener noreferrer"
231
+ className="text-primary hover:underline"
232
+ >
233
+ {taName}
234
+ </a>
235
+ ) : (
236
+ <span className="text-muted-foreground/60">{taName}</span>
237
+ )}
238
+ </div>
239
+ </div>
240
 
241
  {/* ✅ Info / Saved Chat 分割线:固定 #ECECF1 */}
242
  <Separator className="flex-shrink-0 bg-[#ECECF1]" />
 
250
  <Separator className="flex-shrink-0" />
251
 
252
  {/* ================= Saved Chat List(唯一滚动区) ================= */}
253
+ {/* 用你 CSS 里的 panelScroll,保证滚动隔离生效 */}
254
+ <div className="flex-1 min-h-0 px-4 py-3 space-y-3 panelScroll">
255
  {sortedChats.length === 0 ? (
256
  <div className="text-sm text-muted-foreground text-center py-10">
257
  No saved chats yet
 
266
  return (
267
  <div key={chat.id} className="rounded-xl border bg-card px-4 py-3">
268
  <div className="flex items-start justify-between gap-3">
 
269
  <div className="flex-1 min-w-0">
270
  {isEditing ? (
271
  <div className="space-y-2">
 
295
  )}
296
  </div>
297
 
 
298
  {!isEditing && (
299
  <div className="flex gap-2 flex-shrink-0">
300
  <Button variant="ghost" size="icon" onClick={() => startRename(chat)}>