File size: 13,142 Bytes
18a84f0
 
 
0557f62
 
 
 
 
079599d
f95b5ce
 
 
abf264d
f95b5ce
abf264d
0557f62
18a84f0
0557f62
079599d
a6d0fb6
079599d
 
 
f95b5ce
 
 
 
 
18a84f0
f95b5ce
 
 
 
 
 
 
18a84f0
f95b5ce
079599d
f95b5ce
079599d
 
f95b5ce
 
 
18a84f0
f95b5ce
 
 
079599d
 
 
f95b5ce
 
 
 
079599d
f95b5ce
 
 
 
 
079599d
 
 
 
 
 
 
 
 
 
 
f95b5ce
079599d
f95b5ce
 
 
 
 
 
079599d
 
 
f95b5ce
 
 
 
 
 
 
 
079599d
f95b5ce
079599d
f95b5ce
 
079599d
 
 
 
 
f95b5ce
 
 
 
079599d
f95b5ce
 
 
 
079599d
f95b5ce
079599d
f95b5ce
 
079599d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f95b5ce
 
 
 
079599d
f95b5ce
 
 
 
18a84f0
f95b5ce
 
 
 
079599d
f95b5ce
0557f62
 
a8979e3
f95b5ce
079599d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f95b5ce
 
a6d0fb6
f95b5ce
 
 
 
 
 
 
079599d
f95b5ce
 
079599d
 
 
f95b5ce
 
079599d
 
f95b5ce
079599d
 
f95b5ce
 
079599d
 
 
f95b5ce
18a84f0
079599d
18a84f0
079599d
0557f62
079599d
0557f62
 
 
 
 
 
 
 
 
 
079599d
f95b5ce
18a84f0
 
079599d
 
 
18a84f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
079599d
 
18a84f0
079599d
18a84f0
f95b5ce
 
079599d
 
f95b5ce
079599d
 
f95b5ce
079599d
3d0885b
079599d
3d0885b
f95b5ce
 
 
079599d
f95b5ce
079599d
f95b5ce
 
 
 
079599d
f95b5ce
 
 
079599d
 
f95b5ce
 
079599d
f95b5ce
a8979e3
f95b5ce
079599d
 
f95b5ce
 
 
079599d
 
f95b5ce
 
 
079599d
 
 
 
 
f95b5ce
 
 
 
079599d
f95b5ce
 
 
079599d
f95b5ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
079599d
f95b5ce
 
079599d
f95b5ce
 
079599d
f95b5ce
 
 
 
 
 
 
079599d
 
f95b5ce
079599d
f95b5ce
abf264d
18a84f0
abf264d
0557f62
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# app.py — Hugging Face Space entrypoint (name gate + chat + feedback + optional admin exports)
# Dependencies: gradio (uses stdlib sqlite3/csv only)
# Persistent storage: set Space secret ENV DATA_DIR=/data (or enable Persistent storage; auto-detects /data)

import os
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

import sqlite3, csv, time
from datetime import datetime
from typing import Optional, Tuple

import gradio as gr
from First_Pass import ask  # your RAG/LLM core

TITLE = "Askstein — CT Rigidity / FE Q&A"
DESC  = "Enter your first and last name to start chatting. After each answer, leave a 👍 or 👎 — feedback is saved."

# ====== Storage (sessions + feedback) ========================================

# Prefer Space persistent storage if mounted
DEFAULT_DATA_DIR = "/data" if os.path.isdir("/data") and os.access("/data", os.W_OK) else "./data"
DATA_DIR = os.path.abspath(os.getenv("DATA_DIR", DEFAULT_DATA_DIR))
DB_PATH  = os.path.join(DATA_DIR, "askstein.db")
os.makedirs(DATA_DIR, exist_ok=True)

def _db():
    conn = sqlite3.connect(DB_PATH, check_same_thread=False)
    # Reasonable WAL settings for concurrent Gradio threads
    conn.execute("PRAGMA journal_mode=WAL;")
    conn.execute("PRAGMA synchronous=NORMAL;")
    return conn

def init_db():
    conn = _db()
    cur = conn.cursor()
    # One lightweight "session" per start button click (no auth)
    cur.execute("""
    CREATE TABLE IF NOT EXISTS sessions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name  TEXT NOT NULL,
        created_at TEXT NOT NULL
    )
    """)
    # Feedback tied to a session (also stores names redundantly for simpler exports)
    cur.execute("""
    CREATE TABLE IF NOT EXISTS feedback (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id INTEGER,
        first_name TEXT NOT NULL,
        last_name  TEXT NOT NULL,
        question TEXT NOT NULL,
        answer_preview TEXT NOT NULL,
        rating INTEGER NOT NULL,   -- +1 = thumbs up, -1 = thumbs down
        created_at TEXT NOT NULL,
        FOREIGN KEY (session_id) REFERENCES sessions(id)
    )
    """)
    conn.commit()
    conn.close()

def create_session(first: str, last: str) -> int:
    conn = _db()
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO sessions (first_name, last_name, created_at) VALUES (?, ?, ?)",
        (first, last, datetime.utcnow().isoformat())
    )
    sid = cur.lastrowid
    conn.commit()
    conn.close()
    return int(sid)

