@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
4
4
import { Dialog , DialogButton , DialogDescription , DialogRoot , DialogTitle } from '~/components/ui/Dialog' ;
5
5
import { IconButton } from '~/components/ui/IconButton' ;
6
6
import { ThemeSwitch } from '~/components/ui/ThemeSwitch' ;
7
- import { db , deleteById , getAll , chatId , type ChatHistoryItem } from '~/lib/persistence' ;
7
+ import { db , deleteById , getAll , chatId , type ChatHistoryItem , setMessages } from '~/lib/persistence' ;
8
8
import { cubicEasingFn } from '~/utils/easings' ;
9
9
import { logger } from '~/utils/logger' ;
10
10
import { HistoryItem } from './HistoryItem' ;
@@ -31,13 +31,17 @@ const menuVariants = {
31
31
} ,
32
32
} satisfies Variants ;
33
33
34
- type DialogContent = { type : 'delete' ; item : ChatHistoryItem } | null ;
34
+ type DialogContent =
35
+ | { type : 'delete' ; item : ChatHistoryItem }
36
+ | { type : 'rename' ; item : ChatHistoryItem }
37
+ | null ;
35
38
36
39
export function Menu ( ) {
37
40
const menuRef = useRef < HTMLDivElement > ( null ) ;
38
41
const [ list , setList ] = useState < ChatHistoryItem [ ] > ( [ ] ) ;
39
42
const [ open , setOpen ] = useState ( false ) ;
40
43
const [ dialogContent , setDialogContent ] = useState < DialogContent > ( null ) ;
44
+ const [ newName , setNewName ] = useState ( '' ) ;
41
45
42
46
const loadEntries = useCallback ( ( ) => {
43
47
if ( db ) {
@@ -68,6 +72,43 @@ export function Menu() {
68
72
}
69
73
} , [ ] ) ;
70
74
75
+ const renameItem = useCallback ( async ( event : React . UIEvent , item : ChatHistoryItem , newDescription : string ) => {
76
+ event . preventDefault ( ) ;
77
+
78
+ if ( db ) {
79
+ try {
80
+ await setMessages ( db , item . id , item . messages , item . urlId , newDescription ) ;
81
+ loadEntries ( ) ;
82
+ toast . success ( 'Chat renamed successfully' ) ;
83
+ } catch ( error ) {
84
+ toast . error ( 'Failed to rename chat' ) ;
85
+ logger . error ( error ) ;
86
+ }
87
+ }
88
+ } , [ ] ) ;
89
+
90
+ const exportItem = useCallback ( ( event : React . UIEvent , item : ChatHistoryItem ) => {
91
+ event . preventDefault ( ) ;
92
+
93
+ const exportData = {
94
+ description : item . description ,
95
+ messages : item . messages ,
96
+ timestamp : item . timestamp
97
+ } ;
98
+
99
+ const blob = new Blob ( [ JSON . stringify ( exportData , null , 2 ) ] , { type : 'application/json' } ) ;
100
+ const url = URL . createObjectURL ( blob ) ;
101
+ const a = document . createElement ( 'a' ) ;
102
+ a . href = url ;
103
+ a . download = `chat-${ item . description || 'export' } .json` ;
104
+ document . body . appendChild ( a ) ;
105
+ a . click ( ) ;
106
+ document . body . removeChild ( a ) ;
107
+ URL . revokeObjectURL ( url ) ;
108
+
109
+ toast . success ( 'Chat exported successfully' ) ;
110
+ } , [ ] ) ;
111
+
71
112
const closeDialog = ( ) => {
72
113
setDialogContent ( null ) ;
73
114
} ;
@@ -102,24 +143,16 @@ export function Menu() {
102
143
return (
103
144
< motion . div
104
145
ref = { menuRef }
105
- initial = "closed"
106
- animate = { open ? 'open' : 'closed' }
146
+ className = "fixed top-0 bottom-0 w-[300px] bg-bolt-elements-background-depth-2 border-r border-bolt-elements-borderColor z-sidebar"
107
147
variants = { menuVariants }
108
- className = "flex flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
148
+ animate = { open ? 'open' : 'closed' }
149
+ initial = "closed"
109
150
>
110
- < div className = "flex items-center h-[var(--header-height)]" > { /* Placeholder */ } </ div >
111
- < div className = "flex-1 flex flex-col h-full w-full overflow-hidden" >
112
- < div className = "p-4" >
113
- < a
114
- href = "/"
115
- className = "flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
116
- >
117
- < span className = "inline-block i-bolt:chat scale-110" />
118
- Start new chat
119
- </ a >
151
+ < div className = "h-full flex flex-col" >
152
+ < div className = "sticky top-0 z-1 bg-bolt-elements-background-depth-2 p-4 pt-12 flex justify-between items-center border-b border-bolt-elements-borderColor" >
153
+ < div className = "text-bolt-elements-textPrimary font-medium" > History</ div >
120
154
</ div >
121
- < div className = "text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2" > Your Chats</ div >
122
- < div className = "flex-1 overflow-scroll pl-4 pr-5 pb-5" >
155
+ < div className = "flex-1 overflow-y-auto p-2 pb-16" >
123
156
{ list . length === 0 && < div className = "pl-2 text-bolt-elements-textTertiary" > No previous conversations</ div > }
124
157
< DialogRoot open = { dialogContent !== null } >
125
158
{ binDates ( list ) . map ( ( { category, items } ) => (
@@ -128,7 +161,16 @@ export function Menu() {
128
161
{ category }
129
162
</ div >
130
163
{ items . map ( ( item ) => (
131
- < HistoryItem key = { item . id } item = { item } onDelete = { ( ) => setDialogContent ( { type : 'delete' , item } ) } />
164
+ < HistoryItem
165
+ key = { item . id }
166
+ item = { item }
167
+ onDelete = { ( ) => setDialogContent ( { type : 'delete' , item } ) }
168
+ onRename = { ( ) => {
169
+ setNewName ( item . description || '' ) ;
170
+ setDialogContent ( { type : 'rename' , item } ) ;
171
+ } }
172
+ onExport = { ( event ) => exportItem ( event , item ) }
173
+ />
132
174
) ) }
133
175
</ div >
134
176
) ) }
@@ -160,12 +202,45 @@ export function Menu() {
160
202
</ div >
161
203
</ >
162
204
) }
205
+ { dialogContent ?. type === 'rename' && (
206
+ < >
207
+ < DialogTitle > Rename Chat</ DialogTitle >
208
+ < DialogDescription asChild >
209
+ < div >
210
+ < input
211
+ type = "text"
212
+ value = { newName }
213
+ onChange = { ( e ) => setNewName ( e . target . value ) }
214
+ className = "w-full p-2 mt-2 text-bolt-elements-textPrimary bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md focus:outline-none focus:border-bolt-elements-borderColorFocus"
215
+ placeholder = "Enter new name"
216
+ autoFocus
217
+ />
218
+ </ div >
219
+ </ DialogDescription >
220
+ < div className = "px-5 pb-4 bg-bolt-elements-background-depth-2 flex gap-2 justify-end" >
221
+ < DialogButton type = "secondary" onClick = { closeDialog } >
222
+ Cancel
223
+ </ DialogButton >
224
+ < DialogButton
225
+ type = "primary"
226
+ onClick = { ( event ) => {
227
+ if ( newName . trim ( ) ) {
228
+ renameItem ( event , dialogContent . item , newName . trim ( ) ) ;
229
+ closeDialog ( ) ;
230
+ }
231
+ } }
232
+ >
233
+ Rename
234
+ </ DialogButton >
235
+ </ div >
236
+ </ >
237
+ ) }
163
238
</ Dialog >
164
239
</ DialogRoot >
165
240
</ div >
166
- < div className = "flex items-center border-t border-bolt-elements-borderColor p-4" >
167
- < ThemeSwitch className = "ml-auto" / >
168
- </ div >
241
+ </ div >
242
+ < div className = "absolute bottom-4 right-4" >
243
+ < ThemeSwitch / >
169
244
</ div >
170
245
</ motion . div >
171
246
) ;
0 commit comments