SarahXia0405 commited on
Commit
880e5b4
·
verified ·
1 Parent(s): 4d44a10

Update web/src/components/CourseInfoHeader.tsx

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