Skip to content

Commit

Permalink
feat: add copy to clipboard functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
tribhuwan-kumar committed Feb 22, 2025
1 parent 523b8b1 commit f88d5f4
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 56 deletions.
25 changes: 25 additions & 0 deletions pipes/loom/app/api/copy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { exec } from 'child_process';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
const path = req.nextUrl.searchParams.get('path');
if (!path || typeof path !== 'string') {
return NextResponse.json({ error: 'path is required' }, { status: 400 });
}

const command = `powershell.exe -NoProfile -WindowStyle hidden -File copyToClipboard.ps1 -FilePath "${path}"`;

exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
return NextResponse.json({error: `${error.message}`}, { status: 400 });
}
if (stderr) {
console.error(`Stderr: ${stderr}`);
return NextResponse.json({error: `${stderr}`}, { status: 500 });
}
return NextResponse.json({message: 'sucessfully copied to clipboard', stdout: stdout}, {status: 200});
});

return NextResponse.json({message: 'sucessfully copied to clipboard'}, {status: 200});
}
11 changes: 11 additions & 0 deletions pipes/loom/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,14 @@
@apply bg-background text-foreground !pointer-events-auto;
}
}



@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Binary file modified pipes/loom/bun.lockb
Binary file not shown.
2 changes: 0 additions & 2 deletions pipes/loom/components/llm-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ interface LLMChatProps {
}

export function LLMChat({ data, className }: LLMChatProps) {

const { toast } = useToast();
const { health } = useHealthCheck();
const [isLoading, setIsLoading] = useState(false);
const { settings } = useSettings();
const [isAiLoading, setIsAiLoading] = useState(false);
Expand Down
64 changes: 59 additions & 5 deletions pipes/loom/components/pipe.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Clock, Loader2 } from "lucide-react";
import { IconCheck, IconCopy } from "@/components/ui/icons";
import { useToast } from "@/lib/use-toast";
import { Badge } from "@/components/ui/badge";
import { LLMChat } from "@/components/llm-chat";
Expand All @@ -9,6 +10,7 @@ import { DateTimePicker } from './date-time-picker';
import { VideoComponent } from "@/components/video-comp";
import { useHealthCheck } from "@/lib/hooks/use-health-check";
import { useAiProvider } from "@/lib/hooks/use-ai-provider";

