167 lines
4.4 KiB
Python
Executable File
167 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Chat message formatter — converts JSON chat data to Markdown or HTML.
|
|
|
|
Usage:
|
|
python chat_formatter.py -f html chat1.json chat2.json -o output.html
|
|
python chat_formatter.py -f markdown chat1.json chat2.json -o output.md
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from jinja2 import FileSystemLoader, Environment
|
|
|
|
template_dir = Path(__file__).parent / "templates"
|
|
file_loader = FileSystemLoader(template_dir)
|
|
env = Environment(loader=file_loader)
|
|
HTML_TEMPLATE = env.get_template("chat.html")
|
|
|
|
|
|
def load_messages(filepaths: list[str]):
|
|
"""Load and merge messages from one or more JSON files."""
|
|
all_messages = []
|
|
for fp in filepaths:
|
|
path = Path(fp)
|
|
if not path.exists():
|
|
click.echo(f"Warning: {fp} not found, skipping.", err=True)
|
|
continue
|
|
try:
|
|
with path.open("r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError as exc:
|
|
click.echo(f"Error parsing {fp}: {exc}", err=True)
|
|
continue
|
|
if not isinstance(data, list):
|
|
click.echo(f"Warning: {fp} is not a JSON array, skipping.", err=True)
|
|
continue
|
|
all_messages.extend(data)
|
|
return all_messages
|
|
|
|
|
|
def normalize_images(images):
|
|
"""Ensure each image entry is a dict with a 'url' key."""
|
|
result = []
|
|
if not images:
|
|
return result
|
|
for img in images:
|
|
if isinstance(img, str):
|
|
result.append({"url": img})
|
|
elif isinstance(img, dict):
|
|
url = img.get("url") or img.get("src") or ""
|
|
if url:
|
|
result.append({"url": url})
|
|
return result
|
|
|
|
|
|
def escape_html(text):
|
|
return (
|
|
text.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """)
|
|
.replace("'", "'")
|
|
)
|
|
|
|
|
|
def prepare_messages(messages):
|
|
"""Pre-process messages for template rendering."""
|
|
prepared = []
|
|
for msg in messages:
|
|
prepared.append(
|
|
{
|
|
"role": msg.get("role", "unknown"),
|
|
"content_esc": escape_html(msg.get("content", "")),
|
|
"images": normalize_images(msg.get("images", [])),
|
|
}
|
|
)
|
|
return prepared
|
|
|
|
|
|
def format_markdown(messages):
|
|
"""Render messages as Markdown text."""
|
|
lines = []
|
|
for msg in messages:
|
|
role = msg.get("role", "unknown")
|
|
content = msg.get("content", "")
|
|
images = msg.get("images", [])
|
|
|
|
role_label = "👤 User" if role == "user" else "🤖 Assistant"
|
|
lines.append(f"**{role_label}**\n")
|
|
|
|
if content:
|
|
lines.append(content)
|
|
lines.append("")
|
|
|
|
for i, img in enumerate(images):
|
|
if isinstance(img, dict):
|
|
url = img.get("url") or img.get("src") or ""
|
|
else:
|
|
url = str(img)
|
|
if url:
|
|
lines.append(f"")
|
|
if images:
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def format_html(messages, title="Chat History"):
|
|
"""Render messages as a standalone HTML document via Jinja2."""
|
|
prepared = prepare_messages(messages)
|
|
return HTML_TEMPLATE.render(title=title, messages=prepared)
|
|
|
|
|
|
@click.command()
|
|
@click.argument("files", nargs=-1, type=click.Path())
|
|
@click.option(
|
|
"-f",
|
|
"--format",
|
|
type=click.Choice(["html", "markdown"], case_sensitive=False),
|
|
default="markdown",
|
|
show_default=True,
|
|
help="Output format.",
|
|
)
|
|
@click.option(
|
|
"-o",
|
|
"--output",
|
|
type=click.Path(),
|
|
default=None,
|
|
help="Output file path. Defaults to stdout.",
|
|
)
|
|
@click.option(
|
|
"--title",
|
|
default="Chat History",
|
|
show_default=True,
|
|
help="Title for the HTML document.",
|
|
)
|
|
def main(files, format, output, title):
|
|
"""Format JSON chat logs as Markdown or HTML."""
|
|
if not files:
|
|
click.echo("Error: no input files provided.", err=True)
|
|
sys.exit(1)
|
|
|
|
messages = load_messages(files)
|
|
if not messages:
|
|
click.echo("Error: no valid messages loaded.", err=True)
|
|
sys.exit(1)
|
|
|
|
fmt = format.lower()
|
|
|
|
if fmt == "html":
|
|
result = format_html(messages, title=title)
|
|
else:
|
|
result = format_markdown(messages)
|
|
|
|
if output:
|
|
_ = Path(output).write_text(result, encoding="utf-8")
|
|
click.echo(f"Wrote {len(messages)} messages to {output}")
|
|
else:
|
|
click.echo(result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|