graphgrail 0.1.0__py3-none-any.whl
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.
- grail/__init__.py +59 -0
- grail/_version.py +1 -0
- grail/apps/__init__.py +0 -0
- grail/apps/chat/__init__.py +0 -0
- grail/apps/chat/auth.py +69 -0
- grail/apps/chat/database.py +349 -0
- grail/apps/chat/frontend/dist/assets/index-2a40wZqj.css +1 -0
- grail/apps/chat/frontend/dist/assets/index-DfRgtpKs.js +239 -0
- grail/apps/chat/frontend/dist/index.html +17 -0
- grail/apps/chat/frontend/index.html +16 -0
- grail/apps/chat/frontend/package-lock.json +3990 -0
- grail/apps/chat/frontend/package.json +29 -0
- grail/apps/chat/frontend/src/App.tsx +76 -0
- grail/apps/chat/frontend/src/components/ChatInput.tsx +131 -0
- grail/apps/chat/frontend/src/components/ChatView.tsx +96 -0
- grail/apps/chat/frontend/src/components/InfoPanel.tsx +119 -0
- grail/apps/chat/frontend/src/components/Layout.tsx +118 -0
- grail/apps/chat/frontend/src/components/LoginView.tsx +199 -0
- grail/apps/chat/frontend/src/components/MarkdownRenderer.tsx +227 -0
- grail/apps/chat/frontend/src/components/MessageBubble.tsx +237 -0
- grail/apps/chat/frontend/src/components/ModeSelector.tsx +189 -0
- grail/apps/chat/frontend/src/components/Sidebar.tsx +211 -0
- grail/apps/chat/frontend/src/components/WelcomeView.tsx +182 -0
- grail/apps/chat/frontend/src/index.css +166 -0
- grail/apps/chat/frontend/src/lib/api.ts +162 -0
- grail/apps/chat/frontend/src/lib/store.ts +395 -0
- grail/apps/chat/frontend/src/main.tsx +10 -0
- grail/apps/chat/frontend/src/vite-env.d.ts +1 -0
- grail/apps/chat/frontend/tsconfig.app.json +21 -0
- grail/apps/chat/frontend/tsconfig.app.tsbuildinfo +1 -0
- grail/apps/chat/frontend/tsconfig.json +7 -0
- grail/apps/chat/frontend/tsconfig.node.json +19 -0
- grail/apps/chat/frontend/tsconfig.node.tsbuildinfo +1 -0
- grail/apps/chat/frontend/vite.config.ts +13 -0
- grail/apps/chat/schemas.py +90 -0
- grail/apps/chat/server.py +414 -0
- grail/apps/cli_chat/__init__.py +8 -0
- grail/apps/cli_chat/app.py +990 -0
- grail/apps/cli_chat/commands.py +181 -0
- grail/apps/cli_chat/reporter.py +73 -0
- grail/apps/cli_chat/widgets.py +496 -0
- grail/cli/__init__.py +7 -0
- grail/cli/banner.py +819 -0
- grail/cli/main.py +1663 -0
- grail/config.py +548 -0
- grail/core.py +1064 -0
- grail/export/__init__.py +5 -0
- grail/export/neo4j.py +298 -0
- grail/indexing/__init__.py +36 -0
- grail/indexing/communities.py +214 -0
- grail/indexing/community_reports.py +486 -0
- grail/indexing/entities_relationships.py +978 -0
- grail/indexing/entity_dedup.py +396 -0
- grail/indexing/incremental_community.py +382 -0
- grail/indexing/leiden.py +177 -0
- grail/indexing/loader.py +612 -0
- grail/indexing/preprocess.py +312 -0
- grail/indexing/run_manifest.py +274 -0
- grail/indexing/schema_migration.py +114 -0
- grail/indexing/stable_lcc.py +42 -0
- grail/indexing/summarize_descriptions.py +249 -0
- grail/llm/__init__.py +39 -0
- grail/llm/cache.py +122 -0
- grail/llm/cost.py +253 -0
- grail/llm/embeddings.py +228 -0
- grail/llm/providers.py +198 -0
- grail/llm/reranker.py +220 -0
- grail/llm/wrapper.py +654 -0
- grail/memory/__init__.py +55 -0
- grail/memory/_merge.py +288 -0
- grail/memory/analyses/__init__.py +70 -0
- grail/memory/analyses/alias_detect.py +190 -0
- grail/memory/analyses/edge_density.py +164 -0
- grail/memory/analyses/folder_split.py +135 -0
- grail/memory/analyses/membership.py +118 -0
- grail/memory/consolidate.py +164 -0
- grail/memory/identity.py +231 -0
- grail/memory/observation.py +165 -0
- grail/memory/project.py +1805 -0
- grail/memory/proposals.py +240 -0
- grail/memory/types.py +127 -0
- grail/prompts/__init__.py +25 -0
- grail/prompts/builtin/__init__.py +8 -0
- grail/prompts/builtin/claim_extraction.py +107 -0
- grail/prompts/builtin/community_report.py +170 -0
- grail/prompts/builtin/create_custom_entities.py +138 -0
- grail/prompts/builtin/entity_dedup.py +117 -0
- grail/prompts/builtin/entity_relation.py +267 -0
- grail/prompts/builtin/global_map.py +114 -0
- grail/prompts/builtin/global_reduce.py +77 -0
- grail/prompts/builtin/json_correction.py +83 -0
- grail/prompts/builtin/local_search.py +73 -0
- grail/prompts/builtin/summarize_description.py +105 -0
- grail/prompts/loader.py +172 -0
- grail/query/__init__.py +29 -0
- grail/query/agent.py +560 -0
- grail/query/cascade_search.py +376 -0
- grail/query/document_search.py +294 -0
- grail/query/global_search.py +287 -0
- grail/query/local_search.py +331 -0
- grail/query/recall_filter.py +312 -0
- grail/query/recall_search.py +264 -0
- grail/query/retrieval.py +425 -0
- grail/query/trace.py +119 -0
- grail/reporting/__init__.py +7 -0
- grail/reporting/rich_reporter.py +183 -0
- grail/schemas.py +382 -0
- grail/storage/__init__.py +29 -0
- grail/storage/base.py +127 -0
- grail/storage/local.py +116 -0
- grail/storage/s3.py +118 -0
- grail/utils/__init__.py +19 -0
- grail/utils/chunker.py +47 -0
- grail/utils/ids.py +19 -0
- grail/utils/text.py +44 -0
- grail/utils/tokens.py +29 -0
- grail/utils/zip.py +50 -0
- grail/vectorstores/__init__.py +29 -0
- grail/vectorstores/base.py +75 -0
- grail/vectorstores/chroma.py +145 -0
- grail/vectorstores/faiss.py +158 -0
- grail/vectorstores/lancedb.py +111 -0
- grail/viz/__init__.py +24 -0
- grail/viz/builder.py +129 -0
- grail/viz/colors.py +127 -0
- grail/viz/exporter.py +552 -0
- grail/viz/layout.py +315 -0
- grail/viz/template.py +1015 -0
- graphgrail-0.1.0.dist-info/METADATA +68 -0
- graphgrail-0.1.0.dist-info/RECORD +133 -0
- graphgrail-0.1.0.dist-info/WHEEL +4 -0
- graphgrail-0.1.0.dist-info/entry_points.txt +2 -0
- graphgrail-0.1.0.dist-info/licenses/LICENSE +21 -0
grail/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GRAIL — Graph RAG with Advanced Integration and Learning.
|
|
3
|
+
|
|
4
|
+
Provided by Nirvai (Nirvana). Author: Benjamin González Guerrero.
|
|
5
|
+
|
|
6
|
+
Public surface re-exports the most common entry points so users can do::
|
|
7
|
+
|
|
8
|
+
from grail import GRAIL, LLMClient, EmbeddingClient
|
|
9
|
+
from grail import load_config, PromptRegistry
|
|
10
|
+
"""
|
|
11
|
+
from grail._version import __version__
|
|
12
|
+
from grail.config import Config, load_config
|
|
13
|
+
from grail.core import GRAIL
|
|
14
|
+
from grail.llm import EmbeddingClient, LLMClient
|
|
15
|
+
from grail.memory import (
|
|
16
|
+
CommunityRecord,
|
|
17
|
+
EntityRecord,
|
|
18
|
+
MemoryProject,
|
|
19
|
+
Observation,
|
|
20
|
+
RelationshipRecord,
|
|
21
|
+
Reply,
|
|
22
|
+
)
|
|
23
|
+
from grail.prompts import PromptRegistry
|
|
24
|
+
from grail.query.recall_filter import RecallFilter
|
|
25
|
+
from grail.schemas import (
|
|
26
|
+
Community,
|
|
27
|
+
CommunityReport,
|
|
28
|
+
Covariate,
|
|
29
|
+
Document,
|
|
30
|
+
Entity,
|
|
31
|
+
Relationship,
|
|
32
|
+
SearchResult,
|
|
33
|
+
TextUnit,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Community",
|
|
38
|
+
"CommunityRecord",
|
|
39
|
+
"CommunityReport",
|
|
40
|
+
"Config",
|
|
41
|
+
"Covariate",
|
|
42
|
+
"Document",
|
|
43
|
+
"EmbeddingClient",
|
|
44
|
+
"Entity",
|
|
45
|
+
"EntityRecord",
|
|
46
|
+
"GRAIL",
|
|
47
|
+
"LLMClient",
|
|
48
|
+
"MemoryProject",
|
|
49
|
+
"Observation",
|
|
50
|
+
"PromptRegistry",
|
|
51
|
+
"RecallFilter",
|
|
52
|
+
"Relationship",
|
|
53
|
+
"RelationshipRecord",
|
|
54
|
+
"Reply",
|
|
55
|
+
"SearchResult",
|
|
56
|
+
"TextUnit",
|
|
57
|
+
"__version__",
|
|
58
|
+
"load_config",
|
|
59
|
+
]
|
grail/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
grail/apps/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
grail/apps/chat/auth.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT authentication and password hashing for the GRAIL chat API.
|
|
3
|
+
|
|
4
|
+
Provided by Nirvai (Nirvana). Author: Benjamin González Guerrero.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastapi import Depends, HTTPException, status
|
|
15
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
16
|
+
from jose import JWTError, jwt
|
|
17
|
+
from passlib.context import CryptContext
|
|
18
|
+
|
|
19
|
+
_pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
20
|
+
_bearer_scheme = HTTPBearer()
|
|
21
|
+
|
|
22
|
+
ALGORITHM = "HS256"
|
|
23
|
+
ACCESS_TOKEN_EXPIRE_HOURS = 24
|
|
24
|
+
_SECRET_FILE = Path.home() / ".grail" / "jwt_secret"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_secret() -> str:
|
|
28
|
+
env = os.environ.get("GRAIL_JWT_SECRET")
|
|
29
|
+
if env:
|
|
30
|
+
return env
|
|
31
|
+
if _SECRET_FILE.exists():
|
|
32
|
+
return _SECRET_FILE.read_text().strip()
|
|
33
|
+
secret = secrets.token_urlsafe(64)
|
|
34
|
+
_SECRET_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
_SECRET_FILE.write_text(secret)
|
|
36
|
+
return secret
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def hash_password(plain: str) -> str:
|
|
40
|
+
return _pwd_context.hash(plain)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
44
|
+
return _pwd_context.verify(plain, hashed)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_access_token(user_id: str, username: str) -> str:
|
|
48
|
+
expire = datetime.now(timezone.utc) + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
|
49
|
+
payload = {"sub": user_id, "username": username, "exp": expire}
|
|
50
|
+
return jwt.encode(payload, _get_secret(), algorithm=ALGORITHM)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def decode_token(token: str) -> dict[str, Any]:
|
|
54
|
+
return jwt.decode(token, _get_secret(), algorithms=[ALGORITHM])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def get_current_user(
|
|
58
|
+
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
|
|
59
|
+
) -> dict[str, str]:
|
|
60
|
+
token = credentials.credentials
|
|
61
|
+
try:
|
|
62
|
+
payload = decode_token(token)
|
|
63
|
+
user_id: str | None = payload.get("sub")
|
|
64
|
+
username: str | None = payload.get("username")
|
|
65
|
+
if not user_id or not username:
|
|
66
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
67
|
+
return {"id": user_id, "username": username}
|
|
68
|
+
except JWTError:
|
|
69
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite database layer for the GRAIL chat API.
|
|
3
|
+
|
|
4
|
+
Provided by Nirvai (Nirvana). Author: Benjamin González Guerrero.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import uuid
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, AsyncIterator, Optional
|
|
14
|
+
|
|
15
|
+
import aiosqlite
|
|
16
|
+
|
|
17
|
+
_db_path: Path = Path.home() / ".grail" / "chat.db"
|
|
18
|
+
|
|
19
|
+
_SCHEMA = """\
|
|
20
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
username TEXT UNIQUE NOT NULL,
|
|
23
|
+
password_hash TEXT NOT NULL,
|
|
24
|
+
created_at TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
30
|
+
title TEXT NOT NULL DEFAULT 'New Chat',
|
|
31
|
+
mode TEXT NOT NULL DEFAULT 'local',
|
|
32
|
+
source TEXT NOT NULL DEFAULT 'web',
|
|
33
|
+
created_at TEXT NOT NULL,
|
|
34
|
+
updated_at TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
40
|
+
role TEXT NOT NULL,
|
|
41
|
+
content TEXT NOT NULL,
|
|
42
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
43
|
+
tool_calls TEXT,
|
|
44
|
+
tool_call_id TEXT,
|
|
45
|
+
created_at TEXT NOT NULL
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
_MIGRATIONS = [
|
|
53
|
+
"ALTER TABLE messages ADD COLUMN tool_calls TEXT",
|
|
54
|
+
"ALTER TABLE messages ADD COLUMN tool_call_id TEXT",
|
|
55
|
+
"ALTER TABLE sessions ADD COLUMN source TEXT NOT NULL DEFAULT 'web'",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def configure_db_path(path: Path) -> None:
|
|
60
|
+
global _db_path
|
|
61
|
+
_db_path = path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _now() -> str:
|
|
65
|
+
return datetime.now(timezone.utc).isoformat()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _uuid() -> str:
|
|
69
|
+
return str(uuid.uuid4())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def init_db() -> None:
|
|
73
|
+
_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
async with aiosqlite.connect(str(_db_path)) as db:
|
|
75
|
+
await db.execute("PRAGMA journal_mode=WAL")
|
|
76
|
+
await db.execute("PRAGMA foreign_keys=ON")
|
|
77
|
+
await db.executescript(_SCHEMA)
|
|
78
|
+
for migration in _MIGRATIONS:
|
|
79
|
+
try:
|
|
80
|
+
await db.execute(migration)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
await db.commit()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@asynccontextmanager
|
|
87
|
+
async def get_db() -> AsyncIterator[aiosqlite.Connection]:
|
|
88
|
+
db = await aiosqlite.connect(str(_db_path))
|
|
89
|
+
await db.execute("PRAGMA foreign_keys=ON")
|
|
90
|
+
db.row_factory = aiosqlite.Row
|
|
91
|
+
try:
|
|
92
|
+
yield db
|
|
93
|
+
finally:
|
|
94
|
+
await db.close()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------ Users
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get_user_count() -> int:
|
|
101
|
+
async with get_db() as db:
|
|
102
|
+
cursor = await db.execute("SELECT COUNT(*) FROM users")
|
|
103
|
+
row = await cursor.fetchone()
|
|
104
|
+
return row[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def create_user(username: str, password_hash: str) -> dict[str, Any]:
|
|
108
|
+
user_id = _uuid()
|
|
109
|
+
now = _now()
|
|
110
|
+
async with get_db() as db:
|
|
111
|
+
await db.execute(
|
|
112
|
+
"INSERT INTO users (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)",
|
|
113
|
+
(user_id, username, password_hash, now),
|
|
114
|
+
)
|
|
115
|
+
await db.commit()
|
|
116
|
+
return {"id": user_id, "username": username, "created_at": now}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def get_user_by_username(username: str) -> Optional[dict[str, Any]]:
|
|
120
|
+
async with get_db() as db:
|
|
121
|
+
cursor = await db.execute(
|
|
122
|
+
"SELECT id, username, password_hash, created_at FROM users WHERE username = ?",
|
|
123
|
+
(username,),
|
|
124
|
+
)
|
|
125
|
+
row = await cursor.fetchone()
|
|
126
|
+
if not row:
|
|
127
|
+
return None
|
|
128
|
+
return {"id": row[0], "username": row[1], "password_hash": row[2], "created_at": row[3]}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def get_user_by_id(user_id: str) -> Optional[dict[str, Any]]:
|
|
132
|
+
async with get_db() as db:
|
|
133
|
+
cursor = await db.execute(
|
|
134
|
+
"SELECT id, username, created_at FROM users WHERE id = ?",
|
|
135
|
+
(user_id,),
|
|
136
|
+
)
|
|
137
|
+
row = await cursor.fetchone()
|
|
138
|
+
if not row:
|
|
139
|
+
return None
|
|
140
|
+
return {"id": row[0], "username": row[1], "created_at": row[2]}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------ Sessions
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def list_sessions(user_id: str, source: Optional[str] = None) -> list[dict[str, Any]]:
|
|
147
|
+
"""List sessions for a user. If *source* is given, filter by source (e.g. 'web' or 'cli')."""
|
|
148
|
+
async with get_db() as db:
|
|
149
|
+
if source is None:
|
|
150
|
+
cursor = await db.execute(
|
|
151
|
+
"""
|
|
152
|
+
SELECT s.id, s.title, s.mode, s.source, s.created_at, s.updated_at,
|
|
153
|
+
(SELECT COUNT(*) FROM messages WHERE session_id = s.id AND role IN ('user', 'assistant') AND tool_calls IS NULL) AS message_count
|
|
154
|
+
FROM sessions s
|
|
155
|
+
WHERE s.user_id = ?
|
|
156
|
+
ORDER BY s.updated_at DESC
|
|
157
|
+
""",
|
|
158
|
+
(user_id,),
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
cursor = await db.execute(
|
|
162
|
+
"""
|
|
163
|
+
SELECT s.id, s.title, s.mode, s.source, s.created_at, s.updated_at,
|
|
164
|
+
(SELECT COUNT(*) FROM messages WHERE session_id = s.id AND role IN ('user', 'assistant') AND tool_calls IS NULL) AS message_count
|
|
165
|
+
FROM sessions s
|
|
166
|
+
WHERE s.user_id = ? AND s.source = ?
|
|
167
|
+
ORDER BY s.updated_at DESC
|
|
168
|
+
""",
|
|
169
|
+
(user_id, source),
|
|
170
|
+
)
|
|
171
|
+
rows = await cursor.fetchall()
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
"id": r[0], "title": r[1], "mode": r[2], "source": r[3],
|
|
175
|
+
"created_at": r[4], "updated_at": r[5], "message_count": r[6],
|
|
176
|
+
}
|
|
177
|
+
for r in rows
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def create_session(
|
|
182
|
+
user_id: str, title: str = "New Chat", mode: str = "local", source: str = "web",
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
session_id = _uuid()
|
|
185
|
+
now = _now()
|
|
186
|
+
async with get_db() as db:
|
|
187
|
+
await db.execute(
|
|
188
|
+
"INSERT INTO sessions (id, user_id, title, mode, source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
189
|
+
(session_id, user_id, title, mode, source, now, now),
|
|
190
|
+
)
|
|
191
|
+
await db.commit()
|
|
192
|
+
return {
|
|
193
|
+
"id": session_id, "title": title, "mode": mode, "source": source,
|
|
194
|
+
"created_at": now, "updated_at": now,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def get_session(session_id: str, user_id: str) -> Optional[dict[str, Any]]:
|
|
199
|
+
async with get_db() as db:
|
|
200
|
+
cursor = await db.execute(
|
|
201
|
+
"SELECT id, user_id, title, mode, created_at, updated_at FROM sessions WHERE id = ? AND user_id = ?",
|
|
202
|
+
(session_id, user_id),
|
|
203
|
+
)
|
|
204
|
+
row = await cursor.fetchone()
|
|
205
|
+
if not row:
|
|
206
|
+
return None
|
|
207
|
+
return {
|
|
208
|
+
"id": row[0], "user_id": row[1], "title": row[2],
|
|
209
|
+
"mode": row[3], "created_at": row[4], "updated_at": row[5],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def update_session(session_id: str, user_id: str, **fields: Any) -> Optional[dict[str, Any]]:
|
|
214
|
+
allowed = {"title", "mode"}
|
|
215
|
+
updates = {k: v for k, v in fields.items() if k in allowed and v is not None}
|
|
216
|
+
if not updates:
|
|
217
|
+
return await get_session(session_id, user_id)
|
|
218
|
+
updates["updated_at"] = _now()
|
|
219
|
+
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
220
|
+
values = list(updates.values()) + [session_id, user_id]
|
|
221
|
+
async with get_db() as db:
|
|
222
|
+
await db.execute(
|
|
223
|
+
f"UPDATE sessions SET {set_clause} WHERE id = ? AND user_id = ?",
|
|
224
|
+
values,
|
|
225
|
+
)
|
|
226
|
+
await db.commit()
|
|
227
|
+
return await get_session(session_id, user_id)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def delete_session(session_id: str, user_id: str) -> bool:
|
|
231
|
+
async with get_db() as db:
|
|
232
|
+
cursor = await db.execute(
|
|
233
|
+
"DELETE FROM sessions WHERE id = ? AND user_id = ?",
|
|
234
|
+
(session_id, user_id),
|
|
235
|
+
)
|
|
236
|
+
await db.commit()
|
|
237
|
+
return cursor.rowcount > 0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------ Messages
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _row_to_message(r: tuple) -> dict[str, Any]:
|
|
244
|
+
"""Convert a DB row to a message dict."""
|
|
245
|
+
msg: dict[str, Any] = {
|
|
246
|
+
"id": r[0], "role": r[1], "content": r[2],
|
|
247
|
+
"metadata": json.loads(r[3]), "created_at": r[4],
|
|
248
|
+
}
|
|
249
|
+
if r[5]:
|
|
250
|
+
msg["tool_calls"] = json.loads(r[5])
|
|
251
|
+
if r[6]:
|
|
252
|
+
msg["tool_call_id"] = r[6]
|
|
253
|
+
return msg
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def get_messages(session_id: str, *, include_tool_messages: bool = False) -> list[dict[str, Any]]:
|
|
257
|
+
"""Get messages for display. By default excludes tool-call machinery."""
|
|
258
|
+
async with get_db() as db:
|
|
259
|
+
if include_tool_messages:
|
|
260
|
+
cursor = await db.execute(
|
|
261
|
+
"SELECT id, role, content, metadata, created_at, tool_calls, tool_call_id "
|
|
262
|
+
"FROM messages WHERE session_id = ? ORDER BY created_at ASC",
|
|
263
|
+
(session_id,),
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
cursor = await db.execute(
|
|
267
|
+
"SELECT id, role, content, metadata, created_at, tool_calls, tool_call_id "
|
|
268
|
+
"FROM messages WHERE session_id = ? "
|
|
269
|
+
"AND role IN ('user', 'assistant') AND tool_calls IS NULL "
|
|
270
|
+
"ORDER BY created_at ASC",
|
|
271
|
+
(session_id,),
|
|
272
|
+
)
|
|
273
|
+
rows = await cursor.fetchall()
|
|
274
|
+
return [_row_to_message(r) for r in rows]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def create_message(
|
|
278
|
+
session_id: str,
|
|
279
|
+
role: str,
|
|
280
|
+
content: str,
|
|
281
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
282
|
+
tool_calls: Optional[list[dict[str, Any]]] = None,
|
|
283
|
+
tool_call_id: Optional[str] = None,
|
|
284
|
+
) -> dict[str, Any]:
|
|
285
|
+
msg_id = _uuid()
|
|
286
|
+
now = _now()
|
|
287
|
+
meta_json = json.dumps(metadata or {})
|
|
288
|
+
tc_json = json.dumps(tool_calls) if tool_calls else None
|
|
289
|
+
async with get_db() as db:
|
|
290
|
+
await db.execute(
|
|
291
|
+
"INSERT INTO messages (id, session_id, role, content, metadata, tool_calls, tool_call_id, created_at) "
|
|
292
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
293
|
+
(msg_id, session_id, role, content, meta_json, tc_json, tool_call_id, now),
|
|
294
|
+
)
|
|
295
|
+
await db.execute(
|
|
296
|
+
"UPDATE sessions SET updated_at = ? WHERE id = ?",
|
|
297
|
+
(now, session_id),
|
|
298
|
+
)
|
|
299
|
+
await db.commit()
|
|
300
|
+
msg: dict[str, Any] = {
|
|
301
|
+
"id": msg_id, "role": role, "content": content,
|
|
302
|
+
"metadata": metadata or {}, "created_at": now,
|
|
303
|
+
}
|
|
304
|
+
if tool_calls:
|
|
305
|
+
msg["tool_calls"] = tool_calls
|
|
306
|
+
if tool_call_id:
|
|
307
|
+
msg["tool_call_id"] = tool_call_id
|
|
308
|
+
return msg
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def create_messages_batch(
|
|
312
|
+
session_id: str, messages: list[dict[str, Any]],
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Insert a batch of messages preserving order (for agent tool-call sequences)."""
|
|
315
|
+
base = datetime.now(timezone.utc)
|
|
316
|
+
async with get_db() as db:
|
|
317
|
+
for i, msg in enumerate(messages):
|
|
318
|
+
msg_id = _uuid()
|
|
319
|
+
ts = (base + __import__("datetime").timedelta(microseconds=i)).isoformat()
|
|
320
|
+
meta_json = json.dumps(msg.get("metadata") or {})
|
|
321
|
+
tc = msg.get("tool_calls")
|
|
322
|
+
tc_json = json.dumps(tc) if tc else None
|
|
323
|
+
await db.execute(
|
|
324
|
+
"INSERT INTO messages (id, session_id, role, content, metadata, tool_calls, tool_call_id, created_at) "
|
|
325
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
326
|
+
(msg_id, session_id, msg["role"], msg.get("content", ""),
|
|
327
|
+
meta_json, tc_json, msg.get("tool_call_id"), ts),
|
|
328
|
+
)
|
|
329
|
+
await db.execute(
|
|
330
|
+
"UPDATE sessions SET updated_at = ? WHERE id = ?",
|
|
331
|
+
(base.isoformat(), session_id),
|
|
332
|
+
)
|
|
333
|
+
await db.commit()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def get_recent_messages(session_id: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
337
|
+
"""Get recent messages including tool-call messages for history building."""
|
|
338
|
+
async with get_db() as db:
|
|
339
|
+
cursor = await db.execute(
|
|
340
|
+
"""
|
|
341
|
+
SELECT id, role, content, metadata, created_at, tool_calls, tool_call_id
|
|
342
|
+
FROM messages WHERE session_id = ?
|
|
343
|
+
ORDER BY created_at DESC LIMIT ?
|
|
344
|
+
""",
|
|
345
|
+
(session_id, limit),
|
|
346
|
+
)
|
|
347
|
+
rows = await cursor.fetchall()
|
|
348
|
+
rows = list(reversed(rows))
|
|
349
|
+
return [_row_to_message(r) for r in rows]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-900:oklch(39.6% .141 25.723);--color-amber-400:oklch(82.8% .189 84.429);--color-teal-300:#5eead4;--color-teal-400:#2dd4bf;--color-teal-500:#14b8a6;--color-teal-600:#0d9488;--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-2xl:42rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-2{top:calc(var(--spacing) * 2)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-full{bottom:100%}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.-mt-1{margin-top:calc(var(--spacing) * -1)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-\[200px\]{max-height:200px}.min-h-\[44px\]{min-height:44px}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-72{width:calc(var(--spacing) * 72)}.w-\[272px\]{width:272px}.w-\[280px\]{width:280px}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-\[80\%\]{max-width:80%}.max-w-\[90px\]{max-width:90px}.max-w-\[100px\]{max-width:100px}.max-w-\[130px\]{max-width:130px}.max-w-\[240px\]{max-width:240px}.max-w-full{max-width:100%}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-tl-md{border-top-left-radius:var(--radius-md)}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-500\/30{border-color:#14b8a64d}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/30{border-color:color-mix(in oklab,var(--color-teal-500) 30%,transparent)}}.border-teal-500\/40{border-color:#14b8a666}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/40{border-color:color-mix(in oklab,var(--color-teal-500) 40%,transparent)}}.border-teal-500\/50{border-color:#14b8a680}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/50{border-color:color-mix(in oklab,var(--color-teal-500) 50%,transparent)}}.border-transparent{border-color:#0000}.border-zinc-700{border-color:var(--color-zinc-700)}.border-zinc-800{border-color:var(--color-zinc-800)}.border-zinc-800\/60{border-color:#27272a99}@supports (color:color-mix(in lab,red,red)){.border-zinc-800\/60{border-color:color-mix(in oklab,var(--color-zinc-800) 60%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-500\/5{background-color:#14b8a60d}@supports (color:color-mix(in lab,red,red)){.bg-teal-500\/5{background-color:color-mix(in oklab,var(--color-teal-500) 5%,transparent)}}.bg-teal-500\/10{background-color:#14b8a61a}@supports (color:color-mix(in lab,red,red)){.bg-teal-500\/10{background-color:color-mix(in oklab,var(--color-teal-500) 10%,transparent)}}.bg-teal-500\/15{background-color:#14b8a626}@supports (color:color-mix(in lab,red,red)){.bg-teal-500\/15{background-color:color-mix(in oklab,var(--color-teal-500) 15%,transparent)}}.bg-teal-600{background-color:var(--color-teal-600)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-zinc-700{background-color:var(--color-zinc-700)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-zinc-800\/40{background-color:#27272a66}@supports (color:color-mix(in lab,red,red)){.bg-zinc-800\/40{background-color:color-mix(in oklab,var(--color-zinc-800) 40%,transparent)}}.bg-zinc-800\/60{background-color:#27272a99}@supports (color:color-mix(in lab,red,red)){.bg-zinc-800\/60{background-color:color-mix(in oklab,var(--color-zinc-800) 60%,transparent)}}.bg-zinc-800\/80{background-color:#27272acc}@supports (color:color-mix(in lab,red,red)){.bg-zinc-800\/80{background-color:color-mix(in oklab,var(--color-zinc-800) 80%,transparent)}}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-900\/50{background-color:#18181b80}@supports (color:color-mix(in lab,red,red)){.bg-zinc-900\/50{background-color:color-mix(in oklab,var(--color-zinc-900) 50%,transparent)}}.bg-zinc-950{background-color:var(--color-zinc-950)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[5px\]{font-size:5px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-400{color:var(--color-amber-400)}.text-red-400{color:var(--color-red-400)}.text-teal-300{color:var(--color-teal-300)}.text-teal-400{color:var(--color-teal-400)}.text-teal-500{color:var(--color-teal-500)}.text-white{color:var(--color-white)}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.placeholder-zinc-500::placeholder{color:var(--color-zinc-500)}.opacity-0{opacity:0}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color:#0006}@supports (color:color-mix(in lab,red,red)){.shadow-black\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.blur{--tw-blur:blur(8px);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,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);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,)}.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,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-\[cubic-bezier\(0\.25\,0\.4\,0\.25\,1\)\]{--tw-ease:cubic-bezier(.25,.4,.25,1);transition-timing-function:cubic-bezier(.25,.4,.25,1)}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}@media(hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/src\:inline-flex:is(:where(.group\/src):hover *){display:inline-flex}}.first\:mt-0:first-child{margin-top:calc(var(--spacing) * 0)}.first\:mt-1:first-child{margin-top:calc(var(--spacing) * 1)}.focus-within\:border-zinc-700:focus-within{border-color:var(--color-zinc-700)}@media(hover:hover){.hover\:border-teal-600:hover{border-color:var(--color-teal-600)}.hover\:border-zinc-700:hover{border-color:var(--color-zinc-700)}.hover\:bg-teal-500:hover{background-color:var(--color-teal-500)}.hover\:bg-zinc-600:hover{background-color:var(--color-zinc-600)}.hover\:bg-zinc-700\/80:hover{background-color:#3f3f46cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-zinc-700\/80:hover{background-color:color-mix(in oklab,var(--color-zinc-700) 80%,transparent)}}.hover\:bg-zinc-800:hover{background-color:var(--color-zinc-800)}.hover\:bg-zinc-800\/50:hover{background-color:#27272a80}@supports (color:color-mix(in lab,red,red)){.hover\:bg-zinc-800\/50:hover{background-color:color-mix(in oklab,var(--color-zinc-800) 50%,transparent)}}.hover\:bg-zinc-800\/60:hover{background-color:#27272a99}@supports (color:color-mix(in lab,red,red)){.hover\:bg-zinc-800\/60:hover{background-color:color-mix(in oklab,var(--color-zinc-800) 60%,transparent)}}.hover\:bg-zinc-900:hover{background-color:var(--color-zinc-900)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-teal-300:hover{color:var(--color-teal-300)}.hover\:text-zinc-200:hover{color:var(--color-zinc-200)}.hover\:text-zinc-300:hover{color:var(--color-zinc-300)}}.focus\:border-teal-500:focus{border-color:var(--color-teal-500)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-teal-500:focus{--tw-ring-color:var(--color-teal-500)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-\[6px\]{font-size:6px}}@media(min-width:48rem){.md\:relative{position:relative}.md\:z-auto{z-index:auto}.md\:hidden{display:none}.md\:w-0{width:calc(var(--spacing) * 0)}.md\:-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:text-\[46px\]{font-size:46px}}}:root{--surface-0:#0a0a0c;--surface-1:#111114;--surface-2:#18181c;--surface-3:#1f1f24;--border:#25252b;--border-subtle:#1c1c22;--text-primary:#ececf0;--text-secondary:#8b8b98;--text-tertiary:#5a5a68;--accent:#14b8a6;--accent-soft:#14b8a614;--accent-border:#14b8a633}body{background:var(--surface-0);color:var(--text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;min-height:100vh;margin:0;font-family:DM Sans,ui-sans-serif,system-ui,-apple-system,sans-serif}body:after{content:"";z-index:9999;pointer-events:none;opacity:.018;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");background-repeat:repeat;background-size:256px 256px;position:fixed;top:0;right:0;bottom:0;left:0}::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--border);border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:#3f3f46}textarea{field-sizing:content}@keyframes pulse-dot{0%,80%,to{opacity:.3}40%{opacity:1}}.loading-dot:first-child{animation:1.4s ease-in-out infinite pulse-dot}.loading-dot:nth-child(2){animation:1.4s ease-in-out .2s infinite pulse-dot}.loading-dot:nth-child(3){animation:1.4s ease-in-out .4s infinite pulse-dot}@keyframes glow-pulse{0%,to{opacity:.4}50%{opacity:.7}}.glow-ring{box-shadow:0 0 0 1px var(--accent-border),0 0 20px -4px #14b8a626}.prose-grail{line-height:1.75}.prose-grail h1,.prose-grail h2,.prose-grail h3,.prose-grail h4{color:var(--text-primary);letter-spacing:-.01em;margin-top:1.25em;margin-bottom:.5em;font-weight:600}.prose-grail h1{font-size:1.5em}.prose-grail h2{font-size:1.25em}.prose-grail h3{font-size:1.1em}.prose-grail p{margin-top:.5em;margin-bottom:.5em}.prose-grail ul,.prose-grail ol{margin-top:.5em;margin-bottom:.5em;padding-left:1.5em}.prose-grail li{margin-top:.25em;margin-bottom:.25em}.prose-grail ul{list-style-type:disc}.prose-grail ol{list-style-type:decimal}.prose-grail code{background:var(--surface-2);color:#2dd4bf;border-radius:5px;padding:.15em .4em;font-family:JetBrains Mono,monospace;font-size:.85em}.prose-grail pre{background:var(--surface-1);border:1px solid var(--border);border-radius:10px;margin-top:.75em;margin-bottom:.75em;padding:1em;overflow-x:auto}.prose-grail pre code{color:#d4d4d4;background:0 0;padding:0}.prose-grail blockquote{border-left:2px solid var(--accent);color:var(--text-secondary);margin-top:.75em;margin-bottom:.75em;padding-left:1em}.prose-grail table{border-collapse:collapse;width:100%;margin-top:.75em;margin-bottom:.75em}.prose-grail th,.prose-grail td{border:1px solid var(--border);text-align:left;padding:.5em .75em}.prose-grail th{background:var(--surface-2);text-transform:uppercase;letter-spacing:.04em;color:var(--text-secondary);font-size:.85em;font-weight:600}.prose-grail a{color:#2dd4bf;text-decoration:none}.prose-grail a:hover{color:#5eead4;text-decoration:underline}.prose-grail hr{border-color:var(--border);margin-top:1.5em;margin-bottom:1.5em}.prose-grail strong{color:var(--text-primary);font-weight:600}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
|