Spaces:
Running on Zero
Running on Zero
MSG commited on
Commit ·
196a48f
1
Parent(s): 871f869
Feat/fix stuff and space basics (#13)
Browse files* dark theme wip
* loading
* wip loader spinner
* wip research and rag
* benchmark wip
* clean coach page
- apps/gradio-space/src/gradio_space/api/studio.py +2 -1
- apps/gradio-space/src/gradio_space/research_helpers.py +13 -2
- apps/gradio-space/src/gradio_space/ui/studio_html.py +1 -1
- apps/gradio-space/static/studio/index.html +60 -45
- apps/gradio-space/static/studio/studio.css +145 -95
- apps/gradio-space/static/studio/studio.js +347 -206
- libs/agent/src/agent/research_prompts.py +4 -1
- libs/agent/src/agent/runner.py +7 -0
- libs/agent/src/agent/tools/research_tools.py +21 -1
- libs/echocoach/src/echocoach/prompts.py +1 -1
- libs/echocoach/src/echocoach/teacher_voice.py +29 -11
- libs/echocoach/tests/test_teacher_voice.py +27 -1
- libs/inference/src/inference/response_clean.py +36 -1
- libs/inference/tests/test_response_clean.py +12 -0
- libs/researchmind/src/researchmind/citations.py +1 -1
- libs/researchmind/tests/test_citations.py +8 -0
- scripts/benchmark_rag_chat.py +156 -0
apps/gradio-space/src/gradio_space/api/studio.py
CHANGED
|
@@ -579,9 +579,9 @@ def api_teacher_voice_turn(
|
|
| 579 |
assistant=result.assistant_text,
|
| 580 |
status=result.rag_status or "Turn complete.",
|
| 581 |
voiceout_path=result.voiceout_path,
|
|
|
|
| 582 |
)
|
| 583 |
|
| 584 |
-
|
| 585 |
def api_teacher_voice_audio_turn(
|
| 586 |
audio_path: str,
|
| 587 |
mode: TeacherVoiceMode = "lesson",
|
|
@@ -627,6 +627,7 @@ def api_teacher_voice_audio_turn(
|
|
| 627 |
status=result.rag_status or "Turn complete.",
|
| 628 |
voiceout_path=result.voiceout_path,
|
| 629 |
user_text=result.user_text,
|
|
|
|
| 630 |
)
|
| 631 |
|
| 632 |
|
|
|
|
| 579 |
assistant=result.assistant_text,
|
| 580 |
status=result.rag_status or "Turn complete.",
|
| 581 |
voiceout_path=result.voiceout_path,
|
| 582 |
+
rag_references=result.rag_references,
|
| 583 |
)
|
| 584 |
|
|
|
|
| 585 |
def api_teacher_voice_audio_turn(
|
| 586 |
audio_path: str,
|
| 587 |
mode: TeacherVoiceMode = "lesson",
|
|
|
|
| 627 |
status=result.rag_status or "Turn complete.",
|
| 628 |
voiceout_path=result.voiceout_path,
|
| 629 |
user_text=result.user_text,
|
| 630 |
+
rag_references=result.rag_references,
|
| 631 |
)
|
| 632 |
|
| 633 |
|
apps/gradio-space/src/gradio_space/research_helpers.py
CHANGED
|
@@ -126,6 +126,17 @@ def trace_summary_markdown(trace_path: str) -> str:
|
|
| 126 |
"",
|
| 127 |
]
|
| 128 |
for step in data.get("steps", []):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if step.get("type") != "note":
|
| 130 |
continue
|
| 131 |
msg = step.get("message", "")
|
|
@@ -193,8 +204,8 @@ def format_citations_markdown(trace_json: str) -> str:
|
|
| 193 |
return ""
|
| 194 |
lines = ["", "---", "**Sources:**"]
|
| 195 |
for i, cite in enumerate(citations[:5], start=1):
|
| 196 |
-
title = cite.get("title") or cite.get("uri") or "Source"
|
| 197 |
-
uri = cite.get("uri") or ""
|
| 198 |
lines.append(f"{i}. [{title}]({uri})" if uri else f"{i}. {title}")
|
| 199 |
if len(citations) > 5:
|
| 200 |
lines.append(f"_…and {len(citations) - 5} more (see Advanced trace)._")
|
|
|
|
| 126 |
"",
|
| 127 |
]
|
| 128 |
for step in data.get("steps", []):
|
| 129 |
+
if step.get("type") == "step":
|
| 130 |
+
label = step.get("label") or step.get("name") or "step"
|
| 131 |
+
dur = step.get("duration_ms")
|
| 132 |
+
extra = {k: v for k, v in step.items() if k not in ("type", "name", "label", "duration_ms", "elapsed_ms")}
|
| 133 |
+
detail = ""
|
| 134 |
+
if dur is not None:
|
| 135 |
+
detail = f" ({dur} ms)"
|
| 136 |
+
if extra:
|
| 137 |
+
detail += " — " + ", ".join(f"{k}={v!r}" for k, v in extra.items())
|
| 138 |
+
lines.append(f"- **{label}**{detail}")
|
| 139 |
+
continue
|
| 140 |
if step.get("type") != "note":
|
| 141 |
continue
|
| 142 |
msg = step.get("message", "")
|
|
|
|
| 204 |
return ""
|
| 205 |
lines = ["", "---", "**Sources:**"]
|
| 206 |
for i, cite in enumerate(citations[:5], start=1):
|
| 207 |
+
title = cite.get("doc_title") or cite.get("title") or cite.get("doc_uri") or cite.get("uri") or "Source"
|
| 208 |
+
uri = cite.get("doc_uri") or cite.get("uri") or ""
|
| 209 |
lines.append(f"{i}. [{title}]({uri})" if uri else f"{i}. {title}")
|
| 210 |
if len(citations) > 5:
|
| 211 |
lines.append(f"_…and {len(citations) - 5} more (see Advanced trace)._")
|
apps/gradio-space/src/gradio_space/ui/studio_html.py
CHANGED
|
@@ -127,7 +127,7 @@ def render_echo_coach_panel(
|
|
| 127 |
if pace_score is None and not tip and not report_md and not transcript_html:
|
| 128 |
return """
|
| 129 |
<div class="studio-coach-panel studio-coach-idle">
|
| 130 |
-
<p class="studio-coach-hint">Record a pitch
|
| 131 |
</div>"""
|
| 132 |
|
| 133 |
score = pace_score if pace_score is not None else "—"
|
|
|
|
| 127 |
if pace_score is None and not tip and not report_md and not transcript_html:
|
| 128 |
return """
|
| 129 |
<div class="studio-coach-panel studio-coach-idle">
|
| 130 |
+
<p class="studio-coach-hint">Record a pitch below, then click <strong>Analyze pitch</strong> for metrics.</p>
|
| 131 |
</div>"""
|
| 132 |
|
| 133 |
score = pace_score if pace_score is not None else "—"
|
apps/gradio-space/static/studio/index.html
CHANGED
|
@@ -8,6 +8,13 @@
|
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" rel="stylesheet" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
<link rel="stylesheet" href="/static/studio/studio.css" />
|
| 12 |
</head>
|
| 13 |
<body>
|
|
@@ -28,7 +35,6 @@
|
|
| 28 |
<button type="button" class="nav-item" data-view="research"><span class="material-symbols-outlined">search</span>Research</button>
|
| 29 |
<button type="button" class="nav-item active" data-view="slides"><span class="material-symbols-outlined">present_to_all</span>Slides</button>
|
| 30 |
<button type="button" class="nav-item" data-view="voice"><span class="material-symbols-outlined">mic</span>Voice</button>
|
| 31 |
-
<button type="button" class="nav-item" data-view="coach"><span class="material-symbols-outlined">school</span>Coach</button>
|
| 32 |
<button type="button" class="nav-item" data-view="debug"><span class="material-symbols-outlined">bug_report</span>Debug</button>
|
| 33 |
<button type="button" id="btn-open-settings" class="nav-item"><span class="material-symbols-outlined">settings</span>Settings</button>
|
| 34 |
<a href="/classic" class="nav-item nav-link"><span class="material-symbols-outlined">open_in_new</span>Classic UI</a>
|
|
@@ -47,6 +53,9 @@
|
|
| 47 |
<strong id="project-title">Small Model Finetuning</strong>
|
| 48 |
</nav>
|
| 49 |
<div class="topbar-actions">
|
|
|
|
|
|
|
|
|
|
| 50 |
<a href="/classic" class="btn btn-ghost">Classic UI</a>
|
| 51 |
<button type="button" id="btn-export" class="btn btn-primary" disabled>Export</button>
|
| 52 |
</div>
|
|
@@ -266,11 +275,11 @@
|
|
| 266 |
</div>
|
| 267 |
</div>
|
| 268 |
<div id="slide-canvas" class="slide-canvas">
|
| 269 |
-
<div id="canvas-overlay" class="
|
| 270 |
-
<div class="
|
| 271 |
-
<span class="
|
| 272 |
-
<p id="canvas-overlay-text">Generating slides…</p>
|
| 273 |
-
<p class="canvas-overlay-hint">Local CPU models often take 30–90 seconds.</p>
|
| 274 |
</div>
|
| 275 |
</div>
|
| 276 |
<div id="slide-canvas-content" class="slide-canvas-content">
|
|
@@ -296,16 +305,16 @@
|
|
| 296 |
<div class="card voice-rag-card">
|
| 297 |
<p class="card-title">RAG Scope</p>
|
| 298 |
<label class="toggle-row">
|
| 299 |
-
<span>
|
| 300 |
<input id="use-rag" type="checkbox" checked />
|
| 301 |
</label>
|
| 302 |
-
<p class="status-text">
|
| 303 |
</div>
|
| 304 |
<div class="card voice-rail-controls">
|
| 305 |
<p class="card-title">Mode</p>
|
| 306 |
<div class="mode-cards voice-mode-cards" id="voice-modes">
|
| 307 |
<button type="button" class="mode-card" data-mode="explain">Explain</button>
|
| 308 |
-
<button type="button" class="mode-card active" data-mode="lesson">
|
| 309 |
<button type="button" class="mode-card" data-mode="pitch">Practice</button>
|
| 310 |
</div>
|
| 311 |
<label class="field voice-topic-wrap" id="voice-topic-wrap">
|
|
@@ -371,43 +380,46 @@
|
|
| 371 |
<div id="voice-audio-out" class="voice-audio-out"></div>
|
| 372 |
</div>
|
| 373 |
</div>
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
</div>
|
| 388 |
-
|
| 389 |
-
</div>
|
| 390 |
-
<label class="field coach-upload-field">
|
| 391 |
-
<span>Upload pitch (WAV)</span>
|
| 392 |
-
<input id="coach-audio" type="file" accept="audio/*" />
|
| 393 |
-
</label>
|
| 394 |
-
</div>
|
| 395 |
-
<div class="controls-grid coach-presets">
|
| 396 |
-
<label class="field">
|
| 397 |
-
<span>Language</span>
|
| 398 |
-
<select id="coach-language" class="input"></select>
|
| 399 |
-
</label>
|
| 400 |
-
<label class="field">
|
| 401 |
-
<span>ASR preset</span>
|
| 402 |
-
<select id="coach-asr" class="input"></select>
|
| 403 |
-
</label>
|
| 404 |
</div>
|
| 405 |
-
<label class="toggle-row coach-voiceout-toggle">
|
| 406 |
-
<span>Speak full rewrite (VoiceOut)</span>
|
| 407 |
-
<input id="coach-speak-rewrite" type="checkbox" />
|
| 408 |
-
</label>
|
| 409 |
-
<button type="button" id="btn-analyze" class="btn btn-primary btn-block coach-analyze-btn">Analyze pitch</button>
|
| 410 |
-
<div id="coach-panel" class="coach-results-panel"></div>
|
| 411 |
</div>
|
| 412 |
</section>
|
| 413 |
|
|
@@ -415,7 +427,6 @@
|
|
| 415 |
<div class="card card-tall coach-debug-card">
|
| 416 |
<div class="coach-card-head">
|
| 417 |
<h2 class="section-label">Chat (debug)</h2>
|
| 418 |
-
<p class="coach-card-desc view-coach-only">Plain chat or corpus-grounded answers — traces appear below when RAG is on.</p>
|
| 419 |
<p class="status-text view-debug-only">Plain chat or corpus-grounded answers — traces appear below when RAG is on.</p>
|
| 420 |
</div>
|
| 421 |
<label class="toggle-row">
|
|
@@ -467,6 +478,10 @@
|
|
| 467 |
<h2 id="settings-title">Settings</h2>
|
| 468 |
<button type="button" id="btn-close-settings" class="btn btn-ghost btn-icon material-symbols-outlined" aria-label="Close">close</button>
|
| 469 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
<div id="settings-model-wrap">
|
| 471 |
<p id="settings-active-model" class="status-text"></p>
|
| 472 |
<label class="field hidden" id="settings-model-select-wrap">
|
|
|
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" rel="stylesheet" />
|
| 11 |
+
<script>
|
| 12 |
+
(function () {
|
| 13 |
+
var t = localStorage.getItem("studio-theme");
|
| 14 |
+
if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches))
|
| 15 |
+
document.documentElement.dataset.theme = "dark";
|
| 16 |
+
})();
|
| 17 |
+
</script>
|
| 18 |
<link rel="stylesheet" href="/static/studio/studio.css" />
|
| 19 |
</head>
|
| 20 |
<body>
|
|
|
|
| 35 |
<button type="button" class="nav-item" data-view="research"><span class="material-symbols-outlined">search</span>Research</button>
|
| 36 |
<button type="button" class="nav-item active" data-view="slides"><span class="material-symbols-outlined">present_to_all</span>Slides</button>
|
| 37 |
<button type="button" class="nav-item" data-view="voice"><span class="material-symbols-outlined">mic</span>Voice</button>
|
|
|
|
| 38 |
<button type="button" class="nav-item" data-view="debug"><span class="material-symbols-outlined">bug_report</span>Debug</button>
|
| 39 |
<button type="button" id="btn-open-settings" class="nav-item"><span class="material-symbols-outlined">settings</span>Settings</button>
|
| 40 |
<a href="/classic" class="nav-item nav-link"><span class="material-symbols-outlined">open_in_new</span>Classic UI</a>
|
|
|
|
| 53 |
<strong id="project-title">Small Model Finetuning</strong>
|
| 54 |
</nav>
|
| 55 |
<div class="topbar-actions">
|
| 56 |
+
<button type="button" id="theme-toggle-btn" class="btn btn-ghost btn-icon" aria-label="Toggle light/dark theme" title="Toggle theme">
|
| 57 |
+
<span class="material-symbols-outlined" id="theme-icon">dark_mode</span>
|
| 58 |
+
</button>
|
| 59 |
<a href="/classic" class="btn btn-ghost">Classic UI</a>
|
| 60 |
<button type="button" id="btn-export" class="btn btn-primary" disabled>Export</button>
|
| 61 |
</div>
|
|
|
|
| 275 |
</div>
|
| 276 |
</div>
|
| 277 |
<div id="slide-canvas" class="slide-canvas">
|
| 278 |
+
<div id="canvas-overlay" class="region-loading hidden" aria-live="polite">
|
| 279 |
+
<div class="region-loading-inner">
|
| 280 |
+
<span class="studio-spinner" aria-hidden="true"></span>
|
| 281 |
+
<p id="canvas-overlay-text" class="region-loading-text">Generating slides…</p>
|
| 282 |
+
<p class="region-loading-hint canvas-overlay-hint">Local CPU models often take 30–90 seconds.</p>
|
| 283 |
</div>
|
| 284 |
</div>
|
| 285 |
<div id="slide-canvas-content" class="slide-canvas-content">
|
|
|
|
| 305 |
<div class="card voice-rag-card">
|
| 306 |
<p class="card-title">RAG Scope</p>
|
| 307 |
<label class="toggle-row">
|
| 308 |
+
<span>Answer from my indexed sources</span>
|
| 309 |
<input id="use-rag" type="checkbox" checked />
|
| 310 |
</label>
|
| 311 |
+
<p class="status-text">Ground teacher replies in your workspace documents when enabled.</p>
|
| 312 |
</div>
|
| 313 |
<div class="card voice-rail-controls">
|
| 314 |
<p class="card-title">Mode</p>
|
| 315 |
<div class="mode-cards voice-mode-cards" id="voice-modes">
|
| 316 |
<button type="button" class="mode-card" data-mode="explain">Explain</button>
|
| 317 |
+
<button type="button" class="mode-card active" data-mode="lesson">Lesson</button>
|
| 318 |
<button type="button" class="mode-card" data-mode="pitch">Practice</button>
|
| 319 |
</div>
|
| 320 |
<label class="field voice-topic-wrap" id="voice-topic-wrap">
|
|
|
|
| 380 |
<div id="voice-audio-out" class="voice-audio-out"></div>
|
| 381 |
</div>
|
| 382 |
</div>
|
| 383 |
+
<details class="card voice-pitch-analysis hidden" id="voice-pitch-analysis" open>
|
| 384 |
+
<summary class="voice-pitch-summary">
|
| 385 |
+
<span class="section-label">Deep pitch analysis</span>
|
| 386 |
+
<span class="voice-pitch-summary-hint">Pace, fillers, charts, and spoken rewrite</span>
|
| 387 |
+
</summary>
|
| 388 |
+
<div class="coach-panel-wrap">
|
| 389 |
+
<p class="coach-card-desc">Record or upload a short monologue (up to 30s), then analyze for metrics and feedback.</p>
|
| 390 |
+
<div class="coach-capture-row">
|
| 391 |
+
<div class="coach-capture-controls">
|
| 392 |
+
<div class="recording-row coach-recording-row">
|
| 393 |
+
<button type="button" id="btn-coach-record-start" class="btn btn-secondary">Start mic</button>
|
| 394 |
+
<button type="button" id="btn-coach-record-stop" class="btn btn-secondary" disabled>Stop mic</button>
|
| 395 |
+
<button type="button" id="btn-coach-sample" class="btn btn-ghost">Load sample</button>
|
| 396 |
+
</div>
|
| 397 |
+
<p id="coach-record-status" class="status-text coach-record-status"></p>
|
| 398 |
+
</div>
|
| 399 |
+
<label class="field coach-upload-field">
|
| 400 |
+
<span>Upload pitch (WAV)</span>
|
| 401 |
+
<input id="coach-audio" type="file" accept="audio/*" />
|
| 402 |
+
</label>
|
| 403 |
+
</div>
|
| 404 |
+
<div class="controls-grid coach-presets">
|
| 405 |
+
<label class="field">
|
| 406 |
+
<span>Language</span>
|
| 407 |
+
<select id="coach-language" class="input"></select>
|
| 408 |
+
</label>
|
| 409 |
+
<label class="field">
|
| 410 |
+
<span>ASR preset</span>
|
| 411 |
+
<select id="coach-asr" class="input"></select>
|
| 412 |
+
</label>
|
| 413 |
+
</div>
|
| 414 |
+
<label class="toggle-row coach-voiceout-toggle">
|
| 415 |
+
<span>Speak full rewrite (VoiceOut)</span>
|
| 416 |
+
<input id="coach-speak-rewrite" type="checkbox" />
|
| 417 |
+
</label>
|
| 418 |
+
<button type="button" id="btn-analyze" class="btn btn-primary btn-block coach-analyze-btn">Analyze pitch</button>
|
| 419 |
+
<div id="coach-panel" class="coach-results-panel"></div>
|
| 420 |
</div>
|
| 421 |
+
</details>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
</div>
|
| 424 |
</section>
|
| 425 |
|
|
|
|
| 427 |
<div class="card card-tall coach-debug-card">
|
| 428 |
<div class="coach-card-head">
|
| 429 |
<h2 class="section-label">Chat (debug)</h2>
|
|
|
|
| 430 |
<p class="status-text view-debug-only">Plain chat or corpus-grounded answers — traces appear below when RAG is on.</p>
|
| 431 |
</div>
|
| 432 |
<label class="toggle-row">
|
|
|
|
| 478 |
<h2 id="settings-title">Settings</h2>
|
| 479 |
<button type="button" id="btn-close-settings" class="btn btn-ghost btn-icon material-symbols-outlined" aria-label="Close">close</button>
|
| 480 |
</div>
|
| 481 |
+
<label class="toggle-row theme-toggle">
|
| 482 |
+
<span>Dark theme</span>
|
| 483 |
+
<input id="theme-toggle" type="checkbox" role="switch" aria-label="Dark theme" />
|
| 484 |
+
</label>
|
| 485 |
<div id="settings-model-wrap">
|
| 486 |
<p id="settings-active-model" class="status-text"></p>
|
| 487 |
<label class="field hidden" id="settings-model-select-wrap">
|
apps/gradio-space/static/studio/studio.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
| 5 |
--on-primary: #ffffff;
|
| 6 |
--background: #f7f9fb;
|
| 7 |
--surface: #f7f9fb;
|
|
|
|
| 8 |
--surface-container-low: #f2f4f6;
|
| 9 |
--surface-container: #eceef0;
|
| 10 |
--secondary: #565e74;
|
|
@@ -15,6 +16,14 @@
|
|
| 15 |
--inverse-surface: #2d3133;
|
| 16 |
--inverse-on-surface: #eff1f3;
|
| 17 |
--error: #ba1a1a;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
--sidebar-w: 280px;
|
| 19 |
--topbar-h: 64px;
|
| 20 |
--context-bar-h: 72px;
|
|
@@ -23,6 +32,30 @@
|
|
| 23 |
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
* { box-sizing: border-box; }
|
| 27 |
|
| 28 |
body {
|
|
@@ -52,7 +85,7 @@ body {
|
|
| 52 |
}
|
| 53 |
|
| 54 |
.studio-banner-loading { background: var(--secondary-container); color: var(--on-secondary-container); }
|
| 55 |
-
.studio-banner-error { background:
|
| 56 |
|
| 57 |
.sidebar {
|
| 58 |
position: fixed;
|
|
@@ -160,7 +193,7 @@ body {
|
|
| 160 |
left: var(--sidebar-w);
|
| 161 |
right: 0;
|
| 162 |
z-index: 35;
|
| 163 |
-
background:
|
| 164 |
border-bottom: 1px solid var(--outline-variant);
|
| 165 |
padding: 0.5rem 1.5rem;
|
| 166 |
}
|
|
@@ -244,7 +277,7 @@ body {
|
|
| 244 |
}
|
| 245 |
|
| 246 |
.card {
|
| 247 |
-
background:
|
| 248 |
border: 1px solid var(--outline-variant);
|
| 249 |
border-radius: var(--radius-xl);
|
| 250 |
padding: 1rem;
|
|
@@ -288,7 +321,7 @@ body {
|
|
| 288 |
.btn:active { transform: scale(0.98); }
|
| 289 |
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 290 |
.btn-primary { background: var(--primary); color: var(--on-primary); }
|
| 291 |
-
.btn-secondary { background:
|
| 292 |
.btn-ghost { background: transparent; border-color: var(--outline-variant); color: var(--secondary); text-decoration: none; }
|
| 293 |
.btn-block { width: 100%; margin-top: 0.5rem; }
|
| 294 |
|
|
@@ -299,13 +332,14 @@ body {
|
|
| 299 |
border-radius: 8px;
|
| 300 |
font: inherit;
|
| 301 |
font-size: 0.9rem;
|
| 302 |
-
background:
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
.input:focus {
|
| 306 |
outline: none;
|
| 307 |
border-color: var(--primary);
|
| 308 |
-
box-shadow: 0 0 0 2px
|
| 309 |
}
|
| 310 |
|
| 311 |
.upload-zone {
|
|
@@ -350,13 +384,24 @@ body {
|
|
| 350 |
|
| 351 |
.controls-actions { display: flex; justify-content: flex-end; margin-top: 0.75rem; }
|
| 352 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
.slide-canvas {
|
| 354 |
position: relative;
|
| 355 |
flex: 1;
|
| 356 |
min-height: 320px;
|
| 357 |
border: 2px dashed var(--outline-variant);
|
| 358 |
border-radius: var(--radius-xl);
|
| 359 |
-
background:
|
| 360 |
overflow: auto;
|
| 361 |
padding: 1rem;
|
| 362 |
}
|
|
@@ -365,39 +410,52 @@ body {
|
|
| 365 |
min-height: 280px;
|
| 366 |
}
|
| 367 |
|
| 368 |
-
.
|
| 369 |
position: absolute;
|
| 370 |
inset: 0;
|
| 371 |
z-index: 5;
|
| 372 |
display: flex;
|
| 373 |
align-items: center;
|
| 374 |
justify-content: center;
|
| 375 |
-
background:
|
| 376 |
-
|
| 377 |
-
border-radius: var(--radius-lg);
|
| 378 |
}
|
| 379 |
|
| 380 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
text-align: center;
|
| 382 |
-
padding: 1.5rem;
|
| 383 |
}
|
| 384 |
|
|
|
|
| 385 |
.canvas-spinner,
|
| 386 |
.lesson-running-spinner {
|
| 387 |
display: inline-block;
|
| 388 |
-
width:
|
| 389 |
-
height:
|
| 390 |
border: 3px solid var(--outline-variant);
|
| 391 |
border-top-color: var(--primary);
|
| 392 |
border-radius: 50%;
|
| 393 |
animation: studio-spin 0.9s linear infinite;
|
| 394 |
-
|
| 395 |
}
|
| 396 |
|
| 397 |
-
.
|
| 398 |
-
|
|
|
|
| 399 |
font-size: 0.85rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
color: var(--secondary);
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
@keyframes studio-spin {
|
|
@@ -457,7 +515,7 @@ body {
|
|
| 457 |
|
| 458 |
.progress-step.pending { color: var(--secondary); }
|
| 459 |
.progress-step.active { color: var(--primary); font-weight: 600; }
|
| 460 |
-
.progress-step.done { color:
|
| 461 |
|
| 462 |
.progress-log {
|
| 463 |
margin: 0.75rem 0 0;
|
|
@@ -510,7 +568,7 @@ body {
|
|
| 510 |
text-transform: uppercase;
|
| 511 |
}
|
| 512 |
|
| 513 |
-
.studio-badge-rag { background: var(--primary-fixed); color:
|
| 514 |
.studio-badge-muted { background: var(--surface-container); color: var(--secondary); }
|
| 515 |
|
| 516 |
.studio-doc-header { margin-bottom: 0.5rem; }
|
|
@@ -522,7 +580,7 @@ body {
|
|
| 522 |
padding: 0.5rem;
|
| 523 |
border: 1px solid var(--outline-variant);
|
| 524 |
border-radius: 8px;
|
| 525 |
-
background:
|
| 526 |
}
|
| 527 |
|
| 528 |
.studio-doc-card:hover { border-color: var(--primary); }
|
|
@@ -550,10 +608,11 @@ body {
|
|
| 550 |
padding: 0.6rem 0.75rem;
|
| 551 |
border: 1px solid var(--outline-variant);
|
| 552 |
border-radius: 8px;
|
| 553 |
-
background:
|
| 554 |
font: inherit;
|
| 555 |
cursor: pointer;
|
| 556 |
text-align: left;
|
|
|
|
| 557 |
}
|
| 558 |
|
| 559 |
.mode-card.active {
|
|
@@ -870,7 +929,7 @@ body {
|
|
| 870 |
border-radius: 6px;
|
| 871 |
}
|
| 872 |
|
| 873 |
-
.url-choice-item:hover { background:
|
| 874 |
|
| 875 |
.url-choice-item span {
|
| 876 |
word-break: break-all;
|
|
@@ -941,7 +1000,7 @@ body {
|
|
| 941 |
|
| 942 |
.research-chat-assistant {
|
| 943 |
align-self: flex-start;
|
| 944 |
-
background:
|
| 945 |
border: 1px solid var(--outline-variant);
|
| 946 |
}
|
| 947 |
|
|
@@ -961,8 +1020,7 @@ body {
|
|
| 961 |
.workspace[data-view="voice"] .col-research,
|
| 962 |
.workspace[data-view="voice"] .col-slides { display: none; }
|
| 963 |
|
| 964 |
-
.workspace[data-view="voice"] .col-debug
|
| 965 |
-
.workspace[data-view="voice"] .view-coach-only { display: none; }
|
| 966 |
|
| 967 |
.view-voice-only { display: none; }
|
| 968 |
|
|
@@ -995,6 +1053,54 @@ body {
|
|
| 995 |
|
| 996 |
.workspace[data-view="voice"] .voice-main {
|
| 997 |
min-width: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
}
|
| 999 |
|
| 1000 |
.workspace[data-view="voice"] .voice-main-card {
|
|
@@ -1115,69 +1221,9 @@ body {
|
|
| 1115 |
}
|
| 1116 |
}
|
| 1117 |
|
| 1118 |
-
.workspace[data-view="coach"] .col-research,
|
| 1119 |
-
.workspace[data-view="coach"] .col-slides { display: none; }
|
| 1120 |
-
|
| 1121 |
-
.workspace[data-view="coach"] .view-voice-only { display: none; }
|
| 1122 |
-
|
| 1123 |
.workspace[data-view="slides"] .col-studio,
|
| 1124 |
.workspace[data-view="research"] .col-debug { display: none; }
|
| 1125 |
|
| 1126 |
-
.workspace[data-view="coach"] {
|
| 1127 |
-
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
| 1128 |
-
max-width: 1280px;
|
| 1129 |
-
gap: 1.25rem;
|
| 1130 |
-
align-items: start;
|
| 1131 |
-
}
|
| 1132 |
-
|
| 1133 |
-
.workspace[data-view="coach"] .coach-panel-wrap,
|
| 1134 |
-
.workspace[data-view="coach"] .coach-debug-card {
|
| 1135 |
-
display: flex;
|
| 1136 |
-
flex-direction: column;
|
| 1137 |
-
}
|
| 1138 |
-
|
| 1139 |
-
.workspace[data-view="coach"] .coach-results-panel {
|
| 1140 |
-
flex: 1;
|
| 1141 |
-
min-height: 120px;
|
| 1142 |
-
margin-top: 0.75rem;
|
| 1143 |
-
overflow-y: auto;
|
| 1144 |
-
}
|
| 1145 |
-
|
| 1146 |
-
.workspace[data-view="coach"] .coach-results-panel:not(:empty) {
|
| 1147 |
-
border-top: 1px solid var(--outline-variant);
|
| 1148 |
-
padding-top: 0.75rem;
|
| 1149 |
-
}
|
| 1150 |
-
|
| 1151 |
-
.workspace[data-view="coach"] .debug-chat-messages {
|
| 1152 |
-
min-height: 140px;
|
| 1153 |
-
max-height: min(240px, 30vh);
|
| 1154 |
-
margin-bottom: 0.5rem;
|
| 1155 |
-
}
|
| 1156 |
-
|
| 1157 |
-
.workspace[data-view="coach"] .coach-debug-compose {
|
| 1158 |
-
padding-top: 0;
|
| 1159 |
-
border-top: none;
|
| 1160 |
-
}
|
| 1161 |
-
|
| 1162 |
-
.workspace[data-view="coach"] .coach-debug-compose textarea {
|
| 1163 |
-
min-height: 3.5rem;
|
| 1164 |
-
resize: vertical;
|
| 1165 |
-
}
|
| 1166 |
-
|
| 1167 |
-
.workspace[data-view="coach"] .coach-debug-card .studio-debug-trace {
|
| 1168 |
-
flex-shrink: 0;
|
| 1169 |
-
margin-top: 0.5rem;
|
| 1170 |
-
}
|
| 1171 |
-
|
| 1172 |
-
.workspace[data-view="coach"] .coach-debug-card .toggle-row,
|
| 1173 |
-
.workspace[data-view="coach"] .coach-debug-card .debug-rag-scope {
|
| 1174 |
-
flex-shrink: 0;
|
| 1175 |
-
}
|
| 1176 |
-
|
| 1177 |
-
.view-coach-only { display: none; }
|
| 1178 |
-
.workspace[data-view="coach"] .view-coach-only:not(.coach-panel-wrap) { display: block; }
|
| 1179 |
-
.workspace[data-view="coach"] .view-debug-only { display: none; }
|
| 1180 |
-
|
| 1181 |
.coach-card-head {
|
| 1182 |
margin-bottom: 0.85rem;
|
| 1183 |
}
|
|
@@ -1231,13 +1277,6 @@ body {
|
|
| 1231 |
}
|
| 1232 |
|
| 1233 |
@media (max-width: 960px) {
|
| 1234 |
-
.workspace[data-view="coach"] {
|
| 1235 |
-
grid-template-columns: 1fr;
|
| 1236 |
-
max-width: 640px;
|
| 1237 |
-
margin-left: auto;
|
| 1238 |
-
margin-right: auto;
|
| 1239 |
-
}
|
| 1240 |
-
|
| 1241 |
.coach-capture-row {
|
| 1242 |
grid-template-columns: 1fr;
|
| 1243 |
}
|
|
@@ -1297,13 +1336,17 @@ body {
|
|
| 1297 |
padding: 0.75rem;
|
| 1298 |
border: 1px solid var(--outline-variant);
|
| 1299 |
border-radius: 8px;
|
| 1300 |
-
background:
|
| 1301 |
font-size: 0.875rem;
|
| 1302 |
line-height: 1.5;
|
| 1303 |
max-height: 240px;
|
| 1304 |
overflow-y: auto;
|
| 1305 |
}
|
| 1306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
.debug-rag-scope {
|
| 1308 |
margin: 0.75rem 0;
|
| 1309 |
}
|
|
@@ -1399,6 +1442,14 @@ body {
|
|
| 1399 |
font-size: 0.875rem;
|
| 1400 |
}
|
| 1401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1402 |
.voice-replay-row {
|
| 1403 |
display: flex;
|
| 1404 |
flex-wrap: wrap;
|
|
@@ -1509,7 +1560,6 @@ body {
|
|
| 1509 |
margin-top: 0;
|
| 1510 |
}
|
| 1511 |
|
| 1512 |
-
.workspace[data-view="debug"] .view-coach-only { display: none; }
|
| 1513 |
.workspace[data-view="debug"] .view-debug-only { display: block; }
|
| 1514 |
|
| 1515 |
.workspace[data-view="debug"] .col-research,
|
|
|
|
| 5 |
--on-primary: #ffffff;
|
| 6 |
--background: #f7f9fb;
|
| 7 |
--surface: #f7f9fb;
|
| 8 |
+
--surface-elevated: #ffffff;
|
| 9 |
--surface-container-low: #f2f4f6;
|
| 10 |
--surface-container: #eceef0;
|
| 11 |
--secondary: #565e74;
|
|
|
|
| 16 |
--inverse-surface: #2d3133;
|
| 17 |
--inverse-on-surface: #eff1f3;
|
| 18 |
--error: #ba1a1a;
|
| 19 |
+
--banner-error-bg: #ffdad6;
|
| 20 |
+
--banner-error-text: #93000a;
|
| 21 |
+
--canvas-overlay-bg: rgba(247, 249, 251, 0.88);
|
| 22 |
+
--slide-canvas-bg: rgba(216, 218, 220, 0.15);
|
| 23 |
+
--hover-overlay: rgba(255, 255, 255, 0.65);
|
| 24 |
+
--badge-rag-text: #390c00;
|
| 25 |
+
--progress-done: #2d6a4f;
|
| 26 |
+
--input-focus-ring: rgba(168, 51, 0, 0.15);
|
| 27 |
--sidebar-w: 280px;
|
| 28 |
--topbar-h: 64px;
|
| 29 |
--context-bar-h: 72px;
|
|
|
|
| 32 |
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
| 33 |
}
|
| 34 |
|
| 35 |
+
[data-theme="dark"] {
|
| 36 |
+
color-scheme: dark;
|
| 37 |
+
--background: #121416;
|
| 38 |
+
--surface: #1a1c1e;
|
| 39 |
+
--surface-elevated: #242729;
|
| 40 |
+
--surface-container-low: #1e2022;
|
| 41 |
+
--surface-container: #2a2d30;
|
| 42 |
+
--secondary: #b8bcc8;
|
| 43 |
+
--secondary-container: #3d4454;
|
| 44 |
+
--on-secondary-container: #dae2fd;
|
| 45 |
+
--outline-variant: #4a403c;
|
| 46 |
+
--on-surface: #e3e2e6;
|
| 47 |
+
--primary-fixed: #5c2a12;
|
| 48 |
+
--banner-error-bg: #93000a;
|
| 49 |
+
--banner-error-text: #ffdad6;
|
| 50 |
+
--canvas-overlay-bg: rgba(18, 20, 22, 0.88);
|
| 51 |
+
--slide-canvas-bg: rgba(255, 255, 255, 0.04);
|
| 52 |
+
--hover-overlay: rgba(255, 255, 255, 0.06);
|
| 53 |
+
--badge-rag-text: #ffdbd0;
|
| 54 |
+
--progress-done: #6fcf97;
|
| 55 |
+
--input-focus-ring: rgba(203, 74, 24, 0.25);
|
| 56 |
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
* { box-sizing: border-box; }
|
| 60 |
|
| 61 |
body {
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
.studio-banner-loading { background: var(--secondary-container); color: var(--on-secondary-container); }
|
| 88 |
+
.studio-banner-error { background: var(--banner-error-bg); color: var(--banner-error-text); }
|
| 89 |
|
| 90 |
.sidebar {
|
| 91 |
position: fixed;
|
|
|
|
| 193 |
left: var(--sidebar-w);
|
| 194 |
right: 0;
|
| 195 |
z-index: 35;
|
| 196 |
+
background: var(--surface-elevated);
|
| 197 |
border-bottom: 1px solid var(--outline-variant);
|
| 198 |
padding: 0.5rem 1.5rem;
|
| 199 |
}
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
.card {
|
| 280 |
+
background: var(--surface-elevated);
|
| 281 |
border: 1px solid var(--outline-variant);
|
| 282 |
border-radius: var(--radius-xl);
|
| 283 |
padding: 1rem;
|
|
|
|
| 321 |
.btn:active { transform: scale(0.98); }
|
| 322 |
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 323 |
.btn-primary { background: var(--primary); color: var(--on-primary); }
|
| 324 |
+
.btn-secondary { background: var(--surface-elevated); border-color: var(--outline-variant); color: var(--on-surface); }
|
| 325 |
.btn-ghost { background: transparent; border-color: var(--outline-variant); color: var(--secondary); text-decoration: none; }
|
| 326 |
.btn-block { width: 100%; margin-top: 0.5rem; }
|
| 327 |
|
|
|
|
| 332 |
border-radius: 8px;
|
| 333 |
font: inherit;
|
| 334 |
font-size: 0.9rem;
|
| 335 |
+
background: var(--surface-elevated);
|
| 336 |
+
color: var(--on-surface);
|
| 337 |
}
|
| 338 |
|
| 339 |
.input:focus {
|
| 340 |
outline: none;
|
| 341 |
border-color: var(--primary);
|
| 342 |
+
box-shadow: 0 0 0 2px var(--input-focus-ring);
|
| 343 |
}
|
| 344 |
|
| 345 |
.upload-zone {
|
|
|
|
| 384 |
|
| 385 |
.controls-actions { display: flex; justify-content: flex-end; margin-top: 0.75rem; }
|
| 386 |
|
| 387 |
+
.region-loading-host,
|
| 388 |
+
.card-ingest,
|
| 389 |
+
.card-chat,
|
| 390 |
+
.voice-main-card,
|
| 391 |
+
.coach-panel-wrap,
|
| 392 |
+
.coach-debug-card,
|
| 393 |
+
.controls-panel,
|
| 394 |
+
.voice-rail-controls {
|
| 395 |
+
position: relative;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
.slide-canvas {
|
| 399 |
position: relative;
|
| 400 |
flex: 1;
|
| 401 |
min-height: 320px;
|
| 402 |
border: 2px dashed var(--outline-variant);
|
| 403 |
border-radius: var(--radius-xl);
|
| 404 |
+
background: var(--slide-canvas-bg);
|
| 405 |
overflow: auto;
|
| 406 |
padding: 1rem;
|
| 407 |
}
|
|
|
|
| 410 |
min-height: 280px;
|
| 411 |
}
|
| 412 |
|
| 413 |
+
.region-loading {
|
| 414 |
position: absolute;
|
| 415 |
inset: 0;
|
| 416 |
z-index: 5;
|
| 417 |
display: flex;
|
| 418 |
align-items: center;
|
| 419 |
justify-content: center;
|
| 420 |
+
background: transparent;
|
| 421 |
+
pointer-events: none;
|
|
|
|
| 422 |
}
|
| 423 |
|
| 424 |
+
.region-loading-inner {
|
| 425 |
+
display: flex;
|
| 426 |
+
flex-direction: column;
|
| 427 |
+
align-items: center;
|
| 428 |
+
gap: 0.5rem;
|
| 429 |
text-align: center;
|
|
|
|
| 430 |
}
|
| 431 |
|
| 432 |
+
.studio-spinner,
|
| 433 |
.canvas-spinner,
|
| 434 |
.lesson-running-spinner {
|
| 435 |
display: inline-block;
|
| 436 |
+
width: 32px;
|
| 437 |
+
height: 32px;
|
| 438 |
border: 3px solid var(--outline-variant);
|
| 439 |
border-top-color: var(--primary);
|
| 440 |
border-radius: 50%;
|
| 441 |
animation: studio-spin 0.9s linear infinite;
|
| 442 |
+
flex-shrink: 0;
|
| 443 |
}
|
| 444 |
|
| 445 |
+
.region-loading-text,
|
| 446 |
+
.canvas-overlay-text {
|
| 447 |
+
margin: 0;
|
| 448 |
font-size: 0.85rem;
|
| 449 |
+
font-weight: 500;
|
| 450 |
+
color: var(--secondary);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.region-loading-hint,
|
| 454 |
+
.canvas-overlay-hint {
|
| 455 |
+
margin: 0;
|
| 456 |
+
font-size: 0.78rem;
|
| 457 |
color: var(--secondary);
|
| 458 |
+
opacity: 0.85;
|
| 459 |
}
|
| 460 |
|
| 461 |
@keyframes studio-spin {
|
|
|
|
| 515 |
|
| 516 |
.progress-step.pending { color: var(--secondary); }
|
| 517 |
.progress-step.active { color: var(--primary); font-weight: 600; }
|
| 518 |
+
.progress-step.done { color: var(--progress-done); }
|
| 519 |
|
| 520 |
.progress-log {
|
| 521 |
margin: 0.75rem 0 0;
|
|
|
|
| 568 |
text-transform: uppercase;
|
| 569 |
}
|
| 570 |
|
| 571 |
+
.studio-badge-rag { background: var(--primary-fixed); color: var(--badge-rag-text); }
|
| 572 |
.studio-badge-muted { background: var(--surface-container); color: var(--secondary); }
|
| 573 |
|
| 574 |
.studio-doc-header { margin-bottom: 0.5rem; }
|
|
|
|
| 580 |
padding: 0.5rem;
|
| 581 |
border: 1px solid var(--outline-variant);
|
| 582 |
border-radius: 8px;
|
| 583 |
+
background: var(--surface-elevated);
|
| 584 |
}
|
| 585 |
|
| 586 |
.studio-doc-card:hover { border-color: var(--primary); }
|
|
|
|
| 608 |
padding: 0.6rem 0.75rem;
|
| 609 |
border: 1px solid var(--outline-variant);
|
| 610 |
border-radius: 8px;
|
| 611 |
+
background: var(--surface-elevated);
|
| 612 |
font: inherit;
|
| 613 |
cursor: pointer;
|
| 614 |
text-align: left;
|
| 615 |
+
color: var(--on-surface);
|
| 616 |
}
|
| 617 |
|
| 618 |
.mode-card.active {
|
|
|
|
| 929 |
border-radius: 6px;
|
| 930 |
}
|
| 931 |
|
| 932 |
+
.url-choice-item:hover { background: var(--hover-overlay); }
|
| 933 |
|
| 934 |
.url-choice-item span {
|
| 935 |
word-break: break-all;
|
|
|
|
| 1000 |
|
| 1001 |
.research-chat-assistant {
|
| 1002 |
align-self: flex-start;
|
| 1003 |
+
background: var(--surface-elevated);
|
| 1004 |
border: 1px solid var(--outline-variant);
|
| 1005 |
}
|
| 1006 |
|
|
|
|
| 1020 |
.workspace[data-view="voice"] .col-research,
|
| 1021 |
.workspace[data-view="voice"] .col-slides { display: none; }
|
| 1022 |
|
| 1023 |
+
.workspace[data-view="voice"] .col-debug { display: none; }
|
|
|
|
| 1024 |
|
| 1025 |
.view-voice-only { display: none; }
|
| 1026 |
|
|
|
|
| 1053 |
|
| 1054 |
.workspace[data-view="voice"] .voice-main {
|
| 1055 |
min-width: 0;
|
| 1056 |
+
display: flex;
|
| 1057 |
+
flex-direction: column;
|
| 1058 |
+
gap: 1rem;
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
.workspace[data-view="voice"] .voice-pitch-analysis {
|
| 1062 |
+
margin: 0;
|
| 1063 |
+
}
|
| 1064 |
+
|
| 1065 |
+
.workspace[data-view="voice"] .voice-pitch-analysis[open] .voice-pitch-summary {
|
| 1066 |
+
margin-bottom: 0.75rem;
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
.workspace[data-view="voice"] .voice-pitch-summary {
|
| 1070 |
+
cursor: pointer;
|
| 1071 |
+
list-style: none;
|
| 1072 |
+
display: flex;
|
| 1073 |
+
flex-direction: column;
|
| 1074 |
+
gap: 0.2rem;
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
.workspace[data-view="voice"] .voice-pitch-summary::-webkit-details-marker {
|
| 1078 |
+
display: none;
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
.workspace[data-view="voice"] .voice-pitch-summary-hint {
|
| 1082 |
+
font-size: 0.84rem;
|
| 1083 |
+
color: var(--secondary);
|
| 1084 |
+
font-weight: 400;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
.workspace[data-view="voice"] .voice-pitch-analysis .coach-panel-wrap {
|
| 1088 |
+
padding-top: 0.25rem;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
.workspace[data-view="voice"] .voice-discuss-btn {
|
| 1092 |
+
margin-top: 0.75rem;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
.workspace[data-view="voice"] .coach-results-panel {
|
| 1096 |
+
min-height: 80px;
|
| 1097 |
+
margin-top: 0.75rem;
|
| 1098 |
+
overflow-y: auto;
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
.workspace[data-view="voice"] .coach-results-panel:not(:empty) {
|
| 1102 |
+
border-top: 1px solid var(--outline-variant);
|
| 1103 |
+
padding-top: 0.75rem;
|
| 1104 |
}
|
| 1105 |
|
| 1106 |
.workspace[data-view="voice"] .voice-main-card {
|
|
|
|
| 1221 |
}
|
| 1222 |
}
|
| 1223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
.workspace[data-view="slides"] .col-studio,
|
| 1225 |
.workspace[data-view="research"] .col-debug { display: none; }
|
| 1226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1227 |
.coach-card-head {
|
| 1228 |
margin-bottom: 0.85rem;
|
| 1229 |
}
|
|
|
|
| 1277 |
}
|
| 1278 |
|
| 1279 |
@media (max-width: 960px) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1280 |
.coach-capture-row {
|
| 1281 |
grid-template-columns: 1fr;
|
| 1282 |
}
|
|
|
|
| 1336 |
padding: 0.75rem;
|
| 1337 |
border: 1px solid var(--outline-variant);
|
| 1338 |
border-radius: 8px;
|
| 1339 |
+
background: var(--surface-elevated);
|
| 1340 |
font-size: 0.875rem;
|
| 1341 |
line-height: 1.5;
|
| 1342 |
max-height: 240px;
|
| 1343 |
overflow-y: auto;
|
| 1344 |
}
|
| 1345 |
|
| 1346 |
+
.theme-toggle {
|
| 1347 |
+
margin-bottom: 0.25rem;
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
.debug-rag-scope {
|
| 1351 |
margin: 0.75rem 0;
|
| 1352 |
}
|
|
|
|
| 1442 |
font-size: 0.875rem;
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
+
.voice-rag-refs {
|
| 1446 |
+
margin-top: 0.65rem;
|
| 1447 |
+
padding-top: 0.55rem;
|
| 1448 |
+
border-top: 1px solid var(--outline-variant);
|
| 1449 |
+
font-size: 0.82rem;
|
| 1450 |
+
color: var(--on-surface-variant);
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
.voice-replay-row {
|
| 1454 |
display: flex;
|
| 1455 |
flex-wrap: wrap;
|
|
|
|
| 1560 |
margin-top: 0;
|
| 1561 |
}
|
| 1562 |
|
|
|
|
| 1563 |
.workspace[data-view="debug"] .view-debug-only { display: block; }
|
| 1564 |
|
| 1565 |
.workspace[data-view="debug"] .col-research,
|
apps/gradio-space/static/studio/studio.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
| 1 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.14.0/+esm";
|
| 2 |
|
| 3 |
const $ = (sel) => document.querySelector(sel);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const SLIDE_PIPELINE_STEPS = [
|
| 5 |
"Load language model",
|
| 6 |
"Gather lesson sources",
|
|
@@ -33,6 +57,7 @@ const state = {
|
|
| 33 |
browserRecordChunks: [],
|
| 34 |
pendingVoiceAudioPath: null,
|
| 35 |
pendingCoachAudioPath: null,
|
|
|
|
| 36 |
useBrowserMic: true,
|
| 37 |
};
|
| 38 |
|
|
@@ -249,14 +274,16 @@ async function discoverVoiceSources() {
|
|
| 249 |
showError("Set a focus or workspace topic before discovering sources.");
|
| 250 |
return;
|
| 251 |
}
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
async function autoVoiceIngest() {
|
|
@@ -265,46 +292,53 @@ async function autoVoiceIngest() {
|
|
| 265 |
showError("Set a focus or workspace topic before auto-ingest.");
|
| 266 |
return;
|
| 267 |
}
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
|
|
|
|
|
|
| 274 |
}
|
| 275 |
|
| 276 |
async function ingestVoiceSources() {
|
| 277 |
const topic = voiceEffectiveTopic();
|
| 278 |
const pasted = $("#voice-urls-text")?.value.trim() || "";
|
| 279 |
const selected = getSelectedDiscoveredUrls("#voice-url-choices-list");
|
| 280 |
-
const paths = [];
|
| 281 |
const files = $("#voice-ingest-file")?.files;
|
| 282 |
-
if (files?.length) {
|
| 283 |
-
for (const file of files) {
|
| 284 |
-
paths.push(await uploadFile(file));
|
| 285 |
-
}
|
| 286 |
-
}
|
| 287 |
-
if (!pasted && !selected.length && !paths.length) {
|
| 288 |
showError("Add URLs, select suggested sources, or upload a file — then ingest.");
|
| 289 |
return;
|
| 290 |
}
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
function syncVoiceModeUi() {
|
| 305 |
const ragMode = state.voiceMode === "explain" || state.voiceMode === "lesson";
|
|
|
|
| 306 |
$("#voice-topic-wrap")?.classList.toggle("hidden", !ragMode);
|
| 307 |
$("#voice-rag-sources")?.classList.toggle("hidden", !ragMode);
|
|
|
|
|
|
|
| 308 |
const placeholders = {
|
| 309 |
explain: "e.g. How does finetuning differ from pretraining?",
|
| 310 |
lesson: "What is the difference between pretraining and finetuning a small model?",
|
|
@@ -327,7 +361,10 @@ function renderVoiceChat() {
|
|
| 327 |
if (item && typeof item === "object" && item.role) {
|
| 328 |
const role = item.role === "user" ? "user" : "assistant";
|
| 329 |
const label = role === "user" ? "You" : "Teacher";
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
| 331 |
parts.push(
|
| 332 |
`<div class="research-chat-bubble research-chat-${role}"><div class="research-chat-role">${label}</div><div class="research-chat-body">${body}</div></div>`
|
| 333 |
);
|
|
@@ -396,15 +433,17 @@ async function discoverSources() {
|
|
| 396 |
showError("Set a workspace topic before discovering sources.");
|
| 397 |
return;
|
| 398 |
}
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
|
|
|
|
|
|
| 408 |
}
|
| 409 |
|
| 410 |
async function discoverSlideSources() {
|
|
@@ -413,8 +452,10 @@ async function discoverSlideSources() {
|
|
| 413 |
showError("Set a topic before discovering sources.");
|
| 414 |
return;
|
| 415 |
}
|
| 416 |
-
|
| 417 |
-
|
|
|
|
|
|
|
| 418 |
}
|
| 419 |
|
| 420 |
async function autoSearchIngest() {
|
|
@@ -423,12 +464,14 @@ async function autoSearchIngest() {
|
|
| 423 |
showError("Set a workspace topic before auto-ingest.");
|
| 424 |
return;
|
| 425 |
}
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
| 432 |
}
|
| 433 |
|
| 434 |
async function ingestSources({ urlsText = "", selectedUrls = [], pendingFiles = null } = {}) {
|
|
@@ -437,30 +480,32 @@ async function ingestSources({ urlsText = "", selectedUrls = [], pendingFiles =
|
|
| 437 |
let selected = selectedUrls;
|
| 438 |
if (workflow === "select") selected = getSelectedDiscoveredUrls();
|
| 439 |
const pasted = workflow === "direct" ? urlsText : urlsText || $("#ingest-url").value.trim();
|
| 440 |
-
const paths = [];
|
| 441 |
const files = pendingFiles || $("#ingest-file").files;
|
| 442 |
-
if (files?.length) {
|
| 443 |
-
for (const file of files) {
|
| 444 |
-
const b64 = await fileToBase64(file);
|
| 445 |
-
const saved = await callApi("save_upload", [file.name, b64]);
|
| 446 |
-
paths.push(saved.path);
|
| 447 |
-
}
|
| 448 |
-
}
|
| 449 |
-
if (!pasted && !selected.length && !paths.length) {
|
| 450 |
showError("Add URLs, select suggested sources, or upload a file — then ingest.");
|
| 451 |
return;
|
| 452 |
}
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
}
|
| 465 |
|
| 466 |
function renderResearchChat() {
|
|
@@ -512,18 +557,20 @@ async function askResearchQuestion() {
|
|
| 512 |
return;
|
| 513 |
}
|
| 514 |
const docIds = effectiveDocIds([]);
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
| 527 |
}
|
| 528 |
|
| 529 |
async function sendDebugMessage() {
|
|
@@ -537,23 +584,25 @@ async function sendDebugMessage() {
|
|
| 537 |
const debugDocIds = selectedDebugDocIds();
|
| 538 |
const workspaceDocIds = selectedWorkspaceDocIds();
|
| 539 |
const modelKey = $("#debug-model-key")?.value || "";
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
| 557 |
}
|
| 558 |
|
| 559 |
function effectiveDebugSessionId() {
|
|
@@ -651,10 +700,55 @@ async function getClient() {
|
|
| 651 |
return state.client;
|
| 652 |
}
|
| 653 |
|
|
|
|
|
|
|
| 654 |
function setLoading(on) {
|
|
|
|
| 655 |
$("#studio-loading").classList.toggle("hidden", !on);
|
| 656 |
}
|
| 657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
function startProgressPanel() {
|
| 659 |
const panel = $("#progress-panel");
|
| 660 |
const stepsEl = $("#progress-steps");
|
|
@@ -968,91 +1062,107 @@ async function generateSlides() {
|
|
| 968 |
const urlsText = $("#slide-urls-text")?.value.trim() || "";
|
| 969 |
const selectedUrls = getSelectedDiscoveredUrls("#slide-url-choices-list");
|
| 970 |
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
filePaths
|
| 976 |
-
|
| 977 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
<a href="${fileUrl(data.downloads.pptx)}" download>PPTX</a>
|
| 1038 |
<a href="${fileUrl(data.downloads.docx)}" download>DOCX</a>
|
| 1039 |
<a href="${fileUrl(data.downloads.html)}" download>HTML</a>`;
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
}
|
| 1053 |
|
| 1054 |
function renderVoiceReply(data, { keepAudio = false } = {}) {
|
| 1055 |
state.history = data.history ?? state.history;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
renderVoiceChat();
|
| 1057 |
if (data.status) {
|
| 1058 |
$("#voice-turn-status").textContent = stripMd(data.status);
|
|
@@ -1075,19 +1185,21 @@ async function sendVoiceTurn() {
|
|
| 1075 |
const useRag = voiceUseRag();
|
| 1076 |
const docIds = effectiveDocIds([]);
|
| 1077 |
const language = state.voicePresets?.default_language || "en";
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
|
|
|
|
|
|
| 1091 |
}
|
| 1092 |
|
| 1093 |
async function sendVoiceAudioTurn(audioPath) {
|
|
@@ -1096,19 +1208,21 @@ async function sendVoiceAudioTurn(audioPath) {
|
|
| 1096 |
const docIds = effectiveDocIds([]);
|
| 1097 |
const language = state.voicePresets?.default_language || "en";
|
| 1098 |
const asr = state.voicePresets?.default_asr || null;
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
|
|
|
| 1112 |
}
|
| 1113 |
|
| 1114 |
async function speakVoiceReply(firstSentenceOnly) {
|
|
@@ -1139,13 +1253,36 @@ async function analyzePitchWithPath(audioPath) {
|
|
| 1139 |
const language = $("#coach-language")?.value || "en";
|
| 1140 |
const asr = $("#coach-asr")?.value || null;
|
| 1141 |
const speakRewrite = $("#coach-speak-rewrite")?.checked || false;
|
| 1142 |
-
$("#
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
async function analyzePitch() {
|
|
@@ -1265,6 +1402,8 @@ function bindUi() {
|
|
| 1265 |
$("#btn-open-settings")?.addEventListener("click", openSettingsDrawer);
|
| 1266 |
$("#btn-close-settings")?.addEventListener("click", closeSettingsDrawer);
|
| 1267 |
$("#settings-backdrop")?.addEventListener("click", closeSettingsDrawer);
|
|
|
|
|
|
|
| 1268 |
$("#btn-reload-model")?.addEventListener("click", () => reloadModelFromSettings().catch(() => {}));
|
| 1269 |
|
| 1270 |
$("#btn-open-research-view")?.addEventListener("click", openResearchView);
|
|
@@ -1375,6 +1514,8 @@ function bindUi() {
|
|
| 1375 |
syncVoiceModeUi();
|
| 1376 |
});
|
| 1377 |
});
|
|
|
|
|
|
|
| 1378 |
}
|
| 1379 |
|
| 1380 |
bindUi();
|
|
|
|
| 1 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.14.0/+esm";
|
| 2 |
|
| 3 |
const $ = (sel) => document.querySelector(sel);
|
| 4 |
+
const THEME_KEY = "studio-theme";
|
| 5 |
+
|
| 6 |
+
function getPreferredTheme() {
|
| 7 |
+
const saved = localStorage.getItem(THEME_KEY);
|
| 8 |
+
if (saved === "light" || saved === "dark") return saved;
|
| 9 |
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
function applyTheme(theme) {
|
| 13 |
+
document.documentElement.dataset.theme = theme === "dark" ? "dark" : "light";
|
| 14 |
+
localStorage.setItem(THEME_KEY, theme);
|
| 15 |
+
const icon = $("#theme-icon");
|
| 16 |
+
if (icon) icon.textContent = theme === "dark" ? "light_mode" : "dark_mode";
|
| 17 |
+
const checkbox = $("#theme-toggle");
|
| 18 |
+
if (checkbox) checkbox.checked = theme === "dark";
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function toggleTheme() {
|
| 22 |
+
const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
| 23 |
+
applyTheme(current === "dark" ? "light" : "dark");
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
applyTheme(getPreferredTheme());
|
| 27 |
+
|
| 28 |
const SLIDE_PIPELINE_STEPS = [
|
| 29 |
"Load language model",
|
| 30 |
"Gather lesson sources",
|
|
|
|
| 57 |
browserRecordChunks: [],
|
| 58 |
pendingVoiceAudioPath: null,
|
| 59 |
pendingCoachAudioPath: null,
|
| 60 |
+
lastPitchAnalysis: null,
|
| 61 |
useBrowserMic: true,
|
| 62 |
};
|
| 63 |
|
|
|
|
| 274 |
showError("Set a focus or workspace topic before discovering sources.");
|
| 275 |
return;
|
| 276 |
}
|
| 277 |
+
await withRegionLoading($(".voice-rail-controls"), "Discovering sources…", async () => {
|
| 278 |
+
const data = await callApi("discover_sources", [topic, state.workspaceSessionId]);
|
| 279 |
+
$("#voice-ingest-status").textContent = stripMd(data.status || "Discovery complete.");
|
| 280 |
+
renderVoiceUrlChoices(data.urls || [], data.selected_urls || data.urls || []);
|
| 281 |
+
if (data.session_id) {
|
| 282 |
+
state.workspaceSessionId = data.session_id;
|
| 283 |
+
$("#workspace-session").value = data.session_id;
|
| 284 |
+
}
|
| 285 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 286 |
+
});
|
| 287 |
}
|
| 288 |
|
| 289 |
async function autoVoiceIngest() {
|
|
|
|
| 292 |
showError("Set a focus or workspace topic before auto-ingest.");
|
| 293 |
return;
|
| 294 |
}
|
| 295 |
+
await withRegionLoading($(".voice-rail-controls"), "Auto-ingesting sources…", async () => {
|
| 296 |
+
const data = await callApi("auto_search_ingest", [topic, state.workspaceSessionId]);
|
| 297 |
+
applyVoiceIngestResult(data);
|
| 298 |
+
state.voiceDiscoveredUrls = [];
|
| 299 |
+
state.voiceSelectedUrls = [];
|
| 300 |
+
renderVoiceUrlChoices([], []);
|
| 301 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 302 |
+
});
|
| 303 |
}
|
| 304 |
|
| 305 |
async function ingestVoiceSources() {
|
| 306 |
const topic = voiceEffectiveTopic();
|
| 307 |
const pasted = $("#voice-urls-text")?.value.trim() || "";
|
| 308 |
const selected = getSelectedDiscoveredUrls("#voice-url-choices-list");
|
|
|
|
| 309 |
const files = $("#voice-ingest-file")?.files;
|
| 310 |
+
if (!pasted && !selected.length && !files?.length) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
showError("Add URLs, select suggested sources, or upload a file — then ingest.");
|
| 312 |
return;
|
| 313 |
}
|
| 314 |
+
await withRegionLoading($(".voice-rail-controls"), "Ingesting sources…", async () => {
|
| 315 |
+
const paths = [];
|
| 316 |
+
if (files?.length) {
|
| 317 |
+
for (const file of files) {
|
| 318 |
+
paths.push(await uploadFile(file));
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
const data = await callApi("ingest_sources", [
|
| 322 |
+
topic,
|
| 323 |
+
state.workspaceSessionId,
|
| 324 |
+
pasted,
|
| 325 |
+
selected,
|
| 326 |
+
paths,
|
| 327 |
+
]);
|
| 328 |
+
applyVoiceIngestResult(data);
|
| 329 |
+
if (pasted) $("#voice-urls-text").value = "";
|
| 330 |
+
if (files?.length) $("#voice-ingest-file").value = "";
|
| 331 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 332 |
+
});
|
| 333 |
}
|
| 334 |
|
| 335 |
function syncVoiceModeUi() {
|
| 336 |
const ragMode = state.voiceMode === "explain" || state.voiceMode === "lesson";
|
| 337 |
+
const practiceMode = state.voiceMode === "pitch";
|
| 338 |
$("#voice-topic-wrap")?.classList.toggle("hidden", !ragMode);
|
| 339 |
$("#voice-rag-sources")?.classList.toggle("hidden", !ragMode);
|
| 340 |
+
$(".voice-rag-card")?.classList.toggle("hidden", practiceMode);
|
| 341 |
+
$("#voice-pitch-analysis")?.classList.toggle("hidden", !practiceMode);
|
| 342 |
const placeholders = {
|
| 343 |
explain: "e.g. How does finetuning differ from pretraining?",
|
| 344 |
lesson: "What is the difference between pretraining and finetuning a small model?",
|
|
|
|
| 361 |
if (item && typeof item === "object" && item.role) {
|
| 362 |
const role = item.role === "user" ? "user" : "assistant";
|
| 363 |
const label = role === "user" ? "You" : "Teacher";
|
| 364 |
+
let body = renderMarkdownLite(voiceMessageText(item.content));
|
| 365 |
+
if (role === "assistant" && item.rag_references) {
|
| 366 |
+
body += `<div class="voice-rag-refs">${renderMarkdownLite(item.rag_references)}</div>`;
|
| 367 |
+
}
|
| 368 |
parts.push(
|
| 369 |
`<div class="research-chat-bubble research-chat-${role}"><div class="research-chat-role">${label}</div><div class="research-chat-body">${body}</div></div>`
|
| 370 |
);
|
|
|
|
| 433 |
showError("Set a workspace topic before discovering sources.");
|
| 434 |
return;
|
| 435 |
}
|
| 436 |
+
await withRegionLoading($(".card-ingest"), "Discovering sources…", async () => {
|
| 437 |
+
const data = await callApi("discover_sources", [topic, state.workspaceSessionId]);
|
| 438 |
+
$("#ingest-status").textContent = stripMd(data.status || "Discovery complete.");
|
| 439 |
+
renderResearchUrlChoices(data.urls || [], data.selected_urls || data.urls || []);
|
| 440 |
+
if (data.session_id) {
|
| 441 |
+
state.workspaceSessionId = data.session_id;
|
| 442 |
+
$("#workspace-session").value = data.session_id;
|
| 443 |
+
}
|
| 444 |
+
setTracePanel("#research-trace-panel", data);
|
| 445 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 446 |
+
});
|
| 447 |
}
|
| 448 |
|
| 449 |
async function discoverSlideSources() {
|
|
|
|
| 452 |
showError("Set a topic before discovering sources.");
|
| 453 |
return;
|
| 454 |
}
|
| 455 |
+
await withRegionLoading($(".controls-panel"), "Discovering sources…", async () => {
|
| 456 |
+
const data = await callApi("discover_sources", [topic, state.workspaceSessionId]);
|
| 457 |
+
renderSlideUrlChoices(data.urls || [], data.selected_urls || data.urls || []);
|
| 458 |
+
});
|
| 459 |
}
|
| 460 |
|
| 461 |
async function autoSearchIngest() {
|
|
|
|
| 464 |
showError("Set a workspace topic before auto-ingest.");
|
| 465 |
return;
|
| 466 |
}
|
| 467 |
+
await withRegionLoading($(".card-ingest"), "Auto-ingesting sources…", async () => {
|
| 468 |
+
const data = await callApi("auto_search_ingest", [topic, state.workspaceSessionId]);
|
| 469 |
+
applyIngestResult(data);
|
| 470 |
+
state.discoveredUrls = [];
|
| 471 |
+
state.selectedUrls = [];
|
| 472 |
+
renderResearchUrlChoices([], []);
|
| 473 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 474 |
+
});
|
| 475 |
}
|
| 476 |
|
| 477 |
async function ingestSources({ urlsText = "", selectedUrls = [], pendingFiles = null } = {}) {
|
|
|
|
| 480 |
let selected = selectedUrls;
|
| 481 |
if (workflow === "select") selected = getSelectedDiscoveredUrls();
|
| 482 |
const pasted = workflow === "direct" ? urlsText : urlsText || $("#ingest-url").value.trim();
|
|
|
|
| 483 |
const files = pendingFiles || $("#ingest-file").files;
|
| 484 |
+
if (!pasted && !selected.length && !files?.length) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
showError("Add URLs, select suggested sources, or upload a file — then ingest.");
|
| 486 |
return;
|
| 487 |
}
|
| 488 |
+
await withRegionLoading($(".card-ingest"), "Ingesting sources…", async () => {
|
| 489 |
+
const paths = [];
|
| 490 |
+
if (files?.length) {
|
| 491 |
+
for (const file of files) {
|
| 492 |
+
const b64 = await fileToBase64(file);
|
| 493 |
+
const saved = await callApi("save_upload", [file.name, b64]);
|
| 494 |
+
paths.push(saved.path);
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
const data = await callApi("ingest_sources", [
|
| 498 |
+
topic,
|
| 499 |
+
state.workspaceSessionId,
|
| 500 |
+
pasted,
|
| 501 |
+
selected,
|
| 502 |
+
paths,
|
| 503 |
+
]);
|
| 504 |
+
applyIngestResult(data);
|
| 505 |
+
if (pasted) $("#ingest-url").value = "";
|
| 506 |
+
if (files?.length) $("#ingest-file").value = "";
|
| 507 |
+
await refreshWorkspaceSessions(state.workspaceSessionId);
|
| 508 |
+
});
|
| 509 |
}
|
| 510 |
|
| 511 |
function renderResearchChat() {
|
|
|
|
| 557 |
return;
|
| 558 |
}
|
| 559 |
const docIds = effectiveDocIds([]);
|
| 560 |
+
await withRegionLoading($("#research-chat-panel .card-chat"), "Searching sources…", async () => {
|
| 561 |
+
const data = await callApi("research_chat", [
|
| 562 |
+
question,
|
| 563 |
+
state.workspaceSessionId,
|
| 564 |
+
docIds,
|
| 565 |
+
state.researchChatHistory,
|
| 566 |
+
]);
|
| 567 |
+
state.researchChatHistory = data.history || [];
|
| 568 |
+
renderResearchChat();
|
| 569 |
+
$("#research-question").value = "";
|
| 570 |
+
$("#research-chat-status").textContent = stripMd(data.rag_hint || "");
|
| 571 |
+
setTracePanel("#research-trace-panel", data);
|
| 572 |
+
updateResearchRagBadge();
|
| 573 |
+
});
|
| 574 |
}
|
| 575 |
|
| 576 |
async function sendDebugMessage() {
|
|
|
|
| 584 |
const debugDocIds = selectedDebugDocIds();
|
| 585 |
const workspaceDocIds = selectedWorkspaceDocIds();
|
| 586 |
const modelKey = $("#debug-model-key")?.value || "";
|
| 587 |
+
await withRegionLoading($(".coach-debug-card"), "Thinking…", async () => {
|
| 588 |
+
const data = await callApi("debug_chat", [
|
| 589 |
+
message,
|
| 590 |
+
state.debugChatHistory,
|
| 591 |
+
useRag,
|
| 592 |
+
debugSession,
|
| 593 |
+
debugDocIds,
|
| 594 |
+
modelKey,
|
| 595 |
+
state.workspaceSessionId,
|
| 596 |
+
workspaceDocIds,
|
| 597 |
+
]);
|
| 598 |
+
state.debugChatHistory = data.history || [];
|
| 599 |
+
renderDebugChat();
|
| 600 |
+
$("#debug-message").value = "";
|
| 601 |
+
if (data.rag_hint) {
|
| 602 |
+
$("#debug-rag-hint").textContent = stripMd(data.rag_hint);
|
| 603 |
+
}
|
| 604 |
+
setTracePanel("#debug-trace-panel", data);
|
| 605 |
+
});
|
| 606 |
}
|
| 607 |
|
| 608 |
function effectiveDebugSessionId() {
|
|
|
|
| 700 |
return state.client;
|
| 701 |
}
|
| 702 |
|
| 703 |
+
let globalLoadingSuppress = 0;
|
| 704 |
+
|
| 705 |
function setLoading(on) {
|
| 706 |
+
if (on && globalLoadingSuppress > 0) return;
|
| 707 |
$("#studio-loading").classList.toggle("hidden", !on);
|
| 708 |
}
|
| 709 |
|
| 710 |
+
function setRegionLoading(container, on, message = "Working…", { overlayEl = null, hint = "" } = {}) {
|
| 711 |
+
if (!container) return;
|
| 712 |
+
let overlay = overlayEl || container.querySelector(":scope > .region-loading");
|
| 713 |
+
if (!overlay) {
|
| 714 |
+
overlay = document.createElement("div");
|
| 715 |
+
overlay.className = "region-loading hidden";
|
| 716 |
+
overlay.setAttribute("aria-live", "polite");
|
| 717 |
+
overlay.innerHTML = `
|
| 718 |
+
<div class="region-loading-inner">
|
| 719 |
+
<span class="studio-spinner" aria-hidden="true"></span>
|
| 720 |
+
<p class="region-loading-text"></p>
|
| 721 |
+
<p class="region-loading-hint hidden"></p>
|
| 722 |
+
</div>`;
|
| 723 |
+
container.insertBefore(overlay, container.firstChild);
|
| 724 |
+
if (getComputedStyle(container).position === "static") {
|
| 725 |
+
container.classList.add("region-loading-host");
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
const textEl =
|
| 729 |
+
overlay.querySelector(".region-loading-text") || overlay.querySelector("#canvas-overlay-text");
|
| 730 |
+
if (textEl) textEl.textContent = message;
|
| 731 |
+
const hintEl =
|
| 732 |
+
overlay.querySelector(".region-loading-hint") || overlay.querySelector(".canvas-overlay-hint");
|
| 733 |
+
if (hintEl) {
|
| 734 |
+
hintEl.textContent = hint;
|
| 735 |
+
hintEl.classList.toggle("hidden", !hint);
|
| 736 |
+
}
|
| 737 |
+
overlay.classList.toggle("hidden", !on);
|
| 738 |
+
container.setAttribute("aria-busy", on ? "true" : "false");
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
async function withRegionLoading(container, message, fn, options = {}) {
|
| 742 |
+
globalLoadingSuppress += 1;
|
| 743 |
+
setRegionLoading(container, true, message, options);
|
| 744 |
+
try {
|
| 745 |
+
return await fn();
|
| 746 |
+
} finally {
|
| 747 |
+
globalLoadingSuppress -= 1;
|
| 748 |
+
setRegionLoading(container, false, message, options);
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
function startProgressPanel() {
|
| 753 |
const panel = $("#progress-panel");
|
| 754 |
const stepsEl = $("#progress-steps");
|
|
|
|
| 1062 |
const urlsText = $("#slide-urls-text")?.value.trim() || "";
|
| 1063 |
const selectedUrls = getSelectedDiscoveredUrls("#slide-url-choices-list");
|
| 1064 |
|
| 1065 |
+
await withRegionLoading(
|
| 1066 |
+
$("#slide-canvas"),
|
| 1067 |
+
"Generating slides…",
|
| 1068 |
+
async () => {
|
| 1069 |
+
const filePaths = [];
|
| 1070 |
+
const slideFiles = $("#slide-source-files")?.files;
|
| 1071 |
+
if (slideFiles?.length) {
|
| 1072 |
+
for (const file of slideFiles) {
|
| 1073 |
+
filePaths.push(await uploadFile(file));
|
| 1074 |
+
}
|
| 1075 |
+
}
|
| 1076 |
|
| 1077 |
+
startProgressPanel();
|
| 1078 |
+
const waitTimer = advanceProgressWhileWaiting();
|
| 1079 |
+
let data;
|
| 1080 |
+
try {
|
| 1081 |
+
data = await callApi("generate_slides", [
|
| 1082 |
+
topic,
|
| 1083 |
+
grade,
|
| 1084 |
+
slideCount,
|
| 1085 |
+
state.workspaceSessionId,
|
| 1086 |
+
useRag,
|
| 1087 |
+
docIds,
|
| 1088 |
+
sourceMode,
|
| 1089 |
+
searchWorkflow,
|
| 1090 |
+
urlsText,
|
| 1091 |
+
selectedUrls,
|
| 1092 |
+
filePaths,
|
| 1093 |
+
]);
|
| 1094 |
+
} catch (_err) {
|
| 1095 |
+
$("#progress-eta").textContent = "Failed";
|
| 1096 |
+
throw _err;
|
| 1097 |
+
} finally {
|
| 1098 |
+
clearInterval(waitTimer);
|
| 1099 |
+
if (state.progressTimer) {
|
| 1100 |
+
clearInterval(state.progressTimer);
|
| 1101 |
+
state.progressTimer = null;
|
| 1102 |
+
}
|
| 1103 |
+
}
|
| 1104 |
|
| 1105 |
+
finishProgressPanel(data);
|
| 1106 |
+
$("#generate-status").textContent = stripMd(data.status || "Slides generated.");
|
| 1107 |
+
const canvasHtml =
|
| 1108 |
+
data.canvas_html ||
|
| 1109 |
+
(data.preview_html ? `<div class="studio-canvas-inner">${data.preview_html}</div>` : "");
|
| 1110 |
+
$("#slide-canvas-content").innerHTML =
|
| 1111 |
+
canvasHtml || '<div class="studio-canvas-empty"><p>Preview unavailable.</p></div>';
|
| 1112 |
+
|
| 1113 |
+
const galleryEl = $("#slide-gallery");
|
| 1114 |
+
if (data.gallery_html) {
|
| 1115 |
+
galleryEl.innerHTML = data.gallery_html;
|
| 1116 |
+
galleryEl.classList.remove("hidden");
|
| 1117 |
+
} else if (data.gallery?.length) {
|
| 1118 |
+
galleryEl.innerHTML = data.gallery
|
| 1119 |
+
.map(
|
| 1120 |
+
(path, i) =>
|
| 1121 |
+
`<a class="studio-gallery-item" href="${fileUrl(path)}" target="_blank" rel="noopener"><img src="${fileUrl(path)}" alt="Slide ${i + 1}" loading="lazy" /></a>`
|
| 1122 |
+
)
|
| 1123 |
+
.join("");
|
| 1124 |
+
galleryEl.classList.remove("hidden");
|
| 1125 |
+
} else {
|
| 1126 |
+
galleryEl.classList.add("hidden");
|
| 1127 |
+
galleryEl.innerHTML = "";
|
| 1128 |
+
}
|
| 1129 |
|
| 1130 |
+
state.downloads = data.downloads;
|
| 1131 |
+
const dl = $("#downloads");
|
| 1132 |
+
if (data.downloads?.pptx) {
|
| 1133 |
+
dl.classList.remove("hidden");
|
| 1134 |
+
dl.innerHTML = `
|
| 1135 |
<a href="${fileUrl(data.downloads.pptx)}" download>PPTX</a>
|
| 1136 |
<a href="${fileUrl(data.downloads.docx)}" download>DOCX</a>
|
| 1137 |
<a href="${fileUrl(data.downloads.html)}" download>HTML</a>`;
|
| 1138 |
+
$("#btn-export").disabled = false;
|
| 1139 |
+
}
|
| 1140 |
|
| 1141 |
+
const outlineDetails = $("#slide-outline-details");
|
| 1142 |
+
const outlineEl = $("#slide-outline");
|
| 1143 |
+
if (data.outline_md) {
|
| 1144 |
+
outlineEl.innerHTML = renderMarkdownLite(data.outline_md);
|
| 1145 |
+
outlineDetails?.classList.remove("hidden");
|
| 1146 |
+
} else {
|
| 1147 |
+
outlineEl.innerHTML = "";
|
| 1148 |
+
outlineDetails?.classList.add("hidden");
|
| 1149 |
+
}
|
| 1150 |
+
},
|
| 1151 |
+
{
|
| 1152 |
+
overlayEl: $("#canvas-overlay"),
|
| 1153 |
+
hint: "Local CPU models often take 30–90 seconds.",
|
| 1154 |
+
}
|
| 1155 |
+
);
|
| 1156 |
}
|
| 1157 |
|
| 1158 |
function renderVoiceReply(data, { keepAudio = false } = {}) {
|
| 1159 |
state.history = data.history ?? state.history;
|
| 1160 |
+
if (data.rag_references && state.history.length) {
|
| 1161 |
+
const last = state.history[state.history.length - 1];
|
| 1162 |
+
if (last && typeof last === "object" && last.role === "assistant") {
|
| 1163 |
+
last.rag_references = data.rag_references;
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
renderVoiceChat();
|
| 1167 |
if (data.status) {
|
| 1168 |
$("#voice-turn-status").textContent = stripMd(data.status);
|
|
|
|
| 1185 |
const useRag = voiceUseRag();
|
| 1186 |
const docIds = effectiveDocIds([]);
|
| 1187 |
const language = state.voicePresets?.default_language || "en";
|
| 1188 |
+
await withRegionLoading($(".voice-main-card"), "Teacher is thinking…", async () => {
|
| 1189 |
+
const data = await callApi("teacher_voice_turn", [
|
| 1190 |
+
message,
|
| 1191 |
+
state.voiceMode,
|
| 1192 |
+
topic,
|
| 1193 |
+
state.workspaceSessionId,
|
| 1194 |
+
useRag,
|
| 1195 |
+
state.history,
|
| 1196 |
+
docIds,
|
| 1197 |
+
language,
|
| 1198 |
+
null,
|
| 1199 |
+
]);
|
| 1200 |
+
$("#voice-message").value = "";
|
| 1201 |
+
renderVoiceReply(data);
|
| 1202 |
+
});
|
| 1203 |
}
|
| 1204 |
|
| 1205 |
async function sendVoiceAudioTurn(audioPath) {
|
|
|
|
| 1208 |
const docIds = effectiveDocIds([]);
|
| 1209 |
const language = state.voicePresets?.default_language || "en";
|
| 1210 |
const asr = state.voicePresets?.default_asr || null;
|
| 1211 |
+
await withRegionLoading($(".voice-main-card"), "Processing voice…", async () => {
|
| 1212 |
+
const data = await callApi("teacher_voice_audio_turn", [
|
| 1213 |
+
audioPath,
|
| 1214 |
+
state.voiceMode,
|
| 1215 |
+
topic,
|
| 1216 |
+
state.workspaceSessionId,
|
| 1217 |
+
useRag,
|
| 1218 |
+
state.history,
|
| 1219 |
+
docIds,
|
| 1220 |
+
language,
|
| 1221 |
+
asr,
|
| 1222 |
+
]);
|
| 1223 |
+
if (data.user_text) $("#voice-message").value = data.user_text;
|
| 1224 |
+
renderVoiceReply(data);
|
| 1225 |
+
});
|
| 1226 |
}
|
| 1227 |
|
| 1228 |
async function speakVoiceReply(firstSentenceOnly) {
|
|
|
|
| 1253 |
const language = $("#coach-language")?.value || "en";
|
| 1254 |
const asr = $("#coach-asr")?.value || null;
|
| 1255 |
const speakRewrite = $("#coach-speak-rewrite")?.checked || false;
|
| 1256 |
+
await withRegionLoading($("#voice-pitch-analysis"), "Analyzing pitch…", async () => {
|
| 1257 |
+
const data = await callApi("analyze_pitch", [audioPath, language, asr, speakRewrite]);
|
| 1258 |
+
state.lastPitchAnalysis = data;
|
| 1259 |
+
const panel = $("#coach-panel");
|
| 1260 |
+
panel.innerHTML = data.coach_panel_html || "";
|
| 1261 |
+
const discussBtn = document.createElement("button");
|
| 1262 |
+
discussBtn.type = "button";
|
| 1263 |
+
discussBtn.className = "btn btn-secondary voice-discuss-btn";
|
| 1264 |
+
discussBtn.textContent = "Discuss in chat";
|
| 1265 |
+
discussBtn.addEventListener("click", () => discussPitchInChat().catch(() => {}));
|
| 1266 |
+
if (data.transcript_html || data.report_md || data.tip) {
|
| 1267 |
+
panel.appendChild(discussBtn);
|
| 1268 |
+
}
|
| 1269 |
+
});
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
function discussPitchInChat() {
|
| 1273 |
+
const data = state.lastPitchAnalysis;
|
| 1274 |
+
if (!data) return;
|
| 1275 |
+
const parts = [];
|
| 1276 |
+
if (data.tip) parts.push(`Coach tip: ${stripMd(data.tip)}`);
|
| 1277 |
+
if (data.report_md) parts.push(stripMd(data.report_md).slice(0, 800));
|
| 1278 |
+
const prompt =
|
| 1279 |
+
parts.length > 0
|
| 1280 |
+
? `Here is my pitch analysis. Help me improve based on this feedback:\n\n${parts.join("\n\n")}`
|
| 1281 |
+
: "I just ran pitch analysis — what should I work on next?";
|
| 1282 |
+
$("#voice-message").value = prompt;
|
| 1283 |
+
$("#voice-message").focus();
|
| 1284 |
+
const chat = $("#voice-chat-messages");
|
| 1285 |
+
if (chat) chat.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
| 1286 |
}
|
| 1287 |
|
| 1288 |
async function analyzePitch() {
|
|
|
|
| 1402 |
$("#btn-open-settings")?.addEventListener("click", openSettingsDrawer);
|
| 1403 |
$("#btn-close-settings")?.addEventListener("click", closeSettingsDrawer);
|
| 1404 |
$("#settings-backdrop")?.addEventListener("click", closeSettingsDrawer);
|
| 1405 |
+
$("#theme-toggle")?.addEventListener("change", toggleTheme);
|
| 1406 |
+
$("#theme-toggle-btn")?.addEventListener("click", toggleTheme);
|
| 1407 |
$("#btn-reload-model")?.addEventListener("click", () => reloadModelFromSettings().catch(() => {}));
|
| 1408 |
|
| 1409 |
$("#btn-open-research-view")?.addEventListener("click", openResearchView);
|
|
|
|
| 1514 |
syncVoiceModeUi();
|
| 1515 |
});
|
| 1516 |
});
|
| 1517 |
+
|
| 1518 |
+
syncVoiceModeUi();
|
| 1519 |
}
|
| 1520 |
|
| 1521 |
bindUi();
|
libs/agent/src/agent/research_prompts.py
CHANGED
|
@@ -18,6 +18,8 @@ def research_answer_system(skill_body: str, skill_path: Path) -> str:
|
|
| 18 |
"Each context block is numbered [1], [2], … — one number per source document.",
|
| 19 |
"Cite with those numbers only (e.g. [1]). Use at most a few citations per answer.",
|
| 20 |
"Ignore any [n] markers inside source text; never list citation numbers in a row.",
|
|
|
|
|
|
|
| 21 |
skill_body,
|
| 22 |
]
|
| 23 |
if citation_ref:
|
|
@@ -33,4 +35,5 @@ Question: {question}
|
|
| 33 |
|
| 34 |
Write a concise answer with inline [n] citations (one index per source document).
|
| 35 |
Do not append a References section — it is added automatically.
|
| 36 |
-
|
|
|
|
|
|
| 18 |
"Each context block is numbered [1], [2], … — one number per source document.",
|
| 19 |
"Cite with those numbers only (e.g. [1]). Use at most a few citations per answer.",
|
| 20 |
"Ignore any [n] markers inside source text; never list citation numbers in a row.",
|
| 21 |
+
"Write the final answer directly in 2–4 sentences. Do not use thinking tags, "
|
| 22 |
+
"chain-of-thought, or planning text.",
|
| 23 |
skill_body,
|
| 24 |
]
|
| 25 |
if citation_ref:
|
|
|
|
| 35 |
|
| 36 |
Write a concise answer with inline [n] citations (one index per source document).
|
| 37 |
Do not append a References section — it is added automatically.
|
| 38 |
+
Do not use redacted_thinking, think tags, or step-by-step planning — only the answer.
|
| 39 |
+
If context is insufficient, say so in one sentence."""
|
libs/agent/src/agent/runner.py
CHANGED
|
@@ -906,7 +906,13 @@ class AgentRunner:
|
|
| 906 |
model=model_key,
|
| 907 |
user_input=req.model_dump(),
|
| 908 |
)
|
|
|
|
| 909 |
backend.load()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
|
| 911 |
answer_tool = self._tools.get("research_answer")
|
| 912 |
raw_answer, citations, refs = answer_tool.handler(
|
|
@@ -916,6 +922,7 @@ class AgentRunner:
|
|
| 916 |
skill_path=skill.path,
|
| 917 |
session_id=req.session_id,
|
| 918 |
doc_ids=req.doc_ids or None,
|
|
|
|
| 919 |
)
|
| 920 |
trace.log_llm(req.question, raw_answer)
|
| 921 |
trace.log_note(
|
|
|
|
| 906 |
model=model_key,
|
| 907 |
user_input=req.model_dump(),
|
| 908 |
)
|
| 909 |
+
load_started = monotonic()
|
| 910 |
backend.load()
|
| 911 |
+
trace.log_step(
|
| 912 |
+
"load_model",
|
| 913 |
+
"Load language model",
|
| 914 |
+
duration_ms=int((monotonic() - load_started) * 1000),
|
| 915 |
+
)
|
| 916 |
|
| 917 |
answer_tool = self._tools.get("research_answer")
|
| 918 |
raw_answer, citations, refs = answer_tool.handler(
|
|
|
|
| 922 |
skill_path=skill.path,
|
| 923 |
session_id=req.session_id,
|
| 924 |
doc_ids=req.doc_ids or None,
|
| 925 |
+
trace=trace,
|
| 926 |
)
|
| 927 |
trace.log_llm(req.question, raw_answer)
|
| 928 |
trace.log_note(
|
libs/agent/src/agent/tools/research_tools.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from pathlib import Path
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
from researchmind.citations import Citation, clean_model_answer, format_context_block, format_references
|
|
@@ -51,10 +52,12 @@ def tool_research_answer(
|
|
| 51 |
skill_path: Path,
|
| 52 |
session_id: str | None = None,
|
| 53 |
doc_ids: list[str] | None = None,
|
|
|
|
| 54 |
) -> tuple[str, list[Citation], str]:
|
| 55 |
cfg = get_config()
|
| 56 |
store = get_store()
|
| 57 |
scope_session, scope_docs = resolve_retrieve_scope(session_id, doc_ids)
|
|
|
|
| 58 |
chunks = retrieve(
|
| 59 |
question,
|
| 60 |
store,
|
|
@@ -62,6 +65,15 @@ def tool_research_answer(
|
|
| 62 |
session_id=scope_session,
|
| 63 |
doc_ids=scope_docs,
|
| 64 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if not chunks:
|
| 66 |
hint = rag_scope_warning(session_id=session_id, doc_ids=doc_ids)
|
| 67 |
return hint, [], ""
|
|
@@ -73,9 +85,17 @@ def tool_research_answer(
|
|
| 73 |
{"role": "system", "content": system},
|
| 74 |
{"role": "user", "content": user},
|
| 75 |
]
|
|
|
|
| 76 |
answer = clean_model_answer(
|
| 77 |
-
backend.chat(messages, max_tokens=
|
| 78 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
refs = format_references(citations)
|
| 80 |
if session_id:
|
| 81 |
store.add_message(session_id, "user", question, [c.chunk_id for c in citations])
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from pathlib import Path
|
| 4 |
+
from time import monotonic
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
from researchmind.citations import Citation, clean_model_answer, format_context_block, format_references
|
|
|
|
| 52 |
skill_path: Path,
|
| 53 |
session_id: str | None = None,
|
| 54 |
doc_ids: list[str] | None = None,
|
| 55 |
+
trace: Any | None = None,
|
| 56 |
) -> tuple[str, list[Citation], str]:
|
| 57 |
cfg = get_config()
|
| 58 |
store = get_store()
|
| 59 |
scope_session, scope_docs = resolve_retrieve_scope(session_id, doc_ids)
|
| 60 |
+
retrieve_started = monotonic()
|
| 61 |
chunks = retrieve(
|
| 62 |
question,
|
| 63 |
store,
|
|
|
|
| 65 |
session_id=scope_session,
|
| 66 |
doc_ids=scope_docs,
|
| 67 |
)
|
| 68 |
+
if trace is not None:
|
| 69 |
+
trace.log_step(
|
| 70 |
+
"retrieve",
|
| 71 |
+
"Retrieve passages",
|
| 72 |
+
duration_ms=int((monotonic() - retrieve_started) * 1000),
|
| 73 |
+
chunks=len(chunks),
|
| 74 |
+
session_id=scope_session or "",
|
| 75 |
+
doc_ids=scope_docs or [],
|
| 76 |
+
)
|
| 77 |
if not chunks:
|
| 78 |
hint = rag_scope_warning(session_id=session_id, doc_ids=doc_ids)
|
| 79 |
return hint, [], ""
|
|
|
|
| 85 |
{"role": "system", "content": system},
|
| 86 |
{"role": "user", "content": user},
|
| 87 |
]
|
| 88 |
+
generate_started = monotonic()
|
| 89 |
answer = clean_model_answer(
|
| 90 |
+
backend.chat(messages, max_tokens=512, temperature=0.2)
|
| 91 |
)
|
| 92 |
+
if trace is not None:
|
| 93 |
+
trace.log_step(
|
| 94 |
+
"generate",
|
| 95 |
+
"Generate cited answer",
|
| 96 |
+
duration_ms=int((monotonic() - generate_started) * 1000),
|
| 97 |
+
citations=len(citations),
|
| 98 |
+
)
|
| 99 |
refs = format_references(citations)
|
| 100 |
if session_id:
|
| 101 |
store.add_message(session_id, "user", question, [c.chunk_id for c in citations])
|
libs/echocoach/src/echocoach/prompts.py
CHANGED
|
@@ -27,7 +27,7 @@ If a lesson topic is set, stay focused on it. When source excerpts are provided,
|
|
| 27 |
PITCH_SYSTEM = """You are TeacherVoice, a supportive public-speaking coach in a live conversation.
|
| 28 |
Give brief, actionable feedback on what the student just said (opening, clarity, energy, structure).
|
| 29 |
Do not produce JSON or long reports — speak naturally in 2-4 sentences.
|
| 30 |
-
Suggest one concrete improvement for their next attempt. For charts and pace analysis,
|
| 31 |
|
| 32 |
_MODE_SYSTEM: dict[TeacherVoiceMode, str] = {
|
| 33 |
"explain": EXPLAIN_SYSTEM,
|
|
|
|
| 27 |
PITCH_SYSTEM = """You are TeacherVoice, a supportive public-speaking coach in a live conversation.
|
| 28 |
Give brief, actionable feedback on what the student just said (opening, clarity, energy, structure).
|
| 29 |
Do not produce JSON or long reports — speak naturally in 2-4 sentences.
|
| 30 |
+
Suggest one concrete improvement for their next attempt. For charts and pace analysis, expand **Deep pitch analysis** below the chat."""
|
| 31 |
|
| 32 |
_MODE_SYSTEM: dict[TeacherVoiceMode, str] = {
|
| 33 |
"explain": EXPLAIN_SYSTEM,
|
libs/echocoach/src/echocoach/teacher_voice.py
CHANGED
|
@@ -12,7 +12,7 @@ from agent.trace import TraceRecorder
|
|
| 12 |
from inference.base import InferenceBackend
|
| 13 |
from inference.response_clean import (
|
| 14 |
needs_teacher_compaction,
|
| 15 |
-
|
| 16 |
strip_reasoning_output,
|
| 17 |
)
|
| 18 |
from researchmind.ingest import IngestPipeline
|
|
@@ -161,6 +161,7 @@ def fetch_rag_context(
|
|
| 161 |
def _rag_turn_via_agent(
|
| 162 |
user_text: str,
|
| 163 |
*,
|
|
|
|
| 164 |
topic: str | None,
|
| 165 |
session_id: str,
|
| 166 |
doc_ids: list[str] | None,
|
|
@@ -198,8 +199,13 @@ def _rag_turn_via_agent(
|
|
| 198 |
research_trace=result.trace_path,
|
| 199 |
)
|
| 200 |
|
| 201 |
-
|
| 202 |
-
display_reply =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
rag_refs = result.references_markdown or None
|
| 204 |
return assistant_text, rag_refs, rag_status, display_reply
|
| 205 |
|
|
@@ -251,25 +257,36 @@ def _compact_teacher_reply(
|
|
| 251 |
return compact or seed
|
| 252 |
|
| 253 |
|
| 254 |
-
def
|
| 255 |
raw_reply: str,
|
| 256 |
*,
|
| 257 |
mode: TeacherVoiceMode,
|
| 258 |
backend: InferenceBackend,
|
| 259 |
trace: TraceRecorder,
|
| 260 |
) -> tuple[str, str]:
|
|
|
|
| 261 |
assistant_text = strip_reasoning_output(raw_reply).strip()
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
assistant_text = _compact_teacher_reply(
|
| 264 |
raw_reply,
|
| 265 |
mode=mode,
|
| 266 |
backend=backend,
|
| 267 |
trace=trace,
|
| 268 |
)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
|
| 275 |
def build_teacher_messages(
|
|
@@ -320,6 +337,7 @@ def _generate_teacher_reply(
|
|
| 320 |
if use_rag and mode in RAG_MODES:
|
| 321 |
assistant_text, rag_refs, rag_status, display_reply = _rag_turn_via_agent(
|
| 322 |
user_text,
|
|
|
|
| 323 |
topic=topic,
|
| 324 |
session_id=session_id,
|
| 325 |
doc_ids=doc_ids,
|
|
@@ -334,8 +352,8 @@ def _generate_teacher_reply(
|
|
| 334 |
user_text=user_text,
|
| 335 |
topic=topic,
|
| 336 |
)
|
| 337 |
-
raw_reply = backend.chat(messages, max_tokens=
|
| 338 |
-
assistant_text, display_reply =
|
| 339 |
raw_reply,
|
| 340 |
mode=mode,
|
| 341 |
backend=backend,
|
|
|
|
| 12 |
from inference.base import InferenceBackend
|
| 13 |
from inference.response_clean import (
|
| 14 |
needs_teacher_compaction,
|
| 15 |
+
reply_ends_complete_sentence,
|
| 16 |
strip_reasoning_output,
|
| 17 |
)
|
| 18 |
from researchmind.ingest import IngestPipeline
|
|
|
|
| 161 |
def _rag_turn_via_agent(
|
| 162 |
user_text: str,
|
| 163 |
*,
|
| 164 |
+
mode: TeacherVoiceMode,
|
| 165 |
topic: str | None,
|
| 166 |
session_id: str,
|
| 167 |
doc_ids: list[str] | None,
|
|
|
|
| 199 |
research_trace=result.trace_path,
|
| 200 |
)
|
| 201 |
|
| 202 |
+
raw_answer = strip_references_for_tts(result.answer.strip())
|
| 203 |
+
assistant_text, display_reply = _finalize_voice_reply(
|
| 204 |
+
raw_answer,
|
| 205 |
+
mode=mode,
|
| 206 |
+
backend=backend,
|
| 207 |
+
trace=trace,
|
| 208 |
+
)
|
| 209 |
rag_refs = result.references_markdown or None
|
| 210 |
return assistant_text, rag_refs, rag_status, display_reply
|
| 211 |
|
|
|
|
| 257 |
return compact or seed
|
| 258 |
|
| 259 |
|
| 260 |
+
def _finalize_voice_reply(
|
| 261 |
raw_reply: str,
|
| 262 |
*,
|
| 263 |
mode: TeacherVoiceMode,
|
| 264 |
backend: InferenceBackend,
|
| 265 |
trace: TraceRecorder,
|
| 266 |
) -> tuple[str, str]:
|
| 267 |
+
"""Normalize model output into a complete spoken reply and chat display text."""
|
| 268 |
assistant_text = strip_reasoning_output(raw_reply).strip()
|
| 269 |
+
needs_fix = (
|
| 270 |
+
not assistant_text
|
| 271 |
+
or needs_teacher_compaction(raw_reply)
|
| 272 |
+
or needs_teacher_compaction(assistant_text)
|
| 273 |
+
or not reply_ends_complete_sentence(assistant_text)
|
| 274 |
+
)
|
| 275 |
+
if needs_fix:
|
| 276 |
assistant_text = _compact_teacher_reply(
|
| 277 |
raw_reply,
|
| 278 |
mode=mode,
|
| 279 |
backend=backend,
|
| 280 |
trace=trace,
|
| 281 |
)
|
| 282 |
+
if not reply_ends_complete_sentence(assistant_text):
|
| 283 |
+
assistant_text = _compact_teacher_reply(
|
| 284 |
+
assistant_text or raw_reply,
|
| 285 |
+
mode=mode,
|
| 286 |
+
backend=backend,
|
| 287 |
+
trace=trace,
|
| 288 |
+
)
|
| 289 |
+
return assistant_text, assistant_text
|
| 290 |
|
| 291 |
|
| 292 |
def build_teacher_messages(
|
|
|
|
| 337 |
if use_rag and mode in RAG_MODES:
|
| 338 |
assistant_text, rag_refs, rag_status, display_reply = _rag_turn_via_agent(
|
| 339 |
user_text,
|
| 340 |
+
mode=mode,
|
| 341 |
topic=topic,
|
| 342 |
session_id=session_id,
|
| 343 |
doc_ids=doc_ids,
|
|
|
|
| 352 |
user_text=user_text,
|
| 353 |
topic=topic,
|
| 354 |
)
|
| 355 |
+
raw_reply = backend.chat(messages, max_tokens=512, temperature=0.2)
|
| 356 |
+
assistant_text, display_reply = _finalize_voice_reply(
|
| 357 |
raw_reply,
|
| 358 |
mode=mode,
|
| 359 |
backend=backend,
|
libs/echocoach/tests/test_teacher_voice.py
CHANGED
|
@@ -6,6 +6,7 @@ import numpy as np
|
|
| 6 |
import pytest
|
| 7 |
import soundfile as sf
|
| 8 |
|
|
|
|
| 9 |
from echocoach.prompts import PITCH_SYSTEM, system_prompt_for_mode
|
| 10 |
from echocoach.teacher_voice import (
|
| 11 |
RagContext,
|
|
@@ -131,7 +132,7 @@ def test_build_teacher_messages_includes_topic_and_rag():
|
|
| 131 |
|
| 132 |
|
| 133 |
def test_pitch_mode_system_prompt():
|
| 134 |
-
assert "
|
| 135 |
assert PITCH_SYSTEM == system_prompt_for_mode("pitch")
|
| 136 |
|
| 137 |
|
|
@@ -221,6 +222,7 @@ def test_rag_turn_via_agent_mock(monkeypatch, tmp_path):
|
|
| 221 |
trace = TraceRecorder(skill="teacher-voice", model="test", user_input={})
|
| 222 |
text, refs, status, display = _rag_turn_via_agent(
|
| 223 |
"How do plants eat?",
|
|
|
|
| 224 |
topic="Photosynthesis",
|
| 225 |
session_id="",
|
| 226 |
doc_ids=None,
|
|
@@ -251,6 +253,30 @@ def research_env(tmp_path, monkeypatch):
|
|
| 251 |
monkeypatch.setenv("AGENT_OUTPUTS_DIR", str(tmp_path / "outputs"))
|
| 252 |
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
def test_run_teacher_voice_text_turn_mock(monkeypatch, tmp_path):
|
| 255 |
from echocoach.teacher_voice import run_teacher_voice_text_turn
|
| 256 |
|
|
|
|
| 6 |
import pytest
|
| 7 |
import soundfile as sf
|
| 8 |
|
| 9 |
+
from inference.response_clean import reply_ends_complete_sentence
|
| 10 |
from echocoach.prompts import PITCH_SYSTEM, system_prompt_for_mode
|
| 11 |
from echocoach.teacher_voice import (
|
| 12 |
RagContext,
|
|
|
|
| 132 |
|
| 133 |
|
| 134 |
def test_pitch_mode_system_prompt():
|
| 135 |
+
assert "Deep pitch analysis" in system_prompt_for_mode("pitch")
|
| 136 |
assert PITCH_SYSTEM == system_prompt_for_mode("pitch")
|
| 137 |
|
| 138 |
|
|
|
|
| 222 |
trace = TraceRecorder(skill="teacher-voice", model="test", user_input={})
|
| 223 |
text, refs, status, display = _rag_turn_via_agent(
|
| 224 |
"How do plants eat?",
|
| 225 |
+
mode="explain",
|
| 226 |
topic="Photosynthesis",
|
| 227 |
session_id="",
|
| 228 |
doc_ids=None,
|
|
|
|
| 253 |
monkeypatch.setenv("AGENT_OUTPUTS_DIR", str(tmp_path / "outputs"))
|
| 254 |
|
| 255 |
|
| 256 |
+
def test_finalize_voice_reply_compacts_incomplete_sentence():
|
| 257 |
+
from echocoach.teacher_voice import _finalize_voice_reply
|
| 258 |
+
from agent.trace import TraceRecorder
|
| 259 |
+
|
| 260 |
+
class _Backend:
|
| 261 |
+
def chat(self, messages, *, max_tokens=512, temperature=0.2):
|
| 262 |
+
return (
|
| 263 |
+
"Finetuning adapts a pretrained small model to your task using extra labeled data. "
|
| 264 |
+
"You keep most of the base weights and train on a focused dataset. "
|
| 265 |
+
"That usually beats prompting alone for domain-specific work."
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
trace = TraceRecorder(skill="teacher-voice", model="test", user_input={})
|
| 269 |
+
text, display = _finalize_voice_reply(
|
| 270 |
+
"The lesson aims to teach how to fine-tune small",
|
| 271 |
+
mode="lesson",
|
| 272 |
+
backend=_Backend(),
|
| 273 |
+
trace=trace,
|
| 274 |
+
)
|
| 275 |
+
assert reply_ends_complete_sentence(text)
|
| 276 |
+
assert "fine-tune" in text.lower() or "finetun" in text.lower()
|
| 277 |
+
assert text == display
|
| 278 |
+
|
| 279 |
+
|
| 280 |
def test_run_teacher_voice_text_turn_mock(monkeypatch, tmp_path):
|
| 281 |
from echocoach.teacher_voice import run_teacher_voice_text_turn
|
| 282 |
|
libs/inference/src/inference/response_clean.py
CHANGED
|
@@ -50,8 +50,10 @@ _COMPLETE_SENTENCE = re.compile(r"[.!?][\"')\]]*\s*$")
|
|
| 50 |
_LIST_OUTLINE = re.compile(r"^\d+\.\s", re.MULTILINE)
|
| 51 |
_REASONING_OPENERS = (
|
| 52 |
"we need to",
|
|
|
|
| 53 |
"first,",
|
| 54 |
"first, the",
|
|
|
|
| 55 |
"next,",
|
| 56 |
"the user",
|
| 57 |
"let me",
|
|
@@ -62,6 +64,11 @@ _REASONING_OPENERS = (
|
|
| 62 |
"i should",
|
| 63 |
"i recall",
|
| 64 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
def _normalize_extracted(text: str) -> str:
|
|
@@ -167,6 +174,14 @@ def looks_like_reasoning_only(text: str) -> bool:
|
|
| 167 |
return bool(_SENTENCE_PART.search(text) and len(text) > 120)
|
| 168 |
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
def needs_teacher_compaction(text: str) -> bool:
|
| 171 |
cleaned = text.strip()
|
| 172 |
if not cleaned:
|
|
@@ -203,12 +218,31 @@ def prepare_display_reply(text: str) -> str:
|
|
| 203 |
return cleaned
|
| 204 |
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
def strip_thinking_blocks(text: str) -> str:
|
| 207 |
"""Remove chain-of-thought wrapper tags; keep remaining text (e.g. JSON) intact."""
|
| 208 |
cleaned = text.strip()
|
| 209 |
if not cleaned:
|
| 210 |
return ""
|
| 211 |
-
|
|
|
|
| 212 |
|
| 213 |
|
| 214 |
def strip_reasoning_output(text: str) -> str:
|
|
@@ -218,6 +252,7 @@ def strip_reasoning_output(text: str) -> str:
|
|
| 218 |
return ""
|
| 219 |
|
| 220 |
cleaned = _THINK_BLOCKS.sub("", cleaned).strip()
|
|
|
|
| 221 |
if cleaned and not _THINK_BLOCKS.search(text):
|
| 222 |
extracted = _extract_best_answer(cleaned)
|
| 223 |
if extracted:
|
|
|
|
| 50 |
_LIST_OUTLINE = re.compile(r"^\d+\.\s", re.MULTILINE)
|
| 51 |
_REASONING_OPENERS = (
|
| 52 |
"we need to",
|
| 53 |
+
"we are given",
|
| 54 |
"first,",
|
| 55 |
"first, the",
|
| 56 |
+
"first, let's",
|
| 57 |
"next,",
|
| 58 |
"the user",
|
| 59 |
"let me",
|
|
|
|
| 64 |
"i should",
|
| 65 |
"i recall",
|
| 66 |
)
|
| 67 |
+
_THINK_TAG_PAIRS = (
|
| 68 |
+
(_RT_OPEN, _RT_CLOSE),
|
| 69 |
+
(_THINK_OPEN, _THINK_CLOSE),
|
| 70 |
+
("<thinking>", "</thinking>"),
|
| 71 |
+
)
|
| 72 |
|
| 73 |
|
| 74 |
def _normalize_extracted(text: str) -> str:
|
|
|
|
| 174 |
return bool(_SENTENCE_PART.search(text) and len(text) > 120)
|
| 175 |
|
| 176 |
|
| 177 |
+
def reply_ends_complete_sentence(text: str) -> bool:
|
| 178 |
+
"""True when visible reply text ends with sentence-ending punctuation."""
|
| 179 |
+
cleaned = strip_reasoning_output(text).strip()
|
| 180 |
+
if not cleaned:
|
| 181 |
+
return False
|
| 182 |
+
return bool(_COMPLETE_SENTENCE.search(cleaned))
|
| 183 |
+
|
| 184 |
+
|
| 185 |
def needs_teacher_compaction(text: str) -> bool:
|
| 186 |
cleaned = text.strip()
|
| 187 |
if not cleaned:
|
|
|
|
| 218 |
return cleaned
|
| 219 |
|
| 220 |
|
| 221 |
+
def _strip_unclosed_think_tags(text: str) -> str:
|
| 222 |
+
"""Drop thinking blocks that never closed (common when generation hits max_tokens)."""
|
| 223 |
+
cleaned = text.strip()
|
| 224 |
+
if not cleaned:
|
| 225 |
+
return ""
|
| 226 |
+
for open_tag, close_tag in _THINK_TAG_PAIRS:
|
| 227 |
+
lower = cleaned.lower()
|
| 228 |
+
open_lower = open_tag.lower()
|
| 229 |
+
close_lower = close_tag.lower()
|
| 230 |
+
start = lower.find(open_lower)
|
| 231 |
+
if start == -1:
|
| 232 |
+
continue
|
| 233 |
+
if close_lower in lower[start + len(open_tag) :]:
|
| 234 |
+
continue
|
| 235 |
+
cleaned = cleaned[:start].strip()
|
| 236 |
+
return cleaned
|
| 237 |
+
|
| 238 |
+
|
| 239 |
def strip_thinking_blocks(text: str) -> str:
|
| 240 |
"""Remove chain-of-thought wrapper tags; keep remaining text (e.g. JSON) intact."""
|
| 241 |
cleaned = text.strip()
|
| 242 |
if not cleaned:
|
| 243 |
return ""
|
| 244 |
+
cleaned = _THINK_BLOCKS.sub("", cleaned).strip()
|
| 245 |
+
return _strip_unclosed_think_tags(cleaned)
|
| 246 |
|
| 247 |
|
| 248 |
def strip_reasoning_output(text: str) -> str:
|
|
|
|
| 252 |
return ""
|
| 253 |
|
| 254 |
cleaned = _THINK_BLOCKS.sub("", cleaned).strip()
|
| 255 |
+
cleaned = _strip_unclosed_think_tags(cleaned)
|
| 256 |
if cleaned and not _THINK_BLOCKS.search(text):
|
| 257 |
extracted = _extract_best_answer(cleaned)
|
| 258 |
if extracted:
|
libs/inference/tests/test_response_clean.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
from inference.response_clean import (
|
| 4 |
prepare_display_reply,
|
|
|
|
| 5 |
strip_reasoning_output,
|
| 6 |
strip_thinking_blocks,
|
| 7 |
)
|
|
@@ -17,6 +18,12 @@ def test_strips_redacted_thinking_block():
|
|
| 17 |
assert strip_reasoning_output(raw) == "The capital of France is Paris."
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def test_strips_think_block():
|
| 21 |
raw = f"{_THINK_OPEN}\nplanning...\n{_THINK_CLOSE}\n\nAgents use memory [1]."
|
| 22 |
assert strip_reasoning_output(raw) == "Agents use memory [1]."
|
|
@@ -107,6 +114,11 @@ So, three"""
|
|
| 107 |
assert "Sentence 1:" not in out
|
| 108 |
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def test_prepare_display_reply_wraps_malformed_think_prefix():
|
| 111 |
raw = "think> We need to plan the answer.\n\nThe answer is 42."
|
| 112 |
out = prepare_display_reply(raw)
|
|
|
|
| 2 |
|
| 3 |
from inference.response_clean import (
|
| 4 |
prepare_display_reply,
|
| 5 |
+
reply_ends_complete_sentence,
|
| 6 |
strip_reasoning_output,
|
| 7 |
strip_thinking_blocks,
|
| 8 |
)
|
|
|
|
| 18 |
assert strip_reasoning_output(raw) == "The capital of France is Paris."
|
| 19 |
|
| 20 |
|
| 21 |
+
def test_strips_unclosed_redacted_thinking_block():
|
| 22 |
+
raw = f"{_RT_OPEN}\nplanning without a final answer that never closes"
|
| 23 |
+
assert strip_reasoning_output(raw) == ""
|
| 24 |
+
assert strip_thinking_blocks(raw) == ""
|
| 25 |
+
|
| 26 |
+
|
| 27 |
def test_strips_think_block():
|
| 28 |
raw = f"{_THINK_OPEN}\nplanning...\n{_THINK_CLOSE}\n\nAgents use memory [1]."
|
| 29 |
assert strip_reasoning_output(raw) == "Agents use memory [1]."
|
|
|
|
| 114 |
assert "Sentence 1:" not in out
|
| 115 |
|
| 116 |
|
| 117 |
+
def test_reply_ends_complete_sentence():
|
| 118 |
+
assert reply_ends_complete_sentence("Finetuning teaches a small model to specialize.")
|
| 119 |
+
assert not reply_ends_complete_sentence("The lesson aims to teach how to fine-tune small")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
def test_prepare_display_reply_wraps_malformed_think_prefix():
|
| 123 |
raw = "think> We need to plan the answer.\n\nThe answer is 42."
|
| 124 |
out = prepare_display_reply(raw)
|
libs/researchmind/src/researchmind/citations.py
CHANGED
|
@@ -89,4 +89,4 @@ def clean_model_answer(answer: str) -> str:
|
|
| 89 |
"The model returned planning text without a final answer. "
|
| 90 |
"Try asking again or switch to a non-reasoning model preset."
|
| 91 |
)
|
| 92 |
-
return text
|
|
|
|
| 89 |
"The model returned planning text without a final answer. "
|
| 90 |
"Try asking again or switch to a non-reasoning model preset."
|
| 91 |
)
|
| 92 |
+
return text
|
libs/researchmind/tests/test_citations.py
CHANGED
|
@@ -65,3 +65,11 @@ def test_clean_model_answer_strips_thinking_block():
|
|
| 65 |
raw = f"{think_open}\nplan\n{think_close}\n\nAgents use tools and memory [1]."
|
| 66 |
cleaned = clean_model_answer(raw)
|
| 67 |
assert cleaned == "Agents use tools and memory [1]."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
raw = f"{think_open}\nplan\n{think_close}\n\nAgents use tools and memory [1]."
|
| 66 |
cleaned = clean_model_answer(raw)
|
| 67 |
assert cleaned == "Agents use tools and memory [1]."
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_clean_model_answer_rejects_unclosed_thinking():
|
| 71 |
+
rt_open = "<" + "redacted_thinking" + ">"
|
| 72 |
+
raw = f"{rt_open}\nWe are given a context and need to plan the answer."
|
| 73 |
+
cleaned = clean_model_answer(raw)
|
| 74 |
+
assert "redacted_thinking" not in cleaned
|
| 75 |
+
assert "planning text without a final answer" in cleaned
|
scripts/benchmark_rag_chat.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Benchmark ResearchMind RAG retrieval and optional full chat latency."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import statistics
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from researchmind.config import get_config
|
| 14 |
+
from researchmind.embeddings import embed_texts
|
| 15 |
+
from researchmind.ingest import IngestPipeline
|
| 16 |
+
from researchmind.retrieve import retrieve
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _load_sessions() -> list[tuple[str, str]]:
|
| 20 |
+
store = IngestPipeline().store
|
| 21 |
+
return [(s.id, s.topic or "Untitled") for s in store.list_sessions()]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def benchmark_retrieve(
|
| 25 |
+
question: str,
|
| 26 |
+
*,
|
| 27 |
+
session_id: str,
|
| 28 |
+
runs: int,
|
| 29 |
+
) -> dict[str, object]:
|
| 30 |
+
cfg = get_config()
|
| 31 |
+
store = IngestPipeline().store
|
| 32 |
+
chunks_in_scope = store.get_chunks_with_embeddings(session_id=session_id or None)
|
| 33 |
+
timings: list[float] = []
|
| 34 |
+
retrieved = 0
|
| 35 |
+
for _ in range(runs):
|
| 36 |
+
started = time.perf_counter()
|
| 37 |
+
chunks = retrieve(question, store, config=cfg, session_id=session_id or None)
|
| 38 |
+
timings.append((time.perf_counter() - started) * 1000)
|
| 39 |
+
retrieved = len(chunks)
|
| 40 |
+
|
| 41 |
+
warm = timings[1:] if len(timings) > 1 else timings
|
| 42 |
+
|
| 43 |
+
embed_started = time.perf_counter()
|
| 44 |
+
embed_texts(["warmup query"], model_name=cfg.embed_model)
|
| 45 |
+
embed_warm_ms = (time.perf_counter() - embed_started) * 1000
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
"question": question,
|
| 49 |
+
"session_id": session_id,
|
| 50 |
+
"chunks_in_scope": len(chunks_in_scope),
|
| 51 |
+
"retrieved_chunks": retrieved,
|
| 52 |
+
"top_k": cfg.top_k,
|
| 53 |
+
"max_context_chunks": cfg.max_context_chunks,
|
| 54 |
+
"embed_model": cfg.embed_model,
|
| 55 |
+
"embedder_warm_ms": round(embed_warm_ms, 1),
|
| 56 |
+
"retrieve_ms_cold": round(timings[0], 1) if timings else 0.0,
|
| 57 |
+
"retrieve_ms_mean": round(statistics.mean(warm), 1),
|
| 58 |
+
"retrieve_ms_stdev": round(statistics.stdev(warm), 1) if len(warm) > 1 else 0.0,
|
| 59 |
+
"retrieve_ms_min": round(min(warm), 1),
|
| 60 |
+
"retrieve_ms_max": round(max(warm), 1),
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def benchmark_chat(
|
| 65 |
+
question: str,
|
| 66 |
+
*,
|
| 67 |
+
session_id: str,
|
| 68 |
+
model_key: str | None,
|
| 69 |
+
) -> dict[str, object]:
|
| 70 |
+
from agent.runner import AgentRunner
|
| 71 |
+
from gradio_space.model_loading import ensure_model_loaded, get_active_model_key
|
| 72 |
+
from inference.factory import get_backend
|
| 73 |
+
|
| 74 |
+
key = model_key or get_active_model_key()
|
| 75 |
+
load_err = ensure_model_loaded(key)
|
| 76 |
+
if load_err:
|
| 77 |
+
return {"error": load_err, "model": key}
|
| 78 |
+
|
| 79 |
+
backend = get_backend(key)
|
| 80 |
+
runner = AgentRunner()
|
| 81 |
+
started = time.perf_counter()
|
| 82 |
+
result = runner.run_researchmind_chat(
|
| 83 |
+
question=question,
|
| 84 |
+
session_id=session_id,
|
| 85 |
+
model_key=key,
|
| 86 |
+
backend=backend,
|
| 87 |
+
doc_ids=None,
|
| 88 |
+
)
|
| 89 |
+
total_ms = (time.perf_counter() - started) * 1000
|
| 90 |
+
trace = json.loads(Path(result.trace_path).read_text(encoding="utf-8"))
|
| 91 |
+
steps = [
|
| 92 |
+
{
|
| 93 |
+
"name": step.get("name"),
|
| 94 |
+
"label": step.get("label"),
|
| 95 |
+
"duration_ms": step.get("duration_ms"),
|
| 96 |
+
}
|
| 97 |
+
for step in trace.get("steps", [])
|
| 98 |
+
if step.get("type") == "step"
|
| 99 |
+
]
|
| 100 |
+
return {
|
| 101 |
+
"model": key,
|
| 102 |
+
"question": question,
|
| 103 |
+
"session_id": session_id,
|
| 104 |
+
"total_ms": round(total_ms, 1),
|
| 105 |
+
"citations": len(result.citations),
|
| 106 |
+
"answer_preview": result.answer[:240],
|
| 107 |
+
"steps": steps,
|
| 108 |
+
"trace_path": result.trace_path,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def main() -> int:
|
| 113 |
+
parser = argparse.ArgumentParser(description="Benchmark ResearchMind RAG chat")
|
| 114 |
+
parser.add_argument(
|
| 115 |
+
"--question",
|
| 116 |
+
default="how we can finetune model",
|
| 117 |
+
help="Question to benchmark",
|
| 118 |
+
)
|
| 119 |
+
parser.add_argument("--session-id", default="", help="Research session id")
|
| 120 |
+
parser.add_argument("--runs", type=int, default=5, help="Retrieve benchmark repetitions")
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--full-chat",
|
| 123 |
+
action="store_true",
|
| 124 |
+
help="Also run one full RAG chat (loads local LLM)",
|
| 125 |
+
)
|
| 126 |
+
parser.add_argument("--model-key", default="", help="Override ACTIVE_MODEL preset")
|
| 127 |
+
args = parser.parse_args()
|
| 128 |
+
|
| 129 |
+
sessions = _load_sessions()
|
| 130 |
+
session_id = args.session_id.strip()
|
| 131 |
+
if not session_id:
|
| 132 |
+
session_id = sessions[0][0] if sessions else ""
|
| 133 |
+
|
| 134 |
+
if not session_id:
|
| 135 |
+
print("No indexed session found. Ingest sources first.")
|
| 136 |
+
return 1
|
| 137 |
+
|
| 138 |
+
retrieve_report = benchmark_retrieve(
|
| 139 |
+
args.question,
|
| 140 |
+
session_id=session_id,
|
| 141 |
+
runs=max(1, args.runs),
|
| 142 |
+
)
|
| 143 |
+
print(json.dumps({"retrieve": retrieve_report}, indent=2))
|
| 144 |
+
|
| 145 |
+
if args.full_chat:
|
| 146 |
+
chat_report = benchmark_chat(
|
| 147 |
+
args.question,
|
| 148 |
+
session_id=session_id,
|
| 149 |
+
model_key=args.model_key or None,
|
| 150 |
+
)
|
| 151 |
+
print(json.dumps({"chat": chat_report}, indent=2))
|
| 152 |
+
return 0
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
sys.exit(main())
|