diff --git a/examples/client_auth_example.rs b/examples/client_auth_example.rs index f6fb6dd..ad6f28f 100644 --- a/examples/client_auth_example.rs +++ b/examples/client_auth_example.rs @@ -12,7 +12,7 @@ //! ``` use circle_client_ws::CircleWsClientBuilder; -use log::{info, error}; +use log::{error, info}; use std::time::Duration; use tokio::time::sleep; @@ -29,11 +29,11 @@ async fn main() -> Result<(), Box> { // Example 1: Authenticate with private key info!("=== Example 1: Private Key Authentication ==="); let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - + let mut client = CircleWsClientBuilder::new(ws_url.clone()) .with_keypair(private_key.to_string()) .build(); - + match client.connect().await { Ok(_) => { info!("Successfully connected to WebSocket"); @@ -67,27 +67,26 @@ async fn main() -> Result<(), Box> { error!("Play request failed: {}", e); } } - + // Keep connection alive for a moment sleep(Duration::from_secs(2)).await; - + // Disconnect client.disconnect().await; info!("Disconnected from WebSocket"); - // Example 3: Different private key authentication info!("=== Example 3: Different Private Key Authentication ==="); let private_key2 = "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - + let mut client2 = CircleWsClientBuilder::new(ws_url.clone()) .with_keypair(private_key2.to_string()) .build(); - + match client2.connect().await { Ok(_) => { info!("Connected with second private key authentication"); - + match client2.authenticate().await { Ok(true) => { info!("Successfully authenticated with second private key"); @@ -108,7 +107,7 @@ async fn main() -> Result<(), Box> { error!("Second private key authentication failed: {}", e); } } - + client2.disconnect().await; } Err(e) => { @@ -119,11 +118,11 @@ async fn main() -> Result<(), Box> { // Example 4: Non-authenticated connection (fallback) info!("=== Example 4: Non-Authenticated Connection ==="); let mut client3 = CircleWsClientBuilder::new(ws_url).build(); - + match client3.connect().await { Ok(()) => { info!("Connected without authentication (fallback mode)"); - + let script = "print('Hello from non-auth client!');".to_string(); match client3.play(script).await { Ok(result) => { @@ -133,7 +132,7 @@ async fn main() -> Result<(), Box> { error!("Non-auth request failed: {}", e); } } - + client3.disconnect().await; } Err(e) => { @@ -143,4 +142,4 @@ async fn main() -> Result<(), Box> { info!("Simplified authentication example completed"); Ok(()) -} \ No newline at end of file +} diff --git a/examples/client_auth_simulation_example.rs b/examples/client_auth_simulation_example.rs index b00fb42..9e591eb 100644 --- a/examples/client_auth_simulation_example.rs +++ b/examples/client_auth_simulation_example.rs @@ -1,5 +1,5 @@ //! Authentication simulation example -//! +//! //! This example simulates the authentication flow without requiring a running server. //! It demonstrates: //! 1. Key generation and management @@ -8,32 +8,28 @@ //! 4. Credential management //! 5. Authentication state checking -use std::time::{SystemTime, UNIX_EPOCH}; use log::info; +use std::time::{SystemTime, UNIX_EPOCH}; // Import authentication modules use circle_client_ws::CircleWsClientBuilder; #[cfg(feature = "crypto")] use circle_client_ws::auth::{ - generate_private_key, - derive_public_key, - sign_message, - verify_signature, - AuthCredentials, - NonceResponse + derive_public_key, generate_private_key, sign_message, verify_signature, AuthCredentials, + NonceResponse, }; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize logging env_logger::init(); - + info!("🔐 Starting authentication simulation example"); - + // Step 1: Generate cryptographic keys info!("🔑 Generating cryptographic keys..."); - + #[cfg(feature = "crypto")] let (private_key, public_key) = { let private_key = generate_private_key()?; @@ -42,38 +38,39 @@ async fn main() -> Result<(), Box> { info!("✅ Derived public key: {}...", &public_key[..20]); (private_key, public_key) }; - + #[cfg(not(feature = "crypto"))] let (private_key, _public_key) = { - let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(); + let private_key = + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(); let public_key = "04abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(); info!("📝 Using fallback keys (crypto feature disabled)"); (private_key, public_key) }; - + // Step 2: Simulate nonce request and response info!("📡 Simulating nonce request..."); let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + let simulated_nonce = format!("nonce_{}_{}", current_time, "abcdef123456"); let expires_at = current_time + 300; // 5 minutes from now - + #[cfg(feature = "crypto")] let nonce_response = NonceResponse { nonce: simulated_nonce.clone(), expires_at, }; - + info!("✅ Simulated nonce response:"); info!(" Nonce: {}", simulated_nonce); info!(" Expires at: {}", expires_at); - + // Step 3: Sign the nonce info!("✍️ Signing nonce with private key..."); - + #[cfg(feature = "crypto")] let signature = { match sign_message(&private_key, &simulated_nonce) { @@ -87,16 +84,16 @@ async fn main() -> Result<(), Box> { } } }; - + #[cfg(not(feature = "crypto"))] let _signature = { info!("📝 Using fallback signature (crypto feature disabled)"); "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string() }; - + // Step 4: Verify the signature info!("🔍 Verifying signature..."); - + #[cfg(feature = "crypto")] { match verify_signature(&public_key, &simulated_nonce, &signature) { @@ -111,12 +108,12 @@ async fn main() -> Result<(), Box> { } } } - + #[cfg(not(feature = "crypto"))] { info!("📝 Skipping signature verification (crypto feature disabled)"); } - + // Step 5: Create authentication credentials info!("📋 Creating authentication credentials..."); #[cfg(feature = "crypto")] @@ -124,9 +121,9 @@ async fn main() -> Result<(), Box> { public_key.clone(), signature.clone(), nonce_response.nonce.clone(), - expires_at + expires_at, ); - + #[cfg(feature = "crypto")] { info!("✅ Credentials created:"); @@ -136,77 +133,86 @@ async fn main() -> Result<(), Box> { info!(" Expires at: {}", credentials.expires_at); info!(" Is expired: {}", credentials.is_expired()); info!(" Expires within 60s: {}", credentials.expires_within(60)); - info!(" Expires within 400s: {}", credentials.expires_within(400)); + info!( + " Expires within 400s: {}", + credentials.expires_within(400) + ); } - + // Step 6: Create client with authentication info!("🔌 Creating WebSocket client with authentication..."); let _client = CircleWsClientBuilder::new("ws://localhost:8080/ws".to_string()) .with_keypair(private_key.clone()) .build(); - + info!("✅ Client created"); - + // Step 7: Demonstrate key rotation info!("🔄 Demonstrating key rotation..."); - + #[cfg(feature = "crypto")] { let new_private_key = generate_private_key()?; let new_public_key = derive_public_key(&new_private_key)?; - + info!("✅ Generated new keys:"); info!(" New private key: {}...", &new_private_key[..10]); info!(" New public key: {}...", &new_public_key[..20]); - + // Create new client with rotated keys let _new_client = CircleWsClientBuilder::new("ws://localhost:8080/ws".to_string()) .with_keypair(new_private_key) .build(); - + info!("✅ Created client with rotated keys"); } - + #[cfg(not(feature = "crypto"))] { info!("📝 Skipping key rotation (crypto feature disabled)"); } - + // Step 8: Demonstrate credential expiration info!("⏰ Demonstrating credential expiration..."); - + // Create credentials that expire soon #[cfg(feature = "crypto")] let short_lived_credentials = AuthCredentials::new( public_key, signature, nonce_response.nonce, - current_time + 5 // Expires in 5 seconds + current_time + 5, // Expires in 5 seconds ); - + #[cfg(feature = "crypto")] { info!("✅ Created short-lived credentials:"); info!(" Expires at: {}", short_lived_credentials.expires_at); info!(" Is expired: {}", short_lived_credentials.is_expired()); - info!(" Expires within 10s: {}", short_lived_credentials.expires_within(10)); - + info!( + " Expires within 10s: {}", + short_lived_credentials.expires_within(10) + ); + // Wait a moment and check again tokio::time::sleep(std::time::Duration::from_secs(1)).await; info!("⏳ After 1 second:"); info!(" Is expired: {}", short_lived_credentials.is_expired()); - info!(" Expires within 5s: {}", short_lived_credentials.expires_within(5)); + info!( + " Expires within 5s: {}", + short_lived_credentials.expires_within(5) + ); } - + info!("🎉 Authentication simulation completed successfully!"); - + Ok(()) } #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_key_generation() { #[cfg(feature = "crypto")] @@ -214,13 +220,13 @@ mod tests { let private_key = generate_private_key().unwrap(); assert!(private_key.starts_with("0x")); assert_eq!(private_key.len(), 66); // 0x + 64 hex chars - + let public_key = derive_public_key(&private_key).unwrap(); assert!(public_key.starts_with("04")); assert_eq!(public_key.len(), 130); // 04 + 128 hex chars (uncompressed) } } - + #[tokio::test] async fn test_signature_flow() { #[cfg(feature = "crypto")] @@ -228,29 +234,29 @@ mod tests { let private_key = generate_private_key().unwrap(); let public_key = derive_public_key(&private_key).unwrap(); let message = "test_nonce_12345"; - + let signature = sign_message(&private_key, message).unwrap(); let is_valid = verify_signature(&public_key, message, &signature).unwrap(); - + assert!(is_valid); } } - + #[test] fn test_credentials() { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + #[cfg(feature = "crypto")] let credentials = AuthCredentials::new( "04abcdef...".to_string(), "0x123456...".to_string(), "nonce_123".to_string(), - current_time + 300 + current_time + 300, ); - + #[cfg(feature = "crypto")] { assert!(!credentials.is_expired()); @@ -258,4 +264,4 @@ mod tests { assert!(!credentials.expires_within(100)); } } -} \ No newline at end of file +} diff --git a/examples/ourworld/main.rs b/examples/ourworld/main.rs index c4b7987..50212b9 100644 --- a/examples/ourworld/main.rs +++ b/examples/ourworld/main.rs @@ -16,10 +16,10 @@ //! 4. The launcher will run until you stop it with Ctrl+C. use launcher::{run_launcher, Args, CircleConfig}; +use log::{error, info}; +use std::error::Error as StdError; use std::fs; use std::path::PathBuf; -use std::error::Error as StdError; -use log::{error, info}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -54,7 +54,11 @@ async fn main() -> Result<(), Box> { let mut circle_configs: Vec = match serde_json::from_str(&config_content) { Ok(configs) => configs, Err(e) => { - error!("Failed to parse {}: {}. Ensure it's a valid JSON array of CircleConfig.", config_path.display(), e); + error!( + "Failed to parse {}: {}. Ensure it's a valid JSON array of CircleConfig.", + config_path.display(), + e + ); return Err(Box::new(e) as Box); } }; @@ -68,7 +72,10 @@ async fn main() -> Result<(), Box> { } if circle_configs.is_empty() { - info!("No circle configurations found in {}. Exiting.", config_path.display()); + info!( + "No circle configurations found in {}. Exiting.", + config_path.display() + ); return Ok(()); } @@ -80,4 +87,4 @@ async fn main() -> Result<(), Box> { println!("--- OurWorld Example Finished ---"); Ok(()) -} \ No newline at end of file +} diff --git a/examples/ourworld/ourworld_output.json b/examples/ourworld/ourworld_output.json index 9a741cf..c98c062 100644 --- a/examples/ourworld/ourworld_output.json +++ b/examples/ourworld/ourworld_output.json @@ -1,51 +1,51 @@ [ { "name": "OurWorld", - "public_key": "02acbca22369b7f10584348056ae48779e04534cd34d37b7db0f4996f4d9d5e2a5", - "secret_key": "0c75df7425c799eb769049cf48891299761660396d772c687fa84cac5ec62570", - "worker_queue": "rhai_tasks:02acbca22369b7f10584348056ae48779e04534cd34d37b7db0f4996f4d9d5e2a5", + "public_key": "02b1ff38c18f66ffcfde1ff4931093484a96d378db55c1306a0760b39172d74099", + "secret_key": "86ed603c86f8938060575f7b1c7e4e4ddf72030ad2ea1699a8e9d1fb3a610869", + "worker_queue": "rhai_tasks:02b1ff38c18f66ffcfde1ff4931093484a96d378db55c1306a0760b39172d74099", "ws_url": "ws://127.0.0.1:9000" }, { "name": "Dunia Cybercity", - "public_key": "03d97b1a357c3ceb2f0eb78f8e2c71beda9190db5cb7e5112150105132effb35e0", - "secret_key": "4fad664608e8de55f0e5e1712241e71dc0864be125bc8633e50601fca8040791", - "worker_queue": "rhai_tasks:03d97b1a357c3ceb2f0eb78f8e2c71beda9190db5cb7e5112150105132effb35e0", + "public_key": "020d8b1e3baab9991a82e9b55e117f45fda58b3f90b072dbbf10888f3195bfe6b9", + "secret_key": "b1ac20e4c6ace638f7f9e07918997fc35b2425de78152139c8b54629ca303b81", + "worker_queue": "rhai_tasks:020d8b1e3baab9991a82e9b55e117f45fda58b3f90b072dbbf10888f3195bfe6b9", "ws_url": "ws://127.0.0.1:9001" }, { "name": "Sikana", - "public_key": "0389595b28cfa98b45fa3c222db79892f3face65e7ef06d44e35d642967e45ed6e", - "secret_key": "fd59ddbf0d0bada725c911dc7e3317754ac552aa1ac84cfcb899bdfe3591e1f4", - "worker_queue": "rhai_tasks:0389595b28cfa98b45fa3c222db79892f3face65e7ef06d44e35d642967e45ed6e", + "public_key": "0363dbff9f2b6dbaf58d3e8774db54dcccd10e23461ebf9a93cca63f8aa321d11d", + "secret_key": "9383663dcac577c14679c3487e6ffe7ff95040f422d391219ea530b892c1b0a0", + "worker_queue": "rhai_tasks:0363dbff9f2b6dbaf58d3e8774db54dcccd10e23461ebf9a93cca63f8aa321d11d", "ws_url": "ws://127.0.0.1:9002" }, { "name": "Threefold", - "public_key": "03270f06ee4a7d42a9f6c22c9a7d6d0138cd15d4fa659026e2e6572fc6c6a6ea18", - "secret_key": "e204c0215bec80f74df49ea5b1592de3c6739cced339ace801bb7e158eb62231", - "worker_queue": "rhai_tasks:03270f06ee4a7d42a9f6c22c9a7d6d0138cd15d4fa659026e2e6572fc6c6a6ea18", + "public_key": "02c19cd347605dab98fb767b5e53c5fa5131d47a46b5f560b565fd4d79c1190994", + "secret_key": "0c4f5172724218650ea5806f5c9f8d4d4c8197c0c775f9d022fd8a192ad59048", + "worker_queue": "rhai_tasks:02c19cd347605dab98fb767b5e53c5fa5131d47a46b5f560b565fd4d79c1190994", "ws_url": "ws://127.0.0.1:9003" }, { "name": "Mbweni", - "public_key": "02724cf23e4ac95d0f14984f55c6955b3ca5ab2275d7ac2a2e4baf3596caf8606c", - "secret_key": "3c013e2e5f64692f044d17233e5fabdb0577629f898359115e69c3e594d5f43e", - "worker_queue": "rhai_tasks:02724cf23e4ac95d0f14984f55c6955b3ca5ab2275d7ac2a2e4baf3596caf8606c", + "public_key": "0251808090b5b916e6187b63b6c97411f9d5406a9a6179408b90e3ff83042e7a9c", + "secret_key": "c824b3334350e2b267be2d4ceb1db53e98c9f386d2855aa7130227caa580805c", + "worker_queue": "rhai_tasks:0251808090b5b916e6187b63b6c97411f9d5406a9a6179408b90e3ff83042e7a9c", "ws_url": "ws://127.0.0.1:9004" }, { "name": "Geomind", - "public_key": "030d8ceb47d445c92b7c3f13e9e134eebcb1d83beed424425f734164544eb58eed", - "secret_key": "dbd6dd383a6f56042710f72ce2ac68266650bbfb61432cdd139e98043b693e7c", - "worker_queue": "rhai_tasks:030d8ceb47d445c92b7c3f13e9e134eebcb1d83beed424425f734164544eb58eed", + "public_key": "037e2def151e7587b95519370e5d1023b9f24845e8e23a6535b0aad3cff20a859b", + "secret_key": "9c701a02ebba983d04ecbccee5072ed2cebd67ead4677c79a72d089d3ff29295", + "worker_queue": "rhai_tasks:037e2def151e7587b95519370e5d1023b9f24845e8e23a6535b0aad3cff20a859b", "ws_url": "ws://127.0.0.1:9005" }, { "name": "Freezone", - "public_key": "02dd21025c1d47421eccc2264c87538d41126da772a9a3f0e7226807fed89c9971", - "secret_key": "0c0c6b02c20fcd4ccfb2afeae249979ddd623e6f6edd17af4a9a5a19bc1b15ae", - "worker_queue": "rhai_tasks:02dd21025c1d47421eccc2264c87538d41126da772a9a3f0e7226807fed89c9971", + "public_key": "02d4bf2713876cff2428f3f5e7e6191028374994d43a2c0f3d62c728a22d7f4aed", + "secret_key": "602c1bdd95489c7153676488976e9a24483cb353778332ec3b7644c3f05f5af2", + "worker_queue": "rhai_tasks:02d4bf2713876cff2428f3f5e7e6191028374994d43a2c0f3d62c728a22d7f4aed", "ws_url": "ws://127.0.0.1:9006" } ] \ No newline at end of file diff --git a/examples/server_e2e_rhai_flow.rs b/examples/server_e2e_rhai_flow.rs index ff9724a..6f2b5ba 100644 --- a/examples/server_e2e_rhai_flow.rs +++ b/examples/server_e2e_rhai_flow.rs @@ -1,6 +1,6 @@ -use std::process::{Command, Child, Stdio}; -use std::time::Duration; use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; use tokio::time::sleep; // tokio_tungstenite and direct futures_util for ws stream are no longer needed here // use tokio_tungstenite::{connect_async, tungstenite::protocol::Message as WsMessage}; @@ -32,28 +32,54 @@ impl ChildProcessGuard { impl Drop for ChildProcessGuard { fn drop(&mut self) { - log::info!("Cleaning up {} process (PID: {})...", self.name, self.child.id()); + log::info!( + "Cleaning up {} process (PID: {})...", + self.name, + self.child.id() + ); match self.child.kill() { Ok(_) => { - log::info!("Successfully sent kill signal to {} (PID: {}).", self.name, self.child.id()); + log::info!( + "Successfully sent kill signal to {} (PID: {}).", + self.name, + self.child.id() + ); // Optionally wait for a short period or check status match self.child.wait() { - Ok(status) => log::info!("{} (PID: {}) exited with status: {}", self.name, self.child.id(), status), - Err(e) => log::warn!("Error waiting for {} (PID: {}): {}", self.name, self.child.id(), e), + Ok(status) => log::info!( + "{} (PID: {}) exited with status: {}", + self.name, + self.child.id(), + status + ), + Err(e) => log::warn!( + "Error waiting for {} (PID: {}): {}", + self.name, + self.child.id(), + e + ), } } - Err(e) => log::error!("Failed to kill {} (PID: {}): {}", self.name, self.child.id(), e), + Err(e) => log::error!( + "Failed to kill {} (PID: {}): {}", + self.name, + self.child.id(), + e + ), } } } fn find_target_dir() -> Result { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?; - let workspace_root = PathBuf::from(manifest_dir).parent().ok_or("Failed to get workspace root")?.to_path_buf(); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?; + let workspace_root = PathBuf::from(manifest_dir) + .parent() + .ok_or("Failed to get workspace root")? + .to_path_buf(); Ok(workspace_root.join("target").join("debug")) } - #[tokio::main] async fn main() -> Result<(), Box> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); @@ -62,7 +88,7 @@ async fn main() -> Result<(), Box> { log::error!("Could not determine target directory: {}", e); e })?; - + let rhai_worker_path = target_dir.join(RHAI_WORKER_BIN_NAME); let circle_server_ws_path = target_dir.join(CIRCLE_SERVER_WS_BIN_NAME); @@ -79,26 +105,46 @@ async fn main() -> Result<(), Box> { .stdout(Stdio::piped()) // Capture stdout .stderr(Stdio::piped()) // Capture stderr .spawn()?; - let _rhai_worker_guard = ChildProcessGuard::new(rhai_worker_process, RHAI_WORKER_BIN_NAME.to_string()); - log::info!("{} started with PID {}", RHAI_WORKER_BIN_NAME, _rhai_worker_guard.child.id()); + let _rhai_worker_guard = + ChildProcessGuard::new(rhai_worker_process, RHAI_WORKER_BIN_NAME.to_string()); + log::info!( + "{} started with PID {}", + RHAI_WORKER_BIN_NAME, + _rhai_worker_guard.child.id() + ); - log::info!("Starting {} for circle '{}' on port {}...", CIRCLE_SERVER_WS_BIN_NAME, TEST_CIRCLE_NAME, TEST_SERVER_PORT); + log::info!( + "Starting {} for circle '{}' on port {}...", + CIRCLE_SERVER_WS_BIN_NAME, + TEST_CIRCLE_NAME, + TEST_SERVER_PORT + ); let circle_server_process = Command::new(&circle_server_ws_path) - .args(["--port", &TEST_SERVER_PORT.to_string(), "--circle-name", TEST_CIRCLE_NAME]) + .args([ + "--port", + &TEST_SERVER_PORT.to_string(), + "--circle-name", + TEST_CIRCLE_NAME, + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; - let _circle_server_guard = ChildProcessGuard::new(circle_server_process, CIRCLE_SERVER_WS_BIN_NAME.to_string()); - log::info!("{} started with PID {}", CIRCLE_SERVER_WS_BIN_NAME, _circle_server_guard.child.id()); + let _circle_server_guard = + ChildProcessGuard::new(circle_server_process, CIRCLE_SERVER_WS_BIN_NAME.to_string()); + log::info!( + "{} started with PID {}", + CIRCLE_SERVER_WS_BIN_NAME, + _circle_server_guard.child.id() + ); // Give servers a moment to start sleep(Duration::from_secs(3)).await; // Increased sleep let ws_url_str = format!("ws://127.0.0.1:{}/ws", TEST_SERVER_PORT); - + log::info!("Creating CircleWsClient for {}...", ws_url_str); let mut client = CircleWsClientBuilder::new(ws_url_str.clone()).build(); - + log::info!("Connecting CircleWsClient..."); client.connect().await.map_err(|e| { log::error!("CircleWsClient connection failed: {}", e); @@ -108,8 +154,11 @@ async fn main() -> Result<(), Box> { let script_to_run = "let a = 5; let b = 10; print(\"E2E Rhai: \" + (a+b)); a + b"; - log::info!("Sending 'play' request via CircleWsClient for script: '{}'", script_to_run); - + log::info!( + "Sending 'play' request via CircleWsClient for script: '{}'", + script_to_run + ); + match client.play(script_to_run.to_string()).await { Ok(play_result) => { log::info!("Received play result: {:?}", play_result); @@ -121,12 +170,12 @@ async fn main() -> Result<(), Box> { return Err(format!("CircleWsClient play request failed: {}", e).into()); } } - + log::info!("Disconnecting CircleWsClient..."); client.disconnect().await; log::info!("CircleWsClient disconnected."); - + log::info!("E2E Rhai flow example completed successfully."); // Guards will automatically clean up child processes when they go out of scope here Ok(()) -} \ No newline at end of file +} diff --git a/examples/server_timeout_demonstration.rs b/examples/server_timeout_demonstration.rs index a3a5a1a..8b50900 100644 --- a/examples/server_timeout_demonstration.rs +++ b/examples/server_timeout_demonstration.rs @@ -7,9 +7,9 @@ // Ensure circle_server_ws is compiled (cargo build --bin circle_server_ws). use circle_client_ws::CircleWsClientBuilder; -use tokio::time::{sleep, Duration}; -use std::process::{Command, Child, Stdio}; use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use tokio::time::{sleep, Duration}; const EXAMPLE_SERVER_PORT: u16 = 8089; // Using a specific port for this example const WS_URL: &str = "ws://127.0.0.1:8089/ws"; @@ -32,26 +32,56 @@ impl ChildProcessGuard { impl Drop for ChildProcessGuard { fn drop(&mut self) { - log::info!("Cleaning up {} process (PID: {})...", self.name, self.child.id()); + log::info!( + "Cleaning up {} process (PID: {})...", + self.name, + self.child.id() + ); match self.child.kill() { Ok(_) => { - log::info!("Successfully sent kill signal to {} (PID: {}).", self.name, self.child.id()); + log::info!( + "Successfully sent kill signal to {} (PID: {}).", + self.name, + self.child.id() + ); match self.child.wait() { - Ok(status) => log::info!("{} (PID: {}) exited with status: {}", self.name, self.child.id(), status), - Err(e) => log::warn!("Error waiting for {} (PID: {}): {}", self.name, self.child.id(), e), + Ok(status) => log::info!( + "{} (PID: {}) exited with status: {}", + self.name, + self.child.id(), + status + ), + Err(e) => log::warn!( + "Error waiting for {} (PID: {}): {}", + self.name, + self.child.id(), + e + ), } } - Err(e) => log::error!("Failed to kill {} (PID: {}): {}", self.name, self.child.id(), e), + Err(e) => log::error!( + "Failed to kill {} (PID: {}): {}", + self.name, + self.child.id(), + e + ), } } } fn find_target_bin_path(bin_name: &str) -> Result { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?; - let workspace_root = PathBuf::from(manifest_dir).parent().ok_or("Failed to get workspace root")?.to_path_buf(); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map_err(|_| "CARGO_MANIFEST_DIR not set".to_string())?; + let workspace_root = PathBuf::from(manifest_dir) + .parent() + .ok_or("Failed to get workspace root")? + .to_path_buf(); let bin_path = workspace_root.join("target").join("debug").join(bin_name); if !bin_path.exists() { - return Err(format!("Binary '{}' not found at {:?}. Ensure it's built.", bin_name, bin_path)); + return Err(format!( + "Binary '{}' not found at {:?}. Ensure it's built.", + bin_name, bin_path + )); } Ok(bin_path) } @@ -63,18 +93,31 @@ async fn main() -> Result<(), Box> { let server_bin_path = find_target_bin_path(CIRCLE_SERVER_WS_BIN_NAME)?; log::info!("Found server binary at: {:?}", server_bin_path); - log::info!("Starting {} for circle '{}' on port {}...", CIRCLE_SERVER_WS_BIN_NAME, CIRCLE_NAME_FOR_EXAMPLE, EXAMPLE_SERVER_PORT); + log::info!( + "Starting {} for circle '{}' on port {}...", + CIRCLE_SERVER_WS_BIN_NAME, + CIRCLE_NAME_FOR_EXAMPLE, + EXAMPLE_SERVER_PORT + ); let server_process = Command::new(&server_bin_path) .args([ - "--port", &EXAMPLE_SERVER_PORT.to_string(), - "--circle-name", CIRCLE_NAME_FOR_EXAMPLE + "--port", + &EXAMPLE_SERVER_PORT.to_string(), + "--circle-name", + CIRCLE_NAME_FOR_EXAMPLE, ]) .stdout(Stdio::piped()) // Pipe stdout to keep terminal clean, or Stdio::inherit() to see server logs .stderr(Stdio::piped()) // Pipe stderr as well .spawn() - .map_err(|e| format!("Failed to start {}: {}. Ensure it is built.", CIRCLE_SERVER_WS_BIN_NAME, e))?; - - let _server_guard = ChildProcessGuard::new(server_process, CIRCLE_SERVER_WS_BIN_NAME.to_string()); + .map_err(|e| { + format!( + "Failed to start {}: {}. Ensure it is built.", + CIRCLE_SERVER_WS_BIN_NAME, e + ) + })?; + + let _server_guard = + ChildProcessGuard::new(server_process, CIRCLE_SERVER_WS_BIN_NAME.to_string()); log::info!("Giving the server a moment to start up..."); sleep(Duration::from_secs(3)).await; // Wait for server to initialize @@ -99,13 +142,20 @@ async fn main() -> Result<(), Box> { // This part should not be reached if timeout works correctly. print(x); x - ".to_string(); + " + .to_string(); - log::info!("Sending long-running script (expected to time out on server after ~{}s)...", SCRIPT_TIMEOUT_SECONDS); + log::info!( + "Sending long-running script (expected to time out on server after ~{}s)...", + SCRIPT_TIMEOUT_SECONDS + ); match client.play(long_running_script).await { Ok(play_result) => { - log::warn!("Received unexpected success from play request: {:?}", play_result); + log::warn!( + "Received unexpected success from play request: {:?}", + play_result + ); log::warn!("This might indicate the script finished faster than expected, or the timeout didn't trigger."); } Err(e) => { @@ -116,7 +166,10 @@ async fn main() -> Result<(), Box> { if e.to_string().contains("timed out") || e.to_string().contains("-32002") { log::info!("Successfully received timeout error from the server!"); } else { - log::warn!("Received an error, but it might not be the expected timeout error: {}", e); + log::warn!( + "Received an error, but it might not be the expected timeout error: {}", + e + ); } } } @@ -127,4 +180,4 @@ async fn main() -> Result<(), Box> { log::info!("Timeout demonstration example finished."); Ok(()) -} \ No newline at end of file +} diff --git a/src/app/src/app.rs b/src/app/src/app.rs index 39a6357..c41df83 100644 --- a/src/app/src/app.rs +++ b/src/app/src/app.rs @@ -1,17 +1,17 @@ -use yew::prelude::*; -use std::rc::Rc; use std::collections::HashMap; +use std::rc::Rc; +use yew::prelude::*; -use crate::components::circles_view::CirclesView; -use crate::components::nav_island::NavIsland; -use crate::components::library_view::LibraryView; -use crate::components::intelligence_view::IntelligenceView; -use crate::components::inspector_view::InspectorView; -use crate::components::publishing_view::PublishingView; -use crate::components::customize_view::CustomizeViewComponent; -use crate::components::login_component::LoginComponent; use crate::auth::{AuthManager, AuthState}; use crate::components::auth_view::AuthView; +use crate::components::circles_view::CirclesView; +use crate::components::customize_view::CustomizeViewComponent; +use crate::components::inspector_view::InspectorView; +use crate::components::intelligence_view::IntelligenceView; +use crate::components::library_view::LibraryView; +use crate::components::login_component::LoginComponent; +use crate::components::nav_island::NavIsland; +use crate::components::publishing_view::PublishingView; // Props for the App component #[derive(Properties, PartialEq, Clone)] @@ -43,7 +43,7 @@ pub enum Msg { pub struct App { current_view: AppView, active_context_urls: Vec, // Only context URLs from CirclesView - start_circle_ws_url: String, // Initial WebSocket URL for CirclesView + start_circle_ws_url: String, // Initial WebSocket URL for CirclesView auth_manager: AuthManager, auth_state: AuthState, } @@ -55,15 +55,15 @@ impl Component for App { fn create(ctx: &Context) -> Self { wasm_logger::init(wasm_logger::Config::default()); log::info!("App created with authentication support."); - + let start_circle_ws_url = ctx.props().start_circle_ws_url.clone(); let auth_manager = AuthManager::new(); let auth_state = auth_manager.get_state(); - + // Set up auth state change callback let link = ctx.link().clone(); auth_manager.set_on_state_change(link.callback(Msg::AuthStateChanged)); - + // Determine initial view based on authentication state let initial_view = match auth_state { AuthState::Authenticated { .. } => AppView::Circles, @@ -82,7 +82,10 @@ impl Component for App { fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { match msg { Msg::UpdateCirclesContext(context_urls) => { - log::info!("App: Received context update from CirclesView: {:?}", context_urls); + log::info!( + "App: Received context update from CirclesView: {:?}", + context_urls + ); self.active_context_urls = context_urls; true } @@ -98,7 +101,10 @@ impl Component for App { self.current_view = view; true } else { - log::warn!("Attempted to access {} view without authentication", format!("{:?}", view)); + log::warn!( + "Attempted to access {} view without authentication", + format!("{:?}", view) + ); self.current_view = AppView::Login; true } @@ -108,7 +114,7 @@ impl Component for App { Msg::AuthStateChanged(state) => { log::info!("App: Auth state changed: {:?}", state); self.auth_state = state.clone(); - + match state { AuthState::Authenticated { .. } => { // Switch to main app view when authenticated @@ -212,7 +218,7 @@ impl Component for App { /> }, }} - + { if self.current_view != AppView::Login { html! { { "Circles" } - } diff --git a/src/app/src/auth/auth_manager.rs b/src/app/src/auth/auth_manager.rs index a91cb40..cdcd3dc 100644 --- a/src/app/src/auth/auth_manager.rs +++ b/src/app/src/auth/auth_manager.rs @@ -1,17 +1,17 @@ //! Authentication manager for coordinating authentication flows -//! +//! //! This module provides the main AuthManager struct that coordinates //! the entire authentication process, including email lookup and //! integration with the client_ws library for WebSocket connections. -use std::rc::Rc; -use std::cell::RefCell; -use yew::Callback; -use gloo_storage::{LocalStorage, SessionStorage, Storage}; -use circle_client_ws::{CircleWsClient, CircleWsClientError, CircleWsClientBuilder}; -use circle_client_ws::auth::{validate_private_key, derive_public_key}; -use crate::auth::types::{AuthResult, AuthError, AuthState, AuthMethod}; use crate::auth::email_store::{get_key_pair_for_email, is_email_available}; +use crate::auth::types::{AuthError, AuthMethod, AuthResult, AuthState}; +use circle_client_ws::auth::{derive_public_key, validate_private_key}; +use circle_client_ws::{CircleWsClient, CircleWsClientBuilder, CircleWsClientError}; +use gloo_storage::{LocalStorage, SessionStorage, Storage}; +use std::cell::RefCell; +use std::rc::Rc; +use yew::Callback; /// Key for storing authentication state in local storage const AUTH_STATE_STORAGE_KEY: &str = "circles_auth_state_marker"; @@ -35,7 +35,7 @@ impl AuthManager { /// Create a new authentication manager pub fn new() -> Self { let initial_state = Self::load_auth_state().unwrap_or(AuthState::NotAuthenticated); - + Self { state: Rc::new(RefCell::new(initial_state)), on_state_change: Rc::new(RefCell::new(None)), @@ -65,8 +65,7 @@ impl AuthManager { let key_pair = get_key_pair_for_email(&email)?; // Validate the private key using client_ws - validate_private_key(&key_pair.private_key) - .map_err(|e| AuthError::from(e))?; + validate_private_key(&key_pair.private_key).map_err(|e| AuthError::from(e))?; // Set authenticated state let auth_state = AuthState::Authenticated { @@ -84,12 +83,10 @@ impl AuthManager { self.set_state(AuthState::Authenticating); // Validate the private key using client_ws - validate_private_key(&private_key) - .map_err(|e| AuthError::from(e))?; + validate_private_key(&private_key).map_err(|e| AuthError::from(e))?; // Derive public key using client_ws - let public_key = derive_public_key(&private_key) - .map_err(|e| AuthError::from(e))?; + let public_key = derive_public_key(&private_key).map_err(|e| AuthError::from(e))?; // Set authenticated state let auth_state = AuthState::Authenticated { @@ -103,7 +100,10 @@ impl AuthManager { } /// Create an authenticated WebSocket client using message-based authentication - pub async fn create_authenticated_client(&self, ws_url: &str) -> Result { + pub async fn create_authenticated_client( + &self, + ws_url: &str, + ) -> Result { let auth_state = self.state.borrow().clone(); let private_key = match auth_state { @@ -140,10 +140,10 @@ impl AuthManager { /// Set authentication state and notify listeners fn set_state(&self, new_state: AuthState) { *self.state.borrow_mut() = new_state.clone(); - + // Save to local storage (excluding sensitive data) self.save_auth_state(&new_state); - + // Notify listeners if let Some(callback) = &*self.on_state_change.borrow() { callback.emit(new_state); @@ -154,7 +154,11 @@ impl AuthManager { /// Private keys are stored in sessionStorage, method hints in localStorage. fn save_auth_state(&self, state: &AuthState) { match state { - AuthState::Authenticated { public_key: _, private_key, method } => { + AuthState::Authenticated { + public_key: _, + private_key, + method, + } => { match method { AuthMethod::Email(email) => { let marker = format!("email:{}", email); @@ -164,9 +168,15 @@ impl AuthManager { } AuthMethod::PrivateKey => { // Store the actual private key in sessionStorage - let _ = SessionStorage::set(PRIVATE_KEY_SESSION_STORAGE_KEY, private_key.clone()); + let _ = SessionStorage::set( + PRIVATE_KEY_SESSION_STORAGE_KEY, + private_key.clone(), + ); // Store a marker in localStorage - let _ = LocalStorage::set(AUTH_STATE_STORAGE_KEY, "private_key_auth_marker".to_string()); + let _ = LocalStorage::set( + AUTH_STATE_STORAGE_KEY, + "private_key_auth_marker".to_string(), + ); } } } @@ -188,7 +198,9 @@ impl AuthManager { fn load_auth_state() -> Option { if let Ok(marker) = LocalStorage::get::(AUTH_STATE_STORAGE_KEY) { if marker == "private_key_auth_marker" { - if let Ok(private_key) = SessionStorage::get::(PRIVATE_KEY_SESSION_STORAGE_KEY) { + if let Ok(private_key) = + SessionStorage::get::(PRIVATE_KEY_SESSION_STORAGE_KEY) + { if validate_private_key(&private_key).is_ok() { if let Ok(public_key) = derive_public_key(&private_key) { return Some(AuthState::Authenticated { @@ -251,8 +263,7 @@ impl AuthManager { pub fn validate_current_auth(&self) -> AuthResult<()> { match &*self.state.borrow() { AuthState::Authenticated { private_key, .. } => { - validate_private_key(private_key) - .map_err(|e| AuthError::from(e)) + validate_private_key(private_key).map_err(|e| AuthError::from(e)) } _ => Err(AuthError::AuthFailed("Not authenticated".to_string())), } @@ -275,15 +286,17 @@ mod tests { #[wasm_bindgen_test] async fn test_email_authentication() { let auth_manager = AuthManager::new(); - + // Test with valid email - let result = auth_manager.authenticate_with_email("alice@example.com".to_string()).await; + let result = auth_manager + .authenticate_with_email("alice@example.com".to_string()) + .await; assert!(result.is_ok()); assert!(auth_manager.is_authenticated()); - + // Check that we can get the public key assert!(auth_manager.get_public_key().is_some()); - + // Check auth method match auth_manager.get_auth_method() { Some(AuthMethod::Email(email)) => assert_eq!(email, "alice@example.com"), @@ -294,13 +307,15 @@ mod tests { #[wasm_bindgen_test] async fn test_private_key_authentication() { let auth_manager = AuthManager::new(); - + // Test with valid private key let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - let result = auth_manager.authenticate_with_private_key(private_key.to_string()).await; + let result = auth_manager + .authenticate_with_private_key(private_key.to_string()) + .await; assert!(result.is_ok()); assert!(auth_manager.is_authenticated()); - + // Check that we can get the public key assert!(auth_manager.get_public_key().is_some()); } @@ -308,8 +323,10 @@ mod tests { #[wasm_bindgen_test] async fn test_invalid_email() { let auth_manager = AuthManager::new(); - - let result = auth_manager.authenticate_with_email("nonexistent@example.com".to_string()).await; + + let result = auth_manager + .authenticate_with_email("nonexistent@example.com".to_string()) + .await; assert!(result.is_err()); assert!(!auth_manager.is_authenticated()); } @@ -317,8 +334,10 @@ mod tests { #[wasm_bindgen_test] async fn test_invalid_private_key() { let auth_manager = AuthManager::new(); - - let result = auth_manager.authenticate_with_private_key("invalid_key".to_string()).await; + + let result = auth_manager + .authenticate_with_private_key("invalid_key".to_string()) + .await; assert!(result.is_err()); assert!(!auth_manager.is_authenticated()); } @@ -326,11 +345,13 @@ mod tests { #[wasm_bindgen_test] async fn test_logout() { let auth_manager = AuthManager::new(); - + // Authenticate first - let _ = auth_manager.authenticate_with_email("alice@example.com".to_string()).await; + let _ = auth_manager + .authenticate_with_email("alice@example.com".to_string()) + .await; assert!(auth_manager.is_authenticated()); - + // Logout auth_manager.logout(); assert!(!auth_manager.is_authenticated()); @@ -340,9 +361,9 @@ mod tests { #[wasm_bindgen_test] fn test_email_availability() { let auth_manager = AuthManager::new(); - + assert!(auth_manager.is_email_available("alice@example.com")); assert!(auth_manager.is_email_available("admin@circles.com")); assert!(!auth_manager.is_email_available("nonexistent@example.com")); } -} \ No newline at end of file +} diff --git a/src/app/src/auth/email_store.rs b/src/app/src/auth/email_store.rs index 5c61e4c..5d3c972 100644 --- a/src/app/src/auth/email_store.rs +++ b/src/app/src/auth/email_store.rs @@ -1,12 +1,12 @@ //! Hardcoded email-to-private-key mappings -//! +//! //! This module provides a static mapping of email addresses to their corresponding //! private and public key pairs. This is designed for development and app purposes //! where users can authenticate using known email addresses. -use std::collections::HashMap; -use crate::auth::types::{AuthResult, AuthError}; +use crate::auth::types::{AuthError, AuthResult}; use circle_client_ws::auth::derive_public_key; +use std::collections::HashMap; /// A key pair consisting of private and public keys #[derive(Debug, Clone)] @@ -16,50 +16,50 @@ pub struct KeyPair { } /// Get the hardcoded email-to-key mappings -/// +/// /// Returns a HashMap where: /// - Key: email address (String) /// - Value: KeyPair with private and public keys pub fn get_email_key_mappings() -> HashMap { let mut mappings = HashMap::new(); - + // Demo users with their private keys // Note: These are for demonstration purposes only let demo_keys = vec![ ( "alice@example.com", - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", ), ( - "bob@example.com", - "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + "bob@example.com", + "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", ), ( "charlie@example.com", - "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", ), ( "diana@example.com", - "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba" + "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba", ), ( "eve@example.com", - "0x1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff" + "0x1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff", ), ( "admin@circles.com", - "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ), ( "app@circles.com", - "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef" + "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef", ), ( "test@circles.com", - "0xbaadf00dcafebabe9876543210fedcba9876543210fedcba9876543210fedcba" + "0xbaadf00dcafebabe9876543210fedcba9876543210fedcba9876543210fedcba", ), ]; - + // Generate key pairs for each app user for (email, private_key) in demo_keys { if let Ok(public_key) = derive_public_key(private_key) { @@ -68,21 +68,22 @@ pub fn get_email_key_mappings() -> HashMap { KeyPair { private_key: private_key.to_string(), public_key, - } + }, ); } else { log::error!("Failed to derive public key for email: {}", email); } } - + mappings } /// Look up a key pair by email address pub fn get_key_pair_for_email(email: &str) -> AuthResult { let mappings = get_email_key_mappings(); - - mappings.get(email) + + mappings + .get(email) .cloned() .ok_or_else(|| AuthError::EmailNotFound(email.to_string())) } @@ -102,24 +103,28 @@ pub fn is_email_available(email: &str) -> bool { pub fn add_email_key_mapping(email: String, private_key: String) -> AuthResult<()> { // Validate the private key first let public_key = derive_public_key(&private_key)?; - + // In a real implementation, you might want to persist this // For now, we just validate that it would work - log::info!("Would add mapping for email: {} with public key: {}", email, public_key); - + log::info!( + "Would add mapping for email: {} with public key: {}", + email, + public_key + ); + Ok(()) } #[cfg(test)] mod tests { use super::*; - use circle_client_ws::auth::{validate_private_key, verify_signature, sign_message}; + use circle_client_ws::auth::{sign_message, validate_private_key, verify_signature}; #[test] fn test_email_mappings_exist() { let mappings = get_email_key_mappings(); assert!(!mappings.is_empty()); - + // Check that alice@example.com exists assert!(mappings.contains_key("alice@example.com")); assert!(mappings.contains_key("admin@circles.com")); @@ -128,10 +133,10 @@ mod tests { #[test] fn test_key_pair_lookup() { let key_pair = get_key_pair_for_email("alice@example.com").unwrap(); - + // Validate that the private key is valid assert!(validate_private_key(&key_pair.private_key).is_ok()); - + // Validate that the public key matches the private key let derived_public = derive_public_key(&key_pair.private_key).unwrap(); assert_eq!(key_pair.public_key, derived_public); @@ -141,10 +146,10 @@ mod tests { fn test_signing_with_stored_keys() { let key_pair = get_key_pair_for_email("bob@example.com").unwrap(); let message = "Test message"; - + // Sign a message with the stored private key let signature = sign_message(&key_pair.private_key, message).unwrap(); - + // Verify the signature with the stored public key let is_valid = verify_signature(&key_pair.public_key, message, &signature).unwrap(); assert!(is_valid); @@ -154,7 +159,7 @@ mod tests { fn test_email_not_found() { let result = get_key_pair_for_email("nonexistent@example.com"); assert!(result.is_err()); - + match result { Err(AuthError::EmailNotFound(email)) => { assert_eq!(email, "nonexistent@example.com"); @@ -177,4 +182,4 @@ mod tests { assert!(is_email_available("admin@circles.com")); assert!(!is_email_available("nonexistent@example.com")); } -} \ No newline at end of file +} diff --git a/src/app/src/auth/mod.rs b/src/app/src/auth/mod.rs index 871b1f8..5d068de 100644 --- a/src/app/src/auth/mod.rs +++ b/src/app/src/auth/mod.rs @@ -1,10 +1,10 @@ //! Authentication module for the Circles app -//! +//! //! This module provides application-specific authentication functionality including: //! - Email-to-private-key mappings (hardcoded for app) //! - Authentication manager for coordinating auth flows //! - Integration with the client_ws library for WebSocket authentication -//! +//! //! Core cryptographic functionality is provided by the client_ws library. pub mod auth_manager; diff --git a/src/app/src/auth/types.rs b/src/app/src/auth/types.rs index 4fe10a2..ec22f75 100644 --- a/src/app/src/auth/types.rs +++ b/src/app/src/auth/types.rs @@ -1,5 +1,5 @@ //! Application-specific authentication types -//! +//! //! This module defines app-specific authentication types that extend //! the core types from the client_ws library. @@ -45,7 +45,7 @@ impl From for AuthError { /// Authentication method chosen by the user (app-specific) #[derive(Debug, Clone, PartialEq)] pub enum AuthMethod { - PrivateKey, // Direct private key input + PrivateKey, // Direct private key input Email(String), // Email-based lookup (app-specific) } @@ -69,4 +69,4 @@ pub enum AuthState { method: AuthMethod, }, Failed(String), // Error message -} \ No newline at end of file +} diff --git a/src/app/src/components/asset_details_card.rs b/src/app/src/components/asset_details_card.rs index 07daaef..cce288e 100644 --- a/src/app/src/components/asset_details_card.rs +++ b/src/app/src/components/asset_details_card.rs @@ -1,6 +1,6 @@ -use yew::prelude::*; -use heromodels::models::library::items::TocEntry; use crate::components::library_view::DisplayLibraryItem; +use heromodels::models::library::items::TocEntry; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct AssetDetailsCardProps { @@ -22,7 +22,7 @@ impl Component for AssetDetailsCard { fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); - + let back_handler = { let on_back = props.on_back.clone(); Callback::from(move |_: MouseEvent| { @@ -172,4 +172,4 @@ impl AssetDetailsCard { } } -} \ No newline at end of file +} diff --git a/src/app/src/components/auth_view.rs b/src/app/src/components/auth_view.rs index 023e537..04f9b7d 100644 --- a/src/app/src/components/auth_view.rs +++ b/src/app/src/components/auth_view.rs @@ -4,8 +4,8 @@ use yew::prelude::*; #[derive(Properties, PartialEq, Clone)] pub struct AuthViewProps { pub auth_state: AuthState, - pub on_logout: Callback<()>, - pub on_login: Callback<()>, // New callback for login + pub on_logout: Callback<()>, + pub on_login: Callback<()>, // New callback for login } #[function_component(AuthView)] @@ -19,7 +19,11 @@ pub fn auth_view(props: &AuthViewProps) -> Html { // Truncate the public key for display let pk_short = if public_key.len() > 10 { - format!("{}...{}", &public_key[..4], &public_key[public_key.len()-4..]) + format!( + "{}...{}", + &public_key[..4], + &public_key[public_key.len() - 4..] + ) } else { public_key.clone() }; @@ -57,7 +61,7 @@ pub fn auth_view(props: &AuthViewProps) -> Html { } } AuthState::Authenticating => { - html! { + html! {
{ "Authenticating..." }
diff --git a/src/app/src/components/book_viewer.rs b/src/app/src/components/book_viewer.rs index 59c382d..a0e2e87 100644 --- a/src/app/src/components/book_viewer.rs +++ b/src/app/src/components/book_viewer.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use heromodels::models::library::items::{Book, TocEntry}; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct BookViewerProps { @@ -22,9 +22,7 @@ impl Component for BookViewer { type Properties = BookViewerProps; fn create(_ctx: &Context) -> Self { - Self { - current_page: 0, - } + Self { current_page: 0 } } fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { @@ -52,14 +50,14 @@ impl Component for BookViewer { fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); let total_pages = props.book.pages.len(); - + let back_handler = { let on_back = props.on_back.clone(); Callback::from(move |_: MouseEvent| { on_back.emit(()); }) }; - + let prev_handler = ctx.link().callback(|_: MouseEvent| BookViewerMsg::PrevPage); let next_handler = ctx.link().callback(|_: MouseEvent| BookViewerMsg::NextPage); @@ -120,7 +118,7 @@ impl BookViewer { } else if line.starts_with("- ") { html_content.push(html! {
  • { &line[2..] }
  • }); } else if line.starts_with("**") && line.ends_with("**") { - let text = &line[2..line.len()-2]; + let text = &line[2..line.len() - 2]; html_content.push(html! {

    { text }

    }); } else if !line.trim().is_empty() { html_content.push(html! {

    { line }

    }); @@ -152,4 +150,4 @@ impl BookViewer { } } -} \ No newline at end of file +} diff --git a/src/app/src/components/chat.rs b/src/app/src/components/chat.rs index 19a24b0..f158241 100644 --- a/src/app/src/components/chat.rs +++ b/src/app/src/components/chat.rs @@ -1,7 +1,7 @@ -use yew::prelude::*; use chrono::{DateTime, Utc}; -use wasm_bindgen::JsCast; use std::collections::HashMap; +use wasm_bindgen::JsCast; +use yew::prelude::*; #[derive(Clone, Debug, PartialEq)] pub struct ChatMessage { @@ -107,7 +107,7 @@ impl Component for ChatInterface { next_message_id: 0, next_conversation_id: 1, }; - + // Create initial conversation if none exists if chat_interface.active_conversation_id.is_none() { chat_interface.create_new_conversation(); @@ -117,7 +117,7 @@ impl Component for ChatInterface { callback.emit(summaries); } } - + chat_interface } @@ -141,11 +141,15 @@ impl Component for ChatInterface { if self.active_conversation_id.is_none() { self.create_new_conversation(); } - + let conversation_id = self.active_conversation_id.unwrap(); - + // Add user message to active conversation - let input_format = ctx.props().input_format.clone().unwrap_or_else(|| "text".to_string()); + let input_format = ctx + .props() + .input_format + .clone() + .unwrap_or_else(|| "text".to_string()); let user_message = ChatMessage { id: self.next_message_id, content: self.current_input.clone(), @@ -157,11 +161,11 @@ impl Component for ChatInterface { format: input_format.clone(), source: None, }; - + if let Some(conversation) = self.conversations.get_mut(&conversation_id) { conversation.messages.push(user_message); conversation.last_updated = chrono::Utc::now().to_rfc3339(); - + // Update conversation title if it's the first message if conversation.messages.len() == 1 { let title = if self.current_input.len() > 50 { @@ -172,26 +176,30 @@ impl Component for ChatInterface { conversation.title = title; } } - + self.next_message_id += 1; // Process message through callback with response handler let input_data = self.current_input.as_bytes().to_vec(); - + // Create response callback that adds responses to chat let link = ctx.link().clone(); let response_callback = Callback::from(move |response: ChatResponse| { link.send_message(ChatMsg::AddResponse(response)); }); - + // Trigger processing with response callback - ctx.props().on_process_message.emit((input_data, input_format, response_callback)); + ctx.props().on_process_message.emit(( + input_data, + input_format, + response_callback, + )); // Clear inputs self.current_input.clear(); self.current_title = None; self.current_description = None; - + // Notify parent of conversation updates self.notify_conversations_updated(ctx); } @@ -201,13 +209,13 @@ impl Component for ChatInterface { if let Some(conversation_id) = self.active_conversation_id { // Add response from async callback to active conversation let response_content = String::from_utf8_lossy(&response.data).to_string(); - + // Use the format provided by the response to determine status let status = match response.format.as_str() { "error" => "Error".to_string(), _ => "Ok".to_string(), }; - + let response_message = ChatMessage { id: self.next_message_id, content: response_content, @@ -219,14 +227,14 @@ impl Component for ChatInterface { format: response.format.clone(), source: Some(response.source.clone()), }; - + if let Some(conversation) = self.conversations.get_mut(&conversation_id) { conversation.messages.push(response_message); conversation.last_updated = chrono::Utc::now().to_rfc3339(); } - + self.next_message_id += 1; - + // Notify parent of conversation updates self.notify_conversations_updated(ctx); } @@ -259,7 +267,7 @@ impl Component for ChatInterface { fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { let mut should_update = false; - + // Handle external conversation selection if let Some(new_active_id) = ctx.props().external_conversation_selection { if old_props.external_conversation_selection != Some(new_active_id) { @@ -269,7 +277,7 @@ impl Component for ChatInterface { } } } - + // Handle external new conversation trigger if let Some(trigger) = ctx.props().external_new_conversation_trigger { if old_props.external_new_conversation_trigger != Some(trigger) && trigger { @@ -278,24 +286,25 @@ impl Component for ChatInterface { should_update = true; } } - + should_update } fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); - + let on_input = { let link = ctx.link().clone(); Callback::from(move |e: InputEvent| { let target = e.target().unwrap(); - let value = if let Ok(input) = target.clone().dyn_into::() { - input.value() - } else if let Ok(textarea) = target.dyn_into::() { - textarea.value() - } else { - String::new() - }; + let value = + if let Ok(input) = target.clone().dyn_into::() { + input.value() + } else if let Ok(textarea) = target.dyn_into::() { + textarea.value() + } else { + String::new() + }; link.send_message(ChatMsg::UpdateInput(value)); }) }; @@ -327,7 +336,8 @@ impl Component for ChatInterface { // Get current conversation messages let empty_messages = Vec::new(); let current_messages = if let Some(conversation_id) = self.active_conversation_id { - self.conversations.get(&conversation_id) + self.conversations + .get(&conversation_id) .map(|conv| &conv.messages) .unwrap_or(&empty_messages) } else { @@ -336,7 +346,8 @@ impl Component for ChatInterface { // Get conversation title let conversation_title = if let Some(conversation_id) = self.active_conversation_id { - self.conversations.get(&conversation_id) + self.conversations + .get(&conversation_id) .map(|conv| conv.title.clone()) .or_else(|| props.conversation_title.clone()) } else { @@ -437,31 +448,33 @@ impl ChatInterface { created_at: now.clone(), last_updated: now, }; - - self.conversations.insert(self.next_conversation_id, conversation); + + self.conversations + .insert(self.next_conversation_id, conversation); self.active_conversation_id = Some(self.next_conversation_id); self.next_conversation_id += 1; } - + fn notify_conversations_updated(&self, ctx: &Context) { if let Some(callback) = &ctx.props().on_conversations_updated { let summaries = self.get_conversation_summaries(); callback.emit(summaries); } } - + fn get_conversation_summaries(&self) -> Vec { - let mut summaries: Vec<_> = self.conversations.values() + let mut summaries: Vec<_> = self + .conversations + .values() .map(|conv| { - let last_message_preview = conv.messages.last() - .map(|msg| { - if msg.content.len() > 50 { - format!("{}...", &msg.content[..47]) - } else { - msg.content.clone() - } - }); - + let last_message_preview = conv.messages.last().map(|msg| { + if msg.content.len() > 50 { + format!("{}...", &msg.content[..47]) + } else { + msg.content.clone() + } + }); + ConversationSummary { id: conv.id, title: conv.title.clone(), @@ -469,22 +482,22 @@ impl ChatInterface { } }) .collect(); - + // Sort by last updated (most recent first) summaries.sort_by(|a, b| { let a_conv = self.conversations.get(&a.id).unwrap(); let b_conv = self.conversations.get(&b.id).unwrap(); b_conv.last_updated.cmp(&a_conv.last_updated) }); - + summaries } - + pub fn new_conversation(&mut self) -> u32 { self.create_new_conversation(); self.active_conversation_id.unwrap() } - + pub fn select_conversation(&mut self, conversation_id: u32) -> bool { if self.conversations.contains_key(&conversation_id) { self.active_conversation_id = Some(conversation_id); @@ -493,7 +506,7 @@ impl ChatInterface { false } } - + pub fn get_conversations(&self) -> Vec { self.get_conversation_summaries() } @@ -506,20 +519,22 @@ fn view_chat_message(msg: &ChatMessage) -> Html { ChatSender::Assistant => "ai-message", ChatSender::System => "system-message", }; - + // Use source name for responses, fallback to default names let sender_name = match msg.sender { ChatSender::User => "You".to_string(), - ChatSender::Assistant => { - msg.source.as_ref().unwrap_or(&"Assistant".to_string()).clone() - }, + ChatSender::Assistant => msg + .source + .as_ref() + .unwrap_or(&"Assistant".to_string()) + .clone(), ChatSender::System => "System".to_string(), }; // Add format-specific classes let mut message_classes = vec!["message".to_string(), sender_class.to_string()]; message_classes.push(format!("format-{}", msg.format)); - + // Add error class if it's an error message if msg.status.as_ref().map_or(false, |s| s == "Error") { message_classes.push("message-error".to_string()); @@ -577,13 +592,13 @@ fn render_message_content(content: &str, format: &str) -> Html { }, _ => html! {
    { content }
    - } + }, } } fn render_code_with_line_numbers(content: &str, language: &str) -> Html { let lines: Vec<&str> = content.lines().collect(); - + html! {
    @@ -645,7 +660,7 @@ pub fn conversation_list(props: &ConversationListProps) -> Html { on_select_conversation.emit(conv_id); }) }; - + html! {
  • { &conv.title }
    @@ -662,4 +677,4 @@ pub fn conversation_list(props: &ConversationListProps) -> Html {
  • } -} \ No newline at end of file +} diff --git a/src/app/src/components/circles_view.rs b/src/app/src/components/circles_view.rs index eadaa85..a8f642e 100644 --- a/src/app/src/components/circles_view.rs +++ b/src/app/src/components/circles_view.rs @@ -1,10 +1,10 @@ use heromodels::models::circle::Circle; -use yew::prelude::*; -use yew::functional::Reducible; use std::collections::HashMap; use std::rc::Rc; use wasm_bindgen_futures::spawn_local; use web_sys::WheelEvent; +use yew::functional::Reducible; +use yew::prelude::*; use crate::ws_manager::fetch_data_from_ws_url; @@ -47,12 +47,12 @@ pub struct CirclesView { // Two primary dynamic states center_circle: String, is_selected: bool, - + // Supporting state circles: HashMap, navigation_stack: Vec, loading_states: HashMap, - + // Rotation state for surrounding circles rotation_value: i32, } @@ -64,9 +64,12 @@ impl Component for CirclesView { fn create(ctx: &Context) -> Self { let props = ctx.props(); let center_ws_url = props.default_center_ws_url.clone(); - - log::info!("CirclesView: Creating component with center circle: {}", center_ws_url); - + + log::info!( + "CirclesView: Creating component with center circle: {}", + center_ws_url + ); + let mut component = Self { center_circle: center_ws_url.clone(), is_selected: false, @@ -86,39 +89,43 @@ impl Component for CirclesView { match msg { CirclesViewMsg::CenterCircleFetched(mut circle) => { log::info!("CirclesView: Center circle fetched: {}", circle.title); - + // Ensure circle has correct ws_url if circle.ws_url.is_empty() { circle.ws_url = self.center_circle.clone(); } - + // Store center circle self.circles.insert(circle.ws_url.clone(), circle.clone()); - + // Start fetching surrounding circles progressively self.start_surrounding_circles_fetch(ctx, &circle); - + // Update context immediately with center circle self.update_circles_context(ctx); - + true } CirclesViewMsg::SurroundingCircleFetched(ws_url, result) => { - log::debug!("CirclesView: Surrounding circle fetch result for {}: {:?}", ws_url, result.is_ok()); - + log::debug!( + "CirclesView: Surrounding circle fetch result for {}: {:?}", + ws_url, + result.is_ok() + ); + // Remove from loading states self.loading_states.remove(&ws_url); - + match result { Ok(mut circle) => { // Ensure circle has correct ws_url if circle.ws_url.is_empty() { circle.ws_url = ws_url.clone(); } - + // Store the circle self.circles.insert(ws_url, circle); - + // Update context with new circle available self.update_circles_context(ctx); } @@ -127,15 +134,11 @@ impl Component for CirclesView { // Continue without this circle - don't block the UI } } - + true } - CirclesViewMsg::CircleClicked(ws_url) => { - self.handle_circle_click(ctx, ws_url) - } - CirclesViewMsg::BackgroundClicked => { - self.handle_background_click(ctx) - } + CirclesViewMsg::CircleClicked(ws_url) => self.handle_circle_click(ctx, ws_url), + CirclesViewMsg::BackgroundClicked => self.handle_background_click(ctx), CirclesViewMsg::RotateCircles(delta) => { self.rotation_value += delta; log::debug!("CirclesView: Rotation updated to: {}", self.rotation_value); @@ -145,18 +148,24 @@ impl Component for CirclesView { } fn view(&self, ctx: &Context) -> Html { - log::debug!("CirclesView: Rendering view. Center: {}, Circles loaded: {}, Selected: {}", - self.center_circle, self.circles.len(), self.is_selected); - + log::debug!( + "CirclesView: Rendering view. Center: {}, Circles loaded: {}, Selected: {}", + self.center_circle, + self.circles.len(), + self.is_selected + ); + let center_circle_data = self.circles.get(&self.center_circle); - + // Get surrounding circles only if center is not selected let surrounding_circles_data: Vec<&Circle> = if self.is_selected { Vec::new() } else { // Get surrounding circles from center circle's circles field if let Some(center_data) = center_circle_data { - center_data.circles.iter() + center_data + .circles + .iter() .filter_map(|ws_url| self.circles.get(ws_url)) .collect() } else { @@ -165,8 +174,9 @@ impl Component for CirclesView { }; let link = ctx.link(); - let on_background_click_handler = link.callback(|_: MouseEvent| CirclesViewMsg::BackgroundClicked); - + let on_background_click_handler = + link.callback(|_: MouseEvent| CirclesViewMsg::BackgroundClicked); + // Add wheel event handler for rotation let on_wheel_handler = { let link = link.clone(); @@ -177,19 +187,25 @@ impl Component for CirclesView { }) }; - let petals_html: Vec = surrounding_circles_data.iter().enumerate().map(|(original_idx, circle_data)| { - // Calculate rotated position index based on rotation value - let total_circles = surrounding_circles_data.len(); - let rotation_steps = (self.rotation_value / 60) % total_circles as i32; // 60 degrees per step - let rotated_idx = ((original_idx as i32 + rotation_steps) % total_circles as i32 + total_circles as i32) % total_circles as i32; - - self.render_circle_element( - circle_data, - false, // is_center - Some(rotated_idx as usize), // rotated position_index - link, - ) - }).collect(); + let petals_html: Vec = surrounding_circles_data + .iter() + .enumerate() + .map(|(original_idx, circle_data)| { + // Calculate rotated position index based on rotation value + let total_circles = surrounding_circles_data.len(); + let rotation_steps = (self.rotation_value / 60) % total_circles as i32; // 60 degrees per step + let rotated_idx = ((original_idx as i32 + rotation_steps) % total_circles as i32 + + total_circles as i32) + % total_circles as i32; + + self.render_circle_element( + circle_data, + false, // is_center + Some(rotated_idx as usize), // rotated position_index + link, + ) + }) + .collect(); html! {
    , ws_url: &str) { log::debug!("CirclesView: Fetching center circle from {}", ws_url); - + let link = ctx.link().clone(); let ws_url_clone = ws_url.to_string(); - + spawn_local(async move { match fetch_data_from_ws_url::(&ws_url_clone, "get_circle().json()").await { Ok(circle) => { link.send_message(CirclesViewMsg::CenterCircleFetched(circle)); } Err(error) => { - log::error!("CirclesView: Failed to fetch center circle from {}: {}", ws_url_clone, error); + log::error!( + "CirclesView: Failed to fetch center circle from {}: {}", + ws_url_clone, + error + ); // Could emit an error message here if needed } } }); } - + /// Start progressive fetching of surrounding circles fn start_surrounding_circles_fetch(&mut self, ctx: &Context, center_circle: &Circle) { - log::info!("CirclesView: Starting progressive fetch of {} surrounding circles", center_circle.circles.len()); - + log::info!( + "CirclesView: Starting progressive fetch of {} surrounding circles", + center_circle.circles.len() + ); + for surrounding_ws_url in ¢er_circle.circles { self.fetch_surrounding_circle(ctx, surrounding_ws_url); } } - + /// Fetch individual surrounding circle fn fetch_surrounding_circle(&mut self, ctx: &Context, ws_url: &str) { log::debug!("CirclesView: Fetching surrounding circle from {}", ws_url); - + // Mark as loading self.loading_states.insert(ws_url.to_string(), true); - + let link = ctx.link().clone(); let ws_url_clone = ws_url.to_string(); - + spawn_local(async move { - let result = fetch_data_from_ws_url::(&ws_url_clone, "get_circle().json()").await; - link.send_message(CirclesViewMsg::SurroundingCircleFetched(ws_url_clone, result)); + let result = + fetch_data_from_ws_url::(&ws_url_clone, "get_circle().json()").await; + link.send_message(CirclesViewMsg::SurroundingCircleFetched( + ws_url_clone, + result, + )); }); } - + /// Update circles context and notify parent fn update_circles_context(&self, ctx: &Context) { let context_urls = if self.is_selected { @@ -268,7 +295,7 @@ impl CirclesView { } else { // When unselected, context includes center + available surrounding circles let mut urls = vec![self.center_circle.clone()]; - + if let Some(center_circle) = self.circles.get(&self.center_circle) { // Add surrounding circles that are already loaded for surrounding_url in ¢er_circle.circles { @@ -277,36 +304,42 @@ impl CirclesView { } } } - + urls }; - - log::debug!("CirclesView: Updating context with {} URLs", context_urls.len()); + + log::debug!( + "CirclesView: Updating context with {} URLs", + context_urls.len() + ); ctx.props().on_context_update.emit(context_urls); } - + /// Handle circle click logic fn handle_circle_click(&mut self, ctx: &Context, ws_url: String) -> bool { log::debug!("CirclesView: Circle clicked: {}", ws_url); - + if ws_url == self.center_circle { // Center circle clicked - toggle selection self.is_selected = !self.is_selected; - log::info!("CirclesView: Center circle toggled, selected: {}", self.is_selected); + log::info!( + "CirclesView: Center circle toggled, selected: {}", + self.is_selected + ); } else { // Surrounding circle clicked - make it the new center log::info!("CirclesView: Setting new center circle: {}", ws_url); - + // Push current center to navigation stack BEFORE changing it self.push_to_navigation_stack(self.center_circle.clone()); - + // Set new center and unselect self.center_circle = ws_url.clone(); self.is_selected = false; - + // Now push the new center to the stack as well self.push_to_navigation_stack(self.center_circle.clone()); - + // Fetch new center circle if not already loaded if !self.circles.contains_key(&ws_url) { self.fetch_center_circle(ctx, &ws_url); @@ -317,18 +350,21 @@ impl CirclesView { } } } - + // Update context self.update_circles_context(ctx); - + true } - + /// Handle background click logic fn handle_background_click(&mut self, ctx: &Context) -> bool { - log::debug!("CirclesView: Background clicked, selected: {}, stack size: {}", - self.is_selected, self.navigation_stack.len()); - + log::debug!( + "CirclesView: Background clicked, selected: {}, stack size: {}", + self.is_selected, + self.navigation_stack.len() + ); + if self.is_selected { // If selected, unselect self.is_selected = false; @@ -336,11 +372,14 @@ impl CirclesView { } else { // If unselected, navigate back in stack if let Some(previous_center) = self.pop_from_navigation_stack() { - log::info!("CirclesView: Background click - navigating back to: {}", previous_center); - + log::info!( + "CirclesView: Background click - navigating back to: {}", + previous_center + ); + self.center_circle = previous_center.clone(); self.is_selected = false; - + // Fetch previous center if not loaded if !self.circles.contains_key(&previous_center) { self.fetch_center_circle(ctx, &previous_center); @@ -355,37 +394,52 @@ impl CirclesView { return false; // No change } } - + // Update context self.update_circles_context(ctx); - + true } - + /// Push circle to navigation stack fn push_to_navigation_stack(&mut self, ws_url: String) { // Only push if it's different from the current top if self.navigation_stack.last() != Some(&ws_url) { self.navigation_stack.push(ws_url.clone()); - log::debug!("CirclesView: Pushed {} to navigation stack: {:?}", ws_url, self.navigation_stack); + log::debug!( + "CirclesView: Pushed {} to navigation stack: {:?}", + ws_url, + self.navigation_stack + ); } else { - log::debug!("CirclesView: Not pushing {} - already at top of stack", ws_url); + log::debug!( + "CirclesView: Not pushing {} - already at top of stack", + ws_url + ); } } - + /// Pop circle from navigation stack and return the previous one fn pop_from_navigation_stack(&mut self) -> Option { if self.navigation_stack.len() > 1 { // Remove current center from stack let popped = self.navigation_stack.pop(); log::debug!("CirclesView: Popped {:?} from navigation stack", popped); - + // Return the previous center (now at the top of stack) let previous = self.navigation_stack.last().cloned(); - log::debug!("CirclesView: Navigation stack after pop: {:?}, returning: {:?}", self.navigation_stack, previous); + log::debug!( + "CirclesView: Navigation stack after pop: {:?}, returning: {:?}", + self.navigation_stack, + previous + ); previous } else { - log::debug!("CirclesView: Cannot navigate back - stack size: {}, stack: {:?}", self.navigation_stack.len(), self.navigation_stack); + log::debug!( + "CirclesView: Cannot navigate back - stack size: {}, stack: {:?}", + self.navigation_stack.len(), + self.navigation_stack + ); None } } @@ -398,7 +452,7 @@ impl CirclesView { ) -> Html { let ws_url = circle.ws_url.clone(); let show_description = is_center && self.is_selected; - + let on_click_handler = { let ws_url_clone = ws_url.clone(); link.callback(move |e: MouseEvent| { @@ -422,7 +476,7 @@ impl CirclesView { } } let class_name = class_name_parts.join(" "); - + let size = if is_center { if show_description { "400px" // Center circle, selected (description shown) diff --git a/src/app/src/components/customize_view.rs b/src/app/src/components/customize_view.rs index 75012fe..90b75c0 100644 --- a/src/app/src/components/customize_view.rs +++ b/src/app/src/components/customize_view.rs @@ -1,20 +1,19 @@ -use std::rc::Rc; -use std::collections::HashMap; -use yew::prelude::*; use heromodels::models::circle::Circle; +use std::collections::HashMap; +use std::rc::Rc; use web_sys::InputEvent; +use yew::prelude::*; // Import from common_models // Assuming AppMsg is used for updates. This might need to be specific to theme updates. use crate::app::Msg as AppMsg; - // --- Enum for Setting Control Types (can be kept local or moved if shared) --- #[derive(Clone, PartialEq, Debug)] pub enum ThemeSettingControlType { - ColorSelection(Vec), // List of color hex values + ColorSelection(Vec), // List of color hex values PatternSelection(Vec), // List of pattern names/classes - LogoSelection(Vec), // List of predefined logo symbols or image URLs + LogoSelection(Vec), // List of predefined logo symbols or image URLs Toggle, TextInput, // For URL input or custom text } @@ -48,11 +47,21 @@ fn get_theme_setting_definitions() -> Vec { label: "Primary Color".to_string(), description: "Main accent color for the interface.".to_string(), control_type: ThemeSettingControlType::ColorSelection(vec![ - "#3b82f6".to_string(), "#ef4444".to_string(), "#10b981".to_string(), - "#f59e0b".to_string(), "#8b5cf6".to_string(), "#06b6d4".to_string(), - "#ec4899".to_string(), "#84cc16".to_string(), "#f97316".to_string(), - "#6366f1".to_string(), "#14b8a6".to_string(), "#f43f5e".to_string(), - "#ffffff".to_string(), "#cbd5e1".to_string(), "#64748b".to_string(), + "#3b82f6".to_string(), + "#ef4444".to_string(), + "#10b981".to_string(), + "#f59e0b".to_string(), + "#8b5cf6".to_string(), + "#06b6d4".to_string(), + "#ec4899".to_string(), + "#84cc16".to_string(), + "#f97316".to_string(), + "#6366f1".to_string(), + "#14b8a6".to_string(), + "#f43f5e".to_string(), + "#ffffff".to_string(), + "#cbd5e1".to_string(), + "#64748b".to_string(), ]), default_value: "#3b82f6".to_string(), }, @@ -61,9 +70,16 @@ fn get_theme_setting_definitions() -> Vec { label: "Background Color".to_string(), description: "Overall background color.".to_string(), control_type: ThemeSettingControlType::ColorSelection(vec![ - "#000000".to_string(), "#0a0a0a".to_string(), "#121212".to_string(), "#18181b".to_string(), - "#1f2937".to_string(), "#374151".to_string(), "#4b5563".to_string(), - "#f9fafb".to_string(), "#f3f4f6".to_string(), "#e5e7eb".to_string(), + "#000000".to_string(), + "#0a0a0a".to_string(), + "#121212".to_string(), + "#18181b".to_string(), + "#1f2937".to_string(), + "#374151".to_string(), + "#4b5563".to_string(), + "#f9fafb".to_string(), + "#f3f4f6".to_string(), + "#e5e7eb".to_string(), ]), default_value: "#0a0a0a".to_string(), }, @@ -72,8 +88,12 @@ fn get_theme_setting_definitions() -> Vec { label: "Background Pattern".to_string(), description: "Subtle pattern for the background.".to_string(), control_type: ThemeSettingControlType::PatternSelection(vec![ - "none".to_string(), "dots".to_string(), "grid".to_string(), - "diagonal".to_string(), "waves".to_string(), "mesh".to_string(), + "none".to_string(), + "dots".to_string(), + "grid".to_string(), + "diagonal".to_string(), + "waves".to_string(), + "mesh".to_string(), ]), default_value: "none".to_string(), }, @@ -82,9 +102,18 @@ fn get_theme_setting_definitions() -> Vec { label: "Circle Logo/Symbol".to_string(), description: "Select a symbol or provide a URL below.".to_string(), control_type: ThemeSettingControlType::LogoSelection(vec![ - "◯".to_string(), "◆".to_string(), "★".to_string(), "▲".to_string(), - "●".to_string(), "■".to_string(), "🌍".to_string(), "🚀".to_string(), - "💎".to_string(), "🔥".to_string(), "⚡".to_string(), "🎯".to_string(), + "◯".to_string(), + "◆".to_string(), + "★".to_string(), + "▲".to_string(), + "●".to_string(), + "■".to_string(), + "🌍".to_string(), + "🚀".to_string(), + "💎".to_string(), + "🔥".to_string(), + "⚡".to_string(), + "🎯".to_string(), "custom_url".to_string(), // Represents using the URL input ]), default_value: "◯".to_string(), @@ -114,16 +143,18 @@ fn get_theme_setting_definitions() -> Vec { ] } - #[function_component(CustomizeViewComponent)] pub fn customize_view_component(props: &CustomizeViewProps) -> Html { let theme_definitions = get_theme_setting_definitions(); // Determine the active circle for customization - let active_circle_ws_url: Option = props.context_circle_ws_urls.as_ref() + let active_circle_ws_url: Option = props + .context_circle_ws_urls + .as_ref() .and_then(|ws_urls| ws_urls.first().cloned()); - let active_circle_theme: Option> = active_circle_ws_url.as_ref() + let active_circle_theme: Option> = active_circle_ws_url + .as_ref() .and_then(|ws_url| props.all_circles.get(ws_url)) // TODO: Re-implement theme handling. The canonical Circle struct does not have a direct 'theme' field. // .map(|circle_data| circle_data.theme.clone()); @@ -147,7 +178,7 @@ pub fn customize_view_component(props: &CustomizeViewProps) -> Html { let current_value = active_circle_theme.as_ref() .and_then(|theme| theme.get(&setting_def.key).cloned()) .unwrap_or_else(|| setting_def.default_value.clone()); - + render_setting_control( setting_def.clone(), current_value, @@ -171,7 +202,7 @@ fn render_setting_control( app_callback: Callback, ) -> Html { let setting_key = setting_def.key.clone(); - + let on_value_change = { let circle_ws_url_clone = circle_ws_url.clone(); let setting_key_clone = setting_key.clone(); @@ -211,7 +242,7 @@ fn render_setting_control( })}
    } - }, + } ThemeSettingControlType::PatternSelection(ref patterns) => { let on_select = on_value_change.clone(); html! { @@ -234,7 +265,7 @@ fn render_setting_control( })}
    } - }, + } ThemeSettingControlType::LogoSelection(ref logos) => { let on_select = on_value_change.clone(); html! { @@ -258,14 +289,18 @@ fn render_setting_control( })} } - }, + } ThemeSettingControlType::Toggle => { let checked = current_value.to_lowercase() == "true"; let on_toggle = { let on_value_change = on_value_change.clone(); Callback::from(move |e: Event| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); - on_value_change.emit(if input.checked() { "true".to_string() } else { "false".to_string() }); + on_value_change.emit(if input.checked() { + "true".to_string() + } else { + "false".to_string() + }); }) }; html! { @@ -274,7 +309,7 @@ fn render_setting_control( } - }, + } ThemeSettingControlType::TextInput => { let on_input = { let on_value_change = on_value_change.clone(); @@ -292,7 +327,7 @@ fn render_setting_control( oninput={on_input} /> } - }, + } }; html! { diff --git a/src/app/src/components/image_viewer.rs b/src/app/src/components/image_viewer.rs index 2213a45..679431f 100644 --- a/src/app/src/components/image_viewer.rs +++ b/src/app/src/components/image_viewer.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use heromodels::models::library::items::Image; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct ImageViewerProps { @@ -19,7 +19,7 @@ impl Component for ImageViewer { fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); - + let back_handler = { let on_back = props.on_back.clone(); Callback::from(move |_: MouseEvent| { @@ -45,4 +45,4 @@ impl Component for ImageViewer { } } -} \ No newline at end of file +} diff --git a/src/app/src/components/inspector_interact_tab.rs b/src/app/src/components/inspector_interact_tab.rs index c1ac1f5..8011f78 100644 --- a/src/app/src/components/inspector_interact_tab.rs +++ b/src/app/src/components/inspector_interact_tab.rs @@ -1,11 +1,13 @@ -use yew::prelude::*; -use std::rc::Rc; -use std::collections::HashMap; -use wasm_bindgen_futures::spawn_local; -use crate::components::chat::{ChatInterface, ConversationList, ConversationSummary, InputType, ChatResponse}; +use crate::components::chat::{ + ChatInterface, ChatResponse, ConversationList, ConversationSummary, InputType, +}; use crate::rhai_executor::execute_rhai_script_remote; use crate::ws_manager::fetch_data_from_ws_url; use heromodels::models::circle::Circle; +use std::collections::HashMap; +use std::rc::Rc; +use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct InspectorInteractTabProps { @@ -28,21 +30,23 @@ pub struct CircleInfo { #[function_component(InspectorInteractTab)] pub fn inspector_interact_tab(props: &InspectorInteractTabProps) -> Html { let circle_names = use_state(|| HashMap::::new()); - + // Fetch circle names when component mounts or addresses change { let circle_names = circle_names.clone(); let ws_addresses = props.circle_ws_addresses.clone(); - + use_effect_with(ws_addresses.clone(), move |addresses| { let circle_names = circle_names.clone(); - + for ws_url in addresses.iter() { let ws_url_clone = ws_url.clone(); let circle_names_clone = circle_names.clone(); - + spawn_local(async move { - match fetch_data_from_ws_url::(&ws_url_clone, "get_circle().json()").await { + match fetch_data_from_ws_url::(&ws_url_clone, "get_circle().json()") + .await + { Ok(circle) => { let mut names = (*circle_names_clone).clone(); names.insert(ws_url_clone, circle.title); @@ -51,13 +55,14 @@ pub fn inspector_interact_tab(props: &InspectorInteractTabProps) -> Html { Err(_) => { // If we can't fetch the circle name, use a fallback let mut names = (*circle_names_clone).clone(); - names.insert(ws_url_clone.clone(), format!("Circle ({})", ws_url_clone)); + names + .insert(ws_url_clone.clone(), format!("Circle ({})", ws_url_clone)); circle_names_clone.set(names); } } }); } - + || {} }); } @@ -65,41 +70,48 @@ pub fn inspector_interact_tab(props: &InspectorInteractTabProps) -> Html { let on_process_message = { let ws_urls = props.circle_ws_addresses.clone(); let circle_names = circle_names.clone(); - - Callback::from(move |(data, format, response_callback): (Vec, String, Callback)| { - // Convert bytes to string for processing - let script_content = String::from_utf8_lossy(&data).to_string(); - let urls = ws_urls.clone(); - let names = (*circle_names).clone(); - - // Remote execution - async responses - for ws_url in urls.iter() { - let script_clone = script_content.clone(); - let url_clone = ws_url.clone(); - let circle_name = names.get(ws_url).cloned().unwrap_or_else(|| format!("Circle ({})", ws_url)); - let format_clone = format.clone(); - let response_callback_clone = response_callback.clone(); - - spawn_local(async move { - let response = execute_rhai_script_remote(&script_clone, &url_clone, &circle_name).await; - let status = if response.success { "✅" } else { "❌" }; - - // Set format based on execution success - let response_format = if response.success { - format_clone - } else { - "error".to_string() - }; - - let chat_response = ChatResponse { - data: format!("{} {}", status, response.output).into_bytes(), - format: response_format, - source: response.source, - }; - response_callback_clone.emit(chat_response); - }); - } - }) + + Callback::from( + move |(data, format, response_callback): (Vec, String, Callback)| { + // Convert bytes to string for processing + let script_content = String::from_utf8_lossy(&data).to_string(); + let urls = ws_urls.clone(); + let names = (*circle_names).clone(); + + // Remote execution - async responses + for ws_url in urls.iter() { + let script_clone = script_content.clone(); + let url_clone = ws_url.clone(); + let circle_name = names + .get(ws_url) + .cloned() + .unwrap_or_else(|| format!("Circle ({})", ws_url)); + let format_clone = format.clone(); + let response_callback_clone = response_callback.clone(); + + spawn_local(async move { + let response = + execute_rhai_script_remote(&script_clone, &url_clone, &circle_name) + .await; + let status = if response.success { "✅" } else { "❌" }; + + // Set format based on execution success + let response_format = if response.success { + format_clone + } else { + "error".to_string() + }; + + let chat_response = ChatResponse { + data: format!("{} {}", status, response.output).into_bytes(), + format: response_format, + source: response.source, + }; + response_callback_clone.emit(chat_response); + }); + } + }, + ) }; html! { @@ -138,4 +150,4 @@ pub fn inspector_interact_sidebar(props: &InspectorInteractSidebarProps) -> Html title={"Chat History".to_string()} /> } -} \ No newline at end of file +} diff --git a/src/app/src/components/inspector_logs_tab.rs b/src/app/src/components/inspector_logs_tab.rs index 4cf7198..31ab700 100644 --- a/src/app/src/components/inspector_logs_tab.rs +++ b/src/app/src/components/inspector_logs_tab.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use std::rc::Rc; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct InspectorLogsTabProps { @@ -28,7 +28,10 @@ pub fn inspector_logs_tab(props: &InspectorLogsTabProps) -> Html { timestamp: "17:05:25".to_string(), level: "INFO".to_string(), source: "network".to_string(), - message: format!("Monitoring {} circle connections", props.circle_ws_addresses.len()), + message: format!( + "Monitoring {} circle connections", + props.circle_ws_addresses.len() + ), }, LogEntry { timestamp: "17:05:26".to_string(), @@ -59,7 +62,7 @@ pub fn inspector_logs_tab(props: &InspectorLogsTabProps) -> Html { 0 { "stat-warn" } else { "" })}>{warn_count} - +
    { for logs.iter().rev().map(|log| { let level_class = match log.level.as_str() { @@ -68,7 +71,7 @@ pub fn inspector_logs_tab(props: &InspectorLogsTabProps) -> Html { "INFO" => "log-info", _ => "log-debug", }; - + html! {
    {&log.timestamp} @@ -82,4 +85,4 @@ pub fn inspector_logs_tab(props: &InspectorLogsTabProps) -> Html {
    } -} \ No newline at end of file +} diff --git a/src/app/src/components/inspector_network_tab.rs b/src/app/src/components/inspector_network_tab.rs index d5d43f8..b95ee69 100644 --- a/src/app/src/components/inspector_network_tab.rs +++ b/src/app/src/components/inspector_network_tab.rs @@ -1,9 +1,9 @@ -use yew::prelude::*; -use std::rc::Rc; -use std::collections::HashMap; -use crate::components::world_map_svg::render_world_map_svg; use crate::components::network_animation_view::NetworkAnimationView; +use crate::components::world_map_svg::render_world_map_svg; use common_models::CircleData; +use std::collections::HashMap; +use std::rc::Rc; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct InspectorNetworkTabProps { @@ -25,30 +25,33 @@ pub fn inspector_network_tab(props: &InspectorNetworkTabProps) -> Html { // Create circle data for the map animation let circles_data = use_memo(props.circle_ws_addresses.clone(), |addresses| { let mut circles = HashMap::new(); - + for (index, ws_url) in addresses.iter().enumerate() { - circles.insert(index as u32 + 1, CircleData { - id: index as u32 + 1, - name: format!("Circle {}", index + 1), - description: format!("Circle at {}", ws_url), - ws_url: ws_url.clone(), - ws_urls: vec![], - theme: HashMap::new(), - tasks: None, - epics: None, - sprints: None, - proposals: None, - members: None, - library: None, - intelligence: None, - timeline: None, - calendar_events: None, - treasury: None, - publications: None, - deployments: None, - }); + circles.insert( + index as u32 + 1, + CircleData { + id: index as u32 + 1, + name: format!("Circle {}", index + 1), + description: format!("Circle at {}", ws_url), + ws_url: ws_url.clone(), + ws_urls: vec![], + theme: HashMap::new(), + tasks: None, + epics: None, + sprints: None, + proposals: None, + members: None, + library: None, + intelligence: None, + timeline: None, + calendar_events: None, + treasury: None, + publications: None, + deployments: None, + }, + ); } - + Rc::new(circles) }); @@ -117,7 +120,7 @@ pub fn inspector_network_tab(props: &InspectorNetworkTabProps) -> Html { { for traffic_entries.iter().map(|entry| { let direction_class = if entry.direction == "Sent" { "traffic-sent" } else { "traffic-received" }; let status_class = if entry.status == "Success" { "traffic-success" } else { "traffic-error" }; - + html! {
    {&entry.timestamp}
    @@ -133,4 +136,4 @@ pub fn inspector_network_tab(props: &InspectorNetworkTabProps) -> Html {
    } -} \ No newline at end of file +} diff --git a/src/app/src/components/inspector_view.rs b/src/app/src/components/inspector_view.rs index 6a0aaa6..09e4e98 100644 --- a/src/app/src/components/inspector_view.rs +++ b/src/app/src/components/inspector_view.rs @@ -1,12 +1,12 @@ -use yew::prelude::*; -use std::rc::Rc; -use crate::components::chat::{ConversationSummary}; -use crate::components::sidebar_layout::SidebarLayout; -use crate::components::inspector_network_tab::InspectorNetworkTab; -use crate::components::inspector_logs_tab::InspectorLogsTab; use crate::auth::AuthManager; +use crate::components::chat::ConversationSummary; use crate::components::inspector_auth_tab::InspectorAuthTab; -use crate::components::inspector_interact_tab::{InspectorInteractTab, InspectorInteractSidebar}; +use crate::components::inspector_interact_tab::{InspectorInteractSidebar, InspectorInteractTab}; +use crate::components::inspector_logs_tab::InspectorLogsTab; +use crate::components::inspector_network_tab::InspectorNetworkTab; +use crate::components::sidebar_layout::SidebarLayout; +use std::rc::Rc; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct InspectorViewProps { @@ -119,7 +119,7 @@ impl Component for InspectorView { } InspectorViewState::Tab(tab) => { let on_background_click = ctx.link().callback(|_| Msg::BackToOverview); - + let main_content = match tab { InspectorTab::Network => html! { @@ -143,9 +143,9 @@ impl Component for InspectorView { on_new_conversation={on_new_conv} /> } - }, + } InspectorTab::Auth => html! { - @@ -224,13 +224,11 @@ impl InspectorView { _ => false, }; let tab_clone = tab.clone(); - let onclick = ctx.link().callback(move |_| Msg::SelectTab(tab_clone.clone())); + let onclick = ctx + .link() + .callback(move |_| Msg::SelectTab(tab_clone.clone())); - let card_class = if is_selected { - "card selected" - } else { - "card" - }; + let card_class = if is_selected { "card selected" } else { "card" }; html! {
    @@ -292,7 +290,7 @@ impl InspectorView { fn render_network_connections_sidebar(&self, ctx: &Context) -> Html { let props = ctx.props(); let connected_count = props.circle_ws_addresses.len(); - + html! {
    @@ -335,4 +333,4 @@ impl InspectorView {
    } } -} \ No newline at end of file +} diff --git a/src/app/src/components/intelligence_view.rs b/src/app/src/components/intelligence_view.rs index a78bcb3..1f6cadf 100644 --- a/src/app/src/components/intelligence_view.rs +++ b/src/app/src/components/intelligence_view.rs @@ -1,13 +1,13 @@ -use yew::prelude::*; -use std::rc::Rc; -use std::collections::HashMap; use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use std::rc::Rc; use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; // Imports from common_models -use common_models::{AiMessageRole, AiConversation}; -use heromodels::models::circle::Circle; use crate::ws_manager::CircleWsManager; +use common_models::{AiConversation, AiMessageRole}; +use heromodels::models::circle::Circle; #[derive(Properties, PartialEq, Clone)] pub struct IntelligenceViewProps { @@ -21,7 +21,7 @@ pub enum IntelligenceMsg { SubmitPrompt, LoadConversation(u32), StartNewConversation, - CircleDataUpdated(String, Circle), // ws_url, circle_data + CircleDataUpdated(String, Circle), // ws_url, circle_data CircleDataFetchFailed(String, String), // ws_url, error ScriptExecuted(Result), } @@ -56,22 +56,20 @@ impl Component for IntelligenceView { fn create(ctx: &Context) -> Self { let ws_manager = CircleWsManager::new(); - + // Set up callback for circle data updates let link = ctx.link().clone(); - ws_manager.set_on_data_fetched( - link.callback(|(ws_url, result): (String, Result)| { - match result { - Ok(mut circle) => { - if circle.ws_url.is_empty() { - circle.ws_url = ws_url.clone(); - } - IntelligenceMsg::CircleDataUpdated(ws_url, circle) - }, - Err(e) => IntelligenceMsg::CircleDataFetchFailed(ws_url, e), + ws_manager.set_on_data_fetched(link.callback( + |(ws_url, result): (String, Result)| match result { + Ok(mut circle) => { + if circle.ws_url.is_empty() { + circle.ws_url = ws_url.clone(); + } + IntelligenceMsg::CircleDataUpdated(ws_url, circle) } - }) - ); + Err(e) => IntelligenceMsg::CircleDataFetchFailed(ws_url, e), + }, + )); Self { current_input: String::new(), @@ -128,7 +126,7 @@ impl Component for IntelligenceView { fn view(&self, ctx: &Context) -> Html { let link = ctx.link(); - + // Get aggregated conversations from context circles let (active_conversation, conversation_history) = self.get_conversation_data(ctx); @@ -214,7 +212,10 @@ impl Component for IntelligenceView { } impl IntelligenceView { - fn get_conversation_data(&self, _ctx: &Context) -> (Option>, Vec) { + fn get_conversation_data( + &self, + _ctx: &Context, + ) -> (Option>, Vec) { // TODO: The Circle model does not currently have an `intelligence` field. // This logic is temporarily disabled to allow compilation. // We need to determine how to fetch and associate AI conversations with circles. @@ -231,7 +232,8 @@ impl IntelligenceView { // Get target circle for the prompt let props = ctx.props(); - let target_ws_url = props.context_circle_ws_urls + let target_ws_url = props + .context_circle_ws_urls .as_ref() .and_then(|urls| urls.first()) .cloned(); @@ -256,7 +258,10 @@ impl IntelligenceView { link.send_message(IntelligenceMsg::ScriptExecuted(Ok(result.output))); } Err(e) => { - link.send_message(IntelligenceMsg::ScriptExecuted(Err(format!("{:?}", e)))); + link.send_message(IntelligenceMsg::ScriptExecuted(Err(format!( + "{:?}", + e + )))); } } }); @@ -268,7 +273,8 @@ impl IntelligenceView { let script = r#" let intelligence = get_intelligence(); intelligence - "#.to_string(); + "# + .to_string(); if let Some(script_future) = self.ws_manager.execute_script(ws_url, script) { spawn_local(async move { diff --git a/src/app/src/components/library_view.rs b/src/app/src/components/library_view.rs index 5f66560..cd0098d 100644 --- a/src/app/src/components/library_view.rs +++ b/src/app/src/components/library_view.rs @@ -1,25 +1,20 @@ -use std::rc::Rc; -use std::collections::HashMap; -use yew::prelude::*; -use wasm_bindgen_futures::spawn_local; -use heromodels::models::library::collection::Collection; -use heromodels::models::library::items::{Image, Pdf, Markdown, Book, Slides}; -use crate::ws_manager::{fetch_data_from_ws_urls, fetch_data_from_ws_url}; use crate::components::{ - book_viewer::BookViewer, - slides_viewer::SlidesViewer, - image_viewer::ImageViewer, - pdf_viewer::PdfViewer, - markdown_viewer::MarkdownViewer, - asset_details_card::AssetDetailsCard, + asset_details_card::AssetDetailsCard, book_viewer::BookViewer, image_viewer::ImageViewer, + markdown_viewer::MarkdownViewer, pdf_viewer::PdfViewer, slides_viewer::SlidesViewer, }; +use crate::ws_manager::{fetch_data_from_ws_url, fetch_data_from_ws_urls}; +use heromodels::models::library::collection::Collection; +use heromodels::models::library::items::{Book, Image, Markdown, Pdf, Slides}; +use std::collections::HashMap; +use std::rc::Rc; +use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; #[derive(Clone, PartialEq, Properties)] pub struct LibraryViewProps { pub ws_addresses: Vec, } - #[derive(Clone, Debug, PartialEq)] pub enum DisplayLibraryItem { Image(Image), @@ -71,7 +66,7 @@ impl Component for LibraryView { fn create(ctx: &Context) -> Self { let props = ctx.props(); let ws_addresses = props.ws_addresses.clone(); - + let link = ctx.link().clone(); spawn_local(async move { let collections = get_collections(&ws_addresses).await; @@ -93,10 +88,10 @@ impl Component for LibraryView { if ctx.props().ws_addresses != old_props.ws_addresses { let ws_addresses = ctx.props().ws_addresses.clone(); let link = ctx.link().clone(); - + self.loading = true; self.error = None; - + spawn_local(async move { let collections = get_collections(&ws_addresses).await; link.send_message(Msg::CollectionsFetched(collections)); @@ -108,10 +103,13 @@ impl Component for LibraryView { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { Msg::CollectionsFetched(collections) => { - log::info!("Collections fetched: {:?}", collections.keys().collect::>()); + log::info!( + "Collections fetched: {:?}", + collections.keys().collect::>() + ); self.collections = collections.clone(); self.loading = false; - + // Convert collections to display collections and start fetching items for (collection_key, collection) in collections { let ws_url = collection_key.split('_').next().unwrap_or("").to_string(); @@ -123,24 +121,27 @@ impl Component for LibraryView { collection_key: collection_key.clone(), }; self.display_collections.push(display_collection); - + // Fetch items for this collection let link = ctx.link().clone(); let collection_clone = collection.clone(); let collection_key_clone = collection_key.clone(); - + spawn_local(async move { let items = fetch_collection_items(&ws_url, &collection_clone).await; link.send_message(Msg::ItemsFetched(collection_key_clone, items)); }); } - + true } Msg::ItemsFetched(collection_key, items) => { // Find the display collection and update its items using exact key matching - if let Some(display_collection) = self.display_collections.iter_mut() - .find(|dc| dc.collection_key == collection_key) { + if let Some(display_collection) = self + .display_collections + .iter_mut() + .find(|dc| dc.collection_key == collection_key) + { display_collection.items = items.into_iter().map(Rc::new).collect(); } true @@ -177,7 +178,7 @@ impl Component for LibraryView { let toc_callback = Callback::from(|_page: usize| { // TOC navigation is now handled by the BookViewer component }); - + html! {