diff --git a/surfsense_web/components/editor-panel/editor-panel.tsx b/surfsense_web/components/editor-panel/editor-panel.tsx index 30dcdeb2c..b83c4b1d7 100644 --- a/surfsense_web/components/editor-panel/editor-panel.tsx +++ b/surfsense_web/components/editor-panel/editor-panel.tsx @@ -1,7 +1,17 @@ "use client"; import { useAtomValue, useSetAtom } from "jotai"; -import { Download, FileQuestionMark, FileText, Loader2, RefreshCw, XIcon } from "lucide-react"; +import { + Check, + Copy, + Download, + FileQuestionMark, + FileText, + Loader2, + Pencil, + RefreshCw, + XIcon, +} from "lucide-react"; import dynamic from "next/dynamic"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -78,10 +88,13 @@ export function EditorPanelContent({ const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [downloading, setDownloading] = useState(false); + const [isSourceEditing, setIsSourceEditing] = useState(false); const [editedMarkdown, setEditedMarkdown] = useState(null); const [localFileContent, setLocalFileContent] = useState(""); + const [hasCopied, setHasCopied] = useState(false); const markdownRef = useRef(""); + const copyResetTimeoutRef = useRef | null>(null); const initialLoadDone = useRef(false); const changeCountRef = useRef(0); const [displayTitle, setDisplayTitle] = useState(title || "Untitled"); @@ -97,6 +110,8 @@ export function EditorPanelContent({ setEditorDoc(null); setEditedMarkdown(null); setLocalFileContent(""); + setHasCopied(false); + setIsSourceEditing(false); initialLoadDone.current = false; changeCountRef.current = 0; @@ -179,6 +194,14 @@ export function EditorPanelContent({ return () => controller.abort(); }, [documentId, electronAPI, isLocalFileMode, localFilePath, searchSpaceId, title]); + useEffect(() => { + return () => { + if (copyResetTimeoutRef.current) { + clearTimeout(copyResetTimeoutRef.current); + } + }; + }, []); + const handleMarkdownChange = useCallback((md: string) => { markdownRef.current = md; if (!initialLoadDone.current) return; @@ -187,6 +210,22 @@ export function EditorPanelContent({ setEditedMarkdown(md); }, []); + const handleCopy = useCallback(async () => { + try { + const textToCopy = markdownRef.current ?? editorDoc?.source_markdown ?? ""; + await navigator.clipboard.writeText(textToCopy); + setHasCopied(true); + if (copyResetTimeoutRef.current) { + clearTimeout(copyResetTimeoutRef.current); + } + copyResetTimeoutRef.current = setTimeout(() => { + setHasCopied(false); + }, 1400); + } catch (err) { + console.error("Error copying content:", err); + } + }, [editorDoc?.source_markdown]); + const handleSave = useCallback(async (options?: { silent?: boolean }) => { setSaving(true); try { @@ -209,7 +248,7 @@ export function EditorPanelContent({ prev ? { ...prev, source_markdown: contentToSave } : prev ); setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current); - return; + return true; } if (!searchSpaceId || !documentId) { throw new Error("Missing document context"); @@ -239,9 +278,11 @@ export function EditorPanelContent({ setEditorDoc((prev) => (prev ? { ...prev, source_markdown: markdownRef.current } : prev)); setEditedMarkdown(null); toast.success("Document saved! Reindexing in background..."); + return true; } catch (err) { console.error("Error saving document:", err); toast.error(err instanceof Error ? err.message : "Failed to save document"); + return false; } finally { setSaving(false); } @@ -252,26 +293,111 @@ export function EditorPanelContent({ EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "")) && !isLargeDocument : false; + const hasUnsavedChanges = editedMarkdown !== null; + const showDesktopHeader = !!onClose; + const isSourceCodeMode = editorRenderMode === "source_code"; + const showEditingActions = isSourceCodeMode && isSourceEditing; const localFileLanguage = inferMonacoLanguageFromPath(localFilePath); return ( <> -
-
-

{displayTitle}

+ {showDesktopHeader ? ( +
+
+

File

+
+ +
+
+
+
+

{displayTitle}

+
+
+ {showEditingActions ? ( + <> + + + + ) : ( + <> + + {isSourceCodeMode && ( + + )} + + )} + {!showEditingActions && !isLocalFileMode && editorDoc?.document_type && documentId && ( + + )} +
+
-
- {!isLocalFileMode && editorDoc?.document_type && documentId && ( - - )} - {onClose && ( - - )} + ) : ( +
+
+

{displayTitle}

+
+
+ {!isLocalFileMode && editorDoc?.document_type && documentId && ( + + )} +
-
+ )}
{isLoading ? ( @@ -360,8 +486,10 @@ export function EditorPanelContent({ path={localFilePath ?? "local-file.txt"} language={localFileLanguage} value={localFileContent} - onSave={() => handleSave({ silent: true })} - saveMode="auto" + onSave={() => { + void handleSave({ silent: true }); + }} + readOnly={!isSourceEditing} onChange={(next) => { markdownRef.current = next; setLocalFileContent(next); @@ -379,7 +507,9 @@ export function EditorPanelContent({ readOnly={false} placeholder="Start writing..." editorVariant="default" - onSave={handleSave} + onSave={() => { + void handleSave(); + }} hasUnsavedChanges={editedMarkdown !== null} isSaving={saving} defaultEditing={true} diff --git a/surfsense_web/components/editor/source-code-editor.tsx b/surfsense_web/components/editor/source-code-editor.tsx index 2c1f52989..11f9266b6 100644 --- a/surfsense_web/components/editor/source-code-editor.tsx +++ b/surfsense_web/components/editor/source-code-editor.tsx @@ -17,8 +17,6 @@ interface SourceCodeEditorProps { readOnly?: boolean; fontSize?: number; onSave?: () => Promise | void; - saveMode?: "manual" | "auto" | "both"; - autoSaveDelayMs?: number; } export function SourceCodeEditor({ @@ -29,64 +27,78 @@ export function SourceCodeEditor({ readOnly = false, fontSize = 12, onSave, - saveMode = "manual", - autoSaveDelayMs = 800, }: SourceCodeEditorProps) { const { resolvedTheme } = useTheme(); - const saveTimerRef = useRef | null>(null); const onSaveRef = useRef(onSave); - const skipNextAutoSaveRef = useRef(true); + const monacoRef = useRef(null); useEffect(() => { onSaveRef.current = onSave; }, [onSave]); - useEffect(() => { - skipNextAutoSaveRef.current = true; - }, [path]); + const resolveCssColorToHex = (cssColorValue: string): string | null => { + if (typeof document === "undefined") return null; + const probe = document.createElement("div"); + probe.style.color = cssColorValue; + probe.style.position = "absolute"; + probe.style.pointerEvents = "none"; + probe.style.opacity = "0"; + document.body.appendChild(probe); + const computedColor = getComputedStyle(probe).color; + probe.remove(); + const match = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); + if (!match) return null; + const toHex = (value: string) => Number(value).toString(16).padStart(2, "0"); + return `#${toHex(match[1])}${toHex(match[2])}${toHex(match[3])}`; + }; + + const applySidebarTheme = (monaco: any) => { + const isDark = resolvedTheme === "dark"; + const themeName = isDark ? "surfsense-dark" : "surfsense-light"; + const fallbackBg = isDark ? "#1e1e1e" : "#ffffff"; + const sidebarBgHex = resolveCssColorToHex("var(--sidebar)") ?? fallbackBg; + monaco.editor.defineTheme(themeName, { + base: isDark ? "vs-dark" : "vs", + inherit: true, + rules: [], + colors: { + "editor.background": sidebarBgHex, + "editorGutter.background": sidebarBgHex, + "minimap.background": sidebarBgHex, + "editorLineNumber.background": sidebarBgHex, + "editor.lineHighlightBackground": "#00000000", + }, + }); + monaco.editor.setTheme(themeName); + }; useEffect(() => { - if (readOnly || !onSaveRef.current) return; - if (saveMode !== "auto" && saveMode !== "both") return; + if (!monacoRef.current) return; + applySidebarTheme(monacoRef.current); + }, [resolvedTheme]); - if (skipNextAutoSaveRef.current) { - skipNextAutoSaveRef.current = false; - return; - } - - if (saveTimerRef.current) { - clearTimeout(saveTimerRef.current); - } - - saveTimerRef.current = setTimeout(() => { - void onSaveRef.current?.(); - saveTimerRef.current = null; - }, autoSaveDelayMs); - - return () => { - if (saveTimerRef.current) { - clearTimeout(saveTimerRef.current); - saveTimerRef.current = null; - } - }; - }, [autoSaveDelayMs, readOnly, saveMode, value]); - - const isManualSaveEnabled = !!onSave && !readOnly && (saveMode === "manual" || saveMode === "both"); + const isManualSaveEnabled = !!onSave && !readOnly; return ( -
+
onChange(next ?? "")} loading={
} + beforeMount={(monaco) => { + monacoRef.current = monaco; + applySidebarTheme(monaco); + }} onMount={(editor, monaco) => { + monacoRef.current = monaco; + applySidebarTheme(monaco); if (!isManualSaveEnabled) return; editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { void onSaveRef.current?.(); diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx index 2394480b2..c2422bf34 100644 --- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx +++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx @@ -94,7 +94,7 @@ export function RightPanelExpandButton() { Expand panel - Expand panel + Expand panel
);