e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
/**
|
|
* Minimal message/attachment shape the markdown exporter needs. Product chat's
|
|
* `MessageItem` satisfies it structurally, and partner conversations map into
|
|
* it too — so both surfaces share one serializer.
|
|
*/
|
|
export interface ExportableAttachment {
|
|
type?: string;
|
|
filename?: string;
|
|
mime_type?: string;
|
|
}
|
|
|
|
export interface ExportableMessage {
|
|
role: string;
|
|
content: string;
|
|
capability?: string;
|
|
attachments?: ExportableAttachment[];
|
|
}
|
|
|
|
function roleHeading(role: string): string {
|
|
if (role === "user") return "User";
|
|
if (role === "assistant") return "Assistant";
|
|
return "System";
|
|
}
|
|
|
|
function formatAttachments(attachments?: ExportableAttachment[]): string {
|
|
if (!attachments?.length) return "";
|
|
const items = attachments
|
|
.map((a) => {
|
|
const name = a.filename || a.type || "attachment";
|
|
return a.mime_type ? `\`${name}\` (${a.mime_type})` : `\`${name}\``;
|
|
})
|
|
.join(", ");
|
|
return `_Attachments:_ ${items}\n\n`;
|
|
}
|
|
|
|
export interface BuildChatMarkdownOptions {
|
|
title?: string;
|
|
exportedAt?: Date;
|
|
}
|
|
|
|
export function buildChatMarkdown(
|
|
messages: ExportableMessage[],
|
|
options: BuildChatMarkdownOptions = {},
|
|
): string {
|
|
const title = options.title?.trim() || "Chat Session";
|
|
const exportedAt = (options.exportedAt ?? new Date()).toISOString();
|
|
const header = `# ${title}\n\n_Exported: ${exportedAt}_\n\n---\n\n`;
|
|
const body = messages
|
|
.map((msg) => {
|
|
const role = roleHeading(msg.role);
|
|
const cap = msg.capability ? ` _(${msg.capability})_` : "";
|
|
const attachments = formatAttachments(msg.attachments);
|
|
const content = (msg.content ?? "").trim();
|
|
return `## ${role}${cap}\n\n${attachments}${content}`.trimEnd();
|
|
})
|
|
.join("\n\n---\n\n");
|
|
return header + body + "\n";
|
|
}
|
|
|
|
function sanitizeFilename(input: string): string {
|
|
const cleaned = input
|
|
.replace(/[\\/:*?"<>|\n\r\t]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.slice(0, 60);
|
|
return cleaned || "chat";
|
|
}
|
|
|
|
export function downloadChatMarkdown(
|
|
messages: ExportableMessage[],
|
|
options: BuildChatMarkdownOptions = {},
|
|
): void {
|
|
if (!messages.length) return;
|
|
const markdown = buildChatMarkdown(messages, options);
|
|
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
const date = new Date().toISOString().slice(0, 10);
|
|
anchor.download = `${sanitizeFilename(options.title || "chat")}-${date}.md`;
|
|
anchor.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|