Created
July 1, 2025 19:21
-
-
Save numtel/0063cd0811620f522850d1ed4ca3b7e2 to your computer and use it in GitHub Desktop.
Convert gemini chat log json from ai studio into simple html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * A Node.js script to convert Gemini chat log JSON from stdin to a clean HTML file on stdout. | |
| * | |
| * This script is designed to produce a readable HTML document without any external | |
| * or embedded CSS, relying on semantic HTML tags like <fieldset>, <legend>, | |
| * <details>, and <blockquote> for styling. | |
| * | |
| * Usage: | |
| * cat your_chat_log.json | node gemini-log-to-html.js > chat.html | |
| */ | |
| /** | |
| * Reads all data from stdin and returns it as a single string. | |
| * @returns {Promise<string>} | |
| */ | |
| function readStdin() { | |
| return new Promise((resolve, reject) => { | |
| let data = ''; | |
| process.stdin.setEncoding('utf8'); | |
| process.stdin.on('readable', () => { | |
| let chunk; | |
| while ((chunk = process.stdin.read()) !== null) { | |
| data += chunk; | |
| } | |
| }); | |
| process.stdin.on('end', () => { | |
| resolve(data); | |
| }); | |
| process.stdin.on('error', reject); | |
| }); | |
| } | |
| /** | |
| * A simple helper to escape characters that have special meaning in HTML. | |
| * @param {string} text - The raw text to escape. | |
| * @returns {string} The escaped HTML string. | |
| */ | |
| function escapeHtml(text) { | |
| if (typeof text !== 'string') return ''; | |
| return text | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| /** | |
| * Converts a simple Markdown-like text into HTML. | |
| * Supports headings (###), bold (**), and lists (* or 1.). | |
| * @param {string} rawText - The raw text from the model response. | |
| * @returns {string} An HTML representation of the text. | |
| */ | |
| function markdownToHtml(rawText) { | |
| // This function composes an HTML string. It's responsible for escaping raw content | |
| // before wrapping it in HTML tags to prevent rendering issues. | |
| const processInline = (text) => { | |
| return escapeHtml(text).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); | |
| }; | |
| const blocks = rawText.trim().split(/\n\n+/); | |
| const htmlBlocks = blocks.map(block => { | |
| // Headings (e.g., #, ##, ###) | |
| if (block.startsWith('# ')) return `<h1>${processInline(block.substring(2))}</h1>`; | |
| if (block.startsWith('## ')) return `<h2>${processInline(block.substring(3))}</h2>`; | |
| if (block.startsWith('### ')) return `<h3>${processInline(block.substring(4))}</h3>`; | |
| // Lists (unordered or ordered) | |
| if (block.match(/^\s*(\*|\d+\.)/)) { | |
| const listType = block.trim().startsWith('*') ? 'ul' : 'ol'; | |
| const items = block.trim().split('\n').map(item => | |
| `<li>${processInline(item.trim().replace(/^(\*|\d+\.)\s*/, ''))}</li>` | |
| ).join('\n'); | |
| return `<${listType}>\n${items}\n</${listType}>`; | |
| } | |
| // Default to a paragraph. | |
| // We split by newline, process inline formatting for each line, then join with <br />. | |
| const pLines = block.split('\n').map(line => processInline(line)); | |
| return `<p>${pLines.join('<br />')}</p>`; | |
| }); | |
| return htmlBlocks.join('\n\n'); | |
| } | |
| /** | |
| * Main function to execute the script. | |
| */ | |
| async function main() { | |
| try { | |
| const jsonString = await readStdin(); | |
| if (!jsonString) { | |
| console.error("Error: No data received from stdin."); | |
| process.exit(1); | |
| } | |
| const data = JSON.parse(jsonString); | |
| // --- Start HTML Document --- | |
| console.log('<!DOCTYPE html>'); | |
| console.log('<html lang="en">'); | |
| console.log('<head>'); | |
| console.log(' <meta charset="UTF-8">'); | |
| console.log(' <meta name="viewport" content="width=device-width, initial-scale=1.0">'); | |
| console.log(' <title>Gemini Chat Log</title>'); | |
| console.log('</head>'); | |
| console.log('<body style="font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6;">'); | |
| console.log('<h1>Gemini Chat Log</h1>'); | |
| console.log('<hr />'); | |
| const chunks = data?.chunkedPrompt?.chunks; | |
| if (chunks && chunks.length > 0) { | |
| for (const chunk of chunks) { | |
| // Skip empty or placeholder chunks | |
| if (!chunk.text && !chunk.isThought) continue; | |
| if (chunk.role === 'user') { | |
| console.log('<fieldset class="user">'); | |
| console.log(' <legend><strong>User</strong></legend>'); | |
| // Use a blockquote with a <pre> tag to preserve user's prompt formatting. | |
| console.log(` <blockquote><pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(chunk.text)}</pre></blockquote>`); | |
| console.log('</fieldset>'); | |
| } else if (chunk.role === 'model') { | |
| if (chunk.isThought) { | |
| // Use a <details> tag for collapsible internal thoughts. | |
| console.log('<details>'); | |
| console.log(' <summary><i>Model\'s Internal Thoughts (click to expand)</i></summary>'); | |
| console.log(` <pre style="white-space: pre-wrap; padding: 1em;">${escapeHtml(chunk.text)}</pre>`); | |
| console.log('</details>'); | |
| } else { | |
| console.log('<fieldset class="gemini">'); | |
| console.log(' <legend><strong>Gemini</strong></legend>'); | |
| const modelHtml = markdownToHtml(chunk.text); | |
| console.log(modelHtml); | |
| console.log('</fieldset>'); | |
| } | |
| } | |
| console.log('<hr />'); | |
| } | |
| } else { | |
| console.log('<p>No chat chunks found in the provided JSON.</p>'); | |
| } | |
| // --- End HTML Document --- | |
| console.log('</body>'); | |
| console.log('</html>'); | |
| } catch (error) { | |
| console.error("Fatal Error: Could not process JSON from stdin.", error); | |
| process.exit(1); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment