@@ -4,9 +4,10 @@ import { TranscriptionHistory } from "@/types";
44import { useCanRecord , useCanAutoInsert } from "@/contexts/ReadinessContext" ;
55import { invoke } from "@tauri-apps/api/core" ;
66import { ask } from "@tauri-apps/plugin-dialog" ;
7- import { AlertCircle , Mic , Trash2 } from "lucide-react" ;
8- import { useState } from "react" ;
7+ import { AlertCircle , Mic , Trash2 , Search , Copy , Calendar , Clock } from "lucide-react" ;
8+ import { useState , useMemo } from "react" ;
99import { toast } from "sonner" ;
10+ import { cn } from "@/lib/utils" ;
1011
1112interface RecentRecordingsProps {
1213 history : TranscriptionHistory [ ] ;
@@ -16,9 +17,56 @@ interface RecentRecordingsProps {
1617
1718export function RecentRecordings ( { history, hotkey = "Cmd+Shift+Space" , onHistoryUpdate } : RecentRecordingsProps ) {
1819 const [ hoveredId , setHoveredId ] = useState < string | null > ( null ) ;
20+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
1921 const canRecord = useCanRecord ( ) ;
2022 const canAutoInsert = useCanAutoInsert ( ) ;
2123
24+ // Filter history based on search query
25+ const filteredHistory = useMemo ( ( ) => {
26+ if ( ! searchQuery . trim ( ) ) return history ;
27+
28+ const query = searchQuery . toLowerCase ( ) ;
29+ return history . filter ( item =>
30+ item . text . toLowerCase ( ) . includes ( query ) ||
31+ ( item . model && item . model . toLowerCase ( ) . includes ( query ) )
32+ ) ;
33+ } , [ history , searchQuery ] ) ;
34+
35+ // Group history by date
36+ const groupedHistory = useMemo ( ( ) => {
37+ const groups : Record < string , TranscriptionHistory [ ] > = { } ;
38+ const today = new Date ( ) ;
39+ today . setHours ( 0 , 0 , 0 , 0 ) ;
40+ const yesterday = new Date ( today ) ;
41+ yesterday . setDate ( yesterday . getDate ( ) - 1 ) ;
42+
43+ filteredHistory . forEach ( item => {
44+ const itemDate = new Date ( item . timestamp ) ;
45+ itemDate . setHours ( 0 , 0 , 0 , 0 ) ;
46+
47+ let groupKey : string ;
48+ if ( itemDate . getTime ( ) === today . getTime ( ) ) {
49+ groupKey = "Today" ;
50+ } else if ( itemDate . getTime ( ) === yesterday . getTime ( ) ) {
51+ groupKey = "Yesterday" ;
52+ } else {
53+ groupKey = itemDate . toLocaleDateString ( 'en-US' , {
54+ weekday : 'long' ,
55+ month : 'short' ,
56+ day : 'numeric' ,
57+ year : itemDate . getFullYear ( ) !== today . getFullYear ( ) ? 'numeric' : undefined
58+ } ) ;
59+ }
60+
61+ if ( ! groups [ groupKey ] ) {
62+ groups [ groupKey ] = [ ] ;
63+ }
64+ groups [ groupKey ] . push ( item ) ;
65+ } ) ;
66+
67+ return groups ;
68+ } , [ filteredHistory ] ) ;
69+
2270 const handleCopy = ( text : string ) => {
2371 navigator . clipboard . writeText ( text ) ;
2472 toast . success ( "Copied to clipboard" ) ;
@@ -79,47 +127,147 @@ export function RecentRecordings({ history, hotkey = "Cmd+Shift+Space", onHistor
79127 } ;
80128
81129 return (
82- < div className = "h-full flex flex-col p-6" >
83- < div className = "flex items-center justify-between mb-4" >
84- < h2 className = "text-lg font-semibold" > Recent Transcriptions</ h2 >
85- { history . length > 0 && (
86- < button
87- onClick = { handleClearAll }
88- className = "flex items-center gap-2 px-3 py-1.5 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
89- title = "Clear all transcriptions"
90- >
91-
92- Clear All
93- </ button >
94- ) }
130+ < div className = "h-full flex flex-col" >
131+ { /* Header */ }
132+ < div className = "px-6 py-4 border-b border-border/40" >
133+ < div className = "flex items-center justify-between" >
134+ < div >
135+ < h1 className = "text-2xl font-semibold" > History</ h1 >
136+ < p className = "text-sm text-muted-foreground mt-1" >
137+ { history . length } total transcription{ history . length !== 1 ? 's' : '' }
138+ </ p >
139+ </ div >
140+ < div className = "flex items-center gap-3" >
141+ { history . length > 5 && (
142+ < button
143+ onClick = { handleClearAll }
144+ className = "flex items-center gap-2 px-3 py-1.5 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
145+ title = "Clear all transcriptions"
146+ >
147+ < Trash2 className = "h-3.5 w-3.5" />
148+ Clear All
149+ </ button >
150+ ) }
151+ </ div >
152+ </ div >
95153 </ div >
96- < div className = "flex-1 min-h-0" >
97- { history . length > 0 ? (
98- < ScrollArea className = "h-full" >
99- < div className = "flex flex-col gap-2.5" >
100- { history . map ( ( item ) => (
101- < div
102- key = { item . id }
103- className = "group flex items-center relative p-2 rounded-lg cursor-pointer bg-card hover:bg-accent/50 border border-border hover:border-accent transition-all duration-200"
104- onClick = { ( ) => handleCopy ( item . text ) }
105- onMouseEnter = { ( ) => setHoveredId ( item . id ) }
106- onMouseLeave = { ( ) => setHoveredId ( null ) }
107- title = "Click to copy"
154+
155+ { /* Search Bar */ }
156+ { history . length > 0 && (
157+ < div className = "px-6 py-3 border-b border-border/20" >
158+ < div className = "relative" >
159+ < Search className = "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
160+ < input
161+ type = "text"
162+ placeholder = "Search transcriptions..."
163+ value = { searchQuery }
164+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
165+ className = "w-full pl-10 pr-4 py-2 text-sm bg-background border border-border/50 rounded-lg focus:outline-none focus:border-primary/50 transition-colors"
166+ />
167+ { searchQuery && (
168+ < button
169+ onClick = { ( ) => setSearchQuery ( "" ) }
170+ className = "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
108171 >
109- < p className = "text-sm text-card-foreground transcription-text pr-4" > { item . text } </ p >
110- { hoveredId === item . id && (
111- < button
112- onClick = { ( e ) => handleDelete ( e , item . id ) }
113- className = "absolute top-0 right-0 m-1 p-1 rounded hover:bg-destructive/10 transition-colors"
114- title = "Delete"
115- >
116- < Trash2 className = "w-4 h-4 text-destructive" />
117- </ button >
118- ) }
119- </ div >
120- ) ) }
172+ ×
173+ </ button >
174+ ) }
175+ </ div >
176+ { searchQuery && (
177+ < p className = "text-xs text-muted-foreground mt-2" >
178+ Found { filteredHistory . length } result{ filteredHistory . length !== 1 ? 's' : '' }
179+ </ p >
180+ ) }
181+ </ div >
182+ ) }
183+ < div className = "flex-1 min-h-0 overflow-hidden" >
184+ { history . length > 0 ? (
185+ filteredHistory . length > 0 ? (
186+ < ScrollArea className = "h-full" >
187+ < div className = "px-6 py-4 space-y-6" >
188+ { Object . entries ( groupedHistory ) . map ( ( [ date , items ] ) => (
189+ < div key = { date } className = "space-y-3" >
190+ < div className = "flex items-center gap-2 text-xs font-medium text-muted-foreground" >
191+ < Calendar className = "h-3 w-3" />
192+ { date }
193+ < span className = "text-muted-foreground/50" > ({ items . length } )</ span >
194+ </ div >
195+ < div className = "space-y-2" >
196+ { items . map ( ( item ) => (
197+ < div
198+ key = { item . id }
199+ className = { cn (
200+ "group relative p-4 rounded-lg cursor-pointer" ,
201+ "bg-card border border-border/50" ,
202+ "hover:bg-accent/30 hover:border-border" ,
203+ "transition-all duration-200"
204+ ) }
205+ onClick = { ( ) => handleCopy ( item . text ) }
206+ onMouseEnter = { ( ) => setHoveredId ( item . id ) }
207+ onMouseLeave = { ( ) => setHoveredId ( null ) }
208+ >
209+ < div className = "flex items-start justify-between gap-4" >
210+ < div className = "flex-1 min-w-0" >
211+ < p className = "text-sm text-foreground leading-relaxed" >
212+ { item . text }
213+ </ p >
214+ < div className = "flex items-center gap-4 mt-2" >
215+ < span className = "flex items-center gap-1 text-xs text-muted-foreground" >
216+ < Clock className = "h-3 w-3" />
217+ { new Date ( item . timestamp ) . toLocaleTimeString ( 'en-US' , {
218+ hour : 'numeric' ,
219+ minute : '2-digit' ,
220+ hour12 : true
221+ } ) }
222+ </ span >
223+ { item . model && (
224+ < span className = "text-xs text-muted-foreground" >
225+ { item . model }
226+ </ span >
227+ ) }
228+ </ div >
229+ </ div >
230+ < div className = { cn (
231+ "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity" ,
232+ hoveredId === item . id && "opacity-100"
233+ ) } >
234+ < button
235+ onClick = { ( e ) => {
236+ e . stopPropagation ( ) ;
237+ handleCopy ( item . text ) ;
238+ } }
239+ className = "p-1.5 rounded hover:bg-accent transition-colors"
240+ title = "Copy"
241+ >
242+ < Copy className = "w-4 h-4 text-muted-foreground" />
243+ </ button >
244+ < button
245+ onClick = { ( e ) => handleDelete ( e , item . id ) }
246+ className = "p-1.5 rounded hover:bg-destructive/10 transition-colors"
247+ title = "Delete"
248+ >
249+ < Trash2 className = "w-4 h-4 text-destructive" />
250+ </ button >
251+ </ div >
252+ </ div >
253+ </ div >
254+ ) ) }
255+ </ div >
256+ </ div >
257+ ) ) }
258+ </ div >
259+ </ ScrollArea >
260+ ) : (
261+ < div className = "flex-1 flex items-center justify-center" >
262+ < div className = "text-center" >
263+ < Search className = "w-12 h-12 text-muted-foreground/30 mx-auto mb-4" />
264+ < p className = "text-sm text-muted-foreground" > No transcriptions found</ p >
265+ < p className = "text-xs text-muted-foreground/70 mt-2" >
266+ Try adjusting your search query
267+ </ p >
268+ </ div >
121269 </ div >
122- </ ScrollArea >
270+ )
123271 ) : (
124272 < div className = "flex-1 flex items-center justify-center" >
125273 < div className = "text-center" >
0 commit comments