caarleexx commited on
Commit
90a44c2
·
verified ·
1 Parent(s): 9099838

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +893 -1232
app.py CHANGED
@@ -61,7 +61,7 @@ def get_fresh_token(force_refresh=False):
61
  browser.close()
62
  if token:
63
  token_cache["token"] = token
64
- token_cache["expires_at"] = time.time() + 600 # 10 minutos
65
  logger.info(f"Token obtido: {token[:30]}...")
66
  return token
67
  else:
@@ -72,76 +72,30 @@ def get_fresh_token(force_refresh=False):
72
  return None
73
 
74
  def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20):
75
- """
76
- Faz a busca na API do STF filtrando pelas bases selecionadas com paginação
77
- bases: lista com valores 'acordaos' e/ou 'decisoes'
78
- page: número da página (começa em 1)
79
- page_size: resultados por página
80
- """
81
  token = get_fresh_token()
82
  if not token:
83
  return {"error": "Não foi possível obter token de acesso"}, 503
84
 
85
- # Calcular offset baseado na página
86
  from_idx = (page - 1) * page_size
87
 
88
- # Constrói filtro OR para as bases selecionadas
89
  should_filters = []
90
  for base in bases:
91
  should_filters.append({"term": {"base": base}})
92
 
93
- # Payload com filtro flexível para as bases selecionadas
94
  payload = {
95
  "query": {
96
  "bool": {
97
- "filter": [
98
- {
99
- "bool": {
100
- "should": should_filters,
101
- "minimum_should_match": 1
102
- }
103
- }
104
- ],
105
- "must": [
106
- {
107
- "bool": {
108
- "should": [
109
- {
110
- "query_string": {
111
- "fields": ["ementa_texto"],
112
- "query": query,
113
- "default_operator": "AND",
114
- "fuzziness": "AUTO:4,7"
115
- }
116
- },
117
- {
118
- "query_string": {
119
- "fields": ["decisao_texto"],
120
- "query": query,
121
- "default_operator": "AND",
122
- "fuzziness": "AUTO:4,7"
123
- }
124
- },
125
- {
126
- "query_string": {
127
- "fields": ["acordao_ata"],
128
- "query": query,
129
- "default_operator": "AND",
130
- "fuzziness": "AUTO:4,7"
131
- }
132
- }
133
- ]
134
- }
135
- }
136
- ]
137
  }
138
  },
139
- "_source": [
140
- "id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata",
141
- "processo_codigo_completo", "relator_processo_nome",
142
- "orgao_julgador", "julgamento_data", "publicacao_data",
143
- "inteiro_teor_url", "base"
144
- ],
145
  "size": page_size,
146
  "from": from_idx,
147
  "sort": [{"julgamento_data": {"order": "desc"}}],
@@ -156,7 +110,6 @@ def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20):
156
  if response.status_code == 200:
157
  return response.json()
158
  elif response.status_code == 202:
159
- # Tenta renovar o token uma vez
160
  token = get_fresh_token(force_refresh=True)
161
  if token:
162
  headers['Cookie'] = f'aws-waf-token={token}'
@@ -169,1193 +122,917 @@ def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20):
169
  except Exception as e:
170
  return {"error": str(e)}, 502
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  # ============================================
173
  # Rota principal (HTML)
174
  # ============================================
175
  @app.route('/')
176
  def index():
