From 474c8f3d54a34a5be5122512b4e4a1fe86934bd7 Mon Sep 17 00:00:00 2001 From: Louis Beaumont Date: Sat, 9 Nov 2024 11:11:45 -0800 Subject: [PATCH] feat: add dialog for icons --- screenpipe-app-tauri/app/timeline/page.tsx | 10 +- .../timeline/timeline-dock-section.tsx | 264 ++++++++++++------ 2 files changed, 182 insertions(+), 92 deletions(-) diff --git a/screenpipe-app-tauri/app/timeline/page.tsx b/screenpipe-app-tauri/app/timeline/page.tsx index a10250117..485da317b 100644 --- a/screenpipe-app-tauri/app/timeline/page.tsx +++ b/screenpipe-app-tauri/app/timeline/page.tsx @@ -193,8 +193,11 @@ export default function Timeline() { const isWithinAudioPanel = document .querySelector('.audio-transcript-panel') ?.contains(e.target as Node); + const isWithinTimelineDialog = document + .querySelector('[role="dialog"]') + ?.contains(e.target as Node); - if (isWithinAiPanel || isWithinAudioPanel) { + if (isWithinAiPanel || isWithinAudioPanel || isWithinTimelineDialog) { e.stopPropagation(); return; } @@ -248,8 +251,11 @@ export default function Timeline() { const isWithinAudioPanel = document .querySelector('.audio-transcript-panel') ?.contains(e.target as Node); + const isWithinTimelineDialog = document + .querySelector('[role="dialog"]') + ?.contains(e.target as Node); - if (!isWithinAiPanel && !isWithinAudioPanel) { + if (!isWithinAiPanel && !isWithinAudioPanel && !isWithinTimelineDialog) { e.preventDefault(); } }; diff --git a/screenpipe-app-tauri/components/timeline/timeline-dock-section.tsx b/screenpipe-app-tauri/components/timeline/timeline-dock-section.tsx index c8c41fb34..9774c88d1 100644 --- a/screenpipe-app-tauri/components/timeline/timeline-dock-section.tsx +++ b/screenpipe-app-tauri/components/timeline/timeline-dock-section.tsx @@ -4,12 +4,29 @@ import { invoke } from "@tauri-apps/api/core"; import { StreamTimeSeriesResponse } from "@/app/timeline/page"; import { stringToColor } from "@/lib/utils"; import { motion, useAnimation } from "framer-motion"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useTimelineSelection } from "@/lib/hooks/use-timeline-selection"; +import { Button } from "@/components/ui/button"; +import { MessageSquarePlus } from "lucide-react"; + +// Add this near the top of the file, after imports +const GAP_THRESHOLD = 3 * 60 * 1000; // 5 minutes in milliseconds interface ProcessedBlock { appName: string; percentThroughDay: number; timestamp: Date; iconSrc?: string; + windows: Array<{ + title: string; + timestamp: Date; + }>; } export function TimelineIconsSection({ @@ -18,6 +35,8 @@ export function TimelineIconsSection({ blocks: StreamTimeSeriesResponse[]; }) { const [iconCache, setIconCache] = useState<{ [key: string]: string }>({}); + const [selectedApp, setSelectedApp] = useState(null); + const { setSelectionRange } = useTimelineSelection(); // Get the visible time range const timeRange = useMemo(() => { @@ -31,70 +50,84 @@ export function TimelineIconsSection({ const processedBlocks = useMemo(() => { if (!timeRange) return []; - const appGroups: { [key: string]: Date[] } = {}; + const appGroups: { + [key: string]: Array<{ + timestamp: Date; + title?: string; + blockId?: number; + }>; + } = {}; blocks.forEach((frame) => { - // Show all devices without filtering frame.devices.forEach((device) => { if (!device.metadata?.app_name) return; const timestamp = new Date(frame.timestamp); const appName = device.metadata.app_name; + const windowTitle = device.metadata.window_name; if (timestamp < timeRange.start || timestamp > timeRange.end) return; if (!appGroups[appName]) { appGroups[appName] = []; } - appGroups[appName].push(timestamp); + appGroups[appName].push({ timestamp, title: windowTitle }); }); }); - const b: ProcessedBlock[] = []; - Object.entries(appGroups).forEach(([appName, timestamps]) => { - timestamps.sort((a, b) => a.getTime() - b.getTime()); + timestamps.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); - // Changed from 30s to 15s for a more balanced approach - const GAP_THRESHOLD = 15000; // 15 seconds in milliseconds + let currentBlockId = 0; let blockStart = timestamps[0]; let lastTimestamp = timestamps[0]; - timestamps.forEach((timestamp) => { - if (timestamp.getTime() - lastTimestamp.getTime() > GAP_THRESHOLD) { - b.push({ - appName, - timestamp: new Date( - blockStart.getTime() + - (lastTimestamp.getTime() - blockStart.getTime()) / 2 - ), - percentThroughDay: - ((blockStart.getTime() + - (lastTimestamp.getTime() - blockStart.getTime()) / 2 - - timeRange.start.getTime()) / - (timeRange.end.getTime() - timeRange.start.getTime())) * - 100, - iconSrc: iconCache[appName], - }); - blockStart = timestamp; + timestamps.forEach((entry, idx) => { + if ( + entry.timestamp.getTime() - lastTimestamp.timestamp.getTime() > + GAP_THRESHOLD + ) { + currentBlockId++; + blockStart = entry; } - lastTimestamp = timestamp; + entry.blockId = currentBlockId; + lastTimestamp = entry; }); + }); - // Always add the last block - b.push({ - appName, - timestamp: new Date( - blockStart.getTime() + - (lastTimestamp.getTime() - blockStart.getTime()) / 2 - ), - percentThroughDay: - ((blockStart.getTime() + - (lastTimestamp.getTime() - blockStart.getTime()) / 2 - - timeRange.start.getTime()) / - (timeRange.end.getTime() - timeRange.start.getTime())) * - 100, - iconSrc: iconCache[appName], + const b: ProcessedBlock[] = []; + + Object.entries(appGroups).forEach(([appName, entries]) => { + const blockIds = [...new Set(entries.map((e) => e.blockId))]; + + blockIds.forEach((blockId) => { + const blockEntries = entries.filter((e) => e.blockId === blockId); + if (blockEntries.length === 0) return; + + const blockStart = blockEntries[0].timestamp; + const blockEnd = blockEntries[blockEntries.length - 1].timestamp; + const blockMiddle = new Date( + blockStart.getTime() + (blockEnd.getTime() - blockStart.getTime()) / 2 + ); + + const windowsInBlock = blockEntries + .filter((w) => w.title) // only keep windows with titles + .map((w) => ({ + title: w.title!, + timestamp: w.timestamp, + })) + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // most recent first + + b.push({ + appName, + timestamp: blockMiddle, + percentThroughDay: + ((blockMiddle.getTime() - timeRange.start.getTime()) / + (timeRange.end.getTime() - timeRange.start.getTime())) * + 100, + iconSrc: iconCache[appName], + windows: windowsInBlock, + }); }); }); @@ -154,58 +187,109 @@ export function TimelineIconsSection({ }, [processedBlocks, loadAppIcon]); return ( -
- {processedBlocks.map((block, i) => { - const bgColor = stringToColor(block.appName); + <> +
+ {processedBlocks.map((block, i) => { + const bgColor = stringToColor(block.appName); - return ( - { - console.log("hover on:", block.appName); - }} - whileHover={{ - scale: 1.5, - backgroundColor: "red", - y: -20, - }} - transition={{ - type: "spring", - stiffness: 300, - damping: 30, - }} - > - {block.iconSrc ? ( - setSelectedApp(block)} + whileHover={{ + scale: 1.5, + backgroundColor: "red", + y: -20, + }} + transition={{ + type: "spring", + stiffness: 300, + damping: 30, + }} + > + {block.iconSrc ? ( + + {block.appName} + + ) : ( + + )} + + ); + })} +
+ + setSelectedApp(null)} + > + + + +
+ {selectedApp?.iconSrc && ( + {selectedApp.appName} + )} + {selectedApp?.appName} +
+
+ + ask ai about this + + + + + +
+ {selectedApp?.windows.map((window, i) => ( +
+

{window.title}

+

+ {window.timestamp.toLocaleTimeString()} +

+
+ ))} +
+
+ + + ); }