File size: 3,693 Bytes
edac567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""Scrollable + streaming chat interface."""
import streamlit as st
from typing import List, Dict
import html
import time

_CHAT_CSS = """
<style>
#chat-container {
  height: 520px;
  overflow-y: auto;
  padding: 0.5rem 0.75rem 0.25rem 0.75rem;
  border: 1px solid #e3e3e3;
  border-radius: 10px;
  background: #fafafa;
  scroll-behavior: smooth;
}
.chat-msg { margin: 0 0 14px 0; max-width: 85%; }
.chat-row-user { display:flex; justify-content:flex-end; }
.chat-row-assistant { display:flex; justify-content:flex-start; }
.bubble {
  padding:10px 14px;
  border-radius:14px;
  line-height:1.35;
  font-size:0.93rem;
  box-shadow:0 1px 2px rgba(0,0,0,0.08);
  word-wrap:break-word;
  white-space:pre-wrap;
}
.bubble-user {
  background:linear-gradient(135deg,#4b8df8,#2563eb);
  color:#fff;
  border-bottom-right-radius:4px;
}
.bubble-assistant {
  background:#ffffff;
  border:1px solid #ddd;
  border-bottom-left-radius:4px;
}
.meta {
  font-size:0.6rem;
  opacity:0.55;
  margin-top:4px;
  text-align:right;
  user-select:none;
}
</style>
<script>
function scrollChat(){
  const el = window.parent.document.querySelector('#chat-container');
  if(el){ el.scrollTop = el.scrollHeight; }
}
</script>
"""

class ChatInterface:
    """Renders scrollable chat and supports streaming assistant output."""

    def __init__(self):
        self.chat_history = []

    def render(self, chat_history: List[Dict]) -> None:
        st.markdown(_CHAT_CSS, unsafe_allow_html=True)
        if not chat_history:
            st.info("No messages yet. Ask something about the PDF.")
            return
        st.markdown(self._history_to_html(chat_history), unsafe_allow_html=True)

    def stream_assistant(self, chat_history: List[Dict], stream_iter) -> str:
        """
        Render existing messages then stream new assistant message.
        Returns final assistant text.
        """
        st.markdown(_CHAT_CSS, unsafe_allow_html=True)
        placeholder = st.empty()
        assistant_text = ""
        # Re-render on each chunk for smooth streaming
        for chunk in stream_iter:
            assistant_text += chunk
            merged = chat_history + [{"role": "assistant", "content": assistant_text}]
            placeholder.markdown(self._history_to_html(merged), unsafe_allow_html=True)
            time.sleep(0.03) 
        return assistant_text

    def input_box(self, key: str = "chat_input") -> str:
        return st.text_input(
            "Ask a question:",
            key=key,
            placeholder="Type your question and press Enter...",
            label_visibility="collapsed",
        )

    def add_message(self, role: str, content: str):
        """
        Add a message to chat history
        """
        self.chat_history.append({
            "role": role,
            "content": content
        })

    def clear_chat(self):
        """Clear the chat history"""
        self.chat_history = []

    def _history_to_html(self, history: List[Dict]) -> str:
        rows = []
        for m in history:
            role = m.get("role", "user")
            safe = html.escape(m.get("content", ""))
            row_cls = "chat-row-user" if role == "user" else "chat-row-assistant"
            bub_cls = "bubble bubble-user" if role == "user" else "bubble bubble-assistant"
            label = "You" if role == "user" else "Assistant"
            rows.append(
                f'<div class="{row_cls}"><div class="chat-msg">'
                f'<div class="{bub_cls}">{safe}</div>'
                f'<div class="meta">{label}</div>'
                f'</div></div>'
            )
        return f'<div id="chat-container">{"".join(rows)}</div><script>scrollChat();</script>'