Skip to content

Commit

Permalink
feat: add dialog for icons
Browse files Browse the repository at this point in the history
  • Loading branch information
louis030195 committed Nov 9, 2024
1 parent 04957ca commit 474c8f3
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 92 deletions.
10 changes: 8 additions & 2 deletions screenpipe-app-tauri/app/timeline/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}
};
Expand Down
264 changes: 174 additions & 90 deletions screenpipe-app-tauri/components/timeline/timeline-dock-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -18,6 +35,8 @@ export function TimelineIconsSection({
blocks: StreamTimeSeriesResponse[];
}) {
const [iconCache, setIconCache] = useState<{ [key: string]: string }>({});
const [selectedApp, setSelectedApp] = useState<ProcessedBlock | null>(null);
const { setSelectionRange } = useTimelineSelection();

// Get the visible time range
const timeRange = useMemo(() => {
Expand All @@ -31,70 +50,84 @@ export function TimelineIconsSection({
const processedBlocks = useMemo<ProcessedBlock[]>(() => {
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,
});
});
});

Expand Down Expand Up @@ -154,58 +187,109 @@ export function TimelineIconsSection({
}, [processedBlocks, loadAppIcon]);

return (
<div className="absolute -top-8 inset-x-0 h-8">
{processedBlocks.map((block, i) => {
const bgColor = stringToColor(block.appName);
<>
<div className="absolute -top-8 inset-x-0 h-8">
{processedBlocks.map((block, i) => {
const bgColor = stringToColor(block.appName);

return (
<motion.div
key={`${block.appName}-${i}`}
className="absolute h-full pointer-events-auto cursor-pointer"
style={{
left: `${block.percentThroughDay}%`,
transform: "translateX(-50%)",
zIndex: 50,
}}
onMouseEnter={() => {
console.log("hover on:", block.appName);
}}
whileHover={{
scale: 1.5,
backgroundColor: "red",
y: -20,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
>
{block.iconSrc ? (
<motion.div
className="w-5 h-5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
style={{
backgroundColor: `${bgColor}40`,
padding: "2px",
return (
<motion.div
key={`${block.appName}-${i}`}
className="absolute h-full pointer-events-auto cursor-pointer"
style={{
left: `${block.percentThroughDay}%`,
transform: "translateX(-50%)",
zIndex: 50,
}}
onClick={() => setSelectedApp(block)}
whileHover={{
scale: 1.5,
backgroundColor: "red",
y: -20,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
>
{block.iconSrc ? (
<motion.div
className="w-5 h-5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
style={{
backgroundColor: `${bgColor}40`,
padding: "2px",
}}
>
<img
src={`data:image/png;base64,${block.iconSrc}`}
className="w-full h-full opacity-70"
alt={block.appName}
loading="lazy"
decoding="async"
/>
</motion.div>
) : (
<motion.div
className="w-5 h-5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
style={{ backgroundColor: bgColor }}
/>
)}
</motion.div>
);
})}
</div>

<Dialog
open={selectedApp !== null}
onOpenChange={() => setSelectedApp(null)}
>
<DialogContent className="max-w-md p-8">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
{selectedApp?.iconSrc && (
<img
src={`data:image/png;base64,${selectedApp.iconSrc}`}
className="w-6 h-6"
alt={selectedApp.appName}
/>
)}
<span>{selectedApp?.appName}</span>
</div>
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={() => {
if (!selectedApp) return;
setSelectedApp(null);
setSelectionRange({
start: selectedApp.timestamp,
end: new Date(selectedApp.timestamp.getTime() + 60000),
});
}}
>
<img
src={`data:image/png;base64,${block.iconSrc}`}
className="w-full h-full opacity-70"
alt={block.appName}
loading="lazy"
decoding="async"
/>
</motion.div>
) : (
<motion.div
className="w-5 h-5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
style={{ backgroundColor: bgColor }}
/>
)}
</motion.div>
);
})}
</div>
<MessageSquarePlus className="h-4 w-4" />
<span className="text-xs">ask ai about this</span>
</Button>
</DialogTitle>
</DialogHeader>

<ScrollArea className="max-h-[60vh]">
<div className="space-y-2">
{selectedApp?.windows.map((window, i) => (
<div key={i} className="p-2 rounded-lg bg-muted/50 text-sm">
<p className="font-medium">{window.title}</p>
<p className="text-xs text-muted-foreground">
{window.timestamp.toLocaleTimeString()}
</p>
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
</>
);
}

0 comments on commit 474c8f3

Please sign in to comment.