177
- return render_template_string("""
178
- <!DOCTYPE html>
179
- <html>
180
  <head>
181
- <title>⚖️ PARA AI - Busca STF</title>
182
- <meta charset="utf-8">
183
- <meta name="viewport" content="width=device-width, initial-scale=1">
184
- <style>
185
- :root {
186
- --primary: #1E3A8A; /* Azul escuro principal */
187
- --secondary: #2563EB; /* Azul médio */
188
- --accent: #3B82F6; /* Azul claro */
189
- --background: #F8FAFC; /* Fundo claro */
190
- --text: #0F172A; /* Texto escuro */
191
- --card-bg: #FFFFFF; /* Fundo dos cards */
192
- --border: #E2E8F0; /* Bordas suaves */
193
- --success: #10B981; /* Verde */
194
- --warning: #F59E0B; /* Laranja */
195
- --error: #EF4444; /* Vermelho */
196
- --purple: #8B5CF6; /* Roxo para decisões */
197
- }
198
-
199
- body {
200
- font-family: 'Inter', 'Segoe UI', Roboto, system-ui, sans-serif;
201
- max-width: 1400px;
202
- margin: 0 auto;
203
- padding: 20px;
204
- background: var(--background);
205
- color: var(--text);
206
- line-height: 1.5;
207
- }
208
-
209
- .container {
210
- background: var(--card-bg);
211
- border-radius: 24px;
212
- padding: 30px;
213
- box-shadow: 0 20px 40px rgba(0,0,0,0.05), 0 4px 12px rgba(0,0,0,0.1);
214
- border: 1px solid var(--border);
215
- }
216
-
217
- /* Logo e cabeçalho */
218
- .header {
219
- text-align: center;
220
- margin-bottom: 15px;
221
- border-bottom: 2px solid var(--border);
222
- padding-bottom: 25px;
223
- }
224
-
225
- .logo-container {
226
- display: flex;
227
- flex-direction: column;
228
- align-items: center;
229
- gap: 8px;
230
- }
231
-
232
- .logo {
233
- height: 180px;
234
- width: auto;
235
- }
236
-
237
- .logo-text {
238
- font-size: 28px;
239
- font-weight: 700;
240
- background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
241
- -webkit-background-clip: text;
242
- -webkit-text-fill-color: transparent;
243
- letter-spacing: -0.5px;
244
- }
245
-
246
- .tagline {
247
- font-size: 14px;
248
- color: #64748B;
249
- font-weight: 400;
250
- letter-spacing: 1px;
251
- }
252
-
253
- .stats-badge {
254
- background: linear-gradient(135deg, var(--primary), var(--secondary));
255
- color: white;
256
- padding: 8px 20px;
257
- border-radius: 40px;
258
- font-size: 16px;
259
- font-weight: 500;
260
- display: inline-block;
261
- margin-top: 10px;
262
- box-shadow: 0 4px 10px rgba(30,58,138,0.2);
263
- }
264
-
265
- .info-box {
266
- background: #EFF6FF;
267
- border-left: 6px solid var(--primary);
268
- padding: 20px 25px;
269
- margin: 25px 0;
270
- border-radius: 16px;
271
- font-size: 15px;
272
- }
273
-
274
- .base-selector {
275
- background: var(--card-bg);
276
- border-radius: 20px;
277
- padding: 20px;
278
- margin: 20px 0;
279
- border: 1px solid var(--border);
280
- }
281
-
282
- .base-selector h3 {
283
- margin-top: 0;
284
- color: var(--primary);
285
- font-size: 16px;
286
- margin-bottom: 15px;
287
- font-weight: 600;
288
- }
289
-
290
- .checkbox-group {
291
- display: flex;
292
- gap: 30px;
293
- flex-wrap: wrap;
294
- }
295
-
296
- .checkbox-item {
297
- display: flex;
298
- align-items: center;
299
- gap: 12px;
300
- cursor: pointer;
301
- }
302
-
303
- .checkbox-item input[type="checkbox"] {
304
- width: 20px;
305
- height: 20px;
306
- cursor: pointer;
307
- accent-color: var(--primary);
308
- }
309
-
310
- .checkbox-item label {
311
- font-size: 16px;
312
- cursor: pointer;
313
- font-weight: 500;
314
- }
315
-
316
- .badge {
317
- background: var(--primary);
318
- color: white;
319
- padding: 4px 12px;
320
- border-radius: 20px;
321
- font-size: 13px;
322
- font-weight: 500;
323
- margin-left: 8px;
324
- }
325
-
326
- .badge.acordaos {
327
- background: var(--secondary);
328
- }
329
-
330
- .badge.decisoes {
331
- background: var(--purple);
332
- }
333
-
334
- /* Caixa de busca - botão abaixo */
335
- .search-section {
336
- margin: 30px 0;
337
- }
338
-
339
- .search-input {
340
- width: 100%;
341
- padding: 18px 20px;
342
- font-size: 16px;
343
- border: 2px solid var(--border);
344
- border-radius: 16px;
345
- transition: all 0.3s;
346
- background: var(--card-bg);
347
- margin-bottom: 15px;
348
- box-sizing: border-box;
349
- }
350
-
351
- .search-input:focus {
352
- outline: none;
353
- border-color: var(--secondary);
354
- box-shadow: 0 0 0 4px rgba(37,99,235,0.15);
355
- }
356
-
357
- .search-button-container {
358
- display: flex;
359
- justify-content: center;
360
- }
361
-
362
- .search-button {
363
- padding: 16px 48px;
364
- background: linear-gradient(135deg, var(--primary), var(--secondary));
365
- color: white;
366
- border: none;
367
- border-radius: 60px;
368
- font-size: 18px;
369
- font-weight: 600;
370
- cursor: pointer;
371
- transition: all 0.3s;
372
- display: inline-flex;
373
- align-items: center;
374
- gap: 10px;
375
- box-shadow: 0 8px 20px rgba(30,58,138,0.3);
376
- letter-spacing: 0.5px;
377
- }
378
-
379
- .search-button:hover {
380
- transform: translateY(-2px);
381
- box-shadow: 0 12px 28px rgba(30,58,138,0.4);
382
- }
383
-
384
- .search-button:disabled {
385
- opacity: 0.6;
386
- cursor: not-allowed;
387
- transform: none;
388
- box-shadow: none;
389
- }
390
-
391
- .exemplos {
392
- text-align: center;
393
- color: #64748B;
394
- font-size: 14px;
395
- margin: 15px 0 5px;
396
- }
397
-
398
- .exemplos strong {
399
- color: var(--primary);
400
- }
401
-
402
- .stats-grid {
403
- display: grid;
404
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
405
- gap: 15px;
406
- margin: 25px 0;
407
- }
408
-
409
- .stat-card {
410
- background: #F8FAFC;
411
- border-radius: 20px;
412
- padding: 20px;
413
- border: 1px solid var(--border);
414
- text-align: center;
415
- transition: all 0.2s;
416
- }
417
-
418
- .stat-card:hover {
419
- border-color: var(--secondary);
420
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
421
- }
422
-
423
- .stat-value {
424
- font-size: 32px;
425
- font-weight: 700;
426
- color: var(--primary);
427
- line-height: 1.2;
428
- }
429
-
430
- .stat-label {
431
- color: #64748B;
432
- font-size: 14px;
433
- margin-top: 5px;
434
- text-transform: uppercase;
435
- letter-spacing: 0.5px;
436
- }
437
-
438
- /* Resultados */
439
- .result {
440
- border: 1px solid var(--border);
441
- border-radius: 20px;
442
- padding: 25px;
443
- margin: 20px 0;
444
- background: var(--card-bg);
445
- box-shadow: 0 4px 12px rgba(0,0,0,0.03);
446
- transition: all 0.2s;
447
- }
448
-
449
- .result.acordaos {
450
- border-left: 6px solid var(--secondary);
451
- }
452
-
453
- .result.decisoes {
454
- border-left: 6px solid var(--purple);
455
- }
456
-
457
- .result:hover {
458
- box-shadow: 0 8px 24px rgba(0,0,0,0.08);
459
- border-color: var(--secondary);
460
- }
461
-
462
- .result h3 {
463
- margin: 0 0 15px;
464
- color: var(--primary);
465
- font-size: 18px;
466
- display: flex;
467
- align-items: center;
468
- gap: 8px;
469
- flex-wrap: wrap;
470
- }
471
-
472
- .tipo-badge {
473
- font-size: 12px;
474
- padding: 4px 12px;
475
- border-radius: 30px;
476
- font-weight: 500;
477
- }
478
-
479
- .tipo-badge.acordaos {
480
- background: var(--secondary);
481
- color: white;
482
- }
483
-
484
- .tipo-badge.decisoes {
485
- background: var(--purple);
486
- color: white;
487
- }
488
-
489
- .meta {
490
- display: flex;
491
- flex-wrap: wrap;
492
- gap: 10px;
493
- margin-bottom: 15px;
494
- padding-bottom: 15px;
495
- border-bottom: 1px solid var(--border);
496
- }
497
-
498
- .meta-item {
499
- background: #F1F5F9;
500
- padding: 6px 14px;
501
- border-radius: 30px;
502
- font-size: 13px;
503
- color: var(--text);
504
- display: flex;
505
- align-items: center;
506
- gap: 6px;
507
- }
508
-
509
- .campo {
510
- margin: 20px 0;
511
- padding: 20px;
512
- border-radius: 16px;
513
- }
514
-
515
- .ementa {
516
- background: #EFF6FF;
517
- border-left: 4px solid var(--secondary);
518
- }
519
-
520
- .decisao {
521
- background: #F5F3FF;
522
- border-left: 4px solid var(--purple);
523
- }
524
-
525
- .acordao {
526
- background: #FFFBEB;
527
- border-left: 4px solid var(--warning);
528
- }
529
-
530
- .campo-label {
531
- font-weight: 600;
532
- margin-bottom: 10px;
533
- display: block;
534
- font-size: 14px;
535
- text-transform: uppercase;
536
- letter-spacing: 0.5px;
537
- }
538
-
539
- .ementa .campo-label {
540
- color: var(--secondary);
541
- }
542
-
543
- .decisao .campo-label {
544
- color: var(--purple);
545
- }
546
-
547
- .acordao .campo-label {
548
- color: var(--warning);
549
- }
550
-
551
- .campo-conteudo {
552
- font-size: 14px;
553
- line-height: 1.7;
554
- color: var(--text);
555
- white-space: pre-wrap;
556
- max-height: 300px;
557
- overflow-y: auto;
558
- background: rgba(255,255,255,0.5);
559
- padding: 10px;
560
- border-radius: 8px;
561
- }
562
-
563
- .url-link {
564
- margin-top: 15px;
565
- padding: 12px;
566
- background: #F8FAFC;
567
- border-radius: 12px;
568
- border: 1px solid var(--border);
569
- }
570
-
571
- .url-link a {
572
- color: var(--secondary);
573
- text-decoration: none;
574
- display: inline-flex;
575
- align-items: center;
576
- gap: 8px;
577
- font-weight: 500;
578
- }
579
-
580
- .url-link a:hover {
581
- text-decoration: underline;
582
- }
583
-
584
- .action-buttons {
585
- display: flex;
586
- gap: 12px;
587
- margin-top: 20px;
588
- flex-wrap: wrap;
589
- }
590
-
591
- .btn {
592
- padding: 12px 24px;
593
- border-radius: 40px;
594
- font-size: 14px;
595
- font-weight: 500;
596
- text-decoration: none;
597
- display: inline-flex;
598
- align-items: center;
599
- gap: 8px;
600
- transition: all 0.2s;
601
- cursor: pointer;
602
- border: none;
603
- }
604
-
605
- .btn-primary {
606
- background: linear-gradient(135deg, var(--primary), var(--secondary));
607
- color: white;
608
- box-shadow: 0 4px 10px rgba(30,58,138,0.2);
609
- }
610
-
611
- .btn-primary:hover {
612
- background: linear-gradient(135deg, var(--secondary), var(--primary));
613
- box-shadow: 0 6px 15px rgba(30,58,138,0.3);
614
- }
615
-
616
- .btn-outline {
617
- background: white;
618
- border: 1px solid var(--border);
619
- color: var(--primary);
620
- }
621
-
622
- .btn-outline:hover {
623
- background: #F8FAFC;
624
- border-color: var(--secondary);
625
- }
626
-
627
- .btn-secondary {
628
- background: #F1F5F9;
629
- color: var(--text);
630
- }
631
-
632
- .btn-secondary:hover {
633
- background: #E2E8F0;
634
- }
635
-
636
- /* Paginação */
637
- .pagination {
638
- display: flex;
639
- justify-content: center;
640
- align-items: center;
641
- gap: 8px;
642
- margin: 40px 0 20px;
643
- flex-wrap: wrap;
644
- }
645
-
646
- .pagination button {
647
- padding: 10px 16px;
648
- border: 1px solid var(--border);
649
- background: white;
650
- border-radius: 40px;
651
- cursor: pointer;
652
- font-weight: 500;
653
- transition: all 0.2s;
654
- color: var(--text);
655
- }
656
-
657
- .pagination button:hover:not(:disabled) {
658
- background: #F1F5F9;
659
- border-color: var(--secondary);
660
- color: var(--secondary);
661
- }
662
-
663
- .pagination button.active {
664
- background: var(--primary);
665
- color: white;
666
- border-color: var(--primary);
667
- }
668
-
669
- .pagination button:disabled {
670
- opacity: 0.4;
671
- cursor: not-allowed;
672
- }
673
-
674
- .page-info {
675
- margin: 0 15px;
676
- font-weight: 500;
677
- color: var(--text);
678
- }
679
-
680
- .page-size-selector {
681
- display: flex;
682
- align-items: center;
683
- gap: 10px;
684
- margin-left: 20px;
685
- }
686
-
687
- .page-size-selector select {
688
- padding: 8px 16px;
689
- border-radius: 40px;
690
- border: 1px solid var(--border);
691
- background: white;
692
- color: var(--text);
693
- font-weight: 500;
694
- }
695
-
696
- .loading {
697
- display: inline-block;
698
- width: 30px;
699
- height: 30px;
700
- border: 3px solid rgba(30,58,138,0.2);
701
- border-top-color: var(--primary);
702
- border-radius: 50%;
703
- animation: spin 1s linear infinite;
704
- }
705
-
706
- @keyframes spin {
707
- to { transform: rotate(360deg); }
708
- }
709
-
710
- .error {
711
- color: var(--error);
712
- background: #FEF2F2;
713
- border-left: 6px solid var(--error);
714
- padding: 20px 25px;
715
- margin: 15px 0;
716
- border-radius: 16px;
717
- }
718
-
719
- .warning {
720
- color: #92400E;
721
- background: #FFFBEB;
722
- border-left: 6px solid var(--warning);
723
- padding: 20px 25px;
724
- margin: 15px 0;
725
- border-radius: 16px;
726
- }
727
-
728
- .footer {
729
- margin-top: 50px;
730
- text-align: center;
731
- color: #64748B;
732
- font-size: 14px;
733
- border-top: 1px solid var(--border);
734
- padding-top: 25px;
735
- }
736
-
737
- .highlight {
738
- background-color: #FEF9C3;
739
- color: var(--text);
740
- font-weight: 600;
741
- padding: 2px 4px;
742
- border-radius: 4px;
743
- }
744
-
745
- .aviso {
746
- background: #FFFBEB;
747
- border-left: 4px solid var(--warning);
748
- padding: 12px 18px;
749
- border-radius: 12px;
750
- margin: 15px 0;
751
- font-size: 14px;
752
- }
753
-
754
- .loading-container {
755
- display: flex;
756
- flex-direction: column;
757
- align-items: center;
758
- gap: 15px;
759
- margin: 30px 0;
760
- }
761
-
762
- .loading-text {
763
- color: var(--primary);
764
- font-weight: 500;
765
- }
766
- </style>
767
  </head>
768
  <body>
769
- <div class="container">
770
- <div class="header">
771
- <div class="logo-container">
772
- <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo_light.png"
773
- alt="PARA AI"
774
- class="logo"
775
- onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
776
- </div>
777
- </div>
778
-
779
-
780
- <div class="base-selector">
781
- <h3>🔍 Selecionar bases para pesquisa:</h3>
782
- <div class="checkbox-group">
783
- <div class="checkbox-item">
784
- <input type="checkbox" id="baseAcordaos" checked>
785
- <label for="baseAcordaos"><span class="badge acordaos">Acordaos</span></label>
786
- </div>
787
- <div class="checkbox-item">
788
- <input type="checkbox" id="baseDecisoes" checked>
789
- <label for="baseDecisoes"><span class="badge decisoes">Decisoes Monocráticas</span></label>
790
- </div>
791
- </div>
792
- </div>
793
 
794
- <div class="search-section">
795
- <input type="text"
796
- class="search-input"
797
- id="query"
798
- placeholder="Ex: dano moral mordida cachorro, habeas corpus, liminar..."
799
- value=""
800
- autofocus>
801
-
802
- <div class="search-button-container">
803
- <button id="searchBtn" class="search-button" onclick="doSearch(1)">
804
- <span>🔍</span> PESQUISAR
805
- </button>
806
- </div>
807
-
808
- <div class="exemplos">
809
- <strong>Exemplos:</strong> Dano moral em atraso de voo · Agravo de instrumento · Responsabilidade civil
810
- </div>
811
- </div>
812
 
813
- <div id="loading" class="loading-container" style="display: none;">
814
- <div class="loading"></div>
815
- <div class="loading-text">Buscando jurisprudência...</div>
816
- </div>
817
 
818
- <div class="stats-grid" id="stats" style="display: none;">
819
- <div class="stat-card"><div class="stat-value" id="totalDocs">0</div><div class="stat-label">documentos</div></div>
820
- <div class="stat-card"><div class="stat-value" id="totalAcordaos">0</div><div class="stat-label">acórdãos</div></div>
821
- <div class="stat-card"><div class="stat-value" id="totalDecisoes">0</div><div class="stat-label">decisões</div></div>
822
- <div class="stat-card"><div class="stat-value" id="timeMs">0</div><div class="stat-label">tempo (ms)</div></div>
823
- </div>
824
 
825
- <div id="results"></div>
826
-
827
- <!-- Paginação -->
828
- <div id="pagination" class="pagination" style="display: none;">
829
- <button id="firstPageBtn" onclick="goToPage(1)" disabled>⏮️ Primeira</button>
830
- <button id="prevPageBtn" onclick="goToPage(currentPage - 1)" disabled>◀ Anterior</button>
831
- <span id="pageInfo" class="page-info">Página <span id="currentPageDisplay">1</span> de <span id="totalPagesDisplay">1</span></span>
832
- <button id="nextPageBtn" onclick="goToPage(currentPage + 1)" disabled>Próxima </button>
833
- <button id="lastPageBtn" onclick="goToPage(totalPages)" disabled>Última ⏭️</button>
834
- <div class="page-size-selector">
835
- <label for="pageSize">Itens:</label>
836
- <select id="pageSize" onchange="changePageSize()">
837
- <option value="10">10</option>
838
- <option value="20" selected>20</option>
839
- <option value="50">50</option>
840
- <option value="100">100</option>
841
- </select>
842
- </div>
843
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
 
845
- <div class="footer">
846
- <span>⚖️ STF - Supremo Tribunal Federal • </span>
847
- <span>PARA AI - Pesquisa Jurisprudencial</span>
848
- </div>
849
  </div>
 
850
 
851
- <script>
852
- let lastQuery = '';
853
- let currentPage = 1;
854
- let totalPages = 1;
855
- let totalResults = 0;
856
- let pageSize = 20;
857
-
858
- async function doSearch(page = 1) {
859
- const query = document.getElementById('query').value.trim();
860
- if (!query) {
861
- alert('Digite algo para buscar');
862
- return;
863
- }
864
 
865
- const baseAcordaos = document.getElementById('baseAcordaos').checked;
866
- const baseDecisoes = document.getElementById('baseDecisoes').checked;
867
- pageSize = parseInt(document.getElementById('pageSize').value);
868
-
869
-
870
- lastQuery = query;
871
- currentPage = page;
872
-
873
- const btn = document.getElementById('searchBtn');
874
- const loading = document.getElementById('loading');
875
- const resultsDiv = document.getElementById('results');
876
- const statsDiv = document.getElementById('stats');
877
- const paginationDiv = document.getElementById('pagination');
878
-
879
- btn.disabled = true;
880
- loading.style.display = 'flex';
881
- resultsDiv.innerHTML = '';
882
- statsDiv.style.display = 'none';
883
- paginationDiv.style.display = 'none';
884
-
885
- const startTime = performance.now();
886
-
887
- try {
888
- const response = await fetch(`/api/busca-multipla?q=${encodeURIComponent(query)}&acordaos=${baseAcordaos}&decisoes=${baseDecisoes}&page=${page}&page_size=${pageSize}`);
889
- const data = await response.json();
890
- const elapsed = Math.round(performance.now() - startTime);
891
-
892
- if (data.error) {
893
- resultsDiv.innerHTML = `<div class="error">❌ Erro: ${data.error}</div>`;
894
- } else {
895
- const resultados = data.resultados || [];
896
- totalResults = data.total || resultados.length;
897
- totalPages = Math.ceil(totalResults / pageSize);
898
-
899
- document.getElementById('totalDocs').textContent = totalResults;
900
- document.getElementById('totalAcordaos').textContent = data.total_acordaos || 0;
901
- document.getElementById('totalDecisoes').textContent = data.total_decisoes || 0;
902
- document.getElementById('timeMs').textContent = elapsed;
903
- statsDiv.style.display = 'grid';
904
-
905
- updatePagination();
906
-
907
- if (resultados.length === 0) {
908
- resultsDiv.innerHTML = `<div class="warning">🔍 Nenhum resultado encontrado para "${query}"</div>`;
909
- paginationDiv.style.display = 'none';
910
- } else {
911
- let html = `<p style="margin-bottom: 20px; font-weight: 500;">📚 Encontrados ${totalResults} documentos. Página ${currentPage} de ${totalPages}:</p>`;
912
-
913
- resultados.forEach((item, index) => {
914
- const tipo = item.base === 'acordaos' ? 'acordaos' : 'decisoes';
915
- const tipoLabel = item.base === 'acordaos' ? 'Acórdão' : 'Decisão Monocrática';
916
- const temEmenta = item.ementa && item.ementa.trim() !== '';
917
- const temDecisao = item.decisao_texto && item.decisao_texto.trim() !== '';
918
- const temAcordao = item.acordao_ata && item.acordao_ata.trim() !== '';
919
-
920
- html += `
921
- <div class="result ${tipo}" id="doc-${item.id}">
922
- <h3>
923
- ${((currentPage-1)*pageSize) + index + 1}. ${item.titulo || item.processo || 'Documento sem título'}
924
- <span class="tipo-badge ${tipo}">${tipoLabel}</span>
925
- </h3>
926
- <div class="meta">
927
- ${item.relator ? `<span class="meta-item">⚖️ ${item.relator}</span>` : ''}
928
- ${item.orgao ? `<span class="meta-item">🏛️ ${item.orgao}</span>` : ''}
929
- ${item.data ? `<span class="meta-item">📅 ${item.data}</span>` : ''}
930
- ${item.publicacao ? `<span class="meta-item">📰 Pub: ${item.publicacao}</span>` : ''}
931
- ${item.id ? `<span class="meta-item">🆔 ${item.id}</span>` : ''}
932
- </div>
933
- `;
934
-
935
- if (temEmenta) {
936
- html += `
937
- <div class="campo ementa">
938
- <span class="campo-label">📝 Ementa:</span>
939
- <div class="campo-conteudo">${item.ementa.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
940
- </div>
941
- `;
942
- }
943
-
944
- if (temDecisao) {
945
- html += `
946
- <div class="campo decisao">
947
- <span class="campo-label">⚖️ Decisão:</span>
948
- <div class="campo-conteudo">${item.decisao_texto.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
949
- </div>
950
- `;
951
- }
952
-
953
- if (temAcordao) {
954
- html += `
955
- <div class="campo acordao">
956
- <span class="campo-label">📋 Acórdão/Ata:</span>
957
- <div class="campo-conteudo">${item.acordao_ata.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
958
- </div>
959
- `;
960
- }
961
-
962
- // Botões de ação
963
- html += `
964
- <div class="action-buttons">
965
- <a href="/documento/${item.id}" target="_blank" class="btn btn-primary">
966
- <span>🔍</span> Ver detalhes completos
967
- </a>
968
- ${item.url_documento ? `
969
- <a href="${item.url_documento}" target="_blank" class="btn btn-outline">
970
- <span>📄</span> Inteiro teor
971
- </a>
972
- ` : ''}
973
- <a href="#doc-${item.id}" class="btn btn-secondary">
974
- <span>🔗</span> Link direto
975
- </a>
976
- </div>
977
- `;
978
-
979
- html += `</div>`;
980
- });
981
-
982
- resultsDiv.innerHTML = html;
983
- paginationDiv.style.display = 'flex';
984
- }
985
- }
986
- } catch (err) {
987
- resultsDiv.innerHTML = `<div class="error">❌ Erro na requisição: ${err.message}</div>`;
988
- } finally {
989
- btn.disabled = false;
990
- loading.style.display = 'none';
991
- }
992
- }
993
 
994
- function updatePagination() {
995
- document.getElementById('currentPageDisplay').textContent = currentPage;
996
- document.getElementById('totalPagesDisplay').textContent = totalPages;
997
-
998
- document.getElementById('firstPageBtn').disabled = currentPage === 1;
999
- document.getElementById('prevPageBtn').disabled = currentPage === 1;
1000
- document.getElementById('nextPageBtn').disabled = currentPage === totalPages;
1001
- document.getElementById('lastPageBtn').disabled = currentPage === totalPages;
1002
- }
 
 
 
 
 
 
1003
 
1004
- function goToPage(page) {
1005
- if (page < 1 || page > totalPages) return;
1006
- doSearch(page);
1007
- }
 
 
1008
 
1009
- function changePageSize() {
1010
- if (lastQuery) {
1011
- doSearch(1);
1012
- }
1013
- }
1014
 
1015
- document.getElementById('query').addEventListener('keypress', function(e) {
1016
- if (e.key === 'Enter') {
1017
- doSearch(1);
1018
- }
1019
- });
1020
 
1021
- // Validação das checkboxes
1022
- document.getElementById('baseAcordaos').addEventListener('change', validateBases);
1023
- document.getElementById('baseDecisoes').addEventListener('change', validateBases);
1024
 
1025
- function validateBases() {
1026
- const acordaos = document.getElementById('baseAcordaos').checked;
1027
- const decisoes = document.getElementById('baseDecisoes').checked;
1028
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1029
 
1030
- window.onload = function() {
1031
- document.getElementById('query').focus();
1032
- };
1033
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1034
  </body>
1035
  </html>
1036
  """)
