Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

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);
}