SarahXia0405 commited on
Commit
ee09136
·
verified ·
1 Parent(s): 25fcd9d

Create sidebar/SavedChatSection.tsx

Browse files
web/src/components/sidebar/SavedChatSection.tsx ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // web/src/components/sidebar/SavedChatSection.tsx
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { MessageSquare, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
4
+ import { Card } from "../ui/card";
5
+ import { Button } from "../ui/button";
6
+ import { Input } from "../ui/input";
7
+ import type { SavedChat } from "../../App";
8
+
9
+ // ================================
10
+ // Saved Chat Item (moved from LeftSidebar, unchanged)
11
+ // ================================
12
+ function SavedChatItem({
13
+ chat,
14
+ onLoadChat,
15
+ onDeleteSavedChat,
16
+ onRenameSavedChat,
17
+ }: {
18
+ chat: SavedChat;
19
+ onLoadChat: (chat: SavedChat) => void;
20
+ onDeleteSavedChat: (id: string) => void;
21
+ onRenameSavedChat?: (id: string, newTitle: string) => void;
22
+ }) {
23
+ const [isEditing, setIsEditing] = useState(false);
24
+ const [editTitle, setEditTitle] = useState(chat.title);
25
+ const [originalTitle, setOriginalTitle] = useState(chat.title);
26
+ const inputRef = useRef<HTMLInputElement>(null);
27
+ const cancelButtonRef = useRef<HTMLButtonElement>(null);
28
+ const saveButtonRef = useRef<HTMLButtonElement>(null);
29
+
30
+ useEffect(() => {
31
+ if (!isEditing) {
32
+ setOriginalTitle(chat.title);
33
+ setEditTitle(chat.title);
34
+ }
35
+ }, [chat.title, isEditing]);
36
+
37
+ const handleStartEdit = (e: React.MouseEvent) => {
38
+ e.preventDefault();
39
+ e.stopPropagation();
40
+ setOriginalTitle(chat.title);
41
+ setEditTitle(chat.title);
42
+ setIsEditing(true);
43
+ setTimeout(() => inputRef.current?.focus(), 0);
44
+ };
45
+
46
+ const handleSaveEdit = (e: React.MouseEvent) => {
47
+ e.preventDefault();
48
+ e.stopPropagation();
49
+ if (editTitle.trim() && onRenameSavedChat) {
50
+ onRenameSavedChat(chat.id, editTitle.trim());
51
+ setIsEditing(false);
52
+ }
53
+ };
54
+
55
+ const handleCancelEdit = (e: React.MouseEvent) => {
56
+ e.preventDefault();
57
+ e.stopPropagation();
58
+ setEditTitle(originalTitle);
59
+ setIsEditing(false);
60
+ inputRef.current?.blur();
61
+ };
62
+
63
+ const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
64
+ const relatedTarget = e.relatedTarget as HTMLElement;
65
+ if (
66
+ relatedTarget &&
67
+ (cancelButtonRef.current?.contains(relatedTarget) ||
68
+ saveButtonRef.current?.contains(relatedTarget))
69
+ ) {
70
+ return;
71
+ }
72
+ if (editTitle.trim() && editTitle !== originalTitle && onRenameSavedChat) {
73
+ onRenameSavedChat(chat.id, editTitle.trim());
74
+ }
75
+ setIsEditing(false);
76
+ };
77
+
78
+ const handleKeyDown = (e: React.KeyboardEvent) => {
79
+ if (e.key === "Enter") {
80
+ e.preventDefault();
81
+ e.stopPropagation();
82
+ if (editTitle.trim() && onRenameSavedChat) {
83
+ onRenameSavedChat(chat.id, editTitle.trim());
84
+ setIsEditing(false);
85
+ }
86
+ } else if (e.key === "Escape") {
87
+ e.preventDefault();
88
+ e.stopPropagation();
89
+ setEditTitle(originalTitle);
90
+ setIsEditing(false);
91
+ }
92
+ };
93
+
94
+ return (
95
+ <Card
96
+ className="p-3 cursor-pointer hover:bg-muted/50 transition-all bg-muted/30"
97
+ onClick={() => !isEditing && onLoadChat(chat)}
98
+ >
99
+ <div className="flex items-start gap-2">
100
+ <MessageSquare className="h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-muted-foreground" />
101
+ <div className="flex-1 min-w-0">
102
+ <div className="flex items-start justify-between gap-2">
103
+ {isEditing ? (
104
+ <Input
105
+ ref={inputRef}
106
+ value={editTitle}
107
+ onChange={(e) => setEditTitle(e.target.value)}
108
+ onKeyDown={handleKeyDown}
109
+ onClick={(e) => e.stopPropagation()}
110
+ onBlur={handleInputBlur}
111
+ className="h-auto text-sm font-medium px-2 py-1 border border-border bg-background focus-visible:ring-2 focus-visible:ring-ring flex-1"
112
+ style={{ height: "auto" }}
113
+ />
114
+ ) : (
115
+ <h4
116
+ className="text-sm font-medium truncate flex-1 cursor-text"
117
+ onDoubleClick={(e) => {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ handleStartEdit(e);
121
+ }}
122
+ onClick={(e) => e.stopPropagation()}
123
+ title="Double click to rename"
124
+ >
125
+ {chat.title}
126
+ </h4>
127
+ )}
128
+
129
+ <div className="flex items-center gap-1 flex-shrink-0">
130
+ {isEditing ? (
131
+ <>
132
+ <Button
133
+ ref={saveButtonRef}
134
+ variant="ghost"
135
+ size="icon"
136
+ className="h-5 w-5 flex-shrink-0 hover:bg-green-500/20"
137
+ onClick={(e) => {
138
+ e.preventDefault();
139
+ e.stopPropagation();
140
+ handleSaveEdit(e);
141
+ }}
142
+ title="Save"
143
+ type="button"
144
+ >
145
+ <Check className="h-3 w-3" />
146
+ </Button>
147
+ <Button
148
+ ref={cancelButtonRef}
149
+ variant="ghost"
150
+ size="icon"
151
+ className="h-5 w-5 flex-shrink-0 hover:bg-destructive/20"
152
+ onClick={(e) => {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ handleCancelEdit(e);
156
+ }}
157
+ title="Cancel"
158
+ type="button"
159
+ >
160
+ <XIcon className="h-3 w-3" />
161
+ </Button>
162
+ </>
163
+ ) : (
164
+ <>
165
+ {onRenameSavedChat && (
166
+ <Button
167
+ variant="ghost"
168
+ size="icon"
169
+ className="h-5 w-5 flex-shrink-0 hover:bg-muted"
170
+ onClick={handleStartEdit}
171
+ title="Rename chat"
172
+ >
173
+ <Edit2 className="h-3 w-3" />
174
+ </Button>
175
+ )}
176
+ <Button
177
+ variant="ghost"
178
+ size="icon"
179
+ className="h-5 w-5 flex-shrink-0 hover:bg-destructive/20"
180
+ onClick={(e) => {
181
+ e.stopPropagation();
182
+ onDeleteSavedChat(chat.id);
183
+ }}
184
+ title="Delete chat"
185
+ >
186
+ <Trash2 className="h-3 w-3" />
187
+ </Button>
188
+ </>
189
+ )}
190
+ </div>
191
+ </div>
192
+
193
+ <p className="text-xs text-muted-foreground mt-1">
194
+ {chat.chatMode === "ask"
195
+ ? "Ask"
196
+ : chat.chatMode === "review"
197
+ ? "Review"
198
+ : "Quiz"}{" "}
199
+ • {chat.timestamp.toLocaleDateString()}
200
+ </p>
201
+ <p className="text-xs text-muted-foreground/70 mt-1">
202
+ {chat.messages.length} message{chat.messages.length !== 1 ? "s" : ""}
203
+ </p>
204
+ </div>
205
+ </div>
206
+ </Card>
207
+ );
208
+ }
209
+
210
+ // ================================
211
+ // Saved Chat Section (NEW)
212
+ // ================================
213
+ export function SavedChatSection({
214
+ isLoggedIn,
215
+ savedChats,
216
+ onLoadChat,
217
+ onDeleteSavedChat,
218
+ onRenameSavedChat,
219
+ }: {
220
+ isLoggedIn: boolean;
221
+ savedChats: SavedChat[];
222
+ onLoadChat: (chat: SavedChat) => void;
223
+ onDeleteSavedChat: (id: string) => void;
224
+ onRenameSavedChat?: (id: string, newTitle: string) => void;
225
+ }) {
226
+ if (!isLoggedIn) return null;
227
+
228
+ return (
229
+ <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
230
+ <div className="p-4 border-b border-border flex-shrink-0">
231
+ <h3 className="text-base font-medium">Saved Chat</h3>
232
+ </div>
233
+
234
+ {/* ONLY this area scrolls */}
235
+ <div className="flex-1 min-h-0 panelScroll p-4">
236
+ {savedChats.length === 0 ? (
237
+ <div className="text-sm text-muted-foreground text-center py-4">
238
+ <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
239
+ <p>No saved chats yet</p>
240
+ <p className="text-xs mt-1">Save conversations to view them here</p>
241
+ </div>
242
+ ) : (
243
+ <div className="space-y-2">
244
+ {savedChats.map((chat) => (
245
+ <SavedChatItem
246
+ key={chat.id}
247
+ chat={chat}
248
+ onLoadChat={onLoadChat}
249
+ onDeleteSavedChat={onDeleteSavedChat}
250
+ onRenameSavedChat={onRenameSavedChat}
251
+ />
252
+ ))}
253
+ </div>
254
+ )}
255
+ </div>
256
+ </div>
257
+ );
258
+ }