diff --git a/CHEATBOOK.md b/CHEATBOOK.md new file mode 100644 index 0000000..8a9ab03 --- /dev/null +++ b/CHEATBOOK.md @@ -0,0 +1,111 @@ +# Flowk 🌊 Cheatbook + +A comprehensive quick-reference guide for building, orchestrating, and observing AI agent workflows with Flowk. + +--- + +## 1. Core Classes & Decorators + +### `Graph` +The central coordinator for building and executing workflows. + +| Method | Description | Example | +| :--- | :--- | :--- | +| `Graph(state_schema=None, checkpoint_db=None)` | Initialize a graph. Optionally provide a Pydantic schema and a connection string for persistence. | `g = Graph(state_schema=MyState, checkpoint_db="flow.db")` | +| `@g.node(retries=0, fallback=None)` | Decorator to register a function as a node. | `@g.node(retries=3)\ndef task(data): return data` | +| `g.connect(from_node, to_node)` | Connect two nodes sequentially. | `g.connect(start_node, end_node)` | +| `g.route(condition_fn, mapping_dict)` | Create a deterministic branch point. | `g.route(decider, {"yes": node_a, "no": node_b})` | +| `@g.llm_router(targets, model="gpt-4o-mini")` | Decorator for intelligent LLM-based auto-routing. | `@g.llm_router(targets={"a": "desc", "b": "desc"})\ndef router(state): return state["text"]` | +| `g.compile(interrupt_before=None)` | Freeze graph and set breakpoints. | `g.compile(interrupt_before=["final_step"])` | +| `g.run(input, session_id=None)` | Execute graph synchronously. | `result = g.run("Hello")` | +| `await g.arun(input, session_id=None)` | Execute graph asynchronously (supports parallel branches). | `res = await g.arun("Query")` | +| `async for event in g.astream(input)` | Stream execution events (JSON objects). | `async for e in g.astream(inp): print(e)` | +| `g.as_node(state_key=None)` | Wrap an entire graph into a single node for composition. | `sub_node = sub_graph.as_node(state_key="sub")` | +| `g.serve(host="0.0.0.0", port=8000)` | 1-Click FastAPI deployment. | `g.serve(port=8080)` | +| `g.show()` | Visualize graph in terminal as ASCII. | `g.show()` | +| `g.debug(input)` | Run with verbose terminal logging. | `g.debug("test")` | +| `g.replay(run_id)` | Replay a historical execution trace. | `g.replay("run-123")` | + +--- + +## 2. Advanced Features + +### Persistence: `StorageRegistry` & `MemoryStore` +Handled automatically when `checkpoint_db` is passed to `Graph()`. + +- **SQLite**: `checkpoint_db="flow.db"` +- **Redis**: `checkpoint_db="redis://localhost:6379/0"` + +| Method | Description | +| :--- | :--- | +| `StorageRegistry.get_trace(run_id)` | Retrieve raw execution trace. | +| `StorageRegistry.list_runs(session_id=None)` | List historical run IDs. | + +### Metrics: `MetricsRegistry` & `PluginManager` +Track token usage and execution performance. + +| Method | Description | Example | +| :--- | :--- | :--- | +| `MetricsRegistry.get_summary()` | Returns JSON of token usage, costs, and node times. | `print(g.metrics())` | +| `PluginManager.register(plugin)` | Register an LLM metrics plugin (OpenAI/Anthropic). | `PluginManager.register(OpenAIPlugin())` | + +--- + +## 3. CLI Commands + +| Command | Description | +| :--- | :--- | +| `flowk ui` | Launch the **Production-Grade Dashboard** (v2) on port 8502. | +| `flowk serve :` | Serve a graph file as a FastAPI app. | + +--- + +## 4. Examples + +### A. Minimal Async Graph +```python +from flowk import Graph + +g = Graph() + +@g.node() +async def greet(name: str): + return f"Hello {name}!" + +@g.node() +async def process(msg: str): + return msg.upper() + +g.connect(greet, process) + +# Run it +if __name__ == "__main__": + import asyncio + res = asyncio.run(g.arun("World")) + print(res) # => "HELLO WORLD!" +``` + +### B. Intelligent Router +```python +@g.llm_router(targets={ + "billing": "Queries about invoices or payments", + "support": "General help or technical issues" +}) +def supervisor(state): + return state["user_message"] + +g.connect(input_node, supervisor) +``` + +### C. Human-in-the-Loop +```python +g.compile(interrupt_before=["payment"]) + +# Running this will stop before 'payment' +async for event in g.astream(data, session_id="abc"): + if event["type"] == "interrupt": + print("PAUSED. Waiting for review.") + +# Resume later with same session_id +await g.arun(None, session_id="abc") +``` diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index d173d6a..70309b9 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -220,13 +220,19 @@ This isolates complex pipelines without polluting supervisor-level logic domains ## 8. Observability (CLI, Time-Travel, Dashboard) -### Flowk UI (Local Dashboard) -Inspect run histories, trace logic changes, and debug global state permutations explicitly via our Streamlit UI. +## **📊 Observability Dashboard:** Track sessions, visualize topology, and perform step-by-step state diffing through the local production-grade dashboard (`flowk ui`). +Spin up the native **Production-Grade Dashboard** to review these checkpoints visually with interactive graph topology and state diffing: ```bash flowk ui -# Operates at http://localhost:8501 pulling from SQLite local checkpoints +# Launches at http://localhost:8502 ``` +**Features:** +- **Interactive Graph Visualization**: Real-time SVG rendering of your graph topology and execution paths. +- **State Diff Engine**: Side-by-side comparison of global state snapshots between chaque node execution. +- **Persistent Trace Storage**: Backed by the `StorageRegistry` (SQLite/Redis), allowing you to browse historical sessions and runs. +- **Multi-Agent Tracing**: Detailed visibility into nested sub-graph executions. + ### CLI Operations Render beautiful flow charts to your developer terminal before production to visually confirm structural intent: ```python diff --git a/README.md b/README.md index 2b563f2..3b3428f 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ g.arun(None, session_id="user_john") --- +--- + ## 📊 Observability Dashboard & Persistence Flowk effortlessly saves run-histories exactly as they mutate across node transactions. @@ -169,11 +171,17 @@ g = Graph(checkpoint_db="local_traces.db") # SQLite Storage g = Graph(checkpoint_db="redis://localhost:6379/0") # Redis ``` -Spin up the native **Streamlit Time-Machine Dashboard** to review these checkpoints visually without relying on generic SaaS providers: +Spin up the native **Production-Grade Dashboard** (v2) to review these checkpoints visually with interactive graph topology and state diffing: ```bash flowk ui +# Launches at http://localhost:8502 ``` +The new dashboard provides: +- **Interactive SVG Graphs**: Visualize your workflow logic and execution paths. +- **State Diff Engine**: Compare state snapshots step-by-step. +- **Session History**: Browse and resume historical agent runs from SQLite/Redis. + --- ## 📦 Multi-Agent Composition diff --git a/flowk/cli.py b/flowk/cli.py index 08e73c1..94bca0b 100644 --- a/flowk/cli.py +++ b/flowk/cli.py @@ -10,16 +10,24 @@ def main(): command = sys.argv[1] if command == "ui": - # Launch streamlit dashboard + # Launch production-grade v2 dashboard try: - import streamlit # pyre-ignore + from fastapi import FastAPI + import uvicorn except ImportError: - print("Streamlit is not installed. Run 'pip install flowk[ui]' to enable the dashboard.") + print("FastAPI and Uvicorn are required for the v2 dashboard. Run 'pip install flowk[api]'") sys.exit(1) - ui_path = os.path.join(os.path.dirname(__file__), "ui", "dashboard.py") - print("🌊 Starting Flowk Observability Dashboard...") - subprocess.run(["streamlit", "run", ui_path]) + from flowk import Graph + from flowk.server import create_app + + # Create a dummy graph to host the dashboard if none is provided + # It will still serve the /ui/sessions etc. from the global memory store + dummy_graph = Graph(checkpoint_db="flowk_memory.db") + app = create_app(dummy_graph) + + print("🔥 Starting Flowk Production Dashboard (v2)...") + uvicorn.run(app, host="127.0.0.1", port=8502) else: print(f"Unknown command: {command}") diff --git a/flowk/executor.py b/flowk/executor.py index 6f00cd6..25c4eee 100644 --- a/flowk/executor.py +++ b/flowk/executor.py @@ -93,7 +93,7 @@ def execute( PluginManager.on_node_end(run_id, node, output, state) # pyre-ignore if status == "error": - StorageRegistry.save_trace(run_id, execution_trace) # pyre-ignore + StorageRegistry.save_trace(run_id, execution_trace, session_id=session_id) # pyre-ignore if session_id: MemoryStore.save_state(session_id, state.to_dict()) # pyre-ignore raise RuntimeError(f"Execution failed at node '{current_node_name}': {error}") @@ -120,7 +120,7 @@ def execute( ) total_duration = time.time() - start_time - StorageRegistry.save_trace(run_id, execution_trace) # pyre-ignore + StorageRegistry.save_trace(run_id, execution_trace, session_id=session_id) # pyre-ignore MetricsRegistry.record_run(total_duration) # pyre-ignore PluginManager.on_run_end(run_id, self.graph, current_input) # pyre-ignore @@ -240,7 +240,7 @@ async def astream( active_nodes = next_layer total_duration = time.time() - start_time - StorageRegistry.save_trace(run_id, execution_trace) # pyre-ignore + StorageRegistry.save_trace(run_id, execution_trace, session_id=session_id) # pyre-ignore MetricsRegistry.record_run(total_duration) # pyre-ignore PluginManager.on_run_end(run_id, self.graph, None) # pyre-ignore diff --git a/flowk/graph.py b/flowk/graph.py index e116224..17c326d 100644 --- a/flowk/graph.py +++ b/flowk/graph.py @@ -29,7 +29,9 @@ def __init__( if checkpoint_db: from flowk.memory import MemoryStore # pyre-ignore + from flowk.storage import StorageRegistry MemoryStore.configure(checkpoint_db) + StorageRegistry.configure(checkpoint_db) # ------------------------------------------------------------------ # Node registration @@ -167,6 +169,19 @@ def compile(self, interrupt_before: Optional[List[str]] = None) -> "Graph": raise RuntimeError( f"Edge compilation error: Target node '{target}' from '{source}' does not exist." ) + + # Persist topology for UI observability + from flowk.storage import StorageRegistry + nodes = [{"id": n.name, "name": n.name, "type": "agent" if "agent" in n.name.lower() else "node"} for n in self.nodes.values()] + edges = [] + for src, targets in self.edges.items(): + for tgt in targets: + edges.append({"source": src, "target": tgt, "type": "flow"}) + for src, mapping in self.routes.items(): + for val, tgt in mapping.items(): + edges.append({"source": src, "target": tgt, "type": "route", "label": str(val)}) + + StorageRegistry.save_graph("default", {"nodes": nodes, "edges": edges}) self.interrupt_before = interrupt_before if interrupt_before is not None else [] # pyre-ignore self.compiled = True diff --git a/flowk/memory.py b/flowk/memory.py index 05ef1eb..5e2ab1b 100644 --- a/flowk/memory.py +++ b/flowk/memory.py @@ -145,6 +145,35 @@ def clear(cls, session_id: Optional[str] = None) -> None: else: cls._sessions.clear() + # ------------------------------------------------------------------ + # UI / Observability + # ------------------------------------------------------------------ + + @classmethod + def list_sessions(cls) -> Dict[str, dict]: + """Return all persisted sessions and their latest states.""" + redis_client: Any = cls._redis_client + if redis_client is not None: + keys = redis_client.keys("flowk:session:*") + sessions = {} + for key in keys: + s_id = key.decode("utf-8").replace("flowk:session:", "") + raw = redis_client.get(key) + sessions[s_id] = json.loads(raw) if raw else {} + return sessions + + db_path: Optional[str] = cls._db_path + if db_path is not None: + sessions = {} + with sqlite3.connect(db_path) as conn: + rows = conn.execute("SELECT id, state FROM sessions").fetchall() + for row in rows: + sessions[row[0]] = json.loads(row[1]) + return sessions + + # In-memory fallback + return cls._sessions.copy() + # ------------------------------------------------------------------ # Convenience # ------------------------------------------------------------------ diff --git a/flowk/server.py b/flowk/server.py index 51212e7..8d1dad8 100644 --- a/flowk/server.py +++ b/flowk/server.py @@ -3,8 +3,10 @@ try: from fastapi import FastAPI, Request, HTTPException # pyre-ignore - from fastapi.responses import StreamingResponse # pyre-ignore + from fastapi.responses import StreamingResponse, FileResponse # pyre-ignore + from fastapi.staticfiles import StaticFiles # pyre-ignore import uvicorn # pyre-ignore + import os _fastapi_available = True except ImportError: _fastapi_available = False @@ -27,6 +29,18 @@ def create_app(graph: Any) -> Any: version="0.3.0", ) + # ------------------------------------------------------------------ + # Static UI Assets + # ------------------------------------------------------------------ + ui_dist = os.path.join(os.path.dirname(__file__), "ui", "v2", "dist") + + if os.path.exists(ui_dist): + app.mount("/assets", StaticFiles(directory=os.path.join(ui_dist, "assets")), name="assets") + + @app.get("/") + async def serve_ui(): + return FileResponse(os.path.join(ui_dist, "index.html")) + @app.post("/invoke") # pyre-ignore async def invoke(request: Request) -> dict: # pyre-ignore """ @@ -72,4 +86,56 @@ async def event_generator(): return StreamingResponse(event_generator(), media_type="text/event-stream") # pyre-ignore + # ------------------------------------------------------------------ + # UI / Observability Endpoints + # ------------------------------------------------------------------ + + @app.get("/ui/sessions") + async def get_sessions(): + """List all active sessions and their latest state snapshots.""" + from flowk.memory import MemoryStore + return MemoryStore.list_sessions() + + @app.get("/ui/runs") + async def get_runs(session_id: Optional[str] = None): + """List all recorded execution run IDs, optional filter by session.""" + from flowk.storage import StorageRegistry + return StorageRegistry.list_runs(session_id=session_id) + + @app.get("/ui/session/{session_id}/runs") + async def get_session_runs(session_id: str): + """List all run IDs for a specific session.""" + from flowk.storage import StorageRegistry + return StorageRegistry.list_runs(session_id=session_id) + + @app.get("/ui/run/{run_id}") + async def get_run_trace(run_id: str): + """Retrieve the full step-by-step execution trace for a specific run.""" + from flowk.storage import StorageRegistry + try: + return StorageRegistry.get_trace(run_id) + except Exception as e: + raise HTTPException(status_code=404, detail=str(e)) + + @app.get("/ui/graph") + async def get_graph(): + """Export graph topology for UI visualization.""" + from flowk.storage import StorageRegistry + + # Try finding persisted graph first if local graph is dummy + if not graph.nodes: + persisted = StorageRegistry.get_graph("default") + if persisted: + return persisted + + nodes = [{"id": n.name, "name": n.name, "type": "agent" if "agent" in n.name.lower() else "node"} for n in graph.nodes.values()] + edges = [] + for src, targets in graph.edges.items(): + for tgt in targets: + edges.append({"source": src, "target": tgt, "type": "flow"}) + for src, mapping in graph.routes.items(): + for val, tgt in mapping.items(): + edges.append({"source": src, "target": tgt, "type": "route", "label": str(val)}) + return {"nodes": nodes, "edges": edges} + return app diff --git a/flowk/storage.py b/flowk/storage.py index 83d527c..4cc91d4 100644 --- a/flowk/storage.py +++ b/flowk/storage.py @@ -1,25 +1,122 @@ -from typing import Dict, List, Any +import json +import sqlite3 +from typing import Dict, List, Any, Optional from flowk.exceptions import ReplayError class StorageRegistry: - """In-memory trace storage for Run observability and Time Travel features.""" + """Persistent trace storage for Run observability and Time Travel features.""" _traces: Dict[str, List[Dict[str, Any]]] = {} + _db_path: Optional[str] = None + _redis_client: Optional[Any] = None @classmethod - def save_trace(cls, run_id: str, trace: List[Dict[str, Any]]): + def configure(cls, connection_string: Optional[str] = None) -> None: + """Set up the persistence backend for traces.""" + if not connection_string: + return + + if connection_string.startswith("redis://"): + import redis + cls._redis_client = redis.from_url(connection_string) + else: + cls._db_path = connection_string + with sqlite3.connect(connection_string) as conn: + conn.execute( + "CREATE TABLE IF NOT EXISTS runs " + "(id TEXT PRIMARY KEY, session_id TEXT, trace TEXT)" + ) + conn.execute( + "CREATE TABLE IF NOT EXISTS graphs " + "(id TEXT PRIMARY KEY, data TEXT)" + ) + + @classmethod + def save_trace(cls, run_id: str, trace: List[Dict[str, Any]], session_id: Optional[str] = None): + """Persist execution trace associated with a run (and optionally a session).""" + redis_client: Any = cls._redis_client + if redis_client is not None: + data = {"session_id": session_id, "trace": trace} + redis_client.set(f"flowk:run:{run_id}", json.dumps(data)) + if session_id: + redis_client.sadd(f"flowk:session_runs:{session_id}", run_id) + return + + db_path: Optional[str] = cls._db_path + if db_path is not None: + with sqlite3.connect(db_path) as conn: + conn.execute( + "INSERT OR REPLACE INTO runs (id, session_id, trace) VALUES (?, ?, ?)", + (run_id, session_id, json.dumps(trace)), + ) + return + cls._traces[run_id] = trace @classmethod def get_trace(cls, run_id: str) -> List[Dict[str, Any]]: + """Retrieve trace for a specific execution run.""" + redis_client: Any = cls._redis_client + if redis_client is not None: + raw = redis_client.get(f"flowk:run:{run_id}") + return json.loads(raw)["trace"] if raw else None + + db_path: Optional[str] = cls._db_path + if db_path is not None: + with sqlite3.connect(db_path) as conn: + row = conn.execute("SELECT trace FROM runs WHERE id = ?", (run_id,)).fetchone() + return json.loads(row[0]) if row else None + trace = cls._traces.get(run_id) if trace is None: raise ReplayError(f"No run trace found for ID: {run_id}") return trace @classmethod - def list_runs(cls) -> List[str]: + def list_runs(cls, session_id: Optional[str] = None) -> List[str]: + """List all run IDs, optionally filtered by session_id.""" + redis_client: Any = cls._redis_client + if redis_client is not None: + if session_id: + keys = redis_client.smembers(f"flowk:session_runs:{session_id}") + return [k.decode("utf-8") for k in keys] + keys = redis_client.keys("flowk:run:*") + return [k.decode("utf-8").replace("flowk:run:", "") for k in keys] + + db_path: Optional[str] = cls._db_path + if db_path is not None: + with sqlite3.connect(db_path) as conn: + if session_id: + rows = conn.execute("SELECT id FROM runs WHERE session_id = ?", (session_id,)).fetchall() + else: + rows = conn.execute("SELECT id FROM runs").fetchall() + return [row[0] for row in rows] + return list(cls._traces.keys()) + @classmethod + def save_graph(cls, graph_id: str, data: Dict[str, Any]): + """Persist graph topology (nodes and edges).""" + db_path: Optional[str] = cls._db_path + if db_path is not None: + with sqlite3.connect(db_path) as conn: + conn.execute( + "INSERT OR REPLACE INTO graphs (id, data) VALUES (?, ?)", + (graph_id, json.dumps(data)), + ) + return + cls._traces[f"graph:{graph_id}"] = [data] # fallback + + @classmethod + def get_graph(cls, graph_id: str) -> Optional[Dict[str, Any]]: + """Retrieve persisted graph topology.""" + db_path: Optional[str] = cls._db_path + if db_path is not None: + with sqlite3.connect(db_path) as conn: + row = conn.execute("SELECT data FROM graphs WHERE id = ?", (graph_id,)).fetchone() + return json.loads(row[0]) if row else None + return None + @classmethod def clear(cls): + """Clear all in-memory traces.""" cls._traces.clear() diff --git a/flowk/ui/v2/.gitignore b/flowk/ui/v2/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/flowk/ui/v2/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/flowk/ui/v2/index.html b/flowk/ui/v2/index.html new file mode 100644 index 0000000..88253ba --- /dev/null +++ b/flowk/ui/v2/index.html @@ -0,0 +1,75 @@ + + + + + + + Flowk | Production Dashboard + + + + + +
+ + +
+
+
+

