diff --git a/pipes/loom/app/api/copy/route.ts b/pipes/loom/app/api/copy/route.ts new file mode 100644 index 000000000..b5371a748 --- /dev/null +++ b/pipes/loom/app/api/copy/route.ts @@ -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}); +} diff --git a/pipes/loom/app/globals.css b/pipes/loom/app/globals.css index 59e3ad666..59af863af 100644 --- a/pipes/loom/app/globals.css +++ b/pipes/loom/app/globals.css @@ -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; + } +} diff --git a/pipes/loom/bun.lockb b/pipes/loom/bun.lockb index b7d710988..23a75ce2f 100755 Binary files a/pipes/loom/bun.lockb and b/pipes/loom/bun.lockb differ diff --git a/pipes/loom/components/llm-chat.tsx b/pipes/loom/components/llm-chat.tsx index ad32e640f..b4fa83e5a 100644 --- a/pipes/loom/components/llm-chat.tsx +++ b/pipes/loom/components/llm-chat.tsx @@ -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); diff --git a/pipes/loom/components/pipe.tsx b/pipes/loom/components/pipe.tsx index d7e2b18b4..f2e38a72a 100644 --- a/pipes/loom/components/pipe.tsx +++ b/pipes/loom/components/pipe.tsx @@ -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"; @@ -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, @@ -17,18 +19,27 @@ import { } from "@/components/ui/tooltip"; import { useSettings } from "@/lib/hooks/use-settings"; +const Divider = () => ( +
+
+
+
+); + const Pipe: React.FC = () => { const { toast } = useToast(); + const { settings} = useSettings(); + const { isServerDown } = useHealthCheck() + const [isCopied, setIsCopied] = React.useState(false); + const { isAvailable, error } = useAiProvider(settings); const [rawData, setRawData] = useState([]); const [endTime, setEndTime] = useState(new Date()); const [isMerging, setIsMerging] = useState(false); const [startTime, setStartTime] = useState(new Date()); const [mergedVideoPath, setMergedVideoPath] = useState(''); - 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(); @@ -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({ @@ -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 (

@@ -217,11 +260,21 @@ const Pipe: React.FC = () => { {mergedVideoPath && ( -
+
+ +
)} @@ -231,3 +284,4 @@ const Pipe: React.FC = () => { export default Pipe; + // filePath={"C:\\Users\\eirae\\.screenpipe\\videos\\output_28b50b43-cf63-43f3-88f0-8e5dd9e5910e.mp4"} diff --git a/pipes/loom/components/ui/sidebar.tsx b/pipes/loom/components/ui/sidebar.tsx index cb0e31450..a4b993c61 100644 --- a/pipes/loom/components/ui/sidebar.tsx +++ b/pipes/loom/components/ui/sidebar.tsx @@ -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" diff --git a/pipes/loom/components/video-comp.tsx b/pipes/loom/components/video-comp.tsx index 2cf65fbcd..7820edea3 100644 --- a/pipes/loom/components/video-comp.tsx +++ b/pipes/loom/components/video-comp.tsx @@ -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'; @@ -33,17 +32,6 @@ export const VideoComponent = memo(function VideoComponent({
); - const validateMedia = async(path: string): Promise => { - 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 { @@ -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( @@ -121,13 +97,16 @@ export const VideoComponent = memo(function VideoComponent({ return (
- - - +
+ + + +
{/* {renderFileLink()} */}
); diff --git a/pipes/loom/package.json b/pipes/loom/package.json index 33ea1d828..0e0fad1b8 100644 --- a/pipes/loom/package.json +++ b/pipes/loom/package.json @@ -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", @@ -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", diff --git a/screenpipe-server/src/video_utils.rs b/screenpipe-server/src/video_utils.rs index d64bff640..30ffc2905 100644 --- a/screenpipe-server/src/video_utils.rs +++ b/screenpipe-server/src/video_utils.rs @@ -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