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