Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- frontend/style.css +165 -100
- src/court/opposing.py +101 -24
- src/court/orchestrator.py +103 -12
frontend/style.css
CHANGED
|
@@ -58,7 +58,7 @@ body {
|
|
| 58 |
display: flex;
|
| 59 |
align-items: center;
|
| 60 |
gap: 12px;
|
| 61 |
-
padding:
|
| 62 |
border-bottom: 1px solid var(--border);
|
| 63 |
}
|
| 64 |
|
|
@@ -94,21 +94,21 @@ body {
|
|
| 94 |
}
|
| 95 |
|
| 96 |
.new-chat-btn {
|
| 97 |
-
margin:
|
| 98 |
-
padding:
|
| 99 |
background: var(--gold-glow);
|
| 100 |
border: 1px solid var(--border-gold);
|
| 101 |
-
border-radius:
|
| 102 |
color: var(--gold);
|
| 103 |
font-family: 'DM Sans', sans-serif;
|
| 104 |
-
font-size:
|
| 105 |
font-weight: 500;
|
| 106 |
cursor: pointer;
|
| 107 |
display: flex;
|
| 108 |
align-items: center;
|
| 109 |
-
gap:
|
| 110 |
transition: background var(--transition), border-color var(--transition);
|
| 111 |
-
width: calc(100% -
|
| 112 |
}
|
| 113 |
|
| 114 |
.new-chat-btn:hover {
|
|
@@ -119,8 +119,8 @@ body {
|
|
| 119 |
.new-chat-icon { font-size: 16px; font-weight: 300; }
|
| 120 |
|
| 121 |
.sidebar-section-label {
|
| 122 |
-
padding:
|
| 123 |
-
font-size:
|
| 124 |
font-weight: 600;
|
| 125 |
color: var(--text-3);
|
| 126 |
letter-spacing: 1px;
|
|
@@ -189,28 +189,28 @@ body {
|
|
| 189 |
}
|
| 190 |
|
| 191 |
.sidebar-footer {
|
| 192 |
-
padding:
|
| 193 |
border-top: 1px solid var(--border);
|
| 194 |
display: flex;
|
| 195 |
flex-direction: column;
|
| 196 |
-
gap:
|
| 197 |
}
|
| 198 |
|
| 199 |
.footer-disclaimer {
|
| 200 |
display: flex;
|
| 201 |
gap: 8px;
|
| 202 |
align-items: flex-start;
|
| 203 |
-
font-size:
|
| 204 |
color: var(--text-3);
|
| 205 |
-
line-height: 1.
|
| 206 |
}
|
| 207 |
|
| 208 |
.disclaimer-icon { color: var(--gold-dim); flex-shrink: 0; }
|
| 209 |
|
| 210 |
.footer-meta {
|
| 211 |
-
font-size:
|
| 212 |
color: var(--text-3);
|
| 213 |
-
line-height: 1.
|
| 214 |
}
|
| 215 |
|
| 216 |
/* ── Main wrapper ── */
|
|
@@ -229,7 +229,7 @@ body {
|
|
| 229 |
display: flex;
|
| 230 |
align-items: center;
|
| 231 |
justify-content: space-between;
|
| 232 |
-
padding: 0
|
| 233 |
flex-shrink: 0;
|
| 234 |
background: var(--navy);
|
| 235 |
}
|
|
@@ -282,6 +282,7 @@ body {
|
|
| 282 |
display: none;
|
| 283 |
flex: 1;
|
| 284 |
overflow: hidden;
|
|
|
|
| 285 |
}
|
| 286 |
|
| 287 |
.screen.active { display: flex; }
|
|
@@ -290,12 +291,12 @@ body {
|
|
| 290 |
.screen-welcome {
|
| 291 |
align-items: center;
|
| 292 |
justify-content: center;
|
| 293 |
-
padding:
|
| 294 |
flex-direction: column;
|
| 295 |
}
|
| 296 |
|
| 297 |
.welcome-inner {
|
| 298 |
-
max-width:
|
| 299 |
width: 100%;
|
| 300 |
text-align: center;
|
| 301 |
animation: fadeUp 0.5s ease;
|
|
@@ -307,44 +308,44 @@ body {
|
|
| 307 |
}
|
| 308 |
|
| 309 |
.welcome-emblem {
|
| 310 |
-
font-size:
|
| 311 |
-
margin-bottom:
|
| 312 |
opacity: 0.7;
|
| 313 |
}
|
| 314 |
|
| 315 |
.welcome-heading {
|
| 316 |
font-family: 'Cormorant Garamond', serif;
|
| 317 |
-
font-size:
|
| 318 |
font-weight: 700;
|
| 319 |
color: var(--text-1);
|
| 320 |
-
margin-bottom:
|
| 321 |
line-height: 1.1;
|
| 322 |
}
|
| 323 |
|
| 324 |
.welcome-body {
|
| 325 |
-
font-size:
|
| 326 |
color: var(--text-2);
|
| 327 |
-
line-height: 1.
|
| 328 |
-
margin-bottom:
|
| 329 |
}
|
| 330 |
|
| 331 |
.suggestion-grid {
|
| 332 |
display: grid;
|
| 333 |
-
grid-template-columns: 1fr 1fr;
|
| 334 |
-
gap:
|
| 335 |
}
|
| 336 |
|
| 337 |
.suggestion-pill {
|
| 338 |
background: var(--navy-2);
|
| 339 |
border: 1px solid var(--border);
|
| 340 |
border-radius: 10px;
|
| 341 |
-
padding:
|
| 342 |
font-family: 'DM Sans', sans-serif;
|
| 343 |
-
font-size:
|
| 344 |
color: var(--text-2);
|
| 345 |
cursor: pointer;
|
| 346 |
text-align: left;
|
| 347 |
-
line-height: 1.
|
| 348 |
transition: all var(--transition);
|
| 349 |
}
|
| 350 |
|
|
@@ -361,7 +362,8 @@ body {
|
|
| 361 |
.messages-container {
|
| 362 |
flex: 1;
|
| 363 |
overflow-y: auto;
|
| 364 |
-
padding:
|
|
|
|
| 365 |
}
|
| 366 |
|
| 367 |
.messages-container::-webkit-scrollbar { width: 4px; }
|
|
@@ -372,12 +374,12 @@ body {
|
|
| 372 |
margin: 0 auto;
|
| 373 |
padding: 0 24px;
|
| 374 |
display: flex;
|
| 375 |
-
flex-
|
| 376 |
gap: 28px;
|
| 377 |
}
|
| 378 |
|
| 379 |
/* ── Message bubbles ── */
|
| 380 |
-
.msg { display: flex; flex-direction: column; gap:
|
| 381 |
|
| 382 |
.msg-user { align-items: flex-end; }
|
| 383 |
.msg-ai { align-items: flex-start; }
|
|
@@ -386,11 +388,11 @@ body {
|
|
| 386 |
background: var(--navy-4);
|
| 387 |
border: 1px solid var(--border);
|
| 388 |
border-radius: 16px 16px 4px 16px;
|
| 389 |
-
padding:
|
| 390 |
max-width: 72%;
|
| 391 |
color: var(--text-1);
|
| 392 |
-
font-size:
|
| 393 |
-
line-height: 1.
|
| 394 |
}
|
| 395 |
|
| 396 |
.bubble-ai {
|
|
@@ -398,15 +400,15 @@ body {
|
|
| 398 |
border: 1px solid var(--border);
|
| 399 |
border-left: 3px solid var(--gold);
|
| 400 |
border-radius: 4px 16px 16px 16px;
|
| 401 |
-
padding:
|
| 402 |
max-width: 88%;
|
| 403 |
color: var(--text-1);
|
| 404 |
-
font-size:
|
| 405 |
-
line-height: 1.
|
| 406 |
position: relative;
|
| 407 |
}
|
| 408 |
|
| 409 |
-
.bubble-ai p { margin-bottom:
|
| 410 |
.bubble-ai p:last-child { margin-bottom: 0; }
|
| 411 |
|
| 412 |
/* AI bubble meta row */
|
|
@@ -415,18 +417,18 @@ body {
|
|
| 415 |
align-items: center;
|
| 416 |
gap: 10px;
|
| 417 |
flex-wrap: wrap;
|
| 418 |
-
margin-top:
|
| 419 |
-
padding-top:
|
| 420 |
border-top: 1px solid var(--border);
|
| 421 |
}
|
| 422 |
|
| 423 |
.verify-badge {
|
| 424 |
display: inline-flex;
|
| 425 |
align-items: center;
|
| 426 |
-
gap:
|
| 427 |
-
font-size:
|
| 428 |
font-weight: 600;
|
| 429 |
-
padding:
|
| 430 |
border-radius: 20px;
|
| 431 |
}
|
| 432 |
|
|
@@ -445,14 +447,14 @@ body {
|
|
| 445 |
.sources-btn {
|
| 446 |
display: inline-flex;
|
| 447 |
align-items: center;
|
| 448 |
-
gap:
|
| 449 |
-
font-size:
|
| 450 |
font-weight: 500;
|
| 451 |
color: var(--gold);
|
| 452 |
background: var(--gold-glow);
|
| 453 |
border: 1px solid var(--border-gold);
|
| 454 |
border-radius: 20px;
|
| 455 |
-
padding:
|
| 456 |
cursor: pointer;
|
| 457 |
transition: background var(--transition);
|
| 458 |
}
|
|
@@ -460,15 +462,15 @@ body {
|
|
| 460 |
.sources-btn:hover { background: rgba(200,168,75,0.22); }
|
| 461 |
|
| 462 |
.latency-label {
|
| 463 |
-
font-size:
|
| 464 |
color: var(--text-3);
|
| 465 |
margin-left: auto;
|
| 466 |
}
|
| 467 |
|
| 468 |
.truncated-note {
|
| 469 |
-
font-size:
|
| 470 |
color: var(--text-3);
|
| 471 |
-
margin-top:
|
| 472 |
font-style: italic;
|
| 473 |
}
|
| 474 |
|
|
@@ -476,10 +478,10 @@ body {
|
|
| 476 |
.bubble-loading {
|
| 477 |
display: flex;
|
| 478 |
align-items: center;
|
| 479 |
-
gap:
|
| 480 |
color: var(--text-3);
|
| 481 |
-
font-size:
|
| 482 |
-
padding:
|
| 483 |
}
|
| 484 |
|
| 485 |
.dots { display: flex; gap: 4px; }
|
|
@@ -629,19 +631,23 @@ body {
|
|
| 629 |
|
| 630 |
/* ── Input zone ── */
|
| 631 |
.input-zone {
|
| 632 |
-
padding:
|
| 633 |
background: linear-gradient(transparent, var(--navy) 30%);
|
| 634 |
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
}
|
| 636 |
|
| 637 |
.input-box {
|
| 638 |
display: flex;
|
| 639 |
align-items: flex-end;
|
| 640 |
-
gap:
|
| 641 |
background: var(--navy-2);
|
| 642 |
border: 1px solid var(--border);
|
| 643 |
-
border-radius:
|
| 644 |
-
padding:
|
| 645 |
transition: border-color var(--transition);
|
| 646 |
}
|
| 647 |
|
|
@@ -664,11 +670,11 @@ body {
|
|
| 664 |
.query-textarea::placeholder { color: var(--text-3); }
|
| 665 |
|
| 666 |
.send-btn {
|
| 667 |
-
width:
|
| 668 |
-
height:
|
| 669 |
background: var(--gold);
|
| 670 |
border: none;
|
| 671 |
-
border-radius:
|
| 672 |
color: var(--navy);
|
| 673 |
cursor: pointer;
|
| 674 |
display: flex;
|
|
@@ -683,17 +689,18 @@ body {
|
|
| 683 |
|
| 684 |
.input-disclaimer {
|
| 685 |
text-align: center;
|
| 686 |
-
font-size:
|
| 687 |
color: var(--text-3);
|
| 688 |
-
margin-top:
|
|
|
|
| 689 |
}
|
| 690 |
|
| 691 |
/* ── Answer formatting ── */
|
| 692 |
.bubble-ai ol, .bubble-ai ul {
|
| 693 |
-
margin:
|
| 694 |
display: flex;
|
| 695 |
flex-direction: column;
|
| 696 |
-
gap:
|
| 697 |
}
|
| 698 |
|
| 699 |
.bubble-ai ol { list-style: decimal; }
|
|
@@ -701,19 +708,19 @@ body {
|
|
| 701 |
|
| 702 |
.bubble-ai li {
|
| 703 |
color: var(--text-1);
|
| 704 |
-
line-height: 1.
|
| 705 |
-
padding-left:
|
| 706 |
}
|
| 707 |
|
| 708 |
.bubble-ai h1, .bubble-ai h2, .bubble-ai h3 {
|
| 709 |
font-family: 'Cormorant Garamond', serif;
|
| 710 |
color: var(--gold);
|
| 711 |
-
margin:
|
| 712 |
}
|
| 713 |
|
| 714 |
-
.bubble-ai h1 { font-size:
|
| 715 |
-
.bubble-ai h2 { font-size:
|
| 716 |
-
.bubble-ai h3 { font-size:
|
| 717 |
|
| 718 |
.bubble-ai strong { color: var(--text-1); font-weight: 600; }
|
| 719 |
.bubble-ai em { color: var(--text-2); font-style: italic; }
|
|
@@ -730,15 +737,15 @@ body {
|
|
| 730 |
.answer-table {
|
| 731 |
width: 100%;
|
| 732 |
border-collapse: collapse;
|
| 733 |
-
margin:
|
| 734 |
-
font-size:
|
| 735 |
}
|
| 736 |
|
| 737 |
.answer-table td {
|
| 738 |
-
padding:
|
| 739 |
border: 1px solid var(--border);
|
| 740 |
color: var(--text-2);
|
| 741 |
-
line-height: 1.
|
| 742 |
}
|
| 743 |
|
| 744 |
.answer-table tr:first-child td {
|
|
@@ -773,45 +780,75 @@ body {
|
|
| 773 |
}
|
| 774 |
.analytics-btn:hover {
|
| 775 |
background: var(--navy-3);
|
| 776 |
-
color:
|
| 777 |
-
}
|
| 778 |
-
|
| 779 |
-
.screen-analytics {
|
| 780 |
-
padding: 32px;
|
| 781 |
overflow-y: auto;
|
| 782 |
height: 100%;
|
| 783 |
}
|
| 784 |
.analytics-inner {
|
| 785 |
-
max-width:
|
| 786 |
margin: 0 auto;
|
| 787 |
}
|
| 788 |
.analytics-header h2 {
|
| 789 |
font-family: 'Cormorant Garamond', serif;
|
| 790 |
-
font-size:
|
| 791 |
margin: 0 0 4px;
|
| 792 |
}
|
| 793 |
.analytics-header p {
|
| 794 |
color: var(--text-2);
|
| 795 |
-
font-size:
|
| 796 |
-
margin: 0 0
|
| 797 |
}
|
| 798 |
|
| 799 |
.analytics-grid {
|
| 800 |
display: grid;
|
| 801 |
-
grid-template-columns: repeat(
|
| 802 |
-
gap:
|
| 803 |
-
margin-bottom:
|
| 804 |
}
|
| 805 |
.stat-card {
|
| 806 |
background: var(--navy-2);
|
| 807 |
border: 1px solid var(--border);
|
| 808 |
-
border-radius:
|
| 809 |
-
padding:
|
| 810 |
text-align: center;
|
| 811 |
}
|
| 812 |
.stat-value {
|
| 813 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
font-weight: 600;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
}
|
| 816 |
|
| 817 |
/* ── Responsive Improvements ── */
|
|
@@ -821,8 +858,9 @@ html {
|
|
| 821 |
|
| 822 |
@media (max-width: 768px) {
|
| 823 |
:root {
|
| 824 |
-
--sidebar-w:
|
| 825 |
--topbar-h: 48px;
|
|
|
|
| 826 |
}
|
| 827 |
|
| 828 |
.app-layout {
|
|
@@ -832,7 +870,7 @@ html {
|
|
| 832 |
.sidebar {
|
| 833 |
width: 100% !important;
|
| 834 |
height: auto;
|
| 835 |
-
max-height:
|
| 836 |
overflow-y: auto;
|
| 837 |
border-right: none;
|
| 838 |
border-bottom: 1px solid var(--border);
|
|
@@ -840,11 +878,11 @@ html {
|
|
| 840 |
|
| 841 |
.main-wrapper {
|
| 842 |
flex: 1;
|
| 843 |
-
overflow-y:
|
| 844 |
}
|
| 845 |
|
| 846 |
.topbar-title {
|
| 847 |
-
font-size:
|
| 848 |
max-width: 60%;
|
| 849 |
}
|
| 850 |
|
|
@@ -854,19 +892,46 @@ html {
|
|
| 854 |
}
|
| 855 |
|
| 856 |
.screen-welcome {
|
| 857 |
-
padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
}
|
| 859 |
|
| 860 |
-
.
|
| 861 |
-
|
| 862 |
}
|
| 863 |
|
| 864 |
-
.
|
|
|
|
| 865 |
max-width: 95% !important;
|
|
|
|
|
|
|
| 866 |
}
|
| 867 |
|
| 868 |
-
.
|
| 869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
}
|
| 871 |
}
|
| 872 |
|
|
|
|
| 58 |
display: flex;
|
| 59 |
align-items: center;
|
| 60 |
gap: 12px;
|
| 61 |
+
padding: 16px 16px 12px;
|
| 62 |
border-bottom: 1px solid var(--border);
|
| 63 |
}
|
| 64 |
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
.new-chat-btn {
|
| 97 |
+
margin: 12px 12px 8px;
|
| 98 |
+
padding: 8px 12px;
|
| 99 |
background: var(--gold-glow);
|
| 100 |
border: 1px solid var(--border-gold);
|
| 101 |
+
border-radius: 8px;
|
| 102 |
color: var(--gold);
|
| 103 |
font-family: 'DM Sans', sans-serif;
|
| 104 |
+
font-size: 12px;
|
| 105 |
font-weight: 500;
|
| 106 |
cursor: pointer;
|
| 107 |
display: flex;
|
| 108 |
align-items: center;
|
| 109 |
+
gap: 6px;
|
| 110 |
transition: background var(--transition), border-color var(--transition);
|
| 111 |
+
width: calc(100% - 24px);
|
| 112 |
}
|
| 113 |
|
| 114 |
.new-chat-btn:hover {
|
|
|
|
| 119 |
.new-chat-icon { font-size: 16px; font-weight: 300; }
|
| 120 |
|
| 121 |
.sidebar-section-label {
|
| 122 |
+
padding: 8px 16px 4px;
|
| 123 |
+
font-size: 9px;
|
| 124 |
font-weight: 600;
|
| 125 |
color: var(--text-3);
|
| 126 |
letter-spacing: 1px;
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
.sidebar-footer {
|
| 192 |
+
padding: 10px 14px;
|
| 193 |
border-top: 1px solid var(--border);
|
| 194 |
display: flex;
|
| 195 |
flex-direction: column;
|
| 196 |
+
gap: 6px;
|
| 197 |
}
|
| 198 |
|
| 199 |
.footer-disclaimer {
|
| 200 |
display: flex;
|
| 201 |
gap: 8px;
|
| 202 |
align-items: flex-start;
|
| 203 |
+
font-size: 10px;
|
| 204 |
color: var(--text-3);
|
| 205 |
+
line-height: 1.3;
|
| 206 |
}
|
| 207 |
|
| 208 |
.disclaimer-icon { color: var(--gold-dim); flex-shrink: 0; }
|
| 209 |
|
| 210 |
.footer-meta {
|
| 211 |
+
font-size: 9px;
|
| 212 |
color: var(--text-3);
|
| 213 |
+
line-height: 1.4;
|
| 214 |
}
|
| 215 |
|
| 216 |
/* ── Main wrapper ── */
|
|
|
|
| 229 |
display: flex;
|
| 230 |
align-items: center;
|
| 231 |
justify-content: space-between;
|
| 232 |
+
padding: 0 20px;
|
| 233 |
flex-shrink: 0;
|
| 234 |
background: var(--navy);
|
| 235 |
}
|
|
|
|
| 282 |
display: none;
|
| 283 |
flex: 1;
|
| 284 |
overflow: hidden;
|
| 285 |
+
position: relative;
|
| 286 |
}
|
| 287 |
|
| 288 |
.screen.active { display: flex; }
|
|
|
|
| 291 |
.screen-welcome {
|
| 292 |
align-items: center;
|
| 293 |
justify-content: center;
|
| 294 |
+
padding: 20px 24px;
|
| 295 |
flex-direction: column;
|
| 296 |
}
|
| 297 |
|
| 298 |
.welcome-inner {
|
| 299 |
+
max-width: 720px;
|
| 300 |
width: 100%;
|
| 301 |
text-align: center;
|
| 302 |
animation: fadeUp 0.5s ease;
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
.welcome-emblem {
|
| 311 |
+
font-size: 40px;
|
| 312 |
+
margin-bottom: 12px;
|
| 313 |
opacity: 0.7;
|
| 314 |
}
|
| 315 |
|
| 316 |
.welcome-heading {
|
| 317 |
font-family: 'Cormorant Garamond', serif;
|
| 318 |
+
font-size: 42px;
|
| 319 |
font-weight: 700;
|
| 320 |
color: var(--text-1);
|
| 321 |
+
margin-bottom: 10px;
|
| 322 |
line-height: 1.1;
|
| 323 |
}
|
| 324 |
|
| 325 |
.welcome-body {
|
| 326 |
+
font-size: 14px;
|
| 327 |
color: var(--text-2);
|
| 328 |
+
line-height: 1.6;
|
| 329 |
+
margin-bottom: 24px;
|
| 330 |
}
|
| 331 |
|
| 332 |
.suggestion-grid {
|
| 333 |
display: grid;
|
| 334 |
+
grid-template-columns: 1fr 1fr 1fr;
|
| 335 |
+
gap: 8px;
|
| 336 |
}
|
| 337 |
|
| 338 |
.suggestion-pill {
|
| 339 |
background: var(--navy-2);
|
| 340 |
border: 1px solid var(--border);
|
| 341 |
border-radius: 10px;
|
| 342 |
+
padding: 10px 12px;
|
| 343 |
font-family: 'DM Sans', sans-serif;
|
| 344 |
+
font-size: 12px;
|
| 345 |
color: var(--text-2);
|
| 346 |
cursor: pointer;
|
| 347 |
text-align: left;
|
| 348 |
+
line-height: 1.4;
|
| 349 |
transition: all var(--transition);
|
| 350 |
}
|
| 351 |
|
|
|
|
| 362 |
.messages-container {
|
| 363 |
flex: 1;
|
| 364 |
overflow-y: auto;
|
| 365 |
+
padding: 16px 0 12px;
|
| 366 |
+
max-height: calc(100vh - var(--topbar-h) - var(--input-h) - 40px);
|
| 367 |
}
|
| 368 |
|
| 369 |
.messages-container::-webkit-scrollbar { width: 4px; }
|
|
|
|
| 374 |
margin: 0 auto;
|
| 375 |
padding: 0 24px;
|
| 376 |
display: flex;
|
| 377 |
+
flex-d0rection: column;
|
| 378 |
gap: 28px;
|
| 379 |
}
|
| 380 |
|
| 381 |
/* ── Message bubbles ── */
|
| 382 |
+
.msg { display: flex; flex-direction: column; gap: 6px; animation: fadeUp 0.3s ease; }
|
| 383 |
|
| 384 |
.msg-user { align-items: flex-end; }
|
| 385 |
.msg-ai { align-items: flex-start; }
|
|
|
|
| 388 |
background: var(--navy-4);
|
| 389 |
border: 1px solid var(--border);
|
| 390 |
border-radius: 16px 16px 4px 16px;
|
| 391 |
+
padding: 10px 14px;
|
| 392 |
max-width: 72%;
|
| 393 |
color: var(--text-1);
|
| 394 |
+
font-size: 13px;
|
| 395 |
+
line-height: 1.5;
|
| 396 |
}
|
| 397 |
|
| 398 |
.bubble-ai {
|
|
|
|
| 400 |
border: 1px solid var(--border);
|
| 401 |
border-left: 3px solid var(--gold);
|
| 402 |
border-radius: 4px 16px 16px 16px;
|
| 403 |
+
padding: 14px 18px;
|
| 404 |
max-width: 88%;
|
| 405 |
color: var(--text-1);
|
| 406 |
+
font-size: 13px;
|
| 407 |
+
line-height: 1.6;
|
| 408 |
position: relative;
|
| 409 |
}
|
| 410 |
|
| 411 |
+
.bubble-ai p { margin-bottom: 10px; }
|
| 412 |
.bubble-ai p:last-child { margin-bottom: 0; }
|
| 413 |
|
| 414 |
/* AI bubble meta row */
|
|
|
|
| 417 |
align-items: center;
|
| 418 |
gap: 10px;
|
| 419 |
flex-wrap: wrap;
|
| 420 |
+
margin-top: 10px;
|
| 421 |
+
padding-top: 8px;
|
| 422 |
border-top: 1px solid var(--border);
|
| 423 |
}
|
| 424 |
|
| 425 |
.verify-badge {
|
| 426 |
display: inline-flex;
|
| 427 |
align-items: center;
|
| 428 |
+
gap: 4px;
|
| 429 |
+
font-size: 10px;
|
| 430 |
font-weight: 600;
|
| 431 |
+
padding: 2px 8px;
|
| 432 |
border-radius: 20px;
|
| 433 |
}
|
| 434 |
|
|
|
|
| 447 |
.sources-btn {
|
| 448 |
display: inline-flex;
|
| 449 |
align-items: center;
|
| 450 |
+
gap: 5px;
|
| 451 |
+
font-size: 10px;
|
| 452 |
font-weight: 500;
|
| 453 |
color: var(--gold);
|
| 454 |
background: var(--gold-glow);
|
| 455 |
border: 1px solid var(--border-gold);
|
| 456 |
border-radius: 20px;
|
| 457 |
+
padding: 2px 10px;
|
| 458 |
cursor: pointer;
|
| 459 |
transition: background var(--transition);
|
| 460 |
}
|
|
|
|
| 462 |
.sources-btn:hover { background: rgba(200,168,75,0.22); }
|
| 463 |
|
| 464 |
.latency-label {
|
| 465 |
+
font-size: 10px;
|
| 466 |
color: var(--text-3);
|
| 467 |
margin-left: auto;
|
| 468 |
}
|
| 469 |
|
| 470 |
.truncated-note {
|
| 471 |
+
font-size: 10px;
|
| 472 |
color: var(--text-3);
|
| 473 |
+
margin-top: 6px;
|
| 474 |
font-style: italic;
|
| 475 |
}
|
| 476 |
|
|
|
|
| 478 |
.bubble-loading {
|
| 479 |
display: flex;
|
| 480 |
align-items: center;
|
| 481 |
+
gap: 10px;
|
| 482 |
color: var(--text-3);
|
| 483 |
+
font-size: 12px;
|
| 484 |
+
padding: 10px 14px;
|
| 485 |
}
|
| 486 |
|
| 487 |
.dots { display: flex; gap: 4px; }
|
|
|
|
| 631 |
|
| 632 |
/* ── Input zone ── */
|
| 633 |
.input-zone {
|
| 634 |
+
padding: 8px 20px 10px;
|
| 635 |
background: linear-gradient(transparent, var(--navy) 30%);
|
| 636 |
flex-shrink: 0;
|
| 637 |
+
height: var(--input-h);
|
| 638 |
+
display: flex;
|
| 639 |
+
flex-direction: column;
|
| 640 |
+
justify-content: flex-end;
|
| 641 |
}
|
| 642 |
|
| 643 |
.input-box {
|
| 644 |
display: flex;
|
| 645 |
align-items: flex-end;
|
| 646 |
+
gap: 8px;
|
| 647 |
background: var(--navy-2);
|
| 648 |
border: 1px solid var(--border);
|
| 649 |
+
border-radius: 12px;
|
| 650 |
+
padding: 8px 10px 8px 14px;
|
| 651 |
transition: border-color var(--transition);
|
| 652 |
}
|
| 653 |
|
|
|
|
| 670 |
.query-textarea::placeholder { color: var(--text-3); }
|
| 671 |
|
| 672 |
.send-btn {
|
| 673 |
+
width: 36px;
|
| 674 |
+
height: 36px;
|
| 675 |
background: var(--gold);
|
| 676 |
border: none;
|
| 677 |
+
border-radius: 8px;
|
| 678 |
color: var(--navy);
|
| 679 |
cursor: pointer;
|
| 680 |
display: flex;
|
|
|
|
| 689 |
|
| 690 |
.input-disclaimer {
|
| 691 |
text-align: center;
|
| 692 |
+
font-size: 10px;
|
| 693 |
color: var(--text-3);
|
| 694 |
+
margin-top: 4px;
|
| 695 |
+
line-height: 1.3;
|
| 696 |
}
|
| 697 |
|
| 698 |
/* ── Answer formatting ── */
|
| 699 |
.bubble-ai ol, .bubble-ai ul {
|
| 700 |
+
margin: 8px 0 8px 18px;
|
| 701 |
display: flex;
|
| 702 |
flex-direction: column;
|
| 703 |
+
gap: 4px;
|
| 704 |
}
|
| 705 |
|
| 706 |
.bubble-ai ol { list-style: decimal; }
|
|
|
|
| 708 |
|
| 709 |
.bubble-ai li {
|
| 710 |
color: var(--text-1);
|
| 711 |
+
line-height: 1.5;
|
| 712 |
+
padding-left: 2px;
|
| 713 |
}
|
| 714 |
|
| 715 |
.bubble-ai h1, .bubble-ai h2, .bubble-ai h3 {
|
| 716 |
font-family: 'Cormorant Garamond', serif;
|
| 717 |
color: var(--gold);
|
| 718 |
+
margin: 12px 0 6px;
|
| 719 |
}
|
| 720 |
|
| 721 |
+
.bubble-ai h1 { font-size: 18px; }
|
| 722 |
+
.bubble-ai h2 { font-size: 15px; }
|
| 723 |
+
.bubble-ai h3 { font-size: 13px; }
|
| 724 |
|
| 725 |
.bubble-ai strong { color: var(--text-1); font-weight: 600; }
|
| 726 |
.bubble-ai em { color: var(--text-2); font-style: italic; }
|
|
|
|
| 737 |
.answer-table {
|
| 738 |
width: 100%;
|
| 739 |
border-collapse: collapse;
|
| 740 |
+
margin: 10px 0;
|
| 741 |
+
font-size: 12px;
|
| 742 |
}
|
| 743 |
|
| 744 |
.answer-table td {
|
| 745 |
+
padding: 6px 10px;
|
| 746 |
border: 1px solid var(--border);
|
| 747 |
color: var(--text-2);
|
| 748 |
+
line-height: 1.4;
|
| 749 |
}
|
| 750 |
|
| 751 |
.answer-table tr:first-child td {
|
|
|
|
| 780 |
}
|
| 781 |
.analytics-btn:hover {
|
| 782 |
background: var(--navy-3);
|
| 783 |
+
color: va20px 24px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
overflow-y: auto;
|
| 785 |
height: 100%;
|
| 786 |
}
|
| 787 |
.analytics-inner {
|
| 788 |
+
max-width: 900px;
|
| 789 |
margin: 0 auto;
|
| 790 |
}
|
| 791 |
.analytics-header h2 {
|
| 792 |
font-family: 'Cormorant Garamond', serif;
|
| 793 |
+
font-size: 24px;
|
| 794 |
margin: 0 0 4px;
|
| 795 |
}
|
| 796 |
.analytics-header p {
|
| 797 |
color: var(--text-2);
|
| 798 |
+
font-size: 13px;
|
| 799 |
+
margin: 0 0 16px;
|
| 800 |
}
|
| 801 |
|
| 802 |
.analytics-grid {
|
| 803 |
display: grid;
|
| 804 |
+
grid-template-columns: repeat(5, 1fr);
|
| 805 |
+
gap: 12px;
|
| 806 |
+
margin-bottom: 20px;
|
| 807 |
}
|
| 808 |
.stat-card {
|
| 809 |
background: var(--navy-2);
|
| 810 |
border: 1px solid var(--border);
|
| 811 |
+
border-radius: 10px;
|
| 812 |
+
padding: 14px 12px;
|
| 813 |
text-align: center;
|
| 814 |
}
|
| 815 |
.stat-value {
|
| 816 |
+
font-size: 22px;
|
| 817 |
+
font-weight: 600;
|
| 818 |
+
}
|
| 819 |
+
.stat-label {
|
| 820 |
+
font-size: 11px;
|
| 821 |
+
color: var(--text-3);
|
| 822 |
+
margin-top: 6px;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.analytics-charts {
|
| 826 |
+
display: grid;
|
| 827 |
+
grid-template-columns: repeat(3, 1fr);
|
| 828 |
+
gap: 12px;
|
| 829 |
+
margin-bottom: 16px;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.chart-card {
|
| 833 |
+
background: var(--navy-2);
|
| 834 |
+
border: 1px solid var(--border);
|
| 835 |
+
border-radius: 10px;
|
| 836 |
+
padding: 14px;
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
.chart-card h3 {
|
| 840 |
+
font-family: 'Cormorant Garamond', serif;
|
| 841 |
+
font-size: 14px;
|
| 842 |
font-weight: 600;
|
| 843 |
+
color: var(--text-1);
|
| 844 |
+
margin-bottom: 10px;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.chart-container {
|
| 848 |
+
min-height: 140px;
|
| 849 |
+
display: flex;
|
| 850 |
+
align-items: center;
|
| 851 |
+
justify-content: center;
|
| 852 |
}
|
| 853 |
|
| 854 |
/* ── Responsive Improvements ── */
|
|
|
|
| 858 |
|
| 859 |
@media (max-width: 768px) {
|
| 860 |
:root {
|
| 861 |
+
--sidebar-w: 200px;
|
| 862 |
--topbar-h: 48px;
|
| 863 |
+
--input-h: 80px;
|
| 864 |
}
|
| 865 |
|
| 866 |
.app-layout {
|
|
|
|
| 870 |
.sidebar {
|
| 871 |
width: 100% !important;
|
| 872 |
height: auto;
|
| 873 |
+
max-height: 35vh;
|
| 874 |
overflow-y: auto;
|
| 875 |
border-right: none;
|
| 876 |
border-bottom: 1px solid var(--border);
|
|
|
|
| 878 |
|
| 879 |
.main-wrapper {
|
| 880 |
flex: 1;
|
| 881 |
+
overflow-y: hidden;
|
| 882 |
}
|
| 883 |
|
| 884 |
.topbar-title {
|
| 885 |
+
font-size: 13px;
|
| 886 |
max-width: 60%;
|
| 887 |
}
|
| 888 |
|
|
|
|
| 892 |
}
|
| 893 |
|
| 894 |
.screen-welcome {
|
| 895 |
+
padding: 18px 16px 12px !important;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.welcome-heading {
|
| 899 |
+
font-size: 32px !important;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.welcome-body {
|
| 903 |
+
font-size: 13px !important;
|
| 904 |
+
margin-bottom: 16px !important;
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
.suggestion-grid {
|
| 908 |
+
grid-template-columns: 1fr !important;
|
| 909 |
+
gap: 6px !important;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.suggestion-pill {
|
| 913 |
+
font-size: 11px !important;
|
| 914 |
+
padding: 8px 10px !important;
|
| 915 |
}
|
| 916 |
|
| 917 |
+
.messages-list {
|
| 918 |
+
gap: 12px !important;
|
| 919 |
}
|
| 920 |
|
| 921 |
+
.bubble-ai,
|
| 922 |
+
.bubble-user {
|
| 923 |
max-width: 95% !important;
|
| 924 |
+
font-size: 12px !important;
|
| 925 |
+
padding: 10px 12px !important;
|
| 926 |
}
|
| 927 |
|
| 928 |
+
.analytics-grid {
|
| 929 |
+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)) !important;
|
| 930 |
+
gap: 8px !important;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
.analytics-charts {
|
| 934 |
+
grid-template-columns: 1fr !important;
|
| 935 |
}
|
| 936 |
}
|
| 937 |
|
src/court/opposing.py
CHANGED
|
@@ -98,41 +98,41 @@ def build_opposing_prompt(
|
|
| 98 |
Build the messages list for opposing counsel LLM call.
|
| 99 |
|
| 100 |
The opposing counsel sees:
|
| 101 |
-
-
|
|
|
|
|
|
|
| 102 |
- Full recent transcript
|
| 103 |
- User's latest argument
|
| 104 |
- Retrieved precedents to use against user
|
| 105 |
- Any detected trap opportunities
|
| 106 |
-
-
|
| 107 |
"""
|
| 108 |
difficulty = session.get("difficulty", "standard")
|
| 109 |
difficulty_modifier = DIFFICULTY_MODIFIERS.get(difficulty, DIFFICULTY_MODIFIERS["standard"])
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
transcript_recent = _get_recent_transcript(session, last_n=4)
|
| 115 |
|
| 116 |
trap_instruction = ""
|
| 117 |
if trap_opportunity:
|
| 118 |
-
trap_instruction = f"\
|
| 119 |
-
elif inconsistencies:
|
| 120 |
-
trap_instruction = f"\nINCONSISTENCY DETECTED: {inconsistencies}\nConsider using the inconsistency trap."
|
| 121 |
|
| 122 |
-
user_content = f"""
|
| 123 |
-
{
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
{concessions}
|
| 129 |
-
|
| 130 |
-
RETRIEVED LEGAL AUTHORITIES (use these against the user):
|
| 131 |
-
{retrieved_context[:2000] if retrieved_context else "Use your general legal knowledge."}
|
| 132 |
-
|
| 133 |
-
USER'S LATEST ARGUMENT:
|
| 134 |
{user_argument}
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
Round {session.get('current_round', 1)} of {session.get('max_rounds', 5)}.
|
| 137 |
{trap_instruction}
|
| 138 |
|
|
@@ -234,18 +234,95 @@ Deliver your closing argument."""
|
|
| 234 |
]
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
def detect_trap_opportunity(
|
| 238 |
user_argument: str,
|
| 239 |
previous_arguments: List[Dict],
|
| 240 |
session: Dict,
|
| 241 |
) -> Optional[Tuple[str, str]]:
|
| 242 |
"""
|
| 243 |
-
Analyse user's argument to detect trap opportunities.
|
| 244 |
|
| 245 |
Returns (trap_type, description) or None.
|
| 246 |
-
|
| 247 |
-
This runs before the LLM call so we can include
|
| 248 |
-
trap instruction in the prompt when relevant.
|
| 249 |
"""
|
| 250 |
arg_lower = user_argument.lower()
|
| 251 |
|
|
|
|
| 98 |
Build the messages list for opposing counsel LLM call.
|
| 99 |
|
| 100 |
The opposing counsel sees:
|
| 101 |
+
- Full case brief (preserved throughout)
|
| 102 |
+
- All documents filed (critical!)
|
| 103 |
+
- All concessions made (critical!)
|
| 104 |
- Full recent transcript
|
| 105 |
- User's latest argument
|
| 106 |
- Retrieved precedents to use against user
|
| 107 |
- Any detected trap opportunities
|
| 108 |
+
- Trap history
|
| 109 |
"""
|
| 110 |
difficulty = session.get("difficulty", "standard")
|
| 111 |
difficulty_modifier = DIFFICULTY_MODIFIERS.get(difficulty, DIFFICULTY_MODIFIERS["standard"])
|
| 112 |
|
| 113 |
+
# The retrieved_context now contains EVERYTHING from _build_full_context
|
| 114 |
+
# including case brief, documents, concessions, and precedents
|
| 115 |
+
# This is the complete informational context
|
|
|
|
| 116 |
|
| 117 |
trap_instruction = ""
|
| 118 |
if trap_opportunity:
|
| 119 |
+
trap_instruction = f"\n🎯 TRAP OPPORTUNITY DETECTED: {trap_opportunity}\nConsider exploiting this in your response."
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
user_content = f"""COMPLETE SESSION CONTEXT (use all of this):
|
| 122 |
+
{retrieved_context[:4000]}
|
| 123 |
|
| 124 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 125 |
+
USER'S LATEST ARGUMENT (respond to this specifically):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
{user_argument}
|
| 127 |
|
| 128 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 129 |
+
CRITICAL REMINDERS:
|
| 130 |
+
1. You have full access to all documents filed by the user
|
| 131 |
+
2. You have the complete history of the user's arguments
|
| 132 |
+
3. You have all concessions made — exploit them mercilessly
|
| 133 |
+
4. You have seen all trap attempts before — use what worked
|
| 134 |
+
5. You know their case brief — you know their weaknesses
|
| 135 |
+
|
| 136 |
Round {session.get('current_round', 1)} of {session.get('max_rounds', 5)}.
|
| 137 |
{trap_instruction}
|
| 138 |
|
|
|
|
| 234 |
]
|
| 235 |
|
| 236 |
|
| 237 |
+
def detect_trap_opportunity_llm(
|
| 238 |
+
user_argument: str,
|
| 239 |
+
session: Dict,
|
| 240 |
+
) -> Optional[Tuple[str, str]]:
|
| 241 |
+
"""
|
| 242 |
+
Use LLM to detect trap opportunities semantically.
|
| 243 |
+
More sophisticated than keyword matching — understands legal logic.
|
| 244 |
+
Falls back to keyword detection if LLM fails.
|
| 245 |
+
|
| 246 |
+
Returns (trap_type, description) or None.
|
| 247 |
+
"""
|
| 248 |
+
try:
|
| 249 |
+
from src.llm import call_llm_raw
|
| 250 |
+
import json
|
| 251 |
+
import re
|
| 252 |
+
|
| 253 |
+
# Build minimal context for trap analysis
|
| 254 |
+
case_brief = session.get("case_brief", "")[:500]
|
| 255 |
+
concessions = session.get("concessions", [])
|
| 256 |
+
user_args = session.get("user_arguments", [])
|
| 257 |
+
|
| 258 |
+
concessions_text = ""
|
| 259 |
+
if concessions:
|
| 260 |
+
concessions_text = "Previous concessions by user: " + "; ".join(
|
| 261 |
+
[c.get("exact_quote", "")[:60] for c in concessions[-3:]]
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
previous_args_text = ""
|
| 265 |
+
if len(user_args) >= 2:
|
| 266 |
+
previous_args_text = "User argued in Round 1: " + user_args[0].get("text", "")[:150]
|
| 267 |
+
|
| 268 |
+
system_prompt = """You are a legal trap detector for a moot court simulation.
|
| 269 |
+
Analyze if the user's argument contains a trap opportunity for opposing counsel.
|
| 270 |
+
|
| 271 |
+
Return ONLY valid JSON:
|
| 272 |
+
{
|
| 273 |
+
"trap_found": true/false,
|
| 274 |
+
"trap_type": "admission_trap|precedent_trap|inconsistency_trap|none",
|
| 275 |
+
"description": "brief description of the trap"
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
Trap types:
|
| 279 |
+
- admission_trap: User made an absolute claim (e.g., "no exceptions", "unlimited", "cannot be restricted")
|
| 280 |
+
- precedent_trap: User cited a case that actually supports opposition when read carefully
|
| 281 |
+
- inconsistency_trap: User contradicted their own previous argument
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
user_prompt = f"""Case brief: {case_brief}
|
| 285 |
+
|
| 286 |
+
{concessions_text}
|
| 287 |
+
|
| 288 |
+
{previous_args_text}
|
| 289 |
+
|
| 290 |
+
User's latest argument: {user_argument}
|
| 291 |
+
|
| 292 |
+
Detect trap opportunity. Return ONLY JSON."""
|
| 293 |
+
|
| 294 |
+
messages = [
|
| 295 |
+
{"role": "system", "content": system_prompt},
|
| 296 |
+
{"role": "user", "content": user_prompt}
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
response = call_llm_raw(messages)
|
| 300 |
+
|
| 301 |
+
# Extract JSON from response
|
| 302 |
+
match = re.search(r'\{.*\}', response, re.DOTALL)
|
| 303 |
+
if match:
|
| 304 |
+
data = json.loads(match.group())
|
| 305 |
+
if data.get("trap_found"):
|
| 306 |
+
trap_type = data.get("trap_type", "admission_trap")
|
| 307 |
+
if trap_type != "none":
|
| 308 |
+
return (trap_type, data.get("description", "Trap detected"))
|
| 309 |
+
|
| 310 |
+
except Exception as e:
|
| 311 |
+
logger.debug(f"LLM trap detection failed, falling back to keyword: {e}")
|
| 312 |
+
|
| 313 |
+
# Fall back to keyword-based detection
|
| 314 |
+
return detect_trap_opportunity(user_argument, session.get("user_arguments", []), session)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
def detect_trap_opportunity(
|
| 318 |
user_argument: str,
|
| 319 |
previous_arguments: List[Dict],
|
| 320 |
session: Dict,
|
| 321 |
) -> Optional[Tuple[str, str]]:
|
| 322 |
"""
|
| 323 |
+
Analyse user's argument to detect trap opportunities (keyword-based fallback).
|
| 324 |
|
| 325 |
Returns (trap_type, description) or None.
|
|
|
|
|
|
|
|
|
|
| 326 |
"""
|
| 327 |
arg_lower = user_argument.lower()
|
| 328 |
|
src/court/orchestrator.py
CHANGED
|
@@ -64,6 +64,86 @@ def _call_llm(messages: List[Dict]) -> str:
|
|
| 64 |
return call_llm_raw(messages)
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def _retrieve_for_court(query: str, session: Dict) -> str:
|
| 68 |
"""
|
| 69 |
Retrieve relevant precedents for court use.
|
|
@@ -156,9 +236,13 @@ def _handle_briefing(session_id: str, user_argument: str, session: Dict) -> Dict
|
|
| 156 |
|
| 157 |
add_user_argument(session_id, user_argument, [])
|
| 158 |
|
| 159 |
-
#
|
|
|
|
|
|
|
|
|
|
| 160 |
query = f"{session.get('case_title', '')} {' '.join(session.get('legal_issues', []))}"
|
| 161 |
-
|
|
|
|
| 162 |
|
| 163 |
# Check for trap opportunity
|
| 164 |
trap_info = detect_trap_opportunity(user_argument, [], session)
|
|
@@ -167,7 +251,7 @@ def _handle_briefing(session_id: str, user_argument: str, session: Dict) -> Dict
|
|
| 167 |
opposing_messages = build_opposing_prompt(
|
| 168 |
session=session,
|
| 169 |
user_argument=user_argument,
|
| 170 |
-
retrieved_context=
|
| 171 |
trap_opportunity=trap_info[1] if trap_info else None,
|
| 172 |
)
|
| 173 |
|
|
@@ -193,7 +277,7 @@ def _handle_briefing(session_id: str, user_argument: str, session: Dict) -> Dict
|
|
| 193 |
judge_messages = build_judge_prompt(
|
| 194 |
session=get_session(session_id), # Fresh session after updates
|
| 195 |
last_user_argument=user_argument,
|
| 196 |
-
retrieved_context=
|
| 197 |
)
|
| 198 |
|
| 199 |
try:
|
|
@@ -270,12 +354,17 @@ def _handle_round(session_id: str, user_argument: str, session: Dict) -> Dict:
|
|
| 270 |
key_claims=_extract_key_claims(user_argument),
|
| 271 |
)
|
| 272 |
|
| 273 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
legal_issues = " ".join(session.get("legal_issues", []))
|
| 275 |
query = f"{user_argument[:200]} {legal_issues}"
|
| 276 |
-
|
|
|
|
| 277 |
|
| 278 |
-
# Detect traps
|
| 279 |
trap_info = detect_trap_opportunity(
|
| 280 |
user_argument,
|
| 281 |
session.get("user_arguments", []),
|
|
@@ -283,11 +372,10 @@ def _handle_round(session_id: str, user_argument: str, session: Dict) -> Dict:
|
|
| 283 |
)
|
| 284 |
|
| 285 |
# ── LLM Call 1: Opposing counsel ──────────────────────────
|
| 286 |
-
fresh_session = get_session(session_id)
|
| 287 |
opposing_messages = build_opposing_prompt(
|
| 288 |
session=fresh_session,
|
| 289 |
user_argument=user_argument,
|
| 290 |
-
retrieved_context=
|
| 291 |
trap_opportunity=trap_info[1] if trap_info else None,
|
| 292 |
)
|
| 293 |
|
|
@@ -323,7 +411,7 @@ def _handle_round(session_id: str, user_argument: str, session: Dict) -> Dict:
|
|
| 323 |
judge_messages = build_judge_prompt(
|
| 324 |
session=fresh_session,
|
| 325 |
last_user_argument=user_argument,
|
| 326 |
-
retrieved_context=
|
| 327 |
)
|
| 328 |
|
| 329 |
try:
|
|
@@ -410,13 +498,16 @@ def _handle_cross_exam_answer(
|
|
| 410 |
# If more questions remaining (max 3), get next question
|
| 411 |
if question_number < 3:
|
| 412 |
query = " ".join(session.get("legal_issues", []))
|
| 413 |
-
|
| 414 |
|
| 415 |
fresh_session = get_session(session_id)
|
|
|
|
|
|
|
|
|
|
| 416 |
cross_messages = build_cross_examination_prompt(
|
| 417 |
session=fresh_session,
|
| 418 |
question_number=question_number + 1,
|
| 419 |
-
retrieved_context=
|
| 420 |
)
|
| 421 |
|
| 422 |
try:
|
|
|
|
| 64 |
return call_llm_raw(messages)
|
| 65 |
|
| 66 |
|
| 67 |
+
def _build_full_context(session: Dict) -> str:
|
| 68 |
+
"""
|
| 69 |
+
Builds complete context string fed to every LLM call.
|
| 70 |
+
Ensures nothing goes wasted — case brief, all arguments, all documents,
|
| 71 |
+
all concessions, all traps are included in every agent's view.
|
| 72 |
+
|
| 73 |
+
This is the single source of context enrichment for the moot court.
|
| 74 |
+
"""
|
| 75 |
+
parts = []
|
| 76 |
+
|
| 77 |
+
# ── Case foundation ────────────────────────────────────────
|
| 78 |
+
parts.append("=== CASE FOUNDATION ===")
|
| 79 |
+
parts.append(f"Case: {session.get('case_title', '')}")
|
| 80 |
+
parts.append(f"Your side: {session.get('user_side', '').upper()}")
|
| 81 |
+
parts.append(f"Legal issues: {', '.join(session.get('legal_issues', []))}")
|
| 82 |
+
if session.get('brief_facts'):
|
| 83 |
+
parts.append(f"Facts: {session.get('brief_facts', '')[:400]}")
|
| 84 |
+
parts.append("")
|
| 85 |
+
|
| 86 |
+
# ── Case brief (preserved throughout) ───────────────────────
|
| 87 |
+
case_brief = session.get("case_brief", "")
|
| 88 |
+
if case_brief:
|
| 89 |
+
parts.append("=== CASE BRIEF & RESEARCH ===")
|
| 90 |
+
parts.append(case_brief[:1200])
|
| 91 |
+
parts.append("")
|
| 92 |
+
|
| 93 |
+
# ── Documents produced (CRITICAL — opposing counsel sees these) ──
|
| 94 |
+
docs = session.get("documents_produced", [])
|
| 95 |
+
if docs:
|
| 96 |
+
parts.append("=== DOCUMENTS ON RECORD ===")
|
| 97 |
+
for doc in docs:
|
| 98 |
+
parts.append(f"[{doc.get('type', 'DOCUMENT')} — filed by {doc.get('for_side', 'COUNSEL')}]")
|
| 99 |
+
parts.append(doc.get("content", "")[:500])
|
| 100 |
+
parts.append("")
|
| 101 |
+
|
| 102 |
+
# ── All concessions made (CRITICAL — opposing counsel exploits these) ──
|
| 103 |
+
concessions = session.get("concessions", [])
|
| 104 |
+
if concessions:
|
| 105 |
+
parts.append("=== CONCESSIONS ON RECORD (EXPLOIT THESE) ===")
|
| 106 |
+
for c in concessions:
|
| 107 |
+
parts.append(
|
| 108 |
+
f"Round {c.get('round_number', '?')}: \"{c.get('exact_quote', '')[:120]}\" "
|
| 109 |
+
f"({c.get('legal_significance', 'Concession')[:80]})"
|
| 110 |
+
)
|
| 111 |
+
parts.append("")
|
| 112 |
+
|
| 113 |
+
# ── Trap history (opposing counsel knows what worked before) ──
|
| 114 |
+
traps = session.get("trap_events", [])
|
| 115 |
+
if traps:
|
| 116 |
+
parts.append("=== TRAP HISTORY ===")
|
| 117 |
+
for t in traps:
|
| 118 |
+
fell = "USER FELL IN" if t.get("user_fell_in") else "user avoided"
|
| 119 |
+
parts.append(
|
| 120 |
+
f"Round {t.get('round_number', '?')} [{t.get('trap_type', 'trap').upper()}] {fell}: "
|
| 121 |
+
f"{t.get('trap_text', '')[:120]}"
|
| 122 |
+
)
|
| 123 |
+
parts.append("")
|
| 124 |
+
|
| 125 |
+
# ── Full user argument history (consistency checking) ────────
|
| 126 |
+
user_args = session.get("user_arguments", [])
|
| 127 |
+
if user_args:
|
| 128 |
+
parts.append("=== USER'S ARGUMENT HISTORY ===")
|
| 129 |
+
for arg in user_args:
|
| 130 |
+
parts.append(f"Round {arg.get('round', '?')}: {arg.get('text', '')[:250]}")
|
| 131 |
+
parts.append("")
|
| 132 |
+
|
| 133 |
+
# ── Recent transcript (verbatim, untruncated where possible) ─
|
| 134 |
+
transcript = session.get("transcript", [])
|
| 135 |
+
if transcript:
|
| 136 |
+
recent = transcript[-10:] # More entries than before (was 4, now 10)
|
| 137 |
+
parts.append("=== RECENT PROCEEDINGS ===")
|
| 138 |
+
for entry in recent:
|
| 139 |
+
role = entry.get('role_label', entry.get('speaker', 'SPEAKER')).upper()
|
| 140 |
+
content = entry.get('content', '')[:300]
|
| 141 |
+
parts.append(f"{role}: {content}")
|
| 142 |
+
parts.append("")
|
| 143 |
+
|
| 144 |
+
return "\n".join(parts)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
def _retrieve_for_court(query: str, session: Dict) -> str:
|
| 148 |
"""
|
| 149 |
Retrieve relevant precedents for court use.
|
|
|
|
| 236 |
|
| 237 |
add_user_argument(session_id, user_argument, [])
|
| 238 |
|
| 239 |
+
# Build full context with all case info
|
| 240 |
+
full_context = _build_full_context(session)
|
| 241 |
+
|
| 242 |
+
# Retrieve additional precedents
|
| 243 |
query = f"{session.get('case_title', '')} {' '.join(session.get('legal_issues', []))}"
|
| 244 |
+
retrieved_precedents = _retrieve_for_court(query, session)
|
| 245 |
+
combined_context = full_context + "\n\n=== RETRIEVED PRECEDENTS ===\n" + retrieved_precedents if retrieved_precedents else full_context
|
| 246 |
|
| 247 |
# Check for trap opportunity
|
| 248 |
trap_info = detect_trap_opportunity(user_argument, [], session)
|
|
|
|
| 251 |
opposing_messages = build_opposing_prompt(
|
| 252 |
session=session,
|
| 253 |
user_argument=user_argument,
|
| 254 |
+
retrieved_context=combined_context,
|
| 255 |
trap_opportunity=trap_info[1] if trap_info else None,
|
| 256 |
)
|
| 257 |
|
|
|
|
| 277 |
judge_messages = build_judge_prompt(
|
| 278 |
session=get_session(session_id), # Fresh session after updates
|
| 279 |
last_user_argument=user_argument,
|
| 280 |
+
retrieved_context=combined_context,
|
| 281 |
)
|
| 282 |
|
| 283 |
try:
|
|
|
|
| 354 |
key_claims=_extract_key_claims(user_argument),
|
| 355 |
)
|
| 356 |
|
| 357 |
+
# Build full context — EVERYTHING is preserved and fed to agents
|
| 358 |
+
fresh_session = get_session(session_id)
|
| 359 |
+
full_context = _build_full_context(fresh_session)
|
| 360 |
+
|
| 361 |
+
# Retrieve additional precedents and combine
|
| 362 |
legal_issues = " ".join(session.get("legal_issues", []))
|
| 363 |
query = f"{user_argument[:200]} {legal_issues}"
|
| 364 |
+
retrieved_precedents = _retrieve_for_court(query, session)
|
| 365 |
+
combined_context = full_context + "\n\n=== RETRIEVED PRECEDENTS ===\n" + retrieved_precedents if retrieved_precedents else full_context
|
| 366 |
|
| 367 |
+
# Detect traps (based on full history)
|
| 368 |
trap_info = detect_trap_opportunity(
|
| 369 |
user_argument,
|
| 370 |
session.get("user_arguments", []),
|
|
|
|
| 372 |
)
|
| 373 |
|
| 374 |
# ── LLM Call 1: Opposing counsel ──────────────────────────
|
|
|
|
| 375 |
opposing_messages = build_opposing_prompt(
|
| 376 |
session=fresh_session,
|
| 377 |
user_argument=user_argument,
|
| 378 |
+
retrieved_context=combined_context,
|
| 379 |
trap_opportunity=trap_info[1] if trap_info else None,
|
| 380 |
)
|
| 381 |
|
|
|
|
| 411 |
judge_messages = build_judge_prompt(
|
| 412 |
session=fresh_session,
|
| 413 |
last_user_argument=user_argument,
|
| 414 |
+
retrieved_context=combined_context,
|
| 415 |
)
|
| 416 |
|
| 417 |
try:
|
|
|
|
| 498 |
# If more questions remaining (max 3), get next question
|
| 499 |
if question_number < 3:
|
| 500 |
query = " ".join(session.get("legal_issues", []))
|
| 501 |
+
retrieved_precedents = _retrieve_for_court(query, session)
|
| 502 |
|
| 503 |
fresh_session = get_session(session_id)
|
| 504 |
+
full_context = _build_full_context(fresh_session)
|
| 505 |
+
combined_context = full_context + "\n\n=== RETRIEVED PRECEDENTS ===\n" + retrieved_precedents if retrieved_precedents else full_context
|
| 506 |
+
|
| 507 |
cross_messages = build_cross_examination_prompt(
|
| 508 |
session=fresh_session,
|
| 509 |
question_number=question_number + 1,
|
| 510 |
+
retrieved_context=combined_context,
|
| 511 |
)
|
| 512 |
|
| 513 |
try:
|