Skip to content

Instantly share code, notes, and snippets.

@numtel
Created July 1, 2025 19:21
Show Gist options
  • Select an option

  • Save numtel/0063cd0811620f522850d1ed4ca3b7e2 to your computer and use it in GitHub Desktop.

Select an option

Save numtel/0063cd0811620f522850d1ed4ca3b7e2 to your computer and use it in GitHub Desktop.
Convert gemini chat log json from ai studio into simple html
#!/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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
/**
* 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