CaffeinatedCoding commited on
Commit
9b38acd
·
verified ·
1 Parent(s): 7a1e897

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. frontend/style.css +165 -100
  2. src/court/opposing.py +101 -24
  3. 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: 20px 18px 16px;
62
  border-bottom: 1px solid var(--border);
63
  }
64
 
@@ -94,21 +94,21 @@ body {
94
  }
95
 
96
  .new-chat-btn {
97
- margin: 14px 14px 10px;
98
- padding: 10px 14px;
99
  background: var(--gold-glow);
100
  border: 1px solid var(--border-gold);
101
- border-radius: 9px;
102
  color: var(--gold);
103
  font-family: 'DM Sans', sans-serif;
104
- font-size: 13px;
105
  font-weight: 500;
106
  cursor: pointer;
107
  display: flex;
108
  align-items: center;
109
- gap: 8px;
110
  transition: background var(--transition), border-color var(--transition);
111
- width: calc(100% - 28px);
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: 10px 18px 6px;
123
- font-size: 10px;
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: 14px 16px;
193
  border-top: 1px solid var(--border);
194
  display: flex;
195
  flex-direction: column;
196
- gap: 8px;
197
  }
198
 
199
  .footer-disclaimer {
200
  display: flex;
201
  gap: 8px;
202
  align-items: flex-start;
203
- font-size: 11px;
204
  color: var(--text-3);
205
- line-height: 1.4;
206
  }
207
 
208
  .disclaimer-icon { color: var(--gold-dim); flex-shrink: 0; }
209
 
210
  .footer-meta {
211
- font-size: 10px;
212
  color: var(--text-3);
213
- line-height: 1.5;
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 24px;
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: 40px 24px 120px;
294
  flex-direction: column;
295
  }
296
 
297
  .welcome-inner {
298
- max-width: 640px;
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: 48px;
311
- margin-bottom: 20px;
312
  opacity: 0.7;
313
  }
314
 
315
  .welcome-heading {
316
  font-family: 'Cormorant Garamond', serif;
317
- font-size: 52px;
318
  font-weight: 700;
319
  color: var(--text-1);
320
- margin-bottom: 14px;
321
  line-height: 1.1;
322
  }
323
 
324
  .welcome-body {
325
- font-size: 15px;
326
  color: var(--text-2);
327
- line-height: 1.7;
328
- margin-bottom: 40px;
329
  }
330
 
331
  .suggestion-grid {
332
  display: grid;
333
- grid-template-columns: 1fr 1fr;
334
- gap: 10px;
335
  }
336
 
337
  .suggestion-pill {
338
  background: var(--navy-2);
339
  border: 1px solid var(--border);
340
  border-radius: 10px;
341
- padding: 13px 16px;
342
  font-family: 'DM Sans', sans-serif;
343
- font-size: 13px;
344
  color: var(--text-2);
345
  cursor: pointer;
346
  text-align: left;
347
- line-height: 1.5;
348
  transition: all var(--transition);
349
  }
350
 
@@ -361,7 +362,8 @@ body {
361
  .messages-container {
362
  flex: 1;
363
  overflow-y: auto;
364
- padding: 32px 0 16px;
 
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-direction: column;
376
  gap: 28px;
377
  }
378
 
379
  /* ── Message bubbles ── */
380
- .msg { display: flex; flex-direction: column; gap: 8px; animation: fadeUp 0.3s ease; }
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: 12px 16px;
390
  max-width: 72%;
391
  color: var(--text-1);
392
- font-size: 14px;
393
- line-height: 1.6;
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: 18px 20px;
402
  max-width: 88%;
403
  color: var(--text-1);
404
- font-size: 14px;
405
- line-height: 1.75;
406
  position: relative;
407
  }
408
 
409
- .bubble-ai p { margin-bottom: 12px; }
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: 14px;
419
- padding-top: 12px;
420
  border-top: 1px solid var(--border);
421
  }
422
 
423
  .verify-badge {
424
  display: inline-flex;
425
  align-items: center;
426
- gap: 5px;
427
- font-size: 11px;
428
  font-weight: 600;
429
- padding: 3px 10px;
430
  border-radius: 20px;
431
  }
432
 
