SarahXia0405 commited on
Commit
82b2f83
·
verified ·
1 Parent(s): 880e5b4

Update web/src/components/CourseInfoHeader.tsx

Browse files
web/src/components/CourseInfoHeader.tsx CHANGED
@@ -1,198 +1,92 @@
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
  );
 
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 gmailCompose(email: string, subject: string, body?: string) {
13
+ const s = encodeURIComponent(subject);
14
+ const b = body ? `&body=${encodeURIComponent(body)}` : "";
15
+ return `https://mail.google.com/mail/?view=cm&fs=1&to=${encodeURIComponent(email)}&su=${s}${b}`;
 
 
 
 
 
 
 
 
16
  }
17
 
18
+ export function CourseInfoHeader({
19
+ course,
20
+ className,
21
+ }: {
22
+ course: CourseInfo;
23
+ className?: string;
24
+ }) {
25
+ const instructorLink = useMemo(() => {
26
+ const p = course.instructor;
27
+ if (!p?.email) return "";
28
+ return gmailCompose(
29
+ p.email,
30
+ `[Clare] Question about ${course.name}`,
31
+ `Hi ${p.name},\n\nI have a question about ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
 
 
 
 
32
  );
33
+ }, [course]);
34
+
35
+ const taLink = useMemo(() => {
36
+ const p = course.teachingAssistant;
37
+ if (!p?.email) return "";
38
+ return gmailCompose(
39
+ p.email,
40
+ `[Clare] Help request for ${course.name}`,
41
+ `Hi ${p.name},\n\nI need help with ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
42
+ );
43
+ }, [course]);
 
 
 
 
 
 
 
44
 
45
+ const instructorName = course.instructor?.name ?? "N/A";
46
+ const taName = course.teachingAssistant?.name ?? "N/A";
 
 
47
 
48
  return (
49
+ <div className={className ?? "px-4 pt-4 pb-3"}>
50
+ <div className="space-y-1">
51
+ <div className="text-base font-semibold text-foreground truncate">
52
+ {course.name}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
 
 
 
 
 
 
 
 
 
 
54
 
55
+ <div className="text-sm text-muted-foreground">
56
+ Instructor:{" "}
57
+ {instructorLink ? (
58
+ <a
59
+ href={instructorLink}
60
+ target="_blank"
61
+ rel="noopener noreferrer"
62
+ className="text-primary hover:underline"
63
+ title={`Message ${instructorName} in Gmail`}
64
+ onClick={(e) => e.stopPropagation()}
65
+ >
66
+ {instructorName}
67
+ </a>
68
+ ) : (
69
+ <span className="text-muted-foreground/60">{instructorName}</span>
70
+ )}
71
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ <div className="text-sm text-muted-foreground">
74
+ TA:{" "}
75
+ {taLink ? (
76
+ <a
77
+ href={taLink}
78
+ target="_blank"
79
+ rel="noopener noreferrer"
80
+ className="text-primary hover:underline"
81
+ title={`Message ${taName} in Gmail`}
82
+ onClick={(e) => e.stopPropagation()}
83
+ >
84
+ {taName}
85
+ </a>
86
+ ) : (
87
+ <span className="text-muted-foreground/60">{taName}</span>
88
+ )}
89
+ </div>
 
 
 
 
90
  </div>
91
  </div>
92
  );