import {
Tooltip,
TooltipContent,
Expand All @@ -17,18 +19,27 @@ import {
} from "@/components/ui/tooltip";
import { useSettings } from "@/lib/hooks/use-settings";

const Divider = () => (
<div className="flex my-2 justify-center">
<div className="h-[1px] w-[400px] rounded-full bg-gradient-to-l from-slate-500/30 to-transparent"></div>
<div className="h-[1px] w-[400px] rounded-full bg-gradient-to-r from-slate-500/30 to-transparent"></div>
</div>
);

const Pipe: React.FC = () => {
const { toast } = useToast();
const { settings} = useSettings();
const { isServerDown } = useHealthCheck()
const [isCopied, setIsCopied] = React.useState<Boolean>(false);
const { isAvailable, error } = useAiProvider(settings);
const [rawData, setRawData] = useState<any[] | undefined>([]);
const [endTime, setEndTime] = useState<Date>(new Date());
const [isMerging, setIsMerging] = useState<boolean>(false);
const [startTime, setStartTime] = useState<Date>(new Date());
const [mergedVideoPath, setMergedVideoPath] = useState<string>('');
const { isServerDown } = useHealthCheck()
const { settings} = useSettings();

const aiDisabled =
settings.aiProviderType === "screenpipe-cloud" && !settings.user.token;
const { isAvailable, error } = useAiProvider(settings);

const handleQuickTimeFilter = (minutes: number) => {
const now = new Date();
Expand Down Expand Up @@ -98,7 +109,7 @@ const Pipe: React.FC = () => {
setIsMerging(false);
return;
}
await mergeContent(uniqueFilePaths, 'video');
await mergeContent(uniqueFilePaths.reverse(), 'video');
setIsMerging(false);
} catch (e :any) {
toast({
Expand All @@ -119,6 +130,38 @@ const Pipe: React.FC = () => {
}
};

const copyMediaToClipboard = async () => {
if (mergedVideoPath) {
try {
setIsCopied(false);
const response = await fetch(`/api/copy?path=${encodeURIComponent(mergedVideoPath)}`);
const result = await response.json();
if (!response.ok) {
setIsCopied(false);
throw new Error(result.error || "Failed to copy video to clipboard");
}
toast({
title: "media copied to your clipboard",
variant: "default",
duration: 3000,
});
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 3000);
} catch (err) {
console.error("failed to copy media: ", err);
setIsCopied(true);
toast({
title: "failed to copy media to clipboard",
variant: "default",
duration: 3000,
});
}
}
};


return (
<div className="w-full mt-4 flex flex-col justify-center items-center">
<h1 className='font-medium text-xl'>
Expand Down Expand Up @@ -217,11 +260,21 @@ const Pipe: React.FC = () => {
</TooltipProvider>

{mergedVideoPath && (
<div className="border-2 mt-16 w-[1400px] rounded-lg flex-col flex items-center justify-center" >
<div className="border-2 mt-16 w-[1200px] relative rounded-lg flex-col flex items-center justify-center" >
<Button
variant={"outline"}
size={"icon"}
onClick={copyMediaToClipboard}
className="mt-4 absolute !border-none right-5 top-5"
>
{isCopied ? <IconCheck /> : <IconCopy />}
<span className="sr-only">Copy video</span>
</Button>
<VideoComponent
filePath={mergedVideoPath}
className="text-center m-8 "
/>
<Divider />
<LLMChat data={rawData} />
</div>
)}
Expand All @@ -231,3 +284,4 @@ const Pipe: React.FC = () => {

export default Pipe;

// filePath={"C:\\Users\\eirae\\.screenpipe\\videos\\output_28b50b43-cf63-43f3-88f0-8e5dd9e5910e.mp4"}
2 changes: 1 addition & 1 deletion pipes/loom/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip"

const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
Expand Down
61 changes: 20 additions & 41 deletions pipes/loom/components/video-comp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { cn } from "@/lib/utils";
import { getMediaFile } from "@/lib/actions/video-actions";
import { memo, useCallback, useEffect, useState } from "react";
import MediaThemeSutro from 'player.style/sutro/react';

Expand Down Expand Up @@ -33,17 +32,6 @@ export const VideoComponent = memo(function VideoComponent({
</div>
);

const validateMedia = async(path: string): Promise<string> => {
try {
const response = await fetch(`http://localhost:3030/experimental/validate/media?file_path=${encodeURIComponent(path)}`);
const result = await response.json();
return result.status;
} catch (error) {
console.error("Failed to validate media:", error);
return "Failed to validate media";
}
};

useEffect(() => {
async function loadMedia() {
try {
Expand All @@ -53,29 +41,17 @@ export const VideoComponent = memo(function VideoComponent({
throw new Error("Invalid file path");
}

const validationStatus = await validateMedia(sanitizedPath);
console.log("Media file:", validationStatus)

if (validationStatus === "valid media file") {
setIsAudio(
sanitizedPath.toLowerCase().includes("input") ||
sanitizedPath.toLowerCase().includes("output")
);
const { data, mimeType } = await getMediaFile(sanitizedPath);
const binaryData = atob(data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([bytes], { type: mimeType });
setMediaSrc(URL.createObjectURL(blob));
} else if (validationStatus.startsWith("media file does not exist")) {
throw new Error(`${isAudio ? "audio" : "video" } file does not exist, it might get deleted`);
} else if (validationStatus.startsWith("invalid media file")) {
throw new Error(`the ${isAudio ? "audio" : "video" } file is not written completely, please try again later`);
} else {
throw new Error("unknown media validation status");
const response = await fetch(`/api/file?path=${encodeURIComponent(sanitizedPath)}`);
if (!response.ok) {
throw new Error("Failed to fetch media file");
}
const blob = await response.blob();
setMediaSrc(URL.createObjectURL(blob));

setIsAudio(
sanitizedPath.toLowerCase().includes("input") ||
sanitizedPath.toLowerCase().includes("output")
);
} catch (error) {
console.error("Failed to load media:", error);
setError(
Expand Down Expand Up @@ -121,13 +97,16 @@ export const VideoComponent = memo(function VideoComponent({

return (
<div className={cn("flex flex-col items-center justify-center", className)}>
<MediaThemeSutro className="w-[60%]">
<video
slot="media"
src={mediaSrc}
>
</video>
</MediaThemeSutro>
<div className="rounded-xl block w-[80%] overflow-hidden">
<MediaThemeSutro className="w-full h-full">
<video
className="!mb-[-5px]"
slot="media"
src={mediaSrc}
>
</video>
</MediaThemeSutro>
</div>
{/* {renderFileLink()} */}
</div>
);
Expand Down
12 changes: 6 additions & 6 deletions pipes/loom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@screenpipe/browser": "^0.1.26",
"@screenpipe/js": "^1.0.12",
"@types/lodash": "^4.17.13",
Expand All @@ -29,7 +29,7 @@
"date-fns": "^3.6.0",
"framer-motion": "^11.14.4",
"lodash": "^4.17.21",
"lucide-react": "^0.468.0",
"lucide-react": "^0.475.0",
"next": "14.2.15",
"openai": "^4.76.3",
"player.style": "^0.1.1",
Expand Down
3 changes: 2 additions & 1 deletion screenpipe-server/src/video_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ pub async fn merge_videos(
));
}

let output_filename = format!("output_{}.mp4", Uuid::new_v4());
let current_time = chrono::Local::now().format("%Y_%m_%d_%H_%M_%S").to_string();
let output_filename = format!("loom_{}.mp4", current_time);
let output_path = output_dir.join(&output_filename);

// create a temporary file to store the list of input videos
Expand Down

0 comments on commit f88d5f4

Please sign in to comment.