def save_feedback(session_id: Optional[int], first: str, last: str, question: str, answer: str, rating: int) -> Tuple[bool, str]:
    preview = (answer or "").replace("\n", " ").strip()
    if len(preview) > 350:
        preview = preview[:350] + "…"
    try:
        conn = _db()
        conn.execute(
            "INSERT INTO feedback (session_id, first_name, last_name, question, answer_preview, rating, created_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            (session_id, first, last, question, preview, int(rating), datetime.utcnow().isoformat())
        )
        conn.commit()
        return True, "Thanks for your feedback!"
    except Exception as e:
        return False, f"Could not save feedback: {e}"
    finally:
        conn.close()

def export_feedback_csv_for_session(session_id: int) -> str:
    ts = int(time.time())
    out_path = os.path.join(DATA_DIR, f"feedback_session_{session_id}_{ts}.csv")
    conn = _db()
    cur = conn.cursor()
    cur.execute("""
        SELECT id, created_at, rating, question, answer_preview, first_name, last_name
        FROM feedback WHERE session_id = ?
        ORDER BY id DESC
    """, (session_id,))
    rows = cur.fetchall()
    conn.close()
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["id","created_at","rating","question","answer_preview","first_name","last_name"])
        for r in rows:
            w.writerow(r)
    return out_path

def export_all_feedback_csv() -> str:
    ts = int(time.time())
    out_path = os.path.join(DATA_DIR, f"feedback_all_{ts}.csv")
    conn = _db()
    cur = conn.cursor()
    cur.execute("""
        SELECT f.id, f.created_at, f.rating, f.question, f.answer_preview,
               f.first_name, f.last_name, f.session_id
        FROM feedback f
        ORDER BY f.id DESC
    """)
    rows = cur.fetchall()
    conn.close()
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["id","created_at","rating","question","answer_preview","first_name","last_name","session_id"])
        for r in rows:
            w.writerow(r)
    return out_path

def export_sessions_csv() -> str:
    ts = int(time.time())
    out_path = os.path.join(DATA_DIR, f"sessions_{ts}.csv")
    conn = _db()
    cur = conn.cursor()
    cur.execute("SELECT id, first_name, last_name, created_at FROM sessions ORDER BY id DESC")
    rows = cur.fetchall()
    conn.close()
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["id","first_name","last_name","created_at"])
        for r in rows:
            w.writerow(r)
    return out_path

# Initialize DB once on import
init_db()

ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "").strip()

# ====== UI ===================================================================

