@@ -19,24 +19,45 @@ import {
1919 MagnifyingGlassIcon ,
2020 ShareFatIcon ,
2121 TrashIcon ,
22+ DotsThreeIcon ,
2223} from "@phosphor-icons/react" ;
24+ import { EllipsisIcon } from "lucide-react" ;
2325import { toast } from "sonner" ;
2426import { usePathname , useRouter } from "next/navigation" ;
2527import { Logo } from "../svgs/logo" ;
2628import { useUser } from "@/hooks/useUser" ;
2729import Link from "next/link" ;
2830import { useExecutionContext } from "@/contexts/execution-context" ;
31+ import {
32+ Dialog ,
33+ DialogDescription ,
34+ DialogContent ,
35+ DialogTrigger ,
36+ DialogTitle ,
37+ DialogHeader ,
38+ DialogClose ,
39+ DialogFooter ,
40+ } from "@/components/ui/dialog" ;
41+ import {
42+ DropdownMenu ,
43+ DropdownMenuContent ,
44+ DropdownMenuItem ,
45+ DropdownMenuTrigger ,
46+ } from "@/components/ui/dropdown-menu" ;
47+ import { Input } from "@/components/ui/input" ;
2948import { Execution } from "@/hooks/useExecution" ;
3049import { BACKEND_URL , cn } from "@/lib/utils" ;
3150
3251export function UIStructure ( ) {
3352 const [ uiExecutions , setUiExecutions ] = useState < Execution [ ] > ( [ ] ) ;
53+ const [ openMenuId , setOpenMenuId ] = useState < string | null > ( null ) ;
3454 const [ hoverChatId , setHoverChatId ] = useState < string > ( "" ) ;
3555 const [ isAppsDialogOpen , setIsAppsDialogOpen ] = useState ( false ) ;
56+ const [ isSearchDialogOpen , setIsSearchDialogOpen ] = useState ( false ) ;
3657 const [ searchTerm , setSearchTerm ] = useState < string > ( "" ) ;
3758 const { executions, loading, createNewExecution } = useExecutionContext ( ) ;
3859 const router = useRouter ( ) ;
39-
60+
4061 const pathname = usePathname ( ) ;
4162 const currentConversationId = pathname . split ( "/" ) . pop ( ) ;
4263
@@ -48,28 +69,30 @@ export function UIStructure() {
4869 } else {
4970 setUiExecutions (
5071 executions . filter ( ( execution ) =>
51- ( execution . title ?? "" ) . toLowerCase ( ) . includes ( term )
52- )
72+ ( execution . title ?? "" ) . toLowerCase ( ) . includes ( term ) ,
73+ ) ,
5374 ) ;
5475 }
5576 }
5677 } , [ executions , searchTerm ] ) ;
5778
58- const handleDeleteExecution = async ( executionId : string ) => {
59- try {
60- await fetch ( `${ BACKEND_URL } /ai/chat/${ executionId } ` , {
61- method : "DELETE" ,
62- headers : {
63- "Content-Type" : "application/json" ,
64- "Authorization" : `Bearer ${ localStorage . getItem ( "token" ) } `
65- }
66- } ) ;
67- setUiExecutions ( executions . filter ( ( execution ) => execution . id !== executionId ) ) ;
68- toast . success ( "Chat deleted successfully" ) ;
69- } catch ( error ) {
70- console . error ( "Error deleting chat:" , error ) ;
71- }
72- } ;
79+ const handleDeleteExecution = async ( executionId : string ) => {
80+ try {
81+ await fetch ( `${ BACKEND_URL } /ai/chat/${ executionId } ` , {
82+ method : "DELETE" ,
83+ headers : {
84+ "Content-Type" : "application/json" ,
85+ Authorization : `Bearer ${ localStorage . getItem ( "token" ) } ` ,
86+ } ,
87+ } ) ;
88+ setUiExecutions (
89+ executions . filter ( ( execution ) => execution . id !== executionId ) ,
90+ ) ;
91+ toast . success ( "Chat deleted successfully" ) ;
92+ } catch ( error ) {
93+ console . error ( "Error deleting chat:" , error ) ;
94+ }
95+ } ;
7396
7497 // Available AI Apps
7598 const availableApps = [
@@ -91,7 +114,7 @@ export function UIStructure() {
91114 return (
92115 < Sidebar className = { `border py-2 pl-2` } >
93116 < SidebarContent className = "h-full justify-between" >
94- < SidebarGroup className = "flex flex-col gap-4 " >
117+ < SidebarGroup className = "flex flex-col gap-8 " >
95118 < SidebarHeader className = "sticky top-0 !p-0" >
96119 < div className = "flex w-full flex-col items-center gap-2 rounded-lg" >
97120 < div className = "flex w-full items-center gap-2 rounded-lg p-1 text-lg justify-between" >
@@ -112,14 +135,62 @@ export function UIStructure() {
112135 </ Button >
113136 </ div >
114137
115- < div className = "relative flex items-center p-2 gap-2 bg-zinc-400/10 px-2 rounded-xl border border-black/10 dark:border-zinc-400/8 inset-shadow-sm cursor-pointer hover:bg-orange-200/10 dark:hover:bg-orange-100/10 dark:hover:[&>*]:text-orange-100 hover:[&>*]:text-orange-400" >
116- < MagnifyingGlassIcon className = "text-foreground" size = { 18 } />
117- < p className = "rounded-none text-sm border-none bg-transparent px-0 shadow-none ring-0 focus-visible:ring-0 dark:bg-transparent" >
118- Search for chats
119- </ p >
120- </ div >
138+ < Dialog
139+ open = { isSearchDialogOpen }
140+ onOpenChange = { setIsSearchDialogOpen }
141+ >
142+ < DialogTrigger className = "relative flex items-center p-2 gap-2 bg-zinc-400/10 px-2 rounded-xl border border-black/10 dark:border-zinc-400/8 inset-shadow-sm cursor-pointer hover:bg-orange-200/10 dark:hover:bg-orange-100/10 dark:hover:[&>*]:text-orange-100 hover:[&>*]:text-orange-400" >
143+ < MagnifyingGlassIcon className = "text-foreground" size = { 18 } />
144+ < p className = "rounded-none text-sm border-none bg-transparent px-0 shadow-none ring-0 focus-visible:ring-0 dark:bg-transparent" >
145+ Search for chats
146+ </ p >
147+ </ DialogTrigger >
121148
149+ < DialogContent className = "sm:max-w-[35rem] rounded-xl p-0 overflow-auto gap-0 border-none" >
150+ < DialogHeader className = "bg-transparent border-b dark:border-b-orange-100/5" >
151+ < Input
152+ placeholder = "Search for chats"
153+ value = { searchTerm }
154+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
155+ className = "border-none rounded-none text-sm rounded-tl-xl rounded-tr-xl shadow-none focus-visible:ring-0 bg-transparent dark:bg-transparent px-4 py-6"
156+ />
157+ </ DialogHeader >
158+ < div className = "grid min-h-[8rem] max-h-[15rem] overflow-auto pb-3" >
159+ < div className = "flex flex-col px-2 justify-center" >
160+ { uiExecutions . length === 0 ? (
161+ < div className = "flex items-center justify-center" >
162+ < p className = "text-xs" > No chat found!</ p >
163+ </ div >
164+ ) : (
165+ < div >
166+ < DialogTitle className = "font-normal tracking-wider text-xs text-orange-100/50 p-2" >
167+ Chats
168+ </ DialogTitle >
169+ { uiExecutions . map ( ( execution : Execution ) => (
170+ < div key = { execution . id } className = "" >
171+ < div
172+ className = { `p-2 rounded-lg hover:bg-orange-100/10 hover:text-orange-100` }
173+ onMouseEnter = { ( ) => setHoverChatId ( execution . id ) }
174+ onMouseLeave = { ( ) => setHoverChatId ( "" ) }
175+ onClick = { ( ) => {
176+ router . push ( `/ask/${ execution . id } ` ) ;
177+ setIsSearchDialogOpen ( false ) ;
178+ } }
179+ >
180+ < div className = "flex w-full items-center justify-between text-sm" >
181+ < span className = "" > { execution . title } </ span >
182+ </ div >
183+ </ div >
184+ </ div >
185+ ) ) }
186+ </ div >
187+ ) }
188+ </ div >
189+ </ div >
190+ </ DialogContent >
191+ </ Dialog >
122192 </ SidebarHeader >
193+
123194 < SidebarGroupContent >
124195 < SidebarMenu className = "w-full p-0" >
125196 { loading
@@ -133,29 +204,67 @@ export function UIStructure() {
133204 : uiExecutions . map ( ( execution : Execution ) => (
134205 < SidebarMenuItem key = { execution . id } >
135206 < SidebarMenuButton
136- className = { `group relative w-full text-left hover:bg-primary /20 transition ${ execution . id === currentConversationId ? "bg-primary /20" : "" } ` }
207+ className = { `group relative font-normal cursor-pointer rounded-xl w-full active:bg-transparent active: text-inherit hover:bg-orange-300 /20 hover:text-orange-400 dark:hover:text-orange-100 dark:hover:bg-orange-100/10 ${ execution . id === currentConversationId ? "bg-orange-300 /20 text-orange-400 dark:text-orange-100 dark:bg-orange-100/10 " : "" } ` }
137208 onMouseEnter = { ( ) => setHoverChatId ( execution . id ) }
138209 onMouseLeave = { ( ) => setHoverChatId ( "" ) }
139210 onClick = { ( ) => router . push ( `/ask/${ execution . id } ` ) }
140211 >
141212 < div className = "flex w-full items-center justify-between" >
142- < span className = "z-[-1] cursor-pointer truncate" >
213+ < span className = "cursor-pointer truncate" >
143214 { execution . title }
144215 </ span >
145- < div
146- className = { cn (
147- "absolute top-0 right-0 z-[5] h-full w-12 rounded-r-md blur-[2em]" ,
148- execution . id === hoverChatId && "bg-primary"
149- ) }
150- />
151216
152- < div
153- className = { cn (
154- "absolute top-1/2 -right-16 z-[10] flex h-full -translate-y-1/2 items-center justify-center gap-1.5 rounded-r-md bg-transparent px-1 backdrop-blur-xl transition-all duration-200 ease-in-out" ,
155- execution . id === hoverChatId && "group-hover:right-0"
156- ) }
157- >
158- < div
217+ < div className = "realtive group flex items-center" >
218+ < DropdownMenu
219+ open = { openMenuId === execution . id }
220+ onOpenChange = { ( isOpen : boolean ) =>
221+ setOpenMenuId ( isOpen ? execution . id : null )
222+ }
223+ >
224+ < DropdownMenuTrigger asChild >
225+ < button
226+ className = { cn (
227+ "opacity-0 transition-opacity" ,
228+ {
229+ "opacity-100" :
230+ execution . id === hoverChatId ||
231+ openMenuId === execution . id ,
232+ } ,
233+ ) }
234+ >
235+ < EllipsisIcon className = "size-5 text-white" />
236+ </ button >
237+ </ DropdownMenuTrigger >
238+
239+ < DropdownMenuContent className = "border-none rounded-xl" >
240+ < DropdownMenuItem
241+ className = "focus:bg-orange-300/20 focus:text-orange-400 darK:focus:bg-orange-100/10 dark:focus:text-orange-100 rounded-xl"
242+ onClick = { ( ) => {
243+ const shareLink = `${ process . env . NEXT_PUBLIC_APP_URL } /ask/${ execution . id } ` ;
244+ navigator . clipboard . writeText ( shareLink ) ;
245+ toast . success (
246+ "Share link copied to clipboard" ,
247+ ) ;
248+ setOpenMenuId ( null ) ;
249+ } }
250+ >
251+ < ShareFatIcon className = "focus:text-orange-400 dark:text-orange-100" />
252+ Share
253+ </ DropdownMenuItem >
254+
255+ < DropdownMenuItem
256+ onClick = { ( ) => {
257+ handleDeleteExecution ( execution . id ) ;
258+ setOpenMenuId ( null ) ;
259+ } }
260+ className = "focus:bg-red-400/10 text-red-400 rounded-xl focus:text-red-400"
261+ >
262+ < TrashIcon className = "text-red-400" />
263+ Delete
264+ </ DropdownMenuItem >
265+ </ DropdownMenuContent >
266+ </ DropdownMenu >
267+ { /* <div
159268 className="flex items-center justify-center rounded-md"
160269 onClick={(e) => {
161270 e.preventDefault();
@@ -170,9 +279,9 @@ export function UIStructure() {
170279 weight="fill"
171280 className="hover:text-foreground size-4"
172281 />
173- </ div >
282+ </div> */ }
174283
175- < div
284+ { /* <div
176285 className="flex items-center justify-center rounded-md"
177286 onClick={(e) => {
178287 e.stopPropagation();
@@ -183,7 +292,7 @@ export function UIStructure() {
183292 weight={"bold"}
184293 className="hover:text-foreground size-4"
185294 />
186- </ div >
295+ </div> */ }
187296 </ div >
188297 </ div >
189298 </ SidebarMenuButton >
@@ -193,7 +302,7 @@ export function UIStructure() {
193302 </ SidebarGroupContent >
194303 </ SidebarGroup >
195304
196- { /* <SidebarFooter className="sticky bottom-0 flex flex-col gap-2 w-full p-3 bg-background">
305+ { /* <SidebarFooter className="sticky bottom-0 flex flex-col gap-2 w-full p-3 bg-background">
197306 {!isUserLoading && !user && (
198307 <Link href="/auth">
199308 <Button variant="secondary" className="w-full" size="lg">
@@ -247,11 +356,10 @@ export function UIStructure() {
247356 </DialogContent>
248357 </Dialog> */ }
249358
250-
251359 < SidebarFooter className = "sticky bottom-0 flex flex-col gap-2 w-full p-3 bg-transparent" >
252360 < NavSetting />
253361
254- { /* {user && (
362+ { /* {user && (
255363 <Dialog open={isAppsDialogOpen} onOpenChange={setIsAppsDialogOpen}>
256364 <DialogTrigger asChild>
257365 <Button variant="secondary" className="w-full " size="lg">
@@ -298,7 +406,7 @@ export function UIStructure() {
298406 </Dialog>
299407 )} */ }
300408
301- { /* {user && (
409+ { /* {user && (
302410 <Button
303411 variant="destructive"
304412 className="w-full text-sm text-white h-10 px-3.5 rounded-xl inset-shadow-sm inset-shadow-white/60 font-medium border border-black/4 outline-0"
0 commit comments