1037
 
 
1038
  # ============================================
1039
  # Página de detalhes do documento
1040
  # ============================================
1041
  @app.route('/documento/<doc_id>')
1042
  def documento_detalhe(doc_id):
1043
- return render_template_string("""
1044
- <!DOCTYPE html>
1045
- <html>
1046
  <head>
1047
- <title>⚖️ PARA AI - Documento {{ doc_id }}</title>
1048
- <meta charset="utf-8">
1049
- <meta name="viewport" content="width=device-width, initial-scale=1">
1050
- <style>
1051
- :root {
1052
- --primary: #1E3A8A;
1053
- --secondary: #2563EB;
1054
- --accent: #3B82F6;
1055
- --background: #F8FAFC;
1056
- --text: #0F172A;
1057
- --card-bg: #FFFFFF;
1058
- --border: #E2E8F0;
1059
- --success: #10B981;
1060
- --warning: #F59E0B;
1061
- --purple: #8B5CF6;
1062
- }
1063
-
1064
- body {
1065
- font-family: 'Inter', 'Segoe UI', Roboto, system-ui, sans-serif;
1066
- max-width: 1200px;
1067
- margin: 0 auto;
1068
- padding: 20px;
1069
- background: var(--background);
1070
- color: var(--text);
1071
- }
1072
-
1073
- .container {
1074
- background: var(--card-bg);
1075
- border-radius: 24px;
1076
- padding: 30px;
1077
- box-shadow: 0 20px 40px rgba(0,0,0,0.05);
1078
- border: 1px solid var(--border);
1079
- }
1080
-
1081
- .header {
1082
- display: flex;
1083
- justify-content: space-between;
1084
- align-items: center;
1085
- margin-bottom: 30px;
1086
- border-bottom: 2px solid var(--border);
1087
- padding-bottom: 20px;
1088
- }
1089
-
1090
- .logo-small {
1091
- height: 40px;
1092
- width: auto;
1093
- }
1094
-
1095
- .back-btn {
1096
- padding: 12px 24px;
1097
- background: #F1F5F9;
1098
- border-radius: 40px;
1099
- text-decoration: none;
1100
- color: var(--primary);
1101
- font-weight: 500;
1102
- display: inline-flex;
1103
- align-items: center;
1104
- gap: 8px;
1105
- transition: all 0.2s;
1106
- border: 1px solid var(--border);
1107
- }
1108
-
1109
- .back-btn:hover {
1110
- background: #E2E8F0;
1111
- border-color: var(--secondary);
1112
- }
1113
-
1114
- .loading-container {
1115
- display: flex;
1116
- flex-direction: column;
1117
- align-items: center;
1118
- gap: 15px;
1119
- padding: 60px;
1120
- }
1121
-
1122
- .loading {
1123
- width: 50px;
1124
- height: 50px;
1125
- border: 4px solid rgba(30,58,138,0.2);
1126
- border-top-color: var(--primary);
1127
- border-radius: 50%;
1128
- animation: spin 1s linear infinite;
1129
- }
1130
-
1131
- @keyframes spin {
1132
- to { transform: rotate(360deg); }
1133
- }
1134
-
1135
- .error {
1136
- color: #EF4444;
1137
- background: #FEF2F2;
1138
- border-left: 6px solid #EF4444;
1139
- padding: 20px 25px;
1140
- border-radius: 16px;
1141
- }
1142
-
1143
- .doc-header {
1144
- background: #F8FAFC;
1145
- border-radius: 20px;
1146
- padding: 25px;
1147
- margin-bottom: 25px;
1148
- border: 1px solid var(--border);
1149
- }
1150
-
1151
- .doc-header h2 {
1152
- margin-top: 0;
1153
- color: var(--primary);
1154
- }
1155
-
1156
- .meta {
1157
- display: flex;
1158
- flex-wrap: wrap;
1159
- gap: 12px;
1160
- margin: 20px 0;
1161
- }
1162
-
1163
- .meta-item {
1164
- background: #F1F5F9;
1165
- padding: 8px 18px;
1166
- border-radius: 40px;
1167
- font-size: 14px;
1168
- color: var(--text);
1169
- display: flex;
1170
- align-items: center;
1171
- gap: 8px;
1172
- }
1173
-
1174
- .campo {
1175
- margin: 25px 0;
1176
- padding: 25px;
1177
- border-radius: 20px;
1178
- }
1179
-
1180
- .ementa {
1181
- background: #EFF6FF;
1182
- border-left: 6px solid var(--secondary);
1183
- }
1184
-
1185
- .decisao {
1186
- background: #F5F3FF;
1187
- border-left: 6px solid var(--purple);
1188
- }
1189
-
1190
- .acordao {
1191
- background: #FFFBEB;
1192
- border-left: 6px solid var(--warning);
1193
- }
1194
-
1195
- .campo-label {
1196
- font-weight: 600;
1197
- margin-bottom: 15px;
1198
- display: block;
1199
- font-size: 16px;
1200
- }
1201
-
1202
- .campo-conteudo {
1203
- font-size: 15px;
1204
- line-height: 1.8;
1205
- color: var(--text);
1206
- white-space: pre-wrap;
1207
- background: rgba(255,255,255,0.7);
1208
- padding: 15px;
1209
- border-radius: 12px;
1210
- }
1211
-
1212
- .url-link {
1213
- margin-top: 25px;
1214
- padding: 15px;
1215
- background: #F8FAFC;
1216
- border-radius: 12px;
1217
- border: 1px solid var(--border);
1218
- }
1219
-
1220
- .url-link a {
1221
- color: var(--secondary);
1222
- text-decoration: none;
1223
- display: inline-flex;
1224
- align-items: center;
1225
- gap: 8px;
1226
- font-weight: 500;
1227
- font-size: 16px;
1228
- }
1229
-
1230
- .url-link a:hover {
1231
- text-decoration: underline;
1232
- }
1233
-
1234
- .highlight {
1235
- background-color: #FEF9C3;
1236
- font-weight: 600;
1237
- padding: 2px 4px;
1238
- border-radius: 4px;
1239
- }
1240
- </style>
1241
  </head>
1242
  <body>
1243
- <div class="container">
1244
- <div class="header">
1245
- <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo_dark.png"
1246
- alt="PARA AI"
1247
- class="logo-small"
1248
- onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
1249
- <div style="display: none; font-weight: 700; color: var(--primary);">⚖️ PARA AI</div>
1250
- <a href="/" class="back-btn"> Voltar para busca</a>
1251
- </div>
1252
-
1253
- <div id="loading" class="loading-container">
1254
- <div class="loading"></div>
1255
- <p style="color: var(--primary); font-weight: 500;">Carregando documento...</p>
1256
- </div>
1257
-
1258
- <div id="error" style="display: none;"></div>
1259
- <div id="documento" style="display: none;"></div>
 
 
 
 
 
 
 
 
1260
  </div>
 
1261
 
1262
- <script>
1263
- const docId = "{{ doc_id }}";
1264
-
1265
- async function carregarDocumento() {
1266
- try {
1267
- const response = await fetch(`/api/documento/${docId}`);
1268
- const data = await response.json();
1269
-
1270
- document.getElementById('loading').style.display = 'none';
1271
-
1272
- if (data.error) {
1273
- const errorDiv = document.getElementById('error');
1274
- errorDiv.style.display = 'block';
1275
- errorDiv.className = 'error';
1276
- errorDiv.innerHTML = `❌ Erro: ${data.error}`;
1277
- return;
1278
- }
1279
-
1280
- const item = data;
1281
- const docDiv = document.getElementById('documento');
1282
- docDiv.style.display = 'block';
1283
-
1284
- const tipo = item.base === 'acordaos' ? 'acordaos' : 'decisoes';
1285
- const tipoLabel = item.base === 'acordaos' ? 'Acórdão' : 'Decisão Monocrática';
1286
- const temEmenta = item.ementa && item.ementa.trim() !== '';
1287
- const temDecisao = item.decisao_texto && item.decisao_texto.trim() !== '';
1288
- const temAcordao = item.acordao_ata && item.acordao_ata.trim() !== '';
1289
-
1290
- let html = `
1291
- <div class="doc-header">
1292
- <h2>${item.titulo || item.processo || 'Documento sem título'}</h2>
1293
- <div class="meta">
1294
- <span class="meta-item">⚖️ ${tipoLabel}</span>
1295
- ${item.relator ? `<span class="meta-item">⚖️ Relator: ${item.relator}</span>` : ''}
1296
- ${item.orgao ? `<span class="meta-item">🏛️ ${item.orgao}</span>` : ''}
1297
- ${item.data ? `<span class="meta-item">📅 Julgamento: ${item.data}</span>` : ''}
1298
- ${item.publicacao ? `<span class="meta-item">📰 Publicação: ${item.publicacao}</span>` : ''}
1299
- ${item.id ? `<span class="meta-item">🆔 ID: ${item.id}</span>` : ''}
1300
- ${item.processo ? `<span class="meta-item">📋 Processo: ${item.processo}</span>` : ''}
1301
- </div>
1302
- </div>
1303
- `;
1304
-
1305
- if (temEmenta) {
1306
- html += `
1307
- <div class="campo ementa">
1308
- <span class="campo-label">📝 Ementa</span>
1309
- <div class="campo-conteudo">${item.ementa.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
1310
- </div>
1311
- `;
1312
- }
1313
-
1314
- if (temDecisao) {
1315
- html += `
1316
- <div class="campo decisao">
1317
- <span class="campo-label">⚖️ Decisão</span>
1318
- <div class="campo-conteudo">${item.decisao_texto.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
1319
- </div>
1320
- `;
1321
- }
1322
-
1323
- if (temAcordao) {
1324
- html += `
1325
- <div class="campo acordao">
1326
- <span class="campo-label">📋 Acórdão/Ata</span>
1327
- <div class="campo-conteudo">${item.acordao_ata.replace(/\\n/g, '<br>').replace(/<em>/g, '<span class="highlight">').replace(/<\\/em>/g, '</span>')}</div>
1328
- </div>
1329
- `;
1330
- }
1331
-
1332
- if (item.url_documento) {
1333
- html += `
1334
- <div class="url-link">
1335
- <a href="${item.url_documento}" target="_blank">
1336
- <span>📄</span> Ver inteiro teor completo (PDF/HTML)
1337
- </a>
1338
- </div>
1339
- `;
1340
- }
1341
-
1342
- docDiv.innerHTML = html;
1343
-
1344
- } catch (err) {
1345
- document.getElementById('loading').style.display = 'none';
1346
- const errorDiv = document.getElementById('error');
1347
- errorDiv.style.display = 'block';
1348
- errorDiv.className = 'error';
1349
- errorDiv.innerHTML = `❌ Erro ao carregar: ${err.message}`;
1350
- }
1351
- }
1352
-
1353
- carregarDocumento();
1354
- </script>
1355
  </body>
1356
  </html>
1357
  """, doc_id=doc_id)