with gr.Blocks(title=TITLE) as demo:
    gr.Markdown(f"## {TITLE}\n{DESC}")

    # Session state
    st_session_id = gr.State(value=None)   # int session id
    st_first_name = gr.State(value="")     # first name
    st_last_name  = gr.State(value="")     # last name
    st_last_q     = gr.State(value="")
    st_last_a     = gr.State(value="")
    st_can_fb     = gr.State(value=False)
    st_admin_ok   = gr.State(value=False)

    # ---- Name Gate ----
    gate_view = gr.Column(visible=True)
    with gate_view:
        with gr.Row():
            first_tb = gr.Textbox(label="First name", placeholder="Ada")
            last_tb  = gr.Textbox(label="Last name",  placeholder="Lovelace")
        start_btn = gr.Button("Start chatting", variant="primary")
        gate_msg  = gr.Markdown("")

    # ---- Chat View ----
    chat_view = gr.Column(visible=False)
    with chat_view:
        welcome_md = gr.Markdown("### Chat")
        with gr.Row():
            inp = gr.Textbox(
                label="Your question",
                placeholder="e.g., How is bending rigidity (EI) computed from a cortical cross-section?",
                lines=3,
            )
        out = gr.Textbox(label="Askstein", lines=12)
        ask_btn = gr.Button("Ask", variant="primary")

        with gr.Row():
            fb_up   = gr.Button("👍 Helpful")
            fb_down = gr.Button("👎 Not helpful")
            fb_status = gr.Markdown("")

        with gr.Row():
            my_export_btn  = gr.Button("Download this session’s feedback (CSV)")
            my_export_file = gr.File(label="Session feedback CSV", visible=False)

    # ---- Admin (optional) ----
    admin_view = gr.Column(visible=True)
    with admin_view:
        gr.Markdown("### Admin (enter token to unlock)")
        admin_token_in = gr.Textbox(label="Admin token", type="password", placeholder="Set ADMIN_TOKEN secret to use")
        admin_unlock   = gr.Button("Unlock Admin")
        admin_status   = gr.Markdown("")
        with gr.Group(visible=False) as admin_controls:
            all_export_btn      = gr.Button("Export ALL feedback (CSV)")
            sessions_export_btn = gr.Button("Export sessions (CSV)")
            all_export_file      = gr.File(label="All feedback CSV", visible=False)
            sessions_export_file = gr.File(label="Sessions CSV", visible=False)

    # ---- Examples ----
    examples = gr.Examples(
        examples=[
            "Define axial rigidity (EA) and how it is estimated from CT-derived cortical masks.",
            "How does torsional rigidity (GJ) relate to polar moment of area in FE pre-processing?",
            "What are typical pitfalls when mapping Hounsfield units to elastic modulus?",
            "What boundary conditions are common in long-bone FE bending simulations?",
        ],
        inputs=inp,
    )

    # ===== Handlers =====

    # Start chatting (create session) — FLAT return (no nested tuples)
    def on_start(first: str, last: str):
        first = (first or "").strip()
        last  = (last  or "").strip()
        if not first or not last or len(first) > 80 or len(last) > 80:
            # gate_msg, session_id, first, last, gate_view, chat_view, welcome_md
            return (
                gr.Markdown.update(value="Please enter valid first and last names."),
                None, "", "", gr.update(), gr.update(), gr.Markdown.update(value="")
            )
        try:
            sid = create_session(first, last)
            welcome = f"Welcome, **{first} {last}** — session **#{sid}**."
            return (
                gr.Markdown.update(value=""),
                sid, first, last,
                gr.update(visible=False),     # hide gate
                gr.update(visible=True),      # show chat
                gr.Markdown.update(value=welcome),
            )
        except Exception as e:
            return (
                gr.Markdown.update(value=f"Could not start session: {e}"),
                None, "", "", gr.update(), gr.update(), gr.Markdown.update(value="")
            )

    start_btn.click(
        on_start,
        [first_tb, last_tb],
        [gate_msg, st_session_id, st_first_name, st_last_name, gate_view, chat_view, welcome_md],
    )

    # Ask
    def on_ask(session_id, q):
        q = (q or "").strip()
        if not session_id:
            return gr.Textbox.update(value="Please start a session first."), "", "", False
        if not q:
            return gr.Textbox.update(value="Please enter a question."), "", "", False
        try:
            a = ask(q)
        except Exception as e:
            a = f"[runtime error] {e}"
        return gr.Textbox.update(value=a), q, a, True

    ask_btn.click(
        on_ask,
        [st_session_id, inp],
        [out, st_last_q, st_last_a, st_can_fb]
    )
    inp.submit(
        on_ask,
        [st_session_id, inp],
        [out, st_last_q, st_last_a, st_can_fb]
    )

    # Feedback
    def on_feedback(session_id, can_fb, first, last, last_q, last_a, rating):
        if not can_fb or not last_q or not last_a:
            return gr.Markdown.update(value="No recent answer to rate."), False
        ok, msg = save_feedback(session_id, first, last, last_q, last_a, rating)
        return gr.Markdown.update(value=msg), False

    fb_up.click(
        lambda sid, can, fn, ln, q, a: on_feedback(sid, can, fn, ln, q, a, +1),
        [st_session_id, st_can_fb, st_first_name, st_last_name, st_last_q, st_last_a],
        [fb_status, st_can_fb]
    )
    fb_down.click(
        lambda sid, can, fn, ln, q, a: on_feedback(sid, can, fn, ln, q, a, -1),
        [st_session_id, st_can_fb, st_first_name, st_last_name, st_last_q, st_last_a],
        [fb_status, st_can_fb]
    )

    # Export this session’s feedback
    def on_my_export(session_id):
        if not session_id:
            return gr.File.update(visible=False), gr.Markdown.update(value="No active session.")
        path = export_feedback_csv_for_session(session_id)
        return gr.File.update(value=path, visible=True), gr.Markdown.update(value="")

    my_export_btn.click(
        on_my_export,
        [st_session_id],
        [my_export_file, fb_status]
    )

    # Admin unlock
    def on_admin_unlock(token):
        ok = bool(ADMIN_TOKEN) and (token.strip() == ADMIN_TOKEN)
        if ok:
            return True, gr.Markdown.update(value="Admin unlocked ✅"), gr.update(visible=True)
        msg = "Invalid token or ADMIN_TOKEN not set."
        return False, gr.Markdown.update(value=msg), gr.update(visible=False)

    admin_unlock.click(
        on_admin_unlock,
        [admin_token_in],
        [st_admin_ok, admin_status, admin_controls]
    )

    # Admin exports
    def on_export_all(admin_ok):
        if not admin_ok:
            return gr.File.update(visible=False), gr.Markdown.update(value="Admin is locked.")
        path = export_all_feedback_csv()
        return gr.File.update(value=path, visible=True), gr.Markdown.update(value="")

    def on_export_sessions(admin_ok):
        if not admin_ok:
            return gr.File.update(visible=False), gr.Markdown.update(value="Admin is locked.")
        path = export_sessions_csv()
        return gr.File.update(value=path, visible=True), gr.Markdown.update(value="")

    all_export_btn.click(
        on_export_all,
        [st_admin_ok],
        [all_export_file, admin_status]
    )
    sessions_export_btn.click(
        on_export_sessions,
        [st_admin_ok],
        [sessions_export_file, admin_status]
    )

# For HF Spaces (either expose `demo` or call launch in __main__)
if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))