SarahXia0405 commited on
Commit
2f8db1b
·
verified ·
1 Parent(s): 92128df

Update web/src/components/LeftSidebar.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/LeftSidebar.tsx +98 -99
web/src/components/LeftSidebar.tsx CHANGED
@@ -4,62 +4,63 @@ import { Button } from "./ui/button";
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
6
  import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
7
- import type { SavedChat } from "../App";
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  type Props = {
10
- // existing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  savedChats: SavedChat[];
12
- currentSavedChatId?: string | null;
 
13
 
14
- onLoadSavedChat?: (chatId: string) => void;
15
- onDeleteSavedChat?: (chatId: string) => void;
16
 
17
- // ✅ NEW
18
- onRenameSavedChat?: (chatId: string, title: string) => void;
19
 
20
- // optional: you may already have these
21
- className?: string;
22
  };
23
 
24
- function formatSavedChatTitle(chat: SavedChat) {
25
- // ✅ preferred user-defined title
26
- const anyChat = chat as any;
27
- const t = (anyChat?.title ?? anyChat?.name ?? "").toString().trim();
28
- if (t) return t;
29
-
30
- // fallback: keep your old naming style
31
- const mode = (chat as any)?.chatMode ? String((chat as any).chatMode) : "Ask";
32
- const ts =
33
- (chat as any)?.savedAt ||
34
- (chat as any)?.timestamp ||
35
- (chat as any)?.createdAt ||
36
- (chat as any)?.time ||
37
- null;
38
-
39
- // attempt to print a nice date if it exists
40
- try {
41
- if (ts) {
42
- const d = typeof ts === "string" || typeof ts === "number" ? new Date(ts) : ts;
43
- const dateStr = d.toLocaleDateString();
44
- return `Chat - ${mode} - ${dateStr}`;
45
- }
46
- } catch {
47
- // ignore
48
- }
49
-
50
- return `Chat - ${mode}`;
51
- }
52
-
53
- function formatSavedChatSub(chat: SavedChat) {
54
- const ts =
55
- (chat as any)?.savedAt ||
56
- (chat as any)?.timestamp ||
57
- (chat as any)?.createdAt ||
58
- (chat as any)?.time ||
59
- null;
60
-
61
  if (!ts) return "";
62
-
63
  try {
64
  const d = typeof ts === "string" || typeof ts === "number" ? new Date(ts) : ts;
65
  return d.toLocaleString();
@@ -68,34 +69,30 @@ function formatSavedChatSub(chat: SavedChat) {
68
  }
69
  }
70
 
71
- export function LeftSidebar({
72
- savedChats,
73
- currentSavedChatId,
74
- onLoadSavedChat,
75
- onDeleteSavedChat,
76
- onRenameSavedChat,
77
- className,
78
- }: Props) {
79
- // rename state
80
  const [editingId, setEditingId] = useState<string | null>(null);
81
  const [draftTitle, setDraftTitle] = useState<string>("");
82
 
83
  const sortedChats = useMemo(() => {
84
  // newest first
85
  return [...savedChats].sort((a: any, b: any) => {
86
- const ta = new Date(a.savedAt || a.timestamp || a.createdAt || 0).getTime();
87
- const tb = new Date(b.savedAt || b.timestamp || b.createdAt || 0).getTime();
88
  return tb - ta;
89
  });
90
  }, [savedChats]);
91
 
92
  const startRename = (chat: SavedChat) => {
93
- const id = (chat as any).id as string;
94
- setEditingId(id);
95
-
96
- const anyChat = chat as any;
97
- const existing = (anyChat?.title ?? anyChat?.name ?? "").toString();
98
- setDraftTitle(existing || formatSavedChatTitle(chat));
99
  };
100
 
101
  const cancelRename = () => {
@@ -106,43 +103,41 @@ export function LeftSidebar({
106
  const commitRename = (chatId: string) => {
107
  const next = draftTitle.trim();
108
  if (!next) return;
109
-
110
- onRenameSavedChat?.(chatId, next);
111
  setEditingId(null);
112
  setDraftTitle("");
113
  };
114
 
115
  return (
116
- <aside className={className ?? "h-full w-full"}>
117
- <div className="px-4 pt-4 pb-3 flex items-center gap-2">
118
- <Bookmark className="h-4 w-4" />
119
- <h3 className="font-semibold">Saved Chat</h3>
 
 
 
120
  </div>
121
 
122
- <div className="px-4">
123
  <Separator />
124
  </div>
125
 
126
- <div className="px-4 py-3 space-y-3">
 
127
  {sortedChats.length === 0 ? (
128
  <div className="text-sm text-muted-foreground">No saved chats yet.</div>
129
  ) : (
130
  sortedChats.map((chat) => {
131
- const id = (chat as any).id as string;
132
- const title = formatSavedChatTitle(chat);
133
- const sub = formatSavedChatSub(chat);
134
- const isActive = currentSavedChatId ? currentSavedChatId === id : false;
135
- const isEditing = editingId === id;
136
 
137
  return (
138
  <div
139
- key={id}
140
- className={`rounded-xl border bg-card px-4 py-3 ${
141
- isActive ? "border-primary/40" : "border-border"
142
- }`}
143
  >
144
  <div className="flex items-start justify-between gap-3">
145
- {/* left: title + time */}
146
  <div className="min-w-0 flex-1">
147
  {isEditing ? (
148
  <div className="space-y-2">
@@ -154,7 +149,7 @@ export function LeftSidebar({
154
  onKeyDown={(e) => {
155
  if (e.key === "Enter") {
156
  e.preventDefault();
157
- commitRename(id);
158
  }
159
  if (e.key === "Escape") {
160
  e.preventDefault();
@@ -166,7 +161,7 @@ export function LeftSidebar({
166
  <Button
167
  size="sm"
168
  className="h-8 px-2"
169
- onClick={() => commitRename(id)}
170
  disabled={!draftTitle.trim()}
171
  title="Save"
172
  >
@@ -184,24 +179,25 @@ export function LeftSidebar({
184
  </div>
185
  </div>
186
  ) : (
187
- <>
188
- <button
189
- type="button"
190
- className="text-left w-full"
191
- onClick={() => onLoadSavedChat?.(id)}
192
- title="Open saved chat"
193
- >
194
- <div className="font-semibold truncate">{title}</div>
195
- {sub ? <div className="text-xs text-muted-foreground mt-1">{sub}</div> : null}
196
- </button>
197
- </>
 
 
198
  )}
199
  </div>
200
 
201
- {/* right: edit + delete */}
202
  {!isEditing && (
203
  <div className="flex items-center gap-2 flex-shrink-0">
204
- {/* ✅ NEW: rename pen icon */}
205
  <Button
206
  variant="ghost"
207
  size="icon"
@@ -223,7 +219,7 @@ export function LeftSidebar({
223
  onClick={(e) => {
224
  e.preventDefault();
225
  e.stopPropagation();
226
- onDeleteSavedChat?.(id);
227
  }}
228
  title="Delete"
229
  >
@@ -237,6 +233,9 @@ export function LeftSidebar({
237
  })
238
  )}
239
  </div>
240
- </aside>
 
 
 
241
  );
242
  }
 
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
6
  import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
7
+
8
+ import type {
9
+ LearningMode,
10
+ Language,
11
+ SpaceType,
12
+ GroupMember,
13
+ User as UserType,
14
+ SavedItem,
15
+ SavedChat,
16
+ Workspace,
17
+ CourseInfo,
18
+ } from "../App";
19
 
20
  type Props = {
21
+ // ===== App.tsx currently passes these =====
22
+ learningMode: LearningMode;
23
+ language: Language;
24
+ onLearningModeChange: (mode: LearningMode) => void;
25
+ onLanguageChange: (lang: Language) => void;
26
+
27
+ spaceType: SpaceType;
28
+ groupMembers: GroupMember[];
29
+
30
+ user: UserType | null;
31
+ onLogin: (u: UserType | null) => void;
32
+ onLogout: () => void;
33
+ isLoggedIn: boolean;
34
+
35
+ onEditProfile: () => void;
36
+
37
+ savedItems: SavedItem[];
38
+ recentlySavedId: string | null;
39
+ onUnsave: (id: string) => void;
40
+ onSave: (
41
+ content: string,
42
+ type: "export" | "quiz" | "summary",
43
+ saveAsChat?: boolean,
44
+ format?: "pdf" | "text",
45
+ workspaceId?: string
46
+ ) => void;
47
+
48
  savedChats: SavedChat[];
49
+ onLoadChat: (chat: SavedChat) => void;
50
+ onDeleteSavedChat: (id: string) => void;
51
 
52
+ // NEW in your App.tsx already
53
+ onRenameSavedChat: (id: string, newTitle: string) => void;
54
 
55
+ currentWorkspaceId: string;
56
+ workspaces: Workspace[];
57
 
58
+ selectedCourse: string;
59
+ availableCourses: CourseInfo[];
60
  };
61
 
62
+ function formatSub(ts: any) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if (!ts) return "";
 
64
  try {
65
  const d = typeof ts === "string" || typeof ts === "number" ? new Date(ts) : ts;
66
  return d.toLocaleString();
 
69
  }
70
  }
71
 
72
+ export function LeftSidebar(props: Props) {
73
+ const {
74
+ savedChats,
75
+ onLoadChat,
76
+ onDeleteSavedChat,
77
+ onRenameSavedChat,
78
+ } = props;
79
+
80
+ // ===== rename state =====
81
  const [editingId, setEditingId] = useState<string | null>(null);
82
  const [draftTitle, setDraftTitle] = useState<string>("");
83
 
84
  const sortedChats = useMemo(() => {
85
  // newest first
86
  return [...savedChats].sort((a: any, b: any) => {
87
+ const ta = new Date(a.timestamp || a.savedAt || a.createdAt || 0).getTime();
88
+ const tb = new Date(b.timestamp || b.savedAt || b.createdAt || 0).getTime();
89
  return tb - ta;
90
  });
91
  }, [savedChats]);
92
 
93
  const startRename = (chat: SavedChat) => {
94
+ setEditingId(chat.id);
95
+ setDraftTitle((chat.title || "").trim() || "Untitled chat");
 
 
 
 
96
  };
97
 
98
  const cancelRename = () => {
 
103
  const commitRename = (chatId: string) => {
104
  const next = draftTitle.trim();
105
  if (!next) return;
106
+ onRenameSavedChat(chatId, next);
 
107
  setEditingId(null);
108
  setDraftTitle("");
109
  };
110
 
111
  return (
112
+ <div className="h-full w-full flex flex-col min-h-0">
113
+ {/* You can keep other sections above; here we keep minimal structure */}
114
+ <div className="flex-shrink-0 px-4 pt-4 pb-3">
115
+ <div className="flex items-center gap-2">
116
+ <Bookmark className="h-4 w-4" />
117
+ <h3 className="font-semibold">Saved Chat</h3>
118
+ </div>
119
  </div>
120
 
121
+ <div className="px-4 flex-shrink-0">
122
  <Separator />
123
  </div>
124
 
125
+ {/* list */}
126
+ <div className="flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3">
127
  {sortedChats.length === 0 ? (
128
  <div className="text-sm text-muted-foreground">No saved chats yet.</div>
129
  ) : (
130
  sortedChats.map((chat) => {
131
+ const isEditing = editingId === chat.id;
132
+ const sub = formatSub(chat.timestamp);
 
 
 
133
 
134
  return (
135
  <div
136
+ key={chat.id}
137
+ className="rounded-xl border border-border bg-card px-4 py-3"
 
 
138
  >
139
  <div className="flex items-start justify-between gap-3">
140
+ {/* left */}
141
  <div className="min-w-0 flex-1">
142
  {isEditing ? (
143
  <div className="space-y-2">
 
149
  onKeyDown={(e) => {
150
  if (e.key === "Enter") {
151
  e.preventDefault();
152
+ commitRename(chat.id);
153
  }
154
  if (e.key === "Escape") {
155
  e.preventDefault();
 
161
  <Button
162
  size="sm"
163
  className="h-8 px-2"
164
+ onClick={() => commitRename(chat.id)}
165
  disabled={!draftTitle.trim()}
166
  title="Save"
167
  >
 
179
  </div>
180
  </div>
181
  ) : (
182
+ <button
183
+ type="button"
184
+ className="text-left w-full"
185
+ onClick={() => onLoadChat(chat)}
186
+ title="Open saved chat"
187
+ >
188
+ <div className="font-semibold truncate">{chat.title || "Untitled chat"}</div>
189
+ {sub ? (
190
+ <div className="text-xs text-muted-foreground mt-1">
191
+ {sub}
192
+ </div>
193
+ ) : null}
194
+ </button>
195
  )}
196
  </div>
197
 
198
+ {/* right: ✏️ then 🗑️ */}
199
  {!isEditing && (
200
  <div className="flex items-center gap-2 flex-shrink-0">
 
201
  <Button
202
  variant="ghost"
203
  size="icon"
 
219
  onClick={(e) => {
220
  e.preventDefault();
221
  e.stopPropagation();
222
+ onDeleteSavedChat(chat.id);
223
  }}
224
  title="Delete"
225
  >
 
233
  })
234
  )}
235
  </div>
236
+
237
+ {/* Optional footer placeholder (keeps layout stable) */}
238
+ <div className="flex-shrink-0 px-4 pb-4" />
239
+ </div>
240
  );
241
  }