Skip to content

Commit 8bd2135

Browse files
committed
feat: enhance history page with search and improved UI
- Add real-time search functionality for transcriptions - Implement date-based grouping (Today, Yesterday, dates) - Redesign transcription cards with better spacing and typography - Add hover actions for copy and delete operations - Show timestamps in readable format with model info - Match modern design consistent with Overview tab - Improve empty states and search feedback
1 parent 415d730 commit 8bd2135

File tree

1 file changed

+188
-40
lines changed

1 file changed

+188
-40
lines changed

src/components/sections/RecentRecordings.tsx

Lines changed: 188 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { TranscriptionHistory } from "@/types";
44
import { useCanRecord, useCanAutoInsert } from "@/contexts/ReadinessContext";
55
import { invoke } from "@tauri-apps/api/core";
66
import { 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";
99
import { toast } from "sonner";
10+
import { cn } from "@/lib/utils";
1011

1112
interface RecentRecordingsProps {
1213
history: TranscriptionHistory[];
@@ -16,9 +17,56 @@ interface RecentRecordingsProps {
1617

1718
export 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

Comments
 (0)