+ {/* Hover actions - top right */}
+
+
+
+
+ {/* Thumbnail */}
+
onExpand(memory.id)}
+ >
+ {thumbnailUrl ? (
+

+ ) : isLoadingThumbnail ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {/* Page count badge */}
+ {memory.document_page_count !== undefined && (
+
+ {memory.document_page_count} {memory.document_page_count === 1 ? "page" : "pages"}
+
+ )}
+
+
+ {/* Header: Document icon + Title */}
+
+
+
+
+
+ {memory.title || "Document"}
+
+
+
+ {/* Processing status */}
+
+
+ {/* Summary */}
+ {memory.summary ? (
+
+ {memory.summary}
+
+ ) : (
+
+ Generating summary...
+
+ )}
+
+ {/* Tags */}
+ {memory.tags.length > 0 && (
+
+ {memory.tags.map((tag) => (
+
+ {tag.name}
+ {tag.source === "manual" && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Date */}
+
{formatDate(memory.created_at)}
+
+ );
+}
diff --git a/app/src/components/MemoryDetailPanel.tsx b/app/src/components/MemoryDetailPanel.tsx
index a1bfb63..cea404d 100644
--- a/app/src/components/MemoryDetailPanel.tsx
+++ b/app/src/components/MemoryDetailPanel.tsx
@@ -1,6 +1,9 @@
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useNavigate } from "react-router-dom";
+import { Document, Page, pdfjs } from "react-pdf";
+import "react-pdf/dist/Page/AnnotationLayer.css";
+import "react-pdf/dist/Page/TextLayer.css";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -28,7 +31,17 @@ import {
Copy,
SkipBack,
SkipForward,
+ ExternalLink,
+ ChevronLeft,
+ ChevronRight,
} from "lucide-react";
+
+// Set up PDF.js worker - served from public folder (dev) or copied to dist (build)
+// Use import.meta.url to resolve relative to the app base, works in both web and Electron
+pdfjs.GlobalWorkerOptions.workerSrc = new URL(
+ "/pdf.worker.min.mjs",
+ import.meta.url
+).href;
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { apiFetch, getAppToken } from "@/lib/api";
@@ -45,7 +58,7 @@ interface MemoryTag {
interface Memory {
id: number;
- type: "web" | "note" | "voice_memo" | "audio" | "video" | "voice"; // "voice" for backwards compat
+ type: "web" | "note" | "voice_memo" | "audio" | "video" | "document" | "voice"; // "voice" for backwards compat
url: string | null;
title: string;
content?: string;
@@ -64,6 +77,9 @@ interface Memory {
video_height?: number;
thumbnail_path?: string;
video_processing_status?: VideoProcessingStatus;
+ // Document-specific fields
+ document_format?: string;
+ document_page_count?: number;
}
interface Tag {
@@ -130,6 +146,17 @@ export function MemoryDetailPanel({
const videoBlobUrlRef = useRef