diff --git a/engine/Cargo.lock b/engine/Cargo.lock index ced6b963fb..17c92e73d1 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -115,6 +115,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.5.2" @@ -1213,6 +1219,7 @@ dependencies = [ "baml-types", "baml-vm", "base64 0.22.1", + "blake3", "bytes", "cfg-if", "chrono", @@ -1537,6 +1544,19 @@ dependencies = [ "serde", ] +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -2027,6 +2047,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "constptr" version = "0.2.0" @@ -2496,7 +2522,7 @@ dependencies = [ "filetime", "indexmap 2.9.0", "internal-baml-core", - "pathdiff 0.1.0", + "pathdiff 0.2.3", "serde_json", "sugar_path", "walkdir", diff --git a/engine/baml-rpc/src/ui/mod.rs b/engine/baml-rpc/src/ui/mod.rs index d503d0fd4f..89e82e0f9b 100644 --- a/engine/baml-rpc/src/ui/mod.rs +++ b/engine/baml-rpc/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod ui_baml_src; +pub mod ui_blobs; pub mod ui_control_plane_orgs; pub mod ui_control_plane_projects; pub mod ui_dashboard; diff --git a/engine/baml-rpc/src/ui/ui_baml_src.rs b/engine/baml-rpc/src/ui/ui_baml_src.rs index 02b986d834..4bc697857e 100644 --- a/engine/baml-rpc/src/ui/ui_baml_src.rs +++ b/engine/baml-rpc/src/ui/ui_baml_src.rs @@ -32,7 +32,7 @@ pub struct BamlSourceCode { pub struct AstNodeDefinition { pub project_id: String, pub ast_node_id: String, - #[ts(type = "any")] + #[ts(type = "unknown")] pub ast_node_definition: serde_json::Value, pub flattened_dependencies_ast_nodes: Vec, pub baml_src_node_ids: Vec, diff --git a/engine/baml-rpc/src/ui/ui_blobs.rs b/engine/baml-rpc/src/ui/ui_blobs.rs new file mode 100644 index 0000000000..da4c13be8d --- /dev/null +++ b/engine/baml-rpc/src/ui/ui_blobs.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::ProjectId; + +/// Request to fetch blob content +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct GetBlobRequest { + /// The 64-character BLAKE3 hash of the blob + pub blob_hash: String, + /// The project ID that owns this blob + #[ts(type = "string")] + pub project_id: ProjectId, + /// Response format: 'raw' returns binary, 'json' returns base64-encoded content + #[serde(default)] + pub format: BlobFormat, +} + +/// Response format for blob content +#[derive(Debug, Serialize, Deserialize, TS, Default)] +#[ts(export)] +#[serde(rename_all = "lowercase")] +pub enum BlobFormat { + /// Return raw binary content with appropriate Content-Type header + #[default] + Raw, + /// Return JSON with base64-encoded content + Json, +} + +/// JSON response format for blob content +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct GetBlobResponse { + /// The blob hash + pub blob_hash: String, + /// The content type of the decoded blob (e.g., "image/png") + pub content_type: String, + /// Base64-encoded content (exact same as used in LLM request) + pub base64_content: String, +} \ No newline at end of file diff --git a/engine/baml-rpc/src/ui/ui_types.rs b/engine/baml-rpc/src/ui/ui_types.rs index fcb4cc4660..f395387139 100644 --- a/engine/baml-rpc/src/ui/ui_types.rs +++ b/engine/baml-rpc/src/ui/ui_types.rs @@ -94,7 +94,7 @@ pub struct UiFunctionCall { #[ts(optional)] pub function_id: Option, - #[ts(type = "Record")] + #[ts(type = "Record")] pub tags: serde_json::Map, #[serde(rename = "start_epoch_ms")] @@ -105,7 +105,7 @@ pub struct UiFunctionCall { pub end_time: Option, pub status: String, - #[ts(type = "any")] + #[ts(type = "unknown")] pub baml_options: serde_json::Value, pub inputs: Vec, #[ts(as = "Option")] @@ -130,9 +130,9 @@ pub struct UiLlmRequest { pub client_name: String, pub client_provider: String, // TODO: type this out properly. - #[ts(type = "any")] + #[ts(type = "unknown")] pub params: serde_json::Value, - #[ts(type = "any")] + #[ts(type = "unknown")] pub prompt: serde_json::Value, } @@ -186,7 +186,7 @@ pub struct UiHttpRequest { pub start_time: EpochMsTimestamp, pub url: String, pub method: String, - #[ts(type = "Record | undefined")] + #[ts(type = "Record | undefined")] pub headers: Option>, pub body: String, } @@ -197,7 +197,7 @@ pub struct UiHttpResponse { #[ts(type = "number")] pub end_time: EpochMsTimestamp, pub status_code: u16, - #[ts(type = "Record")] + #[ts(type = "Record")] pub headers: HashMap, pub body: String, } diff --git a/engine/baml-runtime/Cargo.toml b/engine/baml-runtime/Cargo.toml index 13793944ad..0c4a478eb1 100644 --- a/engine/baml-runtime/Cargo.toml +++ b/engine/baml-runtime/Cargo.toml @@ -125,6 +125,7 @@ once_cell.workspace = true time.workspace = true tempfile = "3.19.0" sha2 = "0.10.9" +blake3 = "1.8.2" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/engine/baml-runtime/src/tracingv2/publisher/rpc_converters/blob_storage.rs b/engine/baml-runtime/src/tracingv2/publisher/rpc_converters/blob_storage.rs index 68714f090a..4f0c3ea63b 100644 --- a/engine/baml-runtime/src/tracingv2/publisher/rpc_converters/blob_storage.rs +++ b/engine/baml-runtime/src/tracingv2/publisher/rpc_converters/blob_storage.rs @@ -7,7 +7,7 @@ use std::{ use baml_rpc::runtime_api::baml_value::{BamlValue, MediaValue, ValueContent}; use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; +use blake3; use tokio::sync::mpsc; use crate::tracingv2::publisher::publisher::BlobUploaderMessage; @@ -82,11 +82,10 @@ impl BlobRefCache { } } - /// Generate a hash for a blob + /// Generate a hash for a blob using BLAKE3 pub fn hash_blob(content: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(content); - format!("{:x}", hasher.finalize()) + let hash = blake3::hash(content); + hash.to_hex().to_string() } /// Store a blob (as base64 string) and associate it with a function_call_id diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8354fa9740..6460b9e937 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 6.2.6 ts-jest: specifier: ^29.1.1 - version: 29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.2)(@jest/types@30.0.1)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.7)(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@30.0.2)(@jest/types@30.0.1)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.7)(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.12.7)(@types/node@20.19.1)(typescript@5.8.3) @@ -329,7 +329,7 @@ importers: version: 0.0.2 ts-jest: specifier: 29.1.2 - version: 29.1.2(@babel/core@7.27.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.2)(jest@29.7.0(@types/node@20.17.17)(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.15))(@types/node@20.17.17)(typescript@5.4.2)))(typescript@5.4.2) + version: 29.1.2(@babel/core@7.28.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.2)(jest@29.7.0(@types/node@20.17.17)(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.15))(@types/node@20.17.17)(typescript@5.4.2)))(typescript@5.4.2) ts-node: specifier: 10.9.2 version: 10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.15))(@types/node@20.17.17)(typescript@5.4.2) @@ -421,22 +421,22 @@ importers: version: 15.5.13 css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) + version: 7.1.2(webpack@5.99.9) jotai-devtools: specifier: 0.12.0 version: 0.12.0(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(redux@5.0.1)(storybook@8.6.14) style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) + version: 4.0.0(webpack@5.99.9) ts-loader: specifier: ^9.4.0 - version: 9.5.2(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) + version: 9.5.2(typescript@5.8.3)(webpack@5.99.9) typescript: specifier: ^5.0.0 version: 5.8.3 webpack: specifier: ^5.88.0 - version: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + version: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) webpack-cli: specifier: ^5.1.4 version: 5.1.4(webpack@5.99.9) @@ -995,6 +995,9 @@ importers: codemirror-copilot: specifier: 0.0.7 version: 0.0.7(@codemirror/state@6.5.2)(@codemirror/view@6.37.2) + d3-hierarchy: + specifier: ^3.1.2 + version: 3.1.2 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -1068,6 +1071,9 @@ importers: specifier: 3.25.66 version: 3.25.66 devDependencies: + '@types/d3-hierarchy': + specifier: ^3.1.7 + version: 3.1.7 '@types/hast': specifier: 3.0.4 version: 3.0.4 @@ -10731,7 +10737,7 @@ packages: react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: - '@types/react': 19.0.8 + '@types/react': '>=18' react: '>=18' react-markdown@9.1.0: @@ -19139,19 +19145,19 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) '@xtuc/ieee754@1.2.0': {} @@ -20117,7 +20123,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@7.1.2(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)): + css-loader@7.1.2(webpack@5.99.9): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -20128,7 +20134,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) css-select@5.1.0: dependencies: @@ -25828,9 +25834,9 @@ snapshots: dependencies: boundary: 2.0.0 - style-loader@4.0.0(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)): + style-loader@4.0.0(webpack@5.99.9): dependencies: - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) style-mod@4.1.2: {} @@ -25999,26 +26005,25 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(@swc/core@1.12.7)(esbuild@0.25.2)(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.14(@swc/core@1.12.7)(webpack@5.99.9(@swc/core@1.12.7)): dependencies: '@jridgewell/trace-mapping': 0.3.29 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7) optionalDependencies: '@swc/core': 1.12.7 - esbuild: 0.25.2 - terser-webpack-plugin@5.3.14(@swc/core@1.12.7)(webpack@5.99.9(@swc/core@1.12.7)): + terser-webpack-plugin@5.3.14(@swc/core@1.12.7)(webpack@5.99.9): dependencies: '@jridgewell/trace-mapping': 0.3.29 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.99.9(@swc/core@1.12.7) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.12.7 @@ -26135,7 +26140,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.1.2(@babel/core@7.27.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.2)(jest@29.7.0(@types/node@20.17.17)(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.15))(@types/node@20.17.17)(typescript@5.4.2)))(typescript@5.4.2): + ts-jest@29.1.2(@babel/core@7.28.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.2)(jest@29.7.0(@types/node@20.17.17)(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.15))(@types/node@20.17.17)(typescript@5.4.2)))(typescript@5.4.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -26148,12 +26153,12 @@ snapshots: typescript: 5.4.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.27.7 + '@babel/core': 7.28.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.27.7) + babel-jest: 29.7.0(@babel/core@7.28.0) esbuild: 0.25.2 - ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.2)(@jest/types@30.0.1)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.7)(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@30.0.2)(@jest/types@30.0.1)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.7)(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -26167,10 +26172,10 @@ snapshots: typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.27.7 '@jest/transform': 30.0.2 '@jest/types': 30.0.1 - babel-jest: 29.7.0(@babel/core@7.28.0) + babel-jest: 29.7.0(@babel/core@7.27.7) jest-util: 30.0.2 ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.2)(@jest/types@30.0.1)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@30.0.2)(jest@29.7.0(@types/node@22.15.33)(ts-node@10.9.2(@swc/core@1.12.7)(@types/node@22.15.33)(typescript@5.8.3)))(typescript@5.8.3): @@ -26193,7 +26198,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.28.0) jest-util: 30.0.2 - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)): + ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.9): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.2 @@ -26201,7 +26206,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) ts-morph@12.0.0: dependencies: @@ -27096,9 +27101,9 @@ snapshots: webpack-cli@5.1.4(webpack@5.99.9): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.99.9) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.99.9) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.99.9) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -27107,7 +27112,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4) + webpack: 5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4) webpack-merge: 5.10.0 webpack-merge@5.10.0: @@ -27152,7 +27157,7 @@ snapshots: - esbuild - uglify-js - webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4): + webpack@5.99.9(@swc/core@1.12.7)(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -27175,7 +27180,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.12.7)(esbuild@0.25.2)(webpack@5.99.9(@swc/core@1.12.7)(esbuild@0.25.2)(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.14(@swc/core@1.12.7)(webpack@5.99.9) watchpack: 2.4.4 webpack-sources: 3.3.3 optionalDependencies: diff --git a/typescript/.claude/settings.local.json b/typescript/.claude/settings.local.json new file mode 100644 index 0000000000..700d827197 --- /dev/null +++ b/typescript/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm --filter playground-common run build)", + "Bash(pnpm add:*)", + "Bash(pnpm remove:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/typescript/apps/fiddle-web-app/app/[project_id]/_components/ProjectView.tsx b/typescript/apps/fiddle-web-app/app/[project_id]/_components/ProjectView.tsx index 105cbb161e..200b6c53d5 100644 --- a/typescript/apps/fiddle-web-app/app/[project_id]/_components/ProjectView.tsx +++ b/typescript/apps/fiddle-web-app/app/[project_id]/_components/ProjectView.tsx @@ -109,7 +109,7 @@ const ProjectViewImpl = ({ project }: { project: BAMLProject }) => { )}
- {!isMobile && } + {/* {!isMobile && } */}
{ }; export const ProjectSidebar = () => ( -
+
{/* Scrollable Body - takes remaining space */} -
+
{selectedTc ? ( <> - + {/* */} ) : ( diff --git a/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/prompt-render-wrapper.tsx b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/prompt-render-wrapper.tsx index 7506ef30c9..aa5144e5e6 100644 --- a/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/prompt-render-wrapper.tsx +++ b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/prompt-render-wrapper.tsx @@ -15,6 +15,7 @@ import { renderedPromptAtom } from './prompt-preview-content'; import { PromptPreviewCurl, curlAtom } from './prompt-preview-curl'; import { ClientGraphView } from './test-panel/components/ClientGraphView'; import { MermaidGraphView } from './test-panel/components/MermaidGraphView'; +import { WorkflowView } from './test-panel/components/WorkflowView'; // FunctionMetadata component const FunctionMetadata: React.FC = () => { @@ -87,7 +88,7 @@ export const PromptRenderWrapper = () => { const [showCopied, setShowCopied] = React.useState(false); const { open: isSidebarOpen } = useSidebar(); const isBetaEnabled = useAtomValue(betaFeatureEnabledAtom); - const [activeTab, setActiveTab] = React.useState<'preview' | 'curl' | 'client-graph' | 'mermaid-graph'>('preview'); + const [activeTab, setActiveTab] = React.useState<'preview' | 'curl' | 'client-graph' | 'mermaid-graph' | 'workflow'>('workflow'); const curl = useAtomValue(curlAtom); // Hide text when sidebar is open or on smaller screens @@ -129,10 +130,11 @@ export const PromptRenderWrapper = () => { return ( // this used to be flex flex-col h-full min-h-0 - setActiveTab(v as any)} className="flex flex-col min-h-0"> + setActiveTab(v as any)} className="flex flex-col min-h-0 h-full">
+ Workflow Preview cURL Client Graph @@ -177,6 +179,9 @@ export const PromptRenderWrapper = () => {
+ + + diff --git a/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/WorkflowView.tsx b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/WorkflowView.tsx new file mode 100644 index 0000000000..6aa6239414 --- /dev/null +++ b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/WorkflowView.tsx @@ -0,0 +1,962 @@ +'use client'; + +import React, { useState, useCallback, useEffect } from 'react'; +import { Play, PlayCircle } from 'lucide-react'; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@baml/ui/resizable'; +import { cn } from '@baml/ui/lib/utils'; +import { + ReactFlow, + ReactFlowProvider, + Node, + Edge, + MarkerType, + useNodesState, + useEdgesState, + Position, + Handle, + useReactFlow, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { hierarchy, tree } from 'd3-hierarchy'; + +// Mock trace data +interface TraceSpan { + id: string; + name: string; + depth?: number; + children?: TraceSpan[]; + input?: any; + output?: any; + latency?: number; + cost?: number; + metadata?: Record; +} + +interface TraceRun { + id: string; + timestamp: string; + status: 'success' | 'error' | 'partial'; + totalLatency: number; + totalCost: number; + trace: TraceSpan; +} + +const mockTraceRuns: TraceRun[] = [ + { + id: 'run-1', + timestamp: '2025-09-30 14:23:45', + status: 'success', + totalLatency: 1250, + totalCost: 0.0042, + trace: { + id: 'root-1', + name: 'Root', + depth: 0, + input: { query: 'What is the weather today?', context: { user_id: '123' } }, + output: { result: 'The weather is sunny with a high of 75°F', confidence: 0.95 }, + latency: 1250, + cost: 0.0042, + metadata: { model: 'gpt-4', temperature: 0.7 }, + children: [ + { + id: 'llma-1', + name: 'ExtractIntent', + depth: 1, + input: { prompt: 'Extract weather query intent', text: 'What is the weather today?' }, + output: { intent: 'weather_query', location: 'current' }, + latency: 450, + cost: 0.0015, + metadata: { model: 'gpt-3.5-turbo', tokens: 125 }, + }, + { + id: 'llmb-1', + name: 'GetWeather', + depth: 1, + input: { intent: 'weather_query', location: 'current' }, + output: { weather: 'sunny', temperature: 75, unit: 'F' }, + latency: 800, + cost: 0.0027, + metadata: { model: 'gpt-4', tokens: 450 }, + }, + ], + }, + }, + { + id: 'run-2', + timestamp: '2025-09-30 14:18:32', + status: 'success', + totalLatency: 2340, + totalCost: 0.0089, + trace: { + id: 'root-2', + name: 'Root', + depth: 0, + input: { query: 'Analyze this code for bugs', context: { language: 'python' } }, + output: { + result: 'Found 3 potential issues: memory leak, unused variable, missing error handling', + suggestions: ['Add try-catch', 'Remove unused var', 'Fix memory leak'] + }, + latency: 2340, + cost: 0.0089, + metadata: { model: 'gpt-4', temperature: 0.3 }, + children: [ + { + id: 'parse-2', + name: 'ParseCode', + depth: 1, + input: { code: 'def process_data():\n data = load()\n unused_var = 5\n result = transform(data)', language: 'python' }, + output: { ast: '...', symbols: ['process_data', 'data', 'unused_var', 'result'] }, + latency: 320, + cost: 0.0008, + metadata: { model: 'gpt-3.5-turbo', tokens: 180 }, + }, + { + id: 'analyze-2', + name: 'AnalyzeBugs', + depth: 1, + input: { ast: '...', symbols: ['process_data', 'data', 'unused_var', 'result'] }, + output: { issues: ['memory_leak', 'unused_variable', 'no_error_handling'] }, + latency: 1120, + cost: 0.0045, + metadata: { model: 'gpt-4', tokens: 890 }, + children: [ + { + id: 'check-memory-2', + name: 'CheckMemory', + depth: 2, + input: { ast: '...', focus: 'memory' }, + output: { found: true, location: 'line 2', severity: 'medium' }, + latency: 450, + cost: 0.0018, + metadata: { model: 'gpt-4', tokens: 320 }, + }, + { + id: 'check-unused-2', + name: 'CheckUnused', + depth: 2, + input: { symbols: ['process_data', 'data', 'unused_var', 'result'] }, + output: { unused: ['unused_var'] }, + latency: 280, + cost: 0.0011, + metadata: { model: 'gpt-3.5-turbo', tokens: 150 }, + }, + ], + }, + { + id: 'suggest-2', + name: 'GenSuggestions', + depth: 1, + input: { issues: ['memory_leak', 'unused_variable', 'no_error_handling'] }, + output: { suggestions: ['Add try-catch', 'Remove unused var', 'Fix memory leak'] }, + latency: 900, + cost: 0.0036, + metadata: { model: 'gpt-4', tokens: 720 }, + }, + ], + }, + }, + { + id: 'run-3', + timestamp: '2025-09-30 13:45:12', + status: 'error', + totalLatency: 850, + totalCost: 0.0021, + trace: { + id: 'root-3', + name: 'RootFunction()', + depth: 0, + input: { query: 'Translate to Spanish', text: 'Hello, how are you?' }, + output: { error: 'Translation service timeout' }, + latency: 850, + cost: 0.0021, + metadata: { model: 'gpt-4', temperature: 0.5 }, + children: [ + { + id: 'detect-3', + name: 'DetectLanguage()', + depth: 1, + input: { text: 'Hello, how are you?' }, + output: { language: 'english', confidence: 0.99 }, + latency: 250, + cost: 0.0008, + metadata: { model: 'gpt-3.5-turbo', tokens: 45 }, + }, + { + id: 'translate-3', + name: 'TranslateText()', + depth: 1, + input: { text: 'Hello, how are you?', from: 'english', to: 'spanish' }, + output: { error: 'Timeout after 600ms' }, + latency: 600, + cost: 0.0013, + metadata: { model: 'gpt-4', tokens: 0, status: 'timeout' }, + }, + ], + }, + }, + { + id: 'run-4', + timestamp: '2025-09-30 13:22:08', + status: 'success', + totalLatency: 3200, + totalCost: 0.0125, + trace: { + id: 'root-4', + name: 'RootFunction()', + depth: 0, + input: { + query: 'Generate a product recommendation', + user_profile: { age: 28, interests: ['tech', 'fitness'], purchase_history: ['laptop', 'smartwatch'] } + }, + output: { + recommendations: [ + { product: 'Wireless Earbuds', score: 0.92, reason: 'Matches tech and fitness interests' }, + { product: 'Fitness Tracker Pro', score: 0.88, reason: 'Complements smartwatch purchase' }, + { product: 'Laptop Stand', score: 0.75, reason: 'Accessory for recent laptop purchase' } + ] + }, + latency: 3200, + cost: 0.0125, + metadata: { model: 'gpt-4', temperature: 0.8 }, + children: [ + { + id: 'analyze-profile-4', + name: 'AnalyzeUserProfile()', + depth: 1, + input: { age: 28, interests: ['tech', 'fitness'], purchase_history: ['laptop', 'smartwatch'] }, + output: { + segments: ['tech_enthusiast', 'fitness_conscious', 'early_adopter'], + predicted_budget: 'medium-high' + }, + latency: 680, + cost: 0.0028, + metadata: { model: 'gpt-4', tokens: 520 }, + }, + { + id: 'fetch-products-4', + name: 'FetchRelevantProducts()', + depth: 1, + input: { segments: ['tech_enthusiast', 'fitness_conscious', 'early_adopter'] }, + output: { + products: [ + { id: 'p1', name: 'Wireless Earbuds', category: 'tech' }, + { id: 'p2', name: 'Fitness Tracker Pro', category: 'fitness' }, + { id: 'p3', name: 'Laptop Stand', category: 'tech' }, + { id: 'p4', name: 'Smart Water Bottle', category: 'fitness' } + ] + }, + latency: 420, + cost: 0.0015, + metadata: { model: 'gpt-3.5-turbo', tokens: 280 }, + }, + { + id: 'score-products-4', + name: 'ScoreProducts()', + depth: 1, + input: { + products: ['Wireless Earbuds', 'Fitness Tracker Pro', 'Laptop Stand', 'Smart Water Bottle'], + user_profile: { age: 28, interests: ['tech', 'fitness'] } + }, + output: { + scores: [ + { product: 'Wireless Earbuds', score: 0.92 }, + { product: 'Fitness Tracker Pro', score: 0.88 }, + { product: 'Laptop Stand', score: 0.75 }, + { product: 'Smart Water Bottle', score: 0.68 } + ] + }, + latency: 1250, + cost: 0.0052, + metadata: { model: 'gpt-4', tokens: 980 }, + children: [ + { + id: 'score-tech-4', + name: 'ScoreTechProducts()', + depth: 2, + input: { products: ['Wireless Earbuds', 'Laptop Stand'] }, + output: { scores: [0.92, 0.75] }, + latency: 580, + cost: 0.0024, + metadata: { model: 'gpt-4', tokens: 450 }, + }, + { + id: 'score-fitness-4', + name: 'ScoreFitnessProducts()', + depth: 2, + input: { products: ['Fitness Tracker Pro', 'Smart Water Bottle'] }, + output: { scores: [0.88, 0.68] }, + latency: 520, + cost: 0.0021, + metadata: { model: 'gpt-4', tokens: 380 }, + }, + ], + }, + { + id: 'explain-4', + name: 'GenerateExplanations()', + depth: 1, + input: { + recommendations: ['Wireless Earbuds', 'Fitness Tracker Pro', 'Laptop Stand'], + user_profile: { interests: ['tech', 'fitness'], purchase_history: ['laptop', 'smartwatch'] } + }, + output: { + explanations: [ + 'Matches tech and fitness interests', + 'Complements smartwatch purchase', + 'Accessory for recent laptop purchase' + ] + }, + latency: 850, + cost: 0.0030, + metadata: { model: 'gpt-4', tokens: 680 }, + }, + ], + }, + }, +]; + +// Custom node component for React Flow +interface TraceNodeData { + span: TraceSpan; + isRunning: boolean; + isCompleted: boolean; + isDivergent: boolean; + isOriginalPath: boolean; + onPlay: (span: TraceSpan, mode: 'single' | 'subtree') => void; +} + +const randomSuffix = () => Math.random().toString(36).slice(2, 8); + +const NODE_WIDTH = 160; +const NODE_HEIGHT = 64; +const HORIZONTAL_GAP = 120; +const VERTICAL_GAP = 40; + +const collectIdsFromSpans = (spans: TraceSpan[]): string[] => { + const ids: string[] = []; + spans.forEach((child) => { + ids.push(child.id); + if (child.children?.length) { + ids.push(...collectIdsFromSpans(child.children)); + } + }); + return ids; +}; + +const collectBaseSpanIds = (span: TraceSpan): string[] => { + const ids = [span.id]; + span.children?.forEach((child) => { + ids.push(...collectBaseSpanIds(child)); + }); + return ids; +}; + +const generateAltBranch = (parent: TraceSpan): TraceSpan[] => { + const depthBase = (parent.depth ?? 0) + 1; + const suffix = randomSuffix(); + const mainId = `${parent.id}-alt-${suffix}`; + const workerId = `${mainId}-worker`; + const finalizeId = `${mainId}-finalize`; + const fallbackId = `${parent.id}-alt-fallback-${suffix}`; + + const mainLatency = 350 + Math.floor(Math.random() * 220); + const workerLatency = 260 + Math.floor(Math.random() * 160); + const finalizeLatency = 140 + Math.floor(Math.random() * 120); + const fallbackLatency = 120 + Math.floor(Math.random() * 100); + + return [ + { + id: mainId, + name: 'AltPath1()', + depth: depthBase, + input: { reason: 'feature_flag_trigger', parent: parent.name }, + output: { status: 'rerouted', branch: 'alternate-primary' }, + latency: mainLatency, + cost: parseFloat((0.0015 + Math.random() * 0.001).toFixed(4)), + metadata: { simulated: true, path: 'alt-primary' }, + children: [ + { + id: workerId, + name: 'AltWorker()', + depth: depthBase + 1, + input: { payload: 'alternate-data' }, + output: { processed: true, checksum: randomSuffix() }, + latency: workerLatency, + cost: parseFloat((0.001 + Math.random() * 0.0008).toFixed(4)), + metadata: { simulated: true, path: 'alt-worker' }, + children: [ + { + id: finalizeId, + name: 'AltFinalize()', + depth: depthBase + 2, + input: { branch: 'alternate-primary' }, + output: { status: 'complete', result: 'alternate_success' }, + latency: finalizeLatency, + cost: parseFloat((0.0006 + Math.random() * 0.0005).toFixed(4)), + metadata: { simulated: true, path: 'alt-finalize' }, + }, + ], + }, + ], + }, + { + id: fallbackId, + name: 'AltPath2()', + depth: depthBase, + input: { reason: 'cache_hit', parent: parent.name }, + output: { status: 'skipped', cached: true }, + latency: fallbackLatency, + cost: parseFloat((0.0007 + Math.random() * 0.0005).toFixed(4)), + metadata: { simulated: true, path: 'alt-fallback' }, + }, + ]; +}; + +const TraceNode: React.FC<{ data: TraceNodeData; selected: boolean }> = ({ data, selected }) => { + const { span, isRunning, isCompleted, isDivergent, isOriginalPath, onPlay } = data; + const [isHovered, setIsHovered] = useState(false); + const hasChildren = !!span.children?.length; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+
+
+
+ {span.name} +
+ {span.latency && ( +
+ {span.latency}ms +
+ )} +
+ {isHovered && !isRunning && ( +
+ + {hasChildren && ( + + )} +
+ )} +
+
+ +
+ ); +}; + +// Helper to build React Flow graph from trace tree using d3-hierarchy layout +const buildFlowGraph = ( + rootSpan: TraceSpan, + alternateBranches: Record, + runningSpans: Set, + completedSpans: Set, + divergentSpans: Set, + originalSpans: Set, + onPlay: (span: TraceSpan, mode: 'single' | 'subtree') => void +): { nodes: Node[]; edges: Edge[] } => { + const nodes: Node[] = []; + const edges: Edge[] = []; + + // Convert TraceSpan to d3-hierarchy compatible structure + const spanToHierarchy = (span: TraceSpan): any => ({ + id: span.id, + span, + children: [ + ...(span.children?.map(spanToHierarchy) ?? []), + ...(alternateBranches[span.id]?.map(spanToHierarchy) ?? []), + ], + }); + + // Create d3 hierarchy + const root = hierarchy(spanToHierarchy(rootSpan)); + + // Create tree layout with horizontal orientation (left to right) + const treeLayout = tree() + .nodeSize([NODE_HEIGHT + VERTICAL_GAP, NODE_WIDTH + HORIZONTAL_GAP]) + .separation((a, b) => (a.parent === b.parent ? 1 : 1.1)); + + // Apply layout + treeLayout(root); + + // Create nodes and edges from layout + root.descendants().forEach((d: any) => { + const span = d.data.span; + const nodeId = span.id; + + // Create React Flow node with d3 computed positions + nodes.push({ + id: nodeId, + type: 'traceNode', + position: { + x: d.y, // d3 tree uses y for horizontal in vertical layout + y: d.x // d3 tree uses x for vertical in vertical layout + }, + data: { + span, + isRunning: runningSpans.has(nodeId), + isCompleted: completedSpans.has(nodeId), + isDivergent: divergentSpans.has(nodeId), + isOriginalPath: originalSpans.has(nodeId), + onPlay, + }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + }); + + // Create edges + if (d.parent) { + const parentId = d.parent.data.span.id; + edges.push({ + id: `${parentId}-${nodeId}`, + source: parentId, + target: nodeId, + type: 'smoothstep', + animated: runningSpans.has(nodeId), + style: { + stroke: divergentSpans.has(nodeId) ? '#f97316' : originalSpans.has(nodeId) ? '#9ca3af' : '#64748b', + strokeWidth: 2, + strokeDasharray: originalSpans.has(nodeId) ? '5,5' : undefined, + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: divergentSpans.has(nodeId) ? '#f97316' : originalSpans.has(nodeId) ? '#9ca3af' : '#64748b', + }, + }); + } + }); + + return { nodes, edges }; +}; + +interface FunctionDetailsPanelProps { + span: TraceSpan | null; +} + +const FunctionDetailsPanel: React.FC = ({ span }) => { + if (!span) { + return ( +
+ Select a span to view details +
+ ); + } + + return ( +
+
+

{span.name}

+
+ {span.latency && Latency: {span.latency}ms} + {span.cost && Cost: ${span.cost.toFixed(4)}} +
+
+ + {span.metadata && ( +
+

Metadata

+
+
{JSON.stringify(span.metadata, null, 2)}
+
+
+ )} + +
+

Input

+
+
{JSON.stringify(span.input, null, 2)}
+
+
+ +
+

Output

+
+
{JSON.stringify(span.output, null, 2)}
+
+
+
+ ); +}; + +interface TerminalViewProps { + logs: string[]; +} + +const TerminalView: React.FC = ({ logs }) => { + const getLogColor = (log: string) => { + if (log.startsWith('>')) return 'text-blue-400'; + if (log.includes('started')) return 'text-yellow-400'; + if (log.includes('✓') || log.includes('completed')) return 'text-green-400'; + if (log.includes('Input:')) return 'text-cyan-400'; + if (log.includes('Output:')) return 'text-purple-400'; + if (log.includes('error') || log.includes('Error')) return 'text-red-400'; + return 'text-gray-400'; + }; + + return ( +
+ {logs.map((log, idx) => ( +
+ {log} +
+ ))} +
+ ); +}; + +const nodeTypes = { + traceNode: TraceNode, +}; + +const WorkflowViewInner: React.FC = () => { + const [selectedRunIndex, setSelectedRunIndex] = useState(0); + const [selectedSpan, setSelectedSpan] = useState(mockTraceRuns[0]?.trace ?? null); + const [runningSpans, setRunningSpans] = useState>(new Set()); + const [completedSpans, setCompletedSpans] = useState>(new Set()); + const [divergentSpans, setDivergentSpans] = useState>(new Set()); + const [originalSpans, setOriginalSpans] = useState>(new Set()); + const [altBranches, setAltBranches] = useState>({}); + const [logs, setLogs] = useState([ + '> Workflow initialized', + '> Ready to execute trace', + ]); + const { fitView } = useReactFlow(); + + const currentRun = mockTraceRuns[selectedRunIndex] || mockTraceRuns[0]!; + const currentTrace = currentRun.trace; + + const findSpanById = useCallback((span: TraceSpan, id: string): TraceSpan | null => { + if (span.id === id) return span; + if (span.children) { + for (const child of span.children) { + const found = findSpanById(child, id); + if (found) return found; + } + } + const alternates = altBranches[span.id] ?? []; + for (const altChild of alternates) { + const found = findSpanById(altChild, id); + if (found) return found; + } + return null; + }, [altBranches]); + + const simulateRun = useCallback(async (span: TraceSpan, replayMode: 'single' | 'subtree') => { + const targetId = span.id; + const modeLabel = replayMode === 'single' ? '(single)' : '(with children)'; + const hasChildren = !!span.children?.length; + const shouldAttemptDivergence = replayMode === 'subtree' && hasChildren; + const willDiverge = shouldAttemptDivergence && Math.random() < 0.5; + + const previousAltNodes = altBranches[targetId] ?? []; + const previousAltIds = collectIdsFromSpans(previousAltNodes); + const baseChildIds = hasChildren + ? span.children!.flatMap((child) => collectBaseSpanIds(child)) + : []; + + const newAltNodes = willDiverge ? generateAltBranch(span) : []; + const newAltIds = willDiverge ? collectIdsFromSpans(newAltNodes) : []; + + const altLookup = new Map(); + if (willDiverge) { + const populateLookup = (nodes: TraceSpan[]) => { + nodes.forEach((node) => { + altLookup.set(node.id, node); + if (node.children?.length) { + populateLookup(node.children); + } + }); + }; + populateLookup(newAltNodes); + } + + let executionIds: string[] = []; + if (replayMode === 'single') { + executionIds = [targetId]; + } else if (willDiverge) { + executionIds = [targetId, ...newAltIds]; + } else { + executionIds = collectBaseSpanIds(span); + } + + setSelectedSpan(span); + setRunningSpans(new Set()); + setCompletedSpans((prev) => { + const next = new Set(prev); + executionIds.forEach((id) => next.delete(id)); + return next; + }); + + setLogs((prev) => { + const next = [...prev, `\n> Running ${span.name} ${modeLabel}...`]; + if (willDiverge) { + next.push('⚠️ Alternate path detected – executing divergent branch'); + } else if (shouldAttemptDivergence) { + next.push('→ Following recorded trace path'); + } + return next; + }); + + if (willDiverge) { + setAltBranches((prev) => ({ ...prev, [targetId]: newAltNodes })); + setOriginalSpans((prev) => { + const next = new Set(prev); + baseChildIds.forEach((id) => next.add(id)); + return next; + }); + setDivergentSpans((prev) => { + const next = new Set(prev); + previousAltIds.forEach((id) => next.delete(id)); + newAltIds.forEach((id) => next.add(id)); + return next; + }); + } else { + if (previousAltNodes.length) { + setAltBranches((prev) => { + if (!prev[targetId]) return prev; + const next = { ...prev }; + delete next[targetId]; + return next; + }); + } + if (baseChildIds.length) { + setOriginalSpans((prev) => { + const next = new Set(prev); + baseChildIds.forEach((id) => next.delete(id)); + return next; + }); + } + if (previousAltIds.length) { + setDivergentSpans((prev) => { + const next = new Set(prev); + previousAltIds.forEach((id) => next.delete(id)); + return next; + }); + } + } + + for (const spanId of executionIds) { + setRunningSpans((prev) => new Set([...prev, spanId])); + + const currentSpan = findSpanById(currentTrace, spanId) ?? altLookup.get(spanId) ?? null; + if (currentSpan) { + setLogs((prev) => [ + ...prev, + ` → ${currentSpan.name} started`, + ` Input: ${JSON.stringify(currentSpan.input, null, 2)}`, + ]); + } + + await new Promise((resolve) => setTimeout(resolve, 800)); + + setRunningSpans((prev) => { + const next = new Set(prev); + next.delete(spanId); + return next; + }); + setCompletedSpans((prev) => new Set([...prev, spanId])); + + if (currentSpan) { + setLogs((prev) => [ + ...prev, + ` Output: ${JSON.stringify(currentSpan.output, null, 2)}`, + ` ✓ ${currentSpan.name} completed in ${currentSpan.latency ?? '—'}ms`, + ]); + } + } + + if (willDiverge) { + const altNames = newAltNodes.map((node) => node.name).join(', '); + setLogs((prev) => [ + ...prev, + ` → Divergent branch executed (${altNames || 'alternate nodes'})`, + `> ${span.name} execution complete ${modeLabel}\n`, + ]); + } else if (replayMode === 'single') { + setLogs((prev) => [ + ...prev, + `> ${span.name} execution complete ${modeLabel}`, + '✓ Single function replay complete', + ]); + } else { + setLogs((prev) => [ + ...prev, + `> ${span.name} execution complete ${modeLabel}`, + '✓ Execution followed recorded trace', + ]); + } + }, [altBranches, currentTrace, findSpanById]); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + // Update nodes/edges when dependencies change using d3-hierarchy layout + useEffect(() => { + const { nodes: newNodes, edges: newEdges } = buildFlowGraph( + currentTrace, + altBranches, + runningSpans, + completedSpans, + divergentSpans, + originalSpans, + simulateRun + ); + console.log('Setting nodes:', newNodes.length, 'edges:', newEdges.length); + setNodes(newNodes); + setEdges(newEdges); + }, [currentTrace, altBranches, runningSpans, completedSpans, divergentSpans, originalSpans, simulateRun, setNodes, setEdges]); + + useEffect(() => { + if (!nodes.length) return; + requestAnimationFrame(() => { + fitView({ padding: 0.24, duration: 400 }); + }); + }, [fitView, nodes, edges]); + + const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { + const span = node.data.span as TraceSpan; + setSelectedSpan(span); + }, []); + + if (!mockTraceRuns || mockTraceRuns.length === 0) { + return
No mock trace runs available
; + } + + return ( +
+ + + + +
+
+

Trace History

+
+ {mockTraceRuns.map((run, idx) => ( + + ))} +
+
+
+ +
+
+
+ + + + +
+
+

Function Details

+
+
+ +
+
+
+
+
+ + + + +
+
+

Terminal

+
+
+ +
+
+
+
+
+ ); +}; + +export const WorkflowView: React.FC = () => ( + + + +);