SarahXia0405 commited on
Commit
e0b5fb0
·
verified ·
1 Parent(s): 0698e11

Update web/src/components/LeftSidebar.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/LeftSidebar.tsx +101 -54
web/src/components/LeftSidebar.tsx CHANGED
@@ -4,11 +4,8 @@ import { Input } from "./ui/input";
4
  import { Separator } from "./ui/separator";
5
  import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
6
 
7
- import type {
8
- SavedChat,
9
- Workspace,
10
- CourseInfo,
11
- } from "../App";
12
 
13
  type Props = {
14
  savedChats: SavedChat[];
@@ -18,7 +15,9 @@ type Props = {
18
 
19
  currentWorkspaceId: string;
20
  workspaces: Workspace[];
21
- availableCourses: CourseInfo[];
 
 
22
  };
23
 
24
  function formatSub(ts: any) {
@@ -31,6 +30,13 @@ function formatSub(ts: any) {
31
  }
32
  }
33
 
 
 
 
 
 
 
 
34
  export function LeftSidebar({
35
  savedChats,
36
  onLoadChat,
@@ -38,17 +44,51 @@ export function LeftSidebar({
38
  onRenameSavedChat,
39
  currentWorkspaceId,
40
  workspaces,
 
41
  }: Props) {
42
  const [editingId, setEditingId] = useState<string | null>(null);
43
  const [draftTitle, setDraftTitle] = useState("");
44
 
45
  /** ===== 当前 workspace 对应的 course info ===== */
46
- const currentWorkspace = workspaces.find(w => w.id === currentWorkspaceId);
47
- const courseInfo = currentWorkspace?.courseInfo;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  const sortedChats = useMemo(() => {
50
- return [...savedChats].sort((a, b) =>
51
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
52
  );
53
  }, [savedChats]);
54
 
@@ -69,40 +109,63 @@ export function LeftSidebar({
69
  cancelRename();
70
  };
71
 
 
 
 
 
 
 
72
  return (
73
  <div className="h-full w-full flex flex-col min-h-0">
74
-
75
  {/* ================= Course Info(不滚动) ================= */}
76
  {courseInfo && (
77
  <div className="px-4 pt-4 pb-3 flex-shrink-0 space-y-2">
78
- <div className="font-semibold text-base">
79
- {courseInfo.name}
80
- </div>
81
 
82
  <div className="text-sm text-muted-foreground">
83
  Instructor:&nbsp;
84
- <a
85
- href={`mailto:${courseInfo.instructor.email}`}
86
- className="text-primary hover:underline"
87
- >
88
- {courseInfo.instructor.name}
89
- </a>
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
 
92
  <div className="text-sm text-muted-foreground">
93
  TA:&nbsp;
94
- <a
95
- href={`mailto:${courseInfo.teachingAssistant.email}`}
96
- className="text-primary hover:underline"
97
- >
98
- {courseInfo.teachingAssistant.name}
99
- </a>
 
 
 
 
 
 
 
 
 
 
100
  </div>
101
  </div>
102
  )}
103
 
104
- {/* 🔴 关键分割线:Info / Saved Chat */}
105
- <Separator className="flex-shrink-0" />
106
 
107
  {/* ================= Saved Chat Header(不滚动) ================= */}
108
  <div className="px-4 pt-4 pb-2 flex items-center gap-2 flex-shrink-0">
@@ -116,30 +179,27 @@ export function LeftSidebar({
116
  <div className="flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3">
117
  {sortedChats.length === 0 ? (
118
  <div className="text-sm text-muted-foreground text-center py-10">
119
- No saved chats yet<br />
 
120
  <span className="text-xs">Save conversations to view them here</span>
121
  </div>
122
  ) : (
123
- sortedChats.map(chat => {
124
  const isEditing = editingId === chat.id;
125
  const sub = formatSub(chat.timestamp);
126
 
127
  return (
128
- <div
129
- key={chat.id}
130
- className="rounded-xl border bg-card px-4 py-3"
131
- >
132
  <div className="flex items-start justify-between gap-3">
133
-
134
  {/* 左侧 */}
135
  <div className="flex-1 min-w-0">
136
  {isEditing ? (
137
  <div className="space-y-2">
138
  <Input
139
  value={draftTitle}
140
- onChange={e => setDraftTitle(e.target.value)}
141
  autoFocus
142
- onKeyDown={e => {
143
  if (e.key === "Enter") commitRename(chat.id);
144
  if (e.key === "Escape") cancelRename();
145
  }}
@@ -154,18 +214,9 @@ export function LeftSidebar({
154
  </div>
155
  </div>
156
  ) : (
157
- <button
158
- className="text-left w-full"
159
- onClick={() => onLoadChat(chat)}
160
- >
161
- <div className="font-medium truncate">
162
- {chat.title || "Untitled chat"}
163
- </div>
164
- {sub && (
165
- <div className="text-xs text-muted-foreground mt-1">
166
- {sub}
167
- </div>
168
- )}
169
  </button>
170
  )}
171
  </div>
@@ -173,11 +224,7 @@ export function LeftSidebar({
173
  {/* 右侧按钮 */}
174
  {!isEditing && (
175
  <div className="flex gap-2 flex-shrink-0">
176
- <Button
177
- variant="ghost"
178
- size="icon"
179
- onClick={() => startRename(chat)}
180
- >
181
  <Edit2 className="h-4 w-4" />
182
  </Button>
183
  <Button
 
4
  import { Separator } from "./ui/separator";
5
  import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
6
 
7
+ import type { SavedChat, Workspace } from "../App";
8
+ import type { CourseDirectoryItem } from "../lib/courseDirectory";
 
 
 
9
 
10
  type Props = {
11
  savedChats: SavedChat[];
 
15
 
16
  currentWorkspaceId: string;
17
  workspaces: Workspace[];
18
+
19
+ // ✅ use courseDirectory items
20
+ availableCourses: CourseDirectoryItem[];
21
  };
22
 
23
  function formatSub(ts: any) {
 
30
  }
31
  }
32
 
33
+ function gmailComposeLink(email: string, subject?: string, body?: string) {
34
+ const to = `to=${encodeURIComponent(email)}`;
35
+ const su = subject ? `&su=${encodeURIComponent(subject)}` : "";
36
+ const bd = body ? `&body=${encodeURIComponent(body)}` : "";
37
+ return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`;
38
+ }
39
+
40
  export function LeftSidebar({
41
  savedChats,
42
  onLoadChat,
 
44
  onRenameSavedChat,
45
  currentWorkspaceId,
46
  workspaces,
47
+ availableCourses,
48
  }: Props) {
49
  const [editingId, setEditingId] = useState<string | null>(null);
50
  const [draftTitle, setDraftTitle] = useState("");
51
 
52
  /** ===== 当前 workspace 对应的 course info ===== */
53
+ const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId);
54
+ const workspaceCourseInfo = (currentWorkspace as any)?.courseInfo as
55
+ | { id?: string; name?: string }
56
+ | undefined;
57
+
58
+ // ✅ Find full meta from course directory by id (preferred), fallback by name
59
+ const courseFromDirectory = useMemo(() => {
60
+ const id = workspaceCourseInfo?.id?.trim();
61
+ if (id) {
62
+ const hit = availableCourses.find((c) => c.id === id);
63
+ if (hit) return hit;
64
+ }
65
+
66
+ const name = workspaceCourseInfo?.name?.trim();
67
+ if (name) {
68
+ const hit = availableCourses.find((c) => c.name === name);
69
+ if (hit) return hit;
70
+ }
71
+
72
+ return undefined;
73
+ }, [availableCourses, workspaceCourseInfo?.id, workspaceCourseInfo?.name]);
74
+
75
+ // ✅ Final course for display (directory wins; fallback to workspace name/id)
76
+ const courseInfo: CourseDirectoryItem | null = useMemo(() => {
77
+ if (courseFromDirectory) return courseFromDirectory;
78
+
79
+ if (workspaceCourseInfo?.name || workspaceCourseInfo?.id) {
80
+ return {
81
+ id: workspaceCourseInfo?.id ?? "unknown",
82
+ name: workspaceCourseInfo?.name ?? "Untitled course",
83
+ };
84
+ }
85
+
86
+ return null;
87
+ }, [courseFromDirectory, workspaceCourseInfo?.id, workspaceCourseInfo?.name]);
88
 
89
  const sortedChats = useMemo(() => {
90
+ return [...savedChats].sort(
91
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
92
  );
93
  }, [savedChats]);
94
 
 
109
  cancelRename();
110
  };
111
 
112
+ // ===== Display fields (safe) =====
113
+ const instructorName = courseInfo?.instructor?.name ?? "N/A";
114
+ const instructorEmail = courseInfo?.instructor?.email?.trim() || "";
115
+ const taName = courseInfo?.teachingAssistant?.name ?? "N/A";
116
+ const taEmail = courseInfo?.teachingAssistant?.email?.trim() || "";
117
+
118
  return (
119
  <div className="h-full w-full flex flex-col min-h-0">
 
120
  {/* ================= Course Info(不滚动) ================= */}
121
  {courseInfo && (
122
  <div className="px-4 pt-4 pb-3 flex-shrink-0 space-y-2">
123
+ <div className="font-semibold text-base">{courseInfo.name}</div>
 
 
124
 
125
  <div className="text-sm text-muted-foreground">
126
  Instructor:&nbsp;
127
+ {instructorEmail ? (
128
+ <a
129
+ href={gmailComposeLink(
130
+ instructorEmail,
131
+ `[Clare] Question about ${courseInfo.name}`,
132
+ `Hi ${instructorName},\n\nI have a question about ${courseInfo.name}:\n\n(Write your question here)\n\nThanks,\n`
133
+ )}
134
+ target="_blank"
135
+ rel="noopener noreferrer"
136
+ className="text-primary hover:underline"
137
+ >
138
+ {instructorName}
139
+ </a>
140
+ ) : (
141
+ <span className="text-muted-foreground/60">{instructorName}</span>
142
+ )}
143
  </div>
144
 
145
  <div className="text-sm text-muted-foreground">
146
  TA:&nbsp;
147
+ {taEmail ? (
148
+ <a
149
+ href={gmailComposeLink(
150
+ taEmail,
151
+ `[Clare] Help request for ${courseInfo.name}`,
152
+ `Hi ${taName},\n\nI need help with ${courseInfo.name}:\n\n(Write your question here)\n\nThanks,\n`
153
+ )}
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ className="text-primary hover:underline"
157
+ >
158
+ {taName}
159
+ </a>
160
+ ) : (
161
+ <span className="text-muted-foreground/60">{taName}</span>
162
+ )}
163
  </div>
164
  </div>
165
  )}
166
 
167
+ {/* Info / Saved Chat 分割线:固定 #ECECF1 */}
168
+ <Separator className="flex-shrink-0 bg-[#ECECF1]" />
169
 
170
  {/* ================= Saved Chat Header(不滚动) ================= */}
171
  <div className="px-4 pt-4 pb-2 flex items-center gap-2 flex-shrink-0">
 
179
  <div className="flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3">
180
  {sortedChats.length === 0 ? (
181
  <div className="text-sm text-muted-foreground text-center py-10">
182
+ No saved chats yet
183
+ <br />
184
  <span className="text-xs">Save conversations to view them here</span>
185
  </div>
186
  ) : (
187
+ sortedChats.map((chat) => {
188
  const isEditing = editingId === chat.id;
189
  const sub = formatSub(chat.timestamp);
190
 
191
  return (
192
+ <div key={chat.id} className="rounded-xl border bg-card px-4 py-3">
 
 
 
193
  <div className="flex items-start justify-between gap-3">
 
194
  {/* 左侧 */}
195
  <div className="flex-1 min-w-0">
196
  {isEditing ? (
197
  <div className="space-y-2">
198
  <Input
199
  value={draftTitle}
200
+ onChange={(e) => setDraftTitle(e.target.value)}
201
  autoFocus
202
+ onKeyDown={(e) => {
203
  if (e.key === "Enter") commitRename(chat.id);
204
  if (e.key === "Escape") cancelRename();
205
  }}
 
214
  </div>
215
  </div>
216
  ) : (
217
+ <button className="text-left w-full" onClick={() => onLoadChat(chat)}>
218
+ <div className="font-medium truncate">{chat.title || "Untitled chat"}</div>
219
+ {sub && <div className="text-xs text-muted-foreground mt-1">{sub}</div>}
 
 
 
 
 
 
 
 
 
220
  </button>
221
  )}
222
  </div>
 
224
  {/* 右侧按钮 */}
225
  {!isEditing && (
226
  <div className="flex gap-2 flex-shrink-0">
227
+ <Button variant="ghost" size="icon" onClick={() => startRename(chat)}>
 
 
 
 
228
  <Edit2 className="h-4 w-4" />
229
  </Button>
230
  <Button