amine-yagoub commited on
Commit
ca2b985
Β·
1 Parent(s): 8f5b53f

feat: introduce gradio-based interface for courtroom trials

Browse files
src/code_tribunal/ui/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """CodeTribunal Gradio UI package."""
2
+
3
+ from code_tribunal.ui.app import create_app, main
src/code_tribunal/ui/app.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio application layout and launch."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import gradio as gr
7
+
8
+ from code_tribunal.config import TribunalConfig
9
+ from code_tribunal.ui.exports import export_md, export_pdf
10
+ from code_tribunal.ui.pipeline import handle_question, run_courtroom
11
+ from code_tribunal.ui.styles import CSS
12
+
13
+ logging.basicConfig(level=logging.DEBUG, format="[%(asctime)s][%(levelname)s] %(message)s", datefmt="%H:%M:%S")
14
+ logging.getLogger("crewai").setLevel(logging.WARNING)
15
+
16
+
17
+ def create_app() -> gr.Blocks:
18
+ logo = Path(__file__).resolve().parent.parent.parent.parent / "assets" / "logo.png"
19
+
20
+ with gr.Blocks(title="CodeTribunal") as app:
21
+ with gr.Column(visible=True) as hero:
22
+ if logo.exists():
23
+ gr.Image(value=str(logo), show_label=False, height=160, container=False, elem_classes=["hero-logo"])
24
+ gr.Markdown("# CodeTribunal\n### The AI Courtroom That Exposes Bad Freelance Code", elem_classes=["hero-title"])
25
+ gr.Markdown("Upload a .zip of code and watch a multi-agent forensic investigation unfold.", elem_classes=["hero-subtitle"])
26
+
27
+ with gr.Column(visible=True, elem_classes=["upload-area"]) as upload:
28
+ code_input = gr.File(label="Drop your .zip here", file_types=[".zip"], interactive=True)
29
+
30
+ with gr.Column(visible=False) as processing:
31
+ status = gr.Markdown("Initializing...", elem_classes=["status-phase"])
32
+ chatbot = gr.Chatbot(label="Courtroom Transcript", height=600, elem_classes=["chatbot"])
33
+ with gr.Row():
34
+ exp_md = gr.Button("πŸ“„ Export Markdown", visible=False)
35
+ exp_pdf = gr.Button("πŸ“‘ Export PDF", visible=False)
36
+ exp_file = gr.File(label="Download", visible=False)
37
+ qa_input = gr.Textbox(label="Ask a follow-up", placeholder="e.g., 'Why was eval() critical?'", visible=False)
38
+ qa_btn = gr.Button("πŸ•΅οΈ Ask Expert Witness", variant="primary", visible=False)
39
+
40
+ state = gr.State(value={})
41
+
42
+ code_input.upload(
43
+ fn=run_courtroom, inputs=[code_input],
44
+ outputs=[hero, upload, processing, status, chatbot, exp_md, exp_pdf, exp_file, state],
45
+ ).then(
46
+ fn=lambda: (gr.update(visible=True), gr.update(visible=True)),
47
+ outputs=[qa_input, qa_btn],
48
+ )
49
+ exp_md.click(fn=export_md, inputs=[state], outputs=[exp_file])
50
+ exp_pdf.click(fn=export_pdf, inputs=[state], outputs=[exp_file])
51
+ qa_btn.click(fn=handle_question, inputs=[qa_input, chatbot, state], outputs=[chatbot, qa_input])
52
+ qa_input.submit(fn=handle_question, inputs=[qa_input, chatbot, state], outputs=[chatbot, qa_input])
53
+
54
+ return app
55
+
56
+
57
+ def main() -> None:
58
+ app = create_app()
59
+ app.launch(
60
+ server_name=TribunalConfig().server_host,
61
+ server_port=TribunalConfig().server_port,
62
+ css=CSS,
63
+ theme=gr.themes.Base(primary_hue="amber", secondary_hue="slate", neutral_hue="slate"),
64
+ )
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
src/code_tribunal/ui/exports.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Report export: Markdown and PDF."""
2
+
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from markdown_pdf import MarkdownPdf, Section
8
+
9
+ from code_tribunal.ui.helpers import evidence_html
10
+
11
+
12
+ def export_md(ctx: dict) -> str:
13
+ """Export trial context as a Markdown file. Returns the file path."""
14
+ md = [
15
+ "# CodeTribunal β€” Trial Report\n",
16
+ f"**Generated**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n",
17
+ "---\n",
18
+ ]
19
+ for key in ["evidence", "verdict", "report"]:
20
+ value = ctx.get(key, "")
21
+ if value:
22
+ md.append(f"## {key.title()}\n\n{value}\n\n")
23
+ fp = tempfile.mktemp(suffix="_CodeTribunal_Report.md")
24
+ Path(fp).write_text("\n".join(md))
25
+ return fp
26
+
27
+
28
+ def export_pdf(ctx: dict) -> str:
29
+ """Export trial context as a PDF file. Returns the file path."""
30
+ sections = []
31
+ for key in ["evidence", "verdict", "report"]:
32
+ value = ctx.get(key, "")
33
+ if value:
34
+ sections.append(f"## {key.title()}\n\n{value}")
35
+
36
+ full_md = (
37
+ f"# CodeTribunal - Trial Report\n\n"
38
+ f"*Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}*\n\n---\n\n"
39
+ + "\n".join(sections)
40
+ )
41
+
42
+ pdf = MarkdownPdf()
43
+ pdf.add_section(Section(full_md))
44
+ fp = tempfile.mktemp(suffix="_CodeTribunal_Report.pdf")
45
+ pdf.save(fp)
46
+ return fp
src/code_tribunal/ui/helpers.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTML rendering helpers for the Gradio UI."""
2
+
3
+ from pathlib import Path
4
+
5
+ from code_tribunal.ui.styles import SEVERITY_COLORS
6
+
7
+
8
+ def escape_html(text: str) -> str:
9
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
10
+
11
+
12
+ def severity_badge(severity: str) -> str:
13
+ color = SEVERITY_COLORS.get(severity, "#6b7280")
14
+ return (
15
+ f'<span style="background:{color};color:white;padding:2px 8px;'
16
+ f'border-radius:4px;font-size:12px;font-weight:bold">{severity}</span>'
17
+ )
18
+
19
+
20
+ def evidence_html(report) -> str:
21
+ """Render an EvidenceReport as an HTML table."""
22
+ lines = [
23
+ "<h3>Evidence Report</h3>",
24
+ f"<p>Files: <b>{report.file_count}</b> | Findings: <b>{len(report.findings)}</b></p>",
25
+ ]
26
+ for domain, findings in report.findings_by_domain.items():
27
+ lines.append(f"<h4>{domain.title()} ({len(findings)})</h4>")
28
+ lines.append('<table style="width:100%;border-collapse:collapse">')
29
+ lines.append(
30
+ '<tr style="border-bottom:1px solid #333">'
31
+ "<th style='text-align:left;padding:4px'>Sev</th>"
32
+ "<th style='text-align:left;padding:4px'>File</th>"
33
+ "<th style='text-align:left;padding:4px'>Line</th>"
34
+ "<th style='text-align:left;padding:4px'>Code</th></tr>"
35
+ )
36
+ for finding in findings:
37
+ lines.append(
38
+ f'<tr style="border-bottom:1px solid #222">'
39
+ f"<td style='padding:4px'>{severity_badge(finding.severity_hint)}</td>"
40
+ f"<td style='padding:4px;font-family:monospace;font-size:13px'>"
41
+ f"{Path(finding.file).name}</td>"
42
+ f"<td style='padding:4px;font-family:monospace'>{finding.line}</td>"
43
+ f"<td style='padding:4px;font-family:monospace;font-size:13px;color:#a0a0a0'>"
44
+ f"{escape_html(finding.code)}</td></tr>"
45
+ )
46
+ lines.append("</table>")
47
+ return "\n".join(lines)
src/code_tribunal/ui/pipeline.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pipeline orchestration: runs the courtroom and streams UI updates."""
2
+
3
+ import logging
4
+ import tempfile
5
+
6
+ import gradio as gr
7
+
8
+ from code_tribunal.config import TribunalConfig
9
+ from code_tribunal.courtroom import Courtroom
10
+ from code_tribunal.evidence import safe_extract_zip
11
+ from code_tribunal.pipeline import Phase
12
+ from code_tribunal.ui.helpers import escape_html, evidence_html
13
+ from code_tribunal.ui.styles import PHASE_LABELS
14
+
15
+ log = logging.getLogger("code_tribunal")
16
+
17
+
18
+ def run_courtroom(code_input, progress=gr.Progress()):
19
+ """Run the full pipeline, yielding updates to the UI."""
20
+ chat = []
21
+ ev_html = ""
22
+ verdict_text = ""
23
+ report_text = ""
24
+ config = TribunalConfig()
25
+
26
+ if code_input is None or not (hasattr(code_input, "name") and code_input.name.endswith(".zip")):
27
+ yield (
28
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),
29
+ "### Please upload a .zip file.", [],
30
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), {},
31
+ )
32
+ return
33
+
34
+ yield (
35
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),
36
+ "### Extracting files...", [],
37
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), {},
38
+ )
39
+
40
+ tmpdir = tempfile.mkdtemp()
41
+ try:
42
+ safe_extract_zip(code_input.name, tmpdir)
43
+ except ValueError as e:
44
+ yield (
45
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),
46
+ f"### Error: {escape_html(str(e))}", [],
47
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), {},
48
+ )
49
+ return
50
+
51
+ if not config.is_configured:
52
+ yield (
53
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),
54
+ "### ZAI_API_KEY not set.", [],
55
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), {},
56
+ )
57
+ return
58
+
59
+ courtroom = Courtroom(config)
60
+ courtroom.pipeline.create_run(code_input.name)
61
+ current_phase = Phase.IDLE
62
+
63
+ for event in courtroom.run(tmpdir):
64
+ if event.phase != current_phase:
65
+ current_phase = event.phase
66
+ log.debug("[UI] Phase -> %s", current_phase)
67
+
68
+ if event.data and "report" in event.data and event.data["report"] is not None:
69
+ robj = event.data["report"]
70
+ if hasattr(robj, "findings"):
71
+ ev_html = evidence_html(robj)
72
+ chat.append({
73
+ "role": "user",
74
+ "content": f"**Case Filed**: {robj.file_count} files, **{len(robj.findings)}** findings.",
75
+ })
76
+
77
+ if event.data and "reports" in event.data:
78
+ for domain, text in event.data["reports"].items():
79
+ icon = {"security": "πŸ›‘οΈ", "quality": "πŸ“‹", "architecture": "πŸ—οΈ"}.get(domain, "πŸ“")
80
+ chat.append({
81
+ "role": "assistant",
82
+ "content": f"**{icon} {domain.title()} Investigation**\n\n{text[:3000]}{'...' if len(text) > 3000 else ''}",
83
+ })
84
+
85
+ if event.data and "transcript" in event.data:
86
+ for section in event.data["transcript"].split("\n\n"):
87
+ for rnd in ["PROSECUTION", "DEFENSE", "REBUTTAL"]:
88
+ if section.startswith(f"=== {rnd}"):
89
+ content = section.replace(f"=== {rnd} ===", "").strip()
90
+ icon = {"PROSECUTION": "βš–οΈ", "DEFENSE": "πŸ›‘οΈ", "REBUTTAL": "βš–οΈ"}.get(rnd, "πŸ“")
91
+ chat.append({
92
+ "role": "assistant",
93
+ "content": f"**{icon} {rnd.title()}**\n\n{content[:3000]}{'...' if len(content) > 3000 else ''}",
94
+ })
95
+ break
96
+
97
+ if event.data and "verdict" in event.data:
98
+ verdict_text = event.data["verdict"]
99
+ chat.append({
100
+ "role": "assistant",
101
+ "content": f"**πŸ”¨ Judge's Verdict**\n\n{verdict_text[:3000]}{'...' if len(verdict_text) > 3000 else ''}",
102
+ })
103
+
104
+ if event.data and "report" in event.data and isinstance(event.data["report"], str):
105
+ report_text = event.data["report"]
106
+ chat.append({
107
+ "role": "assistant",
108
+ "content": f"**πŸ“„ Final Report**\n\n{report_text[:4000]}{'...' if len(report_text) > 4000 else ''}",
109
+ })
110
+
111
+ show_export = event.phase == Phase.COMPLETE
112
+ ctx = {}
113
+ if show_export:
114
+ log.debug("[UI] COMPLETE β€” showing export")
115
+ ctx = {"evidence": ev_html, "verdict": verdict_text, "report": report_text}
116
+
117
+ label = PHASE_LABELS.get(current_phase, current_phase.value)
118
+ if current_phase not in (Phase.COMPLETE, Phase.FAILED):
119
+ status = f'<h3 class="phase-active">{label}</h3>\n{event.status}'
120
+ else:
121
+ status = f"### {label}\n{event.status}"
122
+
123
+ log.debug("[UI] yield β€” chat msgs: %d, status: %s, export: %s", len(chat), label, show_export)
124
+
125
+ yield (
126
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),
127
+ status, chat,
128
+ gr.update(visible=show_export), gr.update(visible=show_export),
129
+ gr.update(visible=show_export), ctx,
130
+ )
131
+
132
+
133
+ def handle_question(question: str, history: list, ctx: dict) -> tuple:
134
+ """Handle a follow-up question from the Q&A interface."""
135
+ if not question.strip():
136
+ return history, ""
137
+ config = TribunalConfig()
138
+ courtroom = Courtroom(config)
139
+ answer = courtroom.ask_question(question, ctx)
140
+ history.append({"role": "user", "content": question})
141
+ history.append({
142
+ "role": "assistant",
143
+ "content": f"**πŸ•΅οΈ Expert Witness**\n\n{answer[:3000]}{'...' if len(answer) > 3000 else ''}",
144
+ })
145
+ return history, ""
src/code_tribunal/ui/styles.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UI constants: CSS, severity colors, phase labels."""
2
+
3
+ from code_tribunal.pipeline import Phase
4
+
5
+ SEVERITY_COLORS = {"CRITICAL": "#dc2626", "HIGH": "#ea580c", "MEDIUM": "#ca8a04", "LOW": "#2563eb"}
6
+
7
+ CSS = """
8
+ .gradio-container{min-width:1280px!important;max-width:1310px!important;margin:0 auto!important}
9
+ body{background:#0a0a14!important}
10
+ .hero-logo{display:block!important;margin:0 auto 12px auto!important;border-radius:16px!important}
11
+ .hero-title{text-align:center!important;color:#fbbf24!important;font-family:Georgia,serif!important;font-size:2.4em!important}
12
+ .hero-subtitle{text-align:center!important;color:#94a3b8!important;font-size:1.1em!important}
13
+ .upload-area .file-preview{min-height:220px!important;border:2px dashed #fbbf2440!important;border-radius:16px!important;background:#1a1a2e!important}
14
+ .upload-area .file-preview:hover{border-color:#fbbf24!important}
15
+ .chatbot{border:none!important;box-shadow:none!important}
16
+ .contain{border:none!important;box-shadow:none!important;background:transparent!important}
17
+ [data-testid="status-tracker"]{border:none!important;box-shadow:none!important}
18
+ .bot.svelte-1nr59td.message{border:none!important;box-shadow:none!important;background:transparent!important}
19
+ .phase-active{font-family:sans-serif;font-size:1.5rem;font-weight:bold;background:linear-gradient(to left,#333 20%,#888 40%,#eee 50%,#888 60%,#333 80%);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;color:transparent;animation:shine 3s linear infinite}
20
+ @keyframes shine{to{background-position:200% center}}
21
+ """
22
+
23
+ PHASE_LABELS = {
24
+ Phase.EVIDENCE: "Phase 1/7: Forensic Evidence",
25
+ Phase.GRAPH: "Phase 2/7: Code Graph",
26
+ Phase.INVESTIGATION: "Phase 3/7: Investigation",
27
+ Phase.TRIAL: "Phase 4/7: The Trial",
28
+ Phase.VERDICT: "Phase 5/7: Verdict",
29
+ Phase.REPORT: "Phase 6/7: Final Report",
30
+ Phase.COMPLETE: "Trial Complete",
31
+ Phase.FAILED: "Error",
32
+ }