Paramjit Singh commited on
Commit
38d83f3
·
unverified ·
2 Parent(s): ae2a7218371065

Merge pull request #304 from saurabhhhcodes/frontend/mobile-chat-sidebar-277

Browse files
frontend/src/components/chat/ChatSessionSidebar.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
4
- import { Plus, Edit2, Trash2, MessageSquare, ChevronLeft } from "lucide-react";
5
  import { useChatStore, type ChatSession } from "@/store/chat-store";
6
  import { Button } from "@/components/ui/button";
7
  import { Input } from "@/components/ui/input";
@@ -18,6 +18,7 @@ export default function ChatSessionSidebar() {
18
  const fetchSessionHistory = useChatStore((state) => state.fetchSessionHistory);
19
 
20
  const [isOpen, setIsOpen] = useState(true);
 
21
  const [editingId, setEditingId] = useState<string | null>(null);
22
  const [editTitle, setEditTitle] = useState("");
23
  const [creating, setCreating] = useState(false);
@@ -77,108 +78,179 @@ export default function ChatSessionSidebar() {
77
  const handleSelectSession = async (id: string) => {
78
  setActiveSessionId(id);
79
  await fetchSessionHistory(id);
 
80
  };
81
 
82
- return (
83
- <div className={cn("relative flex h-full border-r border-border/50 bg-card/20 select-none transition-all duration-300", isOpen ? "w-64" : "w-0")}>
84
- <div className={cn("flex flex-col h-full w-full overflow-hidden transition-opacity duration-200", isOpen ? "opacity-100" : "opacity-0 pointer-events-none")}>
85
- {/* Sidebar Header */}
86
- <div className="flex items-center justify-between p-3 border-b border-border/50 shrink-0 bg-card/45">
87
- <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Chat Sessions</span>
88
  <Button
89
  onClick={handleCreate}
90
  variant="outline"
91
  size="icon"
92
  className="h-7 w-7 bg-background/50 hover:bg-accent hover:text-accent-foreground"
93
  disabled={creating}
 
94
  >
95
  <Plus className="w-4 h-4" />
96
  </Button>
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
 
98
 
99
- {/* Sessions List */}
100
- <div className="flex-1 overflow-y-auto p-2 space-y-1 scrollbar-thin">
101
- {sessions.length === 0 ? (
102
- <div className="text-center py-8 px-4">
103
- <p className="text-xs text-muted-foreground">No chat sessions. Click &quot;+&quot; to start a new chat.</p>
104
- </div>
105
- ) : (
106
- sessions.map((session) => {
107
- const isActive = session.id === activeSessionId;
108
- const isEditing = session.id === editingId;
109
-
110
- return (
111
- <div
112
- key={session.id}
113
- onClick={() => !isEditing && handleSelectSession(session.id)}
114
- className={cn(
115
- "group flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-all duration-200 cursor-pointer border",
116
- isActive
117
- ? "bg-accent/80 border-accent text-accent-foreground shadow-sm"
118
- : "border-transparent hover:bg-card/60 hover:text-foreground text-muted-foreground"
119
- )}
120
- >
121
- <div className="flex items-center gap-2 min-w-0 flex-1">
122
- <MessageSquare className={cn("w-4 h-4 shrink-0", isActive ? "text-primary" : "text-muted-foreground")} />
123
-
124
- {isEditing ? (
125
- <form
126
- onSubmit={(e) => handleSaveRename(session.id, e)}
127
- className="flex items-center gap-1 w-full"
128
- onClick={(e) => e.stopPropagation()}
129
- >
130
- <Input
131
- value={editTitle}
132
- onChange={(e) => setEditTitle(e.target.value)}
133
- className="h-6 text-xs px-1 py-0 bg-background/50 border-input w-full"
134
- autoFocus
135
- onBlur={() => handleSaveRename(session.id)}
136
- />
137
- </form>
138
- ) : (
139
- <span className="truncate text-xs font-medium">{session.title}</span>
140
- )}
141
- </div>
142
 
143
- {!isEditing && (
144
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-150 shrink-0 ml-1">
145
- <Button
146
- variant="ghost"
147
- size="icon"
148
- className="h-5 w-5 rounded-md hover:bg-background/80"
149
- onClick={(e) => handleStartRename(session, e)}
150
- >
151
- <Edit2 className="w-3 h-3" />
152
- </Button>
153
- <Button
154
- variant="ghost"
155
- size="icon"
156
- className="h-5 w-5 rounded-md hover:bg-destructive/10 hover:text-destructive"
157
- onClick={(e) => handleDelete(session.id, e)}
158
- >
159
- <Trash2 className="w-3 h-3" />
160
- </Button>
161
- </div>
162
  )}
163
  </div>
164
- );
165
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  )}
 
 
167
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
 
170
- {/* Collapse Toggle Button */}
171
  <Button
172
- onClick={() => setIsOpen(!isOpen)}
173
- variant="ghost"
174
  size="icon"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  className={cn(
176
- "absolute -right-3 top-1/2 -translate-y-1/2 z-40 h-6 w-6 rounded-full border border-border bg-background shadow-md hover:bg-accent hover:text-accent-foreground",
177
- !isOpen && "right-auto -left-3 rotate-180"
178
  )}
 
 
 
179
  >
180
- <ChevronLeft className="w-3.5 h-3.5" />
181
- </Button>
182
- </div>
183
  );
184
  }
 
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
4
+ import { Plus, Edit2, Trash2, MessageSquare, ChevronLeft, X } from "lucide-react";
5
  import { useChatStore, type ChatSession } from "@/store/chat-store";
6
  import { Button } from "@/components/ui/button";
7
  import { Input } from "@/components/ui/input";
 
18
  const fetchSessionHistory = useChatStore((state) => state.fetchSessionHistory);
19
 
20
  const [isOpen, setIsOpen] = useState(true);
21
+ const [mobileOpen, setMobileOpen] = useState(false);
22
  const [editingId, setEditingId] = useState<string | null>(null);
23
  const [editTitle, setEditTitle] = useState("");
24
  const [creating, setCreating] = useState(false);
 
78
  const handleSelectSession = async (id: string) => {
79
  setActiveSessionId(id);
80
  await fetchSessionHistory(id);
81
+ setMobileOpen(false);
82
  };
83
 
84
+ const sessionsContent = (showCloseButton = false) => (
85
+ <div className="flex flex-col h-full w-full overflow-hidden">
86
+ {/* Sidebar Header */}
87
+ <div className="flex items-center justify-between p-3 border-b border-border/50 shrink-0 bg-card/45">
88
+ <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Chat Sessions</span>
89
+ <div className="flex items-center gap-1.5">
90
  <Button
91
  onClick={handleCreate}
92
  variant="outline"
93
  size="icon"
94
  className="h-7 w-7 bg-background/50 hover:bg-accent hover:text-accent-foreground"
95
  disabled={creating}
96
+ aria-label="Create chat session"
97
  >
98
  <Plus className="w-4 h-4" />
99
  </Button>
100
+ {showCloseButton && (
101
+ <Button
102
+ onClick={() => setMobileOpen(false)}
103
+ variant="ghost"
104
+ size="icon"
105
+ className="h-7 w-7"
106
+ aria-label="Close chat sessions"
107
+ >
108
+ <X className="w-4 h-4" />
109
+ </Button>
110
+ )}
111
  </div>
112
+ </div>
113
 
114
+ {/* Sessions List */}
115
+ <div className="flex-1 overflow-y-auto p-2 space-y-1 scrollbar-thin">
116
+ {sessions.length === 0 ? (
117
+ <div className="text-center py-8 px-4">
118
+ <p className="text-xs text-muted-foreground">No chat sessions. Click &quot;+&quot; to start a new chat.</p>
119
+ </div>
120
+ ) : (
121
+ sessions.map((session) => {
122
+ const isActive = session.id === activeSessionId;
123
+ const isEditing = session.id === editingId;
124
+
125
+ return (
126
+ <div
127
+ key={session.id}
128
+ onClick={() => !isEditing && handleSelectSession(session.id)}
129
+ className={cn(
130
+ "group flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-all duration-200 cursor-pointer border",
131
+ isActive
132
+ ? "bg-accent/80 border-accent text-accent-foreground shadow-sm"
133
+ : "border-transparent hover:bg-card/60 hover:text-foreground text-muted-foreground"
134
+ )}
135
+ >
136
+ <div className="flex items-center gap-2 min-w-0 flex-1">
137
+ <MessageSquare
138
+ className={cn("w-4 h-4 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
139
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ {isEditing ? (
142
+ <form
143
+ onSubmit={(e) => handleSaveRename(session.id, e)}
144
+ className="flex items-center gap-1 w-full"
145
+ onClick={(e) => e.stopPropagation()}
146
+ >
147
+ <Input
148
+ value={editTitle}
149
+ onChange={(e) => setEditTitle(e.target.value)}
150
+ className="h-6 text-xs px-1 py-0 bg-background/50 border-input w-full"
151
+ autoFocus
152
+ onBlur={() => handleSaveRename(session.id)}
153
+ />
154
+ </form>
155
+ ) : (
156
+ <span className="truncate text-xs font-medium">{session.title}</span>
 
 
 
157
  )}
158
  </div>
159
+
160
+ {!isEditing && (
161
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-150 shrink-0 ml-1">
162
+ <Button
163
+ variant="ghost"
164
+ size="icon"
165
+ className="h-5 w-5 rounded-md hover:bg-background/80"
166
+ onClick={(e) => handleStartRename(session, e)}
167
+ aria-label={`Rename ${session.title}`}
168
+ >
169
+ <Edit2 className="w-3 h-3" />
170
+ </Button>
171
+ <Button
172
+ variant="ghost"
173
+ size="icon"
174
+ className="h-5 w-5 rounded-md hover:bg-destructive/10 hover:text-destructive"
175
+ onClick={(e) => handleDelete(session.id, e)}
176
+ aria-label={`Delete ${session.title}`}
177
+ >
178
+ <Trash2 className="w-3 h-3" />
179
+ </Button>
180
+ </div>
181
+ )}
182
+ </div>
183
+ );
184
+ })
185
+ )}
186
+ </div>
187
+ </div>
188
+ );
189
+
190
+ return (
191
+ <>
192
+ <div
193
+ className={cn(
194
+ "relative hidden h-full border-r border-border/50 bg-card/20 select-none transition-all duration-300 md:flex",
195
+ isOpen ? "w-64" : "w-0"
196
+ )}
197
+ >
198
+ <div
199
+ className={cn(
200
+ "flex h-full w-full flex-col overflow-hidden transition-opacity duration-200",
201
+ isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
202
  )}
203
+ >
204
+ {sessionsContent()}
205
  </div>
206
+
207
+ {/* Collapse Toggle Button */}
208
+ <Button
209
+ onClick={() => setIsOpen(!isOpen)}
210
+ variant="ghost"
211
+ size="icon"
212
+ className={cn(
213
+ "absolute -right-3 top-1/2 -translate-y-1/2 z-40 h-6 w-6 rounded-full border border-border bg-background shadow-md hover:bg-accent hover:text-accent-foreground",
214
+ !isOpen && "right-auto -left-3 rotate-180"
215
+ )}
216
+ aria-label={isOpen ? "Collapse chat sessions" : "Expand chat sessions"}
217
+ >
218
+ <ChevronLeft className="w-3.5 h-3.5" />
219
+ </Button>
220
  </div>
221
 
 
222
  <Button
223
+ onClick={() => setMobileOpen(true)}
224
+ className="fixed bottom-4 left-4 z-30 h-11 w-11 rounded-full shadow-lg md:hidden"
225
  size="icon"
226
+ aria-label="Open chat sessions"
227
+ aria-controls="mobile-chat-sessions"
228
+ aria-expanded={mobileOpen}
229
+ >
230
+ <MessageSquare className="w-5 h-5" />
231
+ </Button>
232
+
233
+ {mobileOpen && (
234
+ <button
235
+ type="button"
236
+ className="fixed inset-0 z-40 bg-background/70 backdrop-blur-sm md:hidden"
237
+ aria-label="Close chat sessions overlay"
238
+ onClick={() => setMobileOpen(false)}
239
+ />
240
+ )}
241
+
242
+ <aside
243
+ id="mobile-chat-sessions"
244
  className={cn(
245
+ "fixed inset-y-0 left-0 z-50 flex w-72 flex-col border-r border-border/50 bg-card shadow-xl transition-transform duration-300 ease-out md:hidden",
246
+ mobileOpen ? "translate-x-0" : "-translate-x-full"
247
  )}
248
+ aria-label="Chat sessions"
249
+ aria-hidden={!mobileOpen}
250
+ inert={!mobileOpen ? true : undefined}
251
  >
252
+ {sessionsContent(true)}
253
+ </aside>
254
+ </>
255
  );
256
  }