124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import type { ChatMessage } from "../types/message";
|
|
import { escHtml } from "./format";
|
|
|
|
function roleLabel(role: ChatMessage["role"]): string {
|
|
switch (role) {
|
|
case "user":
|
|
return "用户";
|
|
case "assistant":
|
|
return "助手";
|
|
case "thinking":
|
|
return "思考";
|
|
case "tool_call":
|
|
return "工具";
|
|
case "system":
|
|
return "系统";
|
|
case "bash":
|
|
return "Shell";
|
|
case "compaction_summary":
|
|
return "压缩摘要";
|
|
case "branch_summary":
|
|
return "分支摘要";
|
|
default:
|
|
return "消息";
|
|
}
|
|
}
|
|
|
|
function exportableMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
return messages.filter((m) => !m.streaming && m.content.trim());
|
|
}
|
|
|
|
export function messagesToMarkdown(title: string, messages: ChatMessage[]): string {
|
|
const lines = [`# ${title}`, ""];
|
|
for (const m of exportableMessages(messages)) {
|
|
switch (m.role) {
|
|
case "user":
|
|
lines.push("## 用户", "", m.content, "");
|
|
break;
|
|
case "assistant":
|
|
lines.push("## 助手", "", m.content, "");
|
|
break;
|
|
case "thinking":
|
|
lines.push("## 思考", "", m.content, "");
|
|
break;
|
|
case "tool_call":
|
|
lines.push("## 工具", "", "```", m.content, "```", "");
|
|
break;
|
|
case "system":
|
|
lines.push(`> ${m.content}`, "");
|
|
break;
|
|
case "bash": {
|
|
const cmd = m.bashCommand ? `$ ${m.bashCommand}` : "Shell";
|
|
lines.push(`## Shell (${cmd})`, "", "```", m.content, "```", "");
|
|
break;
|
|
}
|
|
case "compaction_summary":
|
|
lines.push("## 压缩摘要", "", m.content, "");
|
|
break;
|
|
case "branch_summary":
|
|
lines.push("## 分支摘要", "", m.content, "");
|
|
break;
|
|
}
|
|
}
|
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
}
|
|
|
|
export function messagesToMinimalHtml(title: string, messages: ChatMessage[]): string {
|
|
const sections = exportableMessages(messages)
|
|
.map((m) => {
|
|
const label = roleLabel(m.role);
|
|
return `<section class="block"><h2>${escHtml(label)}</h2><pre>${escHtml(m.content)}</pre></section>`;
|
|
})
|
|
.join("\n");
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${escHtml(title)}</title>
|
|
<style>
|
|
body{font-family:"LXGW WenKai Mono","PingFang SC","Microsoft YaHei UI",ui-monospace,monospace;max-width:720px;margin:2rem auto;padding:0 1rem;line-height:1.55;color:#111;background:#fff}
|
|
h1{font-size:1.25rem;font-weight:600;margin:0 0 1.25rem}
|
|
.block{margin:0 0 1rem}
|
|
h2{font-size:.75rem;font-weight:600;color:#6b7280;margin:0 0 .35rem;text-transform:uppercase;letter-spacing:.04em}
|
|
pre{margin:0;white-space:pre-wrap;word-break:break-word;font-size:.9375rem;font-family:inherit}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${escHtml(title)}</h1>
|
|
${sections}
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
export function safeExportFilename(title: string): string {
|
|
const cleaned = title.replace(/[\\/:*?"<>|]/g, "_").trim();
|
|
return cleaned || "conversation";
|
|
}
|
|
|
|
export function downloadTextFile(filename: string, content: string, mime: string): void {
|
|
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = filename;
|
|
anchor.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
export function singleAssistantMarkdown(content: string): string {
|
|
return content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
|
|
}
|
|
|
|
export function messageMarkdownFilename(content: string): string {
|
|
const snippet =
|
|
content
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.slice(0, 24)
|
|
.replace(/[\\/:*?"<>|]/g, "_") || "message";
|
|
return `${snippet}.md`;
|
|
}
|