diff options
| author | MaheshtheDev <[email protected]> | 2026-01-22 02:35:35 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2026-01-22 02:35:35 +0000 |
| commit | 7972e543c47cb97b1716bbfe305d3244eb4d00bf (patch) | |
| tree | 6968ac91eb4161d33bac4042dbba3958c78482a5 /apps/web/components/new/document-modal/content | |
| parent | Re - feat(pipecat-sdk): add speech-to-speech model support (Gemini Live) (#683) (diff) | |
| download | supermemory-7972e543c47cb97b1716bbfe305d3244eb4d00bf.tar.xz supermemory-7972e543c47cb97b1716bbfe305d3244eb4d00bf.zip | |
chore: cmdk, google docs viewer, image preview, document icons (#691)01-21-chore_cmdk_google_docs_viewer_image_preview_document_icons
Diffstat (limited to 'apps/web/components/new/document-modal/content')
4 files changed, 356 insertions, 0 deletions
diff --git a/apps/web/components/new/document-modal/content/google-doc.tsx b/apps/web/components/new/document-modal/content/google-doc.tsx new file mode 100644 index 00000000..562bac12 --- /dev/null +++ b/apps/web/components/new/document-modal/content/google-doc.tsx @@ -0,0 +1,66 @@ +"use client" + +import { useState } from "react" +import { Loader2 } from "lucide-react" +import { + extractGoogleDocId, + getGoogleEmbedUrl, +} from "@/lib/url-helpers" + +interface GoogleDocViewerProps { + url: string | null | undefined + customId: string | null | undefined + type: "google_doc" | "google_sheet" | "google_slide" +} + +export function GoogleDocViewer({ url, customId, type }: GoogleDocViewerProps) { + const [loading, setLoading] = useState(true) + const [error, setError] = useState<string | null>(null) + + const docId = customId ?? (url ? extractGoogleDocId(url) : null) + + if (!docId) { + return ( + <div className="flex items-center justify-center h-full text-gray-400"> + Unable to load document - no document ID found + </div> + ) + } + + const embedUrl = getGoogleEmbedUrl(docId, type) + + const typeLabels = { + google_doc: "Google Doc", + google_sheet: "Google Sheet", + google_slide: "Google Slides", + } + + return ( + <div className="flex flex-col h-full w-full overflow-hidden"> + {loading && ( + <div className="absolute inset-0 flex items-center justify-center bg-[#14161A] z-10"> + <div className="flex items-center gap-2 text-gray-400"> + <Loader2 className="w-5 h-5 animate-spin" /> + <span>Loading {typeLabels[type]}...</span> + </div> + </div> + )} + {error && ( + <div className="flex items-center justify-center h-full text-red-400"> + {error} + </div> + )} + <iframe + src={embedUrl} + className="flex-1 w-full h-full border-0 rounded-[14px]" + onLoad={() => setLoading(false)} + onError={() => { + setLoading(false) + setError("Failed to load document") + }} + allow="autoplay" + title={typeLabels[type]} + /> + </div> + ) +} diff --git a/apps/web/components/new/document-modal/content/image-preview.tsx b/apps/web/components/new/document-modal/content/image-preview.tsx new file mode 100644 index 00000000..31d5c1c0 --- /dev/null +++ b/apps/web/components/new/document-modal/content/image-preview.tsx @@ -0,0 +1,56 @@ +"use client" + +import { useState } from "react" +import { cn } from "@lib/utils" + +interface ImagePreviewProps { + url: string + title?: string | null +} + +export function ImagePreview({ url, title }: ImagePreviewProps) { + const [imageError, setImageError] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + if (imageError || !url) { + return ( + <div className="flex items-center justify-center h-full text-[#737373]"> + <p>Failed to load image</p> + </div> + ) + } + + return ( + <div className="relative w-full h-full overflow-hidden flex items-center justify-center bg-[#0B1017]"> + {isLoading && ( + <div className="absolute inset-0 bg-cover bg-center animate-pulse"> + <div className="w-full h-full bg-[#1B1F24]" /> + </div> + )} + <div + className="absolute inset-0 bg-cover bg-center" + style={{ + backgroundImage: `url(${url})`, + filter: "blur(100px)", + transform: "scale(1.1)", + opacity: isLoading ? 0.5 : 1, + }} + /> + <div className="absolute inset-0 bg-black/30" /> + <img + src={url} + alt={title || "Image preview"} + className={cn( + "relative max-w-full max-h-full w-auto h-auto object-contain z-10", + isLoading && "opacity-0", + )} + onError={() => { + setImageError(true) + setIsLoading(false) + }} + onLoad={() => setIsLoading(false)} + loading="lazy" + /> + </div> + ) +} diff --git a/apps/web/components/new/document-modal/content/index.tsx b/apps/web/components/new/document-modal/content/index.tsx new file mode 100644 index 00000000..c06bc550 --- /dev/null +++ b/apps/web/components/new/document-modal/content/index.tsx @@ -0,0 +1,143 @@ +"use client" + +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" +import type { z } from "zod" +import dynamic from "next/dynamic" +import { isTwitterUrl } from "@/lib/url-helpers" +import { ImagePreview } from "./image-preview" +import { TweetContent } from "./tweet" +import { NotionDoc } from "./notion-doc" +import { YoutubeVideo } from "./yt-video" +import { WebPageContent } from "./web-page" +import { TextEditorContent } from "./text-editor-content" +import { GoogleDocViewer } from "./google-doc" +import type { TextEditorProps } from "./text-editor-content" + +export type { TextEditorProps } + +const PdfViewer = dynamic( + () => import("./pdf").then((mod) => ({ default: mod.PdfViewer })), + { + ssr: false, + loading: () => ( + <div className="flex items-center justify-center h-full text-gray-400"> + Loading PDF viewer... + </div> + ), + }, +) as typeof import("./pdf").PdfViewer + +type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema> +type DocumentWithMemories = DocumentsResponse["documents"][0] + +interface DocumentContentProps { + document: DocumentWithMemories | null + textEditorProps: TextEditorProps +} + +type ContentType = + | "image" + | "tweet" + | "text" + | "pdf" + | "notion" + | "youtube" + | "webpage" + | "google_doc" + | "google_sheet" + | "google_slide" + | null + +function getContentType(document: DocumentWithMemories | null): ContentType { + if (!document) return null + + const isImage = + document.type === "image" || + document.metadata?.mimeType?.toString().startsWith("image/") + + if (isImage && document.url) return "image" + if ( + document.type === "tweet" || + (document.url && isTwitterUrl(document.url)) + ) + return "tweet" + if (document.type === "text") return "text" + if (document.type === "pdf") return "pdf" + if (document.type === "notion_doc") return "notion" + if (document.type === "google_doc") return "google_doc" + if (document.type === "google_sheet") return "google_sheet" + if (document.type === "google_slide") return "google_slide" + if (document.url?.includes("youtube.com")) return "youtube" + if (document.type === "webpage") return "webpage" + + return null +} + +export function DocumentContent({ + document, + textEditorProps, +}: DocumentContentProps) { + const contentType = getContentType(document) + + if (!document || !contentType) return null + + switch (contentType) { + case "image": + return ( + <ImagePreview url={document.url ?? ""} title={document.title} /> + ) + + case "tweet": + return ( + <TweetContent + url={document.url} + tweetMetadata={document.metadata?.sm_internal_twitter_metadata} + /> + ) + + case "text": + return <TextEditorContent {...textEditorProps} /> + + case "pdf": + return <PdfViewer url={document.url} /> + + case "notion": + return <NotionDoc content={document.content ?? ""} /> + + case "youtube": + return <YoutubeVideo url={document.url ?? ""} /> + + case "webpage": + return <WebPageContent content={document.content ?? ""} /> + + case "google_doc": + return ( + <GoogleDocViewer + url={document.url} + customId={document.customId} + type="google_doc" + /> + ) + + case "google_sheet": + return ( + <GoogleDocViewer + url={document.url} + customId={document.customId} + type="google_sheet" + /> + ) + + case "google_slide": + return ( + <GoogleDocViewer + url={document.url} + customId={document.customId} + type="google_slide" + /> + ) + + default: + return null + } +} diff --git a/apps/web/components/new/document-modal/content/text-editor-content.tsx b/apps/web/components/new/document-modal/content/text-editor-content.tsx new file mode 100644 index 00000000..98c5beb4 --- /dev/null +++ b/apps/web/components/new/document-modal/content/text-editor-content.tsx @@ -0,0 +1,91 @@ +"use client" + +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/lib/fonts" +import { TextEditor } from "../../text-editor" +import { motion, AnimatePresence } from "motion/react" +import { Button } from "@repo/ui/components/button" +import { Loader2 } from "lucide-react" + +export interface TextEditorProps { + documentId: string + editorResetNonce: number + initialEditorContent: string | undefined + hasUnsavedChanges: boolean + isSaving: boolean + onContentChange: (content: string) => void + onSave: () => void + onReset: () => void +} + +export function TextEditorContent({ + documentId, + editorResetNonce, + initialEditorContent, + hasUnsavedChanges, + isSaving, + onContentChange, + onSave, + onReset, +}: TextEditorProps) { + return ( + <> + <div className="p-4 overflow-y-auto flex-1 scrollbar-thin"> + <TextEditor + key={`${documentId}-${editorResetNonce}`} + content={initialEditorContent} + onContentChange={onContentChange} + onSubmit={onSave} + /> + </div> + <AnimatePresence> + {hasUnsavedChanges && ( + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: 20 }} + transition={{ duration: 0.2 }} + className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3 px-4 py-2 bg-[#1B1F24] rounded-full shadow-[0_4px_20px_rgba(0,0,0,0.4),inset_1px_1px_1px_rgba(255,255,255,0.1)]" + > + <span className="text-sm text-[#737373]">Unsaved changes</span> + <Button + variant="ghost" + size="sm" + onClick={onReset} + disabled={isSaving} + className="text-[#737373]/80 hover:text-white rounded-full px-3" + > + Cancel + </Button> + <Button + variant="insideOut" + size="sm" + onClick={onSave} + disabled={isSaving} + className="hover:text-white rounded-full px-4" + > + {isSaving ? ( + <> + <Loader2 className="size-4 animate-spin mr-1" /> + Saving... + </> + ) : ( + <> + Save + <span + className={cn( + "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center", + dmSansClassName(), + )} + > + ⌘+Enter + </span> + </> + )} + </Button> + </motion.div> + )} + </AnimatePresence> + </> + ) +} |