diff --git a/llm_team_ui.py b/llm_team_ui.py index 072392b..2e18522 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -6425,7 +6425,7 @@ def get_runs(): try: with get_db() as conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - _cols = "id, mode, prompt, models_used, created_at, archived, quality_score, score_method, tags, notes, config->>'source' as source, config->>'parent_run' as parent_run" + _cols = "id, mode, prompt, models_used, created_at, archived, quality_score, score_method, tags, notes, config->>'source' as source, config->>'parent_run' as parent_run, config->>'owner' as owner" if show == "archived": cur.execute(f"SELECT {_cols} FROM team_runs WHERE archived = true ORDER BY created_at DESC LIMIT 200") elif show == "all": @@ -7041,7 +7041,16 @@ function renderTable(runs) { // ID var idEl = document.createElement('span'); idEl.className = 'run-id'; idEl.textContent = '#'+r.id; row.appendChild(idEl); // Mode - var modeEl = document.createElement('span'); modeEl.className = 'run-mode'; modeEl.textContent = r.mode; row.appendChild(modeEl); + var modeEl = document.createElement('span'); modeEl.className = 'run-mode'; + modeEl.textContent = r.mode; + if (r.owner === 'public') { + var pubPill = document.createElement('span'); + pubPill.style.cssText = 'font-size:7px;background:rgba(245,158,11,0.1);color:var(--orange);padding:1px 4px;border-radius:2px;border:1px solid rgba(245,158,11,0.2);margin-left:3px;vertical-align:middle'; + pubPill.textContent = 'PUB'; + pubPill.title = 'Public/demo user'; + modeEl.appendChild(pubPill); + } + row.appendChild(modeEl); // Prompt var promptEl = document.createElement('span'); promptEl.className = 'run-prompt'; promptEl.textContent = (r.prompt||'').substring(0,100); promptEl.title = r.prompt||''; row.appendChild(promptEl); // Models @@ -8416,12 +8425,16 @@ def run_team(): run_id = str(_uuid.uuid4())[:8] username = session.get("username", "unknown") + is_public = not session.get("user_id") + run_owner = "public" if is_public else username _active_runs[run_id] = { - "mode": mode, "user": username, "prompt": prompt[:100], + "mode": mode, "user": run_owner, "prompt": prompt[:100], "started": time.time(), "step": 0, "total_steps": 0, "substep": "", "events": 0, "errors": [], "responses_size": 0 } + # Store config for server-side save (survives client disconnect) + _run_config = {"mode": mode, "prompt": config.get("prompt", ""), "config": config, "owner": run_owner, "is_public": is_public} def generate(): import queue @@ -8440,13 +8453,23 @@ def run_team(): hb_thread = threading.Thread(target=heartbeat, daemon=True) hb_thread.start() - # Runner thread: executes the mode runner and pushes events to queue + # Runner thread: executes the mode runner, saves to DB even if client disconnects + _pipeline_collected = [] # shared with generator for run_saved event + def run_pipeline(): try: runner = RUNNERS.get(mode) if runner: for event_str in runner(config): event_queue.put(event_str) + # Also collect responses server-side + if event_str.startswith("data: "): + try: + ev = json.loads(event_str[6:].strip()) + if ev.get("type") == "response": + _pipeline_collected.append({"model": ev.get("model", ""), "text": ev.get("text", ""), "role": ev.get("role", "")}) + except Exception: + pass else: event_queue.put(sse({"type": "response", "model": "system", "text": f"Unknown mode: {mode}", "role": "error"})) event_queue.put(sse({"type": "done"})) @@ -8455,6 +8478,14 @@ def run_team(): event_queue.put(sse({"type": "response", "model": "system", "text": f"Pipeline error: {e}", "role": "error"})) event_queue.put(sse({"type": "done"})) finally: + # ALWAYS save — even if client disconnected + if _pipeline_collected: + cfg = dict(_run_config.get("config", {})) + if _run_config.get("is_public"): + cfg["owner"] = "public" + rid = save_run(_run_config["mode"], _run_config["prompt"], cfg, _pipeline_collected) + if rid: + event_queue.put(sse({"type": "run_saved", "run_id": rid})) event_queue.put(None) # sentinel pipeline_thread = threading.Thread(target=run_pipeline, daemon=True) @@ -8498,10 +8529,6 @@ def run_team(): run["response_count"] = len(collected) _log_run(dict(run, run_id=run_id)) _active_runs.pop(run_id, None) - if collected: - rid = save_run(mode, config.get("prompt", ""), config, collected) - if rid: - yield sse({"type": "run_saved", "run_id": rid}) return Response(generate(), mimetype="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive"})