Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: rewind pipe issues #1483

Merged
merged 7 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions pipes/rewind/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TimelineSlider } from "@/components/timeline/timeline";
import { useTimelineStore } from "@/lib/hooks/use-timeline-store";
import { hasFramesForDate } from "@/lib/actions/has-frames-date";
import { CommandShortcut } from "@/components/ui/command";
import { CurrentFrameTimeline } from "@/components/current-frame-timeline";

export interface StreamTimeSeriesResponse {
timestamp: string;
Expand Down Expand Up @@ -151,7 +152,7 @@ export default function Timeline() {
const direction = -Math.sign(e.deltaY);

// Change this if you want limit the index change
const limitIndexChange = 15;
const limitIndexChange = Infinity;

// Adjust index change based on scroll intensity
const indexChange =
Expand Down Expand Up @@ -397,13 +398,7 @@ export default function Timeline() {
</div>
</div>
)}
{currentFrame && (
<img
src={`http://localhost:3030/frames/${currentFrame.devices[0].frame_id}`}
className="absolute inset-0 w-4/5 h-auto max-h-[75vh] object-contain mx-auto border rounded-xl p-2 mt-20"
alt="Current frame"
/>
)}
{currentFrame && <CurrentFrameTimeline currentFrame={currentFrame} />}
{currentFrame && (
<AudioTranscript
frames={frames}
Expand Down
17 changes: 11 additions & 6 deletions pipes/rewind/src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useKeywordSearchStore } from "@/lib/hooks/use-keyword-search-store";
import { endOfDay, startOfDay } from "date-fns";
import { useCallback, useEffect, useRef } from "react";
import { parser } from "@/lib/keyword-parser";
import { CurrentFrame } from "@/components/current-frame";
import { CurrentFrame } from "@/components/current-frame-search";
import { useKeywordParams } from "@/lib/hooks/use-keyword-params";
import { AppSelect } from "@/components/search-command";
import { ArrowLeft } from "lucide-react";
Expand Down Expand Up @@ -130,11 +130,16 @@ export default function Page() {
</div>
) : null}

{isSearching &&
searchResults.length === 0 &&
Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={`skeleton-${index}`} />
))}
{isSearching && searchResults.length === 0 && (
<div
className="flex flex-row gap-4 p-8 overflow-hidden"
style={{ direction: "rtl" }}
>
{Array.from({ length: 8 }).map((_, index) => (
<SkeletonCard key={`skeleton-${index}`} />
))}
</div>
)}

