Cardiosense-AG commited on
Commit
5d9b181
Β·
verified Β·
1 Parent(s): d699f64

Update src/reasoning_panel.py

Browse files
Files changed (1) hide show
  1. src/reasoning_panel.py +183 -1
src/reasoning_panel.py CHANGED
@@ -154,7 +154,189 @@ def build_panel_data(
154
 
155
  def _escape_script_json(d: Any) -> str:
156
  s = json.dumps(d, ensure_ascii=False)
157
- return s.replace("</",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
 
160
 
 
154
 
155
  def _escape_script_json(d: Any) -> str:
156
  s = json.dumps(d, ensure_ascii=False)
157
+ return s.replace("</", "<\\/") # prevent </script> breakage
158
+
159
+
160
+ def render_reasoning_panel(
161
+ panel_data: List[Dict[str, Any]],
162
+ assessment: str,
163
+ plan: str,
164
+ citations: List[Dict[str, Any]] = None,
165
+ height: int = 720,
166
+ key: str = None
167
+ ):
168
+ """Render dual-pane (SOAP viewer ↔ Reasoning Panel) as an inline component.
169
+
170
+ Left pane shows the claims extracted from Assessment+Plan (read-only preview),
171
+ which keeps scroll-sync deterministic and fast. The editable textareas remain
172
+ on the Streamlit page above this component.
173
+ """
174
+ citations = citations or []
175
+ data_js = _escape_script_json(panel_data)
176
+ refs_js = _escape_script_json(citations)
177
+
178
+ left_claims_html = "".join(
179
+ f'<p class="soap-claim"><span class="marker"></span>{html.escape(d.get("claim",""))}</p>'
180
+ for d in panel_data
181
+ )
182
+
183
+ html_code = f"""
184
+ <div id="rpv4-root">
185
+ <style>
186
+ :root {{
187
+ --bg: #ffffff; --fg: #111; --muted: #6b7280; --surface: #f8fafc; --accent: #2563eb;
188
+ --accent-weak: rgba(37,99,235,.08); --border: #e5e7eb;
189
+ }}
190
+ @media (prefers-color-scheme: dark) {{
191
+ :root {{
192
+ --bg: #0b0f17; --fg: #eaeef5; --muted: #a8b3cf; --surface: #111827; --accent: #60a5fa;
193
+ --accent-weak: rgba(96,165,250,.12); --border: #263145;
194
+ }}
195
+ }}
196
+ #rpv4-root {{ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; color: var(--fg); }}
197
+ .grid {{ display: grid; grid-template-columns: 1fr 0.9fr; gap: 16px; align-items: start; }}
198
+ @media (max-width: 980px) {{ .grid {{ grid-template-columns: 1fr; }} }}
199
+ .panel {{ background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; overflow: auto; max-height: {int(height)-20}px; }}
200
+ .sec {{ margin: 10px 0 8px; font-size: 14px; letter-spacing:.3px; color:var(--muted); text-transform:uppercase; }}
201
+ .soap-claim {{ position: relative; margin: 8px 0; padding: 8px 10px 8px 26px; border-radius: 8px; }}
202
+ .soap-claim.active {{ background: var(--accent-weak); outline: 1px solid var(--accent); }}
203
+ .marker {{ position: absolute; left: 8px; top: 14px; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }}
204
+ .soap-claim.active .marker {{ background: var(--accent); }}
205
+ .claim-card {{ border-bottom: 1px dashed var(--border); padding: 10px 6px; }}
206
+ .claim-title {{ cursor: pointer; font-weight: 600; }}
207
+ .features {{ margin-top: 8px; }}
208
+ .feat {{ display:flex; align-items:center; gap:8px; font-size:13px; margin:3px 0; }}
209
+ .bar {{ flex:1; height:8px; background:var(--border); border-radius:4px; }}
210
+ .fill {{ height:8px; background:var(--accent); }}
211
+ .refs {{ margin-top: 6px; display:flex; flex-wrap:wrap; gap:6px; }}
212
+ .ref {{ padding:3px 6px; background: var(--accent-weak); border:1px solid var(--border); border-radius:6px; cursor:pointer; }}
213
+ .ref:hover {{ border-color: var(--accent); }}
214
+ dialog#evidence {{ border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--fg); max-width: 680px; }}
215
+ dialog::backdrop {{ background: rgba(0,0,0,.45); }}
216
+ .ev-title {{ font-weight:600; margin-bottom:6px; }}
217
+ .ev-body {{ white-space: pre-wrap; line-height:1.4; }}
218
+ .kbd {{ font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; color: var(--muted); }}
219
+ </style>
220
+
221
+ <div class="grid">
222
+ <div class="panel" id="soap-pane">
223
+ <div class="sec" style="margin-top:0;">SOAP β€” Assessment & Plan</div>
224
+ {left_claims_html}
225
+ </div>
226
+ <div class="panel" id="reason-pane">
227
+ <div style="display:flex;justify-content:space-between;align-items:center;">
228
+ <div class="sec" style="margin:0;">REASONING PANEL</div>
229
+ <div class="kbd" title="Keyboard shortcuts">↑/↓ select β€’ Esc close modal</div>
230
+ </div>
231
+ <div id="claims"></div>
232
+ </div>
233
+ </div>
234
+
235
+ <dialog id="evidence">
236
+ <div class="ev-title"></div>
237
+ <div class="ev-body"></div>
238
+ <div style="margin-top:10px;display:flex;justify-content:flex-end;">
239
+ <button id="closeEv" class="ref">Close (Esc)</button>
240
+ </div>
241
+ </dialog>
242
+
243
+ <script>
244
+ const data = {data_js};
245
+ const allRefs = {refs_js};
246
+ const claimsEl = document.getElementById('claims');
247
+ const soap = document.getElementById('soap-pane');
248
+ const evidence = document.getElementById('evidence');
249
+ const evTitle = evidence.querySelector('.ev-title');
250
+ const evBody = evidence.querySelector('.ev-body');
251
+ const closeEv = document.getElementById('closeEv');
252
+
253
+ function pct(n) {{ return Math.round(n * 100); }}
254
+
255
+ function render() {{
256
+ claimsEl.innerHTML = '';
257
+ data.forEach((d, idx) => {{
258
+ const card = document.createElement('div');
259
+ card.className = 'claim-card';
260
+ const title = document.createElement('div');
261
+ title.className = 'claim-title';
262
+ title.textContent = d.claim || `Claim ${idx+1}`;
263
+ title.addEventListener('click', () => activate(idx, true));
264
+ card.appendChild(title);
265
+
266
+ const feats = document.createElement('div');
267
+ feats.className = 'features';
268
+ (d.features || []).forEach(f => {{
269
+ const row = document.createElement('div'); row.className = 'feat';
270
+ const name = document.createElement('div'); name.textContent = f.token;
271
+ const bar = document.createElement('div'); bar.className = 'bar';
272
+ const fill = document.createElement('div'); fill.className = 'fill'; fill.style.width = (pct(f.weight)) + '%';
273
+ const perc = document.createElement('div'); perc.textContent = (pct(f.weight)) + '%';
274
+ bar.appendChild(fill);
275
+ row.appendChild(name); row.appendChild(bar); row.appendChild(perc);
276
+ feats.appendChild(row);
277
+ }});
278
+ card.appendChild(feats);
279
+
280
+ const refs = document.createElement('div');
281
+ refs.className = 'refs';
282
+ (d.refs || []).forEach(r => {{
283
+ const b = document.createElement('button');
284
+ b.className = 'ref';
285
+ b.textContent = r.label || 'Reference';
286
+ b.title = 'Open evidence snippet';
287
+ b.addEventListener('click', () => openEvidence(r));
288
+ refs.appendChild(b);
289
+ }});
290
+ card.appendChild(refs);
291
+
292
+ claimsEl.appendChild(card);
293
+ }});
294
+ }}
295
+
296
+ function openEvidence(ref) {{
297
+ evTitle.textContent = ref.label || 'Evidence';
298
+ evBody.textContent = ref.snippet || 'Snippet not available for this reference.';
299
+ evidence.showModal();
300
+ }}
301
+ closeEv.addEventListener('click', () => evidence.close());
302
+ document.addEventListener('keydown', (e) => {{
303
+ if (e.key === 'Escape') evidence.close();
304
+ if (['ArrowDown','ArrowUp'].includes(e.key)) {{
305
+ e.preventDefault();
306
+ navigate(e.key === 'ArrowDown' ? 1 : -1);
307
+ }}
308
+ }});
309
+
310
+ function activate(idx, scroll) {{
311
+ // Highlight claim in left pane
312
+ const claimEls = soap.querySelectorAll('.soap-claim');
313
+ claimEls.forEach(el => el.classList.remove('active'));
314
+ const el = claimEls[idx];
315
+ if (el) {{
316
+ el.classList.add('active');
317
+ if (scroll) el.scrollIntoView({{behavior:'smooth', block:'center'}});
318
+ }}
319
+ // Highlight card title (optional)
320
+ const cardTitles = document.querySelectorAll('.claim-title');
321
+ cardTitles.forEach((t, i) => t.style.color = (i===idx ? 'var(--accent)' : 'inherit'));
322
+ window.__rpv4_index = idx;
323
+ }}
324
+
325
+ function navigate(delta) {{
326
+ const max = (data || []).length;
327
+ const curr = (window.__rpv4_index || 0);
328
+ const next = Math.max(0, Math.min(max-1, curr + delta));
329
+ activate(next, true);
330
+ }}
331
+
332
+ render();
333
+ // Initial activation
334
+ activate(0, false);
335
+ </script>
336
+ </div>
337
+ """
338
+ components.html(html_code, height=height, scrolling=True)
339
+
340
 
341
 
342