1358
 
 
1359
  # ============================================
1360
  # API para buscar documento por ID
1361
  # ============================================
@@ -1366,17 +1043,10 @@ def api_documento(doc_id):
1366
  return jsonify({"error": "Não foi possível obter token"}), 503
1367
 
1368
  payload = {
1369
- "query": {
1370
- "ids": {
1371
- "values": [doc_id]
1372
- }
1373
- },
1374
- "_source": [
1375
- "id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata",
1376
- "processo_codigo_completo", "relator_processo_nome",
1377
- "orgao_julgador", "julgamento_data", "publicacao_data",
1378
- "inteiro_teor_url", "base"
1379
- ],
1380
  "size": 1
1381
  }
1382
 
@@ -1437,6 +1107,7 @@ def api_documento(doc_id):
1437
  except Exception as e:
1438
  return jsonify({"error": str(e)}), 500
1439
 
 
1440
  # ============================================
1441
  # Endpoint /api/busca-multipla (retorna JSON)
1442
  # ============================================
@@ -1450,22 +1121,17 @@ def busca_multipla():
1450
 
1451
  if not query:
1452
  return jsonify({"error": "Parâmetro 'q' obrigatório"}), 400
1453
-
1454
  if not busca_acordaos and not busca_decisoes:
1455
  return jsonify({"error": "Selecione pelo menos uma base para pesquisa"}), 400
1456
 
1457
- # Determinar quais bases incluir
1458
  bases = []
1459
- if busca_acordaos:
1460
- bases.append("acordaos")
1461
- if busca_decisoes:
1462
- bases.append("decisoes")
1463
 
1464
  result = search_stf(query, bases, page, page_size)
1465
  if isinstance(result, tuple):
1466
  return jsonify(result[0]), result[1]
1467
 
1468
- # Converte para o formato simplificado de saída
1469
  hits = result.get('result', {}).get('hits', {}).get('hits', [])
1470
  resultados = []
1471
  total_acordaos = 0
@@ -1474,11 +1140,8 @@ def busca_multipla():
1474
  for hit in hits:
1475
  src = hit.get('_source', {})
1476
  base = src.get('base', '')
1477
- if base == 'acordaos':
1478
- total_acordaos += 1
1479
- elif base == 'decisoes':
1480
- total_decisoes += 1
1481
-
1482
  item = {
1483
  "id": src.get('id') or hit.get('_id'),
1484
  "titulo": src.get('titulo'),
@@ -1494,21 +1157,18 @@ def busca_multipla():
1494
  "base": base,
1495
  "score": hit.get('_score')
1496
  }
1497
- # Remove campos vazios
1498
  item = {k: v for k, v in item.items() if v is not None}
1499
  resultados.append(item)
1500
 
1501
  return jsonify({
1502
- "q": query,
1503
- "bases": bases,
1504
- "page": page,
1505
- "page_size": page_size,
1506
  "total": result.get('result', {}).get('hits', {}).get('total', {}).get('value', len(resultados)),
1507
  "total_acordaos": total_acordaos,
1508
  "total_decisoes": total_decisoes,
1509
  "resultados": resultados
1510
  })
1511
 
 
1512
  # ============================================
1513
  # Endpoint de saúde
1514
  # ============================================
@@ -1527,6 +1187,7 @@ def health():
1527
  "token_cached": bool(token_cache["token"])
1528
  })
1529
 
 
1530
  if __name__ == '__main__':
1531
  try:
1532
  import certifi
@@ -1539,4 +1200,4 @@ if __name__ == '__main__':
1539
  logger.info("📋 Bases: acordaos e decisoes")
1540
  logger.info("="*50)
1541
  port = int(os.environ.get('PORT', 7860))
