|
|
| import os |
| import sys |
| import json |
| import time |
| import logging |
| import requests |
| import urllib3 |
| from flask import Flask, request, jsonify, render_template_string, url_for |
| from playwright.sync_api import sync_playwright |
|
|
| |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| app = Flask(__name__) |
|
|
| |
| STF_API_URL = "https://jurisprudencia.stf.jus.br/api/search/search" |
|
|
| |
| token_cache = {"token": None, "expires_at": 0} |
|
|
| HEADERS = { |
| "Accept": "application/json, text/plain, */*", |
| "Content-Type": "application/json", |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", |
| "Referer": "https://jurisprudencia.stf.jus.br/pages/search", |
| "Origin": "https://jurisprudencia.stf.jus.br" |
| } |
|
|
| |
| |
| |
| def get_fresh_token(force_refresh=False): |
| global token_cache |
| if not force_refresh and token_cache["token"] and time.time() < token_cache["expires_at"]: |
| logger.info("Usando token em cache") |
| return token_cache["token"] |
| |
| logger.info("Obtendo novo token via Playwright" + (" (forçado)" if force_refresh else "")) |
| try: |
| with sync_playwright() as p: |
| browser = p.chromium.launch(headless=True, args=['--no-sandbox']) |
| context = browser.new_context( |
| viewport={'width': 1920, 'height': 1080}, |
| user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
| ) |
| page = context.new_page() |
| page.goto("https://jurisprudencia.stf.jus.br/pages/search", wait_until='domcontentloaded', timeout=30000) |
| page.wait_for_timeout(3000) |
| cookies = context.cookies() |
| token = None |
| for cookie in cookies: |
| if cookie.get('name') == 'aws-waf-token': |
| token = cookie.get('value') |
| break |
| browser.close() |
| if token: |
| token_cache["token"] = token |
| token_cache["expires_at"] = time.time() + 600 |
| logger.info(f"Token obtido: {token[:30]}...") |
| return token |
| else: |
| logger.warning("Token não encontrado nos cookies") |
| return None |
| except Exception as e: |
| logger.error(f"Erro ao obter token: {str(e)}") |
| return None |
|
|
| def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20): |
| token = get_fresh_token() |
| if not token: |
| return {"error": "Não foi possível obter token de acesso"}, 503 |
|
|
| from_idx = (page - 1) * page_size |
|
|
| should_filters = [] |
| for base in bases: |
| should_filters.append({"term": {"base": base}}) |
| |
| payload = { |
| "query": { |
| "bool": { |
| "filter": [{"bool": {"should": should_filters, "minimum_should_match": 1}}], |
| "must": [{"bool": {"should": [ |
| {"query_string": {"fields": ["ementa_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}}, |
| {"query_string": {"fields": ["decisao_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}}, |
| {"query_string": {"fields": ["acordao_ata"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}} |
| ]}}] |
| } |
| }, |
| "_source": ["id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata", |
| "processo_codigo_completo", "relator_processo_nome", |
| "orgao_julgador", "julgamento_data", "publicacao_data", "inteiro_teor_url", "base"], |
| "size": page_size, |
| "from": from_idx, |
| "sort": [{"julgamento_data": {"order": "desc"}}], |
| "track_total_hits": True |
| } |
|
|
| headers = HEADERS.copy() |
| headers['Cookie'] = f'aws-waf-token={token}' |
|
|
| try: |
| response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) |
| if response.status_code == 200: |
| return response.json() |
| elif response.status_code == 202: |
| token = get_fresh_token(force_refresh=True) |
| if token: |
| headers['Cookie'] = f'aws-waf-token={token}' |
| response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) |
| if response.status_code == 200: |
| return response.json() |
| return {"error": "Token expirado e renovação falhou"}, 503 |
| else: |
| return {"error": f"API retornou status {response.status_code}"}, response.status_code |
| except Exception as e: |
| return {"error": str(e)}, 502 |
|
|
|
|
| |
| |
| |
| SHARED_CSS = """ |
| @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'); |
| |
| :root { |
| --bg: #0e1018; |
| --bg-1: #13161f; |
| --bg-2: #191d28; |
| --bg-3: #1f2435; |
| --gold: #c9a84c; |
| --gold-lt: #e8cf82; |
| --gold-dim: #7a6030; |
| --gold-glow: rgba(201,168,76,.12); |
| --gold-glow2: rgba(201,168,76,.05); |
| --gold-bd: rgba(201,168,76,.22); |
| --blue: #3d7de8; |
| --blue-dim: rgba(61,125,232,.13); |
| --blue-bd: rgba(61,125,232,.30); |
| --purple: #7c5cbf; |
| --purple-dim: rgba(124,92,191,.13); |
| --purple-bd: rgba(124,92,191,.30); |
| --tx: #e6e8ee; |
| --tx-2: #8e91a8; |
| --tx-3: #484c62; |
| --bd: rgba(255,255,255,.07); |
| --bd-2: rgba(255,255,255,.11); |
| --red-dim: rgba(239,68,68,.12); |
| --red-bd: rgba(239,68,68,.28); |
| --serif: 'Cormorant Garamond', Georgia, serif; |
| --sans: 'DM Sans', system-ui, sans-serif; |
| --mono: 'JetBrains Mono', monospace; |
| } |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| html { |
| scroll-behavior: smooth; |
| overflow-x: hidden; |
| } |
| |
| body { |
| font-family: var(--sans); |
| background: var(--bg); |
| color: var(--tx); |
| min-height: 100vh; |
| -webkit-font-smoothing: antialiased; |
| line-height: 1.6; |
| overflow-x: hidden; |
| width: 100%; |
| } |
| |
| /* Grain */ |
| body::after { |
| content: ''; |
| position: fixed; inset: 0; |
| 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"); |
| pointer-events: none; z-index: 9999; opacity: .5; |
| } |
| |
| ::selection { background: var(--gold-glow); color: var(--tx); } |
| ::-webkit-scrollbar { width: 3px; } |
| ::-webkit-scrollbar-thumb { background: var(--gold-dim); border-radius: 3px; } |
| |
| /* ── TOPBAR ── */ |
| .topbar { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 14px 16px; |
| border-bottom: 1px solid var(--bd); |
| position: sticky; top: 0; z-index: 100; |
| background: rgba(14,16,24,.92); |
| backdrop-filter: blur(14px); |
| -webkit-backdrop-filter: blur(14px); |
| width: 100%; box-sizing: border-box; |
| } |
| |
| .brand { display: flex; align-items: center; gap: 14px; text-decoration: none; } |
| |
| .brand-logo { height: 52px; width: auto; object-fit: contain; } |
| |
| .brand-sep { width: 1px; height: 20px; background: var(--bd-2); } |
| |
| .brand-label { |
| font-family: var(--mono); |
| font-size: .65rem; |
| letter-spacing: .18em; |
| text-transform: uppercase; |
| color: var(--tx-3); |
| } |
| |
| .topbar-right { display: flex; align-items: center; gap: 14px; } |
| |
| .status-dot { |
| display: flex; align-items: center; gap: 6px; |
| font-family: var(--mono); font-size: .68rem; |
| color: var(--tx-3); letter-spacing: .06em; |
| } |
| .status-dot::before { |
| content: ''; width: 6px; height: 6px; border-radius: 50%; |
| background: #4ade80; |
| box-shadow: 0 0 6px rgba(74,222,128,.6); |
| } |
| |
| /* ── SHELL ── */ |
| .shell { max-width: 1060px; margin: 0 auto; padding: 0 16px 80px; width: 100%; box-sizing: border-box; } |
| |
| /* ── HERO ── */ |
| .hero { padding: 56px 0 44px; text-align: center; position: relative; align-items: center;} |
| |
| .hero-glow { |
| position: absolute; top: 0; left: 50%; transform: translateX(-50%); |
| width: 600px; height: 300px; |
| background: radial-gradient(ellipse at center top, rgba(201,168,76,.08) 0%, transparent 70%); |
| pointer-events: none; |
| } |
| |
| .hero-logo-wrap { margin-bottom: 28px; margin-top: 82px; } |
| .hero-logo { height: 186px; width: auto; object-fit: contain; filter: drop-shadow(0 0 24px rgba(201,168,76,.15)); } |
| |
| .hero-sub { |
| font-size: .8rem; |
| font-family: var(--mono); |
| letter-spacing: .2em; |
| text-transform: uppercase; |
| color: var(--tx-3); |
| margin-bottom: 10px; |
| } |
| |
| .hero-title { |
| font-family: var(--serif); |
| font-size: clamp(1.8rem, 4vw, 3rem); |
| font-weight: 300; |
| color: var(--tx); |
| line-height: 1.15; |
| margin-bottom: 10px; |
| } |
| .hero-title em { font-style: italic; color: var(--gold-lt); } |
| /* ── SEARCH AREA ── */ |
| .search-wrap { |
| margin-top: 36px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; /* centraliza os filhos horizontalmente */ |
| width: 100%; |
| } |
| |
| /* Filtros de base — linha simples e centralizada */ |
| .bases-line { |
| display: flex; |
| align-items: center; |
| justify-content: center; /* centraliza o conteúdo interno */ |
| gap: 20px; |
| margin-bottom: 14px; |
| font-size: .8rem; |
| color: var(--tx-3); |
| width: 100%; /* ocupa toda a largura para que o centro seja relativo à tela */ |
| } |
| |
| .base-check { |
| display: flex; align-items: center; gap: 7px; |
| cursor: pointer; user-select: none; |
| color: var(--tx-2); |
| transition: color .2s; |
| } |
| .base-check:hover { color: var(--tx); } |
| .base-check input { display: none; } |
| |
| .check-box { |
| width: 15px; height: 15px; |
| border: 1px solid var(--bd-2); border-radius: 4px; |
| display: flex; align-items: center; justify-content: center; |
| transition: all .2s; flex-shrink: 0; |
| } |
| .base-check input:checked + .check-box { border-color: var(--gold-dim); background: var(--gold-glow); } |
| .base-check input:checked + .check-box::after { |
| content: ''; width: 6px; height: 6px; border-radius: 2px; background: var(--gold); |
| } |
| |
| .base-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } |
| .dot-ac { background: var(--blue); } |
| .dot-de { background: var(--purple); } |
| |
| /* Input row */ |
| .search-row { |
| display: flex; gap: 10px; align-items: center; |
| background: var(--bg-1); |
| border: 1px solid var(--bd-2); |
| border-radius: 14px; |
| padding: 6px 12px 6px 16px; |
| transition: border-color .25s, box-shadow .25s; |
| width: 100%; box-sizing: border-box; |
| } |
| .search-row:focus-within { |
| border-color: var(--gold-dim); |
| box-shadow: 0 0 0 3px var(--gold-glow), 0 8px 32px rgba(0,0,0,.3); |
| } |
| |
| .search-input { |
| flex: 1; background: transparent; border: none; outline: none; |
| font-family: var(--sans); font-size: .95rem; color: var(--tx); |
| padding: 10px 0; min-width: 0; |
| } |
| .search-input::placeholder { color: var(--tx-3); } |
| |
| .search-btn { |
| background: linear-gradient(135deg, #6b4f1a, var(--gold)); |
| border: none; border-radius: 10px; |
| padding: 11px 26px; |
| font-family: var(--sans); font-size: .85rem; font-weight: 500; |
| color: #0e1018; letter-spacing: .04em; |
| cursor: pointer; white-space: nowrap; |
| display: flex; align-items: center; gap: 8px; |
| transition: opacity .2s, transform .15s; |
| flex-shrink: 0; |
| } |
| .search-btn:hover { opacity: .88; transform: translateY(-1px); } |
| .search-btn:disabled { opacity: .35; cursor: not-allowed; transform: none; } |
| |
| /* ── STATS CARDS ── */ |
| .stats-row { |
| display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; |
| margin: 32px 0 0; display: none; |
| } |
| |
| .stat-card { |
| background: var(--bg-1); border: 1px solid var(--bd); |
| border-radius: 14px; padding: 20px 22px; text-align: center; |
| position: relative; overflow: hidden; |
| transition: border-color .2s; |
| } |
| .stat-card::before { |
| content: ''; |
| position: absolute; top: 0; left: 0; right: 0; height: 1px; |
| background: linear-gradient(90deg, transparent, var(--gold-dim), transparent); |
| opacity: .5; |
| } |
| .stat-card:hover { border-color: var(--gold-bd); } |
| |
| .stat-val { |
| font-family: var(--serif); font-size: 2rem; font-weight: 300; |
| color: var(--gold-lt); line-height: 1; margin-bottom: 4px; |
| } |
| .stat-label { |
| font-family: var(--mono); font-size: .63rem; letter-spacing: .12em; |
| text-transform: uppercase; color: var(--tx-3); |
| } |
| |
| /* ── DIVIDER ── */ |
| .section-divider { |
| display: none; align-items: center; gap: 14px; margin: 28px 0 20px; |
| } |
| .div-line { flex: 1; height: 1px; background: var(--bd); } |
| .div-label { |
| font-family: var(--mono); font-size: .65rem; letter-spacing: .14em; |
| text-transform: uppercase; color: var(--tx-3); white-space: nowrap; |
| } |
| |
| /* ── RESULT CARDS ── */ |
| .result-card { |
| background: var(--bg-1); border: 1px solid var(--bd); |
| border-radius: 16px; padding: 22px 20px; |
| margin-bottom: 12px; position: relative; overflow: hidden; |
| transition: border-color .22s, box-shadow .22s, transform .15s; |
| animation: riseUp .3s ease both; |
| width: 100%; box-sizing: border-box; |
| } |
| |
| @keyframes riseUp { |
| from { opacity: 0; transform: translateY(8px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| /* Accent linha esquerda — neutro */ |
| .result-card::before { |
| content: ''; |
| position: absolute; left: 0; top: 0; bottom: 0; width: 2px; |
| background: rgba(255,255,255,.08); |
| transition: opacity .2s; |
| } |
| .result-card:hover::before { background: var(--gold-dim); } |
| |
| /* Corner circuit decoration */ |
| .result-card::after { |
| content: ''; |
| position: absolute; right: 0; top: 0; |
| width: 80px; height: 80px; |
| background: radial-gradient(circle at top right, rgba(255,255,255,.025) 0%, transparent 70%); |
| pointer-events: none; |
| } |
| |
| .result-card:hover { |
| border-color: var(--gold-bd); |
| box-shadow: 0 0 0 1px var(--gold-bd), 0 12px 40px rgba(0,0,0,.35); |
| transform: translateY(-1px); |
| } |
| |
| /* Card header */ |
| .card-head { |
| display: flex; align-items: flex-start; justify-content: space-between; |
| gap: 14px; margin-bottom: 14px; |
| } |
| .card-title { |
| font-family: var(--serif); font-size: 1.08rem; font-weight: 400; |
| color: var(--tx); line-height: 1.35; flex: 1; |
| } |
| .card-num { |
| font-family: var(--mono); font-size: .62rem; color: var(--tx-3); |
| flex-shrink: 0; margin-top: 3px; |
| } |
| |
| /* Tipo tag */ |
| .tipo-tag { |
| display: inline-flex; align-items: center; gap: 5px; |
| font-family: var(--mono); font-size: .58rem; letter-spacing: .05em; |
| padding: 3px 8px; border-radius: 20px; font-weight: 500; |
| flex-shrink: 0; margin-top: 2px; white-space: nowrap; |
| } |
| .tipo-tag.acordaos { background: var(--blue-dim); color: #7ab4ff; border: 1px solid var(--blue-bd); } |
| .tipo-tag.decisoes { background: var(--purple-dim); color: #b39dff; border: 1px solid var(--purple-bd); } |
| .tipo-tag-dot { width: 5px; height: 5px; border-radius: 50%; } |
| .tipo-tag.acordaos .tipo-tag-dot { background: var(--blue); } |
| .tipo-tag.decisoes .tipo-tag-dot { background: var(--purple); } |
| |
| /* Meta pills */ |
| .meta-row { |
| display: flex; flex-wrap: wrap; gap: 6px; |
| margin-bottom: 16px; padding-bottom: 16px; |
| border-bottom: 1px solid var(--bd); |
| } |
| .meta-pill { |
| font-size: .70rem; color: var(--tx-2); |
| background: var(--bg-2); border: 1px solid var(--bd); |
| border-radius: 20px; padding: 3px 10px; |
| display: flex; align-items: center; gap: 4px; |
| max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| } |
| .meta-pill-icon { opacity: .55; font-size: .75rem; } |
| |
| /* Field blocks */ |
| .field-block { |
| border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; |
| background: rgba(255,255,255,.03); |
| border: 1px solid rgba(255,255,255,.07); |
| border-left: 2px solid rgba(255,255,255,.12); |
| } |
| .field-block.acordao { border-left-color: var(--gold-dim); } |
| |
| .field-label { |
| font-family: var(--mono); font-size: .60rem; letter-spacing: .14em; |
| text-transform: uppercase; font-weight: 500; |
| margin-bottom: 7px; display: block; |
| color: var(--tx-3); |
| } |
| .field-block.acordao .field-label { color: var(--gold-dim); } |
| |
| .field-text { |
| font-size: .84rem; color: var(--tx-2); line-height: 1.7; |
| white-space: pre-wrap; max-height: 200px; overflow-y: auto; |
| } |
| .field-text::-webkit-scrollbar { width: 2px; } |
| .field-text::-webkit-scrollbar-thumb { background: var(--bd-2); } |
| |
| /* Card footer */ |
| .card-foot { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; } |
| |
| .btn-ghost { |
| font-family: var(--sans); font-size: .77rem; color: var(--tx-2); |
| background: var(--bg-2); border: 1px solid var(--bd); |
| border-radius: 20px; padding: 7px 16px; |
| text-decoration: none; display: inline-flex; align-items: center; gap: 6px; |
| transition: color .2s, border-color .2s; cursor: pointer; |
| } |
| .btn-ghost:hover { color: var(--tx); border-color: var(--bd-2); } |
| |
| .btn-gold { |
| font-family: var(--sans); font-size: .77rem; color: var(--gold); |
| background: var(--gold-glow); border: 1px solid var(--gold-bd); |
| border-radius: 20px; padding: 7px 16px; |
| text-decoration: none; display: inline-flex; align-items: center; gap: 6px; |
| transition: background .2s; |
| } |
| .btn-gold:hover { background: rgba(201,168,76,.18); } |
| |
| /* ── LOADER ── */ |
| #loader { display: none; text-align: center; padding: 60px 0; } |
| .spinner { |
| width: 32px; height: 32px; margin: 0 auto 14px; |
| border: 2px solid var(--bd); border-top-color: var(--gold); |
| border-radius: 50%; animation: spin .75s linear infinite; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .loader-txt { font-family: var(--mono); font-size: .72rem; color: var(--tx-3); letter-spacing: .1em; } |
| |
| /* ── ERROR ── */ |
| .err-box { |
| display: none; background: var(--red-dim); border: 1px solid var(--red-bd); |
| border-radius: 12px; padding: 16px 20px; font-size: .88rem; |
| color: #fca5a5; margin-top: 16px; |
| } |
| |
| /* ── EMPTY ── */ |
| .empty-box { display: none; text-align: center; padding: 70px 0; color: var(--tx-3); } |
| .empty-icon { font-size: 2rem; margin-bottom: 10px; opacity: .4; } |
| |
| /* ── PAGINATION ── */ |
| .pag-wrap { |
| display: none; flex-wrap: wrap; align-items: center; |
| justify-content: center; gap: 6px; margin-top: 36px; |
| } |
| .pag-btn { |
| width: 36px; height: 36px; border-radius: 50%; |
| border: 1px solid var(--bd); background: transparent; |
| color: var(--tx-2); cursor: pointer; font-size: .83rem; |
| display: flex; align-items: center; justify-content: center; |
| transition: all .18s; font-family: var(--sans); |
| } |
| .pag-btn:hover:not(:disabled) { border-color: var(--gold-bd); color: var(--gold); } |
| .pag-btn.active { background: var(--gold-glow); border-color: var(--gold-bd); color: var(--gold); } |
| .pag-btn:disabled { opacity: .25; cursor: default; } |
| .pag-info { font-family: var(--mono); font-size: .68rem; color: var(--tx-3); padding: 0 10px; letter-spacing: .06em; } |
| |
| /* ── FOOTER ── */ |
| .footer { |
| border-top: 1px solid var(--bd); margin-top: 70px; padding-top: 24px; |
| display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; |
| } |
| .footer-brand { font-family: var(--serif); font-size: .9rem; color: var(--tx-3); font-style: italic; } |
| .footer-brand span { color: var(--gold); } |
| .footer-note { font-family: var(--mono); font-size: .63rem; color: var(--tx-3); letter-spacing: .04em; } |
| |
| /* ── DOC PAGE SPECIFICS ── */ |
| .back-link { |
| display: inline-flex; align-items: center; gap: 7px; |
| font-size: .8rem; color: var(--tx-2); text-decoration: none; |
| border: 1px solid var(--bd); border-radius: 20px; padding: 7px 16px; |
| transition: color .2s, border-color .2s; |
| } |
| .back-link:hover { color: var(--tx); border-color: var(--bd-2); } |
| |
| .doc-header-card { |
| background: var(--bg-1); border: 1px solid var(--bd); |
| border-radius: 16px; padding: 28px 30px; margin: 28px 0 20px; |
| position: relative; overflow: hidden; |
| } |
| .doc-header-card::before { |
| content: ''; |
| position: absolute; top: 0; left: 0; right: 0; height: 1px; |
| background: linear-gradient(90deg, transparent, var(--gold-dim), transparent); |
| opacity: .6; |
| } |
| .doc-title { |
| font-family: var(--serif); font-size: 1.5rem; font-weight: 400; |
| color: var(--tx); margin-bottom: 18px; line-height: 1.3; |
| } |
| |
| .doc-field { |
| border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; |
| background: rgba(255,255,255,.03); |
| border: 1px solid rgba(255,255,255,.07); |
| border-left: 2px solid rgba(255,255,255,.12); |
| } |
| .doc-field.acordao { border-left-color: var(--gold-dim); } |
| |
| .doc-field-label { |
| font-family: var(--mono); font-size: .62rem; letter-spacing: .14em; |
| text-transform: uppercase; font-weight: 500; |
| margin-bottom: 12px; display: block; |
| color: var(--tx-3); |
| } |
| .doc-field.acordao .doc-field-label { color: var(--gold-dim); } |
| |
| .doc-field-text { |
| font-size: .9rem; color: var(--tx-2); line-height: 1.78; |
| white-space: pre-wrap; |
| } |
| |
| .inteiro-teor-link { |
| display: inline-flex; align-items: center; gap: 8px; |
| font-family: var(--sans); font-size: .85rem; |
| color: var(--gold); text-decoration: none; |
| border: 1px solid var(--gold-bd); border-radius: 10px; |
| padding: 12px 22px; margin-top: 16px; |
| background: var(--gold-glow); transition: background .2s; |
| } |
| .inteiro-teor-link:hover { background: rgba(201,168,76,.18); } |
| |
| /* SVG icon inline helper */ |
| .ico { display: inline-block; vertical-align: middle; } |
| """ |
|
|
|
|
| |
| |
| |
| @app.route('/') |
| def index(): |
| return render_template_string("""<!DOCTYPE html> |
| <html lang="pt-BR"> |
| <head> |
| <title>PARA AI — Jurisprudência STF</title> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <style>""" + SHARED_CSS + """</style> |
| </head> |
| <body> |
| |
| <!-- TOPBAR --> |
| <nav class="topbar"> |
| <a href="/" class="brand"> |
| <span class="brand-label">STF | TJPR</span> |
| </a> |
| <div class="topbar-right"> |
| <span class="status-dot">online</span> |
| </div> |
| </nav> |
| |
| <div class="shell"> |
| |
| <!-- HERO --> |
| <section class="hero"> |
| <div class="hero-glow"></div> |
| |
| <div class="hero-logo-wrap"> |
| <img src="https://huggingface.co/spaces/caarleexx/paraAI_CHATBOT/resolve/main/public/logo.png" |
| alt="PARA AI" class="hero-logo" |
| onerror="this.style.display='none'"> |
| </div> |
| |
| <h1 class="hero-title">Busque acórdãos e decisões<br></h1> |
| |
| <!-- SEARCH --> |
| <div class="search-wrap"> |
| |
| <!-- Bases: linha discreta acima do input --> |
| <div class="bases-line"> |
| <span>Bases:</span> |
| <label class="base-check"> |
| <input type="checkbox" id="cbAcordaos" checked> |
| <span class="check-box"></span> |
| <span class="base-dot dot-ac"></span> |
| Acórdãos |
| </label> |
| <label class="base-check"> |
| <input type="checkbox" id="cbDecisoes" checked> |
| <span class="check-box"></span> |
| <span class="base-dot dot-de"></span> |
| Decisões Monocráticas |
| </label> |
| </div> |
| |
| <!-- Input + botão --> |
| <div class="search-row"> |
| <input type="text" id="query" class="search-input" |
| placeholder="Ex.: habeas corpus tráfico, dano moral, ADI…" |
| autocomplete="off" autofocus> |
| <button id="searchBtn" class="search-btn" onclick="doSearch(1)"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"> |
| <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/> |
| </svg> |
| Pesquisar |
| </button> |
| </div> |
| |
| </div> |
| </section> |
| |
| <!-- LOADER --> |
| <div id="loader"> |
| <div class="spinner"></div> |
| <div class="loader-txt">consultando o STF…</div> |
| </div> |
| |
| <!-- ERROR --> |
| <div class="err-box" id="errBox"></div> |
| |
| <!-- STATS --> |
| <div class="stats-row" id="statsRow"> |
| <div class="stat-card"> |
| <div class="stat-val" id="statTotal">—</div> |
| <div class="stat-label">Total encontrado</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-val" id="statAcordaos">—</div> |
| <div class="stat-label">Acórdãos</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-val" id="statDecisoes">—</div> |
| <div class="stat-label">Decisões</div> |
| </div> |
| </div> |
| |
| <!-- DIVIDER --> |
| <div class="section-divider" id="secDivider"> |
| <div class="div-line"></div> |
| <span class="div-label" id="divLabel">Resultados</span> |
| <div class="div-line"></div> |
| </div> |
| |
| <!-- EMPTY --> |
| <div class="empty-box" id="emptyBox"> |
| <div class="empty-icon">⚖</div> |
| <p>Nenhum resultado para esta busca.</p> |
| </div> |
| |
| <!-- RESULTS --> |
| <div id="results"></div> |
| |
| <!-- PAGINATION --> |
| <div class="pag-wrap" id="pagWrap"></div> |
| |
| <!-- FOOTER --> |
| <footer class="footer"> |
| <span class="footer-brand">PARA<span>AI</span></span> |
| <span class="footer-note">CONVERGENT LAW TECHNOLOGIES</span> |
| </footer> |
| |
| </div><!-- /shell --> |
| |
| <script> |
| let curPage = 1, curQuery = '', totalRes = 0; |
| const PS = 10; |
| |
| document.getElementById('query').addEventListener('keydown', e => { |
| if (e.key === 'Enter') doSearch(1); |
| }); |
| |
| function fmtNum(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : String(n); } |
| |
| function fmtDate(d) { |
| if (!d) return null; |
| const m = String(d).match(/(\\d{4})-(\\d{2})-(\\d{2})/); |
| return m ? m[3]+'/'+m[2]+'/'+m[1] : String(d).substring(0,10); |
| } |
| |
| function trunc(t, max=420) { |
| if (!t) return ''; |
| return t.length > max ? t.substring(0, max) + '…' : t; |
| } |
| |
| function setVisible(id, val) { |
| const el = document.getElementById(id); |
| if (el) el.style.display = val; |
| } |
| |
| async function doSearch(page) { |
| const q = document.getElementById('query').value.trim(); |
| if (!q) { document.getElementById('query').focus(); return; } |
| |
| const ac = document.getElementById('cbAcordaos').checked; |
| const de = document.getElementById('cbDecisoes').checked; |
| if (!ac && !de) { showErr('Selecione ao menos uma base.'); return; } |
| |
| curQuery = q; curPage = page; |
| |
| document.getElementById('results').innerHTML = ''; |
| setVisible('loader', 'block'); |
| setVisible('errBox', 'none'); |
| setVisible('emptyBox', 'none'); |
| document.getElementById('statsRow').style.display = 'none'; |
| document.getElementById('secDivider').style.display = 'none'; |
| document.getElementById('pagWrap').style.display = 'none'; |
| document.getElementById('searchBtn').disabled = true; |
| |
| try { |
| const params = new URLSearchParams({q, acordaos: ac, decisoes: de, page, page_size: PS}); |
| const res = await fetch('/api/busca-multipla?' + params); |
| const data = await res.json(); |
| |
| setVisible('loader', 'none'); |
| document.getElementById('searchBtn').disabled = false; |
| |
| if (!res.ok || data.error) throw new Error(data.error || 'Erro ' + res.status); |
| if (!data.resultados || !data.resultados.length) { |
| setVisible('emptyBox', 'block'); return; |
| } |
| |
| totalRes = data.total || data.resultados.length; |
| const totalPages = Math.ceil(totalRes / PS); |
| |
| // Stats |
| document.getElementById('statTotal').textContent = fmtNum(totalRes); |
| document.getElementById('statAcordaos').textContent = fmtNum(data.total_acordaos || 0); |
| document.getElementById('statDecisoes').textContent = fmtNum(data.total_decisoes || 0); |
| document.getElementById('statsRow').style.display = 'grid'; |
| |
| // Divider |
| document.getElementById('divLabel').textContent = 'Página ' + page + ' de ' + totalPages; |
| document.getElementById('secDivider').style.display = 'flex'; |
| |
| // Cards |
| const container = document.getElementById('results'); |
| data.resultados.forEach((item, i) => { |
| const offset = (page-1)*PS + i + 1; |
| container.appendChild(buildCard(item, offset)); |
| }); |
| |
| // Pagination |
| buildPag(totalPages); |
| |
| if (page > 1) document.getElementById('results').scrollIntoView({behavior:'smooth'}); |
| |
| } catch(e) { |
| setVisible('loader', 'none'); |
| document.getElementById('searchBtn').disabled = false; |
| showErr(e.message); |
| } |
| } |
| |
| function showErr(msg) { |
| const el = document.getElementById('errBox'); |
| el.style.display = 'block'; |
| el.textContent = '✕ ' + msg; |
| } |
| |
| function buildCard(item, num) { |
| const tipo = item.base === 'acordaos' ? 'acordaos' : 'decisoes'; |
| const label = item.base === 'acordaos' ? 'ACÓRDÃO' : 'DECISÃO'; |
| const date = fmtDate(item.data); |
| const pub = fmtDate(item.publicacao); |
| |
| const div = document.createElement('div'); |
| div.className = 'result-card ' + tipo; |
| div.style.animationDelay = (num % 10 * 35) + 'ms'; |
| |
| let meta = ''; |
| if (item.processo) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚙</span>${item.processo}</span>`; |
| if (item.relator) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚖</span>${item.relator}</span>`; |
| if (item.orgao) meta += `<span class="meta-pill"><span class="meta-pill-icon">🏛</span>${item.orgao}</span>`; |
| if (date) meta += `<span class="meta-pill"><span class="meta-pill-icon">📅</span>${date}</span>`; |
| if (pub) meta += `<span class="meta-pill"><span class="meta-pill-icon">📢</span>DJe ${pub}</span>`; |
| |
| let fields = ''; |
| if (item.ementa) |
| fields += `<div class="field-block ementa"><span class="field-label">Ementa</span><div class="field-text">${trunc(item.ementa)}</div></div>`; |
| if (item.decisao_texto) |
| fields += `<div class="field-block decisao"><span class="field-label">Decisão</span><div class="field-text">${trunc(item.decisao_texto,360)}</div></div>`; |
| if (item.acordao_ata) |
| 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>`; |
| |
| let foot = `<a class="btn-ghost" href="/documento/${encodeURIComponent(item.id)}"> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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"/> |
| </svg> |
| Ver documento |
| </a>`; |
| if (item.url_documento) |
| foot += `<a class="btn-gold" href="${item.url_documento}" target="_blank" rel="noopener"> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> |
| <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/> |
| </svg> |
| Inteiro teor |
| </a>`; |
| |
| div.innerHTML = ` |
| <div class="card-head"> |
| <div class="card-title">${item.titulo || item.processo || 'Documento STF'}</div> |
| <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0"> |
| <span class="tipo-tag ${tipo}"><span class="tipo-tag-dot"></span>${label}</span> |
| <span class="card-num">#${num}</span> |
| </div> |
| </div> |
| <div class="meta-row">${meta}</div> |
| ${fields} |
| <div class="card-foot">${foot}</div>`; |
| |
| return div; |
| } |
| |
| function buildPag(total) { |
| const pg = document.getElementById('pagWrap'); |
| pg.innerHTML = ''; |
| if (total <= 1) return; |
| pg.style.display = 'flex'; |
| |
| const btn = (lbl, page, active, disabled) => { |
| const b = document.createElement('button'); |
| b.className = 'pag-btn' + (active ? ' active' : ''); |
| b.textContent = lbl; b.disabled = disabled; |
| if (!disabled && !active) b.onclick = () => doSearch(page); |
| pg.appendChild(b); |
| }; |
| |
| btn('‹', curPage-1, false, curPage===1); |
| const s = Math.max(1, curPage-2), e = Math.min(total, curPage+2); |
| for (let p=s; p<=e; p++) btn(p, p, p===curPage, false); |
| btn('›', curPage+1, false, curPage===total); |
| |
| const info = document.createElement('span'); |
| info.className = 'pag-info'; |
| info.textContent = curPage + ' / ' + total; |
| pg.insertBefore(info, pg.children[Math.floor(pg.children.length/2)]); |
| } |
| </script> |
| </body> |
| </html> |
| """) |
|
|
|
|
| |
| |
| |
| @app.route('/documento/<doc_id>') |
| def documento_detalhe(doc_id): |
| return render_template_string("""<!DOCTYPE html> |
| <html lang="pt-BR"> |
| <head> |
| <title>PARA AI — Documento {{ doc_id }}</title> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <style>""" + SHARED_CSS + """</style> |
| </head> |
| <body> |
| |
| <!-- TOPBAR --> |
| <nav class="topbar"> |
| <a href="/" class="brand"> |
| <span class="brand-label">STF - TJPR</span> |
| </a> |
| <div class="topbar-right"> |
| <a href="/" class="back-link"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M19 12H5M12 5l-7 7 7 7"/> |
| </svg> |
| Voltar à busca |
| </a> |
| </div> |
| </nav> |
| |
| <div class="shell" style="padding-top:0"> |
| |
| <div id="loader" style="display:block"> |
| <div style="padding:80px 0; text-align:center"> |
| <div class="spinner"></div> |
| <div class="loader-txt">carregando documento…</div> |
| </div> |
| </div> |
| |
| <div class="err-box" id="errBox"></div> |
| <div id="docWrap" style="display:none"></div> |
| |
| <footer class="footer"> |
| <span class="footer-brand">PARA<span>AI</span></span> |
| <span class="footer-note">CONVERGENT LAW TECHNOLOGIES</span> |
| </footer> |
| </div> |
| |
| <script> |
| const DOC_ID = "{{ doc_id }}"; |
| |
| function fmtDate(d) { |
| if (!d) return null; |
| const m = String(d).match(/(\\d{4})-(\\d{2})-(\\d{2})/); |
| return m ? m[3]+'/'+m[2]+'/'+m[1] : String(d).substring(0,10); |
| } |
| |
| async function loadDoc() { |
| try { |
| const res = await fetch('/api/documento/' + DOC_ID); |
| const data = await res.json(); |
| document.getElementById('loader').style.display = 'none'; |
| |
| if (data.error) throw new Error(data.error); |
| |
| const tipo = data.base === 'acordaos' ? 'acordaos' : 'decisoes'; |
| const label = data.base === 'acordaos' ? 'Acórdão' : 'Decisão Monocrática'; |
| const date = fmtDate(data.data); |
| const pub = fmtDate(data.publicacao); |
| |
| let meta = `<span class="meta-pill">${label}</span>`; |
| if (data.relator) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚖</span>${data.relator}</span>`; |
| if (data.orgao) meta += `<span class="meta-pill"><span class="meta-pill-icon">🏛</span>${data.orgao}</span>`; |
| if (date) meta += `<span class="meta-pill"><span class="meta-pill-icon">📅</span>${date}</span>`; |
| if (pub) meta += `<span class="meta-pill"><span class="meta-pill-icon">📢</span>DJe ${pub}</span>`; |
| if (data.processo) meta += `<span class="meta-pill"><span class="meta-pill-icon">⚙</span>${data.processo}</span>`; |
| |
| let fields = ''; |
| if (data.ementa) |
| 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>`; |
| if (data.decisao_texto) |
| 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>`; |
| if (data.acordao_ata) |
| 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>`; |
| |
| let teor = ''; |
| if (data.url_documento) |
| teor = `<a class="inteiro-teor-link" href="${data.url_documento}" target="_blank" rel="noopener"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> |
| <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/> |
| </svg> |
| Ver inteiro teor (PDF/HTML) |
| </a>`; |
| |
| document.getElementById('docWrap').style.display = 'block'; |
| document.getElementById('docWrap').innerHTML = ` |
| <div class="doc-header-card"> |
| <div class="doc-title">${data.titulo || data.processo || 'Documento STF'}</div> |
| <div class="meta-row">${meta}</div> |
| </div> |
| ${fields} |
| ${teor}`; |
| |
| } catch(e) { |
| document.getElementById('loader').style.display = 'none'; |
| const el = document.getElementById('errBox'); |
| el.style.display = 'block'; |
| el.textContent = '✕ ' + e.message; |
| } |
| } |
| |
| loadDoc(); |
| </script> |
| </body> |
| </html> |
| """, doc_id=doc_id) |
|
|
|
|
| |
| |
| |
| @app.route('/api/documento/<doc_id>') |
| def api_documento(doc_id): |
| token = get_fresh_token() |
| if not token: |
| return jsonify({"error": "Não foi possível obter token"}), 503 |
|
|
| payload = { |
| "query": {"ids": {"values": [doc_id]}}, |
| "_source": ["id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata", |
| "processo_codigo_completo", "relator_processo_nome", |
| "orgao_julgador", "julgamento_data", "publicacao_data", "inteiro_teor_url", "base"], |
| "size": 1 |
| } |
|
|
| headers = HEADERS.copy() |
| headers['Cookie'] = f'aws-waf-token={token}' |
|
|
| try: |
| response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) |
| if response.status_code == 200: |
| data = response.json() |
| hits = data.get('result', {}).get('hits', {}).get('hits', []) |
| if hits: |
| src = hits[0].get('_source', {}) |
| item = { |
| "id": src.get('id') or hits[0].get('_id'), |
| "titulo": src.get('titulo'), |
| "processo": src.get('processo_codigo_completo'), |
| "relator": src.get('relator_processo_nome'), |
| "orgao": src.get('orgao_julgador'), |
| "data": src.get('julgamento_data'), |
| "publicacao": src.get('publicacao_data'), |
| "ementa": src.get('ementa_texto'), |
| "decisao_texto": src.get('decisao_texto'), |
| "acordao_ata": src.get('acordao_ata'), |
| "url_documento": src.get('inteiro_teor_url'), |
| "base": src.get('base') |
| } |
| return jsonify({k: v for k, v in item.items() if v is not None}) |
| return jsonify({"error": "Documento não encontrado"}), 404 |
| elif response.status_code == 202: |
| token = get_fresh_token(force_refresh=True) |
| if token: |
| headers['Cookie'] = f'aws-waf-token={token}' |
| response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) |
| if response.status_code == 200: |
| data = response.json() |
| hits = data.get('result', {}).get('hits', {}).get('hits', []) |
| if hits: |
| src = hits[0].get('_source', {}) |
| item = { |
| "id": src.get('id') or hits[0].get('_id'), |
| "titulo": src.get('titulo'), |
| "processo": src.get('processo_codigo_completo'), |
| "relator": src.get('relator_processo_nome'), |
| "orgao": src.get('orgao_julgador'), |
| "data": src.get('julgamento_data'), |
| "publicacao": src.get('publicacao_data'), |
| "ementa": src.get('ementa_texto'), |
| "decisao_texto": src.get('decisao_texto'), |
| "acordao_ata": src.get('acordao_ata'), |
| "url_documento": src.get('inteiro_teor_url'), |
| "base": src.get('base') |
| } |
| return jsonify({k: v for k, v in item.items() if v is not None}) |
| return jsonify({"error": "Token expirado"}), 503 |
| else: |
| return jsonify({"error": f"Erro {response.status_code}"}), response.status_code |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
|
|
| |
| |
| |
| @app.route('/api/busca-multipla') |
| def busca_multipla(): |
| query = request.args.get('q', '') |
| busca_acordaos = request.args.get('acordaos', 'true').lower() == 'true' |
| busca_decisoes = request.args.get('decisoes', 'true').lower() == 'true' |
| page = int(request.args.get('page', '1')) |
| page_size = int(request.args.get('page_size', '20')) |
| |
| if not query: |
| return jsonify({"error": "Parâmetro 'q' obrigatório"}), 400 |
| if not busca_acordaos and not busca_decisoes: |
| return jsonify({"error": "Selecione pelo menos uma base para pesquisa"}), 400 |
|
|
| bases = [] |
| if busca_acordaos: bases.append("acordaos") |
| if busca_decisoes: bases.append("decisoes") |
|
|
| result = search_stf(query, bases, page, page_size) |
| if isinstance(result, tuple): |
| return jsonify(result[0]), result[1] |
| |
| hits = result.get('result', {}).get('hits', {}).get('hits', []) |
| resultados = [] |
| total_acordaos = 0 |
| total_decisoes = 0 |
| |
| for hit in hits: |
| src = hit.get('_source', {}) |
| base = src.get('base', '') |
| if base == 'acordaos': total_acordaos += 1 |
| elif base == 'decisoes': total_decisoes += 1 |
| item = { |
| "id": src.get('id') or hit.get('_id'), |
| "titulo": src.get('titulo'), |
| "processo": src.get('processo_codigo_completo'), |
| "relator": src.get('relator_processo_nome'), |
| "orgao": src.get('orgao_julgador'), |
| "data": src.get('julgamento_data'), |
| "publicacao": src.get('publicacao_data'), |
| "ementa": src.get('ementa_texto'), |
| "decisao_texto": src.get('decisao_texto'), |
| "acordao_ata": src.get('acordao_ata'), |
| "url_documento": src.get('inteiro_teor_url'), |
| "base": base, |
| "score": hit.get('_score') |
| } |
| item = {k: v for k, v in item.items() if v is not None} |
| resultados.append(item) |
|
|
| return jsonify({ |
| "q": query, "bases": bases, "page": page, "page_size": page_size, |
| "total": result.get('result', {}).get('hits', {}).get('total', {}).get('value', len(resultados)), |
| "total_acordaos": total_acordaos, |
| "total_decisoes": total_decisoes, |
| "resultados": resultados |
| }) |
|
|
|
|
| |
| |
| |
| @app.route('/api/health', methods=['GET']) |
| def health(): |
| playwright_status = False |
| try: |
| with sync_playwright() as p: |
| p.chromium.launch(headless=True).close() |
| playwright_status = True |
| except: |
| pass |
| return jsonify({ |
| "status": "healthy", |
| "playwright_ready": playwright_status, |
| "token_cached": bool(token_cache["token"]) |
| }) |
|
|
|
|
| if __name__ == '__main__': |
| try: |
| import certifi |
| os.environ['SSL_CERT_FILE'] = certifi.where() |
| os.environ['REQUESTS_CA_BUNDLE'] = certifi.where() |
| except: |
| pass |
| logger.info("="*50) |
| logger.info("🚀 Iniciando PARA AI - Busca STF") |
| logger.info("📋 Bases: acordaos e decisoes") |
| logger.info("="*50) |
| port = int(os.environ.get('PORT', 7860)) |
| app.run(host='0.0.0.0', port=port, debug=False) |
|
|