-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
255 lines (210 loc) · 8.8 KB
/
Copy pathapp.py
File metadata and controls
255 lines (210 loc) · 8.8 KB
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""
PaderBot — Streamlit UI v4.
"""
import json
import streamlit as st
import streamlit.components.v1 as components
from paderbot import PaderBot
st.set_page_config(
page_title="PaderBot — Q&A for International Students",
page_icon="🎓",
layout="wide",
)
@st.cache_resource(show_spinner="Loading PaderBot (first request takes ~10s)...")
def get_bot():
return PaderBot()
EXAMPLES_EN = [
"What is BAföG and can international students get it?",
"How do I apply to the Computer Science Master's program?",
"Where can students live in Paderborn?",
"Do I need to know German to study at Paderborn?",
]
EXAMPLES_DE = [
"Wie bewerbe ich mich für einen Masterstudiengang?",
"Welche Englisch-sprachigen Master-Programme gibt es?",
"Wo kann ich in Paderborn wohnen?",
"Was kostet ein Semester an der Universität Paderborn?",
]
# Session state
if "history" not in st.session_state:
st.session_state.history = []
if "pending_question" not in st.session_state:
st.session_state.pending_question = None
if "edit_text" not in st.session_state:
st.session_state.edit_text = None
if "input_key_id" not in st.session_state:
st.session_state.input_key_id = 0
def submit_question(q: str):
st.session_state.pending_question = q.strip()
def edit_last_question():
if not st.session_state.history:
return
last = st.session_state.history.pop()
st.session_state.edit_text = last["question"]
st.session_state.input_key_id += 1
def copy_to_clipboard_button(text: str, button_label: str = "📋 Copy answer"):
"""Theme-adaptive copy button. Uses Streamlit's CSS variables so colors
work on both light and dark themes."""
safe_text = json.dumps(text)
html = f"""
<button onclick='navigator.clipboard.writeText({safe_text}).then(
() => {{
const original = this.innerText;
this.innerText = "✓ Copied";
setTimeout(() => this.innerText = original, 1500);
}}
)'
style="
background: var(--secondary-background-color, #f0f2f6);
color: var(--text-color, #262730);
border: 1px solid var(--text-color, rgba(0,0,0,0.2));
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
font-family: inherit;
"
onmouseover="this.style.opacity='0.85'"
onmouseout="this.style.opacity='1'">
{button_label}
</button>
"""
components.html(html, height=42)
# ============================================================
# Top bar — title + help (right)
# ============================================================
header_col1, header_col2 = st.columns([6, 1], vertical_alignment="center")
with header_col1:
st.title("Ask PaderBot")
with header_col2:
with st.popover("❓ Help", use_container_width=True):
st.markdown("**How to use PaderBot**")
st.markdown(
"- 👈 Click any example question in the sidebar, or type your own.\n"
"- 🌐 Answers in English or German — bot replies in the language of your question.\n"
"- 📚 Each answer is grounded in scraped sources, with clickable citations.\n\n"
"**Working with past answers**\n"
"- ✏️ Click **Edit** on the latest question to refine and re-ask.\n"
"- 📋 Click **Copy answer** to copy text for use elsewhere.\n"
"- 🖱️ Select text in any answer with your mouse to copy part of it.\n\n"
"**Note on follow-ups**\n"
"Each question is answered independently — follow-ups should reference "
"their topic explicitly. Instead of *\"How long does it take?\"*, ask "
"*\"How long does the Computer Science Master's program take?\"*"
)
st.caption(
"Antworten auf Englisch oder Deutsch. Bot antwortet in der Sprache der Frage. "
"· Answers in English or German. Bot replies in the language of your question."
)
# Sidebar
with st.sidebar:
st.title("🎓 PaderBot")
st.caption("A Q&A assistant for prospective international students at Paderborn University.")
st.divider()
st.subheader("What I can help with")
st.markdown(
"- **Application & admissions** to Master's programs\n"
"- **Visa, language requirements, fees**\n"
"- **Housing** via Studierendenwerk Paderborn\n"
"- **Financing** (BAföG, scholarships)\n"
"- **Master's program details**\n"
)
st.divider()
st.subheader("💡 Example questions")
st.caption("Click any to ask immediately")
st.caption("English")
for ex in EXAMPLES_EN:
st.button(ex, key=f"en_{ex}", on_click=submit_question, args=(ex,), use_container_width=True)
st.caption("Deutsch")
for ex in EXAMPLES_DE:
st.button(ex, key=f"de_{ex}", on_click=submit_question, args=(ex,), use_container_width=True)
if st.session_state.history:
st.divider()
if st.button("🗑️ Clear session", use_container_width=True):
st.session_state.history = []
st.session_state.pending_question = None
st.rerun()
st.divider()
st.caption(
"**About.** Built as a learning project. "
"PaderBot is grounded in ~94 scraped pages from Paderborn University and Studierendenwerk Paderborn. "
"Each question is answered independently to ensure citation accuracy. "
"Always verify critical decisions against official sources."
)
st.caption("Source: [GitHub](#)")
# Input
default_value = st.session_state.edit_text or ""
st.session_state.edit_text = None
input_key = f"input_text_{st.session_state.input_key_id}"
# Use a form so the input only submits on explicit Enter or button click,
# NOT on focus loss (which was causing half-typed questions to fire).
with st.form(key=f"ask_form_{st.session_state.input_key_id}", clear_on_submit=True):
user_input = st.text_input(
"Your question / Deine Frage",
value=default_value,
placeholder="e.g., How do I apply to a Master's program? / Wie bewerbe ich mich?",
key=input_key,
)
submitted = st.form_submit_button("Ask / Fragen", type="primary", use_container_width=False)
if submitted and user_input.strip():
submit_question(user_input.strip())
st.session_state.input_key_id += 1
st.rerun()
# Process pending question
pending = st.session_state.pending_question
if pending:
bot = get_bot()
with st.spinner(f'Answering: "{pending[:60]}..."'):
result = bot.query(pending)
st.session_state.history.append({"question": pending, "result": result})
st.session_state.pending_question = None
# Render single Q&A
def render_qa(question: str, result: dict, is_latest: bool):
header_cols = st.columns([10, 1])
with header_cols[0]:
st.markdown(f"### 🙋 {question}")
with header_cols[1]:
if is_latest:
st.button(
"✏️ Edit",
key=f"edit_{len(st.session_state.history)}",
on_click=edit_last_question,
help="Edit and re-ask this question",
)
if result["refused"]:
# Refusal: show the message centered, NO sources panel, NO copy button
st.info(result["answer"])
st.caption(
"PaderBot only answers questions grounded in its scraped sources. "
"Try rephrasing, or ask about applications, housing, programs, or financing."
)
else:
# Real answer: two-column layout with answer + sources
col_answer, col_sources = st.columns([3, 2])
with col_answer:
st.markdown(result["answer"])
if result["retrieval_query"] != question:
with st.expander("🔍 Search query used internally"):
st.code(result["retrieval_query"], language=None)
with col_sources:
st.markdown("**Sources**")
for s in result["sources"]:
lang_badge = "🇬🇧" if s["language"] == "en" else "🇩🇪"
title = s["title"][:75]
st.markdown(f"**[{s['rank']}]** {lang_badge} [{title}]({s['url']})")
with st.expander("📄 View retrieved context"):
for c in result["contexts"]:
preview = c["text"][:500]
more = "..." if len(c["text"]) > 500 else ""
st.markdown(f"**[{c['rank']}]** *{c['title'][:80]}*")
st.text(preview + more)
st.divider()
st.divider()
# Render history
if st.session_state.history:
for idx, entry in enumerate(reversed(st.session_state.history)):
render_qa(entry["question"], entry["result"], is_latest=(idx == 0))
else:
# Minimal empty state — just a pointer. Tips are in the ❓ Help popover.
st.info("👈 Click an example question in the sidebar, or type your own above.")