Spaces:
Sleeping
Sleeping
| """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 は <style> タグで包んで st.markdown(unsafe_allow_html=True) に渡す | |
| CSS_BLOCK = f"<style>\n{_load('style.css')}\n</style>" | |
| _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'<a href="\1" target="_blank">\1</a>', safe) | |
| return safe.replace("\n", "<br>") | |
| 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'<div class="avatar">{avatar_char}</div>' | |
| name = f'<div class="partner-name">{html.escape(persona_name)}</div>' | |
| else: | |
| avatar = '<div class="avatar-spacer"></div>' | |
| 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) | |