From f16b9172471a5f603890e88e287a25920459c00a Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Fri, 10 Apr 2026 23:21:06 -0400 Subject: [PATCH 1/3] feat: export conversation --- src/icons/Export.tsx | 18 ++++++++ src/icons/index.ts | 1 + src/plugin/Panel/ChatInput/index.tsx | 68 +++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/icons/Export.tsx diff --git a/src/icons/Export.tsx b/src/icons/Export.tsx new file mode 100644 index 0000000..bb59eb6 --- /dev/null +++ b/src/icons/Export.tsx @@ -0,0 +1,18 @@ +export const Export: React.FC> = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/icons/index.ts b/src/icons/index.ts index 2d61479..34090c7 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -3,5 +3,6 @@ export { ArrowUp } from "./ArrowUp"; export { BulletList } from "./BulletList"; export { Clear } from "./Clear"; export { Close } from "./Close"; +export { Export } from "./Export"; export { Gear } from "./Gear"; export { TextFile } from "./TextFile"; diff --git a/src/plugin/Panel/ChatInput/index.tsx b/src/plugin/Panel/ChatInput/index.tsx index 103ae39..2be5483 100644 --- a/src/plugin/Panel/ChatInput/index.tsx +++ b/src/plugin/Panel/ChatInput/index.tsx @@ -1,6 +1,6 @@ import { Button, PromptInput } from "@components"; import { usePlugin } from "@context"; -import { Add, ArrowUp, Clear } from "@icons"; +import { Add, ArrowUp, Clear, Export } from "@icons"; import { UserMessage } from "@types"; import type { FC } from "react"; import { useState } from "react"; @@ -81,6 +81,61 @@ export const ChatInput: FC = () => { dispatch({ type: "setMediaDialogState", state: "open" }); } + function exportConversation() { + const title = state.manifest.label?.en?.[0] ?? state.manifest.id; + const exportDate = new Date().toISOString(); + + let markdown = `# ${title}\n\n`; + markdown += `- **Name**: ${state.manifest.label?.en?.[0] ?? "N/A"}\n`; + markdown += `- **Manifest URI**: ${state.manifest.id}\n`; + markdown += `- **Exported at**: ${exportDate}\n\n`; + + for (const message of state.messages) { + switch (message.role) { + case "user": { + markdown += `## User Message\n\n`; + for (const content of message.content) { + switch (content.type) { + case "text": + markdown += `${content.content}\n\n`; + break; + case "annotation": + markdown += `### Annotation\n\n${content.content.content}\n\n`; + break; + case "media": + markdown += `### Media\n\n[${content.content?.caption ?? "IIIF Image"}](${content.content.src})\n\n`; + break; + } + } + break; + } + case "assistant": { + markdown += `## Assistant Message\n\n`; + switch (message.type) { + case "tool-call": + markdown += `### Tool Calls\n\n`; + markdown += `**${message.content.tool_name}**: ${message.content.content}\n\n`; + break; + case "response": + markdown += `### Response\n\n${message.content.content}\n\n`; + break; + } + break; + } + default: + break; + } + } + + const blob = new Blob([markdown], { type: "text/markdown" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${title.replace(/[^a-z0-9]/gi, "_")}.md`; + a.click(); + URL.revokeObjectURL(url); + } + const PromptInputButtons = state?.provider?.PromptInputButtons?.bind(state.provider) ?? null; // bind `this`; return (
{ > +