No Session Selected

+

Select a session to view trace details

+
+
+
+ 0 + Nodes +
+
+ 0ms + Duration +
+
+
+ +
+ + + + + + + + + +
+ +
+
Execution Trace
+
+ +
+ +
State Diff
+
+
Select a step to see state changes
+
+
+
+
+ + + + diff --git a/flowk/ui/v2/package-lock.json b/flowk/ui/v2/package-lock.json new file mode 100644 index 0000000..5efb453 --- /dev/null +++ b/flowk/ui/v2/package-lock.json @@ -0,0 +1,868 @@ +{ + "name": "v2", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "v2", + "version": "0.0.0", + "devDependencies": { + "vite": "^8.0.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/flowk/ui/v2/package.json b/flowk/ui/v2/package.json new file mode 100644 index 0000000..8ac6453 --- /dev/null +++ b/flowk/ui/v2/package.json @@ -0,0 +1,14 @@ +{ + "name": "v2", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^8.0.1" + } +} diff --git a/flowk/ui/v2/public/favicon.svg b/flowk/ui/v2/public/favicon.svg new file mode 100644 index 0000000..0906f9c --- /dev/null +++ b/flowk/ui/v2/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/flowk/ui/v2/public/icons.svg b/flowk/ui/v2/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/flowk/ui/v2/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flowk/ui/v2/src/assets/hero.png b/flowk/ui/v2/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/flowk/ui/v2/src/assets/hero.png differ diff --git a/flowk/ui/v2/src/assets/javascript.svg b/flowk/ui/v2/src/assets/javascript.svg new file mode 100644 index 0000000..bebbfc1 --- /dev/null +++ b/flowk/ui/v2/src/assets/javascript.svg @@ -0,0 +1 @@ + diff --git a/flowk/ui/v2/src/assets/vite.svg b/flowk/ui/v2/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/flowk/ui/v2/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/flowk/ui/v2/src/counter.js b/flowk/ui/v2/src/counter.js new file mode 100644 index 0000000..12bf115 --- /dev/null +++ b/flowk/ui/v2/src/counter.js @@ -0,0 +1,9 @@ +export function setupCounter(element) { + let counter = 0 + const setCounter = (count) => { + counter = count + element.innerHTML = `Count is ${counter}` + } + element.addEventListener('click', () => setCounter(counter + 1)) + setCounter(0) +} diff --git a/flowk/ui/v2/src/main.js b/flowk/ui/v2/src/main.js new file mode 100644 index 0000000..73be827 --- /dev/null +++ b/flowk/ui/v2/src/main.js @@ -0,0 +1,216 @@ +import './style.css'; + +/** @type {import('flowk').GraphInfo} */ +let graphData = { nodes: [], edges: [], entrypoint: null }; +let sessions = {}; +let currentSessionId = null; +let currentRunTrace = []; + +// DOM Elements +const sessionList = document.getElementById('session-list'); +const nodesLayer = document.getElementById('nodes-layer'); +const edgesLayer = document.getElementById('edges-layer'); +const traceList = document.getElementById('trace-list'); +const diffContent = document.getElementById('diff-content'); +const currentSessionTitle = document.getElementById('current-session-id'); +const nodeCountStat = document.getElementById('node-count'); +const runTimeStat = document.getElementById('run-time'); + +// ------------------------------------------------------------------ +// API Interaction +// ------------------------------------------------------------------ + +async function fetchData(endpoint) { + try { + const response = await fetch(endpoint); + return await response.json(); + } catch (err) { + console.error(`Error fetching ${endpoint}:`, err); + return null; + } +} + +async function init() { + graphData = await fetchData('/ui/graph') || graphData; + sessions = await fetchData('/ui/sessions') || {}; + + renderGraph(); + renderSessionList(); +} + +// ------------------------------------------------------------------ +// Rendering: Sessions +// ------------------------------------------------------------------ + +function renderSessionList() { + sessionList.innerHTML = ''; + Object.keys(sessions).forEach(id => { + const item = document.createElement('div'); + item.className = `session-item ${id === currentSessionId ? 'active' : ''}`; + item.onclick = () => selectSession(id); + + item.innerHTML = ` + ${id} + Nodes: ${Object.keys(sessions[id]).length} keys in state + `; + sessionList.appendChild(item); + }); +} + +async function selectSession(id) { + currentSessionId = id; + currentSessionTitle.innerText = id; + renderSessionList(); + + // Fetch runs specifically for THIS session + const runs = await fetchData(`/ui/session/${id}/runs`); + if (runs && runs.length > 0) { + // Picking the latest run for this session + const runId = runs[runs.length - 1]; + const trace = await fetchData(`/ui/run/${runId}`); + if (trace) { + currentRunTrace = trace; + renderTrace(); + updateStats(); + + // Auto-select latest step + selectStep(currentRunTrace.length - 1); + } + } else { + currentRunTrace = []; + renderTrace(); + updateStats(); + diffContent.innerHTML = '
No traces found for this session
'; + } +} + +function updateStats() { + nodeCountStat.innerText = graphData.nodes.length; + if (currentRunTrace.length > 0) { + const totalDuration = currentRunTrace.reduce((acc, step) => acc + (step.duration || 0), 0); + runTimeStat.innerText = (totalDuration * 1000).toFixed(0) + 'ms'; + } +} + +// ------------------------------------------------------------------ +// Rendering: Graph (Simple Circular Layout) +// ------------------------------------------------------------------ + +function renderGraph() { + nodesLayer.innerHTML = ''; + edgesLayer.innerHTML = ''; + + const width = document.getElementById('graph-container').clientWidth; + const height = document.getElementById('graph-container').clientHeight; + const centerX = width / 2; + const centerY = height / 2; + const radius = Math.min(width, height) / 3; + + const nodePositions = {}; + + graphData.nodes.forEach((node, i) => { + const angle = (i / graphData.nodes.length) * 2 * Math.PI; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + nodePositions[node.id] = { x, y }; + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('class', 'node'); + g.setAttribute('id', `node-${node.id}`); + + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', x); + circle.setAttribute('cy', y); + circle.setAttribute('r', node.type === 'llm_router' ? 40 : 35); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', x); + text.setAttribute('y', y + 5); + text.setAttribute('text-anchor', 'middle'); + text.textContent = node.id.length > 10 ? node.id.substring(0, 8) + '..' : node.id; + + g.appendChild(circle); + g.appendChild(text); + nodesLayer.appendChild(g); + }); + + graphData.edges.forEach(edge => { + const start = nodePositions[edge.source]; + const end = nodePositions[edge.target]; + if (!start || !end) return; + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const d = `M ${start.x} ${start.y} L ${end.x} ${end.y}`; + line.setAttribute('d', d); + line.setAttribute('class', 'edge'); + line.setAttribute('id', `edge-${edge.source}-${edge.target}`); + edgesLayer.appendChild(line); + }); +} + +// ------------------------------------------------------------------ +// Rendering: Trace & Diffs +// ------------------------------------------------------------------ + +function renderTrace() { + traceList.innerHTML = ''; + currentRunTrace.forEach((step, i) => { + const item = document.createElement('div'); + item.className = 'trace-step'; + item.onclick = () => selectStep(i); + + item.innerHTML = ` +
+ ${step.node} + ${step.status} +
+
+ Duration: ${(step.duration * 1000).toFixed(1)}ms +
+ `; + traceList.appendChild(item); + }); +} + +function selectStep(index) { + // Highlight node in graph + document.querySelectorAll('.node circle').forEach(c => c.style.stroke = 'var(--panel-border)'); + const step = currentRunTrace[index]; + const nodeEl = document.getElementById(`node-${step.node}`); + if (nodeEl) { + nodeEl.querySelector('circle').style.stroke = 'var(--accent)'; + } + + // Build Diff + const prevStep = index > 0 ? currentRunTrace[index - 1] : null; + const prevState = prevStep ? prevStep.state_snapshot : {}; + const currState = step.state_snapshot; + + renderDiff(prevState, currState); +} + +function renderDiff(oldState, newState) { + let diffHtml = ''; + const allKeys = new Set([...Object.keys(oldState), ...Object.keys(newState)]); + + allKeys.forEach(key => { + const oldVal = JSON.stringify(oldState[key]); + const newVal = JSON.stringify(newState[key]); + + if (!(key in oldState)) { + diffHtml += `
+ ${key}: ${newVal}
`; + } else if (!(key in newState)) { + diffHtml += `
- ${key}: ${oldVal}
`; + } else if (oldVal !== newVal) { + diffHtml += `
~ ${key}: ${oldVal} -> ${newVal}
`; + } else { + diffHtml += `
${key}: ${newVal}
`; + } + }); + + diffContent.innerHTML = diffHtml || '
No state changes
'; +} + +// Start +init(); +window.onresize = renderGraph; diff --git a/flowk/ui/v2/src/style.css b/flowk/ui/v2/src/style.css new file mode 100644 index 0000000..426bf81 --- /dev/null +++ b/flowk/ui/v2/src/style.css @@ -0,0 +1,290 @@ +:root { + --bg: #0a0a0c; + --panel-bg: rgba(255, 255, 255, 0.03); + --panel-border: rgba(255, 255, 255, 0.1); + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --accent: #3b82f6; + --accent-glow: rgba(59, 130, 246, 0.3); + --success: #10b981; + --error: #ef4444; + --node-bg: #1e293b; + --sans: 'Inter', system-ui, -apple-system, sans-serif; + --mono: 'JetBrains Mono', 'Fira Code', monospace; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg); + color: var(--text-primary); + font-family: var(--sans); + height: 100vh; + margin: 0; + overflow: hidden; +} + +.app-container { + display: flex; + width: 100%; + height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 320px; + background: var(--panel-bg); + border-right: 1px solid var(--panel-border); + display: flex; + flex-direction: column; + padding: 2rem 1.5rem; + backdrop-filter: blur(20px); + flex-shrink: 0; +} + +.logo-emoji { + font-size: 2.5rem; + margin-right: 0.5rem; + filter: drop-shadow(0 0 10px rgba(0, 195, 255, 0.4)); +} + +.logo h1 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 2rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-primary); +} + +.logo span { + color: var(--accent); +} + +.session-list { + flex: 1; + overflow-y: auto; +} + +.session-item { + padding: 0.75rem 1rem; + border-radius: 0.5rem; + margin-bottom: 0.5rem; + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; +} + +.session-item:hover { + background: rgba(255, 255, 255, 0.05); +} + +.session-item.active { + background: var(--accent-glow); + border-color: var(--accent); +} + +.session-id { + font-weight: 600; + font-size: 0.9rem; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.session-meta { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* Main Content */ +.dashboard-main { + flex: 1; + display: grid; + grid-template-rows: auto 1fr; + grid-template-columns: 1fr 400px; + height: 100vh; + background: #000; +} + +header { + grid-column: 1 / -1; + padding: 1rem 2rem; + background: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid var(--panel-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +header h2 { + font-size: 1.25rem; + font-weight: 600; +} + +.stats { + display: flex; + gap: 2rem; +} + +.stat-item { + text-align: center; +} + +.stat-value { + display: block; + font-weight: 700; + color: var(--accent); +} + +.stat-label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +/* Graph Section */ +#graph-container { + position: relative; + overflow: hidden; + background: radial-gradient(circle at center, #111 0%, #0a0a0c 100%); +} + +#graph-svg { + width: 100%; + height: 100%; +} + +.node { + cursor: pointer; +} + +.node circle { + fill: var(--node-bg); + stroke: var(--panel-border); + stroke-width: 2; + transition: all 0.3s; +} + +.node:hover circle { + stroke: var(--accent); + filter: drop-shadow(0 0 8px var(--accent-glow)); +} + +.node text { + fill: var(--text-primary); + font-size: 12px; + font-weight: 500; + pointer-events: none; +} + +.edge { + stroke: var(--panel-border); + stroke-width: 1.5; + fill: none; + marker-end: url(#arrowhead); +} + +.edge.active { + stroke: var(--accent); + stroke-width: 2; +} + +/* Tracing Section */ +#trace-panel { + background: var(--panel-bg); + border-left: 1px solid var(--panel-border); + display: flex; + flex-direction: column; + backdrop-filter: blur(8px); +} + +.trace-title { + padding: 1.5rem; + border-bottom: 1px solid var(--panel-border); + font-weight: 600; +} + +.trace-list { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +.trace-step { + padding: 1rem; + background: rgba(255, 255, 255, 0.02); + border-radius: 0.5rem; + margin-bottom: 1rem; + border: 1px solid var(--panel-border); + cursor: pointer; + transition: all 0.2s; +} + +.trace-step:hover { + background: rgba(255, 255, 255, 0.04); +} + +.trace-step.active { + border-color: var(--accent); +} + +.step-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.step-node { + font-weight: 600; + color: var(--accent); +} + +.step-status { + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 999px; + background: rgba(16, 185, 129, 0.1); + color: var(--success); +} + +.step-status.error { + background: rgba(239, 68, 68, 0.1); + color: var(--error); +} + +/* State Diff Section */ +#diff-viewer { + padding: 1.5rem; + border-top: 1px solid var(--panel-border); + background: rgba(0, 0, 0, 0.3); + font-family: var(--mono); + font-size: 0.8rem; + max-height: 300px; + overflow-y: auto; +} + +.diff-added { color: var(--success); } +.diff-removed { color: var(--error); text-decoration: line-through; } +.diff-changed { color: var(--accent); } + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--panel-border); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} diff --git a/flowk_memory.db b/flowk_memory.db index b48ea6d..3a552f6 100644 Binary files a/flowk_memory.db and b/flowk_memory.db differ