@danhachuel/pandanews 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/backend/__pycache__/__init__.cpython-314.pyc +0 -0
- package/backend/app/main.py +3 -2
- package/backend/app/routers/noticias.py +42 -1
- package/backend/app/routers/wordpress.py +23 -0
- package/backend/app/schemas.py +3 -0
- package/backend/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/backend/core/__pycache__/config.cpython-314.pyc +0 -0
- package/backend/core/__pycache__/llm_router.cpython-314.pyc +0 -0
- package/backend/core/database.py +2 -0
- package/backend/core/pipeline.py +6 -2
- package/backend/core/wordpress_publisher.py +167 -0
- package/backend/tests/test_llm_fallback.py +64 -1
- package/frontend/dist/assets/{index-ExoYg5_I.css → index-CM6InlUs.css} +1 -1
- package/frontend/dist/assets/index-Dj6FuR5_.js +70 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/start.js +110 -10
- package/frontend/dist/assets/index-Iz5spM-E.js +0 -70
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
|
|
4
4
|
|
|
5
|
+
## 0.5.00 (2026-11-08)
|
|
6
|
+
|
|
7
|
+
- **Módulo Publicador WordPress** (preparação para monetização AdSense): credenciais `wordpress_url` / `wordpress_user` / `wordpress_app_password` salvas no banco local e mascaradas no frontend; coluna `noticias.wordpress_post_id` (com migração leve para bancos existentes)
|
|
8
|
+
- `backend/core/wordpress_publisher.py`: teste de conexão real (`GET /wp-json/wp/v2/users/me`), upload de imagem em 2 etapas (`POST /wp-json/wp/v2/media` → `featured_media`) e criação do post (`POST /wp-json/wp/v2/posts`, `status: publish`, categoria resolvida pelo tema do blog — busca ou cria)
|
|
9
|
+
- Endpoint `POST /api/v1/noticias/{id}/publicar`: bloqueia republicação (400 "Já publicado"), exige **mínimo de 300 palavras** (diretriz AdSense), grava `status=publicado` + `wordpress_post_id` e devolve o link do post; `GET /api/v1/wordpress/status` + `POST /api/v1/wordpress/testar`
|
|
10
|
+
- Frontend: seção **Integração WordPress** em Configurações (URL, usuário, senha de aplicativo mascarada, Testar Conexão com estado) e botão **🌐 Publicar no WordPress** nos cards de notícias — loading durante o envio, badge "Publicado" + link "Ver no site" após o sucesso
|
|
11
|
+
- **Diretriz rígida AdSense no prompt do LLM**: "O texto final deve ter no mínimo 1500 palavras. Expanda o contexto, traga implicações e detalhes para criar um artigo profundo e otimizado para SEO. Nunca gere textos com menos de 1000 palavras." — teto de tokens da redação elevado para 4000
|
|
12
|
+
|
|
13
|
+
## 0.4.10 (2026-11-08)
|
|
14
|
+
|
|
15
|
+
- **Cobertura E2E pelo caminho HTTP real**: o teste de fallback ganhou um cenário que substitui apenas o `httpx.Client` — `chamar_provedor`, a construção da URL, os headers e o `raise_for_status` rodam de verdade. A NVIDIA devolve 410, o Gemini devolve 200 e a notícia nasce do Gemini com o log `Erro na NVIDIA (...). Acionando Google Gemini...` emitido. Fecha a lacuna de testar só a função mockada
|
|
16
|
+
- Teste sobe para 16 verificações, todas verdes local e no CI
|
|
17
|
+
|
|
5
18
|
## 0.4.09 (2026-11-08)
|
|
6
19
|
|
|
7
20
|
- **Frontend — Gemini tratado como a NVIDIA no teste de conexão**: o badge agora distingue `degraded` (âmbar, modelo em fim de vida 404/410) de `falhou` (vermelho) e a mensagem de detalhe usa a cor correspondente. Antes um 404/410 do Gemini aparecia como "falhou" vermelho — agora o usuário entende que é o modelo, não a chave, e que a geração segue pela NVIDIA
|
|
Binary file
|
package/backend/app/main.py
CHANGED
|
@@ -10,7 +10,7 @@ from contextlib import asynccontextmanager
|
|
|
10
10
|
from fastapi import FastAPI
|
|
11
11
|
from fastapi.middleware.cors import CORSMiddleware
|
|
12
12
|
|
|
13
|
-
from backend.app.routers import automacao, blogs, configuracoes, dashboard, imagens, legal, llm, logs, noticias
|
|
13
|
+
from backend.app.routers import automacao, blogs, configuracoes, dashboard, imagens, legal, llm, logs, noticias, wordpress
|
|
14
14
|
from backend.core.database import SessionLocal, init_db
|
|
15
15
|
from backend.core.llm_router import get_config
|
|
16
16
|
|
|
@@ -31,7 +31,7 @@ async def lifespan(_: FastAPI):
|
|
|
31
31
|
scheduler.parar()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
app = FastAPI(title="PandaNews API", version="0.
|
|
34
|
+
app = FastAPI(title="PandaNews API", version="0.5.0", lifespan=lifespan)
|
|
35
35
|
|
|
36
36
|
app.add_middleware(
|
|
37
37
|
CORSMiddleware,
|
|
@@ -48,6 +48,7 @@ app.include_router(noticias.router, prefix="/api/v1")
|
|
|
48
48
|
app.include_router(imagens.router, prefix="/api/v1")
|
|
49
49
|
app.include_router(automacao.router, prefix="/api/v1")
|
|
50
50
|
app.include_router(logs.router, prefix="/api/v1")
|
|
51
|
+
app.include_router(wordpress.router, prefix="/api/v1")
|
|
51
52
|
app.include_router(legal.router) # /legal/* na raiz (URLs de verificação)
|
|
52
53
|
|
|
53
54
|
|
|
@@ -7,7 +7,7 @@ from sqlalchemy import select
|
|
|
7
7
|
from sqlalchemy.orm import Session
|
|
8
8
|
|
|
9
9
|
from backend.app.schemas import NoticiaPatch, STATUS_NOTICIA
|
|
10
|
-
from backend.core.database import Noticia, get_db
|
|
10
|
+
from backend.core.database import Log, Noticia, get_db
|
|
11
11
|
|
|
12
12
|
router = APIRouter(prefix="/noticias", tags=["noticias"])
|
|
13
13
|
|
|
@@ -26,6 +26,7 @@ def _serializar(n: Noticia) -> dict:
|
|
|
26
26
|
"imagem_url": n.imagem_url,
|
|
27
27
|
"status": n.status,
|
|
28
28
|
"llm_provider": n.llm_provider,
|
|
29
|
+
"wordpress_post_id": n.wordpress_post_id,
|
|
29
30
|
"fonte_url": n.fonte_url,
|
|
30
31
|
"fonte_titulo": n.fonte_titulo,
|
|
31
32
|
"created_at": n.created_at,
|
|
@@ -45,6 +46,46 @@ def gerar_noticia(payload: GerarIn) -> dict:
|
|
|
45
46
|
return _serializar(noticia)
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
@router.post("/{noticia_id}/publicar")
|
|
50
|
+
def publicar_noticia(noticia_id: int, db: Session = Depends(get_db)) -> dict:
|
|
51
|
+
"""Publica a notícia no WordPress configurado (imagem + categoria)."""
|
|
52
|
+
from backend.core import wordpress_publisher
|
|
53
|
+
|
|
54
|
+
noticia = db.get(Noticia, noticia_id)
|
|
55
|
+
if noticia is None:
|
|
56
|
+
raise HTTPException(status_code=404, detail="Notícia não encontrada")
|
|
57
|
+
if noticia.wordpress_post_id:
|
|
58
|
+
raise HTTPException(status_code=400, detail="Já publicado no WordPress.")
|
|
59
|
+
palavras = wordpress_publisher.contar_palavras(noticia.corpo)
|
|
60
|
+
if palavras < wordpress_publisher.MINIMO_PALAVRAS_PUBLICACAO:
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=400,
|
|
63
|
+
detail=(
|
|
64
|
+
f"Texto com {palavras} palavras — mínimo de "
|
|
65
|
+
f"{wordpress_publisher.MINIMO_PALAVRAS_PUBLICACAO} para publicar (diretriz AdSense). "
|
|
66
|
+
"Regere a notícia ou edite o corpo antes de publicar."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
resultado = wordpress_publisher.publish_to_wordpress(db, noticia)
|
|
71
|
+
except Exception as exc: # noqa: BLE001
|
|
72
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
73
|
+
|
|
74
|
+
noticia.status = "publicado"
|
|
75
|
+
noticia.wordpress_post_id = resultado["post_id"]
|
|
76
|
+
db.add(
|
|
77
|
+
Log(
|
|
78
|
+
blog_id=noticia.blog_id,
|
|
79
|
+
etapa="publicacao",
|
|
80
|
+
status="sucesso",
|
|
81
|
+
mensagem=f"Publicado no WordPress (post {resultado['post_id']}): {resultado['link']}",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
db.commit()
|
|
85
|
+
db.refresh(noticia)
|
|
86
|
+
return {"ok": True, "post_id": resultado["post_id"], "link": resultado["link"], "noticia": _serializar(noticia)}
|
|
87
|
+
|
|
88
|
+
|
|
48
89
|
@router.post("/{noticia_id}/regenerar")
|
|
49
90
|
def regenerar_noticia(noticia_id: int) -> dict:
|
|
50
91
|
from backend.core import pipeline
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""WordPress (/api/v1/wordpress): teste de conexão e estado das credenciais."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Depends
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from backend.core import wordpress_publisher
|
|
8
|
+
from backend.core.database import get_db
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/wordpress", tags=["wordpress"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/status")
|
|
14
|
+
def status(db: Session = Depends(get_db)) -> dict:
|
|
15
|
+
return wordpress_publisher.credenciais_presentes(db)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/testar")
|
|
19
|
+
def testar(db: Session = Depends(get_db)) -> dict:
|
|
20
|
+
estado = wordpress_publisher.credenciais_presentes(db)
|
|
21
|
+
if not estado["configurado"]:
|
|
22
|
+
return {"ok": False, "detail": "Credenciais não configuradas — informe URL, usuário e Senha de Aplicativo abaixo."}
|
|
23
|
+
return {**wordpress_publisher.testar_conexao(db), **estado}
|
package/backend/app/schemas.py
CHANGED
|
@@ -52,6 +52,9 @@ class ConfiguracoesPatch(BaseModel):
|
|
|
52
52
|
scheduler_ativo: Optional[bool] = None
|
|
53
53
|
intervalo_padrao_horas: Optional[int] = Field(default=None, ge=1, le=168)
|
|
54
54
|
meta_diaria: Optional[int] = Field(default=None, ge=0, le=100)
|
|
55
|
+
wordpress_url: Optional[str] = None
|
|
56
|
+
wordpress_user: Optional[str] = None
|
|
57
|
+
wordpress_app_password: Optional[str] = None
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
class LlmTestIn(BaseModel):
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/backend/core/database.py
CHANGED
|
@@ -55,6 +55,7 @@ class Noticia(Base):
|
|
|
55
55
|
imagem_url: Mapped[str] = mapped_column(String(500), default="")
|
|
56
56
|
status: Mapped[str] = mapped_column(String(20), default="rascunho", index=True)
|
|
57
57
|
llm_provider: Mapped[str] = mapped_column(String(50), default="")
|
|
58
|
+
wordpress_post_id: Mapped[int | None] = mapped_column(nullable=True)
|
|
58
59
|
fonte_url: Mapped[str] = mapped_column(String(600), default="")
|
|
59
60
|
fonte_titulo: Mapped[str] = mapped_column(Text, default="")
|
|
60
61
|
created_at: Mapped[datetime] = mapped_column(default=_now, index=True)
|
|
@@ -105,6 +106,7 @@ def _migrate() -> None:
|
|
|
105
106
|
for coluna, ddl in (
|
|
106
107
|
("fonte_url", "VARCHAR(600) DEFAULT ''"),
|
|
107
108
|
("fonte_titulo", "TEXT DEFAULT ''"),
|
|
109
|
+
("wordpress_post_id", "INTEGER"),
|
|
108
110
|
):
|
|
109
111
|
if coluna not in colunas:
|
|
110
112
|
conn.execute(text(f"ALTER TABLE noticias ADD COLUMN {coluna} {ddl}"))
|
package/backend/core/pipeline.py
CHANGED
|
@@ -93,6 +93,9 @@ def _prompt_redacao(blog: Blog, item: dict) -> str:
|
|
|
93
93
|
f"Resumo da fonte: {item['resumo']}\n"
|
|
94
94
|
"Regras: reescreva com palavras próprias; não invente fatos que não estejam "
|
|
95
95
|
"na fonte; 3 a 5 parágrafos.\n"
|
|
96
|
+
"DIRETRIZ RÍGIDA (AdSense): o texto final deve ter no mínimo 1500 palavras. "
|
|
97
|
+
"Expanda o contexto, traga implicações e detalhes para criar um artigo profundo "
|
|
98
|
+
"e otimizado para SEO. Nunca gere textos com menos de 1000 palavras.\n"
|
|
96
99
|
'Responda SOMENTE com JSON válido no formato: {"titulo": "...", "resumo": "...", "corpo": "..."}'
|
|
97
100
|
)
|
|
98
101
|
|
|
@@ -140,9 +143,10 @@ def gerar_noticia(blog_id: int) -> Noticia | None:
|
|
|
140
143
|
novos.sort(key=lambda i: score_relevancia(i, blog), reverse=True)
|
|
141
144
|
item = novos[0]
|
|
142
145
|
|
|
143
|
-
# 3. Redação via LLM (fallback NVIDIA -> Gemini)
|
|
146
|
+
# 3. Redação via LLM (fallback NVIDIA -> Gemini) — teto alto p/ diretriz
|
|
147
|
+
# AdSense (mínimo 1500 palavras)
|
|
144
148
|
try:
|
|
145
|
-
resposta, provedor = llm_router.gerar_texto(db, _prompt_redacao(blog, item), max_tokens=
|
|
149
|
+
resposta, provedor = llm_router.gerar_texto(db, _prompt_redacao(blog, item), max_tokens=4000)
|
|
146
150
|
except Exception as exc: # noqa: BLE001
|
|
147
151
|
_log(db, blog.id, "redacao", "erro", f"Falha na redação: {exc}")
|
|
148
152
|
return None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Publicador WordPress (Fase 0.5.00).
|
|
2
|
+
|
|
3
|
+
Envia notícias aprovadas para um blog WordPress via REST API usando Basic Auth
|
|
4
|
+
(usuário + Senha de Aplicativo gerada no painel do WP). Fluxo em 2 etapas:
|
|
5
|
+
upload da imagem para /wp-json/wp/v2/media → criação do post em
|
|
6
|
+
/wp-json/wp/v2/posts com featured_media e categoria.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from backend.core.config import IMAGES_DIR
|
|
16
|
+
from backend.core.database import Blog
|
|
17
|
+
from backend.core.llm_router import get_config
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("uvicorn.error.pandanews")
|
|
20
|
+
|
|
21
|
+
MINIMO_PALAVRAS_PUBLICACAO = 300
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _credenciais(db) -> dict:
|
|
25
|
+
url = get_config(db, "wordpress_url").rstrip("/")
|
|
26
|
+
user = get_config(db, "wordpress_user")
|
|
27
|
+
app_password = get_config(db, "wordpress_app_password")
|
|
28
|
+
if not (url and user and app_password):
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
"WordPress não configurado — informe URL, usuário e Senha de Aplicativo em Configurações → Integração WordPress."
|
|
31
|
+
)
|
|
32
|
+
return {"url": url, "user": user, "password": app_password}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def credenciais_presentes(db) -> dict:
|
|
36
|
+
"""Estado das credenciais para a UI (sem expor segredos)."""
|
|
37
|
+
url = get_config(db, "wordpress_url").rstrip("/")
|
|
38
|
+
user = get_config(db, "wordpress_user")
|
|
39
|
+
password = get_config(db, "wordpress_app_password")
|
|
40
|
+
return {
|
|
41
|
+
"configurado": bool(url and user and password),
|
|
42
|
+
"url": url,
|
|
43
|
+
"user": user,
|
|
44
|
+
"password_configured": bool(password),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def testar_conexao(db) -> dict:
|
|
49
|
+
"""GET /wp-json/wp/v2/users/me — valida URL + credenciais."""
|
|
50
|
+
cred = _credenciais(db)
|
|
51
|
+
try:
|
|
52
|
+
resp = httpx.get(
|
|
53
|
+
f"{cred['url']}/wp-json/wp/v2/users/me",
|
|
54
|
+
auth=(cred["user"], cred["password"]),
|
|
55
|
+
timeout=20.0,
|
|
56
|
+
)
|
|
57
|
+
except httpx.HTTPError as exc:
|
|
58
|
+
return {"ok": False, "detail": f"Não foi possível conectar: {exc}"}
|
|
59
|
+
if resp.status_code == 200:
|
|
60
|
+
nome = resp.json().get("name", cred["user"])
|
|
61
|
+
return {"ok": True, "detail": f"Autenticado como {nome}.", "user": nome}
|
|
62
|
+
return {"ok": False, "detail": f"WP respondeu {resp.status_code}: {resp.text[:200]}"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _imagem_local(imagem_url: str) -> Path | None:
|
|
66
|
+
nome = (imagem_url or "").rsplit("/", 1)[-1]
|
|
67
|
+
if not nome:
|
|
68
|
+
return None
|
|
69
|
+
caminho = IMAGES_DIR / nome
|
|
70
|
+
return caminho if caminho.exists() else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _upload_midia(cred: dict, caminho: Path) -> int | None:
|
|
74
|
+
"""Envia a imagem para a biblioteca de mídia; devolve o media ID."""
|
|
75
|
+
with open(caminho, "rb") as arquivo:
|
|
76
|
+
dados = arquivo.read()
|
|
77
|
+
resp = httpx.post(
|
|
78
|
+
f"{cred['url']}/wp-json/wp/v2/media",
|
|
79
|
+
content=dados,
|
|
80
|
+
headers={
|
|
81
|
+
"Content-Disposition": f'attachment; filename="{caminho.name}"',
|
|
82
|
+
"Content-Type": "image/png",
|
|
83
|
+
},
|
|
84
|
+
auth=(cred["user"], cred["password"]),
|
|
85
|
+
timeout=120.0,
|
|
86
|
+
)
|
|
87
|
+
if resp.status_code in (200, 201):
|
|
88
|
+
media_id = resp.json().get("id")
|
|
89
|
+
if media_id:
|
|
90
|
+
return int(media_id)
|
|
91
|
+
logger.warning("WordPress: upload de mídia falhou (%s): %s", resp.status_code, resp.text[:200])
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolver_categoria(cred: dict, nome_tema: str) -> int | None:
|
|
96
|
+
"""Categoria WP pelo nome do tema do blog — busca ou cria."""
|
|
97
|
+
if not nome_tema:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
resp = httpx.get(
|
|
101
|
+
f"{cred['url']}/wp-json/wp/v2/categories",
|
|
102
|
+
params={"search": nome_tema, "per_page": 20},
|
|
103
|
+
auth=(cred["user"], cred["password"]),
|
|
104
|
+
timeout=20.0,
|
|
105
|
+
)
|
|
106
|
+
if resp.status_code == 200:
|
|
107
|
+
for categoria in resp.json():
|
|
108
|
+
if str(categoria.get("name", "")).lower() == nome_tema.lower():
|
|
109
|
+
return int(categoria["id"])
|
|
110
|
+
resp = httpx.post(
|
|
111
|
+
f"{cred['url']}/wp-json/wp/v2/categories",
|
|
112
|
+
json={"name": nome_tema.title()},
|
|
113
|
+
auth=(cred["user"], cred["password"]),
|
|
114
|
+
timeout=20.0,
|
|
115
|
+
)
|
|
116
|
+
if resp.status_code in (200, 201) and resp.json().get("id"):
|
|
117
|
+
return int(resp.json()["id"])
|
|
118
|
+
except httpx.HTTPError as exc:
|
|
119
|
+
logger.warning("WordPress: falha ao resolver categoria (%s)", exc)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def contar_palavras(texto: str) -> int:
|
|
124
|
+
return len((texto or "").split())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def publish_to_wordpress(db, noticia) -> dict:
|
|
128
|
+
"""Publica a notícia completa. Retorna {"post_id": int, "link": str}."""
|
|
129
|
+
cred = _credenciais(db)
|
|
130
|
+
palavras = contar_palavras(noticia.corpo)
|
|
131
|
+
if palavras < MINIMO_PALAVRAS_PUBLICACAO:
|
|
132
|
+
raise RuntimeError(
|
|
133
|
+
f"Texto com {palavras} palavras — mínimo de {MINIMO_PALAVRAS_PUBLICACAO} para publicar (diretriz AdSense)."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
blog = db.get(Blog, noticia.blog_id)
|
|
137
|
+
categoria_id = resolver_categoria(cred, blog.tema if blog else "")
|
|
138
|
+
|
|
139
|
+
featured_media = None
|
|
140
|
+
if noticia.imagem_url:
|
|
141
|
+
caminho = _imagem_local(noticia.imagem_url)
|
|
142
|
+
if caminho:
|
|
143
|
+
featured_media = _upload_midia(cred, caminho)
|
|
144
|
+
|
|
145
|
+
payload: dict = {
|
|
146
|
+
"title": noticia.titulo,
|
|
147
|
+
"content": noticia.corpo,
|
|
148
|
+
"status": "publish",
|
|
149
|
+
}
|
|
150
|
+
if categoria_id:
|
|
151
|
+
payload["categories"] = [categoria_id]
|
|
152
|
+
if featured_media:
|
|
153
|
+
payload["featured_media"] = featured_media
|
|
154
|
+
|
|
155
|
+
resp = httpx.post(
|
|
156
|
+
f"{cred['url']}/wp-json/wp/v2/posts",
|
|
157
|
+
json=payload,
|
|
158
|
+
auth=(cred["user"], cred["password"]),
|
|
159
|
+
timeout=60.0,
|
|
160
|
+
)
|
|
161
|
+
if resp.status_code not in (200, 201):
|
|
162
|
+
raise RuntimeError(f"WP respondeu {resp.status_code}: {resp.text[:300]}")
|
|
163
|
+
dados = resp.json()
|
|
164
|
+
post_id = int(dados["id"])
|
|
165
|
+
link = dados.get("link") or f"{cred['url']}/?p={post_id}"
|
|
166
|
+
logger.info("WordPress: post %s publicado (%s)", post_id, link)
|
|
167
|
+
return {"post_id": post_id, "link": link}
|
|
@@ -13,8 +13,10 @@ Cobre exatamente o que a correção promete:
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import json
|
|
16
17
|
import logging
|
|
17
18
|
import sys
|
|
19
|
+
import types
|
|
18
20
|
from datetime import timedelta as _timedelta
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
|
|
@@ -165,7 +167,7 @@ resultado_410 = _testar_com_resposta(
|
|
|
165
167
|
check(resultado_410["ok"] is False, "test_provider: HTTP 410 -> ok=False")
|
|
166
168
|
check(resultado_410.get("degraded") is True, "test_provider: HTTP 410 -> degraded=True (fallback avisa)")
|
|
167
169
|
check(
|
|
168
|
-
"fim
|
|
170
|
+
"fim de vida" in resultado_410["detail"] or "410" in resultado_410["detail"],
|
|
169
171
|
"test_provider: a string de erro do provedor volta no JSON para a tela",
|
|
170
172
|
)
|
|
171
173
|
|
|
@@ -181,6 +183,67 @@ check(
|
|
|
181
183
|
"test_provider: HTTP 500 -> código e corpo devolvidos ao frontend",
|
|
182
184
|
)
|
|
183
185
|
|
|
186
|
+
# ------------------------------------ 3b) E2E pelo caminho HTTP real (Chamar)
|
|
187
|
+
|
|
188
|
+
# Diferente dos cenários acima (que trocam `chamar_provedor` por um dublê),
|
|
189
|
+
# aqui só o `httpx.Client` é substituído: `chamar_provedor`, a construção da
|
|
190
|
+
# URL, os headers e o `raise_for_status` rodam de verdade. A NVIDIA devolve
|
|
191
|
+
# 410 e o Gemini devolve 200 — a notícia tem de nascer do Gemini.
|
|
192
|
+
llm_router.chamar_provedor = original_chamar
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class _RespostaHttp:
|
|
196
|
+
def __init__(self, status_code: int, corpo: dict) -> None:
|
|
197
|
+
self.status_code = status_code
|
|
198
|
+
self._corpo = corpo
|
|
199
|
+
self.text = json.dumps(corpo)
|
|
200
|
+
|
|
201
|
+
def raise_for_status(self) -> None:
|
|
202
|
+
if self.status_code >= 400:
|
|
203
|
+
raise RuntimeError(f"HTTP {self.status_code}: {self.text}")
|
|
204
|
+
|
|
205
|
+
def json(self) -> dict:
|
|
206
|
+
return self._corpo
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _post_e2e(self, endpoint, headers=None, json=None):
|
|
210
|
+
# distingue pelo modelo pedido: nvidia (410) vs google/gemini (200)
|
|
211
|
+
modelo = (json or {}).get("model", "")
|
|
212
|
+
if "gemini" in modelo:
|
|
213
|
+
return _RespostaHttp(
|
|
214
|
+
200,
|
|
215
|
+
{"choices": [{"message": {"content": '{"titulo":"T","resumo":"R","corpo":"C"}'}}], "model": modelo},
|
|
216
|
+
)
|
|
217
|
+
return _RespostaHttp(410, {"detail": "The model has reached its end of life"})
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class _ClientE2E:
|
|
221
|
+
def __init__(self, *a, **k) -> None:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
def __enter__(self):
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(self, *a):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def post(self, endpoint, headers=None, json=None):
|
|
231
|
+
return _post_e2e(self, endpoint, headers, json)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
llm_router.httpx.Client = _ClientE2E
|
|
235
|
+
|
|
236
|
+
registros.clear()
|
|
237
|
+
|
|
238
|
+
texto_e2e, provedor_e2e = llm_router.gerar_texto(_db_com_chaves(), "escreva uma noticia")
|
|
239
|
+
|
|
240
|
+
check(provedor_e2e == "gemini", "E2E HTTP: NVIDIA 410 -> Gemini assume (provedor: gemini)")
|
|
241
|
+
check("titulo" in texto_e2e, "E2E HTTP: o conteúdo devolvido é o do Gemini (JSON utilizável)")
|
|
242
|
+
check(
|
|
243
|
+
any("Erro na NVIDIA (" in m and "Acionando Google Gemini" in m for m in registros),
|
|
244
|
+
"E2E HTTP: log 'Erro na NVIDIA (...). Acionando Google Gemini...' emitido",
|
|
245
|
+
)
|
|
246
|
+
|
|
184
247
|
# ------------------------------------------- 4) endpoint replicado do Thunderbolt
|
|
185
248
|
|
|
186
249
|
check(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1{left:.25rem}.left-11{left:2.75rem}.left-9{left:2.25rem}.top-1{top:.25rem}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-40{height:10rem}.h-44{height:11rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-40{width:10rem}.w-44{width:11rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-panda\/40{border-color:#22c55e66}.border-red-400\/40{border-color:#f8717166}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black\/60{background-color:#0009}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-night{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.bg-panda{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-panda\/10{background-color:#22c55e1a}.bg-panda\/15{background-color:#22c55e26}.bg-panda\/30{background-color:#22c55e4d}.bg-panda\/70{background-color:#22c55eb3}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-panda{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-panda-dark{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.placeholder-slate-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.placeholder-slate-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1));font-family:Inter,system-ui,sans-serif;--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html:not(.dark) body{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-slate-400:hover{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity, 1))}.hover\:bg-panda-dark:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:text-slate-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-panda:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.dark\:border-slate-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:bg-amber-500\/15:is(.dark *){background-color:#f59e0b26}.dark\:bg-card:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(16 26 44 / var(--tw-bg-opacity, 1))}.dark\:bg-emerald-500\/15:is(.dark *){background-color:#10b98126}.dark\:bg-night:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.dark\:bg-red-500\/15:is(.dark *){background-color:#ef444426}.dark\:bg-sky-500\/15:is(.dark *){background-color:#0ea5e926}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-700\/60:is(.dark *){background-color:#33415599}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-panda:is(.dark *){--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is(.dark *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-slate-500:is(.dark *){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.dark\:text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:hover\:border-slate-500:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-slate-800\/60:hover:is(.dark *){background-color:#1e293b99}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1{left:.25rem}.left-11{left:2.75rem}.left-9{left:2.25rem}.top-1{top:.25rem}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-40{height:10rem}.h-44{height:11rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-40{width:10rem}.w-44{width:11rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-panda\/40{border-color:#22c55e66}.border-red-400\/40{border-color:#f8717166}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black\/60{background-color:#0009}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-night{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.bg-panda{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-panda\/10{background-color:#22c55e1a}.bg-panda\/15{background-color:#22c55e26}.bg-panda\/30{background-color:#22c55e4d}.bg-panda\/70{background-color:#22c55eb3}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-panda{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-panda-dark{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.placeholder-slate-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.placeholder-slate-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1));font-family:Inter,system-ui,sans-serif;--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html:not(.dark) body{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-slate-400:hover{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity, 1))}.hover\:bg-panda-dark:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:text-slate-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-panda:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.dark\:border-slate-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:bg-amber-500\/15:is(.dark *){background-color:#f59e0b26}.dark\:bg-card:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(16 26 44 / var(--tw-bg-opacity, 1))}.dark\:bg-emerald-500\/15:is(.dark *){background-color:#10b98126}.dark\:bg-night:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.dark\:bg-red-500\/15:is(.dark *){background-color:#ef444426}.dark\:bg-sky-500\/15:is(.dark *){background-color:#0ea5e926}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-700\/60:is(.dark *){background-color:#33415599}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-panda:is(.dark *){--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is(.dark *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-slate-500:is(.dark *){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.dark\:text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:hover\:border-slate-500:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-slate-800\/60:hover:is(.dark *){background-color:#1e293b99}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|