{querys.query && !(searchResults.length === 0) ? (
<div className="h-64 flex items-end">
Expand Down
48 changes: 48 additions & 0 deletions pipes/rewind/src/components/current-frame-timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { StreamTimeSeriesResponse } from "@/app/page";
import { FC, useState } from "react";

interface CurrentFrameTimelineProps {
currentFrame: StreamTimeSeriesResponse;
}

export const SkeletonLoader: FC = () => {
return (
<div className="absolute inset-0 w-4/5 h-[75vh] mx-auto mt-20 border rounded-xl p-2 bg-gray-100 overflow-hidden">
<div
className="w-full h-full bg-gradient-to-r from-gray-100 via-gray-200 to-gray-100 animate-shimmer"
style={{
backgroundSize: "200% 100%",
animation: "shimmer 1.5s infinite linear",
}}
/>
</div>
);
};

export const CurrentFrameTimeline: FC<CurrentFrameTimelineProps> = ({
currentFrame,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

return (
<>
{(isLoading || hasError) && <SkeletonLoader />}
<img
src={`http://localhost:3030/frames/${currentFrame.devices[0].frame_id}`}
className={`absolute inset-0 w-4/5 h-auto max-h-[75vh] object-contain mx-auto border rounded-xl p-2 mt-20 transition-opacity duration-300 ${
isLoading || hasError ? "opacity-0" : "opacity-100"
}`}
alt="Current frame"
onLoad={() => {
setIsLoading(false);
setHasError(false);
}}
onError={() => {
setIsLoading(false);
setHasError(true);
}}
/>
</>
);
};
2 changes: 1 addition & 1 deletion pipes/rewind/src/components/image-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useKeywordParams } from "@/lib/hooks/use-keyword-params";
export const SkeletonCard = () => (
<div className="flex flex-col shrink-0 w-56 h-full relative overflow-hidden rounded-lg bg-white shadow-sm">
<div className="aspect-video bg-neutral-200 animate-pulse" />
<div className="p-3 space-y-2">
<div className="p-3 space-y-2" style={{ direction: "ltr" }}>
<div className="h-4 bg-neutral-200 rounded animate-pulse" />
<div className="h-3 bg-neutral-200 rounded animate-pulse w-3/4" />
<div className="h-3 bg-neutral-200 rounded animate-pulse w-1/2" />
Expand Down
45 changes: 41 additions & 4 deletions pipes/rewind/src/components/timeline/ai-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,52 @@ export function AIPanel({
setIsStreaming(true);

try {
const relevantFrames = frames.filter((frame) => {
const relevantFrames = frames.reduce((acc, frame) => {
const frameTime = new Date(frame.timestamp).getTime();
const startTime = new Date(selectionRange.start).getTime();
const endTime = new Date(selectionRange.end).getTime();

const isInRange = frameTime >= startTime && frameTime <= endTime;

return isInRange;
});
if (!isInRange) return acc;

// Get minute timestamp (rounded down to nearest minute)
const minuteTimestamp = Math.floor(frameTime / 60000) * 60000;

// Get unique apps in this frame
const frameApps = new Set(
frame.devices.map((device) => device.metadata.app_name),
);

// Check if we already have this app in this minute
const existingFrameForMinute = acc.find((existing) => {
const existingTime = new Date(existing.timestamp).getTime();
const existingMinute = Math.floor(existingTime / 60000) * 60000;

if (existingMinute !== minuteTimestamp) return false;

// Check if apps are the same
const existingApps = new Set(
existing.devices.map((device) => device.metadata.app_name),
);
return (
Array.from(frameApps).every((app) => existingApps.has(app)) &&
Array.from(existingApps).every((app) => frameApps.has(app))
);
});

// If we have multiple apps or haven't seen this app/minute combo, add the frame
if (frameApps.size > 1 || !existingFrameForMinute) {
acc.push(frame);
}

return acc;
}, [] as StreamTimeSeriesResponse[]);

// Sort frames by timestamp
relevantFrames.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
);

const openai = new OpenAI({
apiKey:
Expand Down
85 changes: 48 additions & 37 deletions pipes/rewind/src/components/timeline/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const TimelineSlider = ({
onSelectionChange,
}: TimelineSliderProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const observerTargetRef = useRef<HTMLDivElement>(null);
const lastFetchRef = useRef<Date | null>(null);
const { scrollXProgress } = useScroll({
container: containerRef,
offset: ["start end", "end start"],
Expand All @@ -58,12 +60,18 @@ export const TimelineSlider = ({
);
const { setSelectionRange, selectionRange } = useTimelineSelection();

const visibleFrames = useMemo(() => {
const start = Math.max(0, currentIndex - 100);
const end = Math.min(frames.length, currentIndex + 100);
return frames.slice(start, end);
}, [frames, currentIndex]);

const appGroups = useMemo(() => {
const groups: AppGroup[] = [];
let currentApp = "";
let currentGroup: StreamTimeSeriesResponse[] = [];

frames.forEach((frame) => {
visibleFrames.forEach((frame) => {
const appName = frame.devices[0].metadata.app_name;
if (appName !== currentApp) {
if (currentGroup.length > 0) {
Expand All @@ -88,9 +96,39 @@ export const TimelineSlider = ({
});
}
return groups;
}, [frames]);
}, [visibleFrames]);

useEffect(() => {
const observerTarget = observerTargetRef.current;
if (!observerTarget) return;

const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry.isIntersecting) return;

const lastDate = subDays(currentDate, 1);
const now = new Date();
const canFetch =
!lastFetchRef.current ||
now.getTime() - lastFetchRef.current.getTime() > 1000;

if (isAfter(lastDate, startAndEndDates.start) && canFetch) {
lastFetchRef.current = now;
fetchNextDayData(lastDate);
}
},
{
root: containerRef.current,
threshold: 1.0,
rootMargin: "0px 20% 0px 0px",
},
);

observer.observe(observerTarget);
return () => observer.disconnect();
}, [fetchNextDayData, currentDate, startAndEndDates]);

// Add effect to keep current frame in view
useEffect(() => {
const container = containerRef.current;
if (!container || !frames[currentIndex]) return;
Expand All @@ -109,47 +147,21 @@ export const TimelineSlider = ({
});
}, [currentIndex, frames.length]);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

const handleScroll = () => {
const { scrollLeft, scrollWidth, clientWidth } = container;

// Check if we're 20% away from the left end (considering RTL)
const threshold = scrollWidth * 0.2; // 20% of total scroll width
const isNearLeftEnd =
Math.abs(scrollLeft) + clientWidth >= scrollWidth - threshold;

const lastDate = subDays(currentDate, 1);
if (isNearLeftEnd && isAfter(lastDate, startAndEndDates.start)) {
console.log("fetching next day's data", currentDate);
fetchNextDayData(lastDate);
}
};

container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, [fetchNextDayData, currentDate, startAndEndDates]);

useEffect(() => {
if (!selectionRange) {
setSelectedIndices(new Set());
}
}, [selectionRange]);

// Handle drag start
const handleDragStart = (index: number) => {
setIsDragging(true);
setDragStartIndex(index);
setSelectedIndices(new Set([index]));

// Set initial selection range
const startDate = new Date(frames[index].timestamp);
setSelectionRange({ start: startDate, end: startDate });
};

// Handle drag over
const handleDragOver = (index: number) => {
if (isDragging && dragStartIndex !== null) {
const start = Math.min(dragStartIndex, index);
Expand All @@ -162,21 +174,18 @@ export const TimelineSlider = ({

setSelectedIndices(newSelection);

// Update selection range
setSelectionRange({
end: new Date(frames[start].timestamp),
start: new Date(frames[end].timestamp),
});

// Notify parent of selection change
if (onSelectionChange) {
const selectedFrames = Array.from(newSelection).map((i) => frames[i]);
onSelectionChange(selectedFrames);
}
}
};

// Handle drag end
const handleDragEnd = () => {
setIsDragging(false);
setDragStartIndex(null);
Expand Down Expand Up @@ -239,15 +248,15 @@ export const TimelineSlider = ({
backgroundColor: group.color,
height:
frameIndex === currentIndex || isSelected || isInRange
? "100%"
: "70%",
? "60%"
: "40%",
opacity:
frameIndex === currentIndex || isSelected || isInRange
? 1
: 0.7,
direction: "ltr",
}}
whileHover={{ height: "100%", opacity: 1 }}
whileHover={{ height: "60%", opacity: 1 }}
onMouseDown={() => handleDragStart(frameIndex)}
onMouseEnter={() => {
setHoveredTimestamp(frame.timestamp);
Expand All @@ -260,8 +269,9 @@ export const TimelineSlider = ({
<AudioLinesIcon className="w-full h-full" />
</div>
)}
{hoveredTimestamp === frame.timestamp && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-6 w-max bg-background border rounded-md px-2 py-1 text-xs shadow-lg">
{(hoveredTimestamp === frame.timestamp ||
frames[currentIndex].timestamp === frame.timestamp) && (
<div className="absolute bottom-full left-1/2 z-50 -translate-x-1/2 mb-6 w-max bg-background border rounded-md px-2 py-1 text-xs shadow-lg">
<p className="font-medium">
{frame.devices[0].metadata.app_name}
</p>
Expand All @@ -275,6 +285,7 @@ export const TimelineSlider = ({
})}
</div>
))}
<div ref={observerTargetRef} className="h-full w-1" />
</motion.div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion pipes/rewind/src/lib/hooks/use-current-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const useCurrentFrame = (setCurrentIndex: (index: number) => void) => {
const lastFramesLen = useRef<number>(0);

useEffect(() => {
console.log("current length", lastFramesLen);
if (!currentFrame && frames.length) {
setCurrentFrame(frames[lastFramesLen.current]);
setCurrentIndex(lastFramesLen.current);
Expand Down
5 changes: 5 additions & 0 deletions pipes/rewind/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,15 @@ export default {
height: "0",
},
},
shimmer: {
"0%": { backgroundPosition: "200% 0" },
"100%": { backgroundPosition: "-200% 0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
shimmer: "shimmer 1.5s infinite linear",
},
},
},
Expand Down
Loading
Loading