neelsoumya commited on
Commit
0518e18
Β·
verified Β·
1 Parent(s): 7d853f2

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +482 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,484 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import time
3
+ import random
4
+ from datetime import datetime
5
+ from collections import Counter
6
+ import re
7
 
8
+ # ── Page config ────────────────────────────────────────────────────────────────
9
+ st.set_page_config(
10
+ page_title="NanoChat Β· LLM Playground",
11
+ page_icon="⚑",
12
+ layout="wide",
13
+ initial_sidebar_state="expanded",
14
+ )
15
+
16
+ # ── Custom CSS ─────────────────────────────────────────────────────────────────
17
+ st.markdown("""
18
+ <style>
19
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
20
+
21
+ html, body, [class*="css"] {
22
+ font-family: 'Syne', sans-serif;
23
+ }
24
+
25
+ /* Dark industrial background */
26
+ .stApp {
27
+ background: #0d0d0f;
28
+ color: #e8e4dc;
29
+ }
30
+
31
+ /* Sidebar */
32
+ section[data-testid="stSidebar"] {
33
+ background: #111114 !important;
34
+ border-right: 1px solid #2a2a30;
35
+ }
36
+
37
+ /* Headers */
38
+ h1, h2, h3 {
39
+ font-family: 'Syne', sans-serif !important;
40
+ font-weight: 800 !important;
41
+ letter-spacing: -0.03em;
42
+ }
43
+
44
+ /* Chat messages */
45
+ .chat-msg {
46
+ padding: 14px 18px;
47
+ border-radius: 4px;
48
+ margin: 8px 0;
49
+ font-family: 'Space Mono', monospace;
50
+ font-size: 0.85rem;
51
+ line-height: 1.7;
52
+ border-left: 3px solid transparent;
53
+ }
54
+ .chat-msg.user {
55
+ background: #1a1a1f;
56
+ border-left-color: #f0c040;
57
+ color: #e8e4dc;
58
+ }
59
+ .chat-msg.assistant {
60
+ background: #141418;
61
+ border-left-color: #4af0a0;
62
+ color: #c8f0dc;
63
+ }
64
+ .chat-msg .role-label {
65
+ font-size: 0.65rem;
66
+ letter-spacing: 0.15em;
67
+ text-transform: uppercase;
68
+ opacity: 0.5;
69
+ margin-bottom: 6px;
70
+ }
71
+
72
+ /* Metric cards */
73
+ .metric-card {
74
+ background: #111114;
75
+ border: 1px solid #2a2a30;
76
+ border-radius: 4px;
77
+ padding: 16px 20px;
78
+ margin: 6px 0;
79
+ }
80
+ .metric-value {
81
+ font-family: 'Space Mono', monospace;
82
+ font-size: 2rem;
83
+ font-weight: 700;
84
+ color: #f0c040;
85
+ line-height: 1;
86
+ }
87
+ .metric-label {
88
+ font-size: 0.7rem;
89
+ letter-spacing: 0.12em;
90
+ text-transform: uppercase;
91
+ opacity: 0.45;
92
+ margin-top: 4px;
93
+ }
94
+
95
+ /* Input box override */
96
+ .stTextInput > div > div > input, .stTextArea textarea {
97
+ background: #111114 !important;
98
+ color: #e8e4dc !important;
99
+ border: 1px solid #2a2a30 !important;
100
+ font-family: 'Space Mono', monospace !important;
101
+ font-size: 0.85rem !important;
102
+ }
103
+ .stTextInput > div > div > input:focus, .stTextArea textarea:focus {
104
+ border-color: #f0c040 !important;
105
+ box-shadow: 0 0 0 2px rgba(240,192,64,0.15) !important;
106
+ }
107
+
108
+ /* Buttons */
109
+ .stButton > button {
110
+ background: #f0c040 !important;
111
+ color: #0d0d0f !important;
112
+ font-family: 'Syne', sans-serif !important;
113
+ font-weight: 700 !important;
114
+ font-size: 0.8rem !important;
115
+ letter-spacing: 0.08em !important;
116
+ text-transform: uppercase !important;
117
+ border: none !important;
118
+ border-radius: 2px !important;
119
+ padding: 10px 24px !important;
120
+ }
121
+ .stButton > button:hover {
122
+ background: #ffd760 !important;
123
+ }
124
+
125
+ /* Selectbox */
126
+ .stSelectbox > div > div {
127
+ background: #111114 !important;
128
+ border: 1px solid #2a2a30 !important;
129
+ color: #e8e4dc !important;
130
+ }
131
+
132
+ /* Tabs */
133
+ .stTabs [data-baseweb="tab-list"] {
134
+ background: transparent;
135
+ border-bottom: 1px solid #2a2a30;
136
+ gap: 0;
137
+ }
138
+ .stTabs [data-baseweb="tab"] {
139
+ font-family: 'Syne', sans-serif !important;
140
+ font-weight: 600 !important;
141
+ font-size: 0.78rem !important;
142
+ letter-spacing: 0.1em !important;
143
+ text-transform: uppercase !important;
144
+ color: #888 !important;
145
+ background: transparent !important;
146
+ border: none !important;
147
+ padding: 10px 24px !important;
148
+ }
149
+ .stTabs [aria-selected="true"] {
150
+ color: #f0c040 !important;
151
+ border-bottom: 2px solid #f0c040 !important;
152
+ }
153
+
154
+ /* Divider */
155
+ hr {
156
+ border-color: #2a2a30 !important;
157
+ }
158
+
159
+ /* Spinner text */
160
+ .stSpinner > div {
161
+ color: #4af0a0 !important;
162
+ }
163
+
164
+ /* Scrollbar */
165
+ ::-webkit-scrollbar { width: 4px; }
166
+ ::-webkit-scrollbar-track { background: #0d0d0f; }
167
+ ::-webkit-scrollbar-thumb { background: #2a2a30; border-radius: 2px; }
168
+
169
+ /* Word freq bars */
170
+ .word-bar-container { margin: 4px 0; }
171
+ .word-bar-label {
172
+ font-family: 'Space Mono', monospace;
173
+ font-size: 0.72rem;
174
+ color: #888;
175
+ display: flex;
176
+ justify-content: space-between;
177
+ margin-bottom: 2px;
178
+ }
179
+ .word-bar {
180
+ height: 6px;
181
+ background: linear-gradient(90deg, #4af0a0, #f0c040);
182
+ border-radius: 1px;
183
+ }
184
+ </style>
185
+ """, unsafe_allow_html=True)
186
+
187
+ # ── Session state ──────────────────────────────────────────────────────────────
188
+ if "messages" not in st.session_state:
189
+ st.session_state.messages = []
190
+ if "model_loaded" not in st.session_state:
191
+ st.session_state.model_loaded = False
192
+ if "pipeline" not in st.session_state:
193
+ st.session_state.pipeline = None
194
+ if "total_tokens" not in st.session_state:
195
+ st.session_state.total_tokens = 0
196
+ if "response_times" not in st.session_state:
197
+ st.session_state.response_times = []
198
+ if "turn_count" not in st.session_state:
199
+ st.session_state.turn_count = 0
200
+
201
+ # ── Model loader ───────────────────────────────────────────────────────────────
202
+ @st.cache_resource(show_spinner=False)
203
+ def load_model(model_id: str):
204
+ from transformers import pipeline as hf_pipeline
205
+ pipe = hf_pipeline(
206
+ "text-generation",
207
+ model=model_id,
208
+ device_map="auto",
209
+ trust_remote_code=True,
210
+ )
211
+ return pipe
212
+
213
+ # ── Helpers ────────────────────────────────────────────────────────────────────
214
+ MODEL_OPTIONS = {
215
+ "SmolLM2-135M-Instruct (HF)": "HuggingFaceTB/SmolLM2-135M-Instruct",
216
+ "SmolLM2-360M-Instruct (HF)": "HuggingFaceTB/SmolLM2-360M-Instruct",
217
+ "TinyLlama-1.1B-Chat": "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
218
+ "Qwen2.5-0.5B-Instruct": "Qwen/Qwen2.5-0.5B-Instruct",
219
+ }
220
+
221
+ def count_tokens_approx(text: str) -> int:
222
+ return max(1, len(text.split()) * 4 // 3)
223
+
224
+ def get_word_freq(messages, top_n=10):
225
+ all_text = " ".join(m["content"] for m in messages).lower()
226
+ words = re.findall(r"\b[a-z]{4,}\b", all_text)
227
+ stopwords = {"that","this","with","from","have","will","been","they",
228
+ "what","when","your","just","more","also","some","than",
229
+ "then","there","their","these","those","about","which","would"}
230
+ words = [w for w in words if w not in stopwords]
231
+ return Counter(words).most_common(top_n)
232
+
233
+ def format_chat_history(messages, model_id: str):
234
+ """Build a prompt string compatible with most instruct models."""
235
+ if "SmolLM2" in model_id or "Qwen" in model_id:
236
+ # ChatML format
237
+ prompt = ""
238
+ for m in messages:
239
+ role = m["role"]
240
+ content = m["content"]
241
+ prompt += f"<|im_start|>{role}\n{content}<|im_end|>\n"
242
+ prompt += "<|im_start|>assistant\n"
243
+ else:
244
+ # TinyLlama / Llama-2 chat format
245
+ prompt = "<s>"
246
+ for m in messages:
247
+ if m["role"] == "user":
248
+ prompt += f"[INST] {m['content']} [/INST]"
249
+ else:
250
+ prompt += f" {m['content']} </s><s>"
251
+ return prompt
252
+
253
+ def generate_response(pipe, messages, model_id, max_new_tokens, temperature):
254
+ prompt = format_chat_history(messages, model_id)
255
+ t0 = time.time()
256
+ out = pipe(
257
+ prompt,
258
+ max_new_tokens=max_new_tokens,
259
+ temperature=temperature,
260
+ do_sample=temperature > 0,
261
+ pad_token_id=pipe.tokenizer.eos_token_id,
262
+ return_full_text=False,
263
+ )
264
+ elapsed = time.time() - t0
265
+ text = out[0]["generated_text"].strip()
266
+ # Strip any trailing special tokens
267
+ for tok in ["<|im_end|>", "</s>", "[INST]"]:
268
+ text = text.split(tok)[0].strip()
269
+ return text, elapsed
270
+
271
+ # ── Sidebar ────────────────────────────────────────────────────────────────────
272
+ with st.sidebar:
273
+ st.markdown("## ⚑ NanoChat")
274
+ st.markdown("<p style='font-size:0.75rem;color:#666;font-family:Space Mono,monospace;margin-top:-8px'>Open-weight LLM Playground</p>", unsafe_allow_html=True)
275
+ st.divider()
276
+
277
+ selected_label = st.selectbox("Model", list(MODEL_OPTIONS.keys()))
278
+ model_id = MODEL_OPTIONS[selected_label]
279
+
280
+ max_new_tokens = st.slider("Max new tokens", 32, 512, 200, 16)
281
+ temperature = st.slider("Temperature", 0.0, 1.5, 0.7, 0.05)
282
+
283
+ st.divider()
284
+
285
+ if st.button("⚑ Load / Reload Model"):
286
+ with st.spinner(f"Loading {selected_label}…"):
287
+ try:
288
+ st.session_state.pipeline = load_model(model_id)
289
+ st.session_state.model_loaded = True
290
+ st.success("Model ready!")
291
+ except Exception as e:
292
+ st.error(f"Error: {e}")
293
+
294
+ if st.button("πŸ—‘ Clear Chat"):
295
+ st.session_state.messages = []
296
+ st.session_state.total_tokens = 0
297
+ st.session_state.response_times = []
298
+ st.session_state.turn_count = 0
299
+ st.rerun()
300
+
301
+ st.divider()
302
+ st.markdown(f"""
303
+ <div style='font-family:Space Mono,monospace;font-size:0.68rem;color:#555;line-height:2'>
304
+ Model ID<br>
305
+ <span style='color:#f0c040'>{model_id.split("/")[-1]}</span><br><br>
306
+ Status<br>
307
+ <span style='color:{"#4af0a0" if st.session_state.model_loaded else "#f06060"}'>
308
+ {"● Loaded" if st.session_state.model_loaded else "β—‹ Not loaded"}
309
+ </span>
310
+ </div>
311
+ """, unsafe_allow_html=True)
312
+
313
+ # ── Main area ──────────────────────────────────────────────────────────────────
314
+ tab_chat, tab_viz = st.tabs(["πŸ’¬ Chat", "πŸ“Š Analytics"])
315
+
316
+ # ─── Chat tab ──────────────────────────────────────────────────────────────────
317
+ with tab_chat:
318
+ st.markdown("<h1 style='margin-bottom:2px'>Chat</h1>", unsafe_allow_html=True)
319
+ st.markdown(f"<p style='font-size:0.78rem;color:#555;font-family:Space Mono,monospace;margin-bottom:24px'>{model_id}</p>", unsafe_allow_html=True)
320
+
321
+ if not st.session_state.model_loaded:
322
+ st.info("πŸ‘ˆ Load a model from the sidebar to begin.")
323
+ else:
324
+ # Render history
325
+ chat_container = st.container()
326
+ with chat_container:
327
+ for msg in st.session_state.messages:
328
+ role_label = "YOU" if msg["role"] == "user" else "AI"
329
+ css_class = "user" if msg["role"] == "user" else "assistant"
330
+ st.markdown(f"""
331
+ <div class='chat-msg {css_class}'>
332
+ <div class='role-label'>{role_label}</div>
333
+ {msg['content']}
334
+ </div>
335
+ """, unsafe_allow_html=True)
336
+
337
+ # Input
338
+ with st.form("chat_form", clear_on_submit=True):
339
+ cols = st.columns([8, 1])
340
+ with cols[0]:
341
+ user_input = st.text_area("Message", height=80, label_visibility="collapsed",
342
+ placeholder="Type a message and press Send…")
343
+ with cols[1]:
344
+ submitted = st.form_submit_button("Send", use_container_width=True)
345
+
346
+ if submitted and user_input.strip():
347
+ st.session_state.messages.append({"role": "user", "content": user_input.strip()})
348
+ st.session_state.total_tokens += count_tokens_approx(user_input)
349
+
350
+ with st.spinner("Thinking…"):
351
+ try:
352
+ reply, elapsed = generate_response(
353
+ st.session_state.pipeline,
354
+ st.session_state.messages,
355
+ model_id,
356
+ max_new_tokens,
357
+ temperature,
358
+ )
359
+ st.session_state.messages.append({"role": "assistant", "content": reply})
360
+ st.session_state.total_tokens += count_tokens_approx(reply)
361
+ st.session_state.response_times.append(round(elapsed, 2))
362
+ st.session_state.turn_count += 1
363
+ except Exception as e:
364
+ st.error(f"Generation error: {e}")
365
+ st.rerun()
366
+
367
+ # ─── Analytics tab ─────────────────────────────────────────────────────────────
368
+ with tab_viz:
369
+ st.markdown("<h1 style='margin-bottom:2px'>Analytics</h1>", unsafe_allow_html=True)
370
+ st.markdown("<p style='font-size:0.78rem;color:#555;font-family:Space Mono,monospace;margin-bottom:24px'>Session insights</p>", unsafe_allow_html=True)
371
+
372
+ msgs = st.session_state.messages
373
+ rt = st.session_state.response_times
374
+
375
+ # ── Metrics row ────────────────────────────────────────────────────────────
376
+ c1, c2, c3, c4 = st.columns(4)
377
+ with c1:
378
+ st.markdown(f"""
379
+ <div class='metric-card'>
380
+ <div class='metric-value'>{st.session_state.turn_count}</div>
381
+ <div class='metric-label'>Turns</div>
382
+ </div>""", unsafe_allow_html=True)
383
+ with c2:
384
+ st.markdown(f"""
385
+ <div class='metric-card'>
386
+ <div class='metric-value'>{st.session_state.total_tokens}</div>
387
+ <div class='metric-label'>Est. Tokens</div>
388
+ </div>""", unsafe_allow_html=True)
389
+ with c3:
390
+ avg_rt = round(sum(rt)/len(rt), 2) if rt else 0.0
391
+ st.markdown(f"""
392
+ <div class='metric-card'>
393
+ <div class='metric-value'>{avg_rt}s</div>
394
+ <div class='metric-label'>Avg Response</div>
395
+ </div>""", unsafe_allow_html=True)
396
+ with c4:
397
+ user_msgs = [m for m in msgs if m["role"]=="user"]
398
+ avg_len = round(sum(len(m["content"].split()) for m in user_msgs)/len(user_msgs)) if user_msgs else 0
399
+ st.markdown(f"""
400
+ <div class='metric-card'>
401
+ <div class='metric-value'>{avg_len}</div>
402
+ <div class='metric-label'>Avg User Words</div>
403
+ </div>""", unsafe_allow_html=True)
404
+
405
+ st.divider()
406
+
407
+ col_left, col_right = st.columns(2)
408
+
409
+ # ── Response time chart ────────────────────────────────────────────────────
410
+ with col_left:
411
+ st.markdown("#### Response times (s)")
412
+ if rt:
413
+ import pandas as pd
414
+ df_rt = pd.DataFrame({"Turn": list(range(1, len(rt)+1)), "Seconds": rt})
415
+ st.line_chart(df_rt.set_index("Turn"), color="#4af0a0", height=200)
416
+ else:
417
+ st.caption("No data yet β€” start chatting!")
418
+
419
+ # ── Message length chart ───────────────────────────────────────────────────
420
+ with col_right:
421
+ st.markdown("#### Message lengths (words)")
422
+ if msgs:
423
+ import pandas as pd
424
+ rows = []
425
+ u_idx = a_idx = 1
426
+ for m in msgs:
427
+ wc = len(m["content"].split())
428
+ if m["role"] == "user":
429
+ rows.append({"idx": u_idx, "role": "User", "words": wc})
430
+ u_idx += 1
431
+ else:
432
+ rows.append({"idx": a_idx, "role": "AI", "words": wc})
433
+ a_idx += 1
434
+ import pandas as pd
435
+ df_ml = pd.DataFrame(rows)
436
+ st.bar_chart(df_ml.pivot_table(index="idx", columns="role", values="words", aggfunc="sum").fillna(0),
437
+ color=["#f0c040", "#4af0a0"], height=200)
438
+ else:
439
+ st.caption("No data yet β€” start chatting!")
440
+
441
+ st.divider()
442
+
443
+ # ── Word frequency ──────────────────────────────────────────────────────────
444
+ st.markdown("#### Top words across conversation")
445
+ if msgs:
446
+ freq = get_word_freq(msgs, top_n=12)
447
+ if freq:
448
+ max_count = freq[0][1]
449
+ for word, count in freq:
450
+ pct = int((count / max_count) * 100)
451
+ st.markdown(f"""
452
+ <div class='word-bar-container'>
453
+ <div class='word-bar-label'><span>{word}</span><span>{count}</span></div>
454
+ <div class='word-bar' style='width:{pct}%'></div>
455
+ </div>""", unsafe_allow_html=True)
456
+ else:
457
+ st.caption("No data yet β€” start chatting!")
458
+
459
+ st.divider()
460
+
461
+ # ── Role distribution ────────────────────────────────────────────────────
462
+ st.markdown("#### Message distribution")
463
+ if msgs:
464
+ u_count = sum(1 for m in msgs if m["role"]=="user")
465
+ a_count = sum(1 for m in msgs if m["role"]=="assistant")
466
+ total = u_count + a_count
467
+ u_pct = int(u_count/total*100)
468
+ a_pct = 100 - u_pct
469
+ st.markdown(f"""
470
+ <div style='display:flex;gap:0;border-radius:3px;overflow:hidden;height:28px;margin:8px 0'>
471
+ <div style='width:{u_pct}%;background:#f0c040;display:flex;align-items:center;
472
+ justify-content:center;font-family:Space Mono,monospace;
473
+ font-size:0.7rem;color:#0d0d0f;font-weight:700'>
474
+ USER {u_pct}%
475
+ </div>
476
+ <div style='width:{a_pct}%;background:#4af0a0;display:flex;align-items:center;
477
+ justify-content:center;font-family:Space Mono,monospace;
478
+ font-size:0.7rem;color:#0d0d0f;font-weight:700'>
479
+ AI {a_pct}%
480
+ </div>
481
+ </div>
482
+ """, unsafe_allow_html=True)
483
+ else:
484
+ st.caption("No data yet.")