UNPKG

ccshare

Version:

Share Claude Code prompts and results easily

485 lines (468 loc) 20.8 kB
import { escape } from 'html-escaper'; export function generateHtml(data) { const { prompts, fileDiffs, assistantActions, toolExecutions, workflow, sessionInfo, techStack } = data; return `<!DOCTYPE html> <html lang="en" class="h-full"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Claude Code Session - ${new Date().toLocaleString('en-US')}</title> <script src="https://cdn.tailwindcss.com"></script> <style> /* Custom styles for diff highlighting */ .diff-line { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } .diff-added { background-color: #065f46; color: #d1fae5; } .diff-removed { background-color: #7f1d1d; color: #fee2e2; } .diff-header { background-color: #374151; color: #9ca3af; } .diff-context { color: #9ca3af; } /* Custom scrollbar for code blocks */ .diff-content::-webkit-scrollbar { height: 8px; width: 8px; } .diff-content::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .diff-content::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .diff-content::-webkit-scrollbar-thumb:hover { background: #555; } </style> <script> function copyPrompt(index) { const promptElement = document.querySelector(\`#prompt-\${index} .prompt-content\`); const promptText = promptElement.textContent.trim(); const markdown = \`## Prompt #\${index + 1}\\n\\n\${promptText}\`; navigator.clipboard.writeText(markdown).then(() => { const btn = document.querySelector(\`#copy-prompt-\${index}\`); const originalText = btn.textContent; btn.textContent = 'Copied!'; btn.classList.add('bg-green-600'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('bg-green-600'); }, 2000); }); } function copyFileDiff(index) { const fileChange = document.querySelector(\`#file-\${index}\`); const filePath = fileChange.querySelector('.file-path').textContent.trim(); const diffContent = fileChange.querySelector('.diff-content pre').textContent.trim(); const markdown = \`#### \${filePath}\\n\\n\\\`\\\`\\\`diff\\n\${diffContent}\\n\\\`\\\`\\\`\`; navigator.clipboard.writeText(markdown).then(() => { const btn = document.querySelector(\`#copy-file-\${index}\`); const originalText = btn.textContent; btn.textContent = 'Copied!'; btn.classList.add('bg-green-600'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('bg-green-600'); }, 2000); }); } </script> </head> <body class="min-h-full bg-gray-900"> <!-- Header --> <header class="bg-gray-800 shadow-lg border-b border-gray-700"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <h1 class="text-3xl text-white"> <span class="font-bold">Claude Code</span> <span class="font-light">Session</span> </h1> <div class="text-gray-400 mt-2 text-sm"> ${new Date().toLocaleString('en-US')} </div> ${sessionInfo ? ` <div class="mt-4 text-gray-300 text-sm space-y-1"> <div>• Total ${sessionInfo.totalPrompts} prompts</div> ${sessionInfo.timeRange ? `<div>• Period: ${sessionInfo.timeRange}</div>` : ''} ${sessionInfo.sources && sessionInfo.sources.length > 0 ? `<div>• Sources: ${sessionInfo.sources.join(', ')}</div>` : ''} ${sessionInfo.projectPath ? `<div>• Project Path: ${escape(sessionInfo.projectPath)}</div>` : ''} ${sessionInfo.claudeProjectPath ? `<div>• Claude Project Path: ${escape(sessionInfo.claudeProjectPath)}</div>` : ''} </div> ` : ''} </div> </header> <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <!-- Tech Stack --> ${techStack && (techStack.languages.length > 0 || techStack.frameworks.length > 0 || techStack.tools.length > 0 || techStack.databases.length > 0) ? ` <div class="bg-gray-800 rounded-lg shadow-sm p-6 mb-8 border border-gray-700"> <h3 class="text-lg font-semibold text-gray-200 mb-4"> Tech Stack </h3> <div class="flex flex-wrap gap-2"> ${techStack.languages.map(lang => ` <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-900 text-blue-300 border border-blue-700"> ${escape(lang)} </span> `).join('')} ${techStack.frameworks.map(fw => ` <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-900 text-red-300 border border-red-700"> ${escape(fw)} </span> `).join('')} ${techStack.tools.map(tool => ` <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-900 text-green-300 border border-green-700"> ${escape(tool)} </span> `).join('')} ${techStack.databases.map(db => ` <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-yellow-900 text-yellow-300 border border-yellow-700"> ${escape(db)} </span> `).join('')} </div> </div> ` : ''} <!-- Prompts Section --> <div class="bg-gray-800 rounded-lg shadow-sm p-6 mb-8 border border-gray-700"> <h2 class="text-2xl font-semibold text-gray-200 mb-6 pb-4 border-b border-gray-700"> Prompts (${prompts.length}) </h2> ${prompts.length > 0 ? ` <div class="space-y-6"> ${prompts.map((item, index) => ` <div id="prompt-${index}" class="border-l-4 border-orange-500 pl-6"> <div class="bg-gray-700 rounded-r-lg p-4"> <div class="flex justify-between items-start mb-2"> <div class="font-semibold text-orange-400">Prompt #${index + 1}</div> <button id="copy-prompt-${index}" onclick="copyPrompt(${index})" class="px-3 py-1 text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 rounded-md transition-colors" > Copy </button> </div> <div class="prompt-content text-gray-200 whitespace-pre-wrap">${escape(item.prompt)}</div> ${item.timestamp || item.sourceFile ? ` <div class="mt-3 flex flex-wrap gap-4 text-sm text-gray-400"> ${item.timestamp ? ` <span class="flex items-center"> ${new Date(item.timestamp).toLocaleString('en-US')} </span> ` : ''} ${item.sourceFile ? ` <span class="flex items-center"> ${escape(item.sourceFile)} </span> ` : ''} </div> ` : ''} </div> </div> `).join('')} </div> ` : ` <div class="text-center text-gray-400 italic py-8"> No prompts found </div> `} </div> <!-- File Changes Section --> ${fileDiffs && fileDiffs.length > 0 ? ` <div class="bg-gray-800 rounded-lg shadow-sm p-6 border border-gray-700"> <h2 class="text-2xl font-semibold text-gray-200 mb-6 pb-4 border-b border-gray-700"> File Changes (${fileDiffs.length}) </h2> <div class="space-y-4"> ${fileDiffs.map((file, index) => { const parsedLines = parseDiff(file.diff); return ` <div id="file-${index}" class="file-change border border-gray-600 rounded-lg overflow-hidden"> <div class="file-path bg-gray-900 text-gray-200 px-4 py-2 text-sm font-mono flex justify-between items-center"> <span>${escape(file.path)}</span> <button id="copy-file-${index}" onclick="copyFileDiff(${index})" class="px-2 py-1 text-xs font-medium text-white bg-gray-700 hover:bg-gray-600 rounded transition-colors" > Copy </button> </div> <div class="diff-content bg-gray-800 overflow-x-auto max-h-96"> <pre class="p-4 text-sm">${parsedLines.map(line => { let className = 'diff-line block px-2 '; if (line.type === 'added') className += 'diff-added'; else if (line.type === 'removed') className += 'diff-removed'; else if (line.type === 'header') className += 'diff-header font-semibold py-1'; else className += 'diff-context'; return `<span class="${className}">${escape(line.content)}</span>`; }).join('')}</pre> </div> </div> `; }).join('')} </div> </div> ` : ''} <!-- Workflow Section --> ${workflow && workflow.length > 0 ? ` <div class="bg-gray-800 rounded-lg shadow-sm p-6 mb-8 border border-gray-700"> <h2 class="text-2xl font-semibold text-gray-200 mb-6 pb-4 border-b border-gray-700"> Workflow (${workflow.length}) </h2> <div class="space-y-4"> ${workflow.map((item, index) => { if (item.type === 'assistant_action') { let icon = '📝'; let colorClass = 'text-gray-400'; switch (item.actionType) { case 'explanation': icon = '💡'; colorClass = 'text-blue-400'; break; case 'analysis': icon = '🔍'; colorClass = 'text-purple-400'; break; case 'code_change': icon = '✏️'; colorClass = 'text-green-400'; break; case 'file_read': icon = '📖'; colorClass = 'text-yellow-400'; break; case 'command_execution': icon = '⚡'; colorClass = 'text-orange-400'; break; } return ` <div class="flex items-start space-x-3 p-3 rounded-lg bg-gray-700/50"> <span class="text-2xl flex-shrink-0">${icon}</span> <div class="flex-1"> <div class="${colorClass} font-medium capitalize">${item.actionType?.replace('_', ' ') || 'Action'}</div> <div class="text-gray-300 text-sm mt-1">${escape(item.description || '')}</div> <div class="text-gray-500 text-xs mt-1">${new Date(item.timestamp).toLocaleTimeString()}</div> </div> </div> `; } else if (item.type === 'tool_execution' || item.type === 'tool_result') { let icon = '⚙️'; let colorClass = 'text-gray-400'; switch (item.tool) { case 'Bash': icon = '⚡'; colorClass = 'text-yellow-400'; break; case 'Edit': case 'MultiEdit': icon = '✏️'; colorClass = 'text-blue-400'; break; case 'Read': icon = '📖'; colorClass = 'text-green-400'; break; case 'Write': icon = '📝'; colorClass = 'text-purple-400'; break; case 'TodoWrite': icon = '✅'; colorClass = 'text-orange-400'; break; } return ` <div class="border border-gray-600 rounded-lg overflow-hidden"> <div class="bg-gray-900 px-4 py-3 flex items-center justify-between"> <div class="flex items-center space-x-3"> <span class="text-2xl">${icon}</span> <span class="${colorClass} font-mono">${item.tool}</span> <span class="text-gray-500 text-sm">${new Date(item.timestamp).toLocaleTimeString()}</span> ${item.type === 'tool_result' ? '<span class="text-xs text-gray-400 ml-2">[Result]</span>' : ''} </div> ${item.status ? ` <span class="text-xs px-2 py-1 rounded ${item.status === 'success' ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'}"> ${item.status} </span> ` : ''} </div> ${item.type === 'tool_execution' && item.parameters ? ` <div class="bg-gray-800 px-4 py-2 border-t border-gray-700"> <div class="text-gray-400 text-sm font-mono"> ${item.tool === 'Bash' && item.parameters.command ? `$ ${escape(item.parameters.command)}` : item.tool === 'Edit' && item.parameters.file_path ? `File: ${escape(item.parameters.file_path)}` : item.tool === 'Read' && item.parameters.file_path ? `File: ${escape(item.parameters.file_path)}` : JSON.stringify(item.parameters, null, 2)} </div> </div> ` : ''} ${item.type === 'tool_result' && item.result ? ` <div class="bg-gray-700 px-4 py-3 border-t border-gray-600 max-h-48 overflow-y-auto"> <pre class="text-gray-300 text-sm whitespace-pre-wrap">${escape(item.result.substring(0, 1000))}${item.result.length > 1000 ? '\n...' : ''}</pre> </div> ` : ''} </div> `; } }).join('')} </div> </div> ` : (assistantActions && assistantActions.length > 0) || (toolExecutions && toolExecutions.length > 0) ? ` <!-- Assistant Actions Section --> ${assistantActions && assistantActions.length > 0 ? ` <div class="bg-gray-800 rounded-lg shadow-sm p-6 mb-8 border border-gray-700"> <h2 class="text-2xl font-semibold text-gray-200 mb-6 pb-4 border-b border-gray-700"> Assistant Actions (${assistantActions.length}) </h2> <div class="space-y-3"> ${assistantActions.map((action, index) => { let icon = '📝'; let colorClass = 'text-gray-400'; switch (action.type) { case 'explanation': icon = '💡'; colorClass = 'text-blue-400'; break; case 'analysis': icon = '🔍'; colorClass = 'text-purple-400'; break; case 'code_change': icon = '✏️'; colorClass = 'text-green-400'; break; case 'file_read': icon = '📖'; colorClass = 'text-yellow-400'; break; case 'command_execution': icon = '⚡'; colorClass = 'text-orange-400'; break; } return ` <div class="flex items-start space-x-3 p-3 rounded-lg bg-gray-700/50"> <span class="text-2xl flex-shrink-0">${icon}</span> <div class="flex-1"> <div class="${colorClass} font-medium capitalize">${action.type.replace('_', ' ')}</div> <div class="text-gray-300 text-sm mt-1">${escape(action.description)}</div> <div class="text-gray-500 text-xs mt-1">${new Date(action.timestamp).toLocaleTimeString()}</div> </div> </div> `; }).join('')} </div> </div> ` : ''} <!-- Tool Executions Section --> ${toolExecutions && toolExecutions.length > 0 ? ` <div class="bg-gray-800 rounded-lg shadow-sm p-6 mb-8 border border-gray-700"> <h2 class="text-2xl font-semibold text-gray-200 mb-6 pb-4 border-b border-gray-700"> Tool Executions (${toolExecutions.length}) </h2> <div class="space-y-4"> ${toolExecutions.map((exec, index) => { let icon = '⚙️'; let colorClass = 'text-gray-400'; switch (exec.tool) { case 'Bash': icon = '⚡'; colorClass = 'text-yellow-400'; break; case 'Edit': case 'MultiEdit': icon = '✏️'; colorClass = 'text-blue-400'; break; case 'Read': icon = '📖'; colorClass = 'text-green-400'; break; case 'Write': icon = '📝'; colorClass = 'text-purple-400'; break; case 'TodoWrite': icon = '✅'; colorClass = 'text-orange-400'; break; } return ` <div class="border border-gray-600 rounded-lg overflow-hidden"> <div class="bg-gray-900 px-4 py-3 flex items-center justify-between"> <div class="flex items-center space-x-3"> <span class="text-2xl">${icon}</span> <span class="${colorClass} font-mono">${exec.tool}</span> <span class="text-gray-500 text-sm">${new Date(exec.timestamp).toLocaleTimeString()}</span> </div> ${exec.status ? ` <span class="text-xs px-2 py-1 rounded ${exec.status === 'success' ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'}"> ${exec.status} </span> ` : ''} </div> ${exec.parameters ? ` <div class="bg-gray-800 px-4 py-2 border-t border-gray-700"> <div class="text-gray-400 text-sm font-mono"> ${exec.tool === 'Bash' && exec.parameters.command ? `$ ${escape(exec.parameters.command)}` : exec.tool === 'Edit' && exec.parameters.file_path ? `File: ${escape(exec.parameters.file_path)}` : exec.tool === 'Read' && exec.parameters.file_path ? `File: ${escape(exec.parameters.file_path)}` : JSON.stringify(exec.parameters, null, 2)} </div> </div> ` : ''} ${exec.result ? ` <div class="bg-gray-700 px-4 py-3 border-t border-gray-600 max-h-48 overflow-y-auto"> <pre class="text-gray-300 text-sm whitespace-pre-wrap">${escape(exec.result.substring(0, 1000))}${exec.result.length > 1000 ? '\n...' : ''}</pre> </div> ` : ''} </div> `; }).join('')} </div> </div> ` : ''} ` : ''} </main> <!-- Footer --> <footer class="mt-12 pb-8"> <div class="text-center text-gray-400 text-sm"> Generated by <a href="https://ccshare.cc" target="_blank" class="text-orange-400 hover:text-orange-500 underline">ccshare</a> </div> </footer> </body> </html>`; } function parseDiff(diff) { const lines = diff.split('\n'); const result = []; for (const line of lines) { if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('diff') || line.startsWith('index')) { result.push({ type: 'header', content: line }); } else if (line.startsWith('@@')) { result.push({ type: 'header', content: line }); } else if (line.startsWith('+')) { result.push({ type: 'added', content: line }); } else if (line.startsWith('-')) { result.push({ type: 'removed', content: line }); } else { result.push({ type: 'context', content: line }); } } return result; } //# sourceMappingURL=html-generator.js.map