@@ -12,6 +12,8 @@ import {
12
12
LogOut ,
13
13
WaypointsIcon ,
14
14
Gauge ,
15
+ HistoryIcon ,
16
+ Trash2 ,
15
17
} from "lucide-react"
16
18
import {
17
19
DropdownMenuContent ,
@@ -26,20 +28,57 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
26
28
import { useAuth } from "@lib/auth-context"
27
29
import { ConnectAIModal } from "./connect-ai-modal"
28
30
import { useTheme } from "next-themes"
29
- import { cn } from "@lib/utils"
30
31
import { usePathname , useRouter } from "next/navigation"
31
32
import { MCPIcon } from "./menu"
32
33
import { authClient } from "@lib/auth"
33
34
import { analytics } from "@/lib/analytics"
34
- import { useGraphModal , usePersistentChat } from "@/stores"
35
+ import { useGraphModal , usePersistentChat , useProject } from "@/stores"
36
+ import {
37
+ Dialog ,
38
+ DialogContent ,
39
+ DialogDescription ,
40
+ DialogHeader ,
41
+ DialogTitle ,
42
+ DialogTrigger ,
43
+ } from "@ui/components/dialog"
44
+ import { ScrollArea } from "@ui/components/scroll-area"
45
+ import { formatDistanceToNow } from "date-fns"
46
+ import { cn } from "@lib/utils"
47
+ import { useMemo , useState } from "react"
35
48
36
49
export function Header ( { onAddMemory } : { onAddMemory ?: ( ) => void } ) {
37
50
const { user } = useAuth ( )
38
51
const { theme, setTheme } = useTheme ( )
39
52
const router = useRouter ( )
40
53
const { setIsOpen : setGraphModalOpen } = useGraphModal ( )
41
- const { getCurrentChat } = usePersistentChat ( )
54
+ const {
55
+ getCurrentChat,
56
+ conversations,
57
+ currentChatId,
58
+ setCurrentChatId,
59
+ deleteConversation,
60
+ } = usePersistentChat ( )
61
+ const { selectedProject } = useProject ( )
42
62
const pathname = usePathname ( )
63
+ const [ isDialogOpen , setIsDialogOpen ] = useState ( false )
64
+
65
+ const sorted = useMemo ( ( ) => {
66
+ return [ ...conversations ] . sort ( ( a , b ) =>
67
+ a . lastUpdated < b . lastUpdated ? 1 : - 1 ,
68
+ )
69
+ } , [ conversations ] )
70
+
71
+ function handleNewChat ( ) {
72
+ analytics . newChatStarted ( )
73
+ const newId = crypto . randomUUID ( )
74
+ setCurrentChatId ( newId )
75
+ router . push ( `/chat/${ newId } ` )
76
+ setIsDialogOpen ( false )
77
+ }
78
+
79
+ function formatRelativeTime ( isoString : string ) : string {
80
+ return formatDistanceToNow ( new Date ( isoString ) , { addSuffix : true } )
81
+ }
43
82
44
83
const handleSignOut = ( ) => {
45
84
analytics . userSignedOut ( )
@@ -87,6 +126,106 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
87
126
c
88
127
</ span >
89
128
</ Button >
129
+ < Dialog
130
+ open = { isDialogOpen }
131
+ onOpenChange = { ( open ) => {
132
+ setIsDialogOpen ( open )
133
+ if ( open ) {
134
+ analytics . chatHistoryViewed ( )
135
+ }
136
+ } }
137
+ >
138
+ < Tooltip >
139
+ < TooltipTrigger asChild >
140
+ < DialogTrigger asChild >
141
+ < Button variant = "ghost" size = "sm" >
142
+ < HistoryIcon className = "h-4 w-4" />
143
+ </ Button >
144
+ </ DialogTrigger >
145
+ </ TooltipTrigger >
146
+ < TooltipContent >
147
+ < p > Chat History</ p >
148
+ </ TooltipContent >
149
+ </ Tooltip >
150
+ < DialogContent className = "sm:max-w-lg" >
151
+ < DialogHeader className = "pb-4 border-b rounded-t-lg" >
152
+ < DialogTitle className = "" > Conversations</ DialogTitle >
153
+ < DialogDescription >
154
+ Project{ " " }
155
+ < span className = "font-mono font-medium" >
156
+ { selectedProject }
157
+ </ span >
158
+ </ DialogDescription >
159
+ </ DialogHeader >
160
+
161
+ < ScrollArea className = "max-h-96" >
162
+ < div className = "flex flex-col gap-1" >
163
+ { sorted . map ( ( c ) => {
164
+ const isActive = c . id === currentChatId
165
+ return (
166
+ < button
167
+ key = { c . id }
168
+ type = "button"
169
+ onClick = { ( ) => {
170
+ setCurrentChatId ( c . id )
171
+ router . push ( `/chat/${ c . id } ` )
172
+ setIsDialogOpen ( false )
173
+ } }
174
+ className = { cn (
175
+ "flex items-center justify-between rounded-md px-3 py-2 outline-none w-full text-left" ,
176
+ "transition-colors" ,
177
+ isActive ? "bg-primary/10" : "hover:bg-muted" ,
178
+ ) }
179
+ aria-current = { isActive ? "true" : undefined }
180
+ >
181
+ < div className = "min-w-0" >
182
+ < div className = "flex items-center gap-2" >
183
+ < span
184
+ className = { cn (
185
+ "text-sm font-medium truncate" ,
186
+ isActive ? "text-foreground" : undefined ,
187
+ ) }
188
+ >
189
+ { c . title || "Untitled Chat" }
190
+ </ span >
191
+ </ div >
192
+ < div className = "text-xs text-muted-foreground" >
193
+ Last updated { formatRelativeTime ( c . lastUpdated ) }
194
+ </ div >
195
+ </ div >
196
+ < Button
197
+ type = "button"
198
+ variant = "ghost"
199
+ size = "icon"
200
+ onClick = { ( e ) => {
201
+ e . stopPropagation ( )
202
+ analytics . chatDeleted ( )
203
+ deleteConversation ( c . id )
204
+ } }
205
+ aria-label = "Delete conversation"
206
+ >
207
+ < Trash2 className = "size-4 text-muted-foreground" />
208
+ </ Button >
209
+ </ button >
210
+ )
211
+ } ) }
212
+ { sorted . length === 0 && (
213
+ < div className = "text-xs text-muted-foreground px-3 py-2" >
214
+ No conversations yet
215
+ </ div >
216
+ ) }
217
+ </ div >
218
+ </ ScrollArea >
219
+ < Button
220
+ variant = "outline"
221
+ size = "lg"
222
+ className = "w-full border-dashed"
223
+ onClick = { handleNewChat }
224
+ >
225
+ < Plus className = "size-4 mr-1" /> New Conversation
226
+ </ Button >
227
+ </ DialogContent >
228
+ </ Dialog >
90
229
< Tooltip >
91
230
< TooltipTrigger asChild >
92
231
< Button
0 commit comments