|
4 | 4 |
|
5 | 5 | import os |
6 | 6 | import json |
| 7 | +import html |
7 | 8 | import time |
8 | 9 | import uuid |
9 | 10 | from pathlib import Path |
@@ -159,6 +160,156 @@ def typewriter(text: str, speed: float = 0.012): |
159 | 160 | placeholder.markdown(f'<div class="glass-card fade-in">{text}</div>', unsafe_allow_html=True) |
160 | 161 |
|
161 | 162 |
|
| 163 | +def _as_float(value, default=0.0): |
| 164 | + try: |
| 165 | + return float(value) |
| 166 | + except (TypeError, ValueError): |
| 167 | + return default |
| 168 | + |
| 169 | + |
| 170 | +def _release_readiness(migration_name: str, completed_payload: dict) -> dict: |
| 171 | + """Build a factual release-readiness summary from persisted backend evidence.""" |
| 172 | + cache_key = f"release_readiness::{migration_name}" |
| 173 | + cached = st.session_state.get(cache_key) |
| 174 | + if isinstance(cached, dict): |
| 175 | + return cached |
| 176 | + result = { |
| 177 | + "release_gate": bool(completed_payload.get("release_ready", False)), |
| 178 | + "semantic_status": "not_available", |
| 179 | + "security_status": "not_available", |
| 180 | + "security_critical": 0, |
| 181 | + "verification_score": None, |
| 182 | + "confidence": "Review", |
| 183 | + } |
| 184 | + try: |
| 185 | + semantic = client.semantic_verification(migration_name).get("data", {}) |
| 186 | + result["semantic_status"] = str(semantic.get("status", "not_available")) |
| 187 | + result["verification_score"] = semantic.get("score") |
| 188 | + except ApiError: |
| 189 | + pass |
| 190 | + try: |
| 191 | + evidence = client.migration_evidence(migration_name).get("data", {}) |
| 192 | + security = evidence.get("security_review", {}) or {} |
| 193 | + result["security_status"] = str(security.get("status", "not_available")) |
| 194 | + result["security_critical"] = int(security.get("critical", 0) or 0) |
| 195 | + except (ApiError, TypeError, ValueError): |
| 196 | + pass |
| 197 | + |
| 198 | + if ( |
| 199 | + result["release_gate"] |
| 200 | + and result["semantic_status"] == "verified" |
| 201 | + and result["security_status"] == "passed" |
| 202 | + and result["security_critical"] == 0 |
| 203 | + ): |
| 204 | + result["confidence"] = "High" |
| 205 | + elif result["semantic_status"] in {"verified", "partial"} and result["security_status"] in {"passed", "review"}: |
| 206 | + result["confidence"] = "Moderate" |
| 207 | + st.session_state[cache_key] = result |
| 208 | + return result |
| 209 | + |
| 210 | + |
| 211 | +def render_release_readiness(migration_name: str, completed_payload: dict) -> None: |
| 212 | + """Render a compact, data-backed release readiness card.""" |
| 213 | + readiness = _release_readiness(migration_name, completed_payload) |
| 214 | + gate_label = "PASS" if readiness["release_gate"] else "BLOCKED" |
| 215 | + gate_icon = "✓" if readiness["release_gate"] else "!" |
| 216 | + st.markdown( |
| 217 | + '<div class="release-grid">' |
| 218 | + f'<div class="release-card"><div class="release-label">Release readiness</div>' |
| 219 | + f'<div style="display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin:.2rem 0 .45rem">' |
| 220 | + f'<div class="release-score">{readiness["confidence"]}</div>' |
| 221 | + f'<div class="release-status">{gate_icon} Release gate · {gate_label}</div></div>' |
| 222 | + f'<div class="change-copy">Semantic verification: <b>{readiness["semantic_status"].replace("_", " ").title()}</b> · Security review: <b>{readiness["security_status"].replace("_", " ").title()}</b></div></div>' |
| 223 | + f'<div class="release-card"><div class="release-label">Evidence snapshot</div>' |
| 224 | + f'<div class="delta-row"><span class="delta-label">Verification score</span><span class="delta-value">{readiness["verification_score"] if readiness["verification_score"] is not None else "—"}</span></div>' |
| 225 | + f'<div class="delta-row"><span class="delta-label">Critical security findings</span><span class="delta-value">{readiness["security_critical"]}</span></div>' |
| 226 | + f'<div class="delta-row"><span class="delta-label">Next action</span><span class="delta-value">{"Review & release" if readiness["release_gate"] else "Resolve release blockers"}</span></div></div>' |
| 227 | + '</div>', |
| 228 | + unsafe_allow_html=True, |
| 229 | + ) |
| 230 | + |
| 231 | + |
| 232 | +def render_change_explorer(migration_name: str) -> None: |
| 233 | + """Show a before/after modernization snapshot using real comparison-report data.""" |
| 234 | + cache_key = f"change_report::{migration_name}" |
| 235 | + report = st.session_state.get(cache_key) |
| 236 | + if not isinstance(report, dict): |
| 237 | + try: |
| 238 | + report = client.report(migration_name, persist=False, include_markdown=False, require_migrated=True) |
| 239 | + st.session_state[cache_key] = report |
| 240 | + except ApiError: |
| 241 | + st.info("Comparison evidence becomes available after the migration report is ready.") |
| 242 | + return |
| 243 | + |
| 244 | + if report.get("status") != "ready": |
| 245 | + st.info(report.get("message", "Comparison report is not ready yet.")) |
| 246 | + return |
| 247 | + |
| 248 | + cards = report.get("module_review_cards") or [] |
| 249 | + mode = report.get("analysis_mode", "unknown") |
| 250 | + st.markdown('<div class="panel-kicker">CODE CHANGE EXPLORER</div>', unsafe_allow_html=True) |
| 251 | + st.markdown('<div class="panel-title">Before → after modernization snapshot</div>', unsafe_allow_html=True) |
| 252 | + st.markdown('<div class="panel-subtitle">A concise comparison built from the persisted migration report — no simulated diff counts.</div>', unsafe_allow_html=True) |
| 253 | + |
| 254 | + if mode == "source_vs_migrated": |
| 255 | + total_files = 0 |
| 256 | + total_functions_delta = 0 |
| 257 | + total_loc_delta = 0 |
| 258 | + total_dep_delta = 0 |
| 259 | + measurable = 0 |
| 260 | + for card in cards: |
| 261 | + delta = card.get("what_changed", {}).get("semantic_delta", {}) or {} |
| 262 | + if delta.get("function_delta") is not None: |
| 263 | + total_functions_delta += int(delta.get("function_delta") or 0) |
| 264 | + total_loc_delta += int(delta.get("loc_delta") or 0) |
| 265 | + total_dep_delta += int(delta.get("dependency_delta") or 0) |
| 266 | + measurable += 1 |
| 267 | + m1, m2, m3, m4 = st.columns(4) |
| 268 | + m1.metric("Modules compared", len(cards)) |
| 269 | + m2.metric("Function delta", f"{total_functions_delta:+d}" if measurable else "—") |
| 270 | + m3.metric("LOC delta", f"{total_loc_delta:+d}" if measurable else "—") |
| 271 | + m4.metric("Dependency delta", f"{total_dep_delta:+d}" if measurable else "—") |
| 272 | + |
| 273 | + preview = cards[:3] |
| 274 | + for card in preview: |
| 275 | + change = card.get("what_changed", {}) or {} |
| 276 | + delta = change.get("semantic_delta", {}) or {} |
| 277 | + module = card.get("module", "module") |
| 278 | + pattern = change.get("legacy_to_modern_pattern", "Comparison available") |
| 279 | + left_copy = f"Legacy pattern: {pattern}" |
| 280 | + right_copy = "; ".join(change.get("key_transformations", [])[:2]) or "Modernized structure recorded in the migration report." |
| 281 | + st.markdown( |
| 282 | + '<div class="change-grid">' |
| 283 | + f'<div class="change-card before"><div class="change-title">Before · {html.escape(str(module))}</div><div class="change-copy">{html.escape(str(left_copy))}</div></div>' |
| 284 | + f'<div class="change-card after"><div class="change-title">After · {html.escape(str(module))}</div><div class="change-copy">{html.escape(str(right_copy))}</div>' |
| 285 | + f'<div class="delta-row"><span class="delta-label">Functions</span><span class="delta-value">{delta.get("function_delta", "—")}</span></div>' |
| 286 | + f'<div class="delta-row"><span class="delta-label">LOC</span><span class="delta-value">{delta.get("loc_delta", "—")}</span></div>' |
| 287 | + f'<div class="delta-row"><span class="delta-label">Dependencies</span><span class="delta-value">{delta.get("dependency_delta", "—")}</span></div></div>' |
| 288 | + '</div>', |
| 289 | + unsafe_allow_html=True, |
| 290 | + ) |
| 291 | + else: |
| 292 | + # Migrated-only mode: be explicit rather than pretending to have a source diff. |
| 293 | + st.warning("Source baseline comparison is not available for this migration, so the explorer is showing migrated-code evidence only.") |
| 294 | + for card in cards[:4]: |
| 295 | + st.markdown( |
| 296 | + f'<div class="change-card after"><div class="change-title">{html.escape(str(card.get("module", "module")))}</div>' |
| 297 | + f'<div class="change-copy">{html.escape(str(card.get("reason", "Review the module-level risk profile.")))}</div></div>', |
| 298 | + unsafe_allow_html=True, |
| 299 | + ) |
| 300 | + |
| 301 | + |
| 302 | +def render_codebase_answer(migration_name: str, answer: dict, is_target: bool) -> None: |
| 303 | + """Render an answer with lightweight evidence metadata, not hidden reasoning.""" |
| 304 | + route = "Target code" if is_target else "Source code" |
| 305 | + st.markdown( |
| 306 | + f'<div class="release-status">⌁ Route · {route}</div>', |
| 307 | + unsafe_allow_html=True, |
| 308 | + ) |
| 309 | + typewriter(answer.get("answer", "")) |
| 310 | + st.caption("Evidence is grounded in the selected migration knowledge base; internal reasoning is not exposed.") |
| 311 | + |
| 312 | + |
162 | 313 | # The backend now streams Agno Workflow step events into the task status API. |
163 | 314 | # This UI renders those persisted events; it is no longer a time-based approximation. |
164 | 315 | PIPELINE_STAGES = [ |
@@ -398,6 +549,8 @@ def count_up(label: str, target: int, duration: float = 0.5, steps: int = 12): |
398 | 549 | st.session_state.active_migration_name = migration_name |
399 | 550 | st.session_state.active_task_started_at = time.time() |
400 | 551 | st.session_state.completion_toast_shown = False |
| 552 | + st.session_state.pop(f"release_readiness::{migration_name}", None) |
| 553 | + st.session_state.pop(f"change_report::{migration_name}", None) |
401 | 554 | st.session_state.last_progress = {"stage": "workflow", "percent": 2, "message": "Preparing workflow telemetry"} |
402 | 555 | st.toast(f"Migration '{migration_name}' queued") |
403 | 556 | st.success(f"Migration queued · `{result['task_id']}`") |
@@ -491,7 +644,10 @@ def count_up(label: str, target: int, duration: float = 0.5, steps: int = 12): |
491 | 644 | st.download_button("Download release artifact", data=data, file_name=f"{st.session_state.active_migration_name}.zip", mime="application/zip", width="stretch") |
492 | 645 | except ApiError: |
493 | 646 | st.caption("Download will be available shortly.") |
494 | | - st.info("Next: open Architecture & Analysis or Semantic Verification to inspect release evidence.") |
| 647 | + if st.session_state.active_migration_name: |
| 648 | + render_release_readiness(st.session_state.active_migration_name, completed_payload) |
| 649 | + render_change_explorer(st.session_state.active_migration_name) |
| 650 | + st.info("Next: inspect Architecture & Analysis, Semantic Verification, or Ask the Codebase.") |
495 | 651 | elif badge_class == "failed": |
496 | 652 | raw_failed = status.get("result") |
497 | 653 | failed_payload = {} |
@@ -767,28 +923,48 @@ def count_up(label: str, target: int, duration: float = 0.5, steps: int = 12): |
767 | 923 | st.caption("Complete a migration, then enter its name here to inspect semantic verification evidence.") |
768 | 924 |
|
769 | 925 | # -------------------------------------------------------------------------- |
770 | | -# Tab: Chat |
| 926 | +# Tab: Ask the Codebase |
771 | 927 | # -------------------------------------------------------------------------- |
772 | 928 | if active_section == "Ask the Codebase": |
773 | | - st.markdown("#### Ask questions about a migrated codebase") |
| 929 | + st.markdown('<div class="panel-kicker">CODE INTELLIGENCE</div>', unsafe_allow_html=True) |
| 930 | + st.markdown('<div class="panel-title">Ask the codebase why the migration looks the way it does.</div>', unsafe_allow_html=True) |
| 931 | + st.markdown('<div class="panel-subtitle">Use the persisted migration knowledge base to inspect source or target behavior without exposing internal agent reasoning.</div>', unsafe_allow_html=True) |
| 932 | + |
774 | 933 | chat_migration = st.text_input( |
775 | | - "Migration name", value=st.session_state.active_migration_name or "" |
| 934 | + "Migration name", value=st.session_state.active_migration_name or "", key="analysis_chat_migration" |
| 935 | + ) |
| 936 | + is_target = st.checkbox("Ask about the target (converted) code", value=False) |
| 937 | + |
| 938 | + st.markdown('<div style="margin:.65rem 0 .35rem"><span class="release-label">Suggested questions</span></div>', unsafe_allow_html=True) |
| 939 | + prompt_cols = st.columns(3) |
| 940 | + prompts = [ |
| 941 | + "What changed in the authentication flow?", |
| 942 | + "Which modules need the most review?", |
| 943 | + "How did the REST contract map to the target?", |
| 944 | + ] |
| 945 | + selected_prompt = None |
| 946 | + for idx, (col, prompt) in enumerate(zip(prompt_cols, prompts)): |
| 947 | + if col.button(prompt, key=f"prompt_{idx}", width="stretch"): |
| 948 | + selected_prompt = prompt |
| 949 | + question = st.text_area( |
| 950 | + "Your question", |
| 951 | + value=selected_prompt or "", |
| 952 | + placeholder="e.g. Why was the payment service split during migration?", |
| 953 | + height=110, |
776 | 954 | ) |
777 | | - question = st.text_area("Your question", placeholder="What does the payment module do?") |
778 | | - is_target = st.checkbox("Ask about the target (converted) code instead of source") |
779 | 955 |
|
780 | | - if st.button("Ask", type="primary"): |
781 | | - if not chat_migration or not question: |
| 956 | + if st.button("Ask the codebase", type="primary", width="stretch"): |
| 957 | + if not chat_migration or not question.strip(): |
782 | 958 | st.warning("Enter a migration name and a question.") |
783 | 959 | else: |
784 | 960 | thinking_slot = st.empty() |
785 | 961 | with thinking_slot: |
786 | 962 | if not try_lottie(THINKING_LOTTIE_URL, height=120): |
787 | | - st.spinner("Thinking...") |
| 963 | + st.spinner("Searching migration evidence...") |
788 | 964 | try: |
789 | | - answer = client.chat_ask(chat_migration, question, is_target=is_target) |
| 965 | + answer = client.chat_ask(chat_migration, question.strip(), is_target=is_target) |
790 | 966 | thinking_slot.empty() |
791 | | - typewriter(answer["answer"]) |
| 967 | + render_codebase_answer(chat_migration, answer, is_target) |
792 | 968 | except ApiError as e: |
793 | 969 | thinking_slot.empty() |
794 | 970 | st.error(e.detail) |
|
0 commit comments