1542
- app.run(host='0.0.0.0', port=port, debug=False)
 
61
  browser.close()
62
  if token:
63
  token_cache["token"] = token
64
+ token_cache["expires_at"] = time.time() + 600
65
  logger.info(f"Token obtido: {token[:30]}...")
66
  return token
67
  else:
 
72
  return None
73
 
74
  def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20):
 
 
 
 
 
 
75
  token = get_fresh_token()
76
  if not token:
77
  return {"error": "Não foi possível obter token de acesso"}, 503
78
 
 
79
  from_idx = (page - 1) * page_size
80
 
 
81
  should_filters = []
82
  for base in bases:
83
  should_filters.append({"term": {"base": base}})
84
 
 
85
  payload = {
86
  "query": {
87
  "bool": {
88
+ "filter": [{"bool": {"should": should_filters, "minimum_should_match": 1}}],
89
+ "must": [{"bool": {"should": [
90
+ {"query_string": {"fields": ["ementa_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}},
91
+ {"query_string": {"fields": ["decisao_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}},
92
+ {"query_string": {"fields": ["acordao_ata"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}}
93
+ ]}}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
  },
96
+ "_source": ["id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata",
97
+ "processo_codigo_completo", "relator_processo_nome",
98
+ "orgao_julgador", "julgamento_data", "publicacao_data", "inteiro_teor_url", "base"],
 
 
 
99
  "size": page_size,
100
  "from": from_idx,
101
  "sort": [{"julgamento_data": {"order": "desc"}}],
 
110
  if response.status_code == 200:
111
  return response.json()
112
  elif response.status_code == 202:
 
113
  token = get_fresh_token(force_refresh=True)
114
  if token:
115
  headers['Cookie'] = f'aws-waf-token={token}'
 
122
  except Exception as e:
123
  return {"error": str(e)}, 502
124
 
125
+
126
+ # ============================================
127
+ # CSS compartilhado
128
+ # ============================================
129
+ SHARED_CSS = """
130
+ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300&family=DM+Sans:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap');
131
+
132
+ :root {
133
+ --bg: #0e1018;
134
+ --bg-1: #13161f;
135
+ --bg-2: #191d28;
136
+ --bg-3: #1f2435;
137
+ --gold: #c9a84c;
138
+ --gold-lt: #e8cf82;
139
+ --gold-dim: #7a6030;
140
+ --gold-glow: rgba(201,168,76,.12);
141
+ --gold-glow2: rgba(201,168,76,.05);
142
+ --gold-bd: rgba(201,168,76,.22);
143
+ --blue: #3d7de8;
144
+ --blue-dim: rgba(61,125,232,.13);
145
+ --blue-bd: rgba(61,125,232,.30);
146
+ --purple: #7c5cbf;
147
+ --purple-dim: rgba(124,92,191,.13);
148
+ --purple-bd: rgba(124,92,191,.30);
149
+ --tx: #e6e8ee;
150
+ --tx-2: #8e91a8;
151
+ --tx-3: #484c62;
152
+ --bd: rgba(255,255,255,.07);
153
+ --bd-2: rgba(255,255,255,.11);
154
+ --red-dim: rgba(239,68,68,.12);
155
+ --red-bd: rgba(239,68,68,.28);
156
+ --serif: 'Cormorant Garamond', Georgia, serif;
157
+ --sans: 'DM Sans', system-ui, sans-serif;
158
+ --mono: 'JetBrains Mono', monospace;
159
+ }
160
+
161
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
162
+
163
+ html { scroll-behavior: smooth; }
164
+
165
+ body {
166
+ font-family: var(--sans);
167
+ background: var(--bg);
168
+ color: var(--tx);
169
+ min-height: 100vh;
170
+ -webkit-font-smoothing: antialiased;
171
+ line-height: 1.6;
172
+ }
173
+
174
+ /* Grain */
175
+ body::after {
176
+ content: '';
177
+ position: fixed; inset: 0;
178
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");
179
+ pointer-events: none; z-index: 9999; opacity: .5;
180
+ }
181
+
182
+ ::selection { background: var(--gold-glow); color: var(--tx); }
183
+ ::-webkit-scrollbar { width: 3px; }
184
+ ::-webkit-scrollbar-thumb { background: var(--gold-dim); border-radius: 3px; }
185
+
186
+ /* ── TOPBAR ── */
187
+ .topbar {
188
+ display: flex; align-items: center; justify-content: space-between;
189
+ padding: 18px 40px;
190
+ border-bottom: 1px solid var(--bd);
191
+ position: sticky; top: 0; z-index: 100;
192
+ background: rgba(14,16,24,.92);
193
+ backdrop-filter: blur(14px);
194
+ -webkit-backdrop-filter: blur(14px);
195
+ }
196
+
197
+ .brand { display: flex; align-items: center; gap: 14px; text-decoration: none; }
198
+
199
+ .brand-logo { height: 36px; width: auto; object-fit: contain; }
200
+
201
+ .brand-sep { width: 1px; height: 20px; background: var(--bd-2); }
202
+
203
+ .brand-label {
204
+ font-family: var(--mono);
205
+ font-size: .65rem;
206
+ letter-spacing: .18em;
207
+ text-transform: uppercase;
208
+ color: var(--tx-3);
209
+ }
210
+
211
+ .topbar-right { display: flex; align-items: center; gap: 14px; }
212
+
213
+ .status-dot {
214
+ display: flex; align-items: center; gap: 6px;
215
+ font-family: var(--mono); font-size: .68rem;
216
+ color: var(--tx-3); letter-spacing: .06em;
217
+ }
218
+ .status-dot::before {
219
+ content: ''; width: 6px; height: 6px; border-radius: 50%;
220
+ background: #4ade80;
221
+ box-shadow: 0 0 6px rgba(74,222,128,.6);
222
+ }
223
+
224
+ /* ── SHELL ── */
225
+ .shell { max-width: 1060px; margin: 0 auto; padding: 0 32px 80px; }
226
+
227
+ /* ── HERO ── */
228
+ .hero { padding: 56px 0 44px; text-align: center; position: relative; }
229
+
230
+ .hero-glow {
231
+ position: absolute; top: 0; left: 50%; transform: translateX(-50%);
232
+ width: 600px; height: 300px;
233
+ background: radial-gradient(ellipse at center top, rgba(201,168,76,.08) 0%, transparent 70%);
234
+ pointer-events: none;
235
+ }
236
+
237
+ .hero-logo-wrap { margin-bottom: 28px; }
238
+ .hero-logo { height: 90px; width: auto; object-fit: contain; filter: drop-shadow(0 0 24px rgba(201,168,76,.15)); }
239
+
240
+ .hero-sub {
241
+ font-size: .8rem;
242
+ font-family: var(--mono);
243
+ letter-spacing: .2em;
244
+ text-transform: uppercase;
245
+ color: var(--tx-3);
246
+ margin-bottom: 10px;
247
+ }
248
+
249
+ .hero-title {
250
+ font-family: var(--serif);
251
+ font-size: clamp(1.8rem, 4vw, 3rem);
252
+ font-weight: 300;
253
+ color: var(--tx);
254
+ line-height: 1.15;
255
+ margin-bottom: 10px;
256
+ }
257
+ .hero-title em { font-style: italic; color: var(--gold-lt); }
258
+
259
+ /* ── SEARCH AREA ── */
260
+ .search-wrap { margin-top: 36px; }
261
+
262
+ /* Filtros de base — linha simples */
263
+ .bases-line {
264
+ display: flex; align-items: center; gap: 20px;
265
+ margin-bottom: 14px;
266
+ font-size: .8rem; color: var(--tx-3);
267
+ }
268
+
269
+ .base-check {
270
+ display: flex; align-items: center; gap: 7px;
271
+ cursor: pointer; user-select: none;
272
+ color: var(--tx-2);
273
+ transition: color .2s;
274
+ }
275
+ .base-check:hover { color: var(--tx); }
276
+ .base-check input { display: none; }
277
+
278
+ .check-box {
279
+ width: 15px; height: 15px;
280
+ border: 1px solid var(--bd-2); border-radius: 4px;
281
+ display: flex; align-items: center; justify-content: center;
282
+ transition: all .2s; flex-shrink: 0;
283
+ }
284
+ .base-check input:checked + .check-box { border-color: var(--gold-dim); background: var(--gold-glow); }
285
+ .base-check input:checked + .check-box::after {
286
+ content: ''; width: 6px; height: 6px; border-radius: 2px; background: var(--gold);
287
+ }
288
+
289
+ .base-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
290
+ .dot-ac { background: var(--blue); }
291
+ .dot-de { background: var(--purple); }
292
+
293
+ /* Input row */
294
+ .search-row {
295
+ display: flex; gap: 10px; align-items: center;
296
+ background: var(--bg-1);
297
+ border: 1px solid var(--bd-2);
298
+ border-radius: 14px;
299
+ padding: 6px 6px 6px 20px;
300
+ transition: border-color .25s, box-shadow .25s;
301
+ }
302
+ .search-row:focus-within {
303
+ border-color: var(--gold-dim);
304
+ box-shadow: 0 0 0 3px var(--gold-glow), 0 8px 32px rgba(0,0,0,.3);
305
+ }
306
+
307
+ .search-input {
308
+ flex: 1; background: transparent; border: none; outline: none;
309
+ font-family: var(--sans); font-size: .95rem; color: var(--tx);
310
+ padding: 10px 0; min-width: 0;
311
+ }
312
+ .search-input::placeholder { color: var(--tx-3); }
313
+
314
+ .search-btn {
315
+ background: linear-gradient(135deg, #6b4f1a, var(--gold));
316
+ border: none; border-radius: 10px;
317
+ padding: 11px 26px;
318
+ font-family: var(--sans); font-size: .85rem; font-weight: 500;
319
+ color: #0e1018; letter-spacing: .04em;
320
+ cursor: pointer; white-space: nowrap;
321
+ display: flex; align-items: center; gap: 8px;
322
+ transition: opacity .2s, transform .15s;
323
+ flex-shrink: 0;
324
+ }
325
+ .search-btn:hover { opacity: .88; transform: translateY(-1px); }
326
+ .search-btn:disabled { opacity: .35; cursor: not-allowed; transform: none; }
327
+
328
+ /* ── STATS CARDS ── */
329
+ .stats-row {
330
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
331
+ margin: 32px 0 0; display: none;
332
+ }
333
+
334
+ .stat-card {
335
+ background: var(--bg-1); border: 1px solid var(--bd);
336
+ border-radius: 14px; padding: 20px 22px; text-align: center;
337
+ position: relative; overflow: hidden;
338
+ transition: border-color .2s;
339
+ }
340
+ .stat-card::before {
341
+ content: '';
342
+ position: absolute; top: 0; left: 0; right: 0; height: 1px;
343
+ background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
344
+ opacity: .5;
345
+ }
346
+ .stat-card:hover { border-color: var(--gold-bd); }
347
+
348
+ .stat-val {
349
+ font-family: var(--serif); font-size: 2rem; font-weight: 300;
350
+ color: var(--gold-lt); line-height: 1; margin-bottom: 4px;
351
+ }
352
+ .stat-label {
353
+ font-family: var(--mono); font-size: .63rem; letter-spacing: .12em;
354
+ text-transform: uppercase; color: var(--tx-3);
355
+ }
356
+
357
+ /* ── DIVIDER ── */
358
+ .section-divider {
359
+ display: none; align-items: center; gap: 14px; margin: 28px 0 20px;
360
+ }
361
+ .div-line { flex: 1; height: 1px; background: var(--bd); }
362
+ .div-label {
363
+ font-family: var(--mono); font-size: .65rem; letter-spacing: .14em;
364
+ text-transform: uppercase; color: var(--tx-3); white-space: nowrap;
365
+ }
366
+
367
+ /* ── RESULT CARDS ── */
368
+ .result-card {
369
+ background: var(--bg-1); border: 1px solid var(--bd);
370
+ border-radius: 16px; padding: 26px 28px;
371
+ margin-bottom: 12px; position: relative; overflow: hidden;
372
+ transition: border-color .22s, box-shadow .22s, transform .15s;
373
+ animation: riseUp .3s ease both;
374
+ }
375
+
376
+ @keyframes riseUp {
377
+ from { opacity: 0; transform: translateY(8px); }
378
+ to { opacity: 1; transform: translateY(0); }
379
+ }
380
+
381
+ /* Circuit accent linha esquerda */
382
+ .result-card::before {
383
+ content: '';
384
+ position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
385
+ transition: opacity .2s;
386
+ }
387
+ .result-card.acordaos::before { background: linear-gradient(180deg, var(--blue), transparent); }
388
+ .result-card.decisoes::before { background: linear-gradient(180deg, var(--purple), transparent); }
389
+
390
+ /* Corner circuit decoration */
391
+ .result-card::after {
392
+ content: '';
393
+ position: absolute; right: 0; top: 0;
394
+ width: 80px; height: 80px;
395
+ background: radial-gradient(circle at top right, rgba(255,255,255,.025) 0%, transparent 70%);
396
+ pointer-events: none;
397
+ }
398
+
399
+ .result-card:hover {
400
+ border-color: var(--gold-bd);
401
+ box-shadow: 0 0 0 1px var(--gold-bd), 0 12px 40px rgba(0,0,0,.35);
402
+ transform: translateY(-1px);
403
+ }
404
+
405
+ /* Card header */
406
+ .card-head {
407
+ display: flex; align-items: flex-start; justify-content: space-between;
408
+ gap: 14px; margin-bottom: 14px;
409
+ }
410
+ .card-title {
411
+ font-family: var(--serif); font-size: 1.08rem; font-weight: 400;
412
+ color: var(--tx); line-height: 1.35; flex: 1;
413
+ }
414
+ .card-num {
415
+ font-family: var(--mono); font-size: .62rem; color: var(--tx-3);
416
+ flex-shrink: 0; margin-top: 3px;
417
+ }
418
+
419
+ /* Tipo tag */
420
+ .tipo-tag {
421
+ display: inline-flex; align-items: center; gap: 5px;
422
+ font-family: var(--mono); font-size: .60rem; letter-spacing: .07em;
423
+ padding: 3px 9px; border-radius: 20px; font-weight: 500;
424
+ flex-shrink: 0; margin-top: 2px;
425
+ }
426
+ .tipo-tag.acordaos { background: var(--blue-dim); color: #7ab4ff; border: 1px solid var(--blue-bd); }
427
+ .tipo-tag.decisoes { background: var(--purple-dim); color: #b39dff; border: 1px solid var(--purple-bd); }
428
+ .tipo-tag-dot { width: 5px; height: 5px; border-radius: 50%; }
429
+ .tipo-tag.acordaos .tipo-tag-dot { background: var(--blue); }
430
+ .tipo-tag.decisoes .tipo-tag-dot { background: var(--purple); }
431
+
432
+ /* Meta pills */
433
+ .meta-row {
434
+ display: flex; flex-wrap: wrap; gap: 6px;
435
+ margin-bottom: 16px; padding-bottom: 16px;
436
+ border-bottom: 1px solid var(--bd);
437
+ }
438
+ .meta-pill {
439
+ font-size: .73rem; color: var(--tx-2);
440
+ background: var(--bg-2); border: 1px solid var(--bd);
441
+ border-radius: 20px; padding: 3px 11px;
442
+ display: flex; align-items: center; gap: 5px;
443
+ }
444
+ .meta-pill-icon { opacity: .55; font-size: .75rem; }
445
+
446
+ /* Field blocks */
447
+ .field-block {
448
+ border-radius: 10px; padding: 14px 16px; margin-bottom: 10px;
449
+ border-left: 2px solid transparent;
450
+ }
451
+ .field-block.ementa { background: rgba(61,125,232,.07); border-left-color: var(--blue); }
452
+ .field-block.decisao { background: rgba(124,92,191,.07); border-left-color: var(--purple); }
453
+ .field-block.acordao { background: rgba(201,168,76,.06); border-left-color: var(--gold-dim); }
454
+
455
+ .field-label {
456
+ font-family: var(--mono); font-size: .60rem; letter-spacing: .14em;
457
+ text-transform: uppercase; font-weight: 500;
458
+ margin-bottom: 7px; display: block;
459
+ }
460
+ .field-block.ementa .field-label { color: #7ab4ff; }
461
+ .field-block.decisao .field-label { color: #b39dff; }
462
+ .field-block.acordao .field-label { color: var(--gold); }
463
+
464
+ .field-text {
465
+ font-size: .84rem; color: var(--tx-2); line-height: 1.7;
466
+ white-space: pre-wrap; max-height: 200px; overflow-y: auto;
467
+ }
468
+ .field-text::-webkit-scrollbar { width: 2px; }
469
+ .field-text::-webkit-scrollbar-thumb { background: var(--bd-2); }
470
+
471
+ /* Card footer */
472
+ .card-foot { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
473
+
474
+ .btn-ghost {
475
+ font-family: var(--sans); font-size: .77rem; color: var(--tx-2);
476
+ background: var(--bg-2); border: 1px solid var(--bd);
477
+ border-radius: 20px; padding: 7px 16px;
478
+ text-decoration: none; display: inline-flex; align-items: center; gap: 6px;
479
+ transition: color .2s, border-color .2s; cursor: pointer;
480
+ }
481
+ .btn-ghost:hover { color: var(--tx); border-color: var(--bd-2); }
482
+
483
+ .btn-gold {
484
+ font-family: var(--sans); font-size: .77rem; color: var(--gold);
485
+ background: var(--gold-glow); border: 1px solid var(--gold-bd);
486
+ border-radius: 20px; padding: 7px 16px;
487
+ text-decoration: none; display: inline-flex; align-items: center; gap: 6px;
488
+ transition: background .2s;
489
+ }
490
+ .btn-gold:hover { background: rgba(201,168,76,.18); }
491
+
492
+ /* ── LOADER ── */
493
+ #loader { display: none; text-align: center; padding: 60px 0; }
494
+ .spinner {
495
+ width: 32px; height: 32px; margin: 0 auto 14px;
496
+ border: 2px solid var(--bd); border-top-color: var(--gold);
497
+ border-radius: 50%; animation: spin .75s linear infinite;
498
+ }
499
+ @keyframes spin { to { transform: rotate(360deg); } }
500
+ .loader-txt { font-family: var(--mono); font-size: .72rem; color: var(--tx-3); letter-spacing: .1em; }
501
+
502
+ /* ── ERROR ── */
503
+ .err-box {
504
+ display: none; background: var(--red-dim); border: 1px solid var(--red-bd);
505
+ border-radius: 12px; padding: 16px 20px; font-size: .88rem;
506
+ color: #fca5a5; margin-top: 16px;
507
+ }
508
+
509
+ /* ── EMPTY ── */
510
+ .empty-box { display: none; text-align: center; padding: 70px 0; color: var(--tx-3); }
511
+ .empty-icon { font-size: 2rem; margin-bottom: 10px; opacity: .4; }
512
+
513
+ /* ── PAGINATION ── */
514
+ .pag-wrap {
515
+ display: none; flex-wrap: wrap; align-items: center;
516
+ justify-content: center; gap: 6px; margin-top: 36px;
517
+ }
518
+ .pag-btn {
519
+ width: 36px; height: 36px; border-radius: 50%;
520
+ border: 1px solid var(--bd); background: transparent;
521
+ color: var(--tx-2); cursor: pointer; font-size: .83rem;
522
+ display: flex; align-items: center; justify-content: center;
523
+ transition: all .18s; font-family: var(--sans);
524
+ }
525
+ .pag-btn:hover:not(:disabled) { border-color: var(--gold-bd); color: var(--gold); }
526
+ .pag-btn.active { background: var(--gold-glow); border-color: var(--gold-bd); color: var(--gold); }
527
+ .pag-btn:disabled { opacity: .25; cursor: default; }
528
+ .pag-info { font-family: var(--mono); font-size: .68rem; color: var(--tx-3); padding: 0 10px; letter-spacing: .06em; }
529
+
530
+ /* ── FOOTER ── */
531
+ .footer {
532
+ border-top: 1px solid var(--bd); margin-top: 70px; padding-top: 24px;
533
+ display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;
534
+ }
535
+ .footer-brand { font-family: var(--serif); font-size: .9rem; color: var(--tx-3); font-style: italic; }
536
+ .footer-brand span { color: var(--gold); }
537
+ .footer-note { font-family: var(--mono); font-size: .63rem; color: var(--tx-3); letter-spacing: .04em; }
538
+
539
+ /* ── DOC PAGE SPECIFICS ── */
540
+ .back-link {
541
+ display: inline-flex; align-items: center; gap: 7px;
542
+ font-size: .8rem; color: var(--tx-2); text-decoration: none;
543
+ border: 1px solid var(--bd); border-radius: 20px; padding: 7px 16px;
544
+ transition: color .2s, border-color .2s;
545
+ }
546
+ .back-link:hover { color: var(--tx); border-color: var(--bd-2); }
547
+
548
+ .doc-header-card {
549
+ background: var(--bg-1); border: 1px solid var(--bd);
550
+ border-radius: 16px; padding: 28px 30px; margin: 28px 0 20px;
551
+ position: relative; overflow: hidden;
552
+ }
553
+ .doc-header-card::before {
554
+ content: '';
555
+ position: absolute; top: 0; left: 0; right: 0; height: 1px;
556
+ background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
557
+ opacity: .6;
558
+ }
559
+ .doc-title {
560
+ font-family: var(--serif); font-size: 1.5rem; font-weight: 400;
561
+ color: var(--tx); margin-bottom: 18px; line-height: 1.3;
562
+ }
563
+
564
+ .doc-field {
565
+ border-radius: 12px; padding: 20px 22px; margin-bottom: 14px;
566
+ border-left: 2px solid transparent;
567
+ }
568
+ .doc-field.ementa { background: rgba(61,125,232,.07); border-left-color: var(--blue); }
569
+ .doc-field.decisao { background: rgba(124,92,191,.07); border-left-color: var(--purple); }
570
+ .doc-field.acordao { background: rgba(201,168,76,.06); border-left-color: var(--gold-dim); }
571
+
572
+ .doc-field-label {
573
+ font-family: var(--mono); font-size: .62rem; letter-spacing: .14em;
574
+ text-transform: uppercase; font-weight: 500;
575
+ margin-bottom: 12px; display: block;
576
+ }
577
+ .doc-field.ementa .doc-field-label { color: #7ab4ff; }
578
+ .doc-field.decisao .doc-field-label { color: #b39dff; }
579
+ .doc-field.acordao .doc-field-label { color: var(--gold); }
580
+
581
+ .doc-field-text {
582
+ font-size: .9rem; color: var(--tx-2); line-height: 1.78;
583
+ white-space: pre-wrap;
584
+ }
585
+
586
+ .inteiro-teor-link {
587
+ display: inline-flex; align-items: center; gap: 8px;
588
+ font-family: var(--sans); font-size: .85rem;
589
+ color: var(--gold); text-decoration: none;
590
+ border: 1px solid var(--gold-bd); border-radius: 10px;
591
+ padding: 12px 22px; margin-top: 16px;
592
+ background: var(--gold-glow); transition: background .2s;
593
+ }
594
+ .inteiro-teor-link:hover { background: rgba(201,168,76,.18); }
595
+
596
+ /* SVG icon inline helper */
597
+ .ico { display: inline-block; vertical-align: middle; }
598
+ """
599
+
600
+
601
  # ============================================
602
  # Rota principal (HTML)
603
  # ============================================
604
  @app.route('/')
605
  def index():
606
+ return render_template_string("""<!DOCTYPE html>
607
+ <html lang="pt-BR">
 
608
  <head>
609
+ <title>PARA AI Jurisprudência STF</title>
610
+ <meta charset="utf-8">
611
+ <meta name="viewport" content="width=device-width, initial-scale=1">
612
+ <style>""" + SHARED_CSS + """</style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  </head>
614
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
+ <!-- TOPBAR -->
617
+ <nav class="topbar">
618
+ <a href="/" class="brand">
619
+ <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo_dark.png"
620
+ alt="PARA AI" class="brand-logo"
621
+ onerror="this.style.display='none'">
622
+ <div class="brand-sep"></div>
623
+ <span class="brand-label">Jurisprudência STF</span>
624
+ </a>
625
+ <div class="topbar-right">
626
+ <span class="status-dot">online</span>
627
+ </div>
628
+ </nav>
 
 
 
 
 
629
 
630
+ <div class="shell">
 
 
 
631
 
632
+ <!-- HERO -->
633
+ <section class="hero">
634
+ <div class="hero-glow"></div>
 
 
 
635
 
636
+ <div class="hero-logo-wrap">
637
+ <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo_light.png"
638
+ alt="PARA AI" class="hero-logo"
639
+ onerror="this.style.display='none'">
640
+ </div>
641
+
642
+ <p class="hero-sub">Supremo Tribunal Federal · Pesquisa Jurisprudencial</p>
643
+ <h1 class="hero-title">Busque acórdãos e decisões<br><em>com precisão.</em></h1>
644
+
645
+ <!-- SEARCH -->
646
+ <div class="search-wrap">
647
+
648
+ <!-- Bases: linha discreta acima do input -->
649
+ <div class="bases-line">
650
+ <span>Bases:</span>
651
+ <label class="base-check">
652
+ <input type="checkbox" id="cbAcordaos" checked>
653
+ <span class="check-box"></span>
654
+ <span class="base-dot dot-ac"></span>
655
+ Acórdãos
656
+ </label>
657
+ <label class="base-check">
658
+ <input type="checkbox" id="cbDecisoes" checked>
659
+ <span class="check-box"></span>
660
+ <span class="base-dot dot-de"></span>
661
+ Decisões Monocráticas
662
+ </label>
663
+ </div>
664
+
665
+ <!-- Input + botão -->
666
+ <div class="search-row">
667
+ <input type="text" id="query" class="search-input"
668
+ placeholder="Ex.: habeas corpus tráfico, dano moral, ADI…"
669
+ autocomplete="off" autofocus>
670
+ <button id="searchBtn" class="search-btn" onclick="doSearch(1)">
671
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
672
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
673
+ </svg>
674
+ Pesquisar
675
+ </button>
676
+ </div>
677
 
 
 
 
 
678
  </div>
679
+ </section>
680
 
681
+ <!-- LOADER -->
682
+ <div id="loader">
683
+ <div class="spinner"></div>
684
+ <div class="loader-txt">consultando o STF…</div>
685
+ </div>
 
 
 
 
 
 
 
 
686
 
687
+ <!-- ERROR -->
688
+ <div class="err-box" id="errBox"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
690
+ <!-- STATS -->
691
+ <div class="stats-row" id="statsRow">
692
+ <div class="stat-card">
693
+ <div class="stat-val" id="statTotal">—</div>
694
+ <div class="stat-label">Total encontrado</div>
695
+ </div>
696
+ <div class="stat-card">
697
+ <div class="stat-val" id="statAcordaos">—</div>
698
+ <div class="stat-label">Acórdãos</div>
699
+ </div>
700
+ <div class="stat-card">
701
+ <div class="stat-val" id="statDecisoes">—</div>
702
+ <div class="stat-label">Decisões</div>
703
+ </div>
704
+ </div>
705
 
706
+ <!-- DIVIDER -->
707
+ <div class="section-divider" id="secDivider">
708
+ <div class="div-line"></div>
709
+ <span class="div-label" id="divLabel">Resultados</span>
710
+ <div class="div-line"></div>
711
+ </div>
712
 
713
+ <!-- EMPTY -->
714
+ <div class="empty-box" id="emptyBox">
715
+ <div class="empty-icon">⚖</div>
716
+ <p>Nenhum resultado para esta busca.</p>
717
+ </div>
718
 
719
+ <!-- RESULTS -->
720
+ <div id="results"></div>
 
 
 
721
 
722
+ <!-- PAGINATION -->
723
+ <div class="pag-wrap" id="pagWrap"></div>
 
724
 
725
+ <!-- FOOTER -->
726
+ <footer class="footer">
727
+ <span class="footer-brand">PARA<span>|AI</span></span>
728
+ <span class="footer-note">Dados: jurisprudencia.stf.jus.br · uso não-oficial</span>
729
+ </footer>
730
+
731
+ </div><!-- /shell -->
732
+
733
+ <script>
734
+ let curPage = 1, curQuery = '', totalRes = 0;
735
+ const PS = 10;
736
+
737
+ document.getElementById('query').addEventListener('keydown', e => {
738
+ if (e.key === 'Enter') doSearch(1);
739
+ });
740
+
741
+ function fmtNum(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : String(n); }
742
+
743
+ function fmtDate(d) {
744
+ if (!d) return null;
745
+ const m = String(d).match(/(\\d{4})-(\\d{2})-(\\d{2})/);
746
+ return m ? m[3]+'/'+m[2]+'/'+m[1] : String(d).substring(0,10);
747
+ }
748
+
749
+ function trunc(t, max=420) {
750
+ if (!t) return '';
751
+ return t.length > max ? t.substring(0, max) + '…' : t;
752
+ }
753
+
754
+ function setVisible(id, val) {
755
+ const el = document.getElementById(id);
756
+ if (el) el.style.display = val;
757
+ }
758
+
759
+ async function doSearch(page) {
760
+ const q = document.getElementById('query').value.trim();
761
+ if (!q) { document.getElementById('query').focus(); return; }
762
+
763
+ const ac = document.getElementById('cbAcordaos').checked;
764
+ const de = document.getElementById('cbDecisoes').checked;
765
+ if (!ac && !de) { showErr('Selecione ao menos uma base.'); return; }
766
+
767
+ curQuery = q; curPage = page;
768
+
769
+ document.getElementById('results').innerHTML = '';
770
+ setVisible('loader', 'block');
771
+ setVisible('errBox', 'none');
772
+ setVisible('emptyBox', 'none');
773
+ document.getElementById('statsRow').style.display = 'none';
774
+ document.getElementById('secDivider').style.display = 'none';
775
+ document.getElementById('pagWrap').style.display = 'none';
776
+ document.getElementById('searchBtn').disabled = true;
777
+
778
+ try {
779
+ const params = new URLSearchParams({q, acordaos: ac, decisoes: de, page, page_size: PS});
780
+ const res = await fetch('/api/busca-multipla?' + params);
781
+ const data = await res.json();
782
+
783
+ setVisible('loader', 'none');
784
+ document.getElementById('searchBtn').disabled = false;
785
+
786
+ if (!res.ok || data.error) throw new Error(data.error || 'Erro ' + res.status);
787
+ if (!data.resultados || !data.resultados.length) {
788
+ setVisible('emptyBox', 'block'); return;
789
+ }
790
+
791
+ totalRes = data.total || data.resultados.length;
792
+ const totalPages = Math.ceil(totalRes / PS);
793
 
794
+ // Stats
795
+ document.getElementById('statTotal').textContent = fmtNum(totalRes);
796
+ document.getElementById('statAcordaos').textContent = fmtNum(data.total_acordaos || 0);
797
+ document.getElementById('statDecisoes').textContent = fmtNum(data.total_decisoes || 0);
798
+ document.getElementById('statsRow').style.display = 'grid';
799
+
800
+ // Divider
801
+ document.getElementById('divLabel').textContent = 'Página ' + page + ' de ' + totalPages;
802
+ document.getElementById('secDivider').style.display = 'flex';
803
+
804
+ // Cards
805
+ const container = document.getElementById('results');
806
+ data.resultados.forEach((item, i) => {
807
+ const offset = (page-1)*PS + i + 1;
808
+ container.appendChild(buildCard(item, offset));
809
+ });
810
+
811
+ // Pagination
812
+ buildPag(totalPages);
813
+
814
+ if (page > 1) document.getElementById('results').scrollIntoView({behavior:'smooth'});
815
+
816
+ } catch(e) {
817
+ setVisible('loader', 'none');
818
+ document.getElementById('searchBtn').disabled = false;
819
+ showErr(e.message);
820
+ }
821
+ }
822
+
823
+ function showErr(msg) {
824
+ const el = document.getElementById('errBox');
825
+ el.style.display = 'block';
826
+ el.textContent = '✕ ' + msg;
827
+ }
828
+
829
+ function buildCard(item, num) {
830
+ const tipo = item.base === 'acordaos' ? 'acordaos' : 'decisoes';
831
+ const label = item.base === 'acordaos' ? 'ACÓRDÃO' : 'DECISÃO';
832
+ const date = fmtDate(item.data);
833
+ const pub = fmtDate(item.publicacao);
834
+
835
+ const div = document.createElement('div');
836
+ div.className = 'result-card ' + tipo;
837
+ div.style.animationDelay = (num % 10 * 35) + 'ms';
838
+
839
+ let meta = '';
840
+ if (item.processo) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚙</span>${item.processo}</span>`;
841
+ if (item.relator) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚖</span>${item.relator}</span>`;
842
+ if (item.orgao) meta += `<span class="meta-pill"><span class="meta-pill-icon">🏛</span>${item.orgao}</span>`;
843
+ if (date) meta += `<span class="meta-pill"><span class="meta-pill-icon">📅</span>${date}</span>`;
844
+ if (pub) meta += `<span class="meta-pill"><span class="meta-pill-icon">📢</span>DJe ${pub}</span>`;
845
+
846
+ let fields = '';
847
+ if (item.ementa)
848
+ fields += `<div class="field-block ementa"><span class="field-label">Ementa</span><div class="field-text">${trunc(item.ementa)}</div></div>`;
849
+ if (item.decisao_texto)
850
+ fields += `<div class="field-block decisao"><span class="field-label">Decisão</span><div class="field-text">${trunc(item.decisao_texto,360)}</div></div>`;
851
+ if (item.acordao_ata)
852
+ fields += `<div class="field-block acordao"><span class="field-label">Acórdão / Ata</span><div class="field-text">${trunc(item.acordao_ata,360)}</div></div>`;
853
+
854
+ let foot = `<a class="btn-ghost" href="/documento/${encodeURIComponent(item.id)}">
855
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
856
+ <path d="M14 2H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
857
+ </svg>
858
+ Ver documento
859
+ </a>`;
860
+ if (item.url_documento)
861
+ foot += `<a class="btn-gold" href="${item.url_documento}" target="_blank" rel="noopener">
862
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
863
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
864
+ <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
865
+ </svg>
866
+ Inteiro teor
867
+ </a>`;
868
+
869
+ div.innerHTML = `
870
+ <div class="card-head">
871
+ <div class="card-title">${item.titulo || item.processo || 'Documento STF'}</div>
872
+ <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">
873
+ <span class="tipo-tag ${tipo}"><span class="tipo-tag-dot"></span>${label}</span>
874
+ <span class="card-num">#${num}</span>
875
+ </div>
876
+ </div>
877
+ <div class="meta-row">${meta}</div>
878
+ ${fields}
879
+ <div class="card-foot">${foot}</div>`;
880
+
881
+ return div;
882
+ }
883
+
884
+ function buildPag(total) {
885
+ const pg = document.getElementById('pagWrap');
886
+ pg.innerHTML = '';
887
+ if (total <= 1) return;
888
+ pg.style.display = 'flex';
889
+
890
+ const btn = (lbl, page, active, disabled) => {
891
+ const b = document.createElement('button');
892
+ b.className = 'pag-btn' + (active ? ' active' : '');
893
+ b.textContent = lbl; b.disabled = disabled;
894
+ if (!disabled && !active) b.onclick = () => doSearch(page);
895
+ pg.appendChild(b);
896
+ };
897
+
898
+ btn('‹', curPage-1, false, curPage===1);
899
+ const s = Math.max(1, curPage-2), e = Math.min(total, curPage+2);
900
+ for (let p=s; p<=e; p++) btn(p, p, p===curPage, false);
901
+ btn('›', curPage+1, false, curPage===total);
902
+
903
+ const info = document.createElement('span');
904
+ info.className = 'pag-info';
905
+ info.textContent = curPage + ' / ' + total;
906
+ pg.insertBefore(info, pg.children[Math.floor(pg.children.length/2)]);
907
+ }
908
+ </script>
909
  </body>
910
  </html>
911
  """)
912
 
913
+
914
  # ============================================
915
  # Página de detalhes do documento
916
  # ============================================
917
  @app.route('/documento/<doc_id>')
918
  def documento_detalhe(doc_id):
919
+ return render_template_string("""<!DOCTYPE html>
920
+ <html lang="pt-BR">
 
921
  <head>
922
+ <title>PARA AI Documento {{ doc_id }}</title>
923
+ <meta charset="utf-8">
924
+ <meta name="viewport" content="width=device-width, initial-scale=1">
925
+ <style>""" + SHARED_CSS + """</style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  </head>
927
  <body>
928
+
929
+ <!-- TOPBAR -->
930
+ <nav class="topbar">
931
+ <a href="/" class="brand">
932
+ <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo_light.png"
933
+ alt="PARA AI" class="brand-logo" onerror="this.style.display='none'">
934
+ <div class="brand-sep"></div>
935
+ <span class="brand-label">Jurisprudência STF</span>
936
+ </a>
937
+ <div class="topbar-right">
938
+ <a href="/" class="back-link">
939
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
940
+ <path d="M19 12H5M12 5l-7 7 7 7"/>
941
+ </svg>
942
+ Voltar à busca
943
+ </a>
944
+ </div>
945
+ </nav>
946
+
947
+ <div class="shell" style="padding-top:0">
948
+
949
+ <div id="loader" style="display:block">
950
+ <div style="padding:80px 0; text-align:center">
951
+ <div class="spinner"></div>
952
+ <div class="loader-txt">carregando documento…</div>
953
  </div>
954
+ </div>
955
 
956
+ <div class="err-box" id="errBox"></div>
957
+ <div id="docWrap" style="display:none"></div>
958
+
959
+ <footer class="footer">
960
+ <span class="footer-brand">PARA<span>|AI</span></span>
961
+ <span class="footer-note">Dados: jurisprudencia.stf.jus.br · uso não-oficial</span>
962
+ </footer>
963
+ </div>
964
+
965
+ <script>
966
+ const DOC_ID = "{{ doc_id }}";
967
+
968
+ function fmtDate(d) {
969
+ if (!d) return null;
970
+ const m = String(d).match(/(\\d{4})-(\\d{2})-(\\d{2})/);
971
+ return m ? m[3]+'/'+m[2]+'/'+m[1] : String(d).substring(0,10);
972
+ }
973
+
974
+ async function loadDoc() {
975
+ try {
976
+ const res = await fetch('/api/documento/' + DOC_ID);
977
+ const data = await res.json();
978
+ document.getElementById('loader').style.display = 'none';
979
+
980
+ if (data.error) throw new Error(data.error);
981
+
982
+ const tipo = data.base === 'acordaos' ? 'acordaos' : 'decisoes';
983
+ const label = data.base === 'acordaos' ? 'Acórdão' : 'Decisão Monocrática';
984
+ const date = fmtDate(data.data);
985
+ const pub = fmtDate(data.publicacao);
986
+
987
+ let meta = `<span class="meta-pill">${label}</span>`;
988
+ if (data.relator) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚖</span>${data.relator}</span>`;
989
+ if (data.orgao) meta += `<span class="meta-pill"><span class="meta-pill-icon">🏛</span>${data.orgao}</span>`;
990
+ if (date) meta += `<span class="meta-pill"><span class="meta-pill-icon">📅</span>${date}</span>`;
991
+ if (pub) meta += `<span class="meta-pill"><span class="meta-pill-icon">📢</span>DJe ${pub}</span>`;
992
+ if (data.processo) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚙</span>${data.processo}</span>`;
993
+
994
+ let fields = '';
995
+ if (data.ementa)
996
+ fields += `<div class="doc-field ementa"><span class="doc-field-label">Ementa</span><div class="doc-field-text">${data.ementa.replace(/\\n/g,'<br>')}</div></div>`;
997
+ if (data.decisao_texto)
998
+ fields += `<div class="doc-field decisao"><span class="doc-field-label">Decisão</span><div class="doc-field-text">${data.decisao_texto.replace(/\\n/g,'<br>')}</div></div>`;
999
+ if (data.acordao_ata)
1000
+ fields += `<div class="doc-field acordao"><span class="doc-field-label">Acórdão / Ata</span><div class="doc-field-text">${data.acordao_ata.replace(/\\n/g,'<br>')}</div></div>`;
1001
+
1002
+ let teor = '';
1003
+ if (data.url_documento)
1004
+ teor = `<a class="inteiro-teor-link" href="${data.url_documento}" target="_blank" rel="noopener">
1005
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1006
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
1007
+ <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
1008
+ </svg>
1009
+ Ver inteiro teor (PDF/HTML)
1010
+ </a>`;
1011
+
1012
+ document.getElementById('docWrap').style.display = 'block';
1013
+ document.getElementById('docWrap').innerHTML = `
1014
+ <div class="doc-header-card">
1015
+ <div class="doc-title">${data.titulo || data.processo || 'Documento STF'}</div>
1016
+ <div class="meta-row">${meta}</div>
1017
+ </div>
1018
+ ${fields}
1019
+ ${teor}`;
1020
+
1021
+ } catch(e) {
1022
+ document.getElementById('loader').style.display = 'none';
1023
+ const el = document.getElementById('errBox');
1024
+ el.style.display = 'block';
1025
+ el.textContent = '✕ ' + e.message;
1026
+ }
1027
+ }
1028
+
1029
+ loadDoc();
1030
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
  </body>
1032
  </html>
1033
  """, doc_id=doc_id)
1034
 
1035
+
1036
  # ============================================
1037
  # API para buscar documento por ID
1038
  # ============================================
 
1043
  return jsonify({"error": "Não foi possível obter token"}), 503
1044
 
1045
  payload = {
1046
+ "query": {"ids": {"values": [doc_id]}},
1047
+ "_source": ["id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata",
1048
+ "processo_codigo_completo", "relator_processo_nome",
1049
+ "orgao_julgador", "julgamento_data", "publicacao_data", "inteiro_teor_url", "base"],
 
 
 
 
 
 
 
1050
  "size": 1
1051
  }
1052
 
 
1107
  except Exception as e:
1108
  return jsonify({"error": str(e)}), 500
1109
 
1110
+
1111
  # ============================================
1112
  # Endpoint /api/busca-multipla (retorna JSON)
1113
  # ============================================
 
1121
 
1122
  if not query:
1123
  return jsonify({"error": "Parâmetro 'q' obrigatório"}), 400
 
1124
  if not busca_acordaos and not busca_decisoes:
1125
  return jsonify({"error": "Selecione pelo menos uma base para pesquisa"}), 400
1126
 
 
1127
  bases = []
1128
+ if busca_acordaos: bases.append("acordaos")
1129
+ if busca_decisoes: bases.append("decisoes")
 
 
1130
 
1131
  result = search_stf(query, bases, page, page_size)
1132
  if isinstance(result, tuple):
1133
  return jsonify(result[0]), result[1]
1134
 
 
1135
  hits = result.get('result', {}).get('hits', {}).get('hits', [])
1136
  resultados = []
1137
  total_acordaos = 0
 
1140
  for hit in hits:
1141
  src = hit.get('_source', {})
1142
  base = src.get('base', '')
1143
+ if base == 'acordaos': total_acordaos += 1
1144
+ elif base == 'decisoes': total_decisoes += 1
 
 
 
1145
  item = {
1146
  "id": src.get('id') or hit.get('_id'),
1147
  "titulo": src.get('titulo'),
 
1157
  "base": base,
1158
  "score": hit.get('_score')
1159
  }
 
1160
  item = {k: v for k, v in item.items() if v is not None}
1161
  resultados.append(item)
1162
 
1163
  return jsonify({
1164
+ "q": query, "bases": bases, "page": page, "page_size": page_size,
 
 
 
1165
  "total": result.get('result', {}).get('hits', {}).get('total', {}).get('value', len(resultados)),
1166
  "total_acordaos": total_acordaos,
1167
  "total_decisoes": total_decisoes,
1168
  "resultados": resultados
1169
  })
1170
 
1171
+
1172
  # ============================================
1173
  # Endpoint de saúde
1174
  # ============================================
 
1187
  "token_cached": bool(token_cache["token"])
1188
  })
1189
 
1190
+
1191
  if __name__ == '__main__':
1192
  try:
1193
  import certifi
 
1200
  logger.info("📋 Bases: acordaos e decisoes")
1201
  logger.info("="*50)
1202
  port = int(os.environ.get('PORT', 7860))
1203
+ app.run(host='0.0.0.0', port=port, debug=False)