1- import { useEffect , useState } from 'react' ;
1+ import { useEffect , useMemo , useState } from 'react' ;
22import { classNames } from '../utils/misc' ;
33import { Conversation } from '../utils/types' ;
44import StorageUtils from '../utils/storage' ;
@@ -38,6 +38,11 @@ export default function Sidebar() {
3838 } ;
3939 } , [ ] ) ;
4040
41+ const groupedConv = useMemo (
42+ ( ) => groupConversationsByDate ( conversations ) ,
43+ [ conversations ]
44+ ) ;
45+
4146 return (
4247 < >
4348 < input
@@ -63,69 +68,90 @@ export default function Sidebar() {
6368 </ label >
6469 </ div >
6570
66- { /* list of conversations */ }
71+ { /* new conversation button */ }
6772 < div
6873 className = { classNames ( {
69- 'btn btn-ghost justify-start' : true ,
74+ 'btn btn-ghost justify-start px-2 ' : true ,
7075 'btn-soft' : ! currConv ,
7176 } ) }
7277 onClick = { ( ) => navigate ( '/' ) }
7378 >
7479 + New conversation
7580 </ div >
76- { conversations . map ( ( conv ) => (
77- < ConversationItem
78- key = { conv . id }
79- conv = { conv }
80- isCurrConv = { currConv ?. id === conv . id }
81- onSelect = { ( ) => {
82- navigate ( `/chat/${ conv . id } ` ) ;
83- } }
84- onDelete = { ( ) => {
85- if ( isGenerating ( conv . id ) ) {
86- toast . error ( 'Cannot delete conversation while generating' ) ;
87- return ;
88- }
89- if (
90- window . confirm ( 'Are you sure to delete this conversation?' )
91- ) {
92- toast . success ( 'Conversation deleted' ) ;
93- StorageUtils . remove ( conv . id ) ;
94- navigate ( '/' ) ;
95- }
96- } }
97- onDownload = { ( ) => {
98- if ( isGenerating ( conv . id ) ) {
99- toast . error ( 'Cannot download conversation while generating' ) ;
100- return ;
101- }
102- const conversationJson = JSON . stringify ( conv , null , 2 ) ;
103- const blob = new Blob ( [ conversationJson ] , {
104- type : 'application/json' ,
105- } ) ;
106- const url = URL . createObjectURL ( blob ) ;
107- const a = document . createElement ( 'a' ) ;
108- a . href = url ;
109- a . download = `conversation_${ conv . id } .json` ;
110- document . body . appendChild ( a ) ;
111- a . click ( ) ;
112- document . body . removeChild ( a ) ;
113- URL . revokeObjectURL ( url ) ;
114- } }
115- onRename = { ( ) => {
116- if ( isGenerating ( conv . id ) ) {
117- toast . error ( 'Cannot rename conversation while generating' ) ;
118- return ;
119- }
120- const newName = window . prompt (
121- 'Enter new name for the conversation' ,
122- conv . name
123- ) ;
124- if ( newName && newName . trim ( ) . length > 0 ) {
125- StorageUtils . updateConversationName ( conv . id , newName ) ;
126- }
127- } }
128- />
81+
82+ { /* list of conversations */ }
83+ { groupedConv . map ( ( group ) => (
84+ < div >
85+ { /* group name (by date) */ }
86+ { group . title . length ? (
87+ < b className = "block text-xs px-2 mb-2 mt-6" > { group . title } </ b >
88+ ) : (
89+ < div className = "h-2" />
90+ ) }
91+
92+ { group . conversations . map ( ( conv ) => (
93+ < ConversationItem
94+ key = { conv . id }
95+ conv = { conv }
96+ isCurrConv = { currConv ?. id === conv . id }
97+ onSelect = { ( ) => {
98+ navigate ( `/chat/${ conv . id } ` ) ;
99+ } }
100+ onDelete = { ( ) => {
101+ if ( isGenerating ( conv . id ) ) {
102+ toast . error (
103+ 'Cannot delete conversation while generating'
104+ ) ;
105+ return ;
106+ }
107+ if (
108+ window . confirm (
109+ 'Are you sure to delete this conversation?'
110+ )
111+ ) {
112+ toast . success ( 'Conversation deleted' ) ;
113+ StorageUtils . remove ( conv . id ) ;
114+ navigate ( '/' ) ;
115+ }
116+ } }
117+ onDownload = { ( ) => {
118+ if ( isGenerating ( conv . id ) ) {
119+ toast . error (
120+ 'Cannot download conversation while generating'
121+ ) ;
122+ return ;
123+ }
124+ const conversationJson = JSON . stringify ( conv , null , 2 ) ;
125+ const blob = new Blob ( [ conversationJson ] , {
126+ type : 'application/json' ,
127+ } ) ;
128+ const url = URL . createObjectURL ( blob ) ;
129+ const a = document . createElement ( 'a' ) ;
130+ a . href = url ;
131+ a . download = `conversation_${ conv . id } .json` ;
132+ document . body . appendChild ( a ) ;
133+ a . click ( ) ;
134+ document . body . removeChild ( a ) ;
135+ URL . revokeObjectURL ( url ) ;
136+ } }
137+ onRename = { ( ) => {
138+ if ( isGenerating ( conv . id ) ) {
139+ toast . error (
140+ 'Cannot rename conversation while generating'
141+ ) ;
142+ return ;
143+ }
144+ const newName = window . prompt (
145+ 'Enter new name for the conversation' ,
146+ conv . name
147+ ) ;
148+ if ( newName && newName . trim ( ) . length > 0 ) {
149+ StorageUtils . updateConversationName ( conv . id , newName ) ;
150+ }
151+ } }
152+ />
153+ ) ) }
154+ </ div >
129155 ) ) }
130156 < div className = "text-center text-xs opacity-40 mt-auto mx-4" >
131157 Conversations are saved to browser's IndexedDB
@@ -154,7 +180,7 @@ function ConversationItem({
154180 return (
155181 < div
156182 className = { classNames ( {
157- 'group flex flex-row btn btn-ghost justify-start items-center font-normal pr-2 ' :
183+ 'group flex flex-row btn btn-ghost justify-start items-center font-normal px-2 h-9 ' :
158184 true ,
159185 'btn-soft' : isCurrConv ,
160186 } ) }
@@ -206,3 +232,99 @@ function ConversationItem({
206232 </ div >
207233 ) ;
208234}
235+
236+ // WARN: vibe code below
237+
238+ export interface GroupedConversations {
239+ title : string ;
240+ conversations : Conversation [ ] ;
241+ }
242+
243+ // TODO @ngxson : add test for this function
244+ // Group conversations by date
245+ // - "Previous 7 Days"
246+ // - "Previous 30 Days"
247+ // - "Month Year" (e.g., "April 2023")
248+ export function groupConversationsByDate (
249+ conversations : Conversation [ ]
250+ ) : GroupedConversations [ ] {
251+ const now = new Date ( ) ;
252+ const today = new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) ; // Start of today
253+
254+ const sevenDaysAgo = new Date ( today ) ;
255+ sevenDaysAgo . setDate ( today . getDate ( ) - 7 ) ;
256+
257+ const thirtyDaysAgo = new Date ( today ) ;
258+ thirtyDaysAgo . setDate ( today . getDate ( ) - 30 ) ;
259+
260+ const groups : { [ key : string ] : Conversation [ ] } = {
261+ Today : [ ] ,
262+ 'Previous 7 Days' : [ ] ,
263+ 'Previous 30 Days' : [ ] ,
264+ } ;
265+ const monthlyGroups : { [ key : string ] : Conversation [ ] } = { } ; // Key format: "Month Year" e.g., "April 2023"
266+
267+ // Sort conversations by lastModified date in descending order (newest first)
268+ // This helps when adding to groups, but the final output order of groups is fixed.
269+ const sortedConversations = [ ...conversations ] . sort (
270+ ( a , b ) => b . lastModified - a . lastModified
271+ ) ;
272+
273+ for ( const conv of sortedConversations ) {
274+ const convDate = new Date ( conv . lastModified ) ;
275+
276+ if ( convDate >= today ) {
277+ groups [ 'Today' ] . push ( conv ) ;
278+ } else if ( convDate >= sevenDaysAgo ) {
279+ groups [ 'Previous 7 Days' ] . push ( conv ) ;
280+ } else if ( convDate >= thirtyDaysAgo ) {
281+ groups [ 'Previous 30 Days' ] . push ( conv ) ;
282+ } else {
283+ const monthName = convDate . toLocaleString ( 'default' , { month : 'long' } ) ;
284+ const year = convDate . getFullYear ( ) ;
285+ const monthYearKey = `${ monthName } ${ year } ` ;
286+ if ( ! monthlyGroups [ monthYearKey ] ) {
287+ monthlyGroups [ monthYearKey ] = [ ] ;
288+ }
289+ monthlyGroups [ monthYearKey ] . push ( conv ) ;
290+ }
291+ }
292+
293+ const result : GroupedConversations [ ] = [ ] ;
294+
295+ if ( groups [ 'Today' ] . length > 0 ) {
296+ result . push ( {
297+ title : '' , // no title for Today
298+ conversations : groups [ 'Today' ] ,
299+ } ) ;
300+ }
301+
302+ if ( groups [ 'Previous 7 Days' ] . length > 0 ) {
303+ result . push ( {
304+ title : 'Previous 7 Days' ,
305+ conversations : groups [ 'Previous 7 Days' ] ,
306+ } ) ;
307+ }
308+
309+ if ( groups [ 'Previous 30 Days' ] . length > 0 ) {
310+ result . push ( {
311+ title : 'Previous 30 Days' ,
312+ conversations : groups [ 'Previous 30 Days' ] ,
313+ } ) ;
314+ }
315+
316+ // Sort monthly groups by date (most recent month first)
317+ const sortedMonthKeys = Object . keys ( monthlyGroups ) . sort ( ( a , b ) => {
318+ const dateA = new Date ( a ) ; // "Month Year" can be parsed by Date constructor
319+ const dateB = new Date ( b ) ;
320+ return dateB . getTime ( ) - dateA . getTime ( ) ;
321+ } ) ;
322+
323+ for ( const monthKey of sortedMonthKeys ) {
324+ if ( monthlyGroups [ monthKey ] . length > 0 ) {
325+ result . push ( { title : monthKey , conversations : monthlyGroups [ monthKey ] } ) ;
326+ }
327+ }
328+
329+ return result ;
330+ }
0 commit comments