File size: 2,395 Bytes
6cd1906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
"""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)