Spaces:
Running
Running
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 |
-
|
| 83 |
-
<div className=
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
<
|
| 87 |
-
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 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 |
-
{
|
| 144 |
-
<
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 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={() =>
|
| 173 |
-
|
| 174 |
size="icon"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
className={cn(
|
| 176 |
-
"
|
| 177 |
-
|
| 178 |
)}
|
|
|
|
|
|
|
|
|
|
| 179 |
>
|
| 180 |
-
|
| 181 |
-
</
|
| 182 |
-
</
|
| 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 "+" 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 |
}
|