@@ -445,14 +447,14 @@ body {
445
  .sources-btn {
446
  display: inline-flex;
447
  align-items: center;
448
- gap: 6px;
449
- font-size: 11px;
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: 3px 12px;
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: 11px;
464
  color: var(--text-3);
465
  margin-left: auto;
466
  }
467
 
468
  .truncated-note {
469
- font-size: 11px;
470
  color: var(--text-3);
471
- margin-top: 8px;
472
  font-style: italic;
473
  }
474
 
@@ -476,10 +478,10 @@ body {
476
  .bubble-loading {
477
  display: flex;
478
  align-items: center;
479
- gap: 12px;
480
  color: var(--text-3);
481
- font-size: 13px;
482
- padding: 14px 18px;
483
  }
484
 
485
  .dots { display: flex; gap: 4px; }
@@ -629,19 +631,23 @@ body {
629
 
630
  /* ── Input zone ── */
631
  .input-zone {
632
- padding: 12px 24px 14px;
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: 10px;
641
  background: var(--navy-2);
642
  border: 1px solid var(--border);
643
- border-radius: 14px;
644
- padding: 10px 10px 10px 16px;
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: 38px;
668
- height: 38px;
669
  background: var(--gold);
670
  border: none;
671
- border-radius: 9px;
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: 11px;
687
  color: var(--text-3);
688
- margin-top: 8px;
 
689
  }
690
 
691
  /* ── Answer formatting ── */
692
  .bubble-ai ol, .bubble-ai ul {
693
- margin: 10px 0 10px 20px;
694
  display: flex;
695
  flex-direction: column;
696
- gap: 6px;
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.6;
705
- padding-left: 4px;
706
  }
707
 
708
  .bubble-ai h1, .bubble-ai h2, .bubble-ai h3 {
709
  font-family: 'Cormorant Garamond', serif;
710
  color: var(--gold);
711
- margin: 16px 0 8px;
712
  }
713
 
714
- .bubble-ai h1 { font-size: 20px; }
715
- .bubble-ai h2 { font-size: 17px; }
716
- .bubble-ai h3 { font-size: 15px; }
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: 12px 0;
734
- font-size: 13px;
735
  }
736
 
737
  .answer-table td {
738
- padding: 8px 12px;
739
  border: 1px solid var(--border);
740
  color: var(--text-2);
741
- line-height: 1.5;
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: var(--text-1);
777
- }
778
-
779
- .screen-analytics {
780
- padding: 32px;
781
  overflow-y: auto;
782
  height: 100%;
783
  }
784
  .analytics-inner {
785
- max-width: 800px;
786
  margin: 0 auto;
787
  }
788
  .analytics-header h2 {
789
  font-family: 'Cormorant Garamond', serif;
790
- font-size: 28px;
791
  margin: 0 0 4px;
792
  }
793
  .analytics-header p {
794
  color: var(--text-2);
795
- font-size: 14px;
796
- margin: 0 0 32px;
797
  }
798
 
799
  .analytics-grid {
800
  display: grid;
801
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
802
- gap: 16px;
803
- margin-bottom: 32px;
804
  }
805
  .stat-card {
806
  background: var(--navy-2);
807
  border: 1px solid var(--border);
808
- border-radius: 12px;
809
- padding: 20px 16px;
810
  text-align: center;
811
  }
812
  .stat-value {
813
- font-size: 28px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 208px;
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: 40vh;
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: auto;
844
  }
845
 
846
  .topbar-title {
847
- font-size: 14px;
848
  max-width: 60%;
849
  }
850
 
@@ -854,19 +892,46 @@ html {
854
  }
855
 
856
  .screen-welcome {
857
- padding: 20px 16px 60px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  }
859
 
860
- .welcome-inner {
861
- max-width: 100% !important;
862
  }
863
 
864
- .chat-bubble {
 
865
  max-width: 95% !important;
 
 
866
  }
867
 
868
- .msg-text {
869
- font-size: 13px;
 
 
 
 
 
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
- - Case brief (user's research, gaps)
 
 
102
  - Full recent transcript
103
  - User's latest argument
104
  - Retrieved precedents to use against user
105
  - Any detected trap opportunities
106
- - All concessions made so far
107
  """
108
  difficulty = session.get("difficulty", "standard")
109
  difficulty_modifier = DIFFICULTY_MODIFIERS.get(difficulty, DIFFICULTY_MODIFIERS["standard"])
110
 
111
- case_brief = session.get("case_brief", "")
112
- concessions = _format_concessions(session.get("concessions", []))
113
- inconsistencies = _detect_inconsistencies(session.get("user_arguments", []))
114
- transcript_recent = _get_recent_transcript(session, last_n=4)
115
 
116
  trap_instruction = ""
117
  if trap_opportunity:
118
- trap_instruction = f"\nTRAP OPPORTUNITY DETECTED: {trap_opportunity}\nConsider exploiting this in your response."
119
- elif inconsistencies:
120
- trap_instruction = f"\nINCONSISTENCY DETECTED: {inconsistencies}\nConsider using the inconsistency trap."
121
 
122
- user_content = f"""CASE BRIEF (your preparation before court):
123
- {case_brief[:1500]}
124
 
125
- RECENT TRANSCRIPT:
126
- {transcript_recent}
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
- # Retrieve context for both agents
 
 
 
160
  query = f"{session.get('case_title', '')} {' '.join(session.get('legal_issues', []))}"
161
- retrieved_context = _retrieve_for_court(query, session)
 
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=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=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
- # Retrieve context
 
 
 
 
274
  legal_issues = " ".join(session.get("legal_issues", []))
275
  query = f"{user_argument[:200]} {legal_issues}"
276
- retrieved_context = _retrieve_for_court(query, session)
 
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=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=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
- retrieved_context = _retrieve_for_court(query, session)
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=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: