const FunctionHistory = { oninit: function (vnode) { const versions = vnode.attrs.versions; vnode.state.mode = "view"; // 'view' or 'diff' vnode.state.selectedVersion = versions[0]; vnode.state.leftVersion = versions.length > 1 ? versions[1] : versions[0]; vnode.state.rightVersion = versions[0]; vnode.state.editor = null; vnode.state.aceDiffer = null; vnode.state.runtime = vnode.attrs.runtime || "node"; // Add runtime support // Listen for theme changes vnode.state.themeListener = (e) => { const newTheme = e.detail.theme; const aceTheme = newTheme === 'dark' ? "ace/theme/monokai" : "ace/theme/chrome"; if (vnode.state.editor) { vnode.state.editor.setTheme(aceTheme); } // Note: AceDiff might need a full re-init to change theme properly, or we can just let it be }; window.addEventListener('themeChanged', vnode.state.themeListener); }, view: function (vnode) { const { versions } = vnode.attrs; const { mode, selectedVersion, leftVersion, rightVersion } = vnode.state; return m(".flex.flex-col.md:flex-row.h-full.bg-white.dark:bg-gray-900.text-gray-900.dark:text-white", [ // Vertical Timeline m(".w-full.md:w-64.border-b.md:border-r.md:border-b-0.border-gray-200.dark:border-gray-700.overflow-y-auto.bg-gray-50.dark:bg-gray-800", [ m("div.p-4.border-b.border-gray-200.dark:border-gray-700.flex.justify-between.items-center", [ m("h3.text-lg.font-semibold", "History"), m( "button.text-sm.bg-gray-200.hover:bg-gray-300.dark:bg-gray-700.dark:hover:bg-gray-600.text-gray-800.dark:text-gray-200.font-semibold.py-1.px-2.rounded", { onclick: () => { vnode.state.mode = mode === "view" ? "diff" : "view"; }, }, mode === "view" ? "Compare" : "View" ), ]), m( "ul.p-2", versions.map((version) => { const isActive = selectedVersion && version.version_number === selectedVersion.version_number; return m( "li.p-2.cursor-pointer.mb-1", { class: isActive ? "bg-blue-100 dark:bg-blue-900/50 rounded" : "hover:bg-gray-200 dark:hover:bg-gray-700 rounded", onclick: () => { vnode.state.selectedVersion = version; if (mode === "diff") { vnode.state.rightVersion = version; } }, }, [ m("div.font-bold", `Version ${version.version_number}`), m( "div.text-sm.text-gray-600.dark:text-gray-400", new Date(version.versioned_at).toLocaleString() ), ] ); }) ), ]), // Code Viewer or Differ m(".flex-1.p-4.bg-white.dark:bg-gray-900", [ mode === "view" ? m("div", [ m("div.flex.justify-between.items-center.mb-2", [ m("h3.text-lg.font-semibold", `Version ${selectedVersion.version_number}`), m("button.bg-blue-500.hover:bg-blue-600.text-white.px-3.py-1.rounded.text-sm", { onclick: () => FunctionHistory.restoreVersion(vnode) }, "Restore this Version") ]), m("div#editor-history", { style: "height: 600px;" }), ]) : m("div", [ m(".flex.flex-col.md:flex-row.gap-4.mb-4", [ m(".flex-1", [ m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300", "Left"), m( "select.mt-1.block.w-full.rounded-md.border-gray-300.dark:border-gray-600.shadow-sm.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white", { onchange: (e) => (vnode.state.leftVersion = versions.find( (v) => v.version_number == e.target.value )), value: leftVersion.version_number, }, versions.map((v) => m( "option", { value: v.version_number }, `Version ${v.version_number}` ) ) ), ]), m(".flex-1", [ m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300", "Right"), m( "select.mt-1.block.w-full.rounded-md.border-gray-300.dark:border-gray-600.shadow-sm.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white", { onchange: (e) => (vnode.state.rightVersion = versions.find( (v) => v.version_number == e.target.value )), value: rightVersion.version_number, }, versions.map((v) => m( "option", { value: v.version_number }, `Version ${v.version_number}` ) ) ), ]), ]), m("#diff-container", { style: "height: 600px;" }), ]), ]), ]); }, oncreate: function (vnode) { this.updateEditorOrDiffer(vnode); }, onupdate: function (vnode) { this.updateEditorOrDiffer(vnode); }, updateEditorOrDiffer: function (vnode) { const { mode, selectedVersion, leftVersion, rightVersion } = vnode.state; const isDark = document.documentElement.classList.contains('dark'); const theme = isDark ? "ace/theme/monokai" : "ace/theme/chrome"; if (mode === "view") { if (vnode.state.aceDiffer) { vnode.state.aceDiffer.destroy(); vnode.state.aceDiffer = null; } if (!vnode.state.editor) { vnode.state.editor = ace.edit("editor-history"); vnode.state.editor.setTheme(theme); const editorMode = vnode.state.runtime === "python" ? "ace/mode/python" : "ace/mode/javascript"; vnode.state.editor.session.setMode(editorMode); vnode.state.editor.setReadOnly(true); } else { vnode.state.editor.setTheme(theme); } vnode.state.editor.setValue(selectedVersion.script, -1); } else { // diff mode if (vnode.state.editor) { vnode.state.editor.destroy(); vnode.state.editor = null; } if (vnode.state.aceDiffer) { vnode.state.aceDiffer.destroy(); } const diffMode = vnode.state.runtime === "python" ? "ace/mode/python" : "ace/mode/javascript"; vnode.state.aceDiffer = new AceDiff({ element: "#diff-container", mode: diffMode, theme: theme, left: { content: leftVersion.script, editable: false }, right: { content: rightVersion.script, editable: false }, }); } }, restoreVersion: async function (vnode) { const { selectedVersion } = vnode.state; const { restore_url } = vnode.attrs; if (!confirm(`Are you sure you want to restore version ${selectedVersion.version_number}? This will create a new version with this content.`)) { return; } try { const response = await m.request({ method: "POST", url: restore_url, body: { version_number: selectedVersion.version_number } }); if (response.status === "success") { Alert.show(response.message, "success"); // Reload the page to show the new version window.location.reload(); } else { Alert.show(response.message || "Error restoring version", "error"); } } catch (err) { Alert.show(err.message || "Error restoring version", "error"); } }, onremove: function (vnode) { if (vnode.state.editor) { vnode.state.editor.destroy(); } if (vnode.state.aceDiffer) { vnode.state.aceDiffer.destroy(); } if (vnode.state.themeListener) { window.removeEventListener('themeChanged', vnode.state.themeListener); } }, };