"""LINE 風 UI: static/ 以下の CSS と HTML テンプレートを読み込んで描画.""" from __future__ import annotations import html import re from pathlib import Path from string import Template STATIC_DIR = Path(__file__).resolve().parent.parent/ "static" def _load(name: str) -> str: return (STATIC_DIR / name).read_text(encoding="utf-8") # CSS は " _TPL_HEADER = Template(_load("header.html")) _TPL_USER = Template(_load("message_user.html")) _TPL_ASSISTANT = Template(_load("message_assistant.html")) _TPL_TYPING = Template(_load("typing.html")) _TPL_NOTICE = Template(_load("notice.html")) _URL_RE = re.compile(r"(https?://[^\s<]+)") def _format_text(text: str) -> str: safe = html.escape(text) safe = _URL_RE.sub(r'\1', safe) return safe.replace("\n", "
") def header_html(persona_name: str) -> str: return _TPL_HEADER.substitute(name=html.escape(persona_name or "彼")) def message_html( role: str, content: str, persona_name: str, show_name: bool ) -> str: body = _format_text(content) if role == "user": return _TPL_USER.substitute(body=body) avatar_char = html.escape((persona_name or "♡")[:1]) if show_name: avatar = f'
{avatar_char}
' name = f'
{html.escape(persona_name)}
' else: avatar = '
' name = "" return _TPL_ASSISTANT.substitute(avatar=avatar, name=name, body=body) def typing_html(persona_name: str) -> str: avatar_char = html.escape((persona_name or "♡")[:1]) return _TPL_TYPING.substitute(avatar_char=avatar_char) def notice_html(text: str) -> str: return _TPL_NOTICE.substitute(text=html.escape(text)) def chat_html(messages, persona_name: str, max_display: int = 50) -> str: """直近 max_display 件のみ描画 (古いものは履歴要約に圧縮済み).""" parts: list[str] = [] prev_role = None visible = messages[-max_display:] for m in visible: show_name = m["role"] == "assistant" and prev_role != "assistant" parts.append(message_html(m["role"], m["content"], persona_name, show_name)) prev_role = m["role"] return "".join(parts)