mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 22:28:48 +03:00
claude-code
This commit is contained in:
154
web/lib/export/html.ts
Normal file
154
web/lib/export/html.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { Conversation, Message, ExportOptions } from "../types";
|
||||
import { extractTextContent } from "../utils";
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderMessageHtml(msg: Message, options: ExportOptions): string {
|
||||
const isUser = msg.role === "user";
|
||||
const isError = msg.status === "error";
|
||||
|
||||
const roleClass = isUser ? "user" : isError ? "error" : "assistant";
|
||||
const roleLabel =
|
||||
msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
|
||||
|
||||
let contentHtml = "";
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
contentHtml = `<p class="message-text">${escapeHtml(msg.content)}</p>`;
|
||||
} else {
|
||||
const parts: string[] = [];
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push(`<p class="message-text">${escapeHtml(block.text)}</p>`);
|
||||
} else if (block.type === "tool_use" && options.includeToolUse) {
|
||||
parts.push(`
|
||||
<details class="tool-block">
|
||||
<summary class="tool-summary">Tool: <code>${escapeHtml(block.name)}</code></summary>
|
||||
<pre class="tool-code">${escapeHtml(JSON.stringify(block.input, null, 2))}</pre>
|
||||
</details>`);
|
||||
} else if (block.type === "tool_result" && options.includeToolUse) {
|
||||
const raw =
|
||||
typeof block.content === "string"
|
||||
? block.content
|
||||
: extractTextContent(block.content);
|
||||
const text =
|
||||
!options.includeFileContents && raw.length > 500
|
||||
? raw.slice(0, 500) + "\n…[truncated]"
|
||||
: raw;
|
||||
parts.push(`
|
||||
<details class="tool-block${block.is_error ? " tool-error" : ""}">
|
||||
<summary class="tool-summary">Tool Result${block.is_error ? " (error)" : ""}</summary>
|
||||
<pre class="tool-code">${escapeHtml(text)}</pre>
|
||||
</details>`);
|
||||
}
|
||||
}
|
||||
contentHtml = parts.join("\n");
|
||||
}
|
||||
|
||||
const timestampHtml = options.includeTimestamps
|
||||
? `<span class="message-time">${new Date(msg.createdAt).toLocaleString()}</span>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="message message--${roleClass}">
|
||||
<div class="message-header">
|
||||
<span class="message-role">${escapeHtml(roleLabel)}</span>
|
||||
${timestampHtml}
|
||||
</div>
|
||||
<div class="message-content">
|
||||
${contentHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const CSS = `
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 15px; line-height: 1.6;
|
||||
background: #0f1117; color: #e2e8f0;
|
||||
padding: 2rem; max-width: 900px; margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 1.5rem; font-weight: 700; color: #f1f5f9; margin-bottom: 0.5rem; }
|
||||
.meta { font-size: 0.8rem; color: #94a3b8; margin-bottom: 2rem; display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.meta span::before { content: ""; }
|
||||
.messages { display: flex; flex-direction: column; gap: 1.5rem; }
|
||||
.message { border-radius: 12px; overflow: hidden; }
|
||||
.message--user .message-header { background: #2563eb; }
|
||||
.message--assistant .message-header { background: #1e293b; }
|
||||
.message--error .message-header { background: #7f1d1d; }
|
||||
.message-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.5rem 1rem; gap: 1rem;
|
||||
}
|
||||
.message-role { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.message-time { font-size: 0.7rem; color: rgba(255,255,255,0.6); }
|
||||
.message--user { background: #1e3a5f; }
|
||||
.message--assistant { background: #1e293b; }
|
||||
.message--error { background: #1c0e0e; border: 1px solid #7f1d1d; }
|
||||
.message-content { padding: 1rem; }
|
||||
.message-text { white-space: pre-wrap; word-break: break-word; color: #e2e8f0; }
|
||||
.message-text + .message-text { margin-top: 0.75rem; }
|
||||
.tool-block { margin-top: 0.75rem; border: 1px solid #334155; border-radius: 8px; overflow: hidden; }
|
||||
.tool-error { border-color: #7f1d1d; }
|
||||
.tool-summary {
|
||||
padding: 0.4rem 0.75rem; font-size: 0.75rem; font-weight: 500;
|
||||
background: #0f172a; cursor: pointer; color: #94a3b8;
|
||||
list-style: none; display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
.tool-summary code { font-family: monospace; color: #7dd3fc; }
|
||||
.tool-code {
|
||||
padding: 0.75rem; font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.8rem; overflow-x: auto; white-space: pre;
|
||||
background: #0a0f1a; color: #94a3b8;
|
||||
}
|
||||
.footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #1e293b; text-align: center; }
|
||||
.footer a { color: #60a5fa; text-decoration: none; font-size: 0.8rem; }
|
||||
.footer a:hover { text-decoration: underline; }
|
||||
`;
|
||||
|
||||
export function toHTML(conv: Conversation, options: ExportOptions): string {
|
||||
let messages = conv.messages;
|
||||
if (options.dateRange) {
|
||||
const { start, end } = options.dateRange;
|
||||
messages = messages.filter((m) => m.createdAt >= start && m.createdAt <= end);
|
||||
}
|
||||
|
||||
const metaParts = [
|
||||
...(conv.model ? [`<span>Model: ${escapeHtml(conv.model)}</span>`] : []),
|
||||
`<span>${messages.length} messages</span>`,
|
||||
...(options.includeTimestamps
|
||||
? [`<span>Created: ${new Date(conv.createdAt).toLocaleString()}</span>`]
|
||||
: []),
|
||||
`<span>Exported: ${new Date().toLocaleString()}</span>`,
|
||||
];
|
||||
|
||||
const messagesHtml = messages
|
||||
.map((m) => renderMessageHtml(m, options))
|
||||
.join("\n");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${escapeHtml(conv.title)} — Claude Code</title>
|
||||
<style>${CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(conv.title)}</h1>
|
||||
<div class="meta">${metaParts.join("")}</div>
|
||||
<div class="messages">${messagesHtml}</div>
|
||||
<div class="footer">
|
||||
<a href="https://claude.ai/code">Powered by Claude Code</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
45
web/lib/export/json.ts
Normal file
45
web/lib/export/json.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Conversation, Message, ExportOptions, ContentBlock } from "../types";
|
||||
|
||||
function filterContent(
|
||||
content: ContentBlock[] | string,
|
||||
options: ExportOptions
|
||||
): ContentBlock[] | string {
|
||||
if (typeof content === "string") return content;
|
||||
|
||||
return content.filter((block) => {
|
||||
if (block.type === "tool_use" || block.type === "tool_result") {
|
||||
return options.includeToolUse;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function filterMessage(msg: Message, options: ExportOptions): Message {
|
||||
return {
|
||||
...msg,
|
||||
content: filterContent(msg.content, options),
|
||||
createdAt: options.includeTimestamps ? msg.createdAt : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function toJSON(conv: Conversation, options: ExportOptions): string {
|
||||
let messages = conv.messages;
|
||||
|
||||
if (options.dateRange) {
|
||||
const { start, end } = options.dateRange;
|
||||
messages = messages.filter((m) => m.createdAt >= start && m.createdAt <= end);
|
||||
}
|
||||
|
||||
const output = {
|
||||
id: conv.id,
|
||||
title: conv.title,
|
||||
model: conv.model,
|
||||
createdAt: options.includeTimestamps ? conv.createdAt : undefined,
|
||||
updatedAt: options.includeTimestamps ? conv.updatedAt : undefined,
|
||||
messageCount: messages.length,
|
||||
messages: messages.map((m) => filterMessage(m, options)),
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return JSON.stringify(output, null, 2);
|
||||
}
|
||||
78
web/lib/export/markdown.ts
Normal file
78
web/lib/export/markdown.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Conversation, Message, ExportOptions } from "../types";
|
||||
import { extractTextContent } from "../utils";
|
||||
|
||||
function renderMessage(msg: Message, options: ExportOptions): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
const roleLabel =
|
||||
msg.role === "user"
|
||||
? "**User**"
|
||||
: msg.role === "assistant"
|
||||
? "**Assistant**"
|
||||
: `**${msg.role}**`;
|
||||
|
||||
parts.push(`### ${roleLabel}`);
|
||||
|
||||
if (options.includeTimestamps) {
|
||||
parts.push(`_${new Date(msg.createdAt).toLocaleString()}_\n`);
|
||||
}
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
parts.push(msg.content);
|
||||
} else {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push(block.text);
|
||||
} else if (block.type === "tool_use" && options.includeToolUse) {
|
||||
parts.push(
|
||||
`\`\`\`tool-use\n// Tool: ${block.name}\n${JSON.stringify(block.input, null, 2)}\n\`\`\``
|
||||
);
|
||||
} else if (block.type === "tool_result" && options.includeToolUse) {
|
||||
const raw =
|
||||
typeof block.content === "string"
|
||||
? block.content
|
||||
: extractTextContent(block.content);
|
||||
const truncated =
|
||||
!options.includeFileContents && raw.length > 500
|
||||
? raw.slice(0, 500) + "\n…[truncated]"
|
||||
: raw;
|
||||
parts.push(
|
||||
`\`\`\`tool-result${block.is_error ? " error" : ""}\n${truncated}\n\`\`\``
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
export function toMarkdown(conv: Conversation, options: ExportOptions): string {
|
||||
const lines: string[] = [
|
||||
`# ${conv.title}`,
|
||||
"",
|
||||
"---",
|
||||
...(options.includeTimestamps
|
||||
? [`**Created:** ${new Date(conv.createdAt).toISOString()}`]
|
||||
: []),
|
||||
...(conv.model ? [`**Model:** ${conv.model}`] : []),
|
||||
`**Messages:** ${conv.messages.length}`,
|
||||
"---",
|
||||
"",
|
||||
];
|
||||
|
||||
let messages = conv.messages;
|
||||
if (options.dateRange) {
|
||||
const { start, end } = options.dateRange;
|
||||
messages = messages.filter((m) => m.createdAt >= start && m.createdAt <= end);
|
||||
}
|
||||
|
||||
for (const msg of messages) {
|
||||
lines.push(renderMessage(msg, options));
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("---");
|
||||
lines.push("*Exported from Claude Code*");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
58
web/lib/export/plaintext.ts
Normal file
58
web/lib/export/plaintext.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Conversation, ExportOptions } from "../types";
|
||||
import { extractTextContent } from "../utils";
|
||||
|
||||
export function toPlainText(conv: Conversation, options: ExportOptions): string {
|
||||
const lines: string[] = [
|
||||
conv.title,
|
||||
"=".repeat(conv.title.length),
|
||||
"",
|
||||
];
|
||||
|
||||
if (conv.model) lines.push(`Model: ${conv.model}`);
|
||||
if (options.includeTimestamps) {
|
||||
lines.push(`Created: ${new Date(conv.createdAt).toLocaleString()}`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
let messages = conv.messages;
|
||||
if (options.dateRange) {
|
||||
const { start, end } = options.dateRange;
|
||||
messages = messages.filter((m) => m.createdAt >= start && m.createdAt <= end);
|
||||
}
|
||||
|
||||
for (const msg of messages) {
|
||||
const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
|
||||
lines.push(`[${role}]${options.includeTimestamps ? ` (${new Date(msg.createdAt).toLocaleString()})` : ""}`);
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
lines.push(msg.content);
|
||||
} else {
|
||||
const parts: string[] = [];
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push(block.text);
|
||||
} else if (block.type === "tool_use" && options.includeToolUse) {
|
||||
parts.push(`[Tool: ${block.name}]\nInput: ${JSON.stringify(block.input)}`);
|
||||
} else if (block.type === "tool_result" && options.includeToolUse) {
|
||||
const raw =
|
||||
typeof block.content === "string"
|
||||
? block.content
|
||||
: extractTextContent(block.content);
|
||||
const text =
|
||||
!options.includeFileContents && raw.length > 500
|
||||
? raw.slice(0, 500) + " …[truncated]"
|
||||
: raw;
|
||||
parts.push(`[Tool Result${block.is_error ? " (error)" : ""}]\n${text}`);
|
||||
}
|
||||
}
|
||||
lines.push(parts.join("\n\n"));
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("---");
|
||||
lines.push("Exported from Claude Code");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user