diff --git a/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts index 1d106796a..384854185 100644 --- a/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts +++ b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts @@ -13,6 +13,7 @@ interface HitlEditPanelState { title: string; content: string; toolName: string; + contentFormat?: "markdown" | "html"; extraFields?: ExtraField[]; onSave: | ((title: string, content: string, extraFieldValues?: Record) => void) @@ -25,6 +26,7 @@ const initialState: HitlEditPanelState = { title: "", content: "", toolName: "", + contentFormat: undefined, extraFields: undefined, onSave: null, onClose: null, @@ -43,6 +45,7 @@ export const openHitlEditPanelAtom = atom( title: string; content: string; toolName: string; + contentFormat?: "markdown" | "html"; extraFields?: ExtraField[]; onSave: (title: string, content: string, extraFieldValues?: Record) => void; onClose?: () => void; @@ -56,6 +59,7 @@ export const openHitlEditPanelAtom = atom( title: payload.title, content: payload.content, toolName: payload.toolName, + contentFormat: payload.contentFormat, extraFields: payload.extraFields, onSave: payload.onSave, onClose: payload.onClose ?? null, diff --git a/surfsense_web/components/editor/plate-editor.tsx b/surfsense_web/components/editor/plate-editor.tsx index 688b481f1..5df5ad1a7 100644 --- a/surfsense_web/components/editor/plate-editor.tsx +++ b/surfsense_web/components/editor/plate-editor.tsx @@ -1,7 +1,8 @@ "use client"; import { MarkdownPlugin, remarkMdx } from "@platejs/markdown"; -import type { AnyPluginConfig } from "platejs"; +import { slateToHtml } from "@slate-serializers/html"; +import type { AnyPluginConfig, Descendant, Value } from "platejs"; import { createPlatePlugin, Key, Plate, usePlateEditor } from "platejs/react"; import { useEffect, useMemo, useRef } from "react"; import remarkGfm from "remark-gfm"; @@ -14,8 +15,12 @@ import { Editor, EditorContainer } from "@/components/ui/editor"; export interface PlateEditorProps { /** Markdown string to load as initial content */ markdown?: string; + /** HTML string to load as initial content. Takes precedence over `markdown`. */ + html?: string; /** Called when the editor content changes, with serialized markdown */ onMarkdownChange?: (markdown: string) => void; + /** Called when the editor content changes, with serialized HTML. Use with the `html` prop. */ + onHtmlChange?: (html: string) => void; /** * Force permanent read-only mode (e.g. public/shared view). * When true, the editor cannot be toggled to editing mode. @@ -57,7 +62,9 @@ export interface PlateEditorProps { export function PlateEditor({ markdown, + html, onMarkdownChange, + onHtmlChange, readOnly = false, placeholder = "Type...", variant = "default", @@ -71,6 +78,7 @@ export function PlateEditor({ extraPlugins = [], }: PlateEditorProps) { const lastMarkdownRef = useRef(markdown); + const lastHtmlRef = useRef(html); // Keep a stable ref to the latest onSave callback so the plugin shortcut // always calls the most recent version without re-creating the editor. @@ -118,17 +126,28 @@ export function PlateEditor({ }, }), ], - // Use markdown deserialization for initial value if provided - value: markdown - ? (editor) => - editor.getApi(MarkdownPlugin).markdown.deserialize(escapeMdxExpressions(markdown)) - : undefined, + value: html + ? (editor) => editor.api.html.deserialize({ element: html }) as Value + : markdown + ? (editor) => + editor.getApi(MarkdownPlugin).markdown.deserialize(escapeMdxExpressions(markdown)) + : undefined, }); + // Update editor content when html prop changes externally + useEffect(() => { + if (html !== undefined && html !== lastHtmlRef.current) { + lastHtmlRef.current = html; + const newValue = editor.api.html.deserialize({ element: html }); + editor.tf.reset(); + editor.tf.setValue(newValue); + } + }, [html, editor]); + // Update editor content when markdown prop changes externally // (e.g., version switching in report panel) useEffect(() => { - if (markdown !== undefined && markdown !== lastMarkdownRef.current) { + if (!html && markdown !== undefined && markdown !== lastMarkdownRef.current) { lastMarkdownRef.current = markdown; const newValue = editor .getApi(MarkdownPlugin) @@ -136,7 +155,7 @@ export function PlateEditor({ editor.tf.reset(); editor.tf.setValue(newValue); } - }, [markdown, editor]); + }, [html, markdown, editor]); // When not forced read-only, the user can toggle between editing/viewing. const canToggleMode = !readOnly; @@ -157,7 +176,10 @@ export function PlateEditor({ // (initialized to true via usePlateEditor, toggled via ModeToolbarButton). {...(readOnly ? { readOnly: true } : {})} onChange={({ value }) => { - if (onMarkdownChange) { + if (onHtmlChange && html) { + const serialized = slateToHtml(value as Descendant[]); + onHtmlChange(serialized); + } else if (onMarkdownChange) { const md = editor.getApi(MarkdownPlugin).markdown.serialize({ value }); lastMarkdownRef.current = md; onMarkdownChange(md); diff --git a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx index b11e9aa84..25e896842 100644 --- a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx +++ b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx @@ -194,6 +194,7 @@ function DateTimePickerField({ export function HitlEditPanelContent({ title: initialTitle, content: initialContent, + contentFormat, extraFields, onSave, onClose, @@ -202,13 +203,14 @@ export function HitlEditPanelContent({ title: string; content: string; toolName: string; + contentFormat?: "markdown" | "html"; extraFields?: ExtraField[]; onSave: (title: string, content: string, extraFieldValues?: Record) => void; onClose?: () => void; showCloseButton?: boolean; }) { const [editedTitle, setEditedTitle] = useState(initialTitle); - const markdownRef = useRef(initialContent); + const contentRef = useRef(initialContent); const [isSaving, setIsSaving] = useState(false); const [extraFieldValues, setExtraFieldValues] = useState>(() => { if (!extraFields) return {}; @@ -219,8 +221,8 @@ export function HitlEditPanelContent({ return initial; }); - const handleMarkdownChange = useCallback((md: string) => { - markdownRef.current = md; + const handleContentChange = useCallback((content: string) => { + contentRef.current = content; }, []); const handleExtraFieldChange = useCallback((key: string, value: string) => { @@ -231,7 +233,7 @@ export function HitlEditPanelContent({ if (!editedTitle.trim()) return; setIsSaving(true); const extras = extraFields && extraFields.length > 0 ? extraFieldValues : undefined; - onSave(editedTitle, markdownRef.current, extras); + onSave(editedTitle, contentRef.current, extras); onClose?.(); }, [editedTitle, onSave, onClose, extraFields, extraFieldValues]); @@ -299,8 +301,9 @@ export function HitlEditPanelContent({
{ setIsPanelOpen(false); setPendingEdits({ title: newTitle, content: newContent }); @@ -334,7 +335,7 @@ function ApprovalCard({ }} > { setIsPanelOpen(false); setEditedArgs({ @@ -305,7 +306,7 @@ function ApprovalCard({ }} > = 8'} + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -5208,6 +5220,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + icu-minify@4.8.3: resolution: {integrity: sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==} @@ -6616,6 +6631,9 @@ packages: slate: '>=0.114.0' slate-dom: '>=0.119.0' + slate@0.102.0: + resolution: {integrity: sha512-RT+tHgqOyZVB1oFV9Pv99ajwh4OUCN9p28QWdnDTIzaN/kZxMsHeQN39UNAgtkZTVVVygFqeg7/R2jiptCvfyA==} + slate@0.120.0: resolution: {integrity: sha512-CXK/DADGgMZb4z9RTtXylzIDOxvmNJEF9bXV2bAGkLWhQ3rm7GORY9q0H/W41YJvAGZsLbH7nnrhMYr550hWDQ==} @@ -6770,6 +6788,9 @@ packages: tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -6808,6 +6829,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -10206,6 +10230,30 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@slate-serializers/dom@2.2.3': + dependencies: + css-select: 5.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + html-entities: 2.6.0 + htmlparser2: 9.1.0 + slate: 0.102.0 + tslib: 2.6.3 + + '@slate-serializers/html@2.2.3': + dependencies: + '@slate-serializers/dom': 2.2.3 + css-select: 5.2.2 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + html-entities: 2.6.0 + htmlparser2: 9.1.0 + slate: 0.102.0 + slate-hyperscript: 0.100.0(slate@0.102.0) + tslib: 2.6.3 + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -11071,6 +11119,14 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -12158,6 +12214,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + icu-minify@4.8.3: dependencies: '@formatjs/icu-messageformat-parser': 3.5.1 @@ -14026,6 +14089,11 @@ snapshots: slate: 0.120.0 tiny-invariant: 1.3.1 + slate-hyperscript@0.100.0(slate@0.102.0): + dependencies: + is-plain-object: 5.0.0 + slate: 0.102.0 + slate-hyperscript@0.100.0(slate@0.120.0): dependencies: is-plain-object: 5.0.0 @@ -14044,6 +14112,12 @@ snapshots: slate-dom: 0.119.0(slate@0.120.0) tiny-invariant: 1.3.1 + slate@0.102.0: + dependencies: + immer: 10.2.0 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + slate@0.120.0: {} snake-case@3.0.4: @@ -14220,6 +14294,8 @@ snapshots: tiny-invariant@1.3.1: {} + tiny-warning@1.0.3: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -14252,6 +14328,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.6.3: {} + tslib@2.8.1: {} tsx@4.21.0: