| | |
| | |
| | """ |
| | Evaluation back-end for the βinteractive-graphβ interface on Hugging Face Spaces |
| | βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | * Serves all evaluation UIs (`/eval_interfaces/<option>`). |
| | * Transparently patches every explanation HTML so you never touch the originals: |
| | βΈ layout / badge / telemetry tweaks (unchanged) |
| | βΈ **NEW** helper that counts steps and answers the parentβs |
| | `xai-get-step-count`, so the prompt shows β1 β Nβ. |
| | * **DEBUG MODE** (toggle with `ICOT_DEBUG=1`) prints: |
| | βΈ every file request + whether it was patched |
| | βΈ console-side step counts inside each iframe. |
| | * Persists results to CSV / JSON and can push them to |
| | `LLM-XAI/interactive-COT-data` if `ICOT_TOKEN` is set. |
| | """ |
| |
|
| | import os, csv, json, uuid, logging |
| | from datetime import datetime |
| | from pathlib import Path |
| | from flask import ( |
| | Flask, abort, Response, send_file, request, |
| | url_for, render_template_string, jsonify |
| | ) |
| | from huggingface_hub import HfApi, login |
| | import re |
| | from datasets import load_dataset |
| | import random |
| |
|
| | |
| | MAX_USERS = 50 |
| | COT_COUNTER_FILE = "regular_cot_counter.txt" |
| | GRAPH_COUNTER_FILE = "graph_counter.txt" |
| | CODE_COUNTER_FILE = "code_counter.txt" |
| | NATURAL_LANG_COUNTER_FILE = "natural_lang_counter.txt" |
| |
|
| |
|
| | SELECTED_CARD = "graph" |
| |
|
| | def get_submit_counter(file_path:str) -> int: |
| | with open(file_path, 'r') as f: |
| | try: |
| | val = int(f.read().strip()) |
| | except ValueError: |
| | val = 0 |
| | return val |
| |
|
| | def increment_submit_counter(file_path:str) -> int: |
| | with open(file_path, 'r+') as f: |
| | current = get_submit_counter(file_path) |
| | new_value = (current+1) % (MAX_USERS+1) |
| | if new_value == 0: |
| | new_value = 1 |
| | f.seek(0) |
| | f.write(str(new_value)) |
| | f.truncate() |
| | return new_value |
| |
|
| | |
| | def increment_submit_counter_absolute(file_path:str) -> int: |
| | with open(file_path, 'r+') as f: |
| | current = get_submit_counter(file_path) |
| | new_value = current+1 |
| | f.seek(0) |
| | f.write(str(new_value)) |
| | f.truncate() |
| | return new_value |
| |
|
| | def get_the_min_interface() -> str: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | format_lst= ["code", "graph", "inl","cot"] |
| | chosen_format = random.choice(format_lst) |
| | return chosen_format |
| |
|
| | |
| | def get_interface_format(sample_list): |
| | file_path = sample_list[0].get("file", "") |
| | parts = file_path.split("eval_interfaces/") |
| | if len(parts) > 1: |
| | return parts[1].split("/")[0] |
| | return None |
| |
|
| | |
| | DEBUG_MODE = os.getenv("ICOT_DEBUG", "0") != "0" |
| |
|
| | logging.basicConfig( |
| | level=logging.DEBUG if DEBUG_MODE else logging.INFO, |
| | format="%(asctime)s | %(levelname)-8s | %(message)s" |
| | ) |
| | log = logging.getLogger(__name__) |
| | log.info("Debug mode: %s", DEBUG_MODE) |
| |
|
| | |
| | HF_TOKEN = os.getenv("ICOT_TOKEN") |
| | if HF_TOKEN: |
| | login(token=HF_TOKEN) |
| | else: |
| | log.warning("ICOT_TOKEN not set β results will stay local") |
| |
|
| | HF_REPO = "Miles1999/interactive-COT-data" |
| | HF_FOLDER = "session_logs" |
| |
|
| | CODEBASE_DIR = "." |
| | EVAL_PAGES = { |
| | "cot" : "evaluation/eval_interfaces/reg_cot_eval_interface.html", |
| | "interactive_nl" : "evaluation/eval_interfaces/nl_eval_interface.html", |
| | "interactive_code" : "evaluation/eval_interfaces/coding_eval_interface.html", |
| | "interactive_graph": "evaluation/eval_interfaces/graph_eval_interface.html", |
| | } |
| | ALLOWED_ROOTS = ["html_explanations", "evaluation"] |
| |
|
| | CSV_FILENAME = "evaluation_stats.csv" |
| | CSV_PATH = Path(CSV_FILENAME).resolve() |
| | CSV_HEADER = [ |
| | "timestamp","session_id","user_name", |
| | "overallAccuracy(%)","correctItemAccuracy(%)","incorrectItemAccuracy(%)", |
| | "avgTimeCorrect","avgTimeIncorrect", |
| | ] |
| |
|
| | SESSION_DIR = Path("/tmp/sessions") |
| | SESSION_DIR.mkdir(parents=True, exist_ok=True) |
| |
|
| |
|
| |
|
| | |
| | def gen_session_id() -> str: |
| | return str(uuid.uuid4()) |
| |
|
| | def save_session_local(sid: str, data: dict) -> Path: |
| | path = SESSION_DIR / f"{sid}.json" |
| | path.write_text(json.dumps(data, indent=2)) |
| | log.info("Stored session JSON β %s", path) |
| | return path |
| |
|
| | def push_to_hf(local_path: Path, sid: str): |
| | try: |
| | HfApi().upload_file( |
| | path_or_fileobj=str(local_path), |
| | path_in_repo=f"{HF_FOLDER}/{local_path.name}", |
| | repo_id=HF_REPO, |
| | repo_type="dataset", |
| | ) |
| | local_path.unlink() |
| | log.info("Uploaded session %s to HF & removed local copy", sid) |
| | except Exception as e: |
| | log.warning("HF upload failed for %s : %s", sid, e) |
| |
|
| | |
| | INJECT_STYLE = """ |
| | <style> |
| | /* layout tweak: 40 / 60 split */ |
| | .left-panel { width:40%!important } |
| | .right-panel{ width:60%!important } |
| | |
| | /* hide βVariablesβ pane */ |
| | .variables-container{display:none!important} |
| | .explanation-container{flex:1!important;height:auto!important} |
| | /* numbered badge next to each step */ |
| | .step{display:none;align-items:center;gap:12px} |
| | .step.shown{display:flex} |
| | .step-content,.step>*:first-child{flex:1} |
| | .badge{ |
| | flex-shrink:0;color:#adadad;font-size:1.7rem; |
| | font-weight:700;padding:4px 14px;border-radius:16px;pointer-events:none; |
| | } |
| | |
| | /* ββ eliminate blank space in Problem Statement ββββββββββββββββββββ */ |
| | .problem-statement{ |
| | /* let the section size itself */ |
| | height:auto!important; /* overrides 50β―% */ |
| | flex:0 0 auto!important; /* occupy only the space it needs */ |
| | padding:20px!important; /* keep your original insets */ |
| | overflow-y:visible!important; /* scroll not usually needed here */ |
| | } |
| | |
| | .problem-understanding{ |
| | /* fill everything thatβs left */ |
| | flex:1 1 auto!important; /* grow/shrink with column */ |
| | height:auto!important; /* overrides 50β―% */ |
| | overflow-y:auto!important; /* still scroll if content is long */ |
| | } |
| | |
| | /* tidy up internal spacing */ |
| | .problem-statement p{ |
| | margin:0!important; |
| | line-height:1.4!important; |
| | } |
| | .section-title{ |
| | margin-bottom:8px!important; |
| | padding-bottom:3px!important; |
| | } |
| | |
| | /* ββ eliminate perβline margins in the Summary panel βββββββββββββββ */ |
| | .problem-understanding .variable-item, |
| | .problem-understanding p, |
| | .problem-understanding li { |
| | margin: 0 !important; /* no extra vertical gap */ |
| | padding: 0 !important; /* align text to the left edge */ |
| | } |
| | |
| | /* ββ make every control button solid black ββ */ |
| | .btn, |
| | .btn-play-pause, |
| | .btn-stop, |
| | .btn-prev, |
| | .btn-next, |
| | .btn-correct, |
| | .btn-wrong{ |
| | background: #000 !important; /* black fill */ |
| | color: #fff !important; /* white text so it stays readable */ |
| | border: none !important; /* ensure no colored borders peek through */ |
| | } |
| | |
| | .btn:hover { /* keep hover subtle */ |
| | opacity: 0.9 !important; |
| | } |
| | |
| | /* if you dim disabled buttons, keep them grey */ |
| | .btn.disabled { |
| | background: #6c6c6c !important; |
| | color: #fff !important; |
| | } |
| | |
| | /* Naturalβlanguage & graph explanations (.step.active) */ |
| | .step.active { /* remove yellow fill */ |
| | background: transparent !important; |
| | border: 2px solid #ffd700 !important; /* gold border */ |
| | padding-left: 10px !important; /* keep text indented */ |
| | } |
| | .code-line.current { |
| | background: transparent !important; |
| | border: 2px solid #ffd700 !important; |
| | padding-left: 8px !important; |
| | } |
| | .step.active::before { display: none !important; } /* hide blinking dot */ |
| | |
| | /* disable clicking */ |
| | .step{ |
| | pointer-events:none !important; /* clicks, hovers, etc. are ignored */ |
| | cursor: default !important; /* arrow cursor instead of pointer */ |
| | } |
| | /* Do not bold the formula*/ |
| | .formula { |
| | font-weight: normal; |
| | margin: 5px 0; |
| | } |
| | |
| | |
| | </style> |
| | """ |
| |
|
| | |
| | INJECT_SCRIPT_BASE = """ |
| | <script> |
| | |
| | document.addEventListener('DOMContentLoaded', ()=>{ |
| | |
| | /* 1. optional: hide the download button in outer pages */ |
| | const dl = document.getElementById('download-btn'); |
| | if (dl) dl.remove(); // safe even if not present |
| | |
| | /* 2. rename headings */ |
| | const h = document.querySelector('.problem-understanding .section-title'); |
| | if (h) h.textContent = 'Summary'; |
| | const q = document.querySelector('.problem-statement .section-title'); |
| | if (q) q.textContent = 'Question'; |
| | |
| | /* 3. βGoalβ wording */ |
| | // Case 1: <div class="what-to-find"><h4>...</h4> |
| | const goalH4 = document.querySelector('.what-to-find h4'); |
| | if (goalH4) { |
| | goalH4.textContent = 'Goal:'; |
| | } |
| | |
| | // Case 2: standalone <h3>What we need to find |
| | const goalH3 = document.querySelector('.problem-understanding h3'); |
| | if (goalH3 && /what we need to find/i.test(goalH3.textContent)) { |
| | goalH3.textContent = 'Goal:'; |
| | } |
| | |
| | /* 5. remove the formula if there is one */ |
| | const stepContents = document.querySelectorAll('.step-content'); |
| | stepContents.forEach(step => { |
| | let currentNode = step.firstChild; |
| | const formulaDiv = step.querySelector('.formula'); |
| | while (currentNode && currentNode !== formulaDiv) { |
| | if (/[+\-*/=]/.test(currentNode.textContent)){ |
| | if(formulaDiv){ |
| | let next = formulaDiv.nextSibling; |
| | while (next) { |
| | const toRemove = next; |
| | next = next.nextSibling; |
| | step.removeChild(toRemove); |
| | formulaDiv.textContent = ""; |
| | } |
| | } |
| | break; |
| | } |
| | currentNode = currentNode.nextSibling; |
| | } |
| | }); |
| | |
| | |
| | /* 4. decorate & reveal steps */ |
| | const steps = [...document.querySelectorAll('.step')]; |
| | steps.forEach((s, i) => { |
| | const wrap = document.createElement('div'); |
| | wrap.className = 'step-content'; |
| | while (s.firstChild) wrap.appendChild(s.firstChild); |
| | s.appendChild(wrap); |
| | |
| | // Add badge for all steps except the last one |
| | if (i < steps.length - 1) { |
| | const badge = document.createElement('span'); |
| | badge.className = 'badge'; |
| | badge.textContent = '' + (i + 1); |
| | s.appendChild(badge); |
| | } |
| | }); |
| | if (steps.length) { |
| | steps[0].classList.add('active', 'shown'); |
| | window.currentStepIndex = 0; |
| | } |
| | |
| | |
| | |
| | /* cumulative reveal on highlight change */ |
| | const sync = ()=>{ |
| | const idx=steps.findIndex(el=>el.classList.contains('active')); |
| | steps.forEach((el,i)=>el.classList.toggle('shown',i<=idx)); |
| | }; |
| | sync(); |
| | const obs=new MutationObserver(sync); |
| | steps.forEach(el=>obs.observe(el,{attributes:true,attributeFilter:['class']})); |
| | |
| | /* click telemetry */ |
| | const post = key => window.parent?.postMessage({type:'xai-click',key},'*'); |
| | const map = {playPauseBtn:'play',stopBtn:'stop',nextBtn:'next',prevBtn:'prev'}; |
| | Object.entries(map).forEach(([id,key])=>{ |
| | const btn=document.getElementById(id); |
| | if (btn) btn.addEventListener('click',()=>post(key),{capture:true}); |
| | }); |
| | /* Number only executable lines (i.e. lines *without* the .comment span). */ |
| | (function renumberCodeLines(){ |
| | const codeLines = document.querySelectorAll('.code-line'); |
| | let n = 0; |
| | codeLines.forEach(cl=>{ |
| | const numSpan = cl.querySelector('.line-number'); |
| | if(!numSpan) return; // safety check |
| | if(cl.querySelector('.comment')){ // comment line |
| | numSpan.textContent = ''; // blank β hides via :empty |
| | }else{ // real code |
| | numSpan.textContent = ++n; |
| | } |
| | }); |
| | })(); |
| | |
| | }); |
| | |
| | </script> |
| | """ |
| |
|
| | |
| | INJECT_STEPCOUNT = """ |
| | <script> |
| | /* Counts steps and answers parent pageβs xai-get-step-count query */ |
| | (function(){ |
| | function countSteps(){ |
| | /* Case 1: explicit JSON */ |
| | if(typeof problemData==='object'){ |
| | if(problemData.totalSteps) return problemData.totalSteps; |
| | if(Array.isArray(problemData.steps))return problemData.steps.length; |
| | } |
| | /* Case 2: count DOM markers */ |
| | const sel=['.step-item','.step','.badge','[data-step]']; |
| | for(const s of sel){ |
| | const n=document.querySelectorAll(s).length; |
| | if(n) return n; |
| | } |
| | return 1; |
| | } |
| | function send(){ |
| | const n = countSteps(); |
| | /* >>> DEBUG <<< */ |
| | console.debug('[step-count]', n, window.location.pathname); |
| | window.parent?.postMessage({type:'xai-step-count', count:n}, '*'); |
| | } |
| | /* answer on request */ |
| | window.addEventListener('message',ev=>{ |
| | if(ev?.data?.type==='xai-get-step-count') send(); |
| | }); |
| | /* β¦and volunteer once */ |
| | if(document.readyState==='loading'){ |
| | document.addEventListener('DOMContentLoaded',send); |
| | }else{send();} |
| | })(); |
| | </script> |
| | """ |
| |
|
| | |
| | INJECT_SCRIPT_GRAPH_BADGE = """ |
| | <script> |
| | document.addEventListener('DOMContentLoaded',()=>{ |
| | const frame = document.getElementById('step-iframe'); |
| | if(!frame) return; /* not a graph explanation */ |
| | /* inline style keeps badge self-contained */ |
| | const styleBadge = [ |
| | 'flex-shrink:0', |
| | 'color:#adadad', |
| | 'font-size:1.7rem', |
| | 'font-weight:700', |
| | 'padding:2px 10px', |
| | 'border-radius:14px', |
| | 'pointer-events:none' |
| | ].join(';'); |
| | function decorate(){ |
| | try{ |
| | const doc = frame.contentDocument; |
| | if(!doc) return; |
| | let num = 0; /* running step counter */ |
| | [...doc.querySelectorAll('.step-item')].forEach(el=>{ |
| | if(el.querySelector('.badge')) return; /* already done */ |
| | const step_id = el.id |
| | /* title text (trim & lowercase) */ |
| | const title = (el.textContent || '').trim().toLowerCase(); |
| | if(title.startsWith('final answer')) return; /* skip badge */ |
| | /* flex container so title & badge share a line */ |
| | el.style.display = 'flex'; |
| | el.style.alignItems = 'center'; |
| | el.style.gap = '12px'; |
| | if(el.firstElementChild) el.firstElementChild.style.flex = '1'; |
| | const b = doc.createElement('span'); |
| | b.className = 'badge'; |
| | b.style.cssText = styleBadge; |
| | b.textContent = '' + step_id; /* number only real steps */ |
| | el.appendChild(b); |
| | }); |
| | }catch(e){} |
| | } |
| | decorate(); /* decorate current content */ |
| | frame.addEventListener('load', decorate); /* β¦and on every reload */ |
| | }); |
| | </script> |
| | """ |
| |
|
| | DISABLE_SCROLL_SCRIPT = """ |
| | <script> |
| | document.addEventListener('DOMContentLoaded', ()=>{ |
| | // Override scrollIntoView for all steps |
| | const steps = document.querySelectorAll('.step'); |
| | steps.forEach(s => { |
| | s.scrollIntoView = function(){}; |
| | }); |
| | }); |
| | </script> |
| | """ |
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def preprocess_html(path: str) -> str: |
| | """Return patched HTML as string, injecting style + scripts.""" |
| | html = Path(path).read_text(encoding="utf-8") |
| |
|
| | |
| | html = re.sub(r'\bconst\s+problemData\b', 'window.problemData', html) |
| |
|
| | |
| | inj = ( |
| | INJECT_STYLE + |
| | INJECT_STEPCOUNT+ |
| | INJECT_SCRIPT_BASE+ |
| | DISABLE_SCROLL_SCRIPT |
| | ) |
| |
|
| | res = html.replace("</head>", inj + "</head>", 1) if "</head>" in html else inj + html |
| |
|
| |
|
| | if DEBUG_MODE: |
| | log.debug("Injected helpers into %s (%d β %d bytes)", path, len(html), len(res)) |
| | return res |
| |
|
| |
|
| | def needs_transform(path: str) -> bool: |
| | p = path.replace("\\", "/").lower() |
| | return ( |
| | p.endswith(".html") and |
| | ( |
| | "/interactive_nat_lang_explanations/" in p or |
| | "/interactive_graph_explanations/" in p or |
| | "/interactive_coding_explanations/" in p |
| | ) |
| | ) |
| |
|
| | |
| | app = Flask(__name__) |
| |
|
| | |
| | |
| | SELECT_TEMPLATE = """ |
| | <!DOCTYPE html><html lang='en'><head><meta charset='utf-8'> |
| | <title>Select Evaluation Mode</title> |
| | <link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap' rel='stylesheet'> |
| | <style> |
| | *,*:before,*:after{box-sizing:border-box} |
| | body{margin:0;min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center; |
| | font-family:'Inter',sans-serif;background:#f1f5f9} |
| | h1{color:#111827;font-size:2.4rem;font-weight:700;margin-bottom:2rem;text-align:center} |
| | .grid{width:90%;max-width:650px;display:grid;gap:80px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))} |
| | .card{width:350px; height:150px;padding:10px;border-radius:16px;color:#fff;font-weight:600;font-size:1.4rem; |
| | display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center; |
| | text-decoration:none;transition:.25s;box-shadow:0 6px 18px rgba(0,0,0,.08)} |
| | .card:hover{transform:translateY(-6px);box-shadow:0 10px 24px rgba(0,0,0,.12)} |
| | .cot{background:#ef4444}.inl{background:#f97316}.code{background:#10b981}.graph{background:#3b82f6} |
| | </style></head><body> |
| | <h1>Choose an Evaluation Interface</h1> |
| | <div class='grid'> |
| | <a class='card cot' href='/eval_interfaces/cot'>Chain of Thought</a> |
| | <a class='card inl' href='/eval_interfaces/interactive_nl'>Interactive Chain of Thought</a> |
| | <a class='card code' href='/eval_interfaces/interactive_code'>Interactive Program of Thought</a> |
| | <a class='card graph' href='/eval_interfaces/interactive_graph'>Interactive Graph</a> |
| | </div> |
| | </body></html> |
| | """ |
| |
|
| | @app.route("/") |
| | def landing(): |
| | log.info("landing page update") |
| | SELECTED_CARD = get_the_min_interface() |
| | return render_template_string(SELECT_TEMPLATE, selected_card = SELECTED_CARD) |
| |
|
| | |
| | @app.route("/eval_interfaces/<option>") |
| | def load_outer(option): |
| | global SELECTED_CARD |
| | rel = EVAL_PAGES.get(option) |
| | if not rel: |
| | abort(404) |
| | |
| | full_path = Path(CODEBASE_DIR) / rel |
| | html = full_path.read_text(encoding="utf-8") |
| |
|
| | |
| | if option == "cot": |
| | counter = increment_submit_counter(COT_COUNTER_FILE) |
| | log.info("cot counter value %d", counter) |
| | injected = f"<script>const USER_COUNTER = {counter};</script>\n" |
| | html = html.replace("</head>", injected + "</head>") |
| | elif option == "interactive_graph": |
| | counter = increment_submit_counter(GRAPH_COUNTER_FILE) |
| | log.info("graph counter value %d", counter) |
| | injected = f"<script>const USER_COUNTER = {counter};</script>\n" |
| | html = html.replace("</head>", injected + "</head>") |
| | elif option == "interactive_code": |
| | counter = increment_submit_counter(CODE_COUNTER_FILE) |
| | log.info("code counter value %d", counter) |
| | injected = f"<script>const USER_COUNTER = {counter};</script>\n" |
| | html = html.replace("</head>", injected + "</head>") |
| | elif option == "interactive_nl": |
| | counter = increment_submit_counter(NATURAL_LANG_COUNTER_FILE) |
| | log.info("natural language counter value %d", counter) |
| | injected = f"<script>const USER_COUNTER = {counter};</script>\n" |
| | html = html.replace("</head>", injected + "</head>") |
| |
|
| | return render_template_string(html) |
| | |
| |
|
| | |
| | @app.route("/interactive-llm-xai/<path:sub>") |
| | @app.route("/eval_interfaces/interactive-llm-xai/<path:sub>") |
| | def serve_explanation(sub): |
| | full = Path(sub).resolve() |
| | needs = needs_transform(str(full)) |
| | log.info("serve_explanation | %s | needs_transform=%s", full, needs) |
| |
|
| | if not full.exists(): |
| | abort(404) |
| | if full.is_dir(): |
| | return browse(sub) |
| | if needs: |
| | return Response(preprocess_html(str(full)), mimetype="text/html") |
| | return send_file(full) |
| |
|
| | |
| | BROWSER_TEMPLATE = """ |
| | <!DOCTYPE html><html><head><meta charset='utf-8'><title>Browse</title> |
| | <style> |
| | html,body{font-family:Arial;margin:20px;height: 100vh !important;} |
| | ul{list-style:none;padding:0} li{margin:4px 0} |
| | a{text-decoration:none;color:#2563eb} a:hover{text-decoration:underline} |
| | .content{margin-top:15px;border:1px solid #e5e7eb;padding:10px;border-radius:8px;background:#f9fafb} |
| | </style></head><body> |
| | {% if parent_link %}<p><a href='{{ parent_link }}'>[Parent]</a></p>{% endif %} |
| | {% if directories %}<h2>Folders</h2><ul>{% for d in directories %} |
| | <li><a href='{{ url_for('browse', req_path=d.link) }}'>{{ d.name }}</a></li>{% endfor %}</ul>{% endif %} |
| | {% if files %}<h2>HTML Files</h2><ul>{% for f in files %} |
| | <li><a href='{{ url_for('browse', req_path=f.link) }}'>{{ f.name }}</a></li>{% endfor %}</ul>{% endif %} |
| | {% if html_content %}<div class='content'>{{ html_content|safe }}</div>{% endif %} |
| | </body></html> |
| | """ |
| |
|
| | @app.route("/browse/", defaults={"req_path": ""}) |
| | @app.route("/browse/<path:req_path>") |
| | def browse(req_path): |
| | if req_path and req_path.split(os.sep)[0] not in ALLOWED_ROOTS: |
| | abort(404) |
| | full = Path(req_path).resolve() |
| | if not full.exists(): |
| | abort(404) |
| |
|
| | if full.is_dir(): |
| | dirs, files = [], [] |
| | for e in sorted(full.iterdir()): |
| | if e.name.startswith("."): |
| | continue |
| | rel = str(e.relative_to(Path("."))) |
| | if e.is_dir(): |
| | dirs.append({"name": e.name, "link": rel}) |
| | elif e.suffix.lower() == ".html": |
| | files.append({"name": e.name, "link": rel}) |
| | parent = url_for("landing") if not req_path else url_for("browse", req_path=str(full.parent)) |
| | return render_template_string(BROWSER_TEMPLATE, |
| | parent_link=parent, |
| | directories=dirs, files=files, |
| | html_content=None) |
| |
|
| | |
| | if full.suffix.lower() == ".html": |
| | return send_file(full) |
| | parent = url_for("browse", req_path=str(full.parent.relative_to(Path(".")))) |
| | txt = full.read_text(encoding="utf-8", errors="replace") |
| | return render_template_string(BROWSER_TEMPLATE, |
| | parent_link=parent, |
| | html_content=f"<pre>{txt}</pre>") |
| |
|
| | |
| | @app.route("/save-stats", methods=["POST"]) |
| | def save_stats(): |
| | |
| | data = request.get_json(force=True, silent=True) or {} |
| | sid = data.get("sessionId") or gen_session_id() |
| | stats = {k: data.get(k) for k in ( |
| | "overallAccuracy","correctItemAccuracy","incorrectItemAccuracy", |
| | "avgTimeCorrect","avgTimeIncorrect","samples","subjective_feedback")} |
| | stats["timestamp"] = datetime.utcnow().isoformat() |
| | stats["session_id"]= sid |
| | stats["user_name"] = data.get("userName","anonymous") |
| |
|
| | |
| | row = [ |
| | stats["timestamp"], sid, stats["user_name"], |
| | data.get("overallAccuracy"), data.get("correctItemAccuracy"), |
| | data.get("incorrectItemAccuracy"), data.get("avgTimeCorrect"), |
| | data.get("avgTimeIncorrect"), |
| | ] |
| | need_header = not CSV_PATH.exists() |
| | with CSV_PATH.open("a", newline="") as f: |
| | w = csv.writer(f) |
| | if need_header: |
| | w.writerow(CSV_HEADER) |
| | w.writerow(row) |
| |
|
| | |
| | path = save_session_local(sid, stats) |
| | if HF_TOKEN: |
| | push_to_hf(path, sid) |
| | log.info("new result pushed to database") |
| | SELECTED_CARD = get_the_min_interface() |
| | log.info("current selected card") |
| | log.info(SELECTED_CARD) |
| | return jsonify({"status":"ok"}) |
| |
|
| | |
| | if __name__ == "__main__": |
| | |
| | app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)), debug=False) |