diff --git a/packages/frontend/src/components/content/Header.vue b/packages/frontend/src/components/content/Header.vue
index 4c160d3..a548180 100644
--- a/packages/frontend/src/components/content/Header.vue
+++ b/packages/frontend/src/components/content/Header.vue
@@ -1,17 +1,40 @@
diff --git a/packages/frontend/src/index.ts b/packages/frontend/src/index.ts
index 1ef127c..89a4c11 100644
--- a/packages/frontend/src/index.ts
+++ b/packages/frontend/src/index.ts
@@ -85,6 +85,9 @@ export const init = (sdk: FrontendSDK) => {
onEnter: () => {
emitter.emit("refreshEditors");
checkLegacyNotes(sdk);
+ setTimeout(() => {
+ emitter.emit("restoreFocus");
+ }, 200);
},
});
diff --git a/packages/frontend/src/utils/eventBus.ts b/packages/frontend/src/utils/eventBus.ts
index 6c120bd..74d3dcd 100644
--- a/packages/frontend/src/utils/eventBus.ts
+++ b/packages/frontend/src/utils/eventBus.ts
@@ -3,6 +3,7 @@ import mitt from "mitt";
type Events = {
refreshEditors: void;
refreshTree: number;
+ restoreFocus: void;
showMigrationDialog: { path: string; content: string }[];
confirmMigration: { path: string; content: string }[];
};
diff --git a/packages/frontend/src/utils/jsonToMarkdown.ts b/packages/frontend/src/utils/jsonToMarkdown.ts
new file mode 100644
index 0000000..28efdbe
--- /dev/null
+++ b/packages/frontend/src/utils/jsonToMarkdown.ts
@@ -0,0 +1,128 @@
+import { Editor } from "@tiptap/core";
+import { StarterKit } from "@tiptap/starter-kit";
+import { type NoteContent, type NoteContentItem } from "shared";
+import { Markdown } from "tiptap-markdown";
+
+import { type FrontendSDK } from "@/types";
+
+async function fetchReplaySessionContent(
+ sdk: FrontendSDK,
+ sessionId: string,
+): Promise {
+ try {
+ const sessionResponse = await sdk.graphql.replaySessionEntries({
+ id: sessionId,
+ });
+ const activeEntryId = sessionResponse?.replaySession?.activeEntry?.id;
+
+ if (!activeEntryId) {
+ return undefined;
+ }
+
+ const entryResponse = await sdk.graphql.replayEntry({
+ id: activeEntryId,
+ });
+
+ return entryResponse?.replayEntry?.raw || undefined;
+ } catch (error) {
+ console.error("Error fetching replay session content:", error);
+ return undefined;
+ }
+}
+
+function collectMentionIds(content: NoteContentItem[]): string[] {
+ const ids: string[] = [];
+
+ for (const node of content) {
+ if (node.type === "mention" && node.attrs?.id) {
+ ids.push(node.attrs.id as string);
+ }
+ if (node.content) {
+ ids.push(...collectMentionIds(node.content));
+ }
+ }
+
+ return ids;
+}
+
+function replaceMentionsWithCodeBlocks(
+ content: NoteContentItem[],
+ httpContentMap: Map,
+): NoteContentItem[] {
+ const result: NoteContentItem[] = [];
+
+ for (const node of content) {
+ if (node.type === "mention" && node.attrs?.id) {
+ const sessionId = node.attrs.id as string;
+ const httpContent = httpContentMap.get(sessionId);
+
+ if (httpContent) {
+ result.push({
+ type: "codeBlock",
+ attrs: { language: "http" },
+ content: [
+ {
+ type: "text",
+ text: httpContent,
+ },
+ ],
+ });
+ } else {
+ result.push({
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: `[Replay Session: ${sessionId} (unavailable)]`,
+ },
+ ],
+ });
+ }
+ } else if (node.content) {
+ result.push({
+ ...node,
+ content: replaceMentionsWithCodeBlocks(node.content, httpContentMap),
+ });
+ } else {
+ result.push(node);
+ }
+ }
+
+ return result;
+}
+
+export async function convertTipTapToMarkdown(
+ content: NoteContent,
+ sdk: FrontendSDK,
+): Promise {
+ const mentionIds = collectMentionIds(content.content || []);
+
+ const httpContentMap = new Map();
+ if (mentionIds.length > 0) {
+ const fetchPromises = mentionIds.map(async (id) => {
+ const httpContent = await fetchReplaySessionContent(sdk, id);
+ if (httpContent) {
+ httpContentMap.set(id, httpContent);
+ }
+ });
+ await Promise.all(fetchPromises);
+ }
+
+ const processedContent: NoteContent = {
+ ...content,
+ content: replaceMentionsWithCodeBlocks(
+ content.content || [],
+ httpContentMap,
+ ),
+ };
+
+ const editor = new Editor({
+ extensions: [StarterKit, Markdown],
+ content: processedContent,
+ });
+
+ const markdown = editor.storage.markdown.getMarkdown();
+ editor.destroy();
+
+ return markdown;
+}