add circles app and libraries

This commit is contained in:
timurgordon 2025-06-19 05:17:14 +03:00
parent ae3077033b
commit 32bcef1d1d
162 changed files with 34903 additions and 1667 deletions

6
.gitignore vendored
View File

@ -1 +1,5 @@
target
target
dump.rdb
worker_rhai_temp_db
launch_data
.DS_Store

View File

@ -1,201 +0,0 @@
# Architecture: Circle Management System
## 1. Introduction & Overview
This document outlines the architecture for a system that manages multiple "Circles." Each Circle is an independent entity comprising its own database, a Rhai scripting engine, a dedicated Rhai worker process, and a WebSocket (WS) server for external interaction. A central command-line orchestrator will be responsible for initializing, running, and monitoring these Circles based on a configuration file.
The primary goal is to allow users to define multiple isolated Rhai environments, each accessible via a unique WebSocket endpoint, and for scripts executed within a circle to interact with that circle's dedicated persistent storage.
## 2. Goals
* Create a command-line application (`circles_orchestrator`) to manage the lifecycle of multiple Circles.
* Each Circle will have:
* An independent `OurDB` instance for data persistence.
* A dedicated `Rhai Engine` configured with its `OurDB`.
* A dedicated `Rhai Worker` processing scripts for that circle.
* A dedicated `WebSocket Server` exposing an endpoint for that circle.
* Circle configurations (name, ID, port) will be loaded from a `circles.json` file.
* The orchestrator will display a status table of all running circles, including their worker queue and WS server URL.
* Utilize existing crates (`ourdb`, `rhai_engine`, `rhai_worker`, `rhai_client`, `server_ws`) with necessary refactoring to support library usage.
## 3. System Components
* **Orchestrator (`circles_orchestrator`)**:
* Location: `/Users/timurgordon/code/git.ourworld.tf/herocode/circles/cmd/src/main.rs`
* Role: Parses `circles.json`, initializes, spawns, and monitors all components for each defined circle. Displays system status.
* **Circle Configuration (`circles.json`)**:
* Location: e.g., `/Users/timurgordon/code/git.ourworld.tf/herocode/circles/cmd/circles.json`
* Role: Defines the set of circles to be managed, including their ID, name, and port.
* **OurDB (Per Circle)**:
* Library: `/Users/timurgordon/code/git.ourworld.tf/herocode/db/ourdb/src/lib.rs`
* Role: Provides persistent key-value storage for each circle, configured in incremental mode. Instance data stored at `~/.hero/circles/{id}/`.
* **Rhai Engine (Per Circle)**:
* Library: `/Users/timurgordon/code/git.ourworld.tf/herocode/rhailib/src/engine/src/lib.rs`
* Role: Provides the Rhai scripting environment for a circle, configured with the circle's specific `OurDB` instance.
* **Rhai Worker (Per Circle)**:
* Library: `/Users/timurgordon/code/git.ourworld.tf/herocode/rhailib/src/worker/src/lib.rs`
* Role: Executes Rhai scripts for a specific circle. Listens on a dedicated Redis queue for tasks and uses the circle's `Rhai Engine`.
* **Rhai Client (Per Circle WS Server)**:
* Library: `/Users/timurgordon/code/git.ourworld.tf/herocode/rhailib/src/client/src/lib.rs`
* Role: Used by a `Circle WebSocket Server` to send script execution tasks to its corresponding `Rhai Worker` via Redis.
* **Circle WebSocket Server (Per Circle)**:
* Library: `/Users/timurgordon/code/git.ourworld.tf/herocode/circles/server_ws/src/lib.rs`
* Role: Exposes a WebSocket endpoint for a specific circle, allowing external clients to submit Rhai scripts for execution within that circle.
* **Redis**:
* URL: `redis://127.0.0.1:6379`
* Role: Acts as the message broker between `Rhai Clients` (within WS Servers) and `Rhai Workers`.
## 4. High-Level Design Aspects
### 4.1. Orchestrator Logic (Conceptual)
The `circles_orchestrator` reads the `circles.json` configuration. For each defined circle, it:
1. Determines the `OurDB` path based on the circle's ID.
2. Initializes the `OurDB` instance.
3. Creates a `Rhai Engine` configured with this `OurDB`.
4. Spawns a `Rhai Worker` task, providing it with the engine and the circle's identity (for Redis queue naming).
5. Spawns a `Circle WebSocket Server` task, providing it with the circle's identity and port.
It then monitors these components and displays their status.
### 4.2. Database Path Convention
* Base: System user's home directory (e.g., `/Users/username` or `/home/username`).
* Structure: `~/.hero/circles/{CIRCLE_ID}/`
* Example: For a circle with ID `1`, the database path would be `~/.hero/circles/1/`.
### 4.3. Configuration File Format (`circles.json`)
A JSON array of objects. Each object represents a circle:
* `id`: `u32` - Unique identifier.
* `name`: `String` - Human-readable name.
* `port`: `u16` - Port for the WebSocket server.
```json
[
{ "id": 1, "name": "Alpha Circle", "port": 8081 },
{ "id": 2, "name": "Beta Circle", "port": 8082 }
]
```
### 4.4. Conceptual Output Table Format (Orchestrator)
| Circle Name | ID | Worker Status | Worker Queues | WS Server URL |
|--------------|----|---------------|------------------------------------|------------------------|
| Alpha Circle | 1 | Running | `rhai_tasks:alpha_circle` | `ws://127.0.0.1:8081/ws` |
### 4.5. Interaction Flow (Single Circle)
1. An external client connects to a specific Circle's WebSocket Server.
2. The client sends a Rhai script via a JSON-RPC message.
3. The WS Server uses its embedded `Rhai Client` to publish the script and task details to a Redis queue specific to that circle (e.g., `rhai_tasks:alpha_circle`).
4. The `Rhai Worker` for that circle picks up the task from its Redis queue.
5. The Worker uses its `Rhai Engine` (which is configured with the circle's `OurDB`) to execute the script.
6. Any database interactions within the script go through the circle's `OurDB`.
7. The Worker updates the task status and result/error in Redis.
8. The `Rhai Client` (in the WS Server), which has been polling Redis for the result, receives the update.
9. The WS Server sends the script's result or error back to the external client via WebSocket.
## 5. Diagrams
### 5.1. Component Diagram
```mermaid
graph TD
UserInterface[User/Admin] -- Manages --> OrchestratorCli{circles_orchestrator}
OrchestratorCli -- Reads --> CirclesJson[circles.json]
subgraph Circle 1
direction LR
OrchestratorCli -- Spawns/Manages --> C1_OurDB[(OurDB @ ~/.hero/circles/1)]
OrchestratorCli -- Spawns/Manages --> C1_RhaiEngine[Rhai Engine 1]
OrchestratorCli -- Spawns/Manages --> C1_RhaiWorker[Rhai Worker 1]
OrchestratorCli -- Spawns/Manages --> C1_WSServer[WS Server 1 @ Port 8081]
C1_RhaiEngine -- Uses --> C1_OurDB
C1_RhaiWorker -- Uses --> C1_RhaiEngine
C1_WSServer -- Contains --> C1_RhaiClient[Rhai Client 1]
end
subgraph Circle 2
direction LR
OrchestratorCli -- Spawns/Manages --> C2_OurDB[(OurDB @ ~/.hero/circles/2)]
OrchestratorCli -- Spawns/Manages --> C2_RhaiEngine[Rhai Engine 2]
OrchestratorCli -- Spawns/Manages --> C2_RhaiWorker[Rhai Worker 2]
OrchestratorCli -- Spawns/Manages --> C2_WSServer[WS Server 2 @ Port 8082]
C2_RhaiEngine -- Uses --> C2_OurDB
C2_RhaiWorker -- Uses --> C2_RhaiEngine
C2_WSServer -- Contains --> C2_RhaiClient[Rhai Client 2]
end
C1_RhaiWorker -- Listens/Publishes --> Redis[(Redis @ 127.0.0.1:6379)]
C1_RhaiClient -- Publishes/Subscribes --> Redis
C2_RhaiWorker -- Listens/Publishes --> Redis
C2_RhaiClient -- Publishes/Subscribes --> Redis
ExternalWSClient1[External WS Client] -- Connects --> C1_WSServer
ExternalWSClient2[External WS Client] -- Connects --> C2_WSServer
```
### 5.2. Sequence Diagram (Request Flow for one Circle)
```mermaid
sequenceDiagram
participant ExtWSClient as External WS Client
participant CircleWSServer as Circle WS Server (e.g., Port 8081)
participant RhaiClientLib as Rhai Client Library (in WS Server)
participant RedisBroker as Redis
participant RhaiWorker as Rhai Worker (for the circle)
participant RhaiEngineLib as Rhai Engine Library (in Worker)
participant CircleOurDB as OurDB (for the circle)
ExtWSClient ->>+ CircleWSServer: Send Rhai Script (JSON-RPC "play" over WS)
CircleWSServer ->>+ RhaiClientLib: submit_script_and_await_result(circle_name, script, ...)
RhaiClientLib ->>+ RedisBroker: LPUSH rhai_tasks:circle_name (task_id)
RhaiClientLib ->>+ RedisBroker: HSET rhai_task_details:task_id (script details)
RhaiClientLib -->>- CircleWSServer: Returns task_id (internally starts polling)
RhaiWorker ->>+ RedisBroker: BLPOP rhai_tasks:circle_name (blocks)
RedisBroker -->>- RhaiWorker: Returns task_id
RhaiWorker ->>+ RedisBroker: HGETALL rhai_task_details:task_id
RedisBroker -->>- RhaiWorker: Returns script details
RhaiWorker ->>+ RhaiEngineLib: eval_with_scope(script)
RhaiEngineLib ->>+ CircleOurDB: DB Operations (if script interacts with DB)
CircleOurDB -->>- RhaiEngineLib: DB Results
RhaiEngineLib -->>- RhaiWorker: Script Result/Error
RhaiWorker ->>+ RedisBroker: HSET rhai_task_details:task_id (status="completed/error", output/error)
RhaiClientLib ->>+ RedisBroker: HGETALL rhai_task_details:task_id (polling)
RedisBroker -->>- RhaiClientLib: Task details (status, output/error)
alt Task Completed
RhaiClientLib -->>- CircleWSServer: Result (output)
CircleWSServer -->>- ExtWSClient: WS Response (JSON-RPC with result)
else Task Errored
RhaiClientLib -->>- CircleWSServer: Error (error message)
CircleWSServer -->>- ExtWSClient: WS Response (JSON-RPC with error)
else Timeout
RhaiClientLib -->>- CircleWSServer: Timeout Error
CircleWSServer -->>- ExtWSClient: WS Response (JSON-RPC with timeout error)
end
```
### 5.3. Conceptual Directory Structure
```
/Users/timurgordon/code/git.ourworld.tf/herocode/
├── circles/
│ ├── cmd/ <-- NEW Orchestrator Crate
│ │ ├── Cargo.toml
│ │ ├── circles.json (example config)
│ │ └── src/
│ │ └── main.rs (Orchestrator logic)
│ ├── server_ws/ <-- EXISTING, to be refactored to lib
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs (WebSocket server library logic)
│ ├── ARCHITECTURE.md (This document)
│ └── README.md
├── db/
│ └── ourdb/
│ └── src/lib.rs (Existing OurDB library)
└── rhailib/ <-- Current Workspace (contains other existing libs)
├── src/
│ ├── client/
│ │ └── src/lib.rs (Existing Rhai Client library)
│ ├── engine/
│ │ └── src/lib.rs (Existing Rhai Engine library)
│ └── worker/
│ └── src/lib.rs (Existing Rhai Worker library, to be refactored)
├── Cargo.toml
└── ...

4484
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

73
Cargo.toml Normal file
View File

@ -0,0 +1,73 @@
[package]
name = "circles"
version = "0.1.0"
edition = "2021"
[workspace]
resolver = "2"
members = [
"src/client_ws",
"src/server_ws",
"src/launcher",
"src/ui_repl",
"src/app",
]
[dependencies]
circle_client_ws = { path = "src/client_ws", features = ["crypto"] }
serde_json.workspace = true
# Define shared dependencies for the entire workspace
[workspace.dependencies]
actix = "0.13"
actix-web = "4"
circle_client_ws = { path = "src/client_ws", features = ["crypto"] }
actix-web-actors = "4"
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.0", features = ["derive"] }
env_logger = "0.10"
futures-channel = "0.3"
futures-util = "0.3"
hex = "0.4"
log = "0.4"
once_cell = "1.19"
rand = "0.8"
redis = { version = "0.25.0", features = ["tokio-comp"] }
secp256k1 = { version = "0.29", features = ["recovery", "rand-std"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha3 = "0.10"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.23.0"
url = "2.5.0"
urlencoding = "2.1"
uuid = { version = "1.6", features = ["v4", "serde", "js"] }
thiserror = "1.0"
# Path dependencies to other local crates from outside this repo
heromodels = { path = "../db/heromodels" }
engine = { path = "../rhailib/src/engine" }
rhailib_worker = { path = "../rhailib/src/worker" }
circle_ws_lib = { path = "src/server_ws" }
# Dev dependencies
[dev-dependencies]
env_logger = "0.10"
tokio = { version = "1", features = ["full"] }
tempfile = "3.10.1"
log = "0.4"
circle_ws_lib = { workspace = true }
heromodels = { workspace = true }
engine = { workspace = true }
rhailib_worker = { workspace = true }
redis = { workspace = true }
secp256k1 = { workspace = true }
hex = { workspace = true }
launcher = { path = "src/launcher" }
[features]
crypto = ["circle_client_ws/crypto"]

View File

@ -1,3 +1,74 @@
# Circles
# Circles Project
Architecture around our digital selves.
Welcome to the `circles` project, a full-stack system featuring a WebSocket server, a cross-platform client, and a launcher to manage multiple instances. This project is designed for executing Rhai scripts in isolated environments, with an optional layer of `secp256k1` cryptographic authentication.
## Overview
The `circles` project provides two core library crates and a utility application:
- **`server_ws`**: The core WebSocket server library, built with `Actix`. It handles client connections, processes JSON-RPC messages, and executes Rhai scripts.
- **`client_ws`**: The core cross-platform WebSocket client library, compatible with both native Rust and WebAssembly (WASM) environments.
- **`launcher`**: A convenient command-line utility that uses the `server_ws` library to read a `circles.json` configuration file and spawn multiple, isolated "Circle" instances.
- **`openrpc.json`**: An OpenRPC specification that formally defines the JSON-RPC 2.0 API used for client-server communication.
## Architecture
The system is designed around a client-server model, with `client_ws` and `server_ws` as the core components. The `launcher` is provided as a utility for orchestrating multiple server instances, each configured as an isolated "Circle" environment.
Clients connect to a `server_ws` instance via WebSocket and interact with it using the JSON-RPC protocol. The server can be configured to require authentication, in which case the client must complete a signature-based challenge-response flow over the WebSocket connection before it can execute protected methods like `play`.
For a more detailed explanation of the system's design, please see the [ARCHITECTURE.md](ARCHITECTURE.md) file.
## Getting Started
To run the system, you will need to use the `launcher`.
1. **Configure Your Circles**: Create a `circles.json` file at the root of the project to define the instances you want to run. Each object in the top-level array defines a "Circle" with a unique `id`, `port`, and associated database and script paths.
```json
[
{
"id": "circle-1",
"port": 9001,
"db_path": "/tmp/circle-1.db",
"rhai_path": "/path/to/your/scripts"
}
]
```
2. **Run the Launcher**:
```bash
cargo run --package launcher
```
The launcher will start a WebSocket server for each configured circle on its specified port.
## API
The client-server communication is handled via JSON-RPC 2.0 over WebSocket. The available methods are:
- `play`: Executes a Rhai script.
- `authenticate`: Authenticates the client.
For a complete definition of the API, including request parameters and response objects, please refer to the [openrpc.json](openrpc.json) file.
## Crates
- **[server_ws](server_ws/README.md)**: Detailed documentation for the server library.
- **[client_ws](client_ws/README.md)**: Detailed documentation for the client library.
- **[launcher](launcher/README.md)**: Detailed documentation for the launcher utility.
- **[app](src/app/README.md)**: A Yew frontend application that uses the `client_ws` to interact with the `server_ws`.
## Running the App
To run the `circles-app`, you'll need to have `trunk` installed. If you don't have it, you can install it with:
```bash
cargo install trunk wasm-bindgen-cli
```
Once `trunk` is installed, you can serve the app with:
```bash
cd src/app && trunk serve
```

View File

@ -1,31 +0,0 @@
[package]
name = "circle_client_ws"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.6", features = ["v4", "serde", "js"] }
log = "0.4"
futures-channel = { version = "0.3", features = ["sink"] } # For mpsc
futures-util = { version = "0.3", features = ["sink"] } # For StreamExt, SinkExt
thiserror = "1.0"
async-trait = "0.1" # May be needed for abstracting WS connection
# WASM-specific dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo-net = { version = "0.4.0", features = ["websocket"] }
wasm-bindgen-futures = "0.4"
gloo-console = "0.3.0" # For wasm logging if needed, or use `log` with wasm_logger
# Native-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio-tungstenite = { version = "0.23.0", features = ["native-tls"] }
tokio = { version = "1", features = ["rt", "macros"] } # For tokio::spawn on native
url = "2.5.0" # For native WebSocket connection
[dev-dependencies]
# For examples within this crate, if any, or for testing
env_logger = "0.10"
# tokio = { version = "1", features = ["full"] } # If examples need full tokio runtime

View File

@ -1,86 +0,0 @@
# Circle WebSocket Client (`circle_client_ws`)
This crate provides a WebSocket client (`CircleWsClient`) designed to interact with a server that expects JSON-RPC messages, specifically for executing Rhai scripts.
It is designed to be compatible with both WebAssembly (WASM) environments (e.g., web browsers) and native Rust applications.
## Features
- **Cross-Platform:** Works in WASM and native environments.
- Uses `gloo-net` for WebSockets in WASM.
- Uses `tokio-tungstenite` for WebSockets in native applications.
- **JSON-RPC Communication:** Implements client-side JSON-RPC 2.0 request and response handling.
- **Rhai Script Execution:** Provides a `play(script: String)` method to send Rhai scripts to the server for execution and receive their output.
- **Asynchronous Operations:** Leverages `async/await` and `futures` for non-blocking communication.
- **Connection Management:** Supports connecting to and disconnecting from a WebSocket server.
- **Error Handling:** Defines a comprehensive `CircleWsClientError` enum for various client-side errors.
## Core Component
- **`CircleWsClient`**: The main client struct.
- `new(ws_url: String)`: Creates a new client instance targeting the given WebSocket URL.
- `connect()`: Establishes the WebSocket connection.
- `play(script: String)`: Sends a Rhai script to the server for execution and returns the result.
- `disconnect()`: Closes the WebSocket connection.
## Usage Example (Conceptual)
```rust
use circle_client_ws::CircleWsClient;
async fn run_client() {
let mut client = CircleWsClient::new("ws://localhost:8080/ws".to_string());
if let Err(e) = client.connect().await {
eprintln!("Failed to connect: {}", e);
return;
}
let script = "print(\"Hello from Rhai via WebSocket!\"); 40 + 2".to_string();
match client.play(script).await {
Ok(result) => {
println!("Script output: {}", result.output);
}
Err(e) => {
eprintln!("Error during play: {}", e);
}
}
client.disconnect().await;
}
// To run this example, you'd need an async runtime like tokio for native
// or wasm-bindgen-test for WASM.
```
## Building
### Native
```bash
cargo build
```
### WASM
```bash
cargo build --target wasm32-unknown-unknown
```
## Dependencies
Key dependencies include:
- `serde`, `serde_json`: For JSON serialization/deserialization.
- `futures-channel`, `futures-util`: For asynchronous stream and sink handling.
- `uuid`: For generating unique request IDs.
- `log`: For logging.
- `thiserror`: For error type definitions.
**WASM-specific:**
- `gloo-net`: For WebSocket communication in WASM.
- `wasm-bindgen-futures`: To bridge Rust futures with JavaScript promises.
**Native-specific:**
- `tokio-tungstenite`: For WebSocket communication in native environments.
- `tokio`: Asynchronous runtime for native applications.
- `url`: For URL parsing.

View File

@ -1,23 +0,0 @@
[package]
name = "circles_orchestrator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# clap = { version = "4.0", features = ["derive"], optional = true } # Optional for future args
dirs = "5.0"
log = "0.4"
env_logger = "0.10"
comfy-table = "7.0" # For table display
# Path dependencies to other local crates
heromodels = { path = "../../db/heromodels" } # Changed from ourdb
rhai_engine = { path = "../../rhailib/src/engine" }
rhai_worker = { path = "../../rhailib/src/worker" }
# rhai_client is used by circle_ws_lib, not directly by orchestrator usually
circle_ws_lib = { path = "../server_ws" }

View File

@ -1,5 +0,0 @@
[
{ "id": 1, "name": "Alpha Circle", "port": 8091 },
{ "id": 2, "name": "Alpha Circle", "port": 8082 },
{ "id": 3, "name": "Beta Circle", "port": 8083 }
]

View File

@ -1,245 +0,0 @@
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use tokio::task::JoinHandle;
use tokio::sync::{oneshot, mpsc}; // For server handles and worker shutdown
use tokio::signal;
use std::time::Duration;
use comfy_table::{Table, Row, Cell, ContentArrangement};
use log::{info, error, warn, debug};
use heromodels::db::hero::{OurDB as HeroOurDB}; // Renamed to avoid conflict if OurDB is used from elsewhere
use rhai_engine::create_heromodels_engine;
use worker_lib::spawn_rhai_worker; // This now takes a shutdown_rx
use circle_ws_lib::spawn_circle_ws_server; // This now takes a server_handle_tx
const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1:6379";
#[derive(Deserialize, Debug, Clone)]
struct CircleConfig {
id: u32,
name: String,
port: u16,
}
struct RunningCircleInfo {
config: CircleConfig,
db_path: PathBuf,
worker_queue: String,
ws_url: String,
worker_handle: JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>>,
worker_shutdown_tx: mpsc::Sender<()>, // To signal worker to stop
// Store the server handle for graceful shutdown, and its JoinHandle
ws_server_instance_handle: Arc<Mutex<Option<actix_web::dev::Server>>>,
ws_server_task_join_handle: JoinHandle<std::io::Result<()>>,
status: Arc<Mutex<String>>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("RUST_LOG", "info,circles_orchestrator=debug,worker_lib=debug,circle_ws_lib=debug,rhai_client=debug,actix_server=info");
env_logger::init();
info!("Starting Circles Orchestrator...");
info!("Press Ctrl+C to initiate graceful shutdown.");
let config_path = PathBuf::from("./circles.json");
if !config_path.exists() {
error!("Configuration file not found at {:?}. Please create circles.json.", config_path);
return Err("circles.json not found".into());
}
let config_content = fs::read_to_string(config_path)?;
let circle_configs: Vec<CircleConfig> = serde_json::from_str(&config_content)?;
if circle_configs.is_empty() {
warn!("No circle configurations found in circles.json. Exiting.");
return Ok(());
}
info!("Loaded {} circle configurations.", circle_configs.len());
let mut running_circles_store: Vec<Arc<Mutex<RunningCircleInfo>>> = Vec::new();
for config in circle_configs {
info!("Initializing Circle ID: {}, Name: '{}', Port: {}", config.id, config.name, config.port);
let current_status = Arc::new(Mutex::new(format!("Initializing Circle {}", config.id)));
let db_base_path = match dirs::home_dir() {
Some(path) => path.join(".hero").join("circles"),
None => {
error!("Failed to get user home directory for Circle ID {}.", config.id);
*current_status.lock().unwrap() = "Error: DB Path".to_string();
// Not pushing to running_circles_store as it can't fully initialize
continue;
}
};
let circle_db_path = db_base_path.join(config.id.to_string());
if !circle_db_path.exists() {
if let Err(e) = fs::create_dir_all(&circle_db_path) {
error!("Failed to create database directory for Circle {}: {:?}. Error: {}", config.id, circle_db_path, e);
*current_status.lock().unwrap() = "Error: DB Create".to_string();
continue;
}
info!("Created database directory for Circle {}: {:?}", config.id, circle_db_path);
}
let db = match HeroOurDB::new(circle_db_path.clone(), false) {
Ok(db_instance) => Arc::new(db_instance),
Err(e) => {
error!("Failed to initialize heromodels::OurDB for Circle {}: {:?}", config.id, e);
*current_status.lock().unwrap() = "Error: DB Init".to_string();
continue;
}
};
info!("OurDB initialized for Circle {}", config.id);
*current_status.lock().unwrap() = format!("DB Ok for Circle {}", config.id);
let engine = create_heromodels_engine(db.clone());
info!("Rhai Engine created for Circle {}", config.id);
*current_status.lock().unwrap() = format!("Engine Ok for Circle {}", config.id);
// Channel for worker shutdown
let (worker_shutdown_tx, worker_shutdown_rx) = mpsc::channel(1); // Buffer of 1 is fine
let worker_handle = spawn_rhai_worker(
config.id,
config.name.clone(),
engine, // engine is Clone
DEFAULT_REDIS_URL.to_string(),
worker_shutdown_rx, // Pass the receiver
);
info!("Rhai Worker spawned for Circle {}", config.id);
let worker_queue_name = format!("rhai_tasks:{}", config.name.replace(" ", "_").to_lowercase());
*current_status.lock().unwrap() = format!("Worker Spawning for Circle {}", config.id);
let (server_handle_tx, server_handle_rx) = oneshot::channel();
let ws_server_task_join_handle = spawn_circle_ws_server(
config.id,
config.name.clone(),
config.port,
DEFAULT_REDIS_URL.to_string(),
server_handle_tx,
);
info!("Circle WebSocket Server task spawned for Circle {} on port {}", config.id, config.port);
let ws_url = format!("ws://127.0.0.1:{}/ws", config.port);
*current_status.lock().unwrap() = format!("WS Server Spawning for Circle {}", config.id);
let server_instance_handle_arc = Arc::new(Mutex::new(None));
let server_instance_handle_clone = server_instance_handle_arc.clone();
let status_clone_for_server_handle = current_status.clone();
let circle_id_for_server_handle = config.id;
tokio::spawn(async move {
match server_handle_rx.await {
Ok(handle) => {
*server_instance_handle_clone.lock().unwrap() = Some(handle);
*status_clone_for_server_handle.lock().unwrap() = format!("Running Circle {}", circle_id_for_server_handle);
info!("Received server handle for Circle {}", circle_id_for_server_handle);
}
Err(_) => {
*status_clone_for_server_handle.lock().unwrap() = format!("Error: No Server Handle for Circle {}", circle_id_for_server_handle);
error!("Failed to receive server handle for Circle {}", circle_id_for_server_handle);
}
}
});
running_circles_store.push(Arc::new(Mutex::new(RunningCircleInfo {
config,
db_path: circle_db_path,
worker_queue: worker_queue_name,
ws_url,
worker_handle,
worker_shutdown_tx,
ws_server_instance_handle: server_instance_handle_arc,
ws_server_task_join_handle,
status: current_status, // This is an Arc<Mutex<String>>
})));
}
info!("All configured circles have been processed. Initializing status table display loop.");
let display_running_circles = running_circles_store.clone();
let display_task = tokio::spawn(async move {
loop {
{ // Scope for MutexGuard
let circles = display_running_circles.iter()
.map(|arc_info| arc_info.lock().unwrap())
.collect::<Vec<_>>(); // Collect locked guards
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["Name", "ID", "Port", "Status", "DB Path", "Worker Queue", "WS URL"]);
for circle_info in circles.iter() {
let mut row = Row::new();
row.add_cell(Cell::new(&circle_info.config.name));
row.add_cell(Cell::new(circle_info.config.id));
row.add_cell(Cell::new(circle_info.config.port));
row.add_cell(Cell::new(&*circle_info.status.lock().unwrap())); // Deref and lock status
row.add_cell(Cell::new(circle_info.db_path.to_string_lossy()));
row.add_cell(Cell::new(&circle_info.worker_queue));
row.add_cell(Cell::new(&circle_info.ws_url));
table.add_row(row);
}
// Clear terminal before printing (basic, might flicker)
// print!("\x1B[2J\x1B[1;1H");
println!("\n--- Circles Status (updated every 5s, Ctrl+C to stop) ---\n{table}");
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
signal::ctrl_c().await?;
info!("Ctrl-C received. Initiating graceful shutdown of all circles...");
display_task.abort(); // Stop the display task
for circle_arc in running_circles_store {
let mut circle_info = circle_arc.lock().unwrap();
info!("Shutting down Circle ID: {}, Name: '{}'", circle_info.config.id, circle_info.config.name);
*circle_info.status.lock().unwrap() = "Shutting down".to_string();
// Signal worker to shut down
if circle_info.worker_shutdown_tx.send(()).await.is_err() {
warn!("Failed to send shutdown signal to worker for Circle {}. It might have already stopped.", circle_info.config.id);
}
// Stop WS server
if let Some(server_handle) = circle_info.ws_server_instance_handle.lock().unwrap().take() {
info!("Stopping WebSocket server for Circle {}...", circle_info.config.id);
server_handle.stop(true).await; // Graceful stop
info!("WebSocket server for Circle {} stop signal sent.", circle_info.config.id);
} else {
warn!("No server handle to stop WebSocket server for Circle {}. It might not have started properly or already stopped.", circle_info.config.id);
}
}
info!("Waiting for all tasks to complete...");
for circle_arc in running_circles_store {
// We need to take ownership of handles to await them, or await mutable refs.
// This part is tricky if the MutexGuard is held.
// For simplicity, we'll just log that we've signaled them.
// Proper awaiting would require more careful structuring of JoinHandles.
let circle_id;
let circle_name;
{ // Short scope for the lock
let circle_info = circle_arc.lock().unwrap();
circle_id = circle_info.config.id;
circle_name = circle_info.config.name.clone();
}
debug!("Orchestrator has signaled shutdown for Circle {} ({}). Main loop will await join handles if structured for it.", circle_name, circle_id);
// Actual awaiting of join handles would happen here if they were collected outside the Mutex.
// For now, the main function will exit after this loop.
}
// Give some time for tasks to shut down before the main process exits.
// This is a simplified approach. A more robust solution would involve awaiting all JoinHandles.
tokio::time::sleep(Duration::from_secs(2)).await;
info!("Orchestrator shut down complete.");
Ok(())
}

137
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,137 @@
# System Architecture
This document provides a detailed overview of the `circles` project architecture. The project is composed of two core library crates, `server_ws` and `client_ws`, and a convenient `launcher` utility.
## 1. High-Level Overview
The `circles` project provides the core components for a client-server system designed to execute Rhai scripts in isolated environments. The `launcher` application is a utility that demonstrates how to use the `server_ws` and `client_ws` libraries to manage multiple server instances, but the libraries themselves are the fundamental building blocks.
The core functionality revolves around:
- **Orchestration**: The `launcher` starts and stops multiple, independent WebSocket servers.
- **Client-Server Communication**: A JSON-RPC 2.0 API over WebSockets allows clients to execute scripts and authenticate.
- **Authentication**: An optional, robust `secp256k1` signature-based authentication mechanism secures the script execution endpoint.
## 2. Component Architecture
### 2.1. `server_ws` (Library)
The `server_ws` crate provides the WebSocket server that handles client connections and API requests. Its key features include:
- **Web Framework**: Built using `Actix`, a powerful actor-based web framework for Rust.
- **WebSocket Handling**: Uses `actix-web-actors` to manage individual WebSocket sessions. Each client connection is handled by a `CircleWs` actor, ensuring that sessions are isolated from one another.
- **JSON-RPC API**: Exposes a JSON-RPC 2.0 API with methods for script execution (`play`) and authentication (`fetch_nonce`, `authenticate`).
- **Authentication Service**: The authentication flow is handled entirely within the WebSocket connection using the dedicated JSON-RPC methods.
### 2.2. `client_ws` (Library)
The `client_ws` crate is a WebSocket client library designed for interacting with the `server_ws`. It is engineered to be cross-platform:
- **Native**: For native Rust applications, it uses `tokio-tungstenite` for WebSocket communication.
- **WebAssembly (WASM)**: For browser-based applications, it uses `gloo-net` to integrate with the browser's native WebSocket API.
- **API**: Provides a flexible builder pattern for client construction and a high-level API (`CircleWsClient`) that abstracts the complexities of the WebSocket connection and the JSON-RPC protocol.
### 2.3. `launcher` (Utility)
The `launcher` is a command-line utility that demonstrates how to use the `server_ws` library. It is responsible for:
- **Configuration**: Reading a `circles.json` file that defines a list of Circle instances to run.
- **Orchestration**: Spawning a dedicated `server_ws` instance for each configured circle.
- **Lifecycle Management**: Managing the lifecycle of all spawned servers and their associated Rhai workers.
### 2.2. `server_ws`
The `server_ws` crate provides the WebSocket server that handles client connections and API requests. Its key features include:
- **Web Framework**: Built using `Actix`, a powerful actor-based web framework for Rust.
- **WebSocket Handling**: Uses `actix-web-actors` to manage individual WebSocket sessions. Each client connection is handled by a `CircleWs` actor, ensuring that sessions are isolated from one another.
- **JSON-RPC API**: Exposes a JSON-RPC 2.0 API with methods for script execution (`play`) and authentication (`fetch_nonce`, `authenticate`).
- **Authentication Service**: The authentication flow is handled entirely within the WebSocket connection using the dedicated JSON-RPC methods.
### 2.3. `client_ws`
The `client_ws` crate is a WebSocket client library designed for interacting with the `server_ws`. It is engineered to be cross-platform:
- **Native**: For native Rust applications, it uses `tokio-tungstenite` for WebSocket communication.
- **WebAssembly (WASM)**: For browser-based applications, it uses `gloo-net` to integrate with the browser's native WebSocket API.
- **API**: Provides a flexible builder pattern for client construction and a high-level API (`CircleWsClient`) that abstracts the complexities of the WebSocket connection and the JSON-RPC protocol.
## 3. Communication and Protocols
### 3.1. JSON-RPC 2.0
All client-server communication, including authentication, uses the JSON-RPC 2.0 protocol over the WebSocket connection. This provides a unified, lightweight, and well-defined structure for all interactions. The formal API contract is defined in the [openrpc.json](openrpc.json) file.
### 3.2. Authentication Flow
The authentication mechanism is designed to verify that a client possesses the private key corresponding to a given public key, without ever exposing the private key. The entire flow happens over the established WebSocket connection.
**Sequence of Events:**
1. **Keypair**: The client is instantiated with a `secp256k1` keypair.
2. **Nonce Request**: The client sends a `fetch_nonce` JSON-RPC request containing its public key.
3. **Nonce Issuance**: The server generates a unique, single-use nonce, stores it in the actor's state, and returns it to the client in a JSON-RPC response.
4. **Signature Creation**: The client signs the received nonce with its private key.
5. **Authentication Request**: The client sends an `authenticate` JSON-RPC message, containing the public key and the generated signature.
6. **Signature Verification**: The server's WebSocket actor retrieves the stored nonce for the given public key and cryptographically verifies the signature.
7. **Session Update**: If verification is successful, the server marks the client's WebSocket session as "authenticated," granting it access to protected methods like `play`.
## 4. Diagrams
### 4.1. System Component Diagram
```mermaid
graph TD
subgraph "User Machine"
Launcher[🚀 launcher]
CirclesConfig[circles.json]
Launcher -- Reads --> CirclesConfig
end
subgraph "Spawned Processes"
direction LR
subgraph "Circle 1"
Server1[🌐 server_ws on port 9001]
end
subgraph "Circle 2"
Server2[🌐 server_ws on port 9002]
end
end
Launcher -- Spawns & Manages --> Server1
Launcher -- Spawns & Manages --> Server2
subgraph "Clients"
Client1[💻 client_ws]
Client2[💻 client_ws]
end
Client1 -- Connects via WebSocket --> Server1
Client2 -- Connects via WebSocket --> Server2
```
### 4.2. Authentication Sequence Diagram
```mermaid
sequenceDiagram
participant Client as client_ws
participant WsActor as CircleWs Actor (WebSocket)
Client->>Client: Instantiate with keypair
Note over Client: Has public_key, private_key
Client->>+WsActor: JSON-RPC "fetch_nonce" (pubkey)
WsActor->>WsActor: generate_nonce()
WsActor->>WsActor: store_nonce(pubkey, nonce)
WsActor-->>-Client: JSON-RPC Response ({"nonce": "..."})
Client->>Client: sign(nonce, private_key)
Note over Client: Has signature
Client->>+WsActor: JSON-RPC "authenticate" (pubkey, signature)
WsActor->>WsActor: retrieve_nonce(pubkey)
WsActor->>WsActor: verify_signature(nonce, signature, pubkey)
alt Signature is Valid
WsActor->>WsActor: Set session as authenticated
WsActor-->>-Client: JSON-RPC Response ({"authenticated": true})
else Signature is Invalid
WsActor-->>-Client: JSON-RPC Error (Invalid Credentials)
end
Note over WsActor: Subsequent "play" requests will include the authenticated public key.

126
docs/openrpc.json Normal file
View File

@ -0,0 +1,126 @@
{
"openrpc": "1.2.6",
"info": {
"title": "Circles RPC",
"description": "A JSON-RPC API for interacting with a Circle, allowing script execution and authentication.",
"version": "1.0.0"
},
"methods": [
{
"name": "fetch_nonce",
"summary": "Fetches a cryptographic nonce for a given public key.",
"params": [
{
"name": "pubkey",
"description": "The client's public key.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "nonce_response",
"description": "The cryptographic nonce to be signed.",
"schema": {
"$ref": "#/components/schemas/NonceResponse"
}
}
},
{
"name": "authenticate",
"summary": "Authenticates the client using a signed nonce.",
"params": [
{
"name": "credentials",
"description": "The authentication credentials, including the public key and the signed nonce.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AuthCredentials"
}
}
],
"result": {
"name": "authentication_status",
"description": "The result of the authentication attempt.",
"schema": {
"type": "object",
"properties": {
"authenticated": {
"type": "boolean"
}
},
"required": ["authenticated"]
}
},
"errors": [
{
"code": -32002,
"message": "Invalid Credentials",
"description": "The provided credentials were not valid."
}
]
},
{
"name": "play",
"summary": "Executes a Rhai script and returns the result.",
"params": [
{
"name": "script",
"description": "The Rhai script to execute.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "play_result",
"description": "The output of the executed script.",
"schema": {
"type": "string"
}
},
"errors": [
{
"code": -32000,
"message": "Execution Error",
"description": "The script failed to execute."
},
{
"code": -32001,
"message": "Authentication Required",
"description": "The client must be authenticated to use this method."
}
]
}
],
"components": {
"schemas": {
"AuthCredentials": {
"type": "object",
"properties": {
"pubkey": {
"type": "string",
"description": "The public key of the client."
},
"signature": {
"type": "string",
"description": "The nonce signed with the client's private key."
}
},
"required": ["pubkey", "signature"]
},
"NonceResponse": {
"type": "object",
"properties": {
"nonce": {
"type": "string",
"description": "The single-use cryptographic nonce."
}
},
"required": ["nonce"]
}
}
}
}

0
examples/.gitkeep Normal file
View File

View File

@ -0,0 +1,146 @@
//! End-to-end authentication example
//!
//! This example demonstrates the complete authentication flow with the simplified approach:
//! 1. Create a WebSocket client with authentication configuration
//! 2. Authenticate using private key
//! 3. Connect to WebSocket with authentication
//! 4. Send authenticated requests
//!
//! To run this example:
//! ```bash
//! cargo run --example client_auth_example --features "crypto"
//! ```
use circle_client_ws::CircleWsClientBuilder;
use log::{info, error};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
env_logger::init();
info!("Starting simplified authentication example");
// Configuration
let ws_url = "ws://localhost:8080/ws".to_string();
// 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");
}
Err(e) => {
error!("WebSocket connection failed: {}", e);
return Err(e.into());
}
}
match client.authenticate().await {
Ok(true) => {
info!("Successfully authenticated with private key");
}
Ok(false) => {
error!("Authentication failed");
}
Err(e) => {
error!("Private key authentication failed: {}", e);
}
}
// Example 2: Send authenticated request
info!("=== Example 2: Send Authenticated Request ===");
let script = "print('Hello from authenticated client!');".to_string();
match client.play(script).await {
Ok(result) => {
info!("Play request successful: {}", result.output);
}
Err(e) => {
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");
let script = "print('Hello from second auth!');".to_string();
match client2.play(script).await {
Ok(result) => {
info!("Second auth request successful: {}", result.output);
}
Err(e) => {
error!("Second auth request failed: {}", e);
}
}
}
Ok(false) => {
error!("Second private key authentication failed");
}
Err(e) => {
error!("Second private key authentication failed: {}", e);
}
}
client2.disconnect().await;
}
Err(e) => {
error!("Second auth connection failed: {}", e);
}
}
// 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) => {
info!("Non-auth request successful: {}", result.output);
}
Err(e) => {
error!("Non-auth request failed: {}", e);
}
}
client3.disconnect().await;
}
Err(e) => {
error!("Non-auth connection failed: {}", e);
}
}
info!("Simplified authentication example completed");
Ok(())
}

View File

@ -0,0 +1,261 @@
//! Authentication simulation example
//!
//! This example simulates the authentication flow without requiring a running server.
//! It demonstrates:
//! 1. Key generation and management
//! 2. Nonce request simulation
//! 3. Message signing and verification
//! 4. Credential management
//! 5. Authentication state checking
use std::time::{SystemTime, UNIX_EPOCH};
use log::info;
// 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
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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()?;
let public_key = derive_public_key(&private_key)?;
info!("✅ Generated private key: {}...", &private_key[..10]);
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 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) {
Ok(sig) => {
info!("✅ Signature created: {}...", &sig[..20]);
sig
}
Err(e) => {
error!("❌ Failed to sign message: {}", e);
return Err(e.into());
}
}
};
#[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) {
Ok(true) => info!("✅ Signature verification successful!"),
Ok(false) => {
error!("❌ Signature verification failed!");
return Err("Signature verification failed".into());
}
Err(e) => {
error!("❌ Signature verification error: {}", e);
return Err(e.into());
}
}
}
#[cfg(not(feature = "crypto"))]
{
info!("📝 Skipping signature verification (crypto feature disabled)");
}
// Step 5: Create authentication credentials
info!("📋 Creating authentication credentials...");
#[cfg(feature = "crypto")]
let credentials = AuthCredentials::new(
public_key.clone(),
signature.clone(),
nonce_response.nonce.clone(),
expires_at
);
#[cfg(feature = "crypto")]
{
info!("✅ Credentials created:");
info!(" Public key: {}...", &credentials.public_key()[..20]);
info!(" Signature: {}...", &credentials.signature()[..20]);
info!(" Nonce: {}", credentials.nonce());
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));
}
// 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
);
#[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));
// 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!("🎉 Authentication simulation completed successfully!");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_key_generation() {
#[cfg(feature = "crypto")]
{
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")]
{
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
);
#[cfg(feature = "crypto")]
{
assert!(!credentials.is_expired());
assert!(credentials.expires_within(400));
assert!(!credentials.expires_within(100));
}
}
}

View File

@ -0,0 +1,68 @@
# OurWorld Example
This directory contains a complete example demonstrating a simulated "OurWorld" network, consisting of multiple interconnected "circles" (nodes). Each circle runs its own WebSocket server and a Rhai script worker, all managed by a central launcher.
This example is designed to showcase:
1. **Multi-Circle Configuration**: How to define and configure multiple circles in a single `circles.json` file.
2. **Programmatic Launching**: How to use the `launcher` library to start, manage, and monitor these circles from within a Rust application.
3. **Dynamic Key Generation**: The launcher generates unique cryptographic keypairs for each circle upon startup.
4. **Output Generation**: How to use the `--output` functionality to get a JSON file containing the connection details (public keys, WebSocket URLs, etc.) for each running circle.
5. **Graceful Shutdown**: How the launcher handles a `Ctrl+C` signal to shut down all running circles cleanly.
## Directory Contents
- `circles.json`: The main configuration file that defines the 7 circles in the OurWorld network, including their names, ports, and associated Rhai scripts.
- `scripts/`: This directory contains the individual Rhai scripts that define the behavior of each circle.
- `ourworld_output.json` (Generated): This file is created after running the example and contains the runtime details of each circle.
## How to Run the Example
There are two ways to run this example, each demonstrating a different way to use the launcher.
### 1. As a Root Example (Recommended)
This method runs the launcher programmatically from the root of the workspace and is the simplest way to see the system in action. It uses the `examples/ourworld.rs` file.
```sh
# From the root of the workspace
cargo run --example ourworld
```
### 2. As a Crate-Level Example
This method runs a similar launcher, but as an example *within* the `launcher` crate itself. It uses the `src/launcher/examples/ourworld/main.rs` file. This is useful for testing the launcher in a more isolated context.
```sh
# Navigate to the launcher's crate directory
cd src/launcher
# Run the 'ourworld' example using cargo
cargo run --example ourworld
```
### 3. Using the Launcher Binary
This method uses the main `launcher` binary to run the configuration, which is useful for testing the command-line interface.
```sh
# From the root of the workspace
cargo run -p launcher -- --config examples/ourworld/circles.json --output examples/ourworld/ourworld_output.json
```
## What to Expect
When you run the example, you will see log output indicating that the launcher is starting up, followed by a table summarizing the running circles:
```
+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
| Name | Public Key | Worker Queue | WS URL |
+=================+==================================================================+==========================================+=======================+
| OurWorld | 02... | rhai_tasks:02... | ws://127.0.0.1:9000/ws|
+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
| Dunia Cybercity | 03... | rhai_tasks:03... | ws://127.0.0.1:9001/ws|
+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
| ... (and so on for all 7 circles) |
+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
```
The launcher will then wait for you to press `Ctrl+C` to initiate a graceful shutdown of all services.

View File

@ -0,0 +1,37 @@
[
{
"name": "OurWorld",
"port": 9000,
"script_path": "scripts/ourworld.rhai"
},
{
"name": "Dunia Cybercity",
"port": 9001,
"script_path": "scripts/dunia_cybercity.rhai"
},
{
"name": "Sikana",
"port": 9002,
"script_path": "scripts/sikana.rhai"
},
{
"name": "Threefold",
"port": 9003,
"script_path": "scripts/threefold.rhai"
},
{
"name": "Mbweni",
"port": 9004,
"script_path": "scripts/mbweni.rhai"
},
{
"name": "Geomind",
"port": 9005,
"script_path": "scripts/geomind.rhai"
},
{
"name": "Freezone",
"port": 9006,
"script_path": "scripts/freezone.rhai"
}
]

83
examples/ourworld/main.rs Normal file
View File

@ -0,0 +1,83 @@
//! Example of launching multiple circles and outputting their details to a file.
//!
//! This example demonstrates how to use the launcher library to start circles
//! programmatically, similar to how the `launcher` binary works.
//!
//! # Usage
//!
//! ```sh
//! cargo run --example ourworld
//! ```
//!
//! This will:
//! 1. Read the `circles.json` file in the `examples/ourworld` directory.
//! 2. Launch all 7 circles defined in the config.
//! 3. Create a `ourworld_output.json` file in the same directory with the details.
//! 4. The launcher will run until you stop it with Ctrl+C.
use launcher::{run_launcher, Args, CircleConfig};
use std::fs;
use std::path::PathBuf;
use std::error::Error as StdError;
use log::{error, info};
#[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> {
println!("--- Launching OurWorld Example Programmatically ---");
// The example is now at the root of the `examples` directory,
// so we can reference its assets directly.
let example_dir = PathBuf::from("./examples/ourworld");
let config_path = example_dir.join("circles.json");
let output_path = example_dir.join("ourworld_output.json");
println!("Using config file: {:?}", config_path);
println!("Output will be written to: {:?}", output_path);
// Manually construct the arguments instead of parsing from command line.
// This is useful when embedding the launcher logic in another application.
let args = Args {
config_path: config_path.clone(),
output: Some(output_path),
debug: true, // Enable debug logging for the example
verbose: 2, // Set verbosity to max
};
if !config_path.exists() {
let msg = format!("Configuration file not found at {:?}", config_path);
error!("{}", msg);
return Err(msg.into());
}
let config_content = fs::read_to_string(&config_path)?;
let mut circle_configs: Vec<CircleConfig> = 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);
return Err(Box::new(e) as Box<dyn StdError>);
}
};
// Make script paths relative to the project root by prepending the example directory path.
for config in &mut circle_configs {
if let Some(script_path) = &config.script_path {
let full_script_path = example_dir.join(script_path);
config.script_path = Some(full_script_path.to_string_lossy().into_owned());
}
}
if circle_configs.is_empty() {
info!("No circle configurations found in {}. Exiting.", config_path.display());
return Ok(());
}
println!("Starting launcher... Press Ctrl+C to exit.");
// The run_launcher function will setup logging, spawn circles, print the table,
// and wait for a shutdown signal (Ctrl+C).
run_launcher(args, circle_configs).await?;
println!("--- OurWorld Example Finished ---");
Ok(())
}

View File

@ -0,0 +1,51 @@
[
{
"name": "OurWorld",
"public_key": "02acbca22369b7f10584348056ae48779e04534cd34d37b7db0f4996f4d9d5e2a5",
"secret_key": "0c75df7425c799eb769049cf48891299761660396d772c687fa84cac5ec62570",
"worker_queue": "rhai_tasks:02acbca22369b7f10584348056ae48779e04534cd34d37b7db0f4996f4d9d5e2a5",
"ws_url": "ws://127.0.0.1:9000"
},
{
"name": "Dunia Cybercity",
"public_key": "03d97b1a357c3ceb2f0eb78f8e2c71beda9190db5cb7e5112150105132effb35e0",
"secret_key": "4fad664608e8de55f0e5e1712241e71dc0864be125bc8633e50601fca8040791",
"worker_queue": "rhai_tasks:03d97b1a357c3ceb2f0eb78f8e2c71beda9190db5cb7e5112150105132effb35e0",
"ws_url": "ws://127.0.0.1:9001"
},
{
"name": "Sikana",
"public_key": "0389595b28cfa98b45fa3c222db79892f3face65e7ef06d44e35d642967e45ed6e",
"secret_key": "fd59ddbf0d0bada725c911dc7e3317754ac552aa1ac84cfcb899bdfe3591e1f4",
"worker_queue": "rhai_tasks:0389595b28cfa98b45fa3c222db79892f3face65e7ef06d44e35d642967e45ed6e",
"ws_url": "ws://127.0.0.1:9002"
},
{
"name": "Threefold",
"public_key": "03270f06ee4a7d42a9f6c22c9a7d6d0138cd15d4fa659026e2e6572fc6c6a6ea18",
"secret_key": "e204c0215bec80f74df49ea5b1592de3c6739cced339ace801bb7e158eb62231",
"worker_queue": "rhai_tasks:03270f06ee4a7d42a9f6c22c9a7d6d0138cd15d4fa659026e2e6572fc6c6a6ea18",
"ws_url": "ws://127.0.0.1:9003"
},
{
"name": "Mbweni",
"public_key": "02724cf23e4ac95d0f14984f55c6955b3ca5ab2275d7ac2a2e4baf3596caf8606c",
"secret_key": "3c013e2e5f64692f044d17233e5fabdb0577629f898359115e69c3e594d5f43e",
"worker_queue": "rhai_tasks:02724cf23e4ac95d0f14984f55c6955b3ca5ab2275d7ac2a2e4baf3596caf8606c",
"ws_url": "ws://127.0.0.1:9004"
},
{
"name": "Geomind",
"public_key": "030d8ceb47d445c92b7c3f13e9e134eebcb1d83beed424425f734164544eb58eed",
"secret_key": "dbd6dd383a6f56042710f72ce2ac68266650bbfb61432cdd139e98043b693e7c",
"worker_queue": "rhai_tasks:030d8ceb47d445c92b7c3f13e9e134eebcb1d83beed424425f734164544eb58eed",
"ws_url": "ws://127.0.0.1:9005"
},
{
"name": "Freezone",
"public_key": "02dd21025c1d47421eccc2264c87538d41126da772a9a3f0e7226807fed89c9971",
"secret_key": "0c0c6b02c20fcd4ccfb2afeae249979ddd623e6f6edd17af4a9a5a19bc1b15ae",
"worker_queue": "rhai_tasks:02dd21025c1d47421eccc2264c87538d41126da772a9a3f0e7226807fed89c9971",
"ws_url": "ws://127.0.0.1:9006"
}
]

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Dunia Cybercity")
.description("Creating a better world.")
.ws_url("ws://localhost:8091/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Zanzibar Digital Freezone")
.description("Creating a better world.")
.ws_url("ws://localhost:8096/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Geomind")
.description("Creating a better world.")
.ws_url("ws://localhost:8095/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Mbweni Ruins & Gardens")
.description("Mbweni ruins and Gardens")
.ws_url("ws://localhost:8094/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,255 @@
// OurWorld Circle and Library Data
new_circle()
.title("Ourworld")
.description("Creating a better world.")
.ws_url("ws://localhost:9000/ws")
.add_circle("ws://localhost:9001/ws")
.add_circle("ws://localhost:9002/ws")
.add_circle("ws://localhost:9003/ws")
.add_circle("ws://localhost:9004/ws")
.add_circle("ws://localhost:9005/ws")
.add_circle("ws://localhost:8096/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Sikana")
.description("Creating a better world.")
.ws_url("ws://localhost:8092/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -0,0 +1,249 @@
// OurWorld Circle and Library Data
new_circle()
.title("Threefold DMCC")
.description("Creating a better world.")
.ws_url("ws://localhost:8093/ws")
.logo("🌍")
.save_circle();
let circle = get_circle();
print("--- Creating OurWorld Library ---");
// === IMAGES ===
print("Creating images...");
let nature1 = save_image(new_image()
.title("Mountain Sunrise")
.description("Breathtaking sunrise over mountain peaks")
.url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
.width(800).height(600));
let nature2 = save_image(new_image()
.title("Ocean Waves")
.description("Powerful ocean waves crashing on rocks")
.url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
.width(800).height(600));
let nature3 = save_image(new_image()
.title("Forest Path")
.description("Peaceful path through ancient forest")
.url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
.width(800).height(600));
let tech1 = save_image(new_image()
.title("Solar Panels")
.description("Modern solar panel installation")
.url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
.width(800).height(600));
let tech2 = save_image(new_image()
.title("Wind Turbines")
.description("Wind turbines generating clean energy")
.url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
.width(800).height(600));
let space1 = save_image(new_image()
.title("Earth from Space")
.description("Our beautiful planet from orbit")
.url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
.width(800).height(600));
let space2 = save_image(new_image()
.title("Galaxy Spiral")
.description("Stunning spiral galaxy in deep space")
.url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
.width(800).height(600));
let city1 = save_image(new_image()
.title("Smart City")
.description("Futuristic smart city at night")
.url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
.width(800).height(600));
// === PDFs ===
print("Creating PDFs...");
let pdf1 = save_pdf(new_pdf()
.title("Climate Action Report 2024")
.description("Comprehensive analysis of global climate initiatives")
.url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
.page_count(42));
let pdf2 = save_pdf(new_pdf()
.title("Sustainable Development Goals")
.description("UN SDG implementation guide")
.url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
.page_count(35));
let pdf3 = save_pdf(new_pdf()
.title("Renewable Energy Handbook")
.description("Technical guide to renewable energy systems")
.url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
.page_count(280));
let pdf4 = save_pdf(new_pdf()
.title("Blockchain for Good")
.description("How blockchain technology can solve global challenges")
.url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
.page_count(24));
let pdf5 = save_pdf(new_pdf()
.title("Future of Work Report")
.description("Analysis of changing work patterns and remote collaboration")
.url("https://www.mckinsey.com/featured-insights/future-of-work")
.page_count(156));
// === MARKDOWN DOCUMENTS ===
print("Creating markdown documents...");
let md1 = save_markdown(new_markdown()
.title("OurWorld Mission Statement")
.description("Our vision for a better world")
.content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
let md2 = save_markdown(new_markdown()
.title("Getting Started Guide")
.description("How to join the OurWorld movement")
.content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
let md3 = save_markdown(new_markdown()
.title("Technology Roadmap 2024")
.description("Our technical development plans")
.content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
let md4 = save_markdown(new_markdown()
.title("Community Guidelines")
.description("How we work together")
.content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
let investor = new_contact()
.name("Example Investor")
.save_contact();
let investors = new_group()
.name("Investors")
.description("A group for example inverstors of ourworld");
investors.add_contact(investor.id)
.save_group();
// === BOOKS ===
print("Creating books...");
let sustainability_book = save_book(new_book()
.title("Sustainability Handbook")
.description("Complete guide to sustainable living and practices")
.add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
.add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
.add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
.add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
.add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
.add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
.add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
.add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
let tech_guide_book = save_book(new_book()
.title("Green Technology Guide")
.description("Understanding and implementing green technologies")
.add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
.add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
.add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
.add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
.add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
.add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
let community_book = save_book(new_book()
.title("Building Communities")
.description("Guide to creating sustainable and inclusive communities")
.add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
.add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
.add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
.add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
// === SLIDES ===
print("Creating slides...");
let climate_slides = save_slides(new_slides()
.title("Climate Change Awareness")
.description("Visual presentation on climate change impacts and solutions")
.add_slide("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200", "Global Temperature Rise")
.add_slide("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200", "Melting Ice Caps")
.add_slide("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200", "Extreme Weather Events")
.add_slide("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200", "Renewable Energy Solutions")
.add_slide("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200", "Sustainable Transportation"));
let innovation_slides = save_slides(new_slides()
.title("Innovation Showcase")
.description("Cutting-edge technologies for a sustainable future")
.add_slide("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200", "AI and Machine Learning")
.add_slide("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200", "Blockchain Technology")
.add_slide("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200", "IoT and Smart Cities")
.add_slide("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200", "Quantum Computing")
.add_slide("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200", "Biotechnology Advances"));
let nature_slides = save_slides(new_slides()
.title("Biodiversity Gallery")
.description("Celebrating Earth's incredible biodiversity")
.add_slide("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200", "Tropical Rainforest")
.add_slide("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200", "Coral Reef Ecosystem")
.add_slide("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200", "Arctic Wildlife")
.add_slide("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200", "Mountain Ecosystems"));
// === COLLECTIONS ===
print("Creating collections...");
let nature_collection = save_collection(new_collection()
.title("Nature & Environment")
.description("Beautiful images and resources about our natural world")
.add_image(nature1.id)
.add_image(nature2.id)
.add_image(nature3.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id)
.add_book(sustainability_book.id)
.add_slides(nature_slides.id));
let technology_collection = save_collection(new_collection()
.title("Sustainable Technology")
.description("Innovations driving positive change")
.add_image(tech1.id)
.add_image(tech2.id)
.add_pdf(pdf3.id)
.add_pdf(pdf4.id)
.add_markdown(md3.id)
.add_book(tech_guide_book.id)
.add_slides(innovation_slides.id));
let space_collection = save_collection(new_collection()
.title("Space & Cosmos")
.description("Exploring the universe and our place in it")
.add_image(space1.id)
.add_image(space2.id)
.add_pdf(pdf2.id)
.add_markdown(md2.id));
let community_collection = save_collection(new_collection()
.title("Community & Collaboration")
.description("Building better communities together")
.add_image(city1.id)
.add_pdf(pdf5.id)
.add_markdown(md4.id)
.add_book(community_book.id));
let climate_collection = save_collection(new_collection()
.title("Climate Action")
.description("Understanding and addressing climate change")
.add_slides(climate_slides.id)
.add_pdf(pdf1.id)
.add_markdown(md1.id));
print("✅ OurWorld library created successfully!");
print("📚 Collections: 5");
print("🖼️ Images: 8");
print("📄 PDFs: 5");
print("📝 Markdown docs: 4");
print("📖 Books: 3");
print("🎞️ Slide shows: 3");

View File

@ -8,15 +8,15 @@ use tokio::time::sleep;
// use serde_json::Value; // No longer needed as CircleWsClient::play takes String
// Uuid is handled by CircleWsClient internally for requests.
// use uuid::Uuid;
use circle_client_ws::CircleWsClient;
use circle_client_ws::CircleWsClientBuilder;
// PlayResultClient and CircleWsClientError will be resolved via the client methods if needed,
// or this indicates they were not actually needed in the scope of this file directly.
// The compiler warning suggests they are unused from this specific import.
const TEST_CIRCLE_NAME: &str = "e2e_test_circle";
const TEST_SERVER_PORT: u16 = 9876; // Choose a unique port for the test
const RHAI_WORKER_BIN_NAME: &str = "rhai_worker";
const CIRCLE_SERVER_WS_BIN_NAME: &str = "circle_server_ws";
const RHAI_WORKER_BIN_NAME: &str = "worker";
const CIRCLE_SERVER_WS_BIN_NAME: &str = "server_ws";
// RAII guard for cleaning up child processes
struct ChildProcessGuard {
@ -48,20 +48,9 @@ impl Drop for ChildProcessGuard {
}
fn find_target_dir() -> Result<PathBuf, String> {
// Try to find the cargo target directory relative to current exe or manifest
let mut current_exe = std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {}", e))?;
// current_exe is target/debug/examples/e2e_rhai_flow
// want target/debug/
if current_exe.ends_with("examples/e2e_rhai_flow") { // Adjust if example name changes
current_exe.pop(); // remove e2e_rhai_flow
current_exe.pop(); // remove examples
Ok(current_exe)
} else {
// Fallback: Assume 'target/debug' relative to workspace root if CARGO_MANIFEST_DIR is set
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"))
}
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"))
}
@ -108,7 +97,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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 = CircleWsClient::new(ws_url_str.clone());
let mut client = CircleWsClientBuilder::new(ws_url_str.clone()).build();
log::info!("Connecting CircleWsClient...");
client.connect().await.map_err(|e| {

View File

@ -6,7 +6,7 @@
// This example will attempt to start its own instance of circle_server_ws.
// Ensure circle_server_ws is compiled (cargo build --bin circle_server_ws).
use circle_client_ws::CircleWsClient;
use circle_client_ws::CircleWsClientBuilder;
use tokio::time::{sleep, Duration};
use std::process::{Command, Child, Stdio};
use std::path::PathBuf;
@ -14,7 +14,7 @@ use std::path::PathBuf;
const EXAMPLE_SERVER_PORT: u16 = 8089; // Using a specific port for this example
const WS_URL: &str = "ws://127.0.0.1:8089/ws";
const CIRCLE_NAME_FOR_EXAMPLE: &str = "timeout_example_circle";
const CIRCLE_SERVER_WS_BIN_NAME: &str = "circle_server_ws";
const CIRCLE_SERVER_WS_BIN_NAME: &str = "server_ws";
const SCRIPT_TIMEOUT_SECONDS: u64 = 30; // This is the server-side timeout we expect to hit
// RAII guard for cleaning up child processes
@ -47,28 +47,10 @@ impl Drop for ChildProcessGuard {
}
fn find_target_bin_path(bin_name: &str) -> Result<PathBuf, String> {
let mut current_exe = std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {}", e))?;
// current_exe is typically target/debug/examples/timeout_demonstration
// We want to find target/debug/[bin_name]
current_exe.pop(); // remove executable name
current_exe.pop(); // remove examples directory
let target_debug_dir = current_exe;
let bin_path = target_debug_dir.join(bin_name);
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() {
// Fallback: try CARGO_BIN_EXE_[bin_name] if running via `cargo run --example` which sets these
if let Ok(cargo_bin_path_str) = std::env::var(format!("CARGO_BIN_EXE_{}", bin_name.to_uppercase())) {
let cargo_bin_path = PathBuf::from(cargo_bin_path_str);
if cargo_bin_path.exists() {
return Ok(cargo_bin_path);
}
}
// Fallback: try target/debug/[bin_name] relative to CARGO_MANIFEST_DIR (crate root)
if let Ok(manifest_dir_str) = std::env::var("CARGO_MANIFEST_DIR") {
let bin_path_rel_manifest = PathBuf::from(manifest_dir_str).join("target").join("debug").join(bin_name);
if bin_path_rel_manifest.exists() {
return Ok(bin_path_rel_manifest);
}
}
return Err(format!("Binary '{}' not found at {:?}. Ensure it's built.", bin_name, bin_path));
}
Ok(bin_path)
@ -98,7 +80,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
sleep(Duration::from_secs(3)).await; // Wait for server to initialize
log::info!("Attempting to connect to WebSocket server at: {}", WS_URL);
let mut client = CircleWsClient::new(WS_URL.to_string());
let mut client = CircleWsClientBuilder::new(WS_URL.to_string()).build();
log::info!("Connecting client...");
if let Err(e) = client.connect().await {
@ -110,16 +92,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// This Rhai script is designed to run for much longer than the typical server timeout.
let long_running_script = "
log(\"Rhai: Starting long-running script...\");
let mut x = 0;
for i in 0..9999999999 { // Extremely large loop
x = x + i;
if i % 100000000 == 0 {
// log(\"Rhai: Loop iteration \" + i);
}
}
// This part should not be reached if timeout works correctly.
log(\"Rhai: Long-running script finished calculation (x = \" + x + \").\");
print(x);
x
".to_string();
@ -135,7 +112,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
log::info!("Received expected error from play request: {}", e);
log::info!("This demonstrates the server timing out the script execution.");
// You can further inspect the error details if CircleWsClientError provides them.
// For example, if e.to_string() contains 'code: -32002' or 'timed out'.
// For example, if e.to_string().contains('code: -32002' or 'timed out'.
if e.to_string().contains("timed out") || e.to_string().contains("-32002") {
log::info!("Successfully received timeout error from the server!");
} else {
@ -150,4 +127,4 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
log::info!("Timeout demonstration example finished.");
Ok(())
}
}

View File

@ -1,17 +0,0 @@
[package]
name = "rhai_repl_cli"
version = "0.1.0"
edition = "2024" # Keep 2024 unless issues arise
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } # Added "time" for potential timeouts
tokio-tungstenite = { version = "0.21", features = ["native-tls"] } # May be removed if client_ws handles all
futures-util = "0.3"
url = "2"
tracing = "0.1" # For logging
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
log = "0.4" # circle_client_ws uses log crate
rustyline = { version = "13.0.0", features = ["derive"] } # For enhanced REPL input
tempfile = "3.8" # For creating temporary files for editing
circle_client_ws = { path = "../client_ws" }

View File

@ -1,32 +0,0 @@
[package]
name = "circle_ws_lib" # Renamed to reflect library nature
version = "0.1.0"
edition = "2021"
[lib]
name = "circle_ws_lib"
path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4"
actix-web-actors = "4"
actix = "0.13"
env_logger = "0.10" # Keep for logging within the lib
log = "0.4"
# clap is removed as CLI parsing moves to the orchestrator bin
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
redis = { version = "0.25.0", features = ["tokio-comp"] } # For async Redis with Actix
uuid = { version = "1.6", features = ["v4", "serde"] } # Still used by RhaiClient or for task details
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } # Added "time" for Duration
chrono = { version = "0.4", features = ["serde"] } # For timestamps
rhai_client = { path = "../../rhailib/src/client" } # Corrected relative path
[dev-dependencies]
tokio-tungstenite = { version = "0.23.0", features = ["native-tls"] }
futures-util = "0.3" # For StreamExt and SinkExt on WebSocket stream
url = "2.5.0" # For parsing WebSocket URL
# circle_client_ws = { path = "../client_ws" } # This might need adjustment if it's a test client for the old binary
# uuid = { version = "1.6", features = ["v4", "serde"] } # Already in dependencies

View File

@ -1,52 +0,0 @@
# Circle Server WebSocket (`server_ws`)
## Overview
The `server_ws` component is an Actix-based WebSocket server designed to handle client connections and execute Rhai scripts. It acts as a bridge between WebSocket clients and a Rhai scripting engine, facilitating remote script execution and result retrieval.
## Key Features
* **WebSocket Communication:** Establishes and manages WebSocket connections with clients.
* **Rhai Script Execution:** Receives Rhai scripts from clients, submits them for execution via `rhai_client`, and returns the results.
* **Timeout Management:** Implements timeouts for Rhai script execution to prevent indefinite blocking, returning specific error codes on timeout.
* **Asynchronous Processing:** Leverages Actix actors for concurrent handling of multiple client connections and script executions.
## Core Components
* **`CircleWs` Actor:** The primary Actix actor responsible for handling individual WebSocket sessions. It manages the lifecycle of a client connection, processes incoming messages (Rhai scripts), and sends back results or errors.
* **`rhai_client` Integration:** Utilizes the `rhai_client` crate to submit scripts to a shared Rhai processing service (likely Redis-backed for task queuing and result storage) and await their completion.
## Dependencies
* `actix`: Actor framework for building concurrent applications.
* `actix-web-actors`: WebSocket support for Actix.
* `rhai_client`: Client library for interacting with the Rhai scripting service.
* `serde_json`: For serializing and deserializing JSON messages exchanged over WebSockets.
* `uuid`: For generating unique task identifiers.
## Workflow
1. A client establishes a WebSocket connection to the `/ws/` endpoint.
2. The server upgrades the connection and spawns a `CircleWs` actor instance for that session.
3. The client sends a JSON-RPC formatted message containing the Rhai script to be executed.
4. The `CircleWs` actor parses the message and uses `rhai_client::RhaiClient::submit_script_and_await_result` to send the script for execution. This method handles the interaction with the underlying task queue (e.g., Redis) and waits for the script's outcome.
5. The `rhai_client` will return the script's result or an error (e.g., timeout, script error).
6. `CircleWs` formats the result/error into a JSON-RPC response and sends it back to the client over the WebSocket.
## Configuration
* **`REDIS_URL`**: The `rhai_client` component (and thus `server_ws` indirectly) relies on a Redis instance. The connection URL for this Redis instance is typically configured via an environment variable or a constant that `rhai_client` uses.
* **Timeout Durations**:
* `TASK_TIMEOUT_DURATION` (e.g., 30 seconds): The maximum time the server will wait for a Rhai script to complete execution.
* `TASK_POLL_INTERVAL_DURATION` (e.g., 200 milliseconds): The interval at which the `rhai_client` polls for task completion (this is an internal detail of `rhai_client` but relevant to understanding its behavior).
## Error Handling
The server implements specific JSON-RPC error responses for various scenarios:
* **Script Execution Timeout:** If a script exceeds `TASK_TIMEOUT_DURATION`, a specific error (e.g., code -32002) is returned.
* **Other `RhaiClientError`s:** Other errors originating from `rhai_client` (e.g., issues with the Redis connection, script compilation errors detected by the remote Rhai engine) are also translated into appropriate JSON-RPC error responses.
* **Message Parsing Errors:** Invalid incoming messages will result in error responses.
## How to Run
(Instructions on how to build and run the server would typically go here, e.g., `cargo run --bin circle_server_ws`)

View File

@ -1,302 +0,0 @@
use actix_web::{web, App, HttpRequest, HttpServer, HttpResponse, Error};
use actix_web_actors::ws;
use actix::{Actor, ActorContext, StreamHandler, AsyncContext, WrapFuture, ActorFutureExt};
// clap::Parser removed
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
use rhai_client::RhaiClientError;
use rhai_client::RhaiClient;
use tokio::task::JoinHandle;
use tokio::sync::oneshot; // For sending the server handle back
// Newtype wrappers for distinct app_data types
#[derive(Clone)]
struct AppCircleName(String);
#[derive(Clone)]
struct AppRedisUrl(String);
// JSON-RPC 2.0 Structures (remain the same)
#[derive(Serialize, Deserialize, Debug, Clone)]
struct JsonRpcRequest {
jsonrpc: String,
method: String,
params: Value,
id: Option<Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct JsonRpcResponse {
jsonrpc: String,
result: Option<Value>,
error: Option<JsonRpcError>,
id: Value,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct JsonRpcError {
code: i32,
message: String,
data: Option<Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct PlayParams {
script: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct PlayResult {
output: String,
}
// WebSocket Actor
struct CircleWs {
server_circle_name: String,
redis_url_for_client: String,
}
const TASK_TIMEOUT_DURATION: Duration = Duration::from_secs(30);
const TASK_POLL_INTERVAL_DURATION: Duration = Duration::from_millis(200);
impl CircleWs {
fn new(name: String, redis_url: String) -> Self {
Self {
server_circle_name: name,
redis_url_for_client: redis_url,
}
}
}
impl Actor for CircleWs {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
log::info!("WebSocket session started for server dedicated to: {}", self.server_circle_name);
}
fn stopping(&mut self, _ctx: &mut Self::Context) -> actix::Running {
log::info!("WebSocket session stopping for server dedicated to: {}", self.server_circle_name);
actix::Running::Stop
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for CircleWs {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Text(text)) => {
log::debug!("WS Text for {}: {}", self.server_circle_name, text); // Changed to debug for less noise
match serde_json::from_str::<JsonRpcRequest>(&text) {
Ok(req) => {
let client_rpc_id = req.id.clone().unwrap_or(Value::Null);
if req.method == "play" {
match serde_json::from_value::<PlayParams>(req.params.clone()) {
Ok(play_params) => {
let script_content = play_params.script;
// Use the server_circle_name which should be correctly set now
let current_circle_name_for_rhai_client = self.server_circle_name.clone();
let rpc_id_for_client = client_rpc_id.clone();
let redis_url_clone = self.redis_url_for_client.clone();
log::info!("Circle '{}' WS: Received 'play' request, ID: {:?}, for RhaiClient target circle: '{}'", self.server_circle_name, rpc_id_for_client, current_circle_name_for_rhai_client);
let fut = async move {
match RhaiClient::new(&redis_url_clone) {
Ok(rhai_task_client) => {
rhai_task_client.submit_script_and_await_result(
&current_circle_name_for_rhai_client, // This name is used for Redis queue
script_content,
Some(rpc_id_for_client.clone()),
TASK_TIMEOUT_DURATION,
TASK_POLL_INTERVAL_DURATION,
).await
}
Err(e) => {
log::error!("Circle '{}' WS: Failed to create RhaiClient for Redis URL {}: {}", current_circle_name_for_rhai_client, redis_url_clone, e);
Err(e)
}
}
};
ctx.spawn(fut.into_actor(self).map(move |result, _act, ws_ctx| {
let response = match result {
Ok(task_details) => {
if task_details.status == "completed" {
// task_details itself doesn't have a task_id field.
// The task_id is known by the client that initiated the poll.
// We log with client_rpc_id which is the JSON-RPC request ID.
log::info!("Circle '{}' WS: Request ID {:?} completed successfully. Output: {:?}", _act.server_circle_name, client_rpc_id, task_details.output);
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: Some(serde_json::to_value(PlayResult {
output: task_details.output.unwrap_or_default()
}).unwrap()),
error: None,
id: client_rpc_id,
}
} else { // status == "error"
log::warn!("Circle '{}' WS: Request ID {:?} execution failed. Error: {:?}", _act.server_circle_name, client_rpc_id, task_details.error);
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
error: Some(JsonRpcError {
code: -32004,
message: task_details.error.unwrap_or_else(|| "Script execution failed".to_string()),
data: None,
}),
id: client_rpc_id,
}
}
}
Err(rhai_err) => {
log::error!("Circle '{}' WS: RhaiClient operation failed for req ID {:?}: {}", _act.server_circle_name, client_rpc_id, rhai_err);
let (code, message) = match rhai_err {
RhaiClientError::Timeout(task_id) => (-32002, format!("Timeout: {}", task_id)),
RhaiClientError::RedisError(e) => (-32003, format!("Redis error: {}", e)),
RhaiClientError::SerializationError(e) => (-32003, format!("Serialization error: {}", e)),
RhaiClientError::TaskNotFound(task_id) => (-32005, format!("Task not found: {}", task_id)),
};
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
id: client_rpc_id,
error: Some(JsonRpcError { code, message, data: None }),
}
}
};
ws_ctx.text(serde_json::to_string(&response).unwrap());
}));
}
Err(e) => {
log::error!("Circle '{}' WS: Invalid params for 'play' method: {}", self.server_circle_name, e);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: client_rpc_id,
error: Some(JsonRpcError { code: -32602, message: "Invalid params".to_string(), data: Some(Value::String(e.to_string())) }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
} else {
log::warn!("Circle '{}' WS: Method not found: {}", self.server_circle_name, req.method);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: client_rpc_id,
error: Some(JsonRpcError { code: -32601, message: "Method not found".to_string(), data: None }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
Err(e) => {
log::error!("Circle '{}' WS: Failed to parse JSON-RPC request: {}", self.server_circle_name, e);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: Value::Null,
error: Some(JsonRpcError { code: -32700, message: "Parse error".to_string(), data: Some(Value::String(e.to_string())) }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
}
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Pong(_)) => {},
Ok(ws::Message::Binary(_bin)) => log::warn!("Circle '{}' WS: Binary messages not supported.", self.server_circle_name),
Ok(ws::Message::Close(reason)) => {
log::info!("Circle '{}' WS: Close message received. Reason: {:?}", self.server_circle_name, reason);
ctx.close(reason);
ctx.stop();
}
Ok(ws::Message::Continuation(_)) => ctx.stop(),
Ok(ws::Message::Nop) => (),
Err(e) => {
log::error!("Circle '{}' WS: Error: {:?}", self.server_circle_name, e);
ctx.stop();
}
}
}
}
// Modified ws_handler to accept newtype wrapped app_data
async fn ws_handler_modified(
req: HttpRequest,
stream: web::Payload,
app_circle_name: web::Data<AppCircleName>, // Use wrapped type
app_redis_url: web::Data<AppRedisUrl>, // Use wrapped type
) -> Result<HttpResponse, Error> {
let circle_name_str = app_circle_name.0.clone();
let redis_url_str = app_redis_url.0.clone();
log::info!("WebSocket handshake attempt for server: '{}' with redis: '{}'", circle_name_str, redis_url_str);
let resp = ws::start(
CircleWs::new(circle_name_str, redis_url_str), // Pass unwrapped strings
&req,
stream
)?;
Ok(resp)
}
// Public factory function to spawn the server
pub fn spawn_circle_ws_server(
_circle_id: u32,
circle_name: String,
port: u16,
redis_url: String,
// Sender to send the server handle back to the orchestrator
server_handle_tx: oneshot::Sender<actix_web::dev::Server>,
) -> JoinHandle<std::io::Result<()>> {
let circle_name_for_log = circle_name.clone();
// redis_url_for_log is not used, but kept for consistency if needed later
tokio::spawn(async move {
let circle_name_outer = circle_name;
let redis_url_outer = redis_url;
let app_factory = move || {
App::new()
.app_data(web::Data::new(AppCircleName(circle_name_outer.clone())))
.app_data(web::Data::new(AppRedisUrl(redis_url_outer.clone())))
.route("/ws", web::get().to(ws_handler_modified))
.default_service(web::route().to(|| async { HttpResponse::NotFound().body("404 Not Found") }))
};
let server_builder = HttpServer::new(app_factory);
let bound_server = match server_builder.bind(("127.0.0.1", port)) {
Ok(srv) => {
log::info!(
"Successfully bound WebSocket server for Circle: '{}' on port {}. Starting...",
circle_name_for_log, port
);
srv
}
Err(e) => {
log::error!(
"Failed to bind WebSocket server for Circle '{}' on port {}: {}",
circle_name_for_log, port, e
);
// If binding fails, we can't send a server handle.
// The orchestrator will see the JoinHandle error out or the oneshot::Sender drop.
return Err(e);
}
};
let server_runnable: actix_web::dev::Server = bound_server.run();
// Send the server handle back to the orchestrator
if server_handle_tx.send(server_runnable.clone()).is_err() {
log::error!(
"Failed to send server handle back to orchestrator for Circle '{}'. Orchestrator might have shut down.",
circle_name_for_log
);
// Server might still run, but orchestrator can't stop it gracefully via this handle.
// Consider stopping it here if sending the handle is critical.
// For now, let it proceed, but log the error.
}
// Now await the server_runnable (which is the Server handle itself)
if let Err(e) = server_runnable.await {
log::error!("WebSocket server for Circle '{}' on port {} failed during run: {}", circle_name_for_log, port, e);
return Err(e);
}
log::info!("WebSocket server for Circle '{}' on port {} shut down gracefully.", circle_name_for_log, port);
Ok(())
})
}

View File

@ -1,260 +0,0 @@
use actix_web::{web, App, HttpRequest, HttpServer, HttpResponse, Error};
use actix_web_actors::ws;
use actix::{Actor, ActorContext, StreamHandler, AsyncContext, WrapFuture, ActorFutureExt};
// HashMap no longer needed
use clap::Parser;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
// AsyncCommands no longer directly used here
use rhai_client::RhaiClientError; // Import RhaiClientError for matching
// Uuid is not directly used here anymore for task_id generation, RhaiClient handles it.
// Utc no longer directly used here
// RhaiClientError is not directly handled here, errors from RhaiClient are strings or RhaiClient's own error type.
use rhai_client::RhaiClient; // ClientRhaiTaskDetails is used via rhai_client::RhaiTaskDetails
const REDIS_URL: &str = "redis://127.0.0.1/"; // Make this configurable if needed
// JSON-RPC 2.0 Structures
#[derive(Serialize, Deserialize, Debug, Clone)] // Added Clone
struct JsonRpcRequest {
jsonrpc: String,
method: String,
params: Value,
id: Option<Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone)] // Added Clone
struct JsonRpcResponse {
jsonrpc: String,
result: Option<Value>,
error: Option<JsonRpcError>,
id: Value,
}
#[derive(Serialize, Deserialize, Debug, Clone)] // Added Clone
struct JsonRpcError {
code: i32,
message: String,
data: Option<Value>,
}
// Specific params and result for "play" method
#[derive(Serialize, Deserialize, Debug, Clone)] // Added Clone
struct PlayParams {
script: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)] // Added Clone
struct PlayResult {
output: String,
}
// Local RhaiTaskDetails struct is removed, will use ClientRhaiTaskDetails from rhai_client crate.
// Ensure field names used in polling logic (e.g. error_message) are updated if they differ.
// rhai_client::RhaiTaskDetails uses 'error' and 'client_rpc_id'.
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, value_parser, default_value_t = 8080)]
port: u16,
#[clap(short, long, value_parser)]
circle_name: String,
}
// WebSocket Actor
struct CircleWs {
server_circle_name: String,
// redis_client field removed as RhaiClient handles its own connection
}
const TASK_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // 30 seconds timeout
const TASK_POLL_INTERVAL_DURATION: Duration = Duration::from_millis(200); // 200 ms poll interval
impl CircleWs {
fn new(name: String) -> Self {
Self {
server_circle_name: name,
}
}
}
impl Actor for CircleWs {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
log::info!("WebSocket session started for server dedicated to: {}", self.server_circle_name);
}
fn stopping(&mut self, _ctx: &mut Self::Context) -> actix::Running {
log::info!("WebSocket session stopping for server dedicated to: {}", self.server_circle_name);
actix::Running::Stop
}
}
// WebSocket message handler
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for CircleWs {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Text(text)) => {
log::info!("WS Text for {}: {}", self.server_circle_name, text);
match serde_json::from_str::<JsonRpcRequest>(&text) {
Ok(req) => {
let client_rpc_id = req.id.clone().unwrap_or(Value::Null);
if req.method == "play" {
match serde_json::from_value::<PlayParams>(req.params.clone()) {
Ok(play_params) => {
// Use RhaiClient to submit the script
let script_content = play_params.script;
let current_circle_name = self.server_circle_name.clone();
let rpc_id_for_client = client_rpc_id.clone(); // client_rpc_id is already Value
let fut = async move {
match RhaiClient::new(REDIS_URL) {
Ok(rhai_task_client) => {
rhai_task_client.submit_script_and_await_result(
&current_circle_name,
script_content,
Some(rpc_id_for_client.clone()),
TASK_TIMEOUT_DURATION,
TASK_POLL_INTERVAL_DURATION,
).await // This returns Result<rhai_client::RhaiTaskDetails, RhaiClientError>
}
Err(e) => {
log::error!("Failed to create RhaiClient: {}", e);
Err(e) // Convert the error from RhaiClient::new into the type expected by the map function's error path.
}
}
};
ctx.spawn(fut.into_actor(self).map(move |result, _act, ws_ctx| {
let response = match result {
Ok(task_details) => { // ClientRhaiTaskDetails
if task_details.status == "completed" {
log::info!("Task completed successfully. Client RPC ID: {:?}, Output: {:?}", task_details.client_rpc_id, task_details.output);
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: Some(serde_json::to_value(PlayResult {
output: task_details.output.unwrap_or_default()
}).unwrap()),
error: None,
id: client_rpc_id, // Use the original client_rpc_id from the request
}
} else { // status == "error"
log::warn!("Task execution failed. Client RPC ID: {:?}, Error: {:?}", task_details.client_rpc_id, task_details.error);
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
error: Some(JsonRpcError {
code: -32004, // Script execution error
message: task_details.error.unwrap_or_else(|| "Script execution failed with no specific error message".to_string()),
data: None,
}),
id: client_rpc_id,
}
}
}
Err(rhai_err) => { // RhaiClientError
log::error!("RhaiClient operation failed: {}", rhai_err);
let (code, message) = match rhai_err {
RhaiClientError::Timeout(task_id) => (-32002, format!("Timeout waiting for task {} to complete", task_id)),
RhaiClientError::RedisError(e) => (-32003, format!("Redis communication error: {}", e)),
RhaiClientError::SerializationError(e) => (-32003, format!("Serialization error: {}", e)),
RhaiClientError::TaskNotFound(task_id) => (-32005, format!("Task {} not found after submission", task_id)),
};
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
id: client_rpc_id,
error: Some(JsonRpcError { code, message, data: None }),
}
}
};
ws_ctx.text(serde_json::to_string(&response).unwrap());
}));
}
Err(e) => { // Invalid params for 'play'
log::error!("Invalid params for 'play' method: {}", e);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: client_rpc_id,
error: Some(JsonRpcError { code: -32602, message: "Invalid params".to_string(), data: Some(Value::String(e.to_string())) }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
} else { // Method not found
log::warn!("Method not found: {}", req.method);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: client_rpc_id,
error: Some(JsonRpcError { code: -32601, message: "Method not found".to_string(), data: None }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
Err(e) => { // Parse error
log::error!("Failed to parse JSON-RPC request: {}", e);
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(), result: None, id: Value::Null, // No ID if request couldn't be parsed
error: Some(JsonRpcError { code: -32700, message: "Parse error".to_string(), data: Some(Value::String(e.to_string())) }),
};
ctx.text(serde_json::to_string(&err_resp).unwrap());
}
}
}
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Pong(_)) => {},
Ok(ws::Message::Binary(_bin)) => log::warn!("Binary messages not supported."),
Ok(ws::Message::Close(reason)) => {
ctx.close(reason);
ctx.stop();
}
Ok(ws::Message::Continuation(_)) => ctx.stop(),
Ok(ws::Message::Nop) => (),
Err(e) => {
log::error!("WS Error: {:?}", e);
ctx.stop();
}
}
}
}
// WebSocket handshake and actor start
async fn ws_handler(
req: HttpRequest,
stream: web::Payload,
server_name: web::Data<String>,
// redis_client: web::Data<redis::Client>, // No longer passed to CircleWs actor directly
) -> Result<HttpResponse, Error> {
log::info!("WebSocket handshake attempt for server: {}", server_name.get_ref());
let resp = ws::start(
CircleWs::new(server_name.get_ref().clone()), // Pass only the server name
&req,
stream
)?;
Ok(resp)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args = Args::parse();
std::env::set_var("RUST_LOG", "info,circle_server_ws=debug");
env_logger::init();
log::info!(
"Starting WebSocket server for Circle: '{}' on port {}...",
args.circle_name, args.port
);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(args.circle_name.clone()))
.route("/ws", web::get().to(ws_handler))
.default_service(web::route().to(|| async { HttpResponse::NotFound().body("404 Not Found - This is a WebSocket-only server for a specific circle.") }))
})
.bind(("127.0.0.1", args.port))?
.run()
.await
}

View File

@ -1,135 +0,0 @@
use tokio::time::Duration; // Removed unused sleep
use futures_util::{sink::SinkExt, stream::StreamExt};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use serde_json::Value; // Removed unused json macro import
use std::process::Command;
use std::thread;
use std::sync::Once;
// Define a simple JSON-RPC request structure for sending scripts
#[derive(serde::Serialize, Debug)]
struct JsonRpcRequest {
jsonrpc: String,
method: String,
params: ScriptParams,
id: u64,
}
#[derive(serde::Serialize, Debug)]
struct ScriptParams {
script: String,
}
// Define a simple JSON-RPC error response structure for assertion
#[derive(serde::Deserialize, Debug)]
struct JsonRpcErrorResponse {
_jsonrpc: String, // Field is present in response, but not used in assert
error: JsonRpcErrorDetails,
_id: Option<Value>, // Field is present in response, but not used in assert
}
#[derive(serde::Deserialize, Debug)]
struct JsonRpcErrorDetails {
code: i32,
message: String,
}
const SERVER_ADDRESS: &str = "ws://127.0.0.1:8088/ws"; // Match port in main.rs or make configurable
const TEST_CIRCLE_NAME: &str = "test_timeout_circle";
const SERVER_STARTUP_TIME: Duration = Duration::from_secs(5); // Time to wait for server to start
const RHAI_TIMEOUT_SECONDS: u64 = 30; // Should match TASK_TIMEOUT_DURATION in circle_server_ws
static START_SERVER: Once = Once::new();
fn ensure_server_is_running() {
START_SERVER.call_once(|| {
println!("Attempting to start circle_server_ws for integration tests...");
// The server executable will be in target/debug relative to the crate root
let server_executable = "target/debug/circle_server_ws";
thread::spawn(move || {
let mut child = Command::new(server_executable)
.arg("--port=8088") // Use a specific port for testing
.arg(format!("--circle-name={}", TEST_CIRCLE_NAME))
.spawn()
.expect("Failed to start circle_server_ws. Make sure it's compiled (cargo build).");
let status = child.wait().expect("Failed to wait on server process.");
println!("Server process exited with status: {}", status);
});
println!("Server start command issued. Waiting for {}s...", SERVER_STARTUP_TIME.as_secs());
thread::sleep(SERVER_STARTUP_TIME);
println!("Presumed server started.");
});
}
#[tokio::test]
async fn test_rhai_script_timeout() {
ensure_server_is_running();
println!("Connecting to WebSocket server: {}", SERVER_ADDRESS);
let (mut ws_stream, _response) = connect_async(SERVER_ADDRESS)
.await
.expect("Failed to connect to WebSocket server");
println!("Connected to WebSocket server.");
// Rhai script designed to run longer than RHAI_TIMEOUT_SECONDS
// A large loop should cause a timeout.
let long_running_script = format!("
let mut x = 0;
for i in 0..999999999 {{
x = x + i;
if i % 10000000 == 0 {{
// debug(\"Looping: \" + i); // Optional: for server-side logging if enabled
}}
}}
print(x); // This line will likely not be reached due to timeout
");
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: "execute_script".to_string(),
params: ScriptParams { script: long_running_script },
id: 1,
};
let request_json = serde_json::to_string(&request).expect("Failed to serialize request");
println!("Sending long-running script request: {}", request_json);
ws_stream.send(Message::Text(request_json)).await.expect("Failed to send message");
println!("Waiting for response (expecting timeout after ~{}s)..", RHAI_TIMEOUT_SECONDS);
// Wait for a response, expecting a timeout error
// The server's timeout is RHAI_TIMEOUT_SECONDS, client should wait a bit longer.
match tokio::time::timeout(Duration::from_secs(RHAI_TIMEOUT_SECONDS + 15), ws_stream.next()).await {
Ok(Some(Ok(Message::Text(text)))) => {
println!("Received response: {}", text);
let response: Result<JsonRpcErrorResponse, _> = serde_json::from_str(&text);
match response {
Ok(err_resp) => {
assert_eq!(err_resp.error.code, -32002, "Error code should indicate timeout.");
assert!(err_resp.error.message.contains("timed out"), "Error message should indicate timeout.");
println!("Timeout test passed! Received correct timeout error.");
}
Err(e) => {
panic!("Failed to deserialize error response: {}. Raw: {}", e, text);
}
}
}
Ok(Some(Ok(other_msg))) => {
panic!("Received unexpected message type: {:?}", other_msg);
}
Ok(Some(Err(e))) => {
panic!("WebSocket error: {}", e);
}
Ok(None) => {
panic!("WebSocket stream closed unexpectedly.");
}
Err(_) => {
panic!("Test timed out waiting for server response. Server might not have sent timeout error or took too long.");
}
}
ws_stream.close(None).await.ok();
println!("Test finished, WebSocket closed.");
}

View File

@ -0,0 +1,5 @@
# This configuration is picked up by Cargo when building the `circles-app` crate.
# It sets the required RUSTFLAGS to enable the JavaScript backend for the `getrandom` crate,
# which is necessary for WebAssembly compilation.
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']

2
src/app/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/dist/
/target/

1387
src/app/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

45
src/app/Cargo.toml Normal file
View File

@ -0,0 +1,45 @@
[package]
name = "circles-app"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
heromodels = { path = "/Users/timurgordon/code/git.ourworld.tf/herocode/db/heromodels" }
circle_client_ws = { path = "../client_ws" }
futures = "0.3"
yew-router = "0.18"
yew = { version = "0.21", features = ["csr"] }
wasm-bindgen = "0.2"
log = "0.4"
wasm-logger = "0.2"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
web-sys = { version = "0.3", features = ["MouseEvent", "Element", "HtmlElement", "SvgElement", "Window", "Document", "CssStyleDeclaration"] }
gloo-timers = "0.3.0"
chrono = { version = "0.4", features = ["serde"] }
gloo-net = "0.4"
wasm-bindgen-futures = "0.4"
gloo-console = "0.3" # For console logging
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } # For StreamExt
futures-channel = "0.3" # For MPSC channels
rand = "0.8" # For random traffic simulation
common_models = { path = "/Users/timurgordon/code/playground/yew/common_models" }
engine = { path = "/Users/timurgordon/code/git.ourworld.tf/herocode/rhailib/src/engine" }
rhai = "1.17"
js-sys = "0.3"
getrandom = { version = "0.3", features = ["wasm_js"] }
# Authentication dependencies
secp256k1 = { workspace = true, features = ["rand", "recovery", "hashes"] }
hex = "0.4"
sha3 = "0.10"
gloo-storage = "0.3"
urlencoding = "2.1"
thiserror = "1.0"
[dev-dependencies]
wasm-bindgen-test = "0.3"

177
src/app/LICENSE-APACHE Normal file
View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

25
src/app/LICENSE-MIT Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) timurgordon <timurgordon@gmail.com>
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

75
src/app/README.md Normal file
View File

@ -0,0 +1,75 @@
# Yew Trunk Template
This is a fairly minimal template for a Yew app that's built with [Trunk].
## Usage
For a more thorough explanation of Trunk and its features, please head over to the [repository][trunk].
### Installation
If you don't already have it installed, it's time to install Rust: <https://www.rust-lang.org/tools/install>.
The rest of this guide assumes a typical Rust installation which contains both `rustup` and Cargo.
To compile Rust to WASM, we need to have the `wasm32-unknown-unknown` target installed.
If you don't already have it, install it with the following command:
```bash
rustup target add wasm32-unknown-unknown
```
Now that we have our basics covered, it's time to install the star of the show: [Trunk].
Simply run the following command to install it:
```bash
cargo install trunk wasm-bindgen-cli
```
That's it, we're done!
### Running
```bash
trunk serve
```
Rebuilds the app whenever a change is detected and runs a local server to host it.
There's also the `trunk watch` command which does the same thing but without hosting it.
### Release
```bash
trunk build --release
```
This builds the app in release mode similar to `cargo build --release`.
You can also pass the `--release` flag to `trunk serve` if you need to get every last drop of performance.
Unless overwritten, the output will be located in the `dist` directory.
## Using this template
There are a few things you have to adjust when adopting this template.
### Remove example code
The code in [src/main.rs](src/main.rs) specific to the example is limited to only the `view` method.
There is, however, a fair bit of Sass in [index.scss](index.scss) you can remove.
### Update metadata
Update the `version`, `description` and `repository` fields in the [Cargo.toml](Cargo.toml) file.
The [index.html](index.html) file also contains a `<title>` tag that needs updating.
Finally, you should update this very `README` file to be about your app.
### License
The template ships with both the Apache and MIT license.
If you don't want to have your app dual licensed, just remove one (or both) of the files and update the `license` field in `Cargo.toml`.
There are two empty spaces in the MIT license you need to fill out: `` and `timurgordon <timurgordon@gmail.com>`.
[trunk]: https://github.com/thedodd/trunk

2
src/app/Trunk.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "index.html"

215
src/app/auth_system_plan.md Normal file
View File

@ -0,0 +1,215 @@
# Authentication System Architecture Plan (Clean Separation)
## Overview
A comprehensive authentication system for a standalone WASM Yew application that uses the `client_ws` library for WebSocket authentication with secp256k1 cryptographic signatures. The system maintains clear separation between generic WebSocket/crypto functionality and app-specific user management.
## 🏗️ Clean System Architecture
```mermaid
graph TB
subgraph "Demo App (Application Layer)"
A[Login Component] --> B[Auth Manager]
B --> C[Email Store - Hardcoded Mapping]
B --> D[App-Specific Auth State]
C --> E[Email-to-Private-Key Lookup]
end
subgraph "client_ws Library (Generic Layer)"
F[CircleWsClient] --> G[Crypto Utils]
F --> H[Nonce Client]
G --> I[secp256k1 Signing]
H --> J[REST Nonce Requests]
end
subgraph "External Services"
K[WebSocket Server] --> L[Auth Middleware]
M[Auth Server] --> N[Nonce Endpoint]
L --> O[Signature Verification]
end
B --> F
F --> K
H --> M
subgraph "Authentication Flow"
P[1. Email/Private Key Input] --> Q[2. App Layer Lookup]
Q --> R[3. Create Authenticated Client]
R --> S[4. Client Fetches Nonce]
S --> T[5. Client Signs Nonce]
T --> U[6. WebSocket Connection]
U --> V[7. Server Verification]
end
```
## 📁 Clean File Structure
```
app/src/auth/
├── mod.rs # App auth module exports + client_ws re-exports
├── auth_manager.rs # App-specific auth coordination
├── email_store.rs # Hardcoded email-to-key mappings (app-specific)
└── types.rs # App-specific auth types + client_ws re-exports
client_ws/src/auth/
├── mod.rs # Generic auth module
├── crypto_utils.rs # secp256k1 operations (generic)
├── nonce_client.rs # REST nonce client (generic)
└── types.rs # Core auth types (generic)
client_ws/src/
└── lib.rs # WebSocket client with auth support
```
## 🔐 Clean Authentication Flow
### 1. Email Authentication (App-Specific)
1. **User enters email** in app app login component
2. **App looks up email** in hardcoded email_store.rs
3. **App retrieves private key** from hardcoded mapping
4. **App creates CircleWsClient** with private key
5. **Client library handles** nonce fetching, signing, WebSocket connection
### 2. Private Key Authentication (Generic)
1. **User enters private key** directly
2. **App creates CircleWsClient** with private key
3. **Client library handles** nonce fetching, signing, WebSocket connection
## 🛠️ Key Separation Principles
### App Layer Responsibilities (app/src/auth/)
- ✅ **Email-to-private-key mappings** (email_store.rs)
- ✅ **User interface logic** (login components)
- ✅ **App-specific auth state** (AuthState, AuthMethod with Email)
- ✅ **Session management** (local storage, UI state)
- ✅ **Business logic** (user management, app data)
### Client Library Responsibilities (client_ws/)
- ✅ **WebSocket connection management**
- ✅ **Cryptographic operations** (secp256k1 signing/verification)
- ✅ **Nonce fetching** from REST endpoints
- ✅ **Private key authentication** (generic)
- ✅ **Cross-platform support** (WASM + Native)
### What Was Removed (Duplicated Code)
- ❌ **crypto_utils.rs** from app (use client_ws instead)
- ❌ **nonce_client.rs** from app (use client_ws instead)
- ❌ **Duplicated auth types** (use client_ws types)
- ❌ **Email authentication** from client_ws (app-specific)
## 📋 Implementation Status
### ✅ Completed
1. **Cleaned client_ws library**
- Removed app-specific email authentication
- Kept only private key authentication
- Updated documentation with clear separation
2. **Updated app app**
- Removed duplicated crypto_utils.rs
- Removed duplicated nonce_client.rs
- Updated auth_manager.rs to use client_ws
- Updated types.rs to re-export client_ws types
- Kept app-specific email_store.rs
3. **Clear integration pattern**
- App handles email-to-key lookup
- App creates CircleWsClient with private key
- Client library handles all WebSocket/crypto operations
## 🔧 Usage Examples
### App-Level Authentication
```rust
// In app app auth_manager.rs
impl AuthManager {
pub async fn authenticate_with_email(&self, email: String) -> AuthResult<()> {
// 1. App-specific: Look up email in hardcoded store
let key_pair = get_key_pair_for_email(&email)?;
// 2. Generic: Validate using client_ws
validate_private_key(&key_pair.private_key)?;
// 3. App-specific: Update app auth state
self.set_state(AuthState::Authenticated {
public_key: key_pair.public_key,
private_key: key_pair.private_key,
method: AuthMethod::Email(email),
});
Ok(())
}
pub async fn create_authenticated_client(&self, ws_url: &str, auth_server_url: &str) -> Result<CircleWsClient, CircleWsClientError> {
// 1. App-specific: Get private key from app state
let private_key = match self.state.borrow().clone() {
AuthState::Authenticated { private_key, .. } => private_key,
_ => return Err(CircleWsClientError::NotConnected),
};
// 2. Generic: Create and authenticate client using client_ws
let mut client = CircleWsClient::new_with_auth(
ws_url.to_string(),
auth_server_url.to_string(),
private_key
);
client.authenticate().await?;
Ok(client)
}
}
```
### Client Library Usage
```rust
// Using client_ws directly (no app-specific logic)
use circle_client_ws::CircleWsClient;
let mut client = CircleWsClient::new_with_auth(
"ws://localhost:8080/ws".to_string(),
"http://localhost:8080".to_string(),
private_key
);
client.authenticate().await?;
client.connect().await?;
let result = client.play("console.log('Hello, authenticated world!');".to_string()).await?;
```
## 🎯 Benefits of Clean Separation
1. **Reusability**: client_ws can be used by any Rust application
2. **Maintainability**: Clear boundaries between WebSocket/crypto and user management
3. **Testability**: Each layer can be tested independently
4. **Security**: Consistent crypto handling at the library level
5. **Flexibility**: Apps can implement any authentication UX they need
## 🔒 Security Considerations
### Client Library (Generic)
- ✅ **Secure crypto operations** using secp256k1
- ✅ **Proper nonce handling** with expiration
- ✅ **Ethereum-compatible signing** (eth_sign style)
- ✅ **Cross-platform security** (WASM + Native)
### Demo App (App-Specific)
- ✅ **Hardcoded keys for app** (easy to rotate)
- ✅ **No sensitive server storage** needed
- ✅ **Local storage** (non-sensitive state only)
- ✅ **Clear separation** of concerns
## 🚀 Migration Benefits
### Before (Mixed Concerns)
- Duplicated crypto code in both client_ws and app
- Email authentication mixed into generic library
- Hard to reuse client_ws in other projects
- Unclear separation of responsibilities
### After (Clean Separation)
- Single source of truth for crypto operations (client_ws)
- App-specific logic clearly separated (app/auth/)
- client_ws is reusable by any application
- Clear integration patterns and documentation
This clean architecture ensures that the `client_ws` library remains focused on its core responsibility (secure WebSocket client with private key authentication) while the app app handles all user-facing and business logic appropriately.

91
src/app/csslint.sh Normal file
View File

@ -0,0 +1,91 @@
#!/bin/bash
# Parse arguments
CLEAN=false
ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--clean" ]]; then
CLEAN=true
else
ARGS+=("$arg")
fi
done
CSS_DIR="${ARGS[0]:-static}"
PROJECT_DIR="${ARGS[1]:-.}"
echo "🔍 Scanning CSS directory: $CSS_DIR"
echo "📁 Project source directory: $PROJECT_DIR"
echo "🧹 Clean mode: $CLEAN"
USED_CLASSES=$(mktemp)
CLASS_NAMES=$(mktemp)
# Step 1: collect all class names used in Rust/Yew
grep -rho --include="*.rs" 'class\s*=\s*["'"'"'][^"'"'"']*["'"'"']' "$PROJECT_DIR" \
| grep -o '[a-zA-Z0-9_-]\+' \
| sort -u > "$USED_CLASSES"
# Step 2: extract class selectors from CSS
grep -rho '^\s*\.[a-zA-Z_-][a-zA-Z0-9_-]*\s*{' "$CSS_DIR" \
| sed -E 's/^\s*//; s/\s*\{.*$//' \
| sort -u > "$CLASS_NAMES"
# Step 3: clean or list unused classes
find "$CSS_DIR" -type f -name "*.css" | while read -r css_file; do
if $CLEAN; then
TMP_CLEANED=$(mktemp)
awk -v used_classes="$USED_CLASSES" '
BEGIN {
while ((getline line < used_classes) > 0) {
used[line] = 1
}
in_block = 0
brace_depth = 0
current_class = ""
}
{
# Start of a class rule
if (!in_block && $0 ~ /^[ \t]*\.[a-zA-Z_-][a-zA-Z0-9_-]*[ \t]*\{/) {
line = $0
gsub(/^[ \t]*\./, "", line)
sub(/[ \t]*\{.*/, "", line)
current_class = line
if (!(current_class in used)) {
in_block = 1
brace_depth = gsub(/\{/, "{") - gsub(/\}/, "}")
next
}
} else if (in_block) {
brace_depth += gsub(/\{/, "{")
brace_depth -= gsub(/\}/, "}")
if (brace_depth <= 0) {
in_block = 0
}
next
}
print $0
}
' "$css_file" > "$TMP_CLEANED"
mv "$TMP_CLEANED" "$css_file"
echo "✅ Cleaned: $css_file"
else
while read -r class_selector; do
class_name=$(echo "$class_selector" | sed 's/^\.//')
if ! grep -qx "$class_name" "$USED_CLASSES"; then
echo "⚠️ Unused CSS class: $class_selector"
fi
done < "$CLASS_NAMES"
break
fi
done
rm "$USED_CLASSES" "$CLASS_NAMES"
if $CLEAN; then
echo "🧹 Done: multi-line unused class blocks removed safely."
fi

31
src/app/index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Circles</title>
<link data-trunk rel="css" href="static/css/common.css" />
<link data-trunk rel="css" href="static/styles.css" />
<link data-trunk rel="css" href="static/css/auth.css" />
<link data-trunk rel="css" href="static/css/nav_island.css" />
<link data-trunk rel="css" href="static/css/library_view.css" />
<link data-trunk rel="css" href="static/css/library_viewer.css" />
<link data-trunk rel="css" href="static/css/members_view.css" />
<link data-trunk rel="css" href="static/css/intelligence_view.css" />
<link data-trunk rel="css" href="static/css/timeline_view.css" />
<link data-trunk rel="css" href="static/css/projects_view.css" />
<link data-trunk rel="css" href="static/css/governance_view.css" />
<link data-trunk rel="css" href="static/css/calendar_view.css" />
<link data-trunk rel="css" href="static/css/treasury_view.css" />
<link data-trunk rel="css" href="static/css/publishing_view.css" />
<link data-trunk rel="css" href="static/css/customize_view.css" />
<link data-trunk rel="css" href="static/css/circles_view.css" />
<link data-trunk rel="css" href="static/css/dashboard_view.css" />
<link data-trunk rel="css" href="static/css/inspector_view.css" />
<link data-trunk rel="css" href="static/css/network_animation.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<!-- Trunk will inject a script tag here for the WASM loader -->
</head>
<body>
<!-- The Yew app will be rendered here -->
</body>
</html>

35
src/app/index.scss Normal file
View File

@ -0,0 +1,35 @@
html,
body {
height: 100%;
margin: 0;
}
body {
align-items: center;
display: flex;
justify-content: center;
background: linear-gradient(to bottom right, #444444, #009a5b);
font-size: 1.5rem;
}
main {
color: #fff6d5;
font-family: sans-serif;
text-align: center;
}
.logo {
height: 20em;
}
.heart:after {
content: "❤️";
font-size: 1.75em;
}
h1 + .subtitle {
display: block;
margin-top: -1em;
}

250
src/app/src/app.rs Normal file
View File

@ -0,0 +1,250 @@
use yew::prelude::*;
use std::rc::Rc;
use std::collections::HashMap;
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;
// Props for the App component
#[derive(Properties, PartialEq, Clone)]
pub struct AppProps {
pub start_circle_ws_url: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppView {
Login,
Circles,
Library,
Intelligence,
Publishing,
Customize,
Inspector, // Added Inspector
}
#[derive(Clone, Debug)]
pub enum Msg {
SwitchView(AppView),
UpdateCirclesContext(Vec<String>), // Context URLs from CirclesView
AuthStateChanged(AuthState),
AuthenticationSuccessful,
AuthenticationFailed(String),
Logout,
}
pub struct App {
current_view: AppView,
active_context_urls: Vec<String>, // Only context URLs from CirclesView
start_circle_ws_url: String, // Initial WebSocket URL for CirclesView
auth_manager: AuthManager,
auth_state: AuthState,
}
impl Component for App {
type Message = Msg;
type Properties = AppProps;
fn create(ctx: &Context<Self>) -> 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,
_ => AppView::Login,
};
Self {
current_view: initial_view,
active_context_urls: Vec::new(),
start_circle_ws_url,
auth_manager,
auth_state,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::UpdateCirclesContext(context_urls) => {
log::info!("App: Received context update from CirclesView: {:?}", context_urls);
self.active_context_urls = context_urls;
true
}
Msg::SwitchView(view) => {
// Check if authentication is required for certain views
match view {
AppView::Login => {
self.current_view = view;
true
}
_ => {
if self.auth_manager.is_authenticated() {
self.current_view = view;
true
} else {
log::warn!("Attempted to access {} view without authentication", format!("{:?}", view));
self.current_view = AppView::Login;
true
}
}
}
}
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
if self.current_view == AppView::Login {
self.current_view = AppView::Circles;
}
}
AuthState::NotAuthenticated | AuthState::Failed(_) => {
// Switch to login view when not authenticated
self.current_view = AppView::Login;
}
_ => {}
}
true
}
Msg::AuthenticationSuccessful => {
log::info!("App: Authentication successful");
self.current_view = AppView::Circles;
true
}
Msg::AuthenticationFailed(error) => {
log::error!("App: Authentication failed: {}", error);
self.current_view = AppView::Login;
true
}
Msg::Logout => {
log::info!("App: User logout");
self.auth_manager.logout();
self.current_view = AppView::Login;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// If not authenticated and not on login view, show login
if !self.auth_manager.is_authenticated() && self.current_view != AppView::Login {
return html! {
<LoginComponent
auth_manager={self.auth_manager.clone()}
on_authenticated={link.callback(|_| Msg::AuthenticationSuccessful)}
on_error={link.callback(Msg::AuthenticationFailed)}
/>
};
}
html! {
<div class="yew-app-container">
{ self.render_header(link) }
{ match self.current_view {
AppView::Login => {
html! {
<LoginComponent
auth_manager={self.auth_manager.clone()}
on_authenticated={link.callback(|_| Msg::AuthenticationSuccessful)}
on_error={link.callback(Msg::AuthenticationFailed)}
/>
}
},
AppView::Circles => {
html!{
<CirclesView
default_center_ws_url={self.start_circle_ws_url.clone()}
on_context_update={link.callback(Msg::UpdateCirclesContext)}
/>
}
},
AppView::Library => {
html! {
<LibraryView ws_addresses={self.active_context_urls.clone()} />
}
},
AppView::Intelligence => html! {
<IntelligenceView
all_circles={Rc::new(HashMap::new())}
context_circle_ws_urls={Some(Rc::new(self.active_context_urls.clone()))}
/>
},
AppView::Publishing => html! {
<PublishingView
all_circles={Rc::new(HashMap::new())}
context_circle_ws_urls={Some(Rc::new(self.active_context_urls.clone()))}
/>
},
AppView::Inspector => {
html! {
<InspectorView
circle_ws_addresses={Rc::new(self.active_context_urls.clone())}
auth_manager={self.auth_manager.clone()}
/>
}
},
AppView::Customize => html! {
<CustomizeViewComponent
all_circles={Rc::new(HashMap::new())}
context_circle_ws_urls={Some(Rc::new(self.active_context_urls.clone()))}
app_callback={link.callback(|msg: Msg| msg)}
/>
},
}}
{ if self.current_view != AppView::Login {
html! {
<NavIsland
current_view={self.current_view.clone()}
on_switch_view={link.callback(Msg::SwitchView)}
/>
}
} else {
html! {}
}}
</div>
}
}
}
impl App {
fn render_header(&self, link: &html::Scope<Self>) -> Html {
if self.current_view == AppView::Login {
return html! {};
}
html! {
<header>
<div class="app-title-button">
<span class="app-title-name">{ "Circles" }</span>
</div>
<AuthView
auth_state={self.auth_state.clone()}
on_logout={link.callback(|_| Msg::Logout)}
on_login={link.callback(|_| Msg::SwitchView(AppView::Login))}
/>
</header>
}
}
}

View File

@ -0,0 +1,348 @@
//! 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};
/// Key for storing authentication state in local storage
const AUTH_STATE_STORAGE_KEY: &str = "circles_auth_state_marker";
const PRIVATE_KEY_SESSION_STORAGE_KEY: &str = "circles_private_key";
/// Authentication manager that coordinates the auth flow
#[derive(Clone)]
pub struct AuthManager {
state: Rc<RefCell<AuthState>>,
on_state_change: Rc<RefCell<Option<Callback<AuthState>>>>,
}
impl PartialEq for AuthManager {
fn eq(&self, other: &Self) -> bool {
// Compare based on the current auth state
self.get_state() == other.get_state()
}
}
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)),
}
}
/// Set callback for authentication state changes
pub fn set_on_state_change(&self, callback: Callback<AuthState>) {
*self.on_state_change.borrow_mut() = Some(callback);
}
/// Get current authentication state
pub fn get_state(&self) -> AuthState {
self.state.borrow().clone()
}
/// Check if currently authenticated
pub fn is_authenticated(&self) -> bool {
matches!(*self.state.borrow(), AuthState::Authenticated { .. })
}
/// Authenticate using email
pub async fn authenticate_with_email(&self, email: String) -> AuthResult<()> {
self.set_state(AuthState::Authenticating);
// Look up the email in the hardcoded store
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))?;
// Set authenticated state
let auth_state = AuthState::Authenticated {
public_key: key_pair.public_key,
private_key: key_pair.private_key,
method: AuthMethod::Email(email),
};
self.set_state(auth_state);
Ok(())
}
/// Authenticate using private key
pub async fn authenticate_with_private_key(&self, private_key: String) -> AuthResult<()> {
self.set_state(AuthState::Authenticating);
// Validate the private key using client_ws
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))?;
// Set authenticated state
let auth_state = AuthState::Authenticated {
public_key,
private_key: private_key.clone(),
method: AuthMethod::PrivateKey,
};
self.set_state(auth_state);
Ok(())
}
/// Create an authenticated WebSocket client using message-based authentication
pub async fn create_authenticated_client(&self, ws_url: &str) -> Result<CircleWsClient, CircleWsClientError> {
let auth_state = self.state.borrow().clone();
let private_key = match auth_state {
AuthState::Authenticated { private_key, .. } => private_key,
_ => return Err(CircleWsClientError::AuthNoKeyPair),
};
let mut client = CircleWsClientBuilder::new(ws_url.to_string())
.with_keypair(private_key)
.build();
client.connect().await?;
client.authenticate().await?;
Ok(client)
}
/// Check if an email is available for authentication
pub fn is_email_available(&self, email: &str) -> bool {
is_email_available(email)
}
/// Get list of available emails for app purposes
pub fn get_available_emails(&self) -> Vec<String> {
crate::auth::email_store::get_available_emails()
}
/// Logout and clear authentication state
pub fn logout(&self) {
self.set_state(AuthState::NotAuthenticated);
self.clear_stored_auth_state();
}
/// 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);
}
}
/// Save authentication state to storage.
/// 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 } => {
match method {
AuthMethod::Email(email) => {
let marker = format!("email:{}", email);
let _ = LocalStorage::set(AUTH_STATE_STORAGE_KEY, marker);
// Clear private key from session storage if user switched to email auth
let _ = SessionStorage::delete(PRIVATE_KEY_SESSION_STORAGE_KEY);
}
AuthMethod::PrivateKey => {
// Store the actual private key in sessionStorage
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());
}
}
}
AuthState::NotAuthenticated => {
let _ = LocalStorage::set(AUTH_STATE_STORAGE_KEY, "not_authenticated".to_string());
let _ = SessionStorage::delete(PRIVATE_KEY_SESSION_STORAGE_KEY);
}
AuthState::Authenticating | AuthState::Failed(_) => {
// Transient states, typically don't need to be persisted or can clear storage.
// For now, let's clear localStorage for these, session might still be loading.
let _ = LocalStorage::delete(AUTH_STATE_STORAGE_KEY);
// Optionally, keep session storage if an auth attempt fails but might be retried.
// However, a full logout or switch to NotAuthenticated should clear it.
}
}
}
/// Load authentication state from storage.
fn load_auth_state() -> Option<AuthState> {
if let Ok(marker) = LocalStorage::get::<String>(AUTH_STATE_STORAGE_KEY) {
if marker == "private_key_auth_marker" {
if let Ok(private_key) = SessionStorage::get::<String>(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 {
public_key,
private_key,
method: AuthMethod::PrivateKey,
});
}
}
// Invalid key in session, clear it
let _ = SessionStorage::delete(PRIVATE_KEY_SESSION_STORAGE_KEY);
}
// Marker present but key missing/invalid, treat as not authenticated
let _ = LocalStorage::set(AUTH_STATE_STORAGE_KEY, "not_authenticated".to_string());
return Some(AuthState::NotAuthenticated);
} else if let Some(email) = marker.strip_prefix("email:") {
if let Ok(key_pair) = get_key_pair_for_email(email) {
// Ensure session storage is clear if we are in email mode
let _ = SessionStorage::delete(PRIVATE_KEY_SESSION_STORAGE_KEY);
return Some(AuthState::Authenticated {
public_key: key_pair.public_key,
private_key: key_pair.private_key, // This is from email_store, not user input
method: AuthMethod::Email(email.to_string()),
});
}
// Email re-auth failed
let _ = LocalStorage::set(AUTH_STATE_STORAGE_KEY, "not_authenticated".to_string());
return Some(AuthState::NotAuthenticated);
} else if marker == "not_authenticated" {
return Some(AuthState::NotAuthenticated);
}
}
// No valid marker or key found
None // Defaults to NotAuthenticated in AuthManager::new()
}
/// Clear stored authentication state from both localStorage and sessionStorage
fn clear_stored_auth_state(&self) {
let _ = LocalStorage::delete(AUTH_STATE_STORAGE_KEY);
let _ = SessionStorage::delete(PRIVATE_KEY_SESSION_STORAGE_KEY);
}
/// Get current authentication method if authenticated
pub fn get_auth_method(&self) -> Option<AuthMethod> {
match &*self.state.borrow() {
AuthState::Authenticated { method, .. } => Some(method.clone()),
_ => None,
}
}
/// Get current public key if authenticated
pub fn get_public_key(&self) -> Option<String> {
match &*self.state.borrow() {
AuthState::Authenticated { public_key, .. } => Some(public_key.clone()),
_ => None,
}
}
/// Validate current authentication state
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))
}
_ => Err(AuthError::AuthFailed("Not authenticated".to_string())),
}
}
}
impl Default for AuthManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[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;
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"),
_ => panic!("Expected email auth method"),
}
}
#[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;
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());
}
#[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;
assert!(result.is_err());
assert!(!auth_manager.is_authenticated());
}
#[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;
assert!(result.is_err());
assert!(!auth_manager.is_authenticated());
}
#[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;
assert!(auth_manager.is_authenticated());
// Logout
auth_manager.logout();
assert!(!auth_manager.is_authenticated());
assert!(auth_manager.get_public_key().is_none());
}
#[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"));
}
}

View File

@ -0,0 +1,180 @@
//! 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 circle_client_ws::auth::derive_public_key;
/// A key pair consisting of private and public keys
#[derive(Debug, Clone)]
pub struct KeyPair {
pub private_key: String,
pub public_key: String,
}
/// 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<String, KeyPair> {
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"
),
(
"bob@example.com",
"0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
),
(
"charlie@example.com",
"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
),
(
"diana@example.com",
"0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"
),
(
"eve@example.com",
"0x1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff"
),
(
"admin@circles.com",
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
),
(
"app@circles.com",
"0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef"
),
(
"test@circles.com",
"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) {
mappings.insert(
email.to_string(),
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<KeyPair> {
let mappings = get_email_key_mappings();
mappings.get(email)
.cloned()
.ok_or_else(|| AuthError::EmailNotFound(email.to_string()))
}
/// Get all available email addresses
pub fn get_available_emails() -> Vec<String> {
get_email_key_mappings().keys().cloned().collect()
}
/// Check if an email address is available in the store
pub fn is_email_available(email: &str) -> bool {
get_email_key_mappings().contains_key(email)
}
/// Add a new email-key mapping (for runtime additions)
/// Note: This will only persist for the current session
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);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use circle_client_ws::auth::{validate_private_key, verify_signature, sign_message};
#[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"));
}
#[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);
}
#[test]
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);
}
#[test]
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");
}
_ => panic!("Expected EmailNotFound error"),
}
}
#[test]
fn test_available_emails() {
let emails = get_available_emails();
assert!(!emails.is_empty());
assert!(emails.contains(&"alice@example.com".to_string()));
assert!(emails.contains(&"admin@circles.com".to_string()));
}
#[test]
fn test_is_email_available() {
assert!(is_email_available("alice@example.com"));
assert!(is_email_available("admin@circles.com"));
assert!(!is_email_available("nonexistent@example.com"));
}
}

17
src/app/src/auth/mod.rs Normal file
View File

@ -0,0 +1,17 @@
//! 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;
pub mod email_store;
pub mod types;
pub use auth_manager::AuthManager;
pub use types::*;
// Re-export commonly used items from client_ws for convenience

72
src/app/src/auth/types.rs Normal file
View File

@ -0,0 +1,72 @@
//! Application-specific authentication types
//!
//! This module defines app-specific authentication types that extend
//! the core types from the client_ws library.
// Re-export core types from client_ws
// Define app-specific AuthResult that uses our local AuthError
pub type AuthResult<T> = Result<T, AuthError>;
// Extend AuthError with app-specific variants
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Error)]
pub enum AuthError {
#[error("Client error: {0}")]
ClientError(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
// App-specific errors
#[error("Email not found: {0}")]
EmailNotFound(String),
#[error("Generic error: {0}")]
Generic(String),
#[error("Not authenticated")]
NotAuthenticated,
}
impl From<circle_client_ws::CircleWsClientError> for AuthError {
fn from(err: circle_client_ws::CircleWsClientError) -> Self {
AuthError::ClientError(err.to_string())
}
}
impl From<circle_client_ws::auth::AuthError> for AuthError {
fn from(err: circle_client_ws::auth::AuthError) -> Self {
AuthError::AuthFailed(err.to_string())
}
}
/// Authentication method chosen by the user (app-specific)
#[derive(Debug, Clone, PartialEq)]
pub enum AuthMethod {
PrivateKey, // Direct private key input
Email(String), // Email-based lookup (app-specific)
}
impl std::fmt::Display for AuthMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthMethod::PrivateKey => write!(f, "Private Key"),
AuthMethod::Email(email) => write!(f, "Email ({})", email),
}
}
}
/// Application-specific authentication state
#[derive(Debug, Clone, PartialEq)]
pub enum AuthState {
NotAuthenticated,
Authenticating,
Authenticated {
public_key: String,
private_key: String,
method: AuthMethod,
},
Failed(String), // Error message
}

View File

@ -0,0 +1,175 @@
use yew::prelude::*;
use heromodels::models::library::items::TocEntry;
use crate::components::library_view::DisplayLibraryItem;
#[derive(Clone, PartialEq, Properties)]
pub struct AssetDetailsCardProps {
pub item: DisplayLibraryItem,
pub on_back: Callback<()>,
pub on_toc_click: Callback<usize>,
pub current_slide_index: Option<usize>,
}
pub struct AssetDetailsCard;
impl Component for AssetDetailsCard {
type Message = ();
type Properties = AssetDetailsCardProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let back_handler = {
let on_back = props.on_back.clone();
Callback::from(move |_: MouseEvent| {
on_back.emit(());
})
};
match &props.item {
DisplayLibraryItem::Image(img) => html! {
<div class="card asset-details-card">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Library"}
</button>
<div class="asset-preview">
<img src={img.url.clone()} alt={img.title.clone()} class="asset-preview-image" />
</div>
<div class="asset-info">
<h3 class="asset-title">{ &img.title }</h3>
{ if let Some(desc) = &img.description {
html! { <p class="asset-description">{ desc }</p> }
} else { html! {} }}
<div class="asset-metadata">
<p><strong>{"Type:"}</strong> {"Image"}</p>
<p><strong>{"Dimensions:"}</strong> { format!("{}×{}", img.width, img.height) }</p>
</div>
</div>
</div>
},
DisplayLibraryItem::Pdf(pdf) => html! {
<div class="card asset-details-card">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Library"}
</button>
<div class="asset-preview">
<i class="fas fa-file-pdf asset-preview-icon"></i>
</div>
<div class="asset-info">
<h3 class="asset-title">{ &pdf.title }</h3>
{ if let Some(desc) = &pdf.description {
html! { <p class="asset-description">{ desc }</p> }
} else { html! {} }}
<div class="asset-metadata">
<p><strong>{"Type:"}</strong> {"PDF Document"}</p>
<p><strong>{"Pages:"}</strong> { pdf.page_count }</p>
</div>
<a href={pdf.url.clone()} target="_blank" class="external-link">
{"Open in new tab ↗"}
</a>
</div>
</div>
},
DisplayLibraryItem::Markdown(md) => html! {
<div class="card asset-details-card">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Library"}
</button>
<div class="asset-preview">
<i class="fab fa-markdown asset-preview-icon"></i>
</div>
<div class="asset-info">
<h3 class="asset-title">{ &md.title }</h3>
{ if let Some(desc) = &md.description {
html! { <p class="asset-description">{ desc }</p> }
} else { html! {} }}
<div class="asset-metadata">
<p><strong>{"Type:"}</strong> {"Markdown Document"}</p>
</div>
</div>
</div>
},
DisplayLibraryItem::Book(book) => html! {
<div class="card asset-details-card">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Library"}
</button>
<div class="asset-preview">
<i class="fas fa-book asset-preview-icon"></i>
</div>
<div class="asset-info">
<h3 class="asset-title">{ &book.title }</h3>
{ if let Some(desc) = &book.description {
html! { <p class="asset-description">{ desc }</p> }
} else { html! {} }}
<div class="asset-metadata">
<p><strong>{"Pages:"}</strong> { book.pages.len() }</p>
</div>
{ if !book.table_of_contents.is_empty() {
html! {
<div class="table-of-contents">
<h4>{"Table of Contents"}</h4>
{ self.render_toc(ctx, &book.table_of_contents) }
</div>
}
} else { html! {} }}
</div>
</div>
},
DisplayLibraryItem::Slides(slides) => html! {
<div class="card asset-details-card">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Library"}
</button>
<div class="asset-preview">
<i class="fas fa-images asset-preview-icon"></i>
</div>
<div class="asset-info">
<h3 class="asset-title">{ &slides.title }</h3>
{ if let Some(desc) = &slides.description {
html! { <p class="asset-description">{ desc }</p> }
} else { html! {} }}
<div class="asset-metadata">
<p><strong>{"Type:"}</strong> {"Slideshow"}</p>
<p><strong>{"Slides:"}</strong> { slides.slide_urls.len() }</p>
{ if let Some(current_slide) = props.current_slide_index {
html! { <p><strong>{"Current Slide:"}</strong> { format!("{} / {}", current_slide + 1, slides.slide_urls.len()) }</p> }
} else { html! {} }}
</div>
</div>
</div>
},
}
}
}
impl AssetDetailsCard {
fn render_toc(&self, ctx: &Context<Self>, toc: &[TocEntry]) -> Html {
let props = ctx.props();
html! {
<ul class="toc-list">
{ toc.iter().map(|entry| {
let page = entry.page as usize;
let on_toc_click = props.on_toc_click.clone();
let onclick = Callback::from(move |_: MouseEvent| {
on_toc_click.emit(page);
});
html! {
<li class="toc-item">
<button class="toc-link" onclick={onclick}>
{ &entry.title }
</button>
{ if !entry.subsections.is_empty() {
self.render_toc(ctx, &entry.subsections)
} else { html! {} }}
</li>
}
}).collect::<Html>() }
</ul>
}
}
}

View File

@ -0,0 +1,67 @@
use crate::auth::types::AuthState;
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
}
#[function_component(AuthView)]
pub fn auth_view(props: &AuthViewProps) -> Html {
match &props.auth_state {
AuthState::Authenticated { public_key, .. } => {
let on_logout = props.on_logout.clone();
let logout_onclick = Callback::from(move |_| {
on_logout.emit(());
});
// Truncate the public key for display
let pk_short = if public_key.len() > 10 {
format!("{}...{}", &public_key[..4], &public_key[public_key.len()-4..])
} else {
public_key.clone()
};
html! {
<div class="auth-view-container">
<span class="public-key" title={public_key.clone()}>{ format!("PK: {}", pk_short) }</span>
<button
class="logout-button"
onclick={logout_onclick}
title="Logout"
>
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
}
}
AuthState::NotAuthenticated | AuthState::Failed(_) => {
let on_login = props.on_login.clone();
let login_onclick = Callback::from(move |_| {
on_login.emit(());
});
html! {
<div class="auth-info">
<span class="auth-status">{ "Not Authenticated" }</span>
<button
class="login-button"
onclick={login_onclick}
title="Login"
>
<i class="fas fa-sign-in-alt"></i>
</button>
</div>
}
}
AuthState::Authenticating => {
html! {
<div class="auth-info">
<span class="auth-status">{ "Authenticating..." }</span>
</div>
}
}
}
}

View File

@ -0,0 +1,155 @@
use yew::prelude::*;
use heromodels::models::library::items::{Book, TocEntry};
#[derive(Clone, PartialEq, Properties)]
pub struct BookViewerProps {
pub book: Book,
pub on_back: Callback<()>,
}
pub enum BookViewerMsg {
GoToPage(usize),
NextPage,
PrevPage,
}
pub struct BookViewer {
current_page: usize,
}
impl Component for BookViewer {
type Message = BookViewerMsg;
type Properties = BookViewerProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
current_page: 0,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
BookViewerMsg::GoToPage(page) => {
self.current_page = page;
true
}
BookViewerMsg::NextPage => {
let props = _ctx.props();
if self.current_page < props.book.pages.len().saturating_sub(1) {
self.current_page += 1;
}
true
}
BookViewerMsg::PrevPage => {
if self.current_page > 0 {
self.current_page -= 1;
}
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> 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);
html! {
<div class="asset-viewer book-viewer">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Collection"}
</button>
<div class="viewer-header">
<h2 class="viewer-title">{ &props.book.title }</h2>
<div class="book-navigation">
<button
class="nav-button"
onclick={prev_handler}
disabled={self.current_page == 0}
>
<i class="fas fa-chevron-left"></i> {"Previous"}
</button>
<span class="page-indicator">
{ format!("Page {} of {}", self.current_page + 1, total_pages) }
</span>
<button
class="nav-button"
onclick={next_handler}
disabled={self.current_page >= total_pages.saturating_sub(1)}
>
{"Next"} <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div class="viewer-content">
<div class="book-page">
{ if let Some(page_content) = props.book.pages.get(self.current_page) {
self.render_markdown(page_content)
} else {
html! { <p>{"Page not found"}</p> }
}}
</div>
</div>
</div>
}
}
}
impl BookViewer {
fn render_markdown(&self, content: &str) -> Html {
// Simple markdown rendering - convert basic markdown to HTML
let lines: Vec<&str> = content.lines().collect();
let mut html_content = Vec::new();
for line in lines {
if line.starts_with("# ") {
html_content.push(html! { <h1>{ &line[2..] }</h1> });
} else if line.starts_with("## ") {
html_content.push(html! { <h2>{ &line[3..] }</h2> });
} else if line.starts_with("### ") {
html_content.push(html! { <h3>{ &line[4..] }</h3> });
} else if line.starts_with("- ") {
html_content.push(html! { <li>{ &line[2..] }</li> });
} else if line.starts_with("**") && line.ends_with("**") {
let text = &line[2..line.len()-2];
html_content.push(html! { <p><strong>{ text }</strong></p> });
} else if !line.trim().is_empty() {
html_content.push(html! { <p>{ line }</p> });
} else {
html_content.push(html! { <br/> });
}
}
html! { <div>{ for html_content }</div> }
}
pub fn render_toc(&self, ctx: &Context<Self>, toc: &[TocEntry]) -> Html {
html! {
<ul class="toc-list">
{ toc.iter().map(|entry| {
let page = entry.page as usize;
let onclick = ctx.link().callback(move |_: MouseEvent| BookViewerMsg::GoToPage(page));
html! {
<li class="toc-item">
<button class="toc-link" onclick={onclick}>
{ &entry.title }
</button>
{ if !entry.subsections.is_empty() {
self.render_toc(ctx, &entry.subsections)
} else { html! {} }}
</li>
}
}).collect::<Html>() }
</ul>
}
}
}

View File

@ -0,0 +1,665 @@
use yew::prelude::*;
use chrono::{DateTime, Utc};
use wasm_bindgen::JsCast;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
pub struct ChatMessage {
pub id: usize,
pub content: String,
pub sender: ChatSender,
pub timestamp: String,
pub title: Option<String>,
pub description: Option<String>,
pub status: Option<String>,
pub format: String,
pub source: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ChatSender {
User,
Assistant,
System,
}
#[derive(Clone, Debug, PartialEq)]
pub enum InputType {
Text,
Code,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ChatResponse {
pub data: Vec<u8>,
pub format: String,
pub source: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Conversation {
pub id: u32,
pub title: String,
pub messages: Vec<ChatMessage>,
pub created_at: String,
pub last_updated: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConversationSummary {
pub id: u32,
pub title: String,
pub last_message_preview: Option<String>,
}
#[derive(Properties, PartialEq)]
pub struct ChatInterfaceProps {
pub on_process_message: Callback<(Vec<u8>, String, Callback<ChatResponse>)>, // (data, format, response_callback)
pub placeholder: String,
pub show_title_description: bool,
pub conversation_title: Option<String>,
pub input_type: Option<InputType>,
pub input_format: Option<String>,
#[prop_or_default]
pub on_conversations_updated: Option<Callback<Vec<ConversationSummary>>>,
#[prop_or_default]
pub active_conversation_id: Option<u32>,
#[prop_or_default]
pub on_conversation_selected: Option<Callback<u32>>,
#[prop_or_default]
pub external_conversation_selection: Option<u32>,
#[prop_or_default]
pub external_new_conversation_trigger: Option<bool>,
}
pub struct ChatInterface {
conversations: HashMap<u32, Conversation>,
active_conversation_id: Option<u32>,
current_input: String,
current_title: Option<String>,
current_description: Option<String>,
next_message_id: usize,
next_conversation_id: u32,
}
pub enum ChatMsg {
UpdateInput(String),
UpdateTitle(String),
UpdateDescription(String),
SendMessage,
AddResponse(ChatResponse),
NewConversation,
SelectConversation(u32),
LoadConversation(u32),
}
impl Component for ChatInterface {
type Message = ChatMsg;
type Properties = ChatInterfaceProps;
fn create(ctx: &Context<Self>) -> Self {
let mut chat_interface = Self {
conversations: HashMap::new(),
active_conversation_id: ctx.props().active_conversation_id,
current_input: String::new(),
current_title: None,
current_description: None,
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();
// Notify parent immediately of the new conversation
if let Some(callback) = &ctx.props().on_conversations_updated {
let summaries = chat_interface.get_conversation_summaries();
callback.emit(summaries);
}
}
chat_interface
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ChatMsg::UpdateInput(input) => {
self.current_input = input;
false
}
ChatMsg::UpdateTitle(title) => {
self.current_title = Some(title);
false
}
ChatMsg::UpdateDescription(description) => {
self.current_description = Some(description);
false
}
ChatMsg::SendMessage => {
if !self.current_input.trim().is_empty() {
// Ensure we have an active conversation
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 user_message = ChatMessage {
id: self.next_message_id,
content: self.current_input.clone(),
sender: ChatSender::User,
timestamp: chrono::Utc::now().to_rfc3339(),
title: self.current_title.clone(),
description: self.current_description.clone(),
status: None,
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 {
format!("{}...", &self.current_input[..47])
} else {
self.current_input.clone()
};
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));
// Clear inputs
self.current_input.clear();
self.current_title = None;
self.current_description = None;
// Notify parent of conversation updates
self.notify_conversations_updated(ctx);
}
true
}
ChatMsg::AddResponse(response) => {
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,
sender: ChatSender::Assistant,
timestamp: chrono::Utc::now().to_rfc3339(),
title: None,
description: None,
status: Some(status),
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);
}
true
}
ChatMsg::NewConversation => {
self.create_new_conversation();
self.notify_conversations_updated(ctx);
if let Some(callback) = &ctx.props().on_conversation_selected {
if let Some(id) = self.active_conversation_id {
callback.emit(id);
}
}
true
}
ChatMsg::SelectConversation(conversation_id) => {
if self.conversations.contains_key(&conversation_id) {
self.active_conversation_id = Some(conversation_id);
true
} else {
false
}
}
ChatMsg::LoadConversation(conversation_id) => {
self.active_conversation_id = Some(conversation_id);
true
}
}
}
fn changed(&mut self, ctx: &Context<Self>, 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) {
if self.conversations.contains_key(&new_active_id) {
self.active_conversation_id = Some(new_active_id);
should_update = true;
}
}
}
// 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 {
self.create_new_conversation();
self.notify_conversations_updated(ctx);
should_update = true;
}
}
should_update
}
fn view(&self, ctx: &Context<Self>) -> 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::<web_sys::HtmlInputElement>() {
input.value()
} else if let Ok(textarea) = target.dyn_into::<web_sys::HtmlTextAreaElement>() {
textarea.value()
} else {
String::new()
};
link.send_message(ChatMsg::UpdateInput(value));
})
};
let on_title = {
let link = ctx.link().clone();
Callback::from(move |e: InputEvent| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(ChatMsg::UpdateTitle(input.value()));
})
};
let on_description = {
let link = ctx.link().clone();
Callback::from(move |e: InputEvent| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(ChatMsg::UpdateDescription(input.value()));
})
};
let on_submit = {
let link = ctx.link().clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
link.send_message(ChatMsg::SendMessage);
})
};
// 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)
.map(|conv| &conv.messages)
.unwrap_or(&empty_messages)
} else {
&empty_messages
};
// Get conversation title
let conversation_title = if let Some(conversation_id) = self.active_conversation_id {
self.conversations.get(&conversation_id)
.map(|conv| conv.title.clone())
.or_else(|| props.conversation_title.clone())
} else {
props.conversation_title.clone()
};
html! {
<div class="chat-panel">
<div class="messages-display">
{
if let Some(title) = &conversation_title {
html! { <h4>{ title }</h4> }
} else {
html! {}
}
}
{
if current_messages.is_empty() {
html! { <p class="empty-message">{ "No messages yet. Start the conversation!" }</p> }
} else {
html! {
<>
{ for current_messages.iter().map(|msg| view_chat_message(msg)) }
</>
}
}
}
</div>
<form onsubmit={on_submit} class="input-area">
{
if props.show_title_description {
html! {
<>
<input
type="text"
class="input-base title-input"
placeholder="Title for this message..."
value={self.current_title.clone().unwrap_or_default()}
oninput={on_title}
/>
<input
type="text"
class="input-base description-input"
placeholder="Description..."
value={self.current_description.clone().unwrap_or_default()}
oninput={on_description}
/>
</>
}
} else {
html! {}
}
}
{
match props.input_type.as_ref().unwrap_or(&InputType::Text) {
InputType::Code => {
let mut class = "input-base chat-input code-input".to_string();
if let Some(format) = &props.input_format {
class.push_str(&format!(" format-{}", format));
}
html! {
<textarea
class={class}
placeholder={props.placeholder.clone()}
value={self.current_input.clone()}
oninput={on_input}
rows="8"
/>
}
}
InputType::Text => {
html! {
<input
type="text"
class="input-base chat-input"
placeholder={props.placeholder.clone()}
value={self.current_input.clone()}
oninput={on_input}
/>
}
}
}
}
<button type="submit" class="button-base button-primary send-button">{ "Send" }</button>
</form>
</div>
}
}
}
impl ChatInterface {
fn create_new_conversation(&mut self) {
let now = chrono::Utc::now().to_rfc3339();
let conversation = Conversation {
id: self.next_conversation_id,
title: format!("New Conversation {}", self.next_conversation_id),
messages: Vec::new(),
created_at: now.clone(),
last_updated: now,
};
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<Self>) {
if let Some(callback) = &ctx.props().on_conversations_updated {
let summaries = self.get_conversation_summaries();
callback.emit(summaries);
}
}
fn get_conversation_summaries(&self) -> Vec<ConversationSummary> {
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()
}
});
ConversationSummary {
id: conv.id,
title: conv.title.clone(),
last_message_preview,
}
})
.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);
true
} else {
false
}
}
pub fn get_conversations(&self) -> Vec<ConversationSummary> {
self.get_conversation_summaries()
}
}
fn view_chat_message(msg: &ChatMessage) -> Html {
let timestamp = format_timestamp(&msg.timestamp);
let sender_class = match msg.sender {
ChatSender::User => "user-message",
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::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());
}
html! {
<div class={classes!(message_classes)} key={msg.id.to_string()}>
<div class="message-header">
<span class="sender">{ sender_name }</span>
<span class="timestamp">{ timestamp }</span>
{
if let Some(status) = &msg.status {
let status_class = match status.as_str() {
"Ok" => "status-ok",
"Error" => "status-error",
_ => "status-pending",
};
html! {
<span class={classes!("status", status_class)}>{ status }</span>
}
} else {
html! {}
}
}
</div>
<div class="message-content">
{
if let Some(title) = &msg.title {
html! { <div class="message-title">{ title }</div> }
} else {
html! {}
}
}
{
if let Some(description) = &msg.description {
html! { <div class="message-description">{ description }</div> }
} else {
html! {}
}
}
{ render_message_content(&msg.content, &msg.format) }
</div>
</div>
}
}
fn render_message_content(content: &str, format: &str) -> Html {
match format {
"rhai" => render_code_with_line_numbers(content, "rhai"),
"error" => html! {
<div class="message-text error-content">
<div class="error-icon">{"⚠️"}</div>
<div class="error-text">{ content }</div>
</div>
},
_ => html! {
<div class="message-text">{ content }</div>
}
}
}
fn render_code_with_line_numbers(content: &str, language: &str) -> Html {
let lines: Vec<&str> = content.lines().collect();
html! {
<div class={format!("code-block language-{}", language)}>
<div class="code-header">
<span class="language-label">{ language.to_uppercase() }</span>
</div>
<div class="code-content">
<div class="line-numbers">
{ for (1..=lines.len()).map(|i| html! {
<div class="line-number">{ i }</div>
}) }
</div>
<div class="code-lines">
{ for lines.iter().map(|line| html! {
<div class="code-line">{ line }</div>
}) }
</div>
</div>
</div>
}
}
fn format_timestamp(timestamp_str: &str) -> String {
match DateTime::parse_from_rfc3339(timestamp_str) {
Ok(dt) => dt.with_timezone(&Utc).format("%H:%M").to_string(),
Err(_) => timestamp_str.to_string(),
}
}
#[derive(Properties, PartialEq)]
pub struct ConversationListProps {
pub conversations: Vec<ConversationSummary>,
pub active_conversation_id: Option<u32>,
pub on_select_conversation: Callback<u32>,
pub on_new_conversation: Callback<()>,
pub title: String,
}
#[function_component(ConversationList)]
pub fn conversation_list(props: &ConversationListProps) -> Html {
let on_new = {
let on_new_conversation = props.on_new_conversation.clone();
Callback::from(move |_| {
on_new_conversation.emit(());
})
};
html! {
<div class="card">
<h3>{ &props.title }</h3>
<button onclick={on_new} class="new-conversation-btn">{ "+ New Chat" }</button>
<ul>
{ for props.conversations.iter().map(|conv| {
let conv_id = conv.id;
let is_active = props.active_conversation_id == Some(conv_id);
let class_name = if is_active { "active-conversation-item" } else { "conversation-item" };
let on_select = {
let on_select_conversation = props.on_select_conversation.clone();
Callback::from(move |_| {
on_select_conversation.emit(conv_id);
})
};
html! {
<li class={class_name} onclick={on_select} key={conv_id.to_string()}>
<div class="conversation-title">{ &conv.title }</div>
{
if let Some(preview) = &conv.last_message_preview {
html! { <div class="conversation-preview">{ preview }</div> }
} else {
html! {}
}
}
</li>
}
}) }
</ul>
</div>
}
}

View File

@ -0,0 +1,456 @@
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 crate::ws_manager::fetch_data_from_ws_url;
#[derive(Clone, Debug, PartialEq)]
struct RotationState {
value: i32,
}
enum RotationAction {
Rotate(i32),
}
impl Reducible for RotationState {
type Action = RotationAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let next_value = match action {
RotationAction::Rotate(change) => self.value + change,
};
RotationState { value: next_value }.into()
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct CirclesViewProps {
pub default_center_ws_url: String, // The starting center circle WebSocket URL
pub on_context_update: Callback<Vec<String>>, // Single callback for context updates
}
#[derive(Clone, Debug)]
pub enum CirclesViewMsg {
CenterCircleFetched(Circle),
SurroundingCircleFetched(String, Result<Circle, String>), // ws_url, result
CircleClicked(String),
BackgroundClicked,
RotateCircles(i32), // rotation delta
}
pub struct CirclesView {
// Two primary dynamic states
center_circle: String,
is_selected: bool,
// Supporting state
circles: HashMap<String, Circle>,
navigation_stack: Vec<String>,
loading_states: HashMap<String, bool>,
// Rotation state for surrounding circles
rotation_value: i32,
}
impl Component for CirclesView {
type Message = CirclesViewMsg;
type Properties = CirclesViewProps;
fn create(ctx: &Context<Self>) -> 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);
let mut component = Self {
center_circle: center_ws_url.clone(),
is_selected: false,
circles: HashMap::new(),
navigation_stack: vec![center_ws_url.clone()],
loading_states: HashMap::new(),
rotation_value: 0,
};
// Fetch center circle immediately
component.fetch_center_circle(ctx, &center_ws_url);
component
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
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());
// 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);
}
Err(error) => {
log::error!("CirclesView: Failed to fetch circle {}: {}", ws_url, error);
// 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::RotateCircles(delta) => {
self.rotation_value += delta;
log::debug!("CirclesView: Rotation updated to: {}", self.rotation_value);
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
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()
.filter_map(|ws_url| self.circles.get(ws_url))
.collect()
} else {
Vec::new()
}
};
let link = ctx.link();
let on_background_click_handler = link.callback(|_: MouseEvent| CirclesViewMsg::BackgroundClicked);
// Add wheel event handler for rotation
let on_wheel_handler = {
let link = link.clone();
Callback::from(move |e: WheelEvent| {
e.prevent_default();
let delta = if e.delta_y() > 0.0 { 10 } else { -10 };
link.send_message(CirclesViewMsg::RotateCircles(delta));
})
};
let petals_html: Vec<Html> = 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! {
<div class="circles-view"
onclick={on_background_click_handler}
onwheel={on_wheel_handler}>
<div class="flower-container">
{if let Some(center_data) = center_circle_data {
self.render_circle_element(
center_data,
true, // is_center
None, // position_index
link,
)
} else {
html! { <p>{ "Loading center circle..." }</p> }
}}
{ for petals_html }
</div>
</div>
}
}
}
impl CirclesView {
/// Fetch center circle data
fn fetch_center_circle(&mut self, ctx: &Context<Self>, 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::<Circle>(&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);
// Could emit an error message here if needed
}
}
});
}
/// Start progressive fetching of surrounding circles
fn start_surrounding_circles_fetch(&mut self, ctx: &Context<Self>, center_circle: &Circle) {
log::info!("CirclesView: Starting progressive fetch of {} surrounding circles", center_circle.circles.len());
for surrounding_ws_url in &center_circle.circles {
self.fetch_surrounding_circle(ctx, surrounding_ws_url);
}
}
/// Fetch individual surrounding circle
fn fetch_surrounding_circle(&mut self, ctx: &Context<Self>, 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::<Circle>(&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<Self>) {
let context_urls = if self.is_selected {
// When selected, context is only the center circle
vec![self.center_circle.clone()]
} 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 &center_circle.circles {
if self.circles.contains_key(surrounding_url) {
urls.push(surrounding_url.clone());
}
}
}
urls
};
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<Self>, 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);
} 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);
} else {
// If already loaded, start fetching its surrounding circles
if let Some(circle) = self.circles.get(&ws_url).cloned() {
self.start_surrounding_circles_fetch(ctx, &circle);
}
}
}
// Update context
self.update_circles_context(ctx);
true
}
/// Handle background click logic
fn handle_background_click(&mut self, ctx: &Context<Self>) -> bool {
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;
log::info!("CirclesView: Background click - unselecting center circle");
} 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);
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);
} else {
// If already loaded, start fetching its surrounding circles
if let Some(circle) = self.circles.get(&previous_center).cloned() {
self.start_surrounding_circles_fetch(ctx, &circle);
}
}
} else {
log::debug!("CirclesView: Background click - no previous circle in stack");
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);
} else {
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<String> {
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);
previous
} else {
log::debug!("CirclesView: Cannot navigate back - stack size: {}, stack: {:?}", self.navigation_stack.len(), self.navigation_stack);
None
}
}
fn render_circle_element(
&self,
circle: &Circle,
is_center: bool,
position_index: Option<usize>,
link: &yew::html::Scope<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| {
e.stop_propagation();
CirclesViewMsg::CircleClicked(ws_url_clone.clone())
})
};
let mut class_name_parts: Vec<String> = vec!["circle".to_string()];
if is_center {
class_name_parts.push("center-circle".to_string());
if show_description {
class_name_parts.push("sole-selected".to_string());
}
} else {
class_name_parts.push("outer-circle-layout".to_string());
if let Some(idx) = position_index {
if idx < 6 {
class_name_parts.push(format!("circle-position-{}", idx + 1));
}
}
}
let class_name = class_name_parts.join(" ");
let size = if is_center {
if show_description {
"400px" // Center circle, selected (description shown)
} else {
"300px" // Center circle, unselected (name only)
}
} else {
"300px" // Surrounding (petal) circles
};
html! {
<div class={class_name}
style={format!("width: {}; height: {};", size, size)}
onclick={on_click_handler}
>
{
if show_description {
html! {
<div class="circle-text-container">
<span class="circle-title">{ &circle.title }</span>
<span class="circle-description">{ &circle.description.as_deref().unwrap_or("") }</span>
</div>
}
} else {
html! { <span class="circle-text">{ &circle.title }</span> }
}
}
</div>
}
}
}

View File

@ -0,0 +1,311 @@
use std::rc::Rc;
use std::collections::HashMap;
use yew::prelude::*;
use heromodels::models::circle::Circle;
use web_sys::InputEvent;
// 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<String>), // List of color hex values
PatternSelection(Vec<String>), // List of pattern names/classes
LogoSelection(Vec<String>), // List of predefined logo symbols or image URLs
Toggle,
TextInput, // For URL input or custom text
}
// --- Data Structure for Defining a Theme Setting ---
#[derive(Clone, PartialEq, Debug)]
pub struct ThemeSettingDefinition {
pub key: String, // Corresponds to the key in CircleData.theme HashMap
pub label: String,
pub description: String,
pub control_type: ThemeSettingControlType,
pub default_value: String, // Used if not present in circle's theme
}
// --- Props for the Component ---
#[derive(Clone, PartialEq, Properties)]
pub struct CustomizeViewProps {
pub all_circles: Rc<HashMap<String, Circle>>,
// Assuming context_circle_ws_urls provides the WebSocket URL of the circle being customized.
// For simplicity, we'll use the first URL if multiple are present.
// A more robust solution might involve a dedicated `active_customization_circle_ws_url: Option<String>` prop.
pub context_circle_ws_urls: Option<Rc<Vec<String>>>,
pub app_callback: Callback<AppMsg>, // For emitting update messages
}
// --- Statically Defined Theme Settings ---
fn get_theme_setting_definitions() -> Vec<ThemeSettingDefinition> {
vec![
ThemeSettingDefinition {
key: "theme_primary_color".to_string(),
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(),
]),
default_value: "#3b82f6".to_string(),
},
ThemeSettingDefinition {
key: "theme_background_color".to_string(),
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(),
]),
default_value: "#0a0a0a".to_string(),
},
ThemeSettingDefinition {
key: "background_pattern".to_string(),
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(),
]),
default_value: "none".to_string(),
},
ThemeSettingDefinition {
key: "circle_logo".to_string(), // Could be a symbol or a key for an image URL
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(),
"custom_url".to_string(), // Represents using the URL input
]),
default_value: "".to_string(),
},
ThemeSettingDefinition {
key: "circle_logo_url".to_string(),
label: "Custom Logo URL".to_string(),
description: "URL for a custom logo image (PNG, SVG recommended).".to_string(),
control_type: ThemeSettingControlType::TextInput,
default_value: "".to_string(),
},
ThemeSettingDefinition {
key: "nav_dashboard_visible".to_string(),
label: "Show Dashboard in Nav".to_string(),
description: "".to_string(),
control_type: ThemeSettingControlType::Toggle,
default_value: "true".to_string(),
},
ThemeSettingDefinition {
key: "nav_timeline_visible".to_string(),
label: "Show Timeline in Nav".to_string(),
description: "".to_string(),
control_type: ThemeSettingControlType::Toggle,
default_value: "true".to_string(),
},
// Add more settings as needed, e.g., font selection, border radius, etc.
]
}
#[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<String> = props.context_circle_ws_urls.as_ref()
.and_then(|ws_urls| ws_urls.first().cloned());
let active_circle_theme: Option<HashMap<String, String>> = 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());
.map(|_circle_data| HashMap::new()); // Placeholder, provides an empty theme
let on_setting_update_emitter = props.app_callback.clone();
html! {
<div class="view-container customize-view">
<div class="view-header">
<h1 class="view-title">{"Customize Appearance"}</h1>
{ if active_circle_ws_url.is_none() {
html!{ <p class="customize-no-circle-msg">{"Select a circle context to customize its appearance."}</p> }
} else { html!{} }}
</div>
{ if let Some(current_circle_ws_url) = active_circle_ws_url {
html! {
<div class="customize-content">
{ for theme_definitions.iter().map(|setting_def| {
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,
current_circle_ws_url.clone(),
on_setting_update_emitter.clone()
)
})}
</div>
}
} else {
html!{} // Or a message indicating no circle is selected for customization
}}
</div>
}
}
fn render_setting_control(
setting_def: ThemeSettingDefinition,
current_value: String,
circle_ws_url: String,
app_callback: Callback<AppMsg>,
) -> 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();
Callback::from(move |new_value: String| {
// Emit a message to app.rs to update the theme
// AppMsg should have a variant like UpdateCircleTheme(circle_id, theme_key, new_value)
// TODO: Update this to use WebSocket URL instead of u32 ID
// For now, we'll need to convert or update the message type
// app_callback.emit(AppMsg::UpdateCircleThemeValue(
// circle_ws_url_clone.clone(),
// setting_key_clone.clone(),
// new_value,
// ));
})
};
let control_html = match setting_def.control_type {
ThemeSettingControlType::ColorSelection(ref colors) => {
let on_select = on_value_change.clone();
html! {
<div class="color-grid">
{ for colors.iter().map(|color_option| {
let is_selected = *color_option == current_value;
let option_value = color_option.clone();
let on_click_handler = {
let on_select = on_select.clone();
Callback::from(move |_| on_select.emit(option_value.clone()))
};
html! {
<div
class={classes!("color-option", is_selected.then_some("selected"))}
style={format!("background-color: {};", color_option)}
onclick={on_click_handler}
title={color_option.clone()}
/>
}
})}
</div>
}
},
ThemeSettingControlType::PatternSelection(ref patterns) => {
let on_select = on_value_change.clone();
html! {
<div class="pattern-grid">
{ for patterns.iter().map(|pattern_option| {
let is_selected = *pattern_option == current_value;
let option_value = pattern_option.clone();
let pattern_class = format!("pattern-preview-{}", pattern_option.replace(" ", "-").to_lowercase());
let on_click_handler = {
let on_select = on_select.clone();
Callback::from(move |_| on_select.emit(option_value.clone()))
};
html! {
<div
class={classes!("pattern-option", pattern_class, is_selected.then_some("selected"))}
onclick={on_click_handler}
title={pattern_option.clone()}
/>
}
})}
</div>
}
},
ThemeSettingControlType::LogoSelection(ref logos) => {
let on_select = on_value_change.clone();
html! {
<div class="logo-grid">
{ for logos.iter().map(|logo_option| {
let is_selected = *logo_option == current_value;
let option_value = logo_option.clone();
let on_click_handler = {
let on_select = on_select.clone();
Callback::from(move |_| on_select.emit(option_value.clone()))
};
html! {
<div
class={classes!("logo-option", is_selected.then_some("selected"))}
onclick={on_click_handler}
title={logo_option.clone()}
>
{ if logo_option == "custom_url" { "URL" } else { logo_option } }
</div>
}
})}
</div>
}
},
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() });
})
};
html! {
<label class="setting-toggle-switch">
<input type="checkbox" checked={checked} onchange={on_toggle} />
<span class="setting-toggle-slider"></span>
</label>
}
},
ThemeSettingControlType::TextInput => {
let on_input = {
let on_value_change = on_value_change.clone();
Callback::from(move |e: InputEvent| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
on_value_change.emit(input.value());
})
};
html! {
<input
type="text"
class="setting-text-input input-base"
placeholder={setting_def.description.clone()}
value={current_value.clone()}
oninput={on_input}
/>
}
},
};
html! {
<div class="setting-item card-base">
<div class="setting-info">
<label class="setting-label">{ &setting_def.label }</label>
{ if !setting_def.description.is_empty() && setting_def.control_type != ThemeSettingControlType::TextInput { // Placeholder is used for TextInput desc
html!{ <p class="setting-description">{ &setting_def.description }</p> }
} else { html!{} }}
</div>
<div class="setting-control">
{ control_html }
</div>
</div>
}
}

View File

@ -0,0 +1,48 @@
use yew::prelude::*;
use heromodels::models::library::items::Image;
#[derive(Clone, PartialEq, Properties)]
pub struct ImageViewerProps {
pub image: Image,
pub on_back: Callback<()>,
}
pub struct ImageViewer;
impl Component for ImageViewer {
type Message = ();
type Properties = ImageViewerProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let back_handler = {
let on_back = props.on_back.clone();
Callback::from(move |_: MouseEvent| {
on_back.emit(());
})
};
html! {
<div class="asset-viewer image-viewer">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Collection"}
</button>
<div class="viewer-header">
<h2 class="viewer-title">{ &props.image.title }</h2>
</div>
<div class="viewer-content">
<img
src={props.image.url.clone()}
alt={props.image.title.clone()}
class="viewer-image"
/>
</div>
</div>
}
}
}

View File

@ -0,0 +1,90 @@
use crate::auth::AuthManager;
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorAuthTabProps {
pub circle_ws_addresses: Rc<Vec<String>>,
pub auth_manager: AuthManager,
}
pub enum Msg {
RunAuth(String),
SetLog(String, String),
}
pub struct InspectorAuthTab {
logs: HashMap<String, String>,
}
impl Component for InspectorAuthTab {
type Message = Msg;
type Properties = InspectorAuthTabProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
logs: HashMap::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::RunAuth(ws_url) => {
let auth_manager = ctx.props().auth_manager.clone();
let link = ctx.link().clone();
let url = ws_url.clone();
self.logs
.insert(url.clone(), "Authenticating...".to_string());
spawn_local(async move {
let result_log = match auth_manager.create_authenticated_client(&url).await {
Ok(_) => format!("Successfully authenticated with {}", url),
Err(e) => format!("Failed to authenticate with {}: {}", url, e),
};
link.send_message(Msg::SetLog(url, result_log));
});
true
}
Msg::SetLog(url, log_msg) => {
self.logs.insert(url, log_msg);
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div class="inspector-auth-tab">
<div class="log-list">
{ for ctx.props().circle_ws_addresses.iter().map(|ws_url| self.render_log_entry(ctx, ws_url)) }
</div>
</div>
}
}
}
impl InspectorAuthTab {
fn render_log_entry(&self, ctx: &Context<Self>, ws_url: &String) -> Html {
let url = ws_url.clone();
let onclick = ctx.link().callback(move |_| Msg::RunAuth(url.clone()));
let log_output = self
.logs
.get(ws_url)
.cloned()
.unwrap_or_else(|| "Ready".to_string());
html! {
<div class="log-entry">
<div class="url-info">
<span class="url">{ ws_url.clone() }</span>
<button class="button is-small" {onclick}>{"Authenticate"}</button>
</div>
<pre class="log-output">{ log_output }</pre>
</div>
}
}
}

View File

@ -0,0 +1,141 @@
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::rhai_executor::execute_rhai_script_remote;
use crate::ws_manager::fetch_data_from_ws_url;
use heromodels::models::circle::Circle;
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorInteractTabProps {
pub circle_ws_addresses: Rc<Vec<String>>,
pub conversations: Vec<ConversationSummary>,
pub active_conversation_id: Option<u32>,
pub external_conversation_selection: Option<u32>,
pub external_new_conversation_trigger: bool,
pub on_conversations_updated: Callback<Vec<ConversationSummary>>,
pub on_conversation_selected: Callback<u32>,
pub on_new_conversation: Callback<()>,
}
#[derive(Clone, Debug)]
pub struct CircleInfo {
pub name: String,
pub ws_url: String,
}
#[function_component(InspectorInteractTab)]
pub fn inspector_interact_tab(props: &InspectorInteractTabProps) -> Html {
let circle_names = use_state(|| HashMap::<String, String>::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::<Circle>(&ws_url_clone, "get_circle().json()").await {
Ok(circle) => {
let mut names = (*circle_names_clone).clone();
names.insert(ws_url_clone, circle.title);
circle_names_clone.set(names);
}
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));
circle_names_clone.set(names);
}
}
});
}
|| {}
});
}
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<u8>, String, Callback<ChatResponse>)| {
// 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! {
<ChatInterface
on_process_message={on_process_message}
placeholder={"Enter your Rhai script here...".to_string()}
show_title_description={false}
conversation_title={Some("Script Interaction".to_string())}
input_type={Some(InputType::Code)}
input_format={Some("rhai".to_string())}
on_conversations_updated={Some(props.on_conversations_updated.clone())}
active_conversation_id={props.active_conversation_id}
on_conversation_selected={Some(props.on_conversation_selected.clone())}
external_conversation_selection={props.external_conversation_selection}
external_new_conversation_trigger={Some(props.external_new_conversation_trigger)}
/>
}
}
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorInteractSidebarProps {
pub conversations: Vec<ConversationSummary>,
pub active_conversation_id: Option<u32>,
pub on_select_conversation: Callback<u32>,
pub on_new_conversation: Callback<()>,
}
#[function_component(InspectorInteractSidebar)]
pub fn inspector_interact_sidebar(props: &InspectorInteractSidebarProps) -> Html {
html! {
<ConversationList
conversations={props.conversations.clone()}
active_conversation_id={props.active_conversation_id}
on_select_conversation={props.on_select_conversation.clone()}
on_new_conversation={props.on_new_conversation.clone()}
title={"Chat History".to_string()}
/>
}
}

View File

@ -0,0 +1,85 @@
use yew::prelude::*;
use std::rc::Rc;
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorLogsTabProps {
pub circle_ws_addresses: Rc<Vec<String>>,
}
#[derive(Clone, Debug)]
pub struct LogEntry {
pub timestamp: String,
pub level: String,
pub source: String,
pub message: String,
}
#[function_component(InspectorLogsTab)]
pub fn inspector_logs_tab(props: &InspectorLogsTabProps) -> Html {
let logs = use_state(|| {
vec![
LogEntry {
timestamp: "17:05:24".to_string(),
level: "INFO".to_string(),
source: "inspector".to_string(),
message: "Inspector initialized".to_string(),
},
LogEntry {
timestamp: "17:05:25".to_string(),
level: "INFO".to_string(),
source: "network".to_string(),
message: format!("Monitoring {} circle connections", props.circle_ws_addresses.len()),
},
LogEntry {
timestamp: "17:05:26".to_string(),
level: "DEBUG".to_string(),
source: "websocket".to_string(),
message: "Connection status checks initiated".to_string(),
},
]
});
let error_count = logs.iter().filter(|l| l.level == "ERROR").count();
let warn_count = logs.iter().filter(|l| l.level == "WARN").count();
html! {
<div class="content-panel">
<div class="logs-overview">
<div class="logs-stats">
<div class="logs-stat">
<span class="stat-label">{"Total"}</span>
<span class="stat-value">{logs.len()}</span>
</div>
<div class="logs-stat">
<span class="stat-label">{"Errors"}</span>
<span class={classes!("stat-value", if error_count > 0 { "stat-error" } else { "" })}>{error_count}</span>
</div>
<div class="logs-stat">
<span class="stat-label">{"Warnings"}</span>
<span class={classes!("stat-value", if warn_count > 0 { "stat-warn" } else { "" })}>{warn_count}</span>
</div>
</div>
<div class="logs-container">
{ for logs.iter().rev().map(|log| {
let level_class = match log.level.as_str() {
"ERROR" => "log-error",
"WARN" => "log-warn",
"INFO" => "log-info",
_ => "log-debug",
};
html! {
<div class={classes!("log-entry", level_class)}>
<span class="log-time">{&log.timestamp}</span>
<span class={classes!("log-level", level_class)}>{&log.level}</span>
<span class="log-source">{&log.source}</span>
<span class="log-message">{&log.message}</span>
</div>
}
})}
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,136 @@
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 common_models::CircleData;
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorNetworkTabProps {
pub circle_ws_addresses: Rc<Vec<String>>,
}
#[derive(Clone, Debug)]
pub struct TrafficEntry {
pub timestamp: String,
pub direction: String, // "Sent" or "Received"
pub ws_url: String,
pub message_type: String,
pub size: String,
pub status: String,
}
#[function_component(InspectorNetworkTab)]
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,
});
}
Rc::new(circles)
});
// Mock traffic data
let traffic_entries = use_state(|| {
vec![
TrafficEntry {
timestamp: "22:28:15".to_string(),
direction: "Sent".to_string(),
ws_url: "ws://localhost:9000".to_string(),
message_type: "get_circle()".to_string(),
size: "245 B".to_string(),
status: "Success".to_string(),
},
TrafficEntry {
timestamp: "22:28:14".to_string(),
direction: "Received".to_string(),
ws_url: "ws://localhost:9001".to_string(),
message_type: "circle_data".to_string(),
size: "1.2 KB".to_string(),
status: "Success".to_string(),
},
TrafficEntry {
timestamp: "22:28:13".to_string(),
direction: "Sent".to_string(),
ws_url: "ws://localhost:9002".to_string(),
message_type: "ping".to_string(),
size: "64 B".to_string(),
status: "Success".to_string(),
},
TrafficEntry {
timestamp: "22:28:12".to_string(),
direction: "Received".to_string(),
ws_url: "ws://localhost:9003".to_string(),
message_type: "pong".to_string(),
size: "64 B".to_string(),
status: "Success".to_string(),
},
TrafficEntry {
timestamp: "22:28:11".to_string(),
direction: "Sent".to_string(),
ws_url: "ws://localhost:9004".to_string(),
message_type: "execute_script".to_string(),
size: "512 B".to_string(),
status: "Success".to_string(),
},
]
});
html! {
<div class="column">
<div class="network-map-container">
{ render_world_map_svg() }
<NetworkAnimationView all_circles={(*circles_data).clone()} />
</div>
<div class="network-traffic">
<div class="traffic-table">
<div class="traffic-header">
<div class="traffic-col">{"Time"}</div>
<div class="traffic-col">{"Direction"}</div>
<div class="traffic-col">{"WebSocket"}</div>
<div class="traffic-col">{"Message"}</div>
<div class="traffic-col">{"Size"}</div>
<div class="traffic-col">{"Status"}</div>
</div>
{ 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! {
<div class="traffic-row">
<div class="traffic-col traffic-time">{&entry.timestamp}</div>
<div class={classes!("traffic-col", "traffic-direction", direction_class)}>{&entry.direction}</div>
<div class="traffic-col traffic-url">{&entry.ws_url}</div>
<div class="traffic-col traffic-message">{&entry.message_type}</div>
<div class="traffic-col traffic-size">{&entry.size}</div>
<div class={classes!("traffic-col", "traffic-status", status_class)}>{&entry.status}</div>
</div>
}
})}
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,338 @@
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::inspector_auth_tab::InspectorAuthTab;
use crate::components::inspector_interact_tab::{InspectorInteractTab, InspectorInteractSidebar};
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorViewProps {
pub circle_ws_addresses: Rc<Vec<String>>,
pub auth_manager: AuthManager,
}
#[derive(Clone, Debug, PartialEq)]
pub enum InspectorTab {
Network,
Logs,
Interact,
Auth,
}
#[derive(Clone, Debug, PartialEq)]
pub enum InspectorViewState {
Overview,
Tab(InspectorTab),
}
impl InspectorTab {
fn icon(&self) -> &'static str {
match self {
InspectorTab::Network => "fa-network-wired",
InspectorTab::Logs => "fa-list-alt",
InspectorTab::Interact => "fa-terminal",
InspectorTab::Auth => "fa-key",
}
}
fn title(&self) -> &'static str {
match self {
InspectorTab::Network => "Network",
InspectorTab::Logs => "Logs",
InspectorTab::Interact => "Interact",
InspectorTab::Auth => "Auth",
}
}
}
pub struct InspectorView {
current_view: InspectorViewState,
// Chat-related state for interact tab
conversations: Vec<ConversationSummary>,
active_conversation_id: Option<u32>,
external_conversation_selection: Option<u32>,
external_new_conversation_trigger: bool,
}
pub enum Msg {
SelectTab(InspectorTab),
BackToOverview,
// Conversation management messages
SelectConversation(u32),
NewConversation,
ConversationsUpdated(Vec<ConversationSummary>),
}
impl Component for InspectorView {
type Message = Msg;
type Properties = InspectorViewProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
current_view: InspectorViewState::Overview,
conversations: Vec::new(),
active_conversation_id: None,
external_conversation_selection: None,
external_new_conversation_trigger: false,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SelectTab(tab) => {
self.current_view = InspectorViewState::Tab(tab);
true
}
Msg::BackToOverview => {
self.current_view = InspectorViewState::Overview;
true
}
Msg::SelectConversation(conv_id) => {
self.active_conversation_id = Some(conv_id);
self.external_conversation_selection = Some(conv_id);
true
}
Msg::NewConversation => {
self.active_conversation_id = None;
self.external_new_conversation_trigger = !self.external_new_conversation_trigger;
true
}
Msg::ConversationsUpdated(conversations) => {
self.conversations = conversations;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
match &self.current_view {
InspectorViewState::Overview => {
html! {
<SidebarLayout
sidebar_content={self.render_overview_sidebar(ctx)}
main_content={self.render_overview_content(ctx)}
/>
}
}
InspectorViewState::Tab(tab) => {
let on_background_click = ctx.link().callback(|_| Msg::BackToOverview);
let main_content = match tab {
InspectorTab::Network => html! {
<InspectorNetworkTab circle_ws_addresses={ctx.props().circle_ws_addresses.clone()} />
},
InspectorTab::Logs => html! {
<InspectorLogsTab circle_ws_addresses={ctx.props().circle_ws_addresses.clone()} />
},
InspectorTab::Interact => {
let on_conv_select = ctx.link().callback(Msg::SelectConversation);
let on_new_conv = ctx.link().callback(|_| Msg::NewConversation);
let on_conv_update = ctx.link().callback(Msg::ConversationsUpdated);
html! {
<InspectorInteractTab
circle_ws_addresses={ctx.props().circle_ws_addresses.clone()}
conversations={self.conversations.clone()}
active_conversation_id={self.active_conversation_id}
external_conversation_selection={self.external_conversation_selection}
external_new_conversation_trigger={self.external_new_conversation_trigger}
on_conversations_updated={on_conv_update}
on_conversation_selected={on_conv_select}
on_new_conversation={on_new_conv}
/>
}
},
InspectorTab::Auth => html! {
<InspectorAuthTab
circle_ws_addresses={ctx.props().circle_ws_addresses.clone()}
auth_manager={ctx.props().auth_manager.clone()}
/>
},
};
html! {
<SidebarLayout
sidebar_content={self.render_tab_sidebar(ctx)}
main_content={main_content}
on_background_click={Some(on_background_click)}
/>
}
}
}
}
}
impl InspectorView {
fn render_overview_sidebar(&self, ctx: &Context<Self>) -> Html {
let tabs = vec![
InspectorTab::Network,
InspectorTab::Logs,
InspectorTab::Interact,
InspectorTab::Auth,
];
html! {
<div class="cards-column">
{ for tabs.iter().map(|tab| self.render_tab_card(ctx, tab)) }
</div>
}
}
fn render_tab_sidebar(&self, ctx: &Context<Self>) -> Html {
if let InspectorViewState::Tab(current_tab) = &self.current_view {
html! {
<div class="sidebar">
<div class="cards-column">
{ self.render_tab_card(ctx, current_tab) }
</div>
{ match current_tab {
InspectorTab::Network => {
self.render_network_connections_sidebar(ctx)
},
InspectorTab::Interact => {
let on_select_conversation = ctx.link().callback(|conv_id: u32| {
Msg::SelectConversation(conv_id)
});
let on_new_conversation = ctx.link().callback(|_| {
Msg::NewConversation
});
html! {
<InspectorInteractSidebar
conversations={self.conversations.clone()}
active_conversation_id={self.active_conversation_id}
on_select_conversation={on_select_conversation}
on_new_conversation={on_new_conversation}
/>
}
},
_ => html! {}
}}
</div>
}
} else {
html! {}
}
}
fn render_tab_card(&self, ctx: &Context<Self>, tab: &InspectorTab) -> Html {
let is_selected = match &self.current_view {
InspectorViewState::Tab(current_tab) => current_tab == tab,
_ => false,
};
let tab_clone = tab.clone();
let onclick = ctx.link().callback(move |_| Msg::SelectTab(tab_clone.clone()));
let card_class = if is_selected {
"card selected"
} else {
"card"
};
html! {
<div class={card_class} onclick={onclick}>
<header>
<i class={classes!("fas", tab.icon())}></i>
<span class="tab-title">{tab.title()}</span>
</header>
{ if is_selected { self.render_tab_details(ctx, tab) } else { html! {} } }
</div>
}
}
fn render_tab_details(&self, ctx: &Context<Self>, tab: &InspectorTab) -> Html {
let props = ctx.props();
match tab {
InspectorTab::Network => html! {
<div class="tab-details">
<div class="detail-item">
<span class="detail-label">{"Circles:"}</span>
<span class="detail-value">{props.circle_ws_addresses.len()}</span>
</div>
</div>
},
InspectorTab::Logs => html! {
<div class="tab-details">
<div class="detail-item">
<span class="detail-label">{"Monitoring:"}</span>
<span class="detail-value">{props.circle_ws_addresses.len()}</span>
</div>
</div>
},
InspectorTab::Interact => html! {
<div class="tab-details">
<div class="detail-item">
<span class="detail-label">{"Mode:"}</span>
<span class="detail-value">{"Rhai Script"}</span>
</div>
<div class="detail-item">
<span class="detail-label">{"Targets:"}</span>
<span class="detail-value">{props.circle_ws_addresses.len()}</span>
</div>
</div>
},
InspectorTab::Auth => html! {
<div class="tab-details">
<div class="detail-item">
<span class="detail-label">{"Action:"}</span>
<span class="detail-value">{"Authenticate"}</span>
</div>
<div class="detail-item">
<span class="detail-label">{"Targets:"}</span>
<span class="detail-value">{props.circle_ws_addresses.len()}</span>
</div>
</div>
},
}
}
fn render_network_connections_sidebar(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let connected_count = props.circle_ws_addresses.len();
html! {
<div class="ws-status">
<div class="ws-status-header">
<span class="ws-status-title">{"Connections"}</span>
<span class="ws-status-count">{format!("{}/{}", connected_count, connected_count)}</span>
</div>
<div class="ws-connections">
{ for props.circle_ws_addresses.iter().enumerate().map(|(index, ws_url)| {
html! {
<div class="ws-connection">
<div class="ws-status-dot ws-status-connected"></div>
<div class="ws-connection-info">
<div class="ws-connection-name">{format!("Circle {}", index + 1)}</div>
<div class="ws-connection-url">{ws_url}</div>
</div>
</div>
}
})}
</div>
</div>
}
}
fn render_overview_content(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
html! {
<div class="content-panel">
<div class="overview-grid">
<div class="overview-card">
<h3>{"Circle Connections"}</h3>
<div class="metric-value">{props.circle_ws_addresses.len()}</div>
<div class="metric-label">{"Active Circles"}</div>
</div>
<div class="overview-card">
<h3>{"Inspector Tools"}</h3>
<div class="metric-value">{"3"}</div>
<div class="metric-label">{"Available tabs"}</div>
</div>
</div>
</div>
}
}
}

View File

@ -0,0 +1,115 @@
use yew::prelude::*;
use std::rc::Rc;
use std::collections::HashMap;
use wasm_bindgen_futures::spawn_local;
use crate::ws_manager::fetch_data_from_ws_url;
use heromodels::models::circle::Circle;
#[derive(Clone, PartialEq, Properties)]
pub struct InspectorWebSocketStatusProps {
pub circle_ws_addresses: Rc<Vec<String>>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ConnectionStatus {
Connecting,
Connected,
Error,
}
impl ConnectionStatus {
fn to_class(&self) -> &'static str {
match self {
ConnectionStatus::Connecting => "status-connecting",
ConnectionStatus::Connected => "status-connected",
ConnectionStatus::Error => "status-error",
}
}
}
#[derive(Clone, Debug)]
pub struct CircleConnectionInfo {
pub name: String,
pub ws_url: String,
pub status: ConnectionStatus,
}
#[function_component(InspectorWebSocketStatus)]
pub fn inspector_websocket_status(props: &InspectorWebSocketStatusProps) -> Html {
let connections = use_state(|| Vec::<CircleConnectionInfo>::new());
// Initialize and check connection status for each WebSocket
{
let connections = connections.clone();
let ws_addresses = props.circle_ws_addresses.clone();
use_effect_with(ws_addresses.clone(), move |addresses| {
// Initialize all connections as connecting
let initial_connections: Vec<CircleConnectionInfo> = addresses.iter().map(|ws_url| {
CircleConnectionInfo {
name: format!("Circle ({})", ws_url.split('/').last().unwrap_or(ws_url)),
ws_url: ws_url.clone(),
status: ConnectionStatus::Connecting,
}
}).collect();
connections.set(initial_connections);
// Check each connection
for (index, ws_url) in addresses.iter().enumerate() {
let ws_url_clone = ws_url.clone();
let connections_clone = connections.clone();
spawn_local(async move {
match fetch_data_from_ws_url::<Circle>(&ws_url_clone, "get_circle().json()").await {
Ok(circle) => {
connections_clone.set({
let mut conns = (*connections_clone).clone();
if let Some(conn) = conns.get_mut(index) {
conn.name = circle.title;
conn.status = ConnectionStatus::Connected;
}
conns
});
}
Err(_) => {
connections_clone.set({
let mut conns = (*connections_clone).clone();
if let Some(conn) = conns.get_mut(index) {
conn.status = ConnectionStatus::Error;
}
conns
});
}
}
});
}
|| {}
});
}
let connected_count = connections.iter().filter(|c| c.status == ConnectionStatus::Connected).count();
html! {
<div class="ws-status">
<div class="ws-status-header">
<span class="ws-status-title">{"Connections"}</span>
<span class="ws-status-count">{format!("{}/{}", connected_count, connections.len())}</span>
</div>
<div class="ws-connections">
{ for connections.iter().map(|conn| {
html! {
<div class="ws-connection">
<div class={classes!("ws-status-dot", conn.status.to_class())}></div>
<div class="ws-connection-info">
<div class="ws-connection-name">{&conn.name}</div>
<div class="ws-connection-url">{&conn.ws_url}</div>
</div>
</div>
}
})}
</div>
</div>
}
}

View File

@ -0,0 +1,294 @@
use yew::prelude::*;
use std::rc::Rc;
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use wasm_bindgen_futures::spawn_local;
// Imports from common_models
use common_models::{AiMessageRole, AiConversation};
use heromodels::models::circle::Circle;
use crate::ws_manager::CircleWsManager;
#[derive(Properties, PartialEq, Clone)]
pub struct IntelligenceViewProps {
pub all_circles: Rc<HashMap<String, Circle>>,
pub context_circle_ws_urls: Option<Rc<Vec<String>>>,
}
#[derive(Clone, Debug)]
pub enum IntelligenceMsg {
UpdateInput(String),
SubmitPrompt,
LoadConversation(u32),
StartNewConversation,
CircleDataUpdated(String, Circle), // ws_url, circle_data
CircleDataFetchFailed(String, String), // ws_url, error
ScriptExecuted(Result<String, String>),
}
pub struct IntelligenceView {
current_input: String,
active_conversation_id: Option<u32>,
ws_manager: CircleWsManager,
loading_states: HashMap<String, bool>,
error_states: HashMap<String, Option<String>>,
}
// A summary for listing conversations, as AiConversation can be large.
#[derive(Properties, PartialEq, Clone, Debug)]
pub struct AiConversationSummary {
pub id: u32,
pub title: Option<String>,
}
impl From<&AiConversation> for AiConversationSummary {
fn from(conv: &AiConversation) -> Self {
AiConversationSummary {
id: conv.id,
title: conv.title.clone(),
}
}
}
impl Component for IntelligenceView {
type Message = IntelligenceMsg;
type Properties = IntelligenceViewProps;
fn create(ctx: &Context<Self>) -> 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<Circle, String>)| {
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(),
active_conversation_id: None,
ws_manager,
loading_states: HashMap::new(),
error_states: HashMap::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
IntelligenceMsg::UpdateInput(input) => {
self.current_input = input;
false // No re-render needed for input updates
}
IntelligenceMsg::SubmitPrompt => {
self.submit_intelligence_prompt(ctx);
true
}
IntelligenceMsg::LoadConversation(conv_id) => {
log::info!("Loading conversation with ID: {}", conv_id);
self.active_conversation_id = Some(conv_id);
true
}
IntelligenceMsg::StartNewConversation => {
log::info!("Starting new conversation");
self.active_conversation_id = None;
self.current_input.clear();
true
}
IntelligenceMsg::CircleDataUpdated(ws_url, _circle) => {
log::info!("Circle data updated for: {}", ws_url);
// Handle real-time updates to circle data
true
}
IntelligenceMsg::CircleDataFetchFailed(ws_url, error) => {
log::error!("Failed to fetch circle data for {}: {}", ws_url, error);
true
}
IntelligenceMsg::ScriptExecuted(result) => {
match result {
Ok(output) => {
log::info!("Script executed successfully: {}", output);
}
Err(e) => {
log::error!("Script execution failed: {}", e);
}
}
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// Get aggregated conversations from context circles
let (active_conversation, conversation_history) = self.get_conversation_data(ctx);
let on_input = link.callback(|e: InputEvent| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
IntelligenceMsg::UpdateInput(input.value())
});
let on_submit = link.callback(|e: SubmitEvent| {
e.prevent_default();
IntelligenceMsg::SubmitPrompt
});
let on_new_conversation = link.callback(|_| IntelligenceMsg::StartNewConversation);
html! {
<div class="view-container sidebar-layout">
<div class="card">
<h3>{"Conversations"}</h3>
<button onclick={on_new_conversation} class="new-conversation-btn">{ "+ New Chat" }</button>
<ul>
{ for conversation_history.iter().map(|conv_summary| {
let conv_id = conv_summary.id;
let conv_title = conv_summary.title.as_deref().unwrap_or("New Conversation");
let is_active = self.active_conversation_id == Some(conv_summary.id);
let class_name = if is_active { "active-conversation-item" } else { "conversation-item" };
let on_load = link.callback(move |_| IntelligenceMsg::LoadConversation(conv_id));
html!{
<li class={class_name} onclick={on_load}>
{ conv_title }
</li>
}
}) }
</ul>
</div>
<div class="chat-panel">
<div class="messages-display">
{
if let Some(active_conv) = &active_conversation {
html! {
<>
<h4>{ active_conv.title.as_deref().unwrap_or("Conversation") }</h4>
{ for active_conv.messages.iter().map(|msg| {
let sender_class = match msg.role {
AiMessageRole::User => "user-message",
AiMessageRole::Assistant => "ai-message",
AiMessageRole::System => "system-message",
};
let sender_name = match msg.role {
AiMessageRole::User => "User",
AiMessageRole::Assistant => "Assistant",
AiMessageRole::System => "System",
};
html!{
<div class={classes!("message", sender_class)}>
<span class="sender">{ sender_name }</span>
<p>{ &msg.content }</p>
<span class="timestamp">{ format_timestamp_string(&msg.timestamp) }</span>
</div>
}
}) }
</>
}
} else {
html!{ <p>{ "Select a conversation or start a new one." }</p> }
}
}
</div>
<form onsubmit={on_submit} class="input-area">
<input
type="text"
class="input-base intelligence-input"
placeholder="Ask anything..."
value={self.current_input.clone()}
oninput={on_input}
/>
<button type="submit" class="button-base button-primary send-button">{ "Send" }</button>
</form>
</div>
</div>
}
}
}
impl IntelligenceView {
fn get_conversation_data(&self, _ctx: &Context<Self>) -> (Option<Rc<AiConversation>>, Vec<AiConversationSummary>) {
// 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.
(None, Vec::new())
}
fn submit_intelligence_prompt(&mut self, ctx: &Context<Self>) {
let user_message_content = self.current_input.trim().to_string();
if user_message_content.is_empty() {
return;
}
self.current_input.clear();
// Get target circle for the prompt
let props = ctx.props();
let target_ws_url = props.context_circle_ws_urls
.as_ref()
.and_then(|urls| urls.first())
.cloned();
if let Some(ws_url) = target_ws_url {
// Execute Rhai script to submit intelligence prompt
let script = format!(
r#"
let conversation_id = {};
let message = "{}";
submit_intelligence_prompt(conversation_id, message);
"#,
self.active_conversation_id.unwrap_or(0),
user_message_content.replace('"', r#"\""#)
);
let link = ctx.link().clone();
if let Some(script_future) = self.ws_manager.execute_script(&ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
link.send_message(IntelligenceMsg::ScriptExecuted(Ok(result.output)));
}
Err(e) => {
link.send_message(IntelligenceMsg::ScriptExecuted(Err(format!("{:?}", e))));
}
}
});
}
}
}
fn fetch_intelligence_data(&mut self, ws_url: &str) {
let script = r#"
let intelligence = get_intelligence();
intelligence
"#.to_string();
if let Some(script_future) = self.ws_manager.execute_script(ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
log::info!("Intelligence data fetched: {}", result.output);
// Parse and handle intelligence data
}
Err(e) => {
log::error!("Failed to fetch intelligence data: {:?}", e);
}
}
});
}
}
}
fn format_timestamp_string(timestamp_str: &str) -> String {
match DateTime::parse_from_rfc3339(timestamp_str) {
Ok(dt) => dt.with_timezone(&Utc).format("%Y-%m-%d %H:%M").to_string(),
Err(_) => timestamp_str.to_string(), // Fallback to raw string if parsing fails
}
}

View File

@ -0,0 +1,446 @@
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,
};
#[derive(Clone, PartialEq, Properties)]
pub struct LibraryViewProps {
pub ws_addresses: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DisplayLibraryItem {
Image(Image),
Pdf(Pdf),
Markdown(Markdown),
Book(Book),
Slides(Slides),
}
#[derive(Clone, Debug)]
pub struct DisplayLibraryCollection {
pub title: String,
pub description: Option<String>,
pub items: Vec<Rc<DisplayLibraryItem>>,
pub ws_url: String,
pub collection_key: String,
}
pub enum Msg {
SelectCollection(usize),
CollectionsFetched(HashMap<String, Collection>),
ItemsFetched(String, Vec<DisplayLibraryItem>), // collection_key, items
ViewItem(DisplayLibraryItem),
BackToLibrary,
BackToCollections,
}
pub struct LibraryView {
selected_collection_index: Option<usize>,
collections: HashMap<String, Collection>,
display_collections: Vec<DisplayLibraryCollection>,
loading: bool,
error: Option<String>,
viewing_item: Option<DisplayLibraryItem>,
view_state: ViewState,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ViewState {
Collections,
CollectionItems,
ItemViewer,
}
impl Component for LibraryView {
type Message = Msg;
type Properties = LibraryViewProps;
fn create(ctx: &Context<Self>) -> 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;
link.send_message(Msg::CollectionsFetched(collections));
});
Self {
selected_collection_index: None,
collections: HashMap::new(),
display_collections: Vec::new(),
loading: true,
error: None,
viewing_item: None,
view_state: ViewState::Collections,
}
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
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));
});
}
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::CollectionsFetched(collections) => {
log::info!("Collections fetched: {:?}", collections.keys().collect::<Vec<_>>());
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();
let display_collection = DisplayLibraryCollection {
title: collection.title.clone(),
description: collection.description.clone(),
items: Vec::new(),
ws_url: ws_url.clone(),
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) {
display_collection.items = items.into_iter().map(Rc::new).collect();
}
true
}
Msg::ViewItem(item) => {
self.viewing_item = Some(item);
self.view_state = ViewState::ItemViewer;
true
}
Msg::BackToLibrary => {
self.viewing_item = None;
self.view_state = ViewState::CollectionItems;
true
}
Msg::BackToCollections => {
self.viewing_item = None;
self.selected_collection_index = None;
self.view_state = ViewState::Collections;
true
}
Msg::SelectCollection(idx) => {
self.selected_collection_index = Some(idx);
self.view_state = ViewState::CollectionItems;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
match &self.view_state {
ViewState::ItemViewer => {
if let Some(item) = &self.viewing_item {
let back_callback = ctx.link().callback(|_| Msg::BackToLibrary);
let toc_callback = Callback::from(|_page: usize| {
// TOC navigation is now handled by the BookViewer component
});
html! {
<div class="view-container sidebar-layout">
<div class="sidebar">
<AssetDetailsCard
item={item.clone()}
on_back={back_callback.clone()}
on_toc_click={toc_callback}
current_slide_index={None}
/>
</div>
<div class="library-content">
{ self.render_viewer_component(item, back_callback) }
</div>
</div>
}
} else {
html! { <p>{"No item selected"}</p> }
}
}
ViewState::CollectionItems => {
// Collection items view with click-outside to go back
let back_handler = ctx.link().callback(|_: MouseEvent| Msg::BackToCollections);
html! {
<div class="view-container layout">
<div class="library-content" onclick={back_handler}>
{ self.render_collection_items_view(ctx) }
</div>
</div>
}
}
ViewState::Collections => {
// Collections view - no click-outside needed
html! {
<div class="view-container layout">
<div class="library-content">
{ self.render_collections_view(ctx) }
</div>
</div>
}
}
}
}
}
impl LibraryView {
fn render_viewer_component(&self, item: &DisplayLibraryItem, back_callback: Callback<()>) -> Html {
match item {
DisplayLibraryItem::Image(img) => html! {
<ImageViewer image={img.clone()} on_back={back_callback} />
},
DisplayLibraryItem::Pdf(pdf) => html! {
<PdfViewer pdf={pdf.clone()} on_back={back_callback} />
},
DisplayLibraryItem::Markdown(md) => html! {
<MarkdownViewer markdown={md.clone()} on_back={back_callback} />
},
DisplayLibraryItem::Book(book) => html! {
<BookViewer book={book.clone()} on_back={back_callback} />
},
DisplayLibraryItem::Slides(slides) => html! {
<SlidesViewer slides={slides.clone()} on_back={back_callback} />
},
}
}
fn render_collections_view(&self, ctx: &Context<Self>) -> Html {
if self.loading {
html! { <p>{"Loading collections..."}</p> }
} else if let Some(err) = &self.error {
html! { <p class="error-message">{format!("Error: {}", err)}</p> }
} else if self.display_collections.is_empty() {
html! { <p class="no-collections-message">{"No collections available."}</p> }
} else {
html! {
<>
<h1>{"Collections"}</h1>
<div class="collections-grid">
{ self.display_collections.iter().enumerate().map(|(idx, collection)| {
let onclick = ctx.link().callback(move |e: MouseEvent| {
e.stop_propagation();
Msg::SelectCollection(idx)
});
let item_count = collection.items.len();
html! {
<div class="card" onclick={onclick}>
<h3 class="collection-title">{ &collection.title }</h3>
{ if let Some(desc) = &collection.description {
html! { <p class="collection-description">{ desc }</p> }
} else {
html! {}
}}
</div>
}
}).collect::<Html>() }
</div>
</>
}
}
}
fn render_collection_items_view(&self, ctx: &Context<Self>) -> Html {
if let Some(selected_index) = self.selected_collection_index {
if let Some(collection) = self.display_collections.get(selected_index) {
html! {
<>
<header>
<h2 onclick={|e: MouseEvent| e.stop_propagation()}>{ &collection.title }</h2>
{ if let Some(desc) = &collection.description {
html! { <p onclick={|e: MouseEvent| e.stop_propagation()}>{ desc }</p> }
} else {
html! {}
}}
</header>
<div class="library-items-grid">
{ collection.items.iter().map(|item| {
let item_clone = item.as_ref().clone();
let onclick = ctx.link().callback(move |e: MouseEvent| {
e.stop_propagation();
Msg::ViewItem(item_clone.clone())
});
match item.as_ref() {
DisplayLibraryItem::Image(img) => html! {
<div class="library-item-card" onclick={onclick}>
<div class="item-preview">
<img src={img.url.clone()} class="item-thumbnail-img" alt={img.title.clone()} />
</div>
<div class="item-details">
<p class="item-title">{ &img.title }</p>
{ if let Some(desc) = &img.description {
html! { <p class="item-description">{ desc }</p> }
} else { html! {} }}
</div>
</div>
},
DisplayLibraryItem::Pdf(pdf) => html! {
<div class="library-item-card" onclick={onclick}>
<div class="item-preview">
<i class="fas fa-file-pdf item-preview-fallback-icon"></i>
</div>
<div class="item-details">
<p class="item-title">{ &pdf.title }</p>
{ if let Some(desc) = &pdf.description {
html! { <p class="item-description">{ desc }</p> }
} else { html! {} }}
<p class="item-meta">{ format!("{} pages", pdf.page_count) }</p>
</div>
</div>
},
DisplayLibraryItem::Markdown(md) => html! {
<div class="library-item-card" onclick={onclick}>
<div class="item-preview">
<i class="fab fa-markdown item-preview-fallback-icon"></i>
</div>
<div class="item-details">
<p class="item-title">{ &md.title }</p>
{ if let Some(desc) = &md.description {
html! { <p class="item-description">{ desc }</p> }
} else { html! {} }}
</div>
</div>
},
DisplayLibraryItem::Book(book) => html! {
<div class="library-item-card" onclick={onclick}>
<div class="item-preview">
<i class="fas fa-book item-preview-fallback-icon"></i>
</div>
<div class="item-details">
<p class="item-title">{ &book.title }</p>
{ if let Some(desc) = &book.description {
html! { <p class="item-description">{ desc }</p> }
} else { html! {} }}
<p class="item-meta">{ format!("{} pages", book.pages.len()) }</p>
</div>
</div>
},
DisplayLibraryItem::Slides(slides) => html! {
<div class="library-item-card" onclick={onclick}>
<div class="item-preview">
<i class="fas fa-images item-preview-fallback-icon"></i>
</div>
<div class="item-details">
<p class="item-title">{ &slides.title }</p>
{ if let Some(desc) = &slides.description {
html! { <p class="item-description">{ desc }</p> }
} else { html! {} }}
<p class="item-meta">{ format!("{} slides", slides.slide_urls.len()) }</p>
</div>
</div>
},
}
}).collect::<Html>() }
</div>
</>
}
} else {
html! { <p>{"Collection not found."}</p> }
}
} else {
self.render_collections_view(ctx)
}
}
}
/// Convenience function to fetch collections from WebSocket URLs
async fn get_collections(ws_urls: &[String]) -> HashMap<String, Collection> {
let collections_arrays: HashMap<String, Vec<Collection>> = fetch_data_from_ws_urls(ws_urls, "list_collections().json()".to_string()).await;
let mut result = HashMap::new();
for (ws_url, collections_vec) in collections_arrays {
for (index, collection) in collections_vec.into_iter().enumerate() {
// Use a unique key combining ws_url and collection index/id
let key = format!("{}_{}", ws_url, collection.base_data.id);
result.insert(key, collection);
}
}
result
}
/// Fetch all items for a collection from a WebSocket URL
async fn fetch_collection_items(ws_url: &str, collection: &Collection) -> Vec<DisplayLibraryItem> {
let mut items = Vec::new();
// Fetch images
for image_id in &collection.images {
match fetch_data_from_ws_url::<Image>(ws_url, &format!("get_image({}).json()", image_id)).await {
Ok(image) => items.push(DisplayLibraryItem::Image(image)),
Err(e) => log::error!("Failed to fetch image {}: {}", image_id, e),
}
}
// Fetch PDFs
for pdf_id in &collection.pdfs {
match fetch_data_from_ws_url::<Pdf>(ws_url, &format!("get_pdf({}).json()", pdf_id)).await {
Ok(pdf) => items.push(DisplayLibraryItem::Pdf(pdf)),
Err(e) => log::error!("Failed to fetch PDF {}: {}", pdf_id, e),
}
}
// Fetch Markdowns
for markdown_id in &collection.markdowns {
match fetch_data_from_ws_url::<Markdown>(ws_url, &format!("get_markdown({}).json()", markdown_id)).await {
Ok(markdown) => items.push(DisplayLibraryItem::Markdown(markdown)),
Err(e) => log::error!("Failed to fetch markdown {}: {}", markdown_id, e),
}
}
// Fetch Books
for book_id in &collection.books {
match fetch_data_from_ws_url::<Book>(ws_url, &format!("get_book({}).json()", book_id)).await {
Ok(book) => items.push(DisplayLibraryItem::Book(book)),
Err(e) => log::error!("Failed to fetch book {}: {}", book_id, e),
}
}
// Fetch Slides
for slides_id in &collection.slides {
match fetch_data_from_ws_url::<Slides>(ws_url, &format!("get_slides({}).json()", slides_id)).await {
Ok(slides) => items.push(DisplayLibraryItem::Slides(slides)),
Err(e) => log::error!("Failed to fetch slides {}: {}", slides_id, e),
}
}
items
}

View File

@ -0,0 +1,633 @@
//! Login component for authentication
//!
//! This component provides a user interface for authentication using either
//! email addresses (with hardcoded key lookup) or direct private key input.
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::auth::{AuthManager, AuthState, AuthMethod};
/// Props for the login component
#[derive(Properties, PartialEq)]
pub struct LoginProps {
/// Authentication manager instance
pub auth_manager: AuthManager,
/// Callback when authentication is successful
#[prop_or_default]
pub on_authenticated: Option<Callback<()>>,
/// Callback when authentication fails
#[prop_or_default]
pub on_error: Option<Callback<String>>,
}
/// Login method selection
#[derive(Debug, Clone, PartialEq)]
enum LoginMethod {
Email,
PrivateKey,
CreateKey,
}
/// Messages for the login component
pub enum LoginMsg {
SetLoginMethod(LoginMethod),
SetEmail(String),
SetPrivateKey(String),
SubmitLogin,
AuthStateChanged(AuthState),
ShowAvailableEmails,
HideAvailableEmails,
SelectEmail(String),
GenerateNewKey,
CopyToClipboard(String),
UseGeneratedKey,
}
/// Login component state
pub struct LoginComponent {
login_method: LoginMethod,
email: String,
private_key: String,
is_loading: bool,
error_message: Option<String>,
show_available_emails: bool,
available_emails: Vec<String>,
auth_state: AuthState,
generated_private_key: Option<String>,
generated_public_key: Option<String>,
copy_feedback: Option<String>,
}
impl Component for LoginComponent {
type Message = LoginMsg;
type Properties = LoginProps;
fn create(ctx: &Context<Self>) -> Self {
let auth_manager = ctx.props().auth_manager.clone();
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(LoginMsg::AuthStateChanged));
// Get available emails for app
let available_emails = auth_manager.get_available_emails();
Self {
login_method: LoginMethod::Email,
email: String::new(),
private_key: String::new(),
is_loading: false,
error_message: None,
show_available_emails: false,
available_emails,
auth_state,
generated_private_key: None,
generated_public_key: None,
copy_feedback: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
LoginMsg::SetLoginMethod(method) => {
self.login_method = method.clone();
self.error_message = None;
self.copy_feedback = None;
// Clear generated keys when switching away from CreateKey method
if method != LoginMethod::CreateKey {
self.generated_private_key = None;
self.generated_public_key = None;
}
true
}
LoginMsg::SetEmail(email) => {
self.email = email;
self.error_message = None;
true
}
LoginMsg::SetPrivateKey(private_key) => {
self.private_key = private_key;
self.error_message = None;
true
}
LoginMsg::SubmitLogin => {
if self.is_loading {
return false;
}
self.is_loading = true;
self.error_message = None;
let auth_manager = ctx.props().auth_manager.clone();
let link = ctx.link().clone();
let on_authenticated = ctx.props().on_authenticated.clone();
let on_error = ctx.props().on_error.clone();
match self.login_method {
LoginMethod::Email => {
let email = self.email.clone();
wasm_bindgen_futures::spawn_local(async move {
match auth_manager.authenticate_with_email(email).await {
Ok(()) => {
if let Some(callback) = on_authenticated {
callback.emit(());
}
}
Err(e) => {
if let Some(callback) = on_error {
callback.emit(e.to_string());
}
link.send_message(LoginMsg::AuthStateChanged(AuthState::Failed(e.to_string())));
}
}
});
}
LoginMethod::PrivateKey => {
let private_key = self.private_key.clone();
wasm_bindgen_futures::spawn_local(async move {
match auth_manager.authenticate_with_private_key(private_key).await {
Ok(()) => {
if let Some(callback) = on_authenticated {
callback.emit(());
}
}
Err(e) => {
if let Some(callback) = on_error {
callback.emit(e.to_string());
}
link.send_message(LoginMsg::AuthStateChanged(AuthState::Failed(e.to_string())));
}
}
});
}
LoginMethod::CreateKey => {
// This shouldn't happen as CreateKey method doesn't have a submit button
// But if it does, treat it as an error
self.error_message = Some("Please generate a key first, then use it to login.".to_string());
}
}
true
}
LoginMsg::AuthStateChanged(state) => {
self.auth_state = state.clone();
match state {
AuthState::Authenticating => {
self.is_loading = true;
self.error_message = None;
}
AuthState::Authenticated { .. } => {
self.is_loading = false;
self.error_message = None;
}
AuthState::Failed(error) => {
self.is_loading = false;
self.error_message = Some(error);
}
AuthState::NotAuthenticated => {
self.is_loading = false;
self.error_message = None;
}
}
true
}
LoginMsg::ShowAvailableEmails => {
self.show_available_emails = true;
true
}
LoginMsg::HideAvailableEmails => {
self.show_available_emails = false;
true
}
LoginMsg::SelectEmail(email) => {
self.email = email;
self.show_available_emails = false;
self.error_message = None;
true
}
LoginMsg::GenerateNewKey => {
use circle_client_ws::auth as crypto_utils;
match crypto_utils::generate_private_key() {
Ok(private_key) => {
match crypto_utils::derive_public_key(&private_key) {
Ok(public_key) => {
self.generated_private_key = Some(private_key);
self.generated_public_key = Some(public_key);
self.error_message = None;
self.copy_feedback = None;
}
Err(e) => {
self.error_message = Some(format!("Failed to derive public key: {}", e));
}
}
}
Err(e) => {
self.error_message = Some(format!("Failed to generate private key: {}", e));
}
}
true
}
LoginMsg::CopyToClipboard(text) => {
// Simple fallback: show the text in an alert for now
// TODO: Implement proper clipboard API when web_sys is properly configured
if let Some(window) = web_sys::window() {
window.alert_with_message(&format!("Copy this key:\n\n{}", text)).ok();
self.copy_feedback = Some("Key shown in alert - please copy manually".to_string());
}
true
}
LoginMsg::UseGeneratedKey => {
if let Some(private_key) = &self.generated_private_key {
self.private_key = private_key.clone();
self.login_method = LoginMethod::PrivateKey;
self.copy_feedback = None;
}
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// If already authenticated, show status
if let AuthState::Authenticated { method, public_key, .. } = &self.auth_state {
return self.render_authenticated_view(method, public_key, link);
}
html! {
<div class="login-container">
<div class="login-card">
<h2 class="login-title">{ "Authenticate to Circles" }</h2>
{ self.render_method_selector(link) }
{ self.render_login_form(link) }
{ self.render_error_message() }
{ self.render_loading_indicator() }
</div>
</div>
}
}
}
impl LoginComponent {
fn render_method_selector(&self, link: &html::Scope<Self>) -> Html {
html! {
<div class="login-method-selector">
<div class="method-tabs">
<button
class={classes!("method-tab", if self.login_method == LoginMethod::Email { "active" } else { "" })}
onclick={link.callback(|_| LoginMsg::SetLoginMethod(LoginMethod::Email))}
disabled={self.is_loading}
>
<i class="fas fa-envelope"></i>
{ " Email" }
</button>
<button
class={classes!("method-tab", if self.login_method == LoginMethod::PrivateKey { "active" } else { "" })}
onclick={link.callback(|_| LoginMsg::SetLoginMethod(LoginMethod::PrivateKey))}
disabled={self.is_loading}
>
<i class="fas fa-key"></i>
{ " Private Key" }
</button>
<button
class={classes!("method-tab", if self.login_method == LoginMethod::CreateKey { "active" } else { "" })}
onclick={link.callback(|_| LoginMsg::SetLoginMethod(LoginMethod::CreateKey))}
disabled={self.is_loading}
>
<i class="fas fa-plus-circle"></i>
{ " Create Key" }
</button>
</div>
</div>
}
}
fn render_login_form(&self, link: &html::Scope<Self>) -> Html {
match self.login_method {
LoginMethod::Email => self.render_email_form(link),
LoginMethod::PrivateKey => self.render_private_key_form(link),
LoginMethod::CreateKey => self.render_create_key_form(link),
}
}
fn render_email_form(&self, link: &html::Scope<Self>) -> Html {
let on_email_input = link.batch_callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
Some(LoginMsg::SetEmail(input.value()))
});
let on_submit = link.callback(|e: SubmitEvent| {
e.prevent_default();
LoginMsg::SubmitLogin
});
html! {
<form class="login-form" onsubmit={on_submit}>
<div class="form-group">
<label for="email-input">{ "Email Address" }</label>
<div class="email-input-container">
<input
id="email-input"
type="email"
class="form-input"
placeholder="Enter your email address"
value={self.email.clone()}
oninput={on_email_input}
disabled={self.is_loading}
required=true
/>
<button
type="button"
class="email-dropdown-btn"
onclick={link.callback(|_| LoginMsg::ShowAvailableEmails)}
disabled={self.is_loading}
title="Show available app emails"
>
<i class="fas fa-chevron-down"></i>
</button>
</div>
{ self.render_email_dropdown(link) }
<small class="form-help">
{ "Use one of the app email addresses or click the dropdown to see available options." }
</small>
</div>
<button
type="submit"
class="login-btn"
disabled={self.is_loading || self.email.is_empty()}
>
{ if self.is_loading { "Authenticating..." } else { "Login with Email" } }
</button>
</form>
}
}
fn render_private_key_form(&self, link: &html::Scope<Self>) -> Html {
let on_private_key_input = link.batch_callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
Some(LoginMsg::SetPrivateKey(input.value()))
});
let on_submit = link.callback(|e: SubmitEvent| {
e.prevent_default();
LoginMsg::SubmitLogin
});
html! {
<form class="login-form" onsubmit={on_submit}>
<div class="form-group">
<label for="private-key-input">{ "Private Key" }</label>
<input
id="private-key-input"
type="password"
class="form-input"
placeholder="Enter your private key (hex format)"
value={self.private_key.clone()}
oninput={on_private_key_input}
disabled={self.is_loading}
required=true
/>
<small class="form-help">
{ "Enter your secp256k1 private key in hexadecimal format (with or without 0x prefix)." }
</small>
</div>
<button
type="submit"
class="login-btn"
disabled={self.is_loading || self.private_key.is_empty()}
>
{ if self.is_loading { "Authenticating..." } else { "Login with Private Key" } }
</button>
</form>
}
}
fn render_email_dropdown(&self, link: &html::Scope<Self>) -> Html {
if !self.show_available_emails {
return html! {};
}
html! {
<div class="email-dropdown">
<div class="dropdown-header">
<span>{ "Available Demo Emails" }</span>
<button
type="button"
class="dropdown-close"
onclick={link.callback(|_| LoginMsg::HideAvailableEmails)}
>
<i class="fas fa-times"></i>
</button>
</div>
<div class="dropdown-list">
{ for self.available_emails.iter().map(|email| {
let email_clone = email.clone();
html! {
<button
type="button"
class="dropdown-item"
onclick={link.callback(move |_| LoginMsg::SelectEmail(email_clone.clone()))}
>
<i class="fas fa-user"></i>
{ email }
</button>
}
}) }
</div>
</div>
}
}
fn render_error_message(&self) -> Html {
if let Some(error) = &self.error_message {
html! {
<div class="error-message">
<i class="fas fa-exclamation-triangle"></i>
{ error }
</div>
}
} else {
html! {}
}
}
fn render_loading_indicator(&self) -> Html {
if self.is_loading {
html! {
<div class="loading-indicator">
<div class="spinner"></div>
<span>{ "Authenticating..." }</span>
</div>
}
} else {
html! {}
}
}
fn render_authenticated_view(&self, method: &AuthMethod, public_key: &str, link: &html::Scope<Self>) -> Html {
let method_display = match method {
AuthMethod::Email(email) => format!("Email: {}", email),
AuthMethod::PrivateKey => "Private Key".to_string(),
};
let short_public_key = if public_key.len() > 20 {
format!("{}...{}", &public_key[..10], &public_key[public_key.len()-10..])
} else {
public_key.to_string()
};
html! {
<div class="authenticated-container">
<div class="authenticated-card">
<div class="auth-success-icon">
<i class="fas fa-check-circle"></i>
</div>
<h3>{ "Authentication Successful" }</h3>
<div class="auth-details">
<div class="auth-detail">
<label>{ "Method:" }</label>
<span>{ method_display }</span>
</div>
<div class="auth-detail">
<label>{ "Public Key:" }</label>
<span class="public-key" title={public_key.to_string()}>{ short_public_key }</span>
</div>
</div>
<button
class="logout-btn"
onclick={link.callback(|_| {
// This would need to be handled by the parent component
LoginMsg::AuthStateChanged(AuthState::NotAuthenticated)
})}
>
{ "Logout" }
</button>
</div>
</div>
}
}
fn render_create_key_form(&self, link: &html::Scope<Self>) -> Html {
html! {
<div class="create-key-form">
<div class="form-group">
<h3>{ "Generate New secp256k1 Keypair" }</h3>
<p class="form-help">
{ "Create a new cryptographic keypair for authentication. " }
{ "Make sure to securely store your private key!" }
</p>
<button
type="button"
class="generate-key-btn"
onclick={link.callback(|_| LoginMsg::GenerateNewKey)}
disabled={self.is_loading}
>
<i class="fas fa-dice"></i>
{ " Generate New Keypair" }
</button>
</div>
{ self.render_generated_keys(link) }
{ self.render_copy_feedback() }
</div>
}
}
fn render_generated_keys(&self, link: &html::Scope<Self>) -> Html {
if let (Some(private_key), Some(public_key)) = (&self.generated_private_key, &self.generated_public_key) {
let private_key_clone = private_key.clone();
let public_key_clone = public_key.clone();
html! {
<div class="generated-keys">
<div class="key-section">
<label>{ "Private Key (Keep Secret!)" }</label>
<div class="key-display">
<input
type="text"
class="key-input"
value={private_key.clone()}
readonly=true
/>
<button
type="button"
class="copy-btn"
onclick={link.callback(move |_| LoginMsg::CopyToClipboard(private_key_clone.clone()))}
title="Copy private key to clipboard"
>
<i class="fas fa-copy"></i>
</button>
</div>
<small class="key-warning">
<i class="fas fa-exclamation-triangle"></i>
{ " Store this private key securely! Anyone with access to it can control your account." }
</small>
</div>
<div class="key-section">
<label>{ "Public Key (Safe to Share)" }</label>
<div class="key-display">
<input
type="text"
class="key-input"
value={public_key.clone()}
readonly=true
/>
<button
type="button"
class="copy-btn"
onclick={link.callback(move |_| LoginMsg::CopyToClipboard(public_key_clone.clone()))}
title="Copy public key to clipboard"
>
<i class="fas fa-copy"></i>
</button>
</div>
<small class="key-info">
{ "This is your public address that others can use to identify you." }
</small>
</div>
<div class="key-actions">
<button
type="button"
class="use-key-btn"
onclick={link.callback(|_| LoginMsg::UseGeneratedKey)}
>
<i class="fas fa-arrow-right"></i>
{ " Use This Key to Login" }
</button>
<button
type="button"
class="generate-new-btn"
onclick={link.callback(|_| LoginMsg::GenerateNewKey)}
>
<i class="fas fa-redo"></i>
{ " Generate New Keypair" }
</button>
</div>
</div>
}
} else {
html! {}
}
}
fn render_copy_feedback(&self) -> Html {
if let Some(feedback) = &self.copy_feedback {
html! {
<div class="copy-feedback">
<i class="fas fa-check"></i>
{ feedback }
</div>
}
} else {
html! {}
}
}
}

View File

@ -0,0 +1,75 @@
use yew::prelude::*;
use heromodels::models::library::items::Markdown;
#[derive(Clone, PartialEq, Properties)]
pub struct MarkdownViewerProps {
pub markdown: Markdown,
pub on_back: Callback<()>,
}
pub struct MarkdownViewer;
impl Component for MarkdownViewer {
type Message = ();
type Properties = MarkdownViewerProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let back_handler = {
let on_back = props.on_back.clone();
Callback::from(move |_: MouseEvent| {
on_back.emit(());
})
};
html! {
<div class="asset-viewer markdown-viewer">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Collection"}
</button>
<div class="viewer-header">
<h2 class="viewer-title">{ &props.markdown.title }</h2>
</div>
<div class="viewer-content">
<div class="markdown-content">
{ self.render_markdown(&props.markdown.content) }
</div>
</div>
</div>
}
}
}
impl MarkdownViewer {
fn render_markdown(&self, content: &str) -> Html {
// Simple markdown rendering - convert basic markdown to HTML
let lines: Vec<&str> = content.lines().collect();
let mut html_content = Vec::new();
for line in lines {
if line.starts_with("# ") {
html_content.push(html! { <h1>{ &line[2..] }</h1> });
} else if line.starts_with("## ") {
html_content.push(html! { <h2>{ &line[3..] }</h2> });
} else if line.starts_with("### ") {
html_content.push(html! { <h3>{ &line[4..] }</h3> });
} else if line.starts_with("- ") {
html_content.push(html! { <li>{ &line[2..] }</li> });
} else if line.starts_with("**") && line.ends_with("**") {
let text = &line[2..line.len()-2];
html_content.push(html! { <p><strong>{ text }</strong></p> });
} else if !line.trim().is_empty() {
html_content.push(html! { <p>{ line }</p> });
} else {
html_content.push(html! { <br/> });
}
}
html! { <div>{ for html_content }</div> }
}
}

View File

@ -0,0 +1,31 @@
// This file declares the `components` module.
pub mod circles_view;
pub mod nav_island;
pub mod library_view;
// pub use library_view::{LibraryView, LibraryViewProps}; // Kept commented as it's unused or handled in app.rs
// Kept commented as it's unused or handled in app.rs
// pub mod dashboard_view; // Commented out as dashboard_view.rs doesn't exist yet
pub mod intelligence_view;
pub mod network_animation_view;
pub mod publishing_view;
pub mod customize_view;
pub mod inspector_view;
pub mod inspector_network_tab;
pub mod inspector_logs_tab;
pub mod inspector_interact_tab;
pub mod inspector_auth_tab;
pub mod chat;
pub mod sidebar_layout;
pub mod world_map_svg;
// Authentication components
pub mod login_component;
pub mod auth_view;
// Library viewer components
pub mod book_viewer;
pub mod slides_viewer;
pub mod image_viewer;
pub mod pdf_viewer;
pub mod markdown_viewer;
pub mod asset_details_card;

View File

@ -0,0 +1,74 @@
use yew::{function_component, Callback, Properties, classes, use_state, use_node_ref};
use web_sys::MouseEvent;
use crate::app::AppView; // Assuming AppView is accessible
#[derive(Properties, PartialEq, Clone)]
pub struct NavIslandProps {
pub current_view: AppView,
pub on_switch_view: Callback<AppView>,
}
#[function_component(NavIsland)]
pub fn nav_island(props: &NavIslandProps) -> yew::Html {
let is_clicked = use_state(|| false);
let nav_island_ref = use_node_ref();
// Create all button data with their view/tab info
let mut all_buttons = vec![
(AppView::Circles, None::<()>, "fas fa-circle-notch", "Circles"),
(AppView::Library, None::<()>, "fas fa-book", "Library"),
(AppView::Intelligence, None::<()>, "fas fa-brain", "Intelligence"),
(AppView::Publishing, None::<()>, "fas fa-rocket", "Publishing"),
(AppView::Inspector, None::<()>, "fas fa-search-location", "Inspector"),
(AppView::Customize, None::<()>, "fas fa-paint-brush", "Customize"),
];
// Find and move the active button to the front
let active_index = all_buttons.iter().position(|(view, tab, _, _)| {
*view == props.current_view && tab.is_none() // A button is active if its view matches and it has no specific tab
});
if let Some(index) = active_index {
let active_button = all_buttons.remove(index);
all_buttons.insert(0, active_button);
}
let is_clicked_clone = is_clicked.clone();
let onmouseenter = Callback::from(move |_: MouseEvent| {
// Only reset clicked state if user explicitly hovers after clicking
if *is_clicked_clone {
is_clicked_clone.set(false);
}
});
yew::html! {
<div
class={classes!("nav-island", "collapsed", (*is_clicked).then_some("clicked"))}
ref={nav_island_ref.clone()}
onmouseenter={onmouseenter}
>
<div class="nav-island-buttons">
{ for all_buttons.iter().map(|(button_app_view, _button_tab_opt, icon_class, text)| {
let on_select_view_cb = props.on_switch_view.clone();
let view_to_emit = *button_app_view;
let is_clicked_setter = is_clicked.setter();
let button_click_handler = Callback::from(move |_| {
is_clicked_setter.set(true);
on_select_view_cb.emit(view_to_emit.clone());
});
let is_active = *button_app_view == props.current_view && _button_tab_opt.is_none();
yew::html! {
<button
class={classes!("nav-button", is_active.then_some("active"))}
onclick={button_click_handler}>
<i class={*icon_class}></i>
<span>{ text }</span>
</button>
}
}) }
</div>
</div>
}
}

View File

@ -0,0 +1,260 @@
use yew::prelude::*;
use std::collections::HashMap;
use std::rc::Rc;
use common_models::CircleData;
use gloo_timers::callback::{Interval, Timeout};
use rand::seq::SliceRandom;
use rand::Rng;
#[derive(Clone, Debug, PartialEq)]
struct ServerNode {
x: f32,
y: f32,
name: String,
id: u32,
is_active: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct DataTransmission {
id: usize,
from_node: u32,
to_node: u32,
progress: f32,
transmission_type: TransmissionType,
}
#[derive(Clone, Debug, PartialEq)]
enum TransmissionType {
Data,
Sync,
Heartbeat,
}
#[derive(Properties, Clone, PartialEq)]
pub struct NetworkAnimationViewProps {
pub all_circles: Rc<HashMap<u32, CircleData>>,
}
pub enum Msg {
StartTransmission,
UpdateTransmissions,
RemoveTransmission(usize),
PulseNode(u32),
}
pub struct NetworkAnimationView {
server_nodes: Rc<HashMap<u32, ServerNode>>,
active_transmissions: Vec<DataTransmission>,
next_transmission_id: usize,
_transmission_interval: Option<Interval>,
_update_interval: Option<Interval>,
}
impl NetworkAnimationView {
fn calculate_server_positions(all_circles: &Rc<HashMap<u32, CircleData>>) -> Rc<HashMap<u32, ServerNode>> {
let mut nodes = HashMap::new();
// Predefined realistic server locations on the world map (coordinates scaled to viewBox 783.086 x 400.649)
let server_positions = vec![
(180.0, 150.0, "North America"), // USA/Canada
(420.0, 130.0, "Europe"), // Central Europe
(580.0, 160.0, "Asia"), // East Asia
(220.0, 280.0, "South America"), // Brazil/Argentina
(450.0, 220.0, "Africa"), // Central Africa
(650.0, 320.0, "Oceania"), // Australia
(400.0, 90.0, "Nordic"), // Scandinavia
(520.0, 200.0, "Middle East"), // Middle East
];
for (i, (id, circle_data)) in all_circles.iter().enumerate() {
if let Some((x, y, region)) = server_positions.get(i % server_positions.len()) {
nodes.insert(*id, ServerNode {
x: *x,
y: *y,
name: format!("{}", circle_data.name),
id: *id,
is_active: true,
});
}
}
Rc::new(nodes)
}
fn create_transmission(&mut self, from_id: u32, to_id: u32, transmission_type: TransmissionType) -> usize {
let id = self.next_transmission_id;
self.next_transmission_id += 1;
self.active_transmissions.push(DataTransmission {
id,
from_node: from_id,
to_node: to_id,
progress: 0.0,
transmission_type,
});
id
}
}
impl Component for NetworkAnimationView {
type Message = Msg;
type Properties = NetworkAnimationViewProps;
fn create(ctx: &Context<Self>) -> Self {
let server_nodes = Self::calculate_server_positions(&ctx.props().all_circles);
let link = ctx.link().clone();
let transmission_interval = Interval::new(3000, move || {
link.send_message(Msg::StartTransmission);
});
let link2 = ctx.link().clone();
let update_interval = Interval::new(50, move || {
link2.send_message(Msg::UpdateTransmissions);
});
Self {
server_nodes,
active_transmissions: Vec::new(),
next_transmission_id: 0,
_transmission_interval: Some(transmission_interval),
_update_interval: Some(update_interval),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::StartTransmission => {
if self.server_nodes.len() < 2 {
return false;
}
let mut rng = rand::thread_rng();
let node_ids: Vec<u32> = self.server_nodes.keys().cloned().collect();
if let (Some(&from_id), Some(&to_id)) = (
node_ids.choose(&mut rng),
node_ids.choose(&mut rng)
) {
if from_id != to_id {
let transmission_type = match rng.gen_range(0..3) {
0 => TransmissionType::Data,
1 => TransmissionType::Sync,
_ => TransmissionType::Heartbeat,
};
let transmission_id = self.create_transmission(from_id, to_id, transmission_type);
// Pulse the source node
ctx.link().send_message(Msg::PulseNode(from_id));
// Remove transmission after completion
let link = ctx.link().clone();
let timeout = Timeout::new(2000, move || {
link.send_message(Msg::RemoveTransmission(transmission_id));
});
timeout.forget();
return true;
}
}
false
}
Msg::UpdateTransmissions => {
let mut updated = false;
for transmission in &mut self.active_transmissions {
if transmission.progress < 1.0 {
transmission.progress += 0.02; // 2% per update (50ms * 50 = 2.5s total)
updated = true;
}
}
updated
}
Msg::RemoveTransmission(id) => {
let initial_len = self.active_transmissions.len();
self.active_transmissions.retain(|t| t.id != id);
self.active_transmissions.len() != initial_len
}
Msg::PulseNode(_node_id) => {
// This will trigger a re-render for node pulse animation
true
}
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let server_pins = self.server_nodes.iter().map(|(_id, node)| {
html! {
<g class="server-node" transform={format!("translate({}, {})", node.x, node.y)}>
// Subtle glow background
<circle r="6" class="node-glow" />
// Main white pin
<circle r="3" class="node-pin" />
// Ultra-subtle breathing effect
<circle r="4" class="node-pulse" />
// Clean label
<text x="0" y="16" class="node-label" text-anchor="middle">{&node.name}</text>
</g>
}
});
let transmissions = self.active_transmissions.iter().map(|transmission| {
if let (Some(from_node), Some(to_node)) = (
self.server_nodes.get(&transmission.from_node),
self.server_nodes.get(&transmission.to_node)
) {
html! {
<g class="transmission-group">
// Simple connection line with subtle animation
<line
x1={from_node.x.to_string()}
y1={from_node.y.to_string()}
x2={to_node.x.to_string()}
y2={to_node.y.to_string()}
class="transmission-line"
/>
</g>
}
} else {
html! {}
}
}).collect::<Html>();
html! {
<div class="network-animation-overlay">
<svg
viewBox="0 0 783.086 400.649"
class="network-overlay-svg"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"
>
<defs>
// Minimal gradient for node glow
<@{"radialGradient"} id="nodeGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color: var(--primary-color, #007bff); stop-opacity: 0.3" />
<stop offset="100%" style="stop-color: var(--primary-color, #007bff); stop-opacity: 0" />
</@>
</defs>
<g class="server-nodes">
{ for server_pins }
</g>
<g class="transmissions">
{ transmissions }
</g>
</svg>
</div>
}
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
if ctx.props().all_circles != old_props.all_circles {
self.server_nodes = Self::calculate_server_positions(&ctx.props().all_circles);
self.active_transmissions.clear();
return true;
}
false
}
}

View File

@ -0,0 +1,48 @@
use yew::prelude::*;
use heromodels::models::library::items::Pdf;
#[derive(Clone, PartialEq, Properties)]
pub struct PdfViewerProps {
pub pdf: Pdf,
pub on_back: Callback<()>,
}
pub struct PdfViewer;
impl Component for PdfViewer {
type Message = ();
type Properties = PdfViewerProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let back_handler = {
let on_back = props.on_back.clone();
Callback::from(move |_: MouseEvent| {
on_back.emit(());
})
};
html! {
<div class="asset-viewer pdf-viewer">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Collection"}
</button>
<div class="viewer-header">
<h2 class="viewer-title">{ &props.pdf.title }</h2>
</div>
<div class="viewer-content">
<iframe
src={format!("{}#toolbar=1&navpanes=1&scrollbar=1", props.pdf.url)}
class="pdf-frame"
title={props.pdf.title.clone()}
/>
</div>
</div>
}
}
}

View File

@ -0,0 +1,870 @@
use yew::prelude::*;
use heromodels::models::circle::Circle;
use std::rc::Rc;
use std::collections::HashMap;
use chrono::{Utc, DateTime}; // Added TimeZone
use web_sys::MouseEvent;
use wasm_bindgen_futures::spawn_local;
// Import from common_models
use common_models::{
Publication,
Deployment,
PublicationType,
PublicationStatus,
PublicationSource,
PublicationSourceType,
};
// --- Component-Specific View State Enums ---
#[derive(Clone, PartialEq, Debug)]
pub enum PublishingViewEnum {
PublicationsList,
PublicationDetail(u32), // publication_id
}
#[derive(Clone, PartialEq, Debug)]
pub enum PublishingPublicationTab {
Overview,
Analytics,
Deployments,
Settings,
}
// --- Props for the Component ---
#[derive(Clone, PartialEq, Properties)]
pub struct PublishingViewProps {
pub all_circles: Rc<HashMap<String, Circle>>,
pub context_circle_ws_urls: Option<Rc<Vec<String>>>,
}
#[derive(Clone, Debug)]
pub enum PublishingMsg {
SwitchView(PublishingViewEnum),
SwitchPublicationTab(PublishingPublicationTab),
CreateNewPublication,
TriggerDeployment(u32), // publication_id
DeletePublication(u32), // publication_id
SavePublicationSettings(u32), // publication_id
FetchPublications(String), // ws_url
PublicationsReceived(String, Vec<Publication>), // ws_url, publications
ActionCompleted(Result<String, String>),
}
pub struct PublishingView {
current_view: PublishingViewEnum,
active_publication_tab: PublishingPublicationTab,
ws_manager: crate::ws_manager::CircleWsManager,
loading_states: HashMap<String, bool>,
error_states: HashMap<String, Option<String>>,
}
impl Component for PublishingView {
type Message = PublishingMsg;
type Properties = PublishingViewProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
current_view: PublishingViewEnum::PublicationsList,
active_publication_tab: PublishingPublicationTab::Overview,
ws_manager: crate::ws_manager::CircleWsManager::new(),
loading_states: HashMap::new(),
error_states: HashMap::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PublishingMsg::SwitchView(view) => {
self.current_view = view;
true
}
PublishingMsg::SwitchPublicationTab(tab) => {
self.active_publication_tab = tab;
true
}
PublishingMsg::CreateNewPublication => {
log::info!("Creating new publication");
self.create_publication_via_script(ctx);
true
}
PublishingMsg::TriggerDeployment(publication_id) => {
log::info!("Triggering deployment for publication {}", publication_id);
self.trigger_deployment_via_script(ctx, publication_id);
true
}
PublishingMsg::DeletePublication(publication_id) => {
log::info!("Deleting publication {}", publication_id);
self.delete_publication_via_script(ctx, publication_id);
true
}
PublishingMsg::SavePublicationSettings(publication_id) => {
log::info!("Saving settings for publication {}", publication_id);
self.save_publication_settings_via_script(ctx, publication_id);
true
}
PublishingMsg::FetchPublications(ws_url) => {
log::info!("Fetching publications from: {}", ws_url);
self.fetch_publications_from_circle(&ws_url);
true
}
PublishingMsg::PublicationsReceived(ws_url, publications) => {
log::info!("Received {} publications from {}", publications.len(), ws_url);
// Handle received publications - could update local cache if needed
true
}
PublishingMsg::ActionCompleted(result) => {
match result {
Ok(output) => log::info!("Action completed successfully: {}", output),
Err(e) => log::error!("Action failed: {}", e),
}
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let props = ctx.props();
// Aggregate publications and deployments from all_circles based on context
let (filtered_publications, filtered_deployments) =
get_filtered_publishing_data(&props.all_circles, &props.context_circle_ws_urls);
match &self.current_view {
PublishingViewEnum::PublicationsList => {
html! {
<div class="view-container publishing-view-container">
<div class="view-header publishing-header">
<h1 class="view-title">{"Publications"}</h1>
<div class="publishing-actions">
<button class="action-btn primary" onclick={link.callback(|_| PublishingMsg::CreateNewPublication)}>
<i class="fas fa-plus"></i>
<span>{"New Publication"}</span>
</button>
</div>
</div>
<div class="publishing-content">
{ render_publications_list(&filtered_publications, link) }
</div>
</div>
}
},
PublishingViewEnum::PublicationDetail(publication_id) => {
let publication = filtered_publications.iter()
.find(|p| p.id == *publication_id) // Now u32 == u32
.cloned();
if let Some(pub_data) = publication {
// Filter deployments specific to this publication
let specific_deployments: Vec<Rc<Deployment>> = filtered_deployments.iter()
.filter(|d| d.publication_id == pub_data.id)
.cloned()
.collect();
html! {
<div class="view-container publishing-view-container">
<div class="publishing-content">
{ render_expanded_publication_card(
&pub_data,
link,
&self.active_publication_tab,
&specific_deployments
)}
</div>
</div>
}
} else {
// Fallback to list if specific publication not found (e.g., after context change)
html! {
<div class="view-container publishing-view-container">
<div class="view-header publishing-header">
<h1 class="view-title">{"Publications"}</h1>
</div>
<div class="publishing-content">
<p>{"Publication not found. Showing list."}</p>
{ render_publications_list(&filtered_publications, link) }
</div>
</div>
}
}
}
}
}
}
impl PublishingView {
fn create_publication_via_script(&mut self, ctx: &Context<Self>) {
let props = ctx.props();
let target_ws_url = props.context_circle_ws_urls
.as_ref()
.and_then(|urls| urls.first())
.cloned();
if let Some(ws_url) = target_ws_url {
let script = r#"create_publication("New Publication", "Website", "Draft");"#.to_string();
let link = ctx.link().clone();
if let Some(script_future) = self.ws_manager.execute_script(&ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
link.send_message(PublishingMsg::ActionCompleted(Ok(result.output)));
}
Err(e) => {
link.send_message(PublishingMsg::ActionCompleted(Err(format!("{:?}", e))));
}
}
});
}
}
}
fn trigger_deployment_via_script(&mut self, ctx: &Context<Self>, publication_id: u32) {
let props = ctx.props();
let target_ws_url = props.context_circle_ws_urls
.as_ref()
.and_then(|urls| urls.first())
.cloned();
if let Some(ws_url) = target_ws_url {
let script = format!(r#"trigger_deployment({});"#, publication_id);
let link = ctx.link().clone();
if let Some(script_future) = self.ws_manager.execute_script(&ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
link.send_message(PublishingMsg::ActionCompleted(Ok(result.output)));
}
Err(e) => {
link.send_message(PublishingMsg::ActionCompleted(Err(format!("{:?}", e))));
}
}
});
}
}
}
fn delete_publication_via_script(&mut self, ctx: &Context<Self>, publication_id: u32) {
let props = ctx.props();
let target_ws_url = props.context_circle_ws_urls
.as_ref()
.and_then(|urls| urls.first())
.cloned();
if let Some(ws_url) = target_ws_url {
let script = format!(r#"delete_publication({});"#, publication_id);
let link = ctx.link().clone();
if let Some(script_future) = self.ws_manager.execute_script(&ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
link.send_message(PublishingMsg::ActionCompleted(Ok(result.output)));
}
Err(e) => {
link.send_message(PublishingMsg::ActionCompleted(Err(format!("{:?}", e))));
}
}
});
}
}
}
fn save_publication_settings_via_script(&mut self, ctx: &Context<Self>, publication_id: u32) {
let props = ctx.props();
let target_ws_url = props.context_circle_ws_urls
.as_ref()
.and_then(|urls| urls.first())
.cloned();
if let Some(ws_url) = target_ws_url {
let script = format!(r#"save_publication_settings({});"#, publication_id);
let link = ctx.link().clone();
if let Some(script_future) = self.ws_manager.execute_script(&ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
link.send_message(PublishingMsg::ActionCompleted(Ok(result.output)));
}
Err(e) => {
link.send_message(PublishingMsg::ActionCompleted(Err(format!("{:?}", e))));
}
}
});
}
}
}
fn fetch_publications_from_circle(&mut self, ws_url: &str) {
let script = r#"
let publications = get_publications();
publications
"#.to_string();
if let Some(script_future) = self.ws_manager.execute_script(ws_url, script) {
spawn_local(async move {
match script_future.await {
Ok(result) => {
log::info!("Publications data fetched: {}", result.output);
// Parse and handle publications data
}
Err(e) => {
log::error!("Failed to fetch publications data: {:?}", e);
}
}
});
}
}
}
fn get_filtered_publishing_data(
_all_circles: &Rc<HashMap<String, Circle>>,
_context_circle_ws_urls: &Option<Rc<Vec<String>>>,
) -> (Vec<Rc<Publication>>, Vec<Rc<Deployment>>) {
// TODO: Implement actual data fetching based on context_circle_ws_urls
// For now, return mock data.
let filtered_publications = get_mock_publications();
let filtered_deployments = get_mock_deployments();
(filtered_publications, filtered_deployments)
}
fn render_publication_tab_button(
link: &yew::html::Scope<PublishingView>,
tab: PublishingPublicationTab,
active_tab: &PublishingPublicationTab,
icon: &str,
label: &str,
) -> Html {
let is_active = *active_tab == tab;
let tab_clone = tab.clone();
let icon_owned = icon.to_string();
let label_owned = label.to_string();
let on_click_handler = link.callback(move |_| PublishingMsg::SwitchPublicationTab(tab_clone.clone()));
html! {
<button
class={classes!("tab-btn", is_active.then_some("active"))}
onclick={on_click_handler}
>
<i class={icon_owned}></i>
<span>{label_owned}</span>
</button>
}
}
fn render_publications_list(publications: &[Rc<Publication>], link: &yew::html::Scope<PublishingView>) -> Html {
if publications.is_empty() {
return html! {
<div class="publications-view empty-state">
<i class="fas fa-cloud-upload-alt"></i>
<h4>{"No publications yet"}</h4>
<p>{"Create a new publication to deploy your projects."}</p>
<button class="action-btn primary" onclick={link.callback(|_| PublishingMsg::CreateNewPublication)}>
<i class="fas fa-plus"></i>
<span>{"New Publication"}</span>
</button>
</div>
};
}
html! {
<div class="publications-view">
<div class="publications-grid">
{ for publications.iter().map(|publication| {
render_publication_card(publication, link)
}) }
// "Add New" card can be a permanent fixture or conditional
<div class="card-base add-publication-card" onclick={link.callback(|_| PublishingMsg::CreateNewPublication)}>
<div class="add-publication-content">
<i class="fas fa-plus"></i>
<h3 class="card-title">{"Create New Publication"}</h3>
<div class="card-content">
<p>{"Deploy your website, app, or API"}</p>
</div>
</div>
</div>
</div>
</div>
}
}
fn render_publication_source(source: &Option<PublicationSource>) -> Html {
match source {
Some(s) => match s.source_type {
PublicationSourceType::GitRepository => html! {
<div class="source-detail code-repo-source">
{match s.repo_type.as_deref() {
Some("GitHub") => html! { <i class="fab fa-github"></i> },
Some("GitLab") => html! { <i class="fab fa-gitlab"></i> },
Some("Bitbucket") => html! { <i class="fab fa-bitbucket"></i> },
_ => html! { <i class="fas fa-code-branch"></i> },
}}
<a href={s.url.clone().unwrap_or_default()} target="_blank" onclick={|e: MouseEvent| e.stop_propagation()}>
{s.url.as_ref().and_then(|u| u.split('/').last()).unwrap_or("Repository")}
</a>
{ if let Some(branch) = &s.repo_branch {
html!{ <span class="repo-branch">{format!("({})", branch)}</span> }
} else { html!{} }}
</div>
},
PublicationSourceType::StaticFolder => html! {
<div class="source-detail static-folder-source">
<i class="fas fa-folder"></i>
<span>{"Static Folder"}</span>
{ if let Some(p) = &s.path { html!{<span class="source-path">{p}</span>}} else {html!{}} }
</div>
},
PublicationSourceType::FileAsset => html! {
<div class="source-detail static-asset-source">
<i class="fas fa-file-code"></i>
<span>{s.path.as_deref().unwrap_or("File Asset")}</span>
</div>
},
PublicationSourceType::ExternalLink => html! {
<div class="source-detail external-link-source">
<i class="fas fa-external-link-alt"></i>
<a href={s.url.clone().unwrap_or_default()} target="_blank" onclick={|e: MouseEvent| e.stop_propagation()}>
{s.url.as_ref().and_then(|u| u.split("//").nth(1)).unwrap_or("External Link")}
</a>
</div>
},
PublicationSourceType::NotApplicable => html! {
<div class="source-detail not-applicable-source">
<i class="fas fa-ban"></i>
<span>{"N/A"}</span>
</div>
} // End of PublicationSourceType::NotApplicable arm's html!
}, // End of Some(s) arm
None => html! { <div class="source-detail">{"Source not specified"}</div> }
} // End of match source for render_publication_source
} // End of fn render_publication_source
fn render_publication_card(publication: &Rc<Publication>, link: &yew::html::Scope<PublishingView>) -> Html {
let status_class_name = format!("status-{}", format!("{:?}", publication.status).to_lowercase());
let status_color = get_status_color(&publication.status);
let type_icon = get_publication_type_icon(&publication.publication_type);
let publication_id = publication.id;
let onclick_details = link.callback(move |_| PublishingMsg::SwitchView(PublishingViewEnum::PublicationDetail(publication_id)));
html! {
<div class={classes!("publication-card", status_class_name)} key={publication.id} onclick={onclick_details}>
<div class="publication-cell-info">
<h3 class="publication-name">{&publication.name}</h3>
<p class="publication-description">{publication.description.as_deref().unwrap_or("")}</p>
</div>
<div class="publication-cell-category">
<div class="publication-type-display">
<i class={type_icon}></i>
<span>{format!("{:?}", publication.publication_type)}</span>
</div>
<div class="publication-source-display">
{ render_publication_source(&publication.source) } // This call should be fine now
</div>
</div>
<div class="publication-cell-asset-link">
{ if let Some(url) = &publication.live_url {
html! {
<div class="publication-url-display">
<i class="fas fa-link"></i>
<a href={url.clone()} target="_blank" onclick={|e: MouseEvent| e.stop_propagation()}>
{url.split("//").nth(1).unwrap_or_else(|| url.as_str())}
</a>
</div>
}
} else if let Some(domain) = &publication.custom_domain {
html!{
<div class="publication-url-display">
<i class="fas fa-globe-americas"></i>
<span>{domain}</span>
</div>
}
} else {
html! { <div class="publication-url-display">{"Not available"}</div> }
}}
</div>
<div class="publication-cell-stats">
// Simplified stats or remove if not readily available
{ if let Some(visitors) = publication.monthly_visitors {
html!{ <div class="stat-item"><i class="fas fa-users"></i> {format!("{}k", visitors / 1000)}</div> }
} else {html!{}}}
{ if let Some(uptime) = publication.uptime_percentage {
html!{ <div class="stat-item"><i class="fas fa-heartbeat"></i> {format!("{:.1}%", uptime)}</div> }
} else {html!{}}}
</div>
<div class="publication-cell-deployment">
<div class="publication-status-display" style={format!("color: {}", status_color)}>
<div class="status-indicator" style={format!("background-color: {}", status_color)}></div>
<span>{format!("{:?}", publication.status)}</span>
</div>
<div class="deployment-detail-item">
<span class="detail-label">{"Last Deployed:"}</span>
<span>
{ publication.last_deployed_at.as_ref().map_or_else(
|| "Never".to_string(),
|ts| format_timestamp_string(ts)
)}
</span>
</div>
</div>
</div>
}
} // End of fn render_publication_card
fn render_expanded_publication_card(
publication: &Publication,
link: &yew::html::Scope<PublishingView>,
active_tab: &PublishingPublicationTab,
deployments: &[Rc<Deployment>] // Pass only relevant deployments
) -> Html {
let status_color = get_status_color(&publication.status);
let type_icon = get_publication_type_icon(&publication.publication_type);
html! {
<div class="card-base expanded-publication-card" key={publication.id.clone()}>
<div class="card-header expanded-card-header">
<div class="expanded-header-top">
<button
class="back-btn"
onclick={link.callback(|_| PublishingMsg::SwitchView(PublishingViewEnum::PublicationsList))}
>
<i class="fas fa-arrow-left"></i>
<span>{"Back to Publications"}</span>
</button>
<div class="publication-header">
<div class="publication-type">
<i class={type_icon}></i>
<span>{format!("{:?}", publication.publication_type)}</span>
</div>
<div class="publication-status" style={format!("color: {}", status_color)}>
<div class="status-indicator" style={format!("background-color: {}", status_color)}></div>
<span>{format!("{:?}", publication.status)}</span>
</div>
</div>
</div>
<div class="expanded-card-title">
<h2 class="card-title">{&publication.name}</h2>
<p class="expanded-description">{publication.description.as_deref().unwrap_or("")}</p>
{ if let Some(url) = &publication.live_url {
html! {
<div class="publication-url">
<i class="fas fa-external-link-alt"></i>
<a href={url.clone()} target="_blank">{url}</a>
</div>
}
} else if let Some(domain) = &publication.custom_domain {
html! { <div class="publication-url"><i class="fas fa-globe-americas"></i> {domain}</div> }
} else { html! {} }}
</div>
<div class="expanded-card-tabs">
{ render_publication_tab_button(link, PublishingPublicationTab::Overview, active_tab, "fas fa-home", "Overview") }
{ render_publication_tab_button(link, PublishingPublicationTab::Analytics, active_tab, "fas fa-chart-line", "Analytics") }
{ render_publication_tab_button(link, PublishingPublicationTab::Deployments, active_tab, "fas fa-rocket", "Deployments") }
{ render_publication_tab_button(link, PublishingPublicationTab::Settings, active_tab, "fas fa-cog", "Settings") }
</div>
</div>
<div class="expanded-card-content">
{
match active_tab {
PublishingPublicationTab::Overview => render_expanded_publication_overview(publication, deployments),
PublishingPublicationTab::Analytics => render_publication_analytics(publication),
PublishingPublicationTab::Deployments => render_publication_deployments_tab(publication, deployments, link),
PublishingPublicationTab::Settings => render_publication_settings(publication, link),
}
}
</div>
</div>
}
}
fn render_expanded_publication_overview(publication: &Publication, deployments: &[Rc<Deployment>]) -> Html {
let recent_deployments: Vec<&Rc<Deployment>> = deployments.iter().take(3).collect();
html! {
<div class="expanded-overview">
<div class="overview-sections">
<div class="overview-section">
<h3>{"Live Metrics"}</h3>
<div class="live-metrics">
// Simplified metrics - data comes from publication model
<div class="card-base metric-card">
<div class="metric-icon visitors"><i class="fas fa-users"></i></div>
<div class="metric-info">
<span class="metric-value">{publication.monthly_visitors.map_or("N/A".to_string(), |v| format!("{}k", v/1000))}</span>
<span class="metric-label">{"Monthly Visitors"}</span>
</div>
</div>
<div class="card-base metric-card">
<div class="metric-icon uptime"><i class="fas fa-heartbeat"></i></div>
<div class="metric-info">
<span class="metric-value">{publication.uptime_percentage.map_or("N/A".to_string(), |u| format!("{:.1}%", u))}</span>
<span class="metric-label">{"Uptime"}</span>
</div>
</div>
<div class="card-base metric-card">
<div class="metric-icon performance"><i class="fas fa-tachometer-alt"></i></div>
<div class="metric-info">
<span class="metric-value">{publication.average_build_time_seconds.map_or("N/A".to_string(), |s| format!("{}s", s))}</span>
<span class="metric-label">{"Avg Build"}</span>
</div>
</div>
<div class="card-base metric-card">
<div class="metric-icon size"><i class="fas fa-hdd"></i></div>
<div class="metric-info">
<span class="metric-value">{publication.average_size_mb.map_or("N/A".to_string(), |s| format!("{:.1}MB", s))}</span>
<span class="metric-label">{"Avg Size"}</span>
</div>
</div>
</div>
</div>
<div class="overview-section">
<h3>{"Recent Activity"}</h3>
<div class="recent-deployments">
{ if !recent_deployments.is_empty() {
html! { for recent_deployments.iter().map(|deployment| render_compact_deployment_item(deployment)) }
} else {
html! { <div class="no-deployments"><i class="fas fa-rocket"></i><span>{"No recent deployments"}</span></div> }
}}
</div>
</div>
<div class="overview-section">
<h3>{"Configuration Summary"}</h3>
<div class="config-summary">
<div class="config-item"><span class="config-label">{"Source:"}</span> <span class="config-value">{render_publication_source(&publication.source)}</span></div>
<div class="config-item"><span class="config-label">{"Build Cmd:"}</span> <span class="config-value">{publication.build_command.as_deref().unwrap_or("N/A")}</span></div>
<div class="config-item"><span class="config-label">{"Output Dir:"}</span> <span class="config-value">{publication.output_directory.as_deref().unwrap_or("N/A")}</span></div>
<div class="config-item"><span class="config-label">{"SSL:"}</span> <span class="config-value">{publication.ssl_enabled.map_or("N/A", |s| if s {"Enabled"} else {"Disabled"}).to_string()}</span></div>
<div class="config-item"><span class="config-label">{"Custom Domain:"}</span> <span class="config-value">{publication.custom_domain.as_deref().unwrap_or("None")}</span></div>
</div>
</div>
</div>
</div>
}
}
fn render_compact_deployment_item(deployment: &Deployment) -> Html {
let status_color = get_status_color(&deployment.status);
html! {
<div class="compact-deployment">
<div class="deployment-status">
<div class="status-indicator" style={format!("background-color: {}", status_color)}></div>
</div>
<div class="deployment-info">
<div class="commit-message">{deployment.commit_message.as_deref().unwrap_or("Deployment")}</div>
<div class="deployment-meta">
{if let Some(hash) = &deployment.commit_hash {
html!{<span class="commit-hash">{"#"}{hash.chars().take(8).collect::<String>()}</span>}
} else {html!{}}}
<span class="deploy-time">{format_timestamp_string(&deployment.deployed_at)}</span>
</div>
</div>
</div>
}
}
fn render_publication_analytics(_publication: &Publication) -> Html {
html! {
<div class="publication-analytics">
<div class="analytics-grid"> /* Placeholder for actual charts/data */ </div>
<div class="analytics-charts">
<div class="card-base chart-card">
<h3 class="card-title">{"Traffic Overview"}</h3>
<div class="card-content"><p>{"Detailed analytics coming soon..."}</p></div>
</div>
<div class="card-base chart-card">
<h3 class="card-title">{"Performance Metrics"}</h3>
<div class="card-content"><p>{"Performance monitoring coming soon..."}</p></div>
</div>
</div>
</div>
}
}
fn render_publication_deployments_tab(_publication: &Publication, deployments: &[Rc<Deployment>], link: &yew::html::Scope<PublishingView>) -> Html {
let publication_id = _publication.id;
html! {
<div class="publication-deployments-tab">
<div class="deployments-header">
<h3>{"Deployment History"}</h3>
<button class="action-btn primary" onclick={link.callback(move |_| PublishingMsg::TriggerDeployment(publication_id))}>
<i class="fas fa-rocket"></i>
<span>{"Deploy Now"}</span>
</button>
</div>
<div class="deployments-list">
{ if deployments.is_empty() {
html! {
<div class="empty-state">
<i class="fas fa-rocket"></i>
<h4>{"No deployments yet"}</h4>
<p>{"Deploy your publication to see deployment history here."}</p>
</div>
}
} else {
html! { for deployments.iter().map(|deployment| render_full_deployment_item(deployment)) }
}}
</div>
</div>
}
}
fn render_full_deployment_item(deployment: &Deployment) -> Html {
let status_color = get_status_color(&deployment.status);
html! {
<div class="deployment-item full-deployment-item" key={deployment.id.clone()}>
<div class="deployment-status">
<div class="status-indicator" style={format!("background-color: {}", status_color)}></div>
<span>{format!("{:?}", deployment.status)}</span>
</div>
<div class="deployment-content">
<div class="deployment-header">
<h4>{deployment.commit_message.as_deref().unwrap_or("Deployment Event")}</h4>
{if let Some(branch) = &deployment.branch {
html!{<span class="deployment-branch">{branch}</span>}
} else {html!{}}}
</div>
<div class="deployment-meta">
{if let Some(hash) = &deployment.commit_hash {
html!{<span class="commit-hash">{"Commit: #"}{hash.chars().take(8).collect::<String>()}</span>}
} else {html!{}}}
<span class="deploy-time">{"Deployed: "}{format_timestamp_string(&deployment.deployed_at)}</span>
{if let Some(duration) = deployment.build_duration_seconds {
html!{<span class="build-duration">{"Build: "}{format!("{}s", duration)}</span>}
} else {html!{}}}
{if let Some(user_id) = &deployment.deployed_by_user_id {
html!{<span class="deployed-by">{"By: "}{user_id}</span>} // Ideally resolve to user name
} else {html!{}}}
</div>
</div>
{if let Some(log_url) = &deployment.deploy_log_url {
html! {
<div class="deployment-actions">
<a href={log_url.clone()} target="_blank" class="deploy-link action-btn-secondary">
<i class="fas fa-file-alt"></i> {" View Logs"}
</a>
</div>
}
} else {html!{}}}
</div>
}
}
fn render_publication_settings(publication: &Publication, link: &yew::html::Scope<PublishingView>) -> Html {
let publication_id = publication.id;
html! {
<div class="publication-settings">
<div class="settings-section">
<h3>{"Build & Deploy Settings"}</h3>
<div class="settings-grid">
<div class="setting-item">
<label for={format!("build-cmd-{}", publication.id)}>{"Build Command"}</label>
<input id={format!("build-cmd-{}", publication.id)} type="text" class="input-base" value={publication.build_command.clone().unwrap_or_default()} placeholder="e.g., npm run build" />
</div>
<div class="setting-item">
<label for={format!("output-dir-{}", publication.id)}>{"Output Directory"}</label>
<input id={format!("output-dir-{}", publication.id)} type="text" class="input-base" value={publication.output_directory.clone().unwrap_or_default()} placeholder="e.g., dist, public" />
</div>
{if let Some(source) = &publication.source {
if source.source_type == PublicationSourceType::GitRepository {
html!{
<>
<div class="setting-item">
<label for={format!("repo-url-{}", publication.id)}>{"Repository URL"}</label>
<input id={format!("repo-url-{}", publication.id)} type="text" class="input-base" value={source.url.clone().unwrap_or_default()} />
</div>
<div class="setting-item">
<label for={format!("repo-branch-{}", publication.id)}>{"Deploy Branch"}</label>
<input id={format!("repo-branch-{}", publication.id)} type="text" class="input-base" value={source.repo_branch.clone().unwrap_or_default()} placeholder="e.g., main, master" />
</div>
</>
}
} else {html!{}}
} else {html!{}}}
</div>
</div>
<div class="settings-section">
<h3>{"Domain Management"}</h3>
<div class="settings-grid">
<div class="setting-item">
<label for={format!("custom-domain-{}", publication.id)}>{"Custom Domain"}</label>
<input id={format!("custom-domain-{}", publication.id)} type="text" class="input-base" value={publication.custom_domain.clone().unwrap_or_default()} placeholder="e.g., myapp.example.com" />
</div>
<div class="setting-item toggle-item">
<label for={format!("ssl-enabled-{}", publication.id)}>{"SSL Enabled"}</label>
<input id={format!("ssl-enabled-{}", publication.id)} type="checkbox" checked={publication.ssl_enabled.unwrap_or(false)} />
</div>
</div>
</div>
<div class="settings-section">
<h3>{"Danger Zone"}</h3>
<div class="danger-actions">
<button class="action-btn danger" onclick={link.callback(move |_| PublishingMsg::DeletePublication(publication_id))}>
<i class="fas fa-trash"></i>
<span>{"Delete Publication"}</span>
</button>
</div>
</div>
<button class="action-btn primary save-settings-btn" onclick={link.callback(move |_| PublishingMsg::SavePublicationSettings(publication_id))}>{"Save Settings"}</button>
</div>
}
}
fn get_status_color(status: &PublicationStatus) -> &'static str {
match status {
PublicationStatus::Deployed => "var(--success-color, #28a745)",
PublicationStatus::Building => "var(--warning-color, #ffc107)",
PublicationStatus::Failed => "var(--danger-color, #dc3545)",
PublicationStatus::Draft => "var(--info-color, #6c757d)",
PublicationStatus::Maintenance => "var(--primary-color, #007bff)",
PublicationStatus::Archived => "var(--muted-color, #adb5bd)",
}
}
fn get_publication_type_icon(pub_type: &PublicationType) -> &'static str {
match pub_type {
PublicationType::Website => "fas fa-globe",
PublicationType::WebApp => "fas fa-desktop",
PublicationType::StaticAsset => "fas fa-file-archive", // Changed for better distinction
PublicationType::Server => "fas fa-server",
PublicationType::API => "fas fa-plug",
PublicationType::Database => "fas fa-database",
PublicationType::CDN => "fas fa-cloud-upload-alt", // Changed for better distinction
PublicationType::Other => "fas fa-cube",
}
}
// Mock data fetcher functions
fn get_mock_publications() -> Vec<Rc<Publication>> {
// TODO: Replace with actual data fetching logic or more realistic mock data
vec![]
}
fn get_mock_deployments() -> Vec<Rc<Deployment>> {
// TODO: Replace with actual data fetching logic or more realistic mock data
vec![]
}
fn format_timestamp_string(timestamp_str: &str) -> String {
match DateTime::parse_from_rfc3339(timestamp_str) {
Ok(dt) => dt.with_timezone(&Utc).format("%b %d, %Y %H:%M UTC").to_string(),
Err(_) => timestamp_str.to_string(), // Fallback if parsing fails
}
}

View File

@ -0,0 +1,40 @@
use yew::prelude::*;
#[derive(Clone, PartialEq, Properties)]
pub struct SidebarLayoutProps {
pub sidebar_content: Html,
pub main_content: Html,
#[prop_or_default]
pub on_background_click: Option<Callback<()>>,
}
#[function_component(SidebarLayout)]
pub fn sidebar_layout(props: &SidebarLayoutProps) -> Html {
let on_background_click_handler = {
let on_background_click = props.on_background_click.clone();
Callback::from(move |e: MouseEvent| {
if let Some(callback) = &on_background_click {
callback.emit(());
}
})
};
let on_sidebar_click = Callback::from(|e: MouseEvent| {
e.stop_propagation();
});
let on_main_click = Callback::from(|e: MouseEvent| {
e.stop_propagation();
});
html! {
<div class="sidebar-layout" onclick={on_background_click_handler}>
<div class="sidebar" onclick={on_sidebar_click}>
{ props.sidebar_content.clone() }
</div>
<div class="main" onclick={on_main_click}>
{ props.main_content.clone() }
</div>
</div>
}
}

View File

@ -0,0 +1,136 @@
use yew::prelude::*;
use heromodels::models::library::items::Slides;
#[derive(Clone, PartialEq, Properties)]
pub struct SlidesViewerProps {
pub slides: Slides,
pub on_back: Callback<()>,
}
pub enum SlidesViewerMsg {
GoToSlide(usize),
NextSlide,
PrevSlide,
}
pub struct SlidesViewer {
current_slide_index: usize,
}
impl Component for SlidesViewer {
type Message = SlidesViewerMsg;
type Properties = SlidesViewerProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
current_slide_index: 0,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
SlidesViewerMsg::GoToSlide(slide) => {
self.current_slide_index = slide;
true
}
SlidesViewerMsg::NextSlide => {
let props = _ctx.props();
if self.current_slide_index < props.slides.slide_urls.len().saturating_sub(1) {
self.current_slide_index += 1;
}
true
}
SlidesViewerMsg::PrevSlide => {
if self.current_slide_index > 0 {
self.current_slide_index -= 1;
}
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let total_slides = props.slides.slide_urls.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| SlidesViewerMsg::PrevSlide);
let next_handler = ctx.link().callback(|_: MouseEvent| SlidesViewerMsg::NextSlide);
html! {
<div class="asset-viewer slides-viewer">
<button class="back-button" onclick={back_handler}>
<i class="fas fa-arrow-left"></i> {"Back to Collection"}
</button>
<div class="viewer-header">
<h2 class="viewer-title">{ &props.slides.title }</h2>
<div class="slides-navigation">
<button
class="nav-button"
onclick={prev_handler}
disabled={self.current_slide_index == 0}
>
<i class="fas fa-chevron-left"></i> {"Previous"}
</button>
<span class="slide-indicator">
{ format!("Slide {} of {}", self.current_slide_index + 1, total_slides) }
</span>
<button
class="nav-button"
onclick={next_handler}
disabled={self.current_slide_index >= total_slides.saturating_sub(1)}
>
{"Next"} <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div class="viewer-content">
<div class="slide-container">
{ if let Some(slide_url) = props.slides.slide_urls.get(self.current_slide_index) {
html! {
<div class="slide">
<img
src={slide_url.clone()}
alt={
props.slides.slide_titles.get(self.current_slide_index)
.and_then(|t| t.as_ref())
.unwrap_or(&format!("Slide {}", self.current_slide_index + 1))
.clone()
}
class="slide-image"
/>
{ if let Some(Some(title)) = props.slides.slide_titles.get(self.current_slide_index) {
html! { <div class="slide-title">{ title }</div> }
} else { html! {} }}
</div>
}
} else {
html! { <p>{"Slide not found"}</p> }
}}
</div>
<div class="slide-thumbnails">
{ props.slides.slide_urls.iter().enumerate().map(|(index, url)| {
let is_current = index == self.current_slide_index;
let onclick = ctx.link().callback(move |_: MouseEvent| SlidesViewerMsg::GoToSlide(index));
html! {
<div
class={classes!("slide-thumbnail", if is_current { "active" } else { "" })}
onclick={onclick}
>
<img src={url.clone()} alt={format!("Slide {}", index + 1)} />
<span class="thumbnail-number">{ index + 1 }</span>
</div>
}
}).collect::<Html>() }
</div>
</div>
</div>
}
}
}

File diff suppressed because one or more lines are too long

22
src/app/src/lib.rs Normal file
View File

@ -0,0 +1,22 @@
use wasm_bindgen::prelude::*; // Import wasm_bindgen
mod app;
mod auth; // Declares the authentication module
mod components; // Declares the components module
mod rhai_executor; // Declares the rhai_executor module
mod ws_manager; // Declares the WebSocket manager module
// This function is called when the WASM module is loaded.
#[wasm_bindgen(start)]
pub fn run_app() {
// Initialize wasm_logger. This allows use of `log::info!`, `log::warn!`, etc.
wasm_logger::init(wasm_logger::Config::default());
log::info!("App is starting"); // Example log
// Mount the Yew app to the document body with WebSocket URL
let props = app::AppProps {
start_circle_ws_url: "ws://localhost:9000/ws".to_string(), // Default WebSocket URL
};
yew::Renderer::<app::App>::with_props(props).render();
}

View File

@ -0,0 +1,165 @@
use rhai::Engine;
use engine::{create_heromodels_engine, mock_db::{create_mock_db, seed_mock_db}, eval_script};
use circle_client_ws::{CircleWsClient, CircleWsClientBuilder};
// Since we're in a WASM environment, we need to handle the database differently
// We'll create a mock database that works in WASM
pub struct RhaiExecutor {
engine: Engine,
}
impl RhaiExecutor {
pub fn new() -> Self {
// Create a mock database for the engine
let db = create_mock_db();
seed_mock_db(db.clone());
// Create the heromodels engine with all the registered functions
let engine = create_heromodels_engine(db);
Self { engine }
}
pub fn execute_script(&self, script: &str) -> Result<String, String> {
if script.trim().is_empty() {
return Err("Script cannot be empty".to_string());
}
match eval_script(&self.engine, script) {
Ok(result) => {
let output = if result.is_unit() {
"Script executed successfully (no return value)".to_string()
} else {
format!("Result: {}", result)
};
Ok(output)
}
Err(err) => {
Err(format!("Rhai execution error: {}", err))
}
}
}
}
impl Default for RhaiExecutor {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct ScriptResponse {
pub output: String,
pub success: bool,
pub source: String,
}
// For local execution (self circle)
pub fn execute_rhai_script_local(script: &str) -> ScriptResponse {
let executor = RhaiExecutor::new();
match executor.execute_script(script) {
Ok(output) => ScriptResponse {
output,
success: true,
source: "Local (My Space)".to_string(),
},
Err(error) => ScriptResponse {
output: error,
success: false,
source: "Local (My Space)".to_string(),
},
}
}
// For remote execution (other circles via WebSocket)
pub async fn execute_rhai_script_remote(script: &str, ws_url: &str, source_name: &str) -> ScriptResponse {
if script.trim().is_empty() {
return ScriptResponse {
output: "Error: Script cannot be empty".to_string(),
success: false,
source: source_name.to_string(),
};
}
let mut client = CircleWsClientBuilder::new(ws_url.to_string()).build();
// Connect to the WebSocket
match client.connect().await {
Ok(_) => {
// Send the script for execution
match client.play(script.to_string()).await {
Ok(result) => {
// Disconnect after execution
client.disconnect().await;
ScriptResponse {
output: result.output,
success: true,
source: source_name.to_string(),
}
}
Err(e) => {
client.disconnect().await;
ScriptResponse {
output: format!("Remote execution error: {}", e),
success: false,
source: source_name.to_string(),
}
}
}
}
Err(e) => {
ScriptResponse {
output: format!("Connection error: {}", e),
success: false,
source: source_name.to_string(),
}
}
}
}
// Broadcast script to all WebSocket URLs and return all responses
pub async fn broadcast_rhai_script(script: &str, ws_urls: &[String]) -> Vec<ScriptResponse> {
let mut responses = Vec::new();
// Add local execution first
// responses.push(execute_rhai_script_local(script));
// Execute on all remote circles
for (index, ws_url) in ws_urls.iter().enumerate() {
let source_name = format!("Circle {}", index + 1);
let response = execute_rhai_script_remote(script, ws_url, &source_name).await;
responses.push(response);
}
responses
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_script_execution() {
let executor = RhaiExecutor::new();
// Test simple arithmetic
let result = executor.execute_script("2 + 3");
assert!(result.is_ok());
assert!(result.unwrap().contains("5"));
// Test variable assignment
let result = executor.execute_script("let x = 10; x * 2");
assert!(result.is_ok());
assert!(result.unwrap().contains("20"));
}
#[test]
fn test_empty_script() {
let executor = RhaiExecutor::new();
let result = executor.execute_script("");
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
}

354
src/app/src/ws_manager.rs Normal file
View File

@ -0,0 +1,354 @@
use std::collections::HashMap;
use circle_client_ws::{CircleWsClient, CircleWsClientBuilder, CircleWsClientError, PlayResultClient};
use log::{info, error, warn};
use serde::de::DeserializeOwned;
use std::rc::Rc;
use std::cell::RefCell;
use wasm_bindgen_futures::spawn_local;
use yew::Callback;
use heromodels::models::circle::Circle;
use crate::auth::AuthManager;
/// Type alias for Circle-specific WebSocket manager
pub type CircleWsManager = WsManager<Circle>;
/// Manages multiple WebSocket connections to servers
/// Generic over the data type T that will be fetched and deserialized
#[derive(Clone)]
pub struct WsManager<T>
where
T: DeserializeOwned + Clone + 'static,
{
/// Map of WebSocket URL to client
clients: Rc<RefCell<HashMap<String, CircleWsClient>>>,
/// Callback to notify when data is fetched
on_data_fetched: Rc<RefCell<Option<Callback<(String, Result<T, String>)>>>>,
/// Optional authentication manager
auth_manager: Option<AuthManager>,
}
impl<T> WsManager<T>
where
T: DeserializeOwned + Clone + 'static,
{
pub fn new() -> Self {
Self {
clients: Rc::new(RefCell::new(HashMap::new())),
on_data_fetched: Rc::new(RefCell::new(None)),
auth_manager: None,
}
}
/// Create a new WsManager with authentication support
pub fn new_with_auth(auth_manager: AuthManager) -> Self {
Self {
clients: Rc::new(RefCell::new(HashMap::new())),
on_data_fetched: Rc::new(RefCell::new(None)),
auth_manager: Some(auth_manager),
}
}
/// Set the authentication manager
pub fn set_auth_manager(&mut self, auth_manager: AuthManager) {
self.auth_manager = Some(auth_manager);
}
/// Check if authentication is enabled
pub fn has_auth(&self) -> bool {
self.auth_manager.is_some()
}
/// Check if currently authenticated
pub fn is_authenticated(&self) -> bool {
self.auth_manager.as_ref().map_or(false, |auth| auth.is_authenticated())
}
/// Set callback for when data is fetched
pub fn set_on_data_fetched(&self, callback: Callback<(String, Result<T, String>)>) {
*self.on_data_fetched.borrow_mut() = Some(callback);
}
/// Connect to a WebSocket server
pub async fn connect(&self, ws_url: String) -> Result<(), CircleWsClientError> {
if self.clients.borrow().contains_key(&ws_url) {
info!("Already connected to {}", ws_url);
return Ok(());
}
let client = if let Some(auth_manager) = &self.auth_manager {
if auth_manager.is_authenticated() {
auth_manager.create_authenticated_client(&ws_url).await?
} else {
let mut client = CircleWsClientBuilder::new(ws_url.clone()).build();
client.connect().await?;
client
}
} else {
let mut client = CircleWsClientBuilder::new(ws_url.clone()).build();
client.connect().await?;
client
};
info!("Connected to WebSocket: {}", ws_url);
self.clients.borrow_mut().insert(ws_url, client);
Ok(())
}
/// Connect to a WebSocket server with explicit authentication
pub async fn connect_with_auth(&self, ws_url: String, force_auth: bool) -> Result<(), CircleWsClientError> {
if self.clients.borrow().contains_key(&ws_url) {
info!("Already connected to {}", ws_url);
return Ok(());
}
let client = if force_auth {
if let Some(auth_manager) = &self.auth_manager {
if auth_manager.is_authenticated() {
auth_manager.create_authenticated_client(&ws_url).await?
} else {
return Err(CircleWsClientError::ConnectionError("Authentication required but not authenticated".to_string()));
}
} else {
return Err(CircleWsClientError::ConnectionError("Authentication required but no auth manager available".to_string()));
}
} else {
let mut client = CircleWsClientBuilder::new(ws_url.clone()).build();
client.connect().await?;
client
};
info!("Connected to WebSocket with auth: {}", ws_url);
self.clients.borrow_mut().insert(ws_url, client);
Ok(())
}
/// Fetch data from a WebSocket server using the provided script
pub fn fetch_data(&self, ws_url: &str, script: String) {
// Check if client exists without cloning
let has_client = self.clients.borrow().contains_key(ws_url);
if has_client {
let ws_url_clone = ws_url.to_string();
let callback = self.on_data_fetched.borrow().clone();
let clients = self.clients.clone();
spawn_local(async move {
// Get the client inside the async block
let play_future = {
let clients_borrow = clients.borrow();
if let Some(client) = clients_borrow.get(&ws_url_clone) {
Some(client.play(script))
} else {
None
}
};
if let Some(future) = play_future {
match future.await {
Ok(result) => {
info!("Received data from {}: {}", ws_url_clone, result.output);
// Parse the JSON response to extract data
match serde_json::from_str::<T>(&result.output) {
Ok(data) => {
if let Some(cb) = callback {
cb.emit((ws_url_clone.clone(), Ok(data)));
}
}
Err(e) => {
error!("Failed to parse data from {}: {}", ws_url_clone, e);
if let Some(cb) = callback {
cb.emit((ws_url_clone.clone(), Err(format!("Failed to parse data: {}", e))));
}
}
}
}
Err(e) => {
error!("Failed to fetch data from {}: {:?}", ws_url_clone, e);
if let Some(cb) = callback {
cb.emit((ws_url_clone.clone(), Err(format!("WebSocket error: {:?}", e))));
}
}
}
}
});
} else {
warn!("No client found for WebSocket URL: {}", ws_url);
if let Some(cb) = &*self.on_data_fetched.borrow() {
cb.emit((ws_url.to_string(), Err(format!("No connection to {}", ws_url))));
}
}
}
/// Execute a Rhai script on a specific server
pub fn execute_script(&self, ws_url: &str, script: String) -> Option<impl std::future::Future<Output = Result<PlayResultClient, CircleWsClientError>>> {
let clients = self.clients.clone();
let ws_url = ws_url.to_string();
if clients.borrow().contains_key(&ws_url) {
Some(async move {
let clients_borrow = clients.borrow();
if let Some(client) = clients_borrow.get(&ws_url) {
client.play(script).await
} else {
Err(CircleWsClientError::NotConnected)
}
})
} else {
None
}
}
/// Get all connected WebSocket URLs
pub fn get_connected_urls(&self) -> Vec<String> {
self.clients.borrow().keys().cloned().collect()
}
/// Disconnect from a specific WebSocket
pub async fn disconnect(&self, ws_url: &str) {
if let Some(mut client) = self.clients.borrow_mut().remove(ws_url) {
client.disconnect().await;
info!("Disconnected from WebSocket: {}", ws_url);
}
}
/// Disconnect from all WebSockets
pub async fn disconnect_all(&self) {
let mut clients = self.clients.borrow_mut();
for (ws_url, mut client) in clients.drain() {
client.disconnect().await;
info!("Disconnected from WebSocket: {}", ws_url);
}
}
}
impl<T> Drop for WsManager<T>
where
T: DeserializeOwned + Clone + 'static,
{
fn drop(&mut self) {
// Note: We can't call async disconnect_all in drop, but the clients
// should handle cleanup in their own Drop implementations
info!("WsManager dropped");
}
}
/// Fetch data from multiple WebSocket servers
/// Generic function that can fetch any deserializable type T
pub async fn fetch_data_from_ws_urls<T>(ws_urls: &[String], script: String) -> HashMap<String, T>
where
T: DeserializeOwned + Clone,
{
let mut results = HashMap::new();
for ws_url in ws_urls {
match fetch_data_from_ws_url::<T>(ws_url, &script).await {
Ok(data) => {
results.insert(ws_url.clone(), data);
}
Err(e) => {
error!("Failed to fetch data from {}: {}", ws_url, e);
}
}
}
results
}
/// Fetch data from a single WebSocket server
pub async fn fetch_data_from_ws_url<T>(ws_url: &str, script: &str) -> Result<T, String>
where
T: DeserializeOwned,
{
let mut client = CircleWsClientBuilder::new(ws_url.to_string()).build();
// Connect to the WebSocket
client.connect().await
.map_err(|e| format!("Failed to connect to {}: {:?}", ws_url, e))?;
info!("Connected to WebSocket: {}", ws_url);
// Execute the script
let result = client.play(script.to_string()).await
.map_err(|e| format!("Failed to execute script on {}: {:?}", ws_url, e))?;
info!("Received data from {}: {}", ws_url, result.output);
// Parse the JSON response
let data: T = serde_json::from_str(&result.output)
.map_err(|e| format!("Failed to parse data from {}: {}", ws_url, e))?;
// Disconnect
client.disconnect().await;
info!("Disconnected from WebSocket: {}", ws_url);
Ok(data)
}
/// Fetch data from a single WebSocket server with authentication
pub async fn fetch_data_from_ws_url_with_auth<T>(
ws_url: &str,
script: &str,
auth_manager: &AuthManager,
) -> Result<T, String>
where
T: DeserializeOwned,
{
let mut client = if auth_manager.is_authenticated() {
auth_manager
.create_authenticated_client(ws_url)
.await
.map_err(|e| format!("Authentication failed: {}", e))?
} else {
let mut client = CircleWsClientBuilder::new(ws_url.to_string()).build();
client
.connect()
.await
.map_err(|e| format!("Connection failed: {}", e))?;
client
};
info!("Connected to WebSocket: {}", ws_url);
// Execute the script
let result = client
.play(script.to_string())
.await
.map_err(|e| format!("Failed to execute script on {}: {:?}", ws_url, e))?;
info!("Received data from {}: {}", ws_url, result.output);
// Parse the JSON response
let data: T = serde_json::from_str(&result.output)
.map_err(|e| format!("Failed to parse data from {}: {}", ws_url, e))?;
// Disconnect
client.disconnect().await;
info!("Disconnected from WebSocket: {}", ws_url);
Ok(data)
}
/// Fetch data from multiple WebSocket servers with authentication
pub async fn fetch_data_from_ws_urls_with_auth<T>(
ws_urls: &[String],
script: String,
auth_manager: &AuthManager
) -> HashMap<String, T>
where
T: DeserializeOwned + Clone,
{
let mut results = HashMap::new();
for ws_url in ws_urls {
match fetch_data_from_ws_url_with_auth::<T>(ws_url, &script, auth_manager).await {
Ok(data) => {
results.insert(ws_url.clone(), data);
}
Err(e) => {
error!("Failed to fetch data from {}: {}", ws_url, e);
}
}
}
results
}

719
src/app/static/css/auth.css Normal file
View File

@ -0,0 +1,719 @@
/* Authentication Component Styles */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 40px;
width: 100%;
max-width: 450px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-title {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 24px;
font-weight: 600;
}
/* Method Selector */
.login-method-selector {
margin-bottom: 30px;
}
.method-tabs {
display: flex;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e1e5e9;
}
.method-tab {
flex: 1;
padding: 12px 16px;
background: #f8f9fa;
border: none;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 500;
color: #6c757d;
}
.method-tab:hover {
background: #e9ecef;
}
.method-tab.active {
background: #007bff;
color: white;
}
.method-tab:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.method-tab i {
margin-right: 8px;
}
/* Form Styles */
.login-form {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.form-input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.form-help {
display: block;
margin-top: 6px;
font-size: 12px;
color: #6c757d;
line-height: 1.4;
}
/* Email Input Container */
.email-input-container {
position: relative;
display: flex;
}
.email-input-container .form-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
.email-dropdown-btn {
padding: 12px;
background: #f8f9fa;
border: 1px solid #ddd;
border-left: none;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
color: #6c757d;
}
.email-dropdown-btn:hover {
background: #e9ecef;
}
.email-dropdown-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Email Dropdown */
.email-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
margin-top: 4px;
animation: dropdownSlide 0.2s ease-out;
}
@keyframes dropdownSlide {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.dropdown-header span {
font-weight: 500;
color: #333;
font-size: 14px;
}
.dropdown-close {
background: none;
border: none;
cursor: pointer;
color: #6c757d;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.dropdown-close:hover {
background: #e9ecef;
}
.dropdown-list {
max-height: 200px;
overflow-y: auto;
}
.dropdown-item {
width: 100%;
padding: 12px 16px;
background: none;
border: none;
text-align: left;
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
font-size: 14px;
color: #333;
}
.dropdown-item:hover {
background: #f8f9fa;
}
.dropdown-item i {
margin-right: 12px;
color: #6c757d;
}
/* Login Button */
.login-btn {
width: 100%;
padding: 14px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.login-btn:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
.login-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
/* Error Message */
.error-message {
background: #f8d7da;
color: #721c24;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #f5c6cb;
margin-bottom: 20px;
display: flex;
align-items: center;
font-size: 14px;
}
.error-message i {
margin-right: 8px;
color: #dc3545;
}
/* Loading Indicator */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #6c757d;
font-size: 14px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #e9ecef;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Authenticated View */
.authenticated-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.authenticated-card {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 40px;
width: 100%;
max-width: 450px;
text-align: center;
animation: slideIn 0.3s ease-out;
}
.auth-success-icon {
margin-bottom: 20px;
}
.auth-success-icon i {
font-size: 48px;
color: #28a745;
}
.authenticated-card h3 {
color: #333;
margin-bottom: 30px;
font-size: 24px;
font-weight: 600;
}
.auth-details {
margin-bottom: 30px;
text-align: left;
}
.auth-detail {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #e9ecef;
}
.auth-detail:last-child {
border-bottom: none;
}
.auth-detail label {
font-weight: 500;
color: #333;
font-size: 14px;
}
.auth-detail span {
color: #6c757d;
font-size: 14px;
font-family: monospace;
}
.public-key {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logout-btn {
width: 100%;
padding: 12px;
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.logout-btn:hover {
background: #c82333;
}
/* Responsive Design */
@media (max-width: 480px) {
.login-container,
.authenticated-container {
padding: 10px;
}
.login-card,
.authenticated-card {
padding: 30px 20px;
}
.login-title,
.authenticated-card h3 {
font-size: 20px;
}
.method-tab {
padding: 10px 12px;
font-size: 13px;
}
.auth-detail {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.public-key {
max-width: 100%;
}
}
.auth-info {
display: flex;
align-items: center;
gap: 12px;
}
.auth-method,
.auth-status {
font-size: 14px;
color: #333;
font-weight: 500;
}
.auth-method {
color: #28a745;
}
.auth-status {
color: #6c757d;
}
.logout-button,
.login-button {
background: none;
border: 1px solid #dc3545;
color: #dc3545;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.login-button {
border-color: #007bff;
color: #007bff;
}
.logout-button:hover {
background: #dc3545;
color: white;
}
.login-button:hover {
background: #007bff;
color: white;
}
.logout-button i,
.login-button i {
font-size: 12px;
}
/* Create Key Form Styles */
.create-key-form {
margin-bottom: 20px;
}
.create-key-form h3 {
color: #333;
margin-bottom: 12px;
font-size: 18px;
font-weight: 600;
}
.generate-key-btn {
width: 100%;
padding: 14px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.generate-key-btn:hover:not(:disabled) {
background: #218838;
transform: translateY(-1px);
}
.generate-key-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
/* Generated Keys Display */
.generated-keys {
margin-top: 24px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.key-section {
margin-bottom: 20px;
}
.key-section:last-of-type {
margin-bottom: 24px;
}
.key-section label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.key-display {
display: flex;
gap: 8px;
}
.key-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 12px;
font-family: 'Courier New', monospace;
background: white;
color: #333;
word-break: break-all;
}
.copy-btn {
padding: 10px 12px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn:hover {
background: #0056b3;
}
.key-warning {
display: block;
margin-top: 8px;
font-size: 12px;
color: #dc3545;
line-height: 1.4;
font-weight: 500;
}
.key-warning i {
margin-right: 4px;
}
.key-info {
display: block;
margin-top: 8px;
font-size: 12px;
color: #6c757d;
line-height: 1.4;
}
/* Key Actions */
.key-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.use-key-btn {
flex: 1;
min-width: 140px;
padding: 12px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.use-key-btn:hover {
background: #0056b3;
}
.generate-new-btn {
flex: 1;
min-width: 140px;
padding: 12px 16px;
background: #6c757d;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.generate-new-btn:hover {
background: #5a6268;
}
/* Copy Feedback */
.copy-feedback {
margin-top: 12px;
padding: 8px 12px;
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 6px;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive adjustments for create key form */
@media (max-width: 480px) {
.key-actions {
flex-direction: column;
}
.use-key-btn,
.generate-new-btn {
min-width: 100%;
}
.key-display {
flex-direction: column;
gap: 8px;
}
.copy-btn {
min-width: 100%;
}
.generated-keys {
padding: 16px;
}
.key-input {
font-size: 11px;
}
}

View File

@ -0,0 +1,676 @@
/* Calendar View - Game-like Minimalistic Design */
/* :root variables moved to common.css or are view-specific if necessary */
.calendar-view-container {
/* Extends .view-container from common.css */
/* Specific height and margins for calendar view */
height: calc(100vh - 120px); /* Overrides common.css if different */
margin: 100px 40px 60px 40px; /* Specific margins */
/* background: transparent; */ /* Should inherit from body or be set if needed */
/* color: var(--text-primary); */ /* Inherits from common.css */
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; */ /* Uses font from common.css */
/* display, flex-direction, overflow are covered by .view-container */
}
/* Header */
.calendar-header {
/* Extends .view-header from common.css */
/* .view-header provides: display, justify-content, align-items, padding-bottom, border-bottom, margin-bottom */
/* Original margin-bottom: 24px; padding: 0 8px; */
/* common.css .view-header has margin-bottom: var(--spacing-md) (16px) and padding-bottom for border */
/* If specific padding for calendar-header itself is needed beyond what .view-header provides, add here */
}
.calendar-navigation {
display: flex;
align-items: center;
gap: 16px;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-dark); /* Common.css variable */
color: var(--text-primary); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
padding: 12px; /* Specific padding */
border-radius: var(--border-radius-medium); /* Common.css variable (8px) */
cursor: pointer;
font-size: 14px; /* Specific font size */
font-weight: 500;
transition: background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, transform var(--transition-speed) ease; /* Align transition properties */
min-width: 44px; /* Specific size */
height: 44px; /* Specific size */
}
.nav-btn:hover {
background: var(--surface-medium); /* Common.css variable for hover */
border-color: var(--primary-accent); /* Common.css variable for primary interaction */
transform: translateY(-1px); /* Specific transform */
}
.today-btn {
padding: 12px 20px;
min-width: auto;
}
.calendar-title {
/* Extends .view-title from common.css */
/* .view-title provides: font-size, font-weight, color */
/* Original font-size: 24px; font-weight: 600; color: var(--text-primary); */
/* common.css .view-title has font-size: 1.8em; color: var(--primary-accent) */
/* If var(--text-primary) is desired over var(--primary-accent) for this title: */
color: var(--text-primary);
}
.calendar-view-switcher {
display: flex;
gap: 8px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: 12px;
padding: 6px;
}
.view-btn {
background: transparent;
color: var(--text-secondary); /* Common.css variable */
border: none;
padding: 12px 16px; /* Specific padding */
border-radius: var(--border-radius-medium); /* Common.css variable (8px) */
cursor: pointer;
font-size: 14px; /* Specific font size */
font-weight: 500;
transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, transform var(--transition-speed) ease; /* Align transition properties */
position: relative;
overflow: hidden; /* For shimmer effect */
}
.view-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s ease;
}
.view-btn:hover::before {
left: 100%;
}
.view-btn:hover {
color: var(--text-primary); /* Common.css variable */
background: var(--surface-medium); /* Common.css variable for hover */
transform: translateY(-1px); /* Specific transform */
}
.view-btn.active {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color for active/primary state, common pattern */
box-shadow: 0 0 20px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Use common variable and alpha mix for glow */
}
.calendar-actions {
display: flex;
gap: 12px;
}
.action-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm); /* Use common spacing variable */
background: var(--surface-dark); /* Use common.css variable */
color: var(--text-primary); /* Consistent with common.css */
border: 1px solid var(--border-color); /* Use common.css variable */
padding: 12px 20px; /* Specific padding for this view's action buttons */
border-radius: var(--border-radius-medium); /* Use common.css variable (8px) */
cursor: pointer;
font-size: 14px; /* Specific font size */
font-weight: 500;
transition: all 0.2s ease; /* Consider aligning with var(--transition-speed) if appropriate */
position: relative;
overflow: hidden;
}
.action-btn.primary {
background: var(--primary-accent); /* Use common.css variable */
border-color: var(--primary-accent); /* Use common.css variable */
color: var(--bg-dark); /* Text color for primary button, from common.css .button-primary */
box-shadow: 0 0 15px color-mix(in srgb, var(--primary-accent) 20%, transparent); /* Use common.css variable in shadow */
}
.action-btn:hover {
/* Specific hover effect for this view's action buttons */
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); /* This shadow is specific */
/* Consider standardizing hover background if possible, e.g., var(--surface-medium) */
background: var(--surface-medium); /* Example: align hover bg with common */
border-color: var(--primary-accent); /* Example: align hover border with common */
}
.action-btn.primary:hover {
/* Specific hover effect for primary action buttons */
box-shadow: 0 4px 25px color-mix(in srgb, var(--primary-accent) 40%, transparent); /* Use common.css variable in shadow */
/* background for .primary.hover from common.css .button-primary:hover is color-mix(in srgb, var(--primary-accent) 85%, white) */
background: color-mix(in srgb, var(--primary-accent) 85%, white);
}
/* Content Area */
.calendar-content {
flex: 1;
overflow: hidden;
border-radius: 12px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
}
/* Month View */
.month-view {
height: 100%;
padding: 20px;
}
.month-grid {
height: 100%;
display: flex;
flex-direction: column;
}
.weekday-headers {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
margin-bottom: 1px;
}
.weekday-header {
padding: 16px 8px;
text-align: center;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--surface-light); /* Common.css variable */
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(6, 1fr);
gap: 1px;
flex: 1;
background: var(--border-color); /* Common.css variable */
}
.calendar-day {
background: var(--surface-light); /* Common.css variable */
padding: 12px 8px;
display: flex;
flex-direction: column;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.calendar-day:hover {
background: var(--surface-medium); /* Common.css variable */
transform: scale(1.02);
}
.calendar-day.other-month {
opacity: 0.4;
}
.calendar-day.today {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
box-shadow: 0 0 20px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.calendar-day.today .day-number {
color: var(--bg-dark); /* Text color on primary accent */
font-weight: 700;
}
.day-number {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.day-events {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.event-dot {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.event-dot:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.event-title {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.more-events {
font-size: 10px;
color: var(--text-muted);
font-weight: 500;
padding: 2px 4px;
text-align: center;
}
/* Week View */
.week-view {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
}
.week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
margin-bottom: 16px;
}
.week-day-header {
padding: 16px 8px;
text-align: center;
background: var(--surface-light); /* Common.css variable */
border-radius: 8px;
transition: all 0.2s ease;
}
.week-day-header.today {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
box-shadow: 0 0 15px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.day-name {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.day-number {
font-size: 18px;
font-weight: 700;
}
.week-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12px;
flex: 1;
}
.week-day-column {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--surface-light); /* Common.css variable */
border-radius: 8px;
overflow-y: auto;
}
/* Day View */
.day-view {
height: 100%;
padding: 20px;
overflow-y: auto;
}
.day-schedule {
display: flex;
flex-direction: column;
}
.time-slots {
display: flex;
flex-direction: column;
}
.time-slot {
display: flex;
min-height: 60px;
border-bottom: 1px solid var(--border-color); /* Common.css variable */
}
.time-label {
width: 80px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
background: var(--surface-light); /* Common.css variable */
display: flex;
align-items: flex-start;
justify-content: flex-end;
}
.time-content {
flex: 1;
padding: 8px 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
/* Agenda View */
.agenda-view {
height: 100%;
padding: 20px;
overflow-y: auto;
}
.agenda-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.agenda-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--surface-light); /* Common.css variable */
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.agenda-item:hover {
background: var(--surface-medium); /* Common.css variable */
transform: translateX(4px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.agenda-date {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
padding: 8px;
background: var(--surface-dark); /* Common.css variable */
border-radius: 8px;
}
.date-day {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
line-height: 1;
}
.date-month {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
}
.agenda-event-indicator {
width: 4px;
height: 40px;
border-radius: 2px;
flex-shrink: 0;
}
.agenda-event-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.agenda-event-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.agenda-event-time {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.agenda-event-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.4;
}
.agenda-event-location {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
.agenda-event-location i {
font-size: 10px;
}
.agenda-event-type {
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--surface-dark); /* Common.css variable */
color: var(--text-secondary);
}
/* Event Blocks */
.event-block {
padding: 8px 12px;
background: var(--surface-light); /* Common.css variable */
border-radius: 6px;
border-left: 4px solid var(--primary-accent); /* Common.css variable */
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 4px;
}
.event-block:hover {
background: var(--surface-medium); /* Common.css variable */
transform: translateX(2px);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
.event-block .event-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.event-block .event-location {
font-size: 11px;
color: var(--text-muted);
}
/* Event Type Styles */
.event-meeting {
border-left-color: var(--primary-accent) !important; /* Mapped to common primary */
}
.event-deadline {
border-left-color: #ef4444 !important; /* Literal color (original var(--error)) */
}
.event-milestone {
border-left-color: #10b981 !important; /* Literal color (original var(--success)) */
}
.event-social {
border-left-color: #8b5cf6 !important; /* Literal color (original var(--accent)) */
}
.event-workshop {
border-left-color: #f59e0b !important; /* Literal color (original var(--warning)) */
}
.event-conference {
border-left-color: #06b6d4 !important; /* Literal color (original var(--info)) */
}
.event-personal {
border-left-color: #6b7280 !important; /* Literal color (original var(--secondary)) */
}
/* Agenda Event Type Colors */
.agenda-event-type.event-meeting {
background: var(--primary-accent); /* Mapped to common primary */
color: var(--bg-dark); /* Text on primary accent */
}
.agenda-event-type.event-deadline {
background: #ef4444; /* Literal color */
color: white;
}
.agenda-event-type.event-milestone {
background: #10b981; /* Literal color */
color: white;
}
.agenda-event-type.event-social {
background: #8b5cf6; /* Literal color */
color: white;
}
.agenda-event-type.event-workshop {
background: #f59e0b; /* Literal color */
color: var(--bg-dark); /* Text on amber/orange */
}
.agenda-event-type.event-conference {
background: #06b6d4; /* Literal color */
color: var(--bg-dark); /* Text on cyan */
}
.agenda-event-type.event-personal {
background: #6b7280; /* Literal color */
color: white; /* Text on gray */
}
/* Scrollbar styling is now handled globally by common.css */
/* Responsive design */
@media (max-width: 768px) {
.calendar-view-container {
margin: 20px;
height: calc(100vh - 80px);
}
.calendar-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.calendar-navigation {
justify-content: center;
}
.calendar-title {
text-align: center;
font-size: 20px;
}
.calendar-view-switcher {
justify-content: center;
}
.weekday-header,
.week-day-header {
padding: 12px 4px;
font-size: 11px;
}
.calendar-day {
padding: 8px 4px;
}
.day-number {
font-size: 12px;
}
.event-dot {
font-size: 9px;
padding: 1px 4px;
}
.week-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.agenda-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.agenda-date {
align-self: flex-start;
}
}
@media (max-width: 480px) {
.calendar-view-switcher {
flex-wrap: wrap;
}
.view-btn {
flex: 1;
min-width: 0;
justify-content: center;
}
.calendar-grid {
grid-template-rows: repeat(6, minmax(80px, 1fr));
}
.time-label {
width: 60px;
font-size: 10px;
}
}

View File

@ -0,0 +1,302 @@
/* app/static/css/circles_view.css */
/* Styles for the interactive circles background view */
:root {
/* --glow was in styles.css :root, used by .circle:hover and .center-circle */
--glow: 0 0 15px var(--primary-accent); /* Using var(--primary-accent) from common.css */
}
.circles-view {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0; /* Base layer, ensure content is above this */
/* background-color: rgba(0, 255, 0, 0.1); */ /* Debug background removed */
}
.app-title { /* This was in styles.css, seems related to the circles view context */
position: absolute;
top: 20px;
left: 20px;
font-size: 28px;
font-weight: 600;
color: var(--primary-accent); /* Using var(--primary-accent) from common.css */
z-index: 1001; /* Keep Yew's higher z-index over circles */
}
.flower-of-life-outer-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
.flower-of-life-container { /* This is the Yew component's .flower-container */
position: relative;
z-index: 1; /* Ensure circles are above the .circles-view green background */
width: 1px; /* Circles are positioned relative to this central point */
height: 1px;
}
.center-circle-area, .surrounding-circles-area {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Base .circle style from reference, adapted for Yew */
.circle {
position: absolute;
/* width & height will be set by inline styles in Yew (e.g., 80px or 120px) */
border-radius: 50%;
/* border: 2px solid rgba(187, 134, 252, 0.3); Using accent color from common.css */
border: 2px solid color-mix(in srgb, var(--primary-accent) 30%, transparent);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.5s ease;
cursor: pointer;
/* background: radial-gradient(circle at center, rgba(187, 134, 252, 0.1) 0%, transparent 70%); */
background: radial-gradient(circle at center, color-mix(in srgb, var(--primary-accent) 10%, transparent) 0%, transparent 70%);
z-index: 1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0; /* Circles are invisible until positioned by specific class */
/* transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* More specific transition on positional classes */
}
.circle .circle-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: max-content;
max-width: 200px; /* Prevent excessively wide text */
text-align: center;
font-size: 0.9em;
font-weight: 500;
color: var(--text-primary);
opacity: 1; /* Text is fully opaque */
background-color: #000000; /* Solid black background */
padding: 5px 10px;
border-radius: var(--border-radius-medium, 6px);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
z-index: 3; /* Above parent circle (z-index 1 or 2) */
transition: all 0.3s ease;
}
.circle:hover {
border-color: var(--primary-accent);
box-shadow: var(--glow);
/* background: radial-gradient(circle at center, rgba(187, 134, 252, 0.2) 0%, transparent 70%); */
background: radial-gradient(circle at center, color-mix(in srgb, var(--primary-accent) 20%, transparent) 0%, transparent 70%);
}
.circle:hover .circle-text {
opacity: 1;
color: var(--primary-accent);
}
.circle.center-circle {
opacity: 1 !important;
z-index: 2;
}
.circle.center-circle:hover {
border-color: var(--primary-accent);
box-shadow: 0 0 25px var(--primary-accent); /* Enhanced glow */
}
/* Positional classes from reference, for Yew's .circle.circle-position-N */
.circle.circle-position-1, .circle.circle-position-2, .circle.circle-position-3, .circle.circle-position-4, .circle.circle-position-5, .circle.circle-position-6 {
opacity: 1;
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.5s ease;
}
.circle.circle-position-1 { transform: translate(-50%, -50%) translateY(-144px); }
.circle.circle-position-2 { transform: translate(-50%, -50%) translate(124px, -72px); }
.circle.circle-position-3 { transform: translate(-50%, -50%) translate(124px, 72px); }
.circle.circle-position-4 { transform: translate(-50%, -50%) translateY(144px); }
.circle.circle-position-5 { transform: translate(-50%, -50%) translate(-124px, 72px); }
.circle.circle-position-6 { transform: translate(-50%, -50%) translate(-124px, -72px); }
/* Styling for sole-selected circle text (title and description) */
.circle .circle-text-container {
display: flex;
flex-direction: column;
justify-content: center; /* Vertically center the block */
align-items: center; /* Horizontally center the text within the block */
text-align: center; /* Ensure text itself is centered if it wraps */
padding: 10px; /* Add some padding */
box-sizing: border-box; /* Include padding in width/height */
height: 100%; /* Make container take full height of circle */
overflow-y: auto; /* Allow scrolling if content overflows */
}
.circle .circle-title {
font-size: 1.8em; /* Larger font for the title */
font-weight: bold;
margin-bottom: 0.5em; /* Space between title and description */
color: #e0e0e0; /* Light color for title */
}
.circle .circle-description {
font-size: 1em; /* Standard font size for description */
color: #b0b0b0; /* Slightly dimmer color for description */
line-height: 1.4;
}
/* Ensure existing .circle-text for single line names is still centered */
.circle .circle-text {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
box-sizing: border-box;
}
/* Additional styling for the .sole-selected class if needed for other effects */
.circle.center-circle.sole-selected {
/* Example: slightly different border or shadow if desired */
/* box-shadow: 0 0 20px 5px var(--primary, #3b82f6); */
}
header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.app-title-button {
display: flex;
align-items: center;
background-color: var(--bg-medium, #2a2a2a);
color: var(--text-primary, #e0e0e0);
border: 1px solid var(--border-color, #444);
padding: 10px 15px;
border-radius: var(--border-radius-large, 8px);
font-size: 1.2em;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
min-height: 50px; /* Ensure consistent height */
min-width: 150px; /* Minimum width */
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.app-title-button:hover {
background-color: var(--bg-light, #3a3a3a);
border-color: var(--primary-accent, #bb86fc);
}
.app-title-logo-img {
height: 30px; /* Adjust as needed */
width: auto;
max-width: 100px; /* Prevent overly wide logos */
object-fit: contain;
margin-right: 10px;
border-radius: 4px; /* Slight rounding for image logos */
}
.app-title-logo-symbol {
font-size: 1.5em; /* Larger for symbol */
margin-right: 10px;
line-height: 1; /* Ensure it aligns well */
}
.app-title-name {
margin-right: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px; /* Prevent very long names from breaking layout */
}
.app-title-status-icon {
font-size: 1em;
margin-left: auto; /* Pushes icon to the right if space allows */
color: var(--primary-accent, #bb86fc);
}
.app-title-dropdown {
position: absolute;
top: 100%; /* Position below the button */
left: 0;
background-color: var(--bg-dark, #1e1e1e);
border: 1px solid var(--border-color-light, #555);
border-top: none; /* Avoid double border with button */
border-radius: 0 0 var(--border-radius-large, 8px) var(--border-radius-large, 8px);
padding: 10px;
min-width: 250px; /* Ensure dropdown is wide enough */
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
max-height: 300px; /* Limit height and allow scrolling */
overflow-y: auto;
}
.app-title-dropdown .dropdown-header {
font-size: 0.9em;
color: var(--text-secondary, #aaa);
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid var(--border-color, #444);
}
.app-title-dropdown-item {
display: flex;
align-items: center;
padding: 8px 5px;
cursor: pointer;
border-radius: var(--border-radius-medium, 6px);
transition: background-color 0.15s ease;
}
.app-title-dropdown-item:hover {
background-color: var(--bg-medium, #2a2a2a);
}
.app-title-dropdown-item input[type="checkbox"] {
margin-right: 10px;
cursor: pointer;
/* Consider custom checkbox styling for better theme integration if needed */
}
.app-title-dropdown-item label {
color: var(--text-primary, #e0e0e0);
font-size: 1em;
cursor: pointer;
flex-grow: 1; /* Allow label to take remaining space */
}
.app-title-dropdown .no-sub-circles-message {
color: var(--text-secondary, #aaa);
font-style: italic;
padding: 10px 5px;
}
/* Remove default h1 styling for app-title if it's no longer an h1 or if it conflicts */
/* This was the old .app-title style, adjust or remove if .app-title-container replaces it */
/*
.app-title {
position: absolute;
top: 20px;
left: 20px;
font-size: 28px;
font-weight: 600;
color: var(--primary-accent);
z-index: 1001;
}
*/

View File

@ -0,0 +1,593 @@
/* app/static/css/common.css */
/* Global CSS Custom Properties */
:root {
--font-primary: 'Inter', sans-serif;
--font-secondary: 'Roboto Mono', monospace;
--bg-dark: #121212;
--bg-medium: #1E1E1E;
--bg-light: #2C2C2C;
--surface-dark: #1E1E1E;
--surface-medium: #4A4A4A;
--surface-light: #5A5A5A;
--primary-accent: #00AEEF; /* Bright Blue */
--secondary-accent: #FF4081; /* Bright Pink */
--tertiary-accent: #FFC107; /* Amber */
--text-primary: #E0E0E0;
--text-secondary: #B0B0B0;
--text-disabled: #757575;
--border-color: #424242;
--border-radius-small: 4px;
--border-radius-medium: 8px;
--border-radius-large: 16px;
--shadow-small: 0 2px 4px rgba(0,0,0,0.2);
--shadow-medium: 0 4px 8px rgba(0,0,0,0.3);
--shadow-large: 0 8px 16px rgba(0,0,0,0.4);
--spacing-unit: 8px;
--spacing-xs: calc(var(--spacing-unit) * 0.5); /* 4px */
--spacing-sm: var(--spacing-unit); /* 8px */
--spacing-md: calc(var(--spacing-unit) * 2); /* 16px */
--spacing-lg: calc(var(--spacing-unit) * 3); /* 24px */
--spacing-xl: calc(var(--spacing-unit) * 4); /* 32px */
--transition-speed: 0.3s;
/* Common Component Variables (can be extended) */
--button-padding: var(--spacing-sm) var(--spacing-md);
--input-padding: var(--spacing-sm);
}
/* Basic Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
height: 100vh;
scroll-behavior: smooth;
}
body {
font-family: var(--font-primary);
background-color: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden; /* Prevent horizontal scroll */
}
/* Common Scrollbar Styles */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-medium);
border-radius: var(--border-radius-small);
}
::-webkit-scrollbar-thumb {
background: var(--surface-medium);
border-radius: var(--border-radius-small);
border: 2px solid var(--bg-medium); /* Creates padding around thumb */
}
::-webkit-scrollbar-thumb:hover {
background: var(--surface-light);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Base Layout Classes */
.view-container {
display: flex;
flex-direction: column;
height: calc(100vh - 60px); /* Assuming nav-island is 60px, adjust as needed */
overflow: hidden;
padding: var(--spacing-lg);
gap: var(--spacing-lg);
}
.view-main-content {
flex-grow: 1;
overflow-y: auto; /* For scrollable content within views */
padding-right: var(--spacing-sm); /* Space for scrollbar */
}
.view-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
margin-bottom: var(--spacing-md); /* Added margin for separation */
}
.view-title {
font-size: 1.8em;
font-weight: 600;
color: var(--primary-accent);
}
/* Base Component Styles */
.button-base {
padding: var(--button-padding);
border: 1px solid transparent;
border-radius: var(--border-radius-medium);
font-family: var(--font-primary);
font-weight: 500;
cursor: pointer;
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed) ease, transform var(--transition-speed) ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
line-height: 1; /* Ensure consistent height */
}
.button-base:active {
transform: translateY(1px); /* Subtle press effect */
}
.button-primary {
background-color: var(--primary-accent);
color: var(--bg-dark);
}
.button-primary:hover {
background-color: color-mix(in srgb, var(--primary-accent) 85%, white);
}
.button-secondary {
background-color: var(--surface-dark);
color: var(--text-primary);
border-color: var(--border-color);
}
.button-secondary:hover {
background-color: var(--surface-medium);
border-color: var(--primary-accent);
}
.button-danger {
background-color: var(--secondary-accent);
color: white;
}
.button-danger:hover {
background-color: color-mix(in srgb, var(--secondary-accent) 85%, black);
}
.tab-button {
padding: var(--spacing-sm) var(--spacing-md);
background-color: transparent;
color: var(--text-secondary);
border: none;
border-bottom: 2px solid transparent;
border-radius: 0; /* Tabs often don't have rounded corners */
font-weight: 500;
cursor: pointer;
transition: color var(--transition-speed) ease, border-color var(--transition-speed) ease;
}
.tab-button.active,
.tab-button:hover {
color: var(--primary-accent);
border-bottom-color: var(--primary-accent);
}
.action-button {
padding: var(--spacing-xs) var(--spacing-sm); /* Smaller action buttons */
font-size: 0.9em;
border-radius: var(--border-radius-small);
/* Default to secondary style, can be combined with .button-primary etc. */
background-color: var(--surface-medium);
color: var(--text-primary);
border: 1px solid var(--border-color);
font-weight: 500; /* Added for consistency */
cursor: pointer; /* Added for consistency */
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed) ease, transform var(--transition-speed) ease; /* Added for consistency */
}
.action-button:hover {
border-color: var(--primary-accent);
background-color: var(--surface-light);
}
.action-button:active {
transform: translateY(1px); /* Subtle press effect */
}
.action-button.primary { /* Modifier for primary action button */
background-color: var(--primary-accent);
color: var(--bg-dark);
border-color: var(--primary-accent);
}
.action-button.primary:hover {
background-color: color-mix(in srgb, var(--primary-accent) 85%, white);
}
.card-base {
background-color: var(--surface-dark);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
box-shadow: var(--shadow-small);
transition: box-shadow var(--transition-speed) ease, transform var(--transition-speed) ease;
display: flex;
flex-direction: column;
}
.card-base:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
padding-bottom: var(--spacing-sm); /* Added padding */
border-bottom: 1px solid var(--border-color); /* Separator for header */
}
.card-title {
font-size: 1.2em;
font-weight: 600;
color: var(--tertiary-accent);
}
.card-content {
flex-grow: 1;
font-size: 0.95em;
color: var(--text-secondary);
line-height: 1.5;
}
.card-footer {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md); /* Increased padding */
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
align-items: center; /* Align items in footer */
gap: var(--spacing-sm);
}
.input-base {
background-color: var(--bg-light);
color: var(--text-primary);
border: 0px;
border-radius: var(--border-radius-small);
padding: var(--input-padding);
font-family: var(--font-primary);
font-size: 1em;
width: 100%;
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}
.input-base:focus {
outline: none;
border-color: var(--primary-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent);
}
.input-base::placeholder {
color: var(--text-disabled);
}
/* Utility Classes */
.flex-row { display: flex; flex-direction: row; }
.flex-col { display: flex; flex-direction: column; }
.items-center { align-items: center; }
.justify-start { justify-content: flex-start; }
.justify-end { justify-content: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-xs { gap: var(--spacing-xs); }
.gap-sm { gap: var(--spacing-sm); }
.gap-md { gap: var(--spacing-md); }
.gap-lg { gap: var(--spacing-lg); }
.gap-xl { gap: var(--spacing-xl); }
.p-xs { padding: var(--spacing-xs); }
.p-sm { padding: var(--spacing-sm); }
.p-md { padding: var(--spacing-md); }
.p-lg { padding: var(--spacing-lg); }
.p-xl { padding: var(--spacing-xl); }
.m-xs { margin: var(--spacing-xs); }
.m-sm { margin: var(--spacing-sm); }
.m-md { margin: var(--spacing-md); }
.m-lg { margin: var(--spacing-lg); }
.m-xl { margin: var(--spacing-xl); }
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-primary-accent { color: var(--primary-accent); }
.text-secondary-accent { color: var(--secondary-accent); }
.text-tertiary-accent { color: var(--tertiary-accent); }
.text-light { color: var(--text-primary); }
.text-muted { color: var(--text-secondary); }
.text-disabled { color: var(--text-disabled); }
.font-bold { font-weight: bold; }
.font-semibold { font-weight: 600; }
.font-normal { font-weight: normal; }
.hidden { display: none !important; }
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Responsive Design Placeholders (can be expanded) */
@media (max-width: 768px) {
:root {
/* Adjust base font size or spacing for smaller screens if needed */
/* Example: --spacing-unit: 6px; */
}
.view-container {
padding: var(--spacing-md);
gap: var(--spacing-md);
height: calc(100vh - 50px); /* Example: smaller nav island */
}
.view-title {
font-size: 1.6em;
}
.button-base {
/* padding: calc(var(--button-padding) * 0.8); /* Slightly smaller buttons */
/* Consider adjusting padding directly if calc() is problematic or for clarity */
padding: calc(var(--spacing-sm) * 0.8) calc(var(--spacing-md) * 0.8);
}
.card-base {
padding: var(--spacing-sm);
}
/* Add more mobile-specific adjustments */
}
@media (max-width: 480px) {
.view-title {
font-size: 1.4em;
}
.view-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
}
/* Further adjustments for very small screens */
}
/* Chat Message Styling */
.message {
margin-bottom: var(--spacing-md);
border-radius: var(--border-radius-medium);
overflow: hidden;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.message-header .sender {
font-weight: 600;
color: var(--text-primary);
}
.message-header .timestamp {
font-size: 0.85em;
color: var(--text-secondary);
}
.status {
padding: 2px 6px;
border-radius: var(--border-radius-small);
font-size: 0.8em;
font-weight: 500;
}
.status-ok {
background-color: color-mix(in srgb, #4CAF50 20%, transparent);
color: #4CAF50;
}
.status-error {
background-color: color-mix(in srgb, var(--secondary-accent) 20%, transparent);
color: var(--secondary-accent);
}
.status-pending {
background-color: color-mix(in srgb, var(--tertiary-accent) 20%, transparent);
color: var(--tertiary-accent);
}
.message-content {
padding: var(--spacing-md);
background-color: var(--surface-dark);
}
.message-title {
font-weight: 600;
color: var(--primary-accent);
margin-bottom: var(--spacing-xs);
}
.message-description {
font-size: 0.9em;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.message-text {
color: var(--text-primary);
line-height: 1.5;
}
/* Format-specific styling */
.format-rhai .message-content {
padding: 0;
}
.message-error .message-content {
background-color: color-mix(in srgb, var(--secondary-accent) 10%, var(--surface-dark));
}
/* Code block styling */
.code-block {
background-color: var(--bg-dark);
border-radius: var(--border-radius-medium);
overflow: hidden;
font-family: var(--font-secondary);
}
.code-header {
background-color: var(--surface-medium);
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.language-label {
font-size: 0.8em;
font-weight: 600;
color: var(--primary-accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.code-content {
display: flex;
max-height: 400px;
overflow-y: auto;
}
.line-numbers {
background-color: var(--bg-medium);
padding: var(--spacing-md) var(--spacing-sm);
border-right: 1px solid var(--border-color);
user-select: none;
min-width: 40px;
}
.line-number {
font-size: 0.85em;
color: var(--text-disabled);
text-align: right;
line-height: 1.5;
font-family: var(--font-secondary);
}
.code-lines {
flex: 1;
padding: var(--spacing-md);
overflow-x: auto;
}
.code-line {
font-size: 0.9em;
color: var(--text-primary);
line-height: 1.5;
font-family: var(--font-secondary);
white-space: pre;
min-height: 1.5em;
}
.code-line:empty::before {
content: " ";
}
/* Error content styling */
.error-content {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background-color: color-mix(in srgb, var(--secondary-accent) 10%, transparent);
border-left: 4px solid var(--secondary-accent);
border-radius: var(--border-radius-small);
}
.error-icon {
font-size: 1.2em;
flex-shrink: 0;
}
.error-text {
color: var(--text-primary);
line-height: 1.5;
}
/* User vs Assistant message styling */
.user-message .message-header {
background-color: color-mix(in srgb, var(--primary-accent) 15%, var(--surface-medium));
}
.user-message .message-header .sender {
color: var(--primary-accent);
}
.ai-message .message-header .sender {
color: var(--tertiary-accent);
}
/* Input styling for code format */
.format-rhai {
font-family: var(--font-secondary);
background-color: var(--bg-dark);
border: 1px solid var(--border-color);
line-height: 1.5;
}
.format-rhai::placeholder {
color: var(--text-disabled);
font-style: italic;
}
.column {
display: flex;
flex-direction: column;
flex: 1;
gap: var(--spacing-md)
}
.card {
background-color: var(--surface-dark);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
}
.card:hover {
transform: translateY(-2px);
border-color: var(--primary-accent);
box-shadow: 0 4px 10px color-mix(in srgb, var(--primary-accent) 20%, transparent);
}
.card .collection-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
}
.card .collection-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.4;
margin: 0;
}

View File

@ -0,0 +1,348 @@
/* Customize View - Ultra Minimalistic */
/* :root variables moved to common.css or are view-specific if necessary */
.customize-view {
/* Extends .view-container from common.css */
align-items: center; /* Specific alignment for this view */
height: calc(100vh - 120px); /* Specific height */
margin: 100px 40px 60px 40px; /* Specific margins */
/* font-family will be inherited from common.css body */
/* Other .view-container properties like display, flex-direction, color, background are inherited or set by common.css */
}
.view-header {
/* Extends .view-header from common.css */
margin-bottom: var(--spacing-xl); /* Was 40px, using common.css spacing */
text-align: center; /* Specific alignment */
}
.view-title {
/* Extends .view-title from common.css */
font-size: 1.2em; /* Was 18px, common.css .view-title is 1.8em. This is an override. */
/* common.css .view-title color is var(--primary-accent). This view uses var(--text-primary). */
color: var(--text-primary); /* Specific color */
margin: 0;
}
/* Content Area */
.customize-content {
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable (16px, was 12px) */
padding: 32px;
width: 100%;
max-width: 700px;
}
.settings-section {
margin-bottom: 32px;
}
.settings-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 16px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.settings-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.setting-item.row {
flex-direction: row;
align-items: center;
gap: 24px;
}
.setting-item.row .setting-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.setting-label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 6px 0;
}
.setting-control {
width: 100%;
}
/* Input Styles */
.setting-input,
.setting-input-select {
padding: 8px 12px; /* Specific padding, common.css input-base is var(--spacing-sm) (8px) */
background: var(--bg-light); /* Align with common.css .input-base */
border: 1px solid var(--border-color); /* Common.css variable */
color: var(--text-primary);
border-radius: var(--border-radius-medium); /* Was 6px, common.css input-base is var(--border-radius-small) (4px). Using medium (8px) for a slightly larger feel. */
font-size: 14px;
transition: border-color 0.2s ease;
width: 100%;
outline: none;
}
.setting-input:focus,
.setting-input-select:focus {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Focus shadow from common.css .input-base */
}
/* Text Input */
.setting-text-input {
padding: 8px 12px; /* Specific padding */
background: var(--bg-light); /* Align with common.css .input-base */
border: 1px solid var(--border-color); /* Common.css variable */
color: var(--text-primary);
border-radius: var(--border-radius-medium); /* Was 6px, using medium (8px) */
font-size: 14px;
transition: border-color 0.2s ease;
width: 100%;
outline: none;
}
.setting-text-input:focus {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Focus shadow from common.css .input-base */
}
.setting-text-input::placeholder {
color: var(--text-disabled); /* Align with common.css .input-base */
}
/* Color Selection Grid */
.color-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
width: 100%;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 6px;
border: 2px solid var(--border-color); /* Common.css variable */
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.color-option:hover {
border-color: var(--text-secondary);
transform: scale(1.05);
}
.color-option.selected {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.color-option.selected::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
text-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
}
/* Pattern Selection Grid */
.pattern-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
width: 100%;
}
.pattern-option {
width: 60px;
height: 40px;
border: 2px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Was 6px, using medium (8px) */
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: var(--surface-medium); /* Common.css variable, was var(--hover) */
}
.pattern-option:hover {
border-color: var(--text-secondary);
transform: scale(1.02);
}
.pattern-option.selected {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.pattern-option.selected::after {
content: '✓';
position: absolute;
top: 4px;
right: 4px;
color: var(--primary-accent); /* Common.css variable */
font-size: 10px;
font-weight: bold;
}
/* Pattern Previews */
.pattern-dots {
background-image: radial-gradient(circle, var(--text-muted) 1px, transparent 1px);
background-size: 8px 8px;
}
.pattern-grid-lines {
background-image:
linear-gradient(var(--text-muted) 1px, transparent 1px),
linear-gradient(90deg, var(--text-muted) 1px, transparent 1px);
background-size: 10px 10px;
}
.pattern-diagonal {
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 4px,
var(--text-muted) 4px,
var(--text-muted) 5px
);
}
.pattern-waves {
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10c2.5-2.5 7.5-2.5 10 0s7.5 2.5 10 0v10H0V10z' fill='%23555555' fill-opacity='0.4'/%3E%3C/svg%3E");
}
/* Toggle Switch */
.setting-toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.setting-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.setting-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-light); /* Align with common.css .input-base */
border: 1px solid var(--border-color); /* Common.css variable */
transition: 0.3s;
border-radius: 24px; /* Specific large radius for toggle */
}
.setting-toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background: var(--text-secondary);
transition: 0.3s;
border-radius: 50%;
}
input:checked + .setting-toggle-slider {
background: var(--primary-accent); /* Common.css variable */
border-color: var(--primary-accent); /* Common.css variable */
}
input:checked + .setting-toggle-slider:before {
transform: translateX(20px);
background: white;
}
/* Logo Selection */
.logo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
width: 100%;
}
.logo-option {
width: 60px;
height: 40px;
border: 2px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Was 6px, using medium (8px) */
cursor: pointer;
transition: all 0.2s ease;
position: relative;
background: var(--surface-medium); /* Common.css variable, was var(--hover) */
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.logo-option:hover {
border-color: var(--text-secondary);
transform: scale(1.02);
}
.logo-option.selected {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
/* Responsive Design */
@media (max-width: 768px) {
.customize-view {
margin: 20px;
}
.customize-content {
padding: 24px;
max-width: none;
}
.setting-item.row {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
.color-grid {
grid-template-columns: repeat(4, 1fr);
}
.pattern-grid {
grid-template-columns: repeat(3, 1fr);
}
.logo-grid {
grid-template-columns: repeat(2, 1fr);
}
}

View File

@ -0,0 +1,51 @@
/* app/static/css/dashboard_view.css */
/* Styles for the dashboard overlay view */
.dashboard-view {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-lg); /* Use common spacing */
z-index: 2000;
}
.dashboard-view h2 {
color: var(--primary-accent); /* Use common variable */
margin-bottom: var(--spacing-xl); /* Use common spacing */
}
.dashboard-cards {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-lg); /* Use common spacing */
justify-content: center;
max-width: 900px;
}
/* Specific card style for dashboard, distinct from common.css .card-base */
.dashboard-view .card {
background-color: var(--surface-dark);
/* border: 1px solid #333; Using var(--border-color) */
border: 1px solid var(--border-color);
border-radius: var(--border-radius-large); /* Larger radius for dashboard cards */
padding: var(--spacing-lg); /* Use common spacing */
width: 280px;
box-shadow: var(--shadow-medium); /* Use common shadow */
transition: transform 0.2s, box-shadow 0.2s;
}
.dashboard-view .card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-large); /* Use common shadow */
}
.dashboard-view .card h3 {
color: var(--secondary-accent); /* Use common variable */
margin-top: 0;
margin-bottom: var(--spacing-md); /* Add some space below h3 */
}

View File

@ -0,0 +1,581 @@
/* Governance View - Game-like Interactive Design */
/* :root variables moved to common.css or are view-specific if necessary (using literal hex for some status colors) */
.governance-view-container {
/* Extends .view-container from common.css */
height: calc(100vh - 120px); /* Specific height */
margin: 100px 40px 60px 40px; /* Specific margins */
gap: var(--spacing-lg); /* Was 24px, using common.css spacing */
/* font-family will be inherited from common.css body */
/* Other .view-container properties like display, flex-direction, color, background are inherited or set by common.css */
}
/* Featured Proposal - Main Focus */
.featured-proposal-container {
flex: 1;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.featured-proposal-container.urgent {
border-color: #ef4444; /* Literal error color */
box-shadow: 0 0 30px color-mix(in srgb, #ef4444 40%, transparent); /* urgent-glow replacement */
animation: urgentPulse 2s ease-in-out infinite;
}
@keyframes urgentPulse {
0%, 100% { box-shadow: 0 0 30px color-mix(in srgb, #ef4444 40%, transparent); }
50% { box-shadow: 0 0 50px color-mix(in srgb, #ef4444 40%, transparent); }
}
.featured-proposal-header {
padding: 24px 32px;
background: linear-gradient(135deg, var(--surface-light), var(--surface-dark)); /* Common.css variables */
border-bottom: 1px solid var(--border-color); /* Common.css variable */
position: relative;
overflow: hidden;
}
.featured-proposal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, color-mix(in srgb, var(--primary-accent) 10%, transparent), transparent); /* Use common primary */
pointer-events: none;
}
.featured-proposal-meta {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
position: relative;
z-index: 1;
}
.featured-proposal-title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
flex: 1;
margin-right: 24px;
}
.featured-proposal-title.urgent {
color: #ef4444; /* Literal error color */
}
.featured-proposal-status {
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.urgent {
background: #ef4444; /* Literal error color */
color: white;
box-shadow: 0 0 20px color-mix(in srgb, #ef4444 40%, transparent); /* urgent-glow replacement */
}
.status-badge.popular {
background: #10b981; /* Literal success color */
color: white;
box-shadow: 0 0 20px color-mix(in srgb, #10b981 30%, transparent); /* success-glow replacement */
}
.status-badge.controversial {
background: #f59e0b; /* Literal warning color */
color: var(--bg-dark); /* Common.css variable for text on amber/orange */
}
.time-remaining {
font-size: 14px;
font-weight: 600;
color: #f59e0b; /* Literal warning color */
display: flex;
align-items: center;
gap: 8px;
}
.time-remaining.critical {
color: #ef4444; /* Literal error color */
animation: criticalBlink 1s ease-in-out infinite;
}
@keyframes criticalBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.featured-proposal-content {
flex: 1;
display: flex;
padding: 32px;
gap: 32px;
overflow: hidden;
}
.proposal-main {
flex: 2;
display: flex;
flex-direction: column;
gap: 24px;
}
.proposal-description {
font-size: 16px;
line-height: 1.6;
color: var(--text-primary);
max-height: 200px;
overflow-y: auto;
padding-right: 8px;
}
.proposal-attachments {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.attachment-item {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface-medium); /* Common.css variable */
padding: 8px 12px;
border-radius: var(--border-radius-medium); /* Common.css variable */
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.attachment-item:hover {
background: var(--surface-light); /* Common.css variable */
color: var(--text-primary);
transform: translateY(-1px);
}
.proposal-sidebar {
flex: 1;
display: flex;
flex-direction: column;
gap: 24px;
}
/* Voting Section */
.voting-section {
background: var(--surface-light); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: 24px;
border: 1px solid var(--border-color); /* Common.css variable */
}
.voting-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
.vote-tally {
margin-bottom: 20px;
}
.vote-option {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.vote-bar {
flex: 1;
height: 8px;
background: var(--surface-medium); /* Common.css variable */
border-radius: var(--border-radius-small); /* Common.css variable */
margin: 0 12px;
overflow: hidden;
}
.vote-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.vote-fill.for { background: #10b981; } /* Literal success color */
.vote-fill.against { background: #ef4444; } /* Literal error color */
.vote-fill.abstain { background: #6b7280; } /* Literal secondary color */
.vote-buttons {
display: flex;
gap: 8px;
}
.vote-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: var(--border-radius-medium); /* Common.css variable */
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.vote-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s ease;
}
.vote-btn:hover::before {
left: 100%;
}
.vote-btn.for {
background: #10b981; /* Literal success color */
color: white;
}
.vote-btn.against {
background: #ef4444; /* Literal error color */
color: white;
}
.vote-btn.abstain {
background: #6b7280; /* Literal secondary color */
color: white;
}
.vote-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* Comments Section */
.comments-section {
background: var(--surface-light); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: 24px;
border: 1px solid var(--border-color); /* Common.css variable */
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.comments-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
.comments-list {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
padding-right: 8px;
}
.comment-item {
background: var(--surface-medium); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Common.css variable */
padding: 12px;
margin-bottom: 8px;
border-left: 3px solid var(--primary-accent); /* Common.css variable */
}
.comment-author {
font-size: 12px;
font-weight: 600;
color: #06b6d4; /* Literal info color */
margin-bottom: 4px;
}
.comment-text {
font-size: 14px;
line-height: 1.4;
color: var(--text-primary);
}
.comment-input {
display: flex;
gap: 8px;
}
.comment-input input {
flex: 1;
background: var(--bg-light); /* Align with common.css .input-base */
border: 1px solid var(--border-color); /* Common.css variable */
color: var(--text-primary);
padding: 8px 12px; /* Specific padding, common.css input-base is var(--spacing-sm) (8px) */
border-radius: var(--border-radius-medium); /* Was 6px, using medium (8px) for consistency */
font-size: 14px;
}
.comment-input button {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color for primary button */
border: none;
padding: 8px 16px;
border-radius: var(--border-radius-medium); /* Was 6px, using medium (8px) */
cursor: pointer;
font-weight: 500;
}
/* Proposal Cards Row */
.proposals-row-container {
height: 200px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: 16px;
overflow: hidden;
}
.proposals-row-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.proposals-row-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.proposals-row {
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 8px;
height: calc(100% - 40px);
}
.proposal-card {
min-width: 280px;
background: var(--surface-light); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Common.css variable */
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.proposal-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.05), transparent);
opacity: 0;
transition: opacity 0.2s ease;
}
.proposal-card:hover::before {
opacity: 1;
}
.proposal-card:hover {
transform: translateY(-4px);
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.proposal-card.selected {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 20px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.proposal-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.proposal-card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
flex: 1;
margin-right: 8px;
line-height: 1.3;
}
.proposal-card-status {
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
}
.proposal-card-description {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.proposal-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: var(--text-muted);
}
.proposal-urgency {
display: flex;
align-items: center;
gap: 4px;
}
.urgency-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.urgency-indicator.high { background: #ef4444; } /* Literal error color */
.urgency-indicator.medium { background: #f59e0b; } /* Literal warning color */
.urgency-indicator.low { background: #10b981; } /* Literal success color */
/* Scrollbar Styling is now handled globally by common.css */
/* Empty States */
.governance-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
}
.governance-empty-state i {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.governance-empty-state p {
font-size: 16px;
margin: 0;
}
/* Responsive Design */
@media (max-width: 1200px) {
.featured-proposal-content {
flex-direction: column;
gap: 24px;
}
.proposal-main {
flex: none;
}
.proposal-sidebar {
flex: none;
flex-direction: row;
gap: 16px;
}
.voting-section,
.comments-section {
flex: 1;
}
}
@media (max-width: 768px) {
.governance-view-container {
margin: 20px;
height: calc(100vh - 80px);
}
.featured-proposal-content {
padding: 20px;
}
.featured-proposal-header {
padding: 20px;
}
.featured-proposal-title {
font-size: 22px;
}
.proposal-sidebar {
flex-direction: column;
}
.proposals-row-container {
height: 160px;
}
.proposal-card {
min-width: 240px;
}
}
@media (max-width: 480px) {
.featured-proposal-meta {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.featured-proposal-status {
align-self: stretch;
justify-content: space-between;
}
.vote-buttons {
flex-direction: column;
}
.proposal-card {
min-width: 200px;
}
}

View File

@ -0,0 +1,735 @@
/* Inspector View - New minimal design with sidebar cards */
.inspector-container {
display: flex;
height: calc(100vh - var(--app-title-bar-height, 60px) - var(--nav-island-height, 70px) - (2 * var(--spacing-lg)));
margin: 0 var(--spacing-lg) var(--spacing-lg) var(--spacing-lg);
gap: var(--spacing-lg);
}
/* Sidebar with tab cards */
.inspector-sidebar {
width: 300px;
background: var(--surface-dark);
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-content {
display: flex;
flex-direction: column;
height: 100%;
}
.sidebar-title {
margin: 0 0 var(--spacing-lg) 0;
font-size: 1.5em;
color: var(--text-primary);
font-weight: 600;
text-align: center;
border-bottom: 2px solid var(--primary-accent);
padding-bottom: var(--spacing-sm);
}
.tab-cards {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
/* Tab card styling */
.tab-card {
background: var(--surface-medium);
border: 2px solid var(--border-color);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-card:hover {
border-color: var(--primary-accent);
background: color-mix(in srgb, var(--surface-medium) 80%, var(--primary-accent) 20%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.tab-card.selected {
background: var(--primary-accent);
border-color: var(--primary-accent);
color: var(--bg-dark);
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
z-index: 10;
}
.tab-card-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.tab-card-header i {
font-size: 1.2em;
width: 20px;
text-align: center;
}
.tab-title {
font-weight: 600;
font-size: 1.1em;
}
.tab-card.selected .tab-title,
.tab-card.selected i {
color: var(--bg-dark);
}
/* Tab details that appear when selected */
.tab-details {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.detail-label {
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.detail-value {
color: var(--bg-dark);
font-weight: 600;
}
.detail-value.status-active {
color: var(--bg-dark);
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: var(--border-radius-small);
font-size: 0.8em;
}
.detail-value.error {
color: #ff6b6b;
font-weight: 700;
}
.content-panel {
height: 100%;
display: flex;
flex-direction: column;
}
.content-panel h2 {
margin: 0 0 var(--spacing-lg) 0;
color: var(--primary-accent);
font-size: 1.8em;
font-weight: 600;
border-bottom: 2px solid var(--primary-accent);
padding-bottom: var(--spacing-sm);
}
/* Overview content */
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-lg);
flex: 1;
}
.overview-card {
background: var(--surface-medium);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-medium);
padding: var(--spacing-lg);
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.overview-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.overview-card h3 {
margin: 0 0 var(--spacing-md) 0;
color: var(--text-primary);
font-size: 1.1em;
font-weight: 600;
}
.metric-value {
font-size: 2.5em;
font-weight: 700;
color: var(--primary-accent);
margin-bottom: var(--spacing-xs);
}
.metric-value.status-active {
color: #4ecdc4;
font-size: 1.5em;
}
.metric-label {
color: var(--text-secondary);
font-size: 0.9em;
}
/* Network content */
.network-nodes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-md);
flex: 1;
}
.network-node {
background: var(--surface-medium);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
transition: transform 0.2s ease;
}
.network-node:hover {
transform: translateY(-2px);
}
.network-node.node-connected {
border-left: 4px solid #4ecdc4;
}
.network-node.node-disconnected {
border-left: 4px solid #ff6b6b;
}
.network-node.node-unknown {
border-left: 4px solid #ffa726;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
}
.node-name {
font-weight: 600;
color: var(--text-primary);
font-size: 1.1em;
}
.node-status {
padding: 4px 8px;
border-radius: var(--border-radius-small);
font-size: 0.8em;
font-weight: 600;
}
.node-status.node-connected {
background: rgba(78, 205, 196, 0.2);
color: #4ecdc4;
}
.node-status.node-disconnected {
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
}
.node-details {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.node-id,
.node-latency {
font-size: 0.9em;
color: var(--text-secondary);
}
/* Logs content */
.logs-container {
flex: 1;
overflow-y: auto;
background: var(--surface-medium);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
}
.log-entry {
display: grid;
grid-template-columns: auto auto 1fr 3fr;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
font-size: 0.9em;
align-items: center;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.log-error {
background: rgba(255, 107, 107, 0.1);
border-left: 3px solid #ff6b6b;
}
.log-entry.log-warn {
background: rgba(255, 167, 38, 0.1);
border-left: 3px solid #ffa726;
}
.log-entry.log-info {
background: rgba(78, 205, 196, 0.1);
border-left: 3px solid #4ecdc4;
}
.log-timestamp {
color: var(--text-tertiary);
font-family: var(--font-secondary);
font-size: 0.8em;
}
.log-level {
padding: 2px 6px;
border-radius: var(--border-radius-small);
font-size: 0.8em;
font-weight: 600;
text-align: center;
min-width: 50px;
}
.log-level.log-error {
background: #ff6b6b;
color: white;
}
.log-level.log-warn {
background: #ffa726;
color: white;
}
.log-level.log-info {
background: #4ecdc4;
color: white;
}
.log-level.log-debug {
background: var(--text-tertiary);
color: white;
}
.log-source {
color: var(--text-secondary);
font-weight: 500;
}
.log-message {
color: var(--text-primary);
}
/* Interact content */
.interact-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
flex: 1;
}
.script-input-section,
.script-output-section {
flex: 1;
display: flex;
flex-direction: column;
}
.script-input-section label,
.script-output-section label {
color: var(--text-primary);
font-weight: 600;
margin-bottom: var(--spacing-sm);
font-size: 1.1em;
}
.script-input {
flex: 1;
background: var(--bg-light);
border: 2px solid var(--border-color);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
color: var(--text-primary);
font-family: var(--font-secondary);
font-size: 0.9em;
resize: none;
min-height: 150px;
}
.script-input:focus {
outline: none;
border-color: var(--primary-accent);
box-shadow: 0 0 0 3px rgba(var(--primary-accent-rgb), 0.2);
}
.execute-button {
align-self: flex-start;
background: var(--primary-accent);
color: var(--bg-dark);
border: none;
border-radius: var(--border-radius-medium);
padding: var(--spacing-sm) var(--spacing-lg);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-top: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.execute-button:hover {
background: color-mix(in srgb, var(--primary-accent) 90%, white);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.script-output {
flex: 1;
background: var(--bg-dark);
border: 2px solid var(--border-color);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
color: var(--text-secondary);
font-family: var(--font-secondary);
font-size: 0.9em;
white-space: pre-wrap;
overflow-y: auto;
min-height: 150px;
margin: 0;
}
/* Responsive design */
@media (max-width: 768px) {
.inspector-container {
flex-direction: column;
height: auto;
}
.inspector-sidebar {
width: 100%;
order: 2;
}
.inspector-main {
order: 1;
min-height: 400px;
}
.overview-grid {
grid-template-columns: 1fr;
}
.network-nodes {
grid-template-columns: 1fr;
}
.log-entry {
grid-template-columns: 1fr;
gap: var(--spacing-xs);
}
}
/* WebSocket Status Sidebar */
.ws-status {
background: var(--surface-dark);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.ws-status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border-color);
}
.ws-status-title {
font-weight: 600;
color: var(--text-primary);
font-size: 1.1em;
}
.ws-status-count {
background: var(--primary-accent);
color: var(--bg-dark);
padding: 2px 8px;
border-radius: var(--border-radius-small);
font-size: 0.8em;
font-weight: 600;
}
.ws-connections {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.ws-connection {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs);
border-radius: var(--border-radius-small);
transition: background-color 0.2s ease;
}
.ws-connection:hover {
background: rgba(255, 255, 255, 0.05);
}
.ws-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.ws-status-dot.ws-status-connected {
background: #4ecdc4;
box-shadow: 0 0 4px rgba(78, 205, 196, 0.5);
}
.ws-status-dot.ws-status-connecting {
background: #ffa726;
animation: pulse 1.5s infinite;
}
.ws-status-dot.ws-status-error {
background: #ff6b6b;
}
.ws-connection-info {
flex: 1;
min-width: 0;
}
.ws-connection-name {
font-size: 0.9em;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 2px;
}
.ws-connection-url {
font-size: 0.8em;
color: var(--text-tertiary);
font-family: var(--font-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Network Tab Styles */
.network-overview {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
height: 100%;
}
.stat-value.status-connected {
color: #4ecdc4;
}
/* Traffic Table Styles */
.network-traffic {
flex: 1;
display: flex;
flex-direction: column;
}
.network-traffic h3 {
margin: 0 0 var(--spacing-md) 0;
color: var(--text-primary);
font-size: 1.2em;
font-weight: 600;
}
.traffic-table {
background: var(--surface-medium);
border-radius: var(--border-radius-medium);
border: 1px solid var(--border-color);
overflow: hidden;
flex: 1;
}
.traffic-header {
display: grid;
grid-template-columns: 80px 100px 200px 150px 80px 100px;
background: var(--bg-dark);
border-bottom: 2px solid var(--border-color);
font-weight: 600;
color: var(--text-primary);
font-size: 0.9em;
}
.traffic-row {
display: grid;
grid-template-columns: 80px 100px 200px 150px 80px 100px;
border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s ease;
}
.traffic-row:hover {
background: rgba(255, 255, 255, 0.05);
}
.traffic-row:last-child {
border-bottom: none;
}
.traffic-col {
padding: var(--spacing-sm);
display: flex;
align-items: center;
font-size: 0.85em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.traffic-time {
color: var(--text-tertiary);
font-family: var(--font-secondary);
}
.traffic-direction {
font-weight: 600;
}
.traffic-direction.traffic-sent {
color: #ffa726;
}
.traffic-direction.traffic-received {
color: #4ecdc4;
}
.traffic-url {
color: var(--text-secondary);
font-family: var(--font-secondary);
}
.traffic-message {
color: var(--text-primary);
font-family: var(--font-secondary);
}
.traffic-size {
color: var(--text-secondary);
text-align: right;
justify-content: flex-end;
}
.traffic-status {
font-weight: 600;
}
.traffic-status.traffic-success {
color: #4ecdc4;
}
.traffic-status.traffic-error {
color: #ff6b6b;
}
/* Logs Tab Styles */
.logs-overview {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
height: 100%;
}
.logs-stats {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
}
.logs-stat {
display: flex;
flex-direction: column;
align-items: center;
background: var(--surface-medium);
padding: var(--spacing-md);
border-radius: var(--border-radius-medium);
border: 1px solid var(--border-color);
min-width: 100px;
}
.logs-stat .stat-value.stat-error {
color: #ff6b6b;
}
.logs-stat .stat-value.stat-warn {
color: #ffa726;
}
.log-time {
color: var(--text-tertiary);
font-family: var(--font-secondary);
font-size: 0.8em;
}
/* Animations */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Connection status dots for network nodes */
.node-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: var(--spacing-xs);
}
.node-status-dot.node-connected {
background: #4ecdc4;
box-shadow: 0 0 4px rgba(78, 205, 196, 0.5);
}
.node-status-dot.node-disconnected {
background: #ff6b6b;
}
.node-latency {
color: var(--text-tertiary);
font-size: 0.8em;
font-family: var(--font-secondary);
}

View File

@ -0,0 +1,185 @@
/* Intelligence View - Ultra Minimalistic Design */
/* :root variables moved to common.css or are view-specific if necessary */
.intelligence-view-container {
/* Extends .view-container from common.css but with flex-direction: row */
flex-direction: row; /* Specific direction for this view */
height: calc(100vh - 120px); /* Specific height */
margin: 100px 40px 60px 40px; /* Specific margins */
gap: var(--spacing-lg); /* Was 24px, using common.css spacing */
/* font-family will be inherited from common.css body */
/* Other .view-container properties like display, color, background, overflow are inherited or set by common.css */
}
.new-conversation-btn {
width: 100%;
background: transparent; /* Specific style */
color: var(--text-secondary); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
padding: 12px 16px;
border-radius: var(--border-radius-medium); /* Common.css variable */
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
margin-bottom: 20px;
text-align: left;
}
.new-conversation-btn:hover {
background: var(--surface-medium); /* Common.css variable for hover */
color: var(--text-primary); /* Common.css variable */
border-color: var(--primary-accent); /* Common interaction color */
}
.conversation-item,
.active-conversation-item {
padding: 12px 16px;
border-radius: var(--border-radius-medium); /* Common.css variable */
margin-bottom: 4px;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.2s ease;
font-size: 14px;
border: 1px solid transparent;
}
.conversation-item:hover {
background: var(--surface-medium); /* Common.css variable for hover */
color: var(--text-primary); /* Common.css variable */
}
.active-conversation-item {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
font-weight: 500;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--surface-dark); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
overflow: hidden;
}
.messages-display {
flex: 1;
overflow-y: auto;
padding: 24px;
background: transparent;
}
.messages-display h4 {
margin: 0 0 24px 0;
color: var(--text-primary);
font-weight: 600;
font-size: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color); /* Common.css variable */
}
.messages-display p {
color: var(--text-secondary);
font-size: 14px;
text-align: center;
margin-top: 40px;
}
.message {
margin-bottom: 20px;
max-width: 80%;
}
.user-message {
margin-left: auto;
text-align: right;
}
.ai-message {
margin-right: auto;
}
.message .sender {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
margin-bottom: 6px;
display: block;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.message p {
background: var(--surface-light); /* Common.css variable for AI message bubble */
padding: 12px 16px;
border-radius: var(--border-radius-large); /* Common.css variable */
margin: 0;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
text-align: left;
}
.user-message p {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
border-bottom-right-radius: var(--border-radius-small); /* Common.css variable */
}
.ai-message p {
border-bottom-left-radius: 4px;
}
.input-area {
display: flex;
align-items: center;
padding: 20px 24px;
border-top: 1px solid var(--border-color); /* Common.css variable */
background: transparent;
gap: var(--spacing-md); /* Was 12px */
}
.intelligence-input {
flex: 1;
background: var(--bg-light); /* Align with common.css .input-base */
color: var(--text-primary);
padding: 12px 16px; /* Specific padding, common.css input-base is var(--spacing-sm) (8px) */
border-radius: var(--border-radius-medium); /* Common.css variable */
font-size: 14px;
transition: all 0.2s ease;
}
.intelligence-input:focus {
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Common input focus shadow */
}
.intelligence-input::placeholder {
color: var(--text-disabled); /* Align with common.css .input-base */
}
.send-button {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
border: none;
padding: 12px 20px;
border-radius: var(--border-radius-medium); /* Common.css variable */
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.send-button:hover {
background: color-mix(in srgb, var(--primary-accent) 85%, white); /* Common primary button hover */
}
.send-button:disabled {
background: var(--surface-dark); /* Common disabled background */
color: var(--text-disabled); /* Common disabled text color */
cursor: not-allowed;
}
/* Scrollbar styling is now handled globally by common.css */

View File

@ -0,0 +1,465 @@
/* Library View - Ultra Minimalistic Design */
/* :root variables moved to common.css or are view-specific if necessary */
.sidebar-layout {
display: flex;
flex-direction: row;
height: calc(100vh - 120px);
margin: 100px 40px;
gap: var(--spacing-lg);
}
.layout {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
margin: 100px 40px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.layout .library-content {
flex: 1;
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
overflow-y: auto;
}
.library-content > h1 {
margin-bottom: 10px;
}
.library-sidebar {
width: 280px;
overflow-y: auto;
flex-shrink: 0;
}
.main {
display: flex;
flex: 1;
}
.sidebar {
width: 280px;
overflow-y: auto;
height: fit-content;
display: flex;
gap: 10px;
flex-direction: column;
flex-shrink: 0;
}
.sidebar .card {
background: var(--surface-dark);
color: var(--text-secondary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-medium);
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease, border-color 0.2s ease;
cursor: pointer;
}
.sidebar .cards-column {
overflow: auto;
border-radius: var(--border-radius-medium);
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar-section {
margin-bottom: 32px;
}
.sidebar-section:last-child {
margin-bottom: 0;
}
.sidebar-section h4 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--text-primary);
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.no-collections-message {
padding: 16px 12px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
font-style: italic;
}
.library-content {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
}
/* Collections Grid for main view */
.collections-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
/* Click-outside areas for navigation */
.view-container.layout[onclick],
.view-container.sidebar-layout[onclick] {
cursor: pointer;
}
.library-content[onclick] {
cursor: default;
}
.library-content > header {
margin-bottom: 20px;
}
.library-items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: var(--spacing-md);
}
/* Library Item Card in Main Content Grid */
.library-item-card {
border-radius: var(--border-radius-medium);
display: flex;
flex-direction: column;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
height: 180px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.library-item-card:hover {
background: var(--surface-dark);
transform: translateY(-2px);
border-color: var(--primary-accent);
box-shadow: 0 4px 10px color-mix(in srgb, var(--primary-accent) 20%, transparent);
}
.library-item-card .item-preview {
height: 100px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--surface-dark);
border-bottom: 1px solid var(--border-color);
overflow: hidden;
}
.library-item-card .item-preview .item-thumbnail-img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.library-item-card .item-preview .item-preview-fallback-icon {
font-size: 36px;
color: var(--text-muted);
}
.library-item-card .item-details {
padding: 12px;
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.library-item-card .item-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.library-item-card .item-description {
font-size: 12px;
color: var(--text-secondary);
margin: 0 0 6px 0;
line-height: 1.3;
height: 2.6em; /* Approx 2 lines */
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-clamp: 2; /* Standard property */
}
.library-item-card .item-meta {
font-size: 11px;
color: var(--text-muted);
margin-top: auto; /* Pushes to the bottom */
}
/* Removed .shelf, .shelf-header, .shelf-label-icon, .shelf-label, .shelf-description, .shelf-items, .shelf-item as they are not used by the current LibraryView component structure */
/* Removed .filter-btn and related styles as .collection-list-item is used instead */
.item-preview-fallback-icon {
font-size: 24px;
color: var(--text-muted);
}
.csv-preview-table {
width: 100%;
height: 100%;
border-collapse: collapse;
font-size: 8px;
table-layout: fixed;
}
.csv-preview-table th,
.csv-preview-table td {
border: 1px solid var(--border-color); /* Common.css variable */
padding: 2px 3px;
text-align: left;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.csv-preview-table th {
background: var(--surface-light); /* Common.css variable for table header */
font-weight: 600;
color: var(--text-primary);
}
.item-name {
padding: 12px;
font-size: 12px;
line-height: 1.3;
color: var(--text-primary);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
/* Asset Viewer Styles */
.asset-viewer {
display: flex;
flex-direction: column;
height: 100%;
}
.viewer-header {
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color-subtle);
}
.viewer-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.viewer-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.viewer-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--border-radius-medium);
}
.pdf-frame {
width: 100%;
height: 100%;
border: none;
border-radius: var(--border-radius-medium);
}
.markdown-content {
overflow-y: auto;
padding: var(--spacing-md);
background: var(--surface-medium);
border-radius: var(--border-radius-medium);
}
.book-navigation,
.slides-navigation {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-top: var(--spacing-md);
}
.nav-button {
background: var(--surface-medium);
color: var(--text-secondary);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: var(--border-radius-medium);
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.nav-button:hover:not(:disabled) {
background: var(--surface-light);
color: var(--text-primary);
border-color: var(--primary-accent);
}
.nav-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-indicator,
.slide-indicator {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.book-page {
overflow-y: auto;
padding: var(--spacing-md);
background: var(--surface-medium);
border-radius: var(--border-radius-medium);
flex: 1;
}
.slide-container {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
margin-bottom: var(--spacing-md);
}
.slide {
max-width: 100%;
max-height: 100%;
text-align: center;
}
.slide-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--border-radius-medium);
}
.slide-title {
margin-top: var(--spacing-sm);
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.slide-thumbnails {
display: flex;
gap: var(--spacing-sm);
overflow-x: auto;
padding: var(--spacing-sm);
background: var(--surface-medium);
border-radius: var(--border-radius-medium);
}
.slide-thumbnail {
flex-shrink: 0;
width: 60px;
height: 40px;
position: relative;
cursor: pointer;
border: 2px solid transparent;
border-radius: var(--border-radius-small);
overflow: hidden;
transition: all 0.2s ease;
}
.slide-thumbnail:hover {
border-color: var(--primary-accent);
}
.slide-thumbnail.active {
border-color: var(--primary-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-accent) 30%, transparent);
}
.slide-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-number {
position: absolute;
bottom: 2px;
right: 2px;
background: rgba(0, 0, 0, 0.7);
color: white;
font-size: 10px;
padding: 1px 3px;
border-radius: 2px;
}
/* Scrollbar styling is now handled globally by common.css */
/* Responsive adjustments */
@media (max-width: 768px) {
.sidebar-layout {
margin: 40px 20px;
flex-direction: column;
}
.layout {
margin: 40px 20px;
}
.library-sidebar {
width: 100%;
order: 2;
}
.library-content {
order: 1;
}
.collections-grid {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
.library-items-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
}
.library-item-card {
height: 140px;
}
.library-item-card .item-preview {
height: 80px;
}
}

View File

@ -0,0 +1,480 @@
/* Library Viewer Styles */
/* Asset Details Card (Sidebar) */
.asset-details-card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
color: #fff;
}
.back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #333;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
}
.back-button:hover {
background: #444;
}
.asset-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
background: #222;
border-radius: 8px;
overflow: hidden;
}
.asset-preview-image {
max-width: 100%;
max-height: 120px;
object-fit: cover;
border-radius: 4px;
}
.asset-preview-icon {
font-size: 3rem;
color: #666;
}
.asset-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.asset-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #fff;
line-height: 1.3;
}
.asset-description {
margin: 0;
color: #aaa;
font-size: 0.9rem;
line-height: 1.4;
}
.asset-metadata {
display: flex;
flex-direction: column;
gap: 4px;
}
.asset-metadata p {
margin: 0;
font-size: 0.85rem;
color: #ccc;
}
.asset-metadata strong {
color: #fff;
}
/* Asset Viewer (Main Content) */
.asset-viewer {
display: flex;
flex-direction: column;
height: 100%;
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
border: 1px solid #333;
}
.viewer-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #333;
background: #222;
border-radius: 12px 12px 0 0;
}
.viewer-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #fff;
}
.viewer-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 20px;
min-height: 400px;
}
/* Image Viewer */
.viewer-image {
max-width: 100%;
max-height: calc(100vh - 300px);
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* PDF Viewer */
.pdf-frame {
width: 100%;
height: calc(100vh - 200px);
min-height: 500px;
border: none;
background: #fff;
border-radius: 4px;
}
.external-link {
color: #60a5fa;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
display: inline-block;
margin-top: 8px;
padding: 6px 12px;
background: #333;
border-radius: 4px;
font-size: 0.85rem;
}
.external-link:hover {
color: #93c5fd;
background: #444;
}
/* Markdown Content */
.markdown-content {
padding: 0;
overflow-y: auto;
line-height: 1.6;
color: #e5e5e5;
max-height: calc(100vh - 200px);
}
/* Table of Contents */
.table-of-contents {
margin-top: 16px;
}
.table-of-contents h4 {
margin: 0 0 8px 0;
color: #fff;
font-size: 0.9rem;
font-weight: 600;
}
.toc-list {
list-style: none;
padding: 0;
margin: 0;
}
.toc-item {
margin: 4px 0;
}
.toc-link {
display: block;
width: 100%;
padding: 6px 8px;
background: #333;
color: #ccc;
border: none;
border-radius: 4px;
text-align: left;
cursor: pointer;
font-size: 0.8rem;
transition: background-color 0.2s ease;
}
.toc-link:hover {
background: #444;
color: #fff;
}
/* Book Viewer */
.book-viewer .viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.book-navigation {
display: flex;
align-items: center;
gap: 12px;
}
.nav-button {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #333;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
}
.nav-button:hover:not(:disabled) {
background: #444;
}
.nav-button:disabled {
background: #222;
color: #666;
cursor: not-allowed;
}
.page-indicator,
.slide-indicator {
font-size: 0.9rem;
color: #aaa;
font-weight: 500;
}
.book-page {
padding: 20px;
background: #222;
border-radius: 8px;
max-height: calc(100vh - 250px);
overflow-y: auto;
}
/* Slides Viewer */
.slides-viewer .viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.slides-navigation {
display: flex;
align-items: center;
gap: 12px;
}
.slide-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
background: #222;
border-radius: 8px;
margin-bottom: 20px;
}
.slide {
text-align: center;
max-width: 100%;
max-height: 100%;
}
.slide-image {
max-width: 100%;
max-height: calc(100vh - 350px);
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.slide-title {
margin-top: 12px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
}
.slide-thumbnails {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 8px 0;
background: #222;
border-radius: 8px;
padding: 12px;
}
.slide-thumbnail {
position: relative;
flex-shrink: 0;
width: 80px;
height: 60px;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s ease, transform 0.2s ease;
}
.slide-thumbnail:hover {
transform: scale(1.05);
border-color: #555;
}
.slide-thumbnail.active {
border-color: #60a5fa;
}
.slide-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-number {
position: absolute;
top: 2px;
right: 2px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 0.7rem;
padding: 2px 4px;
border-radius: 2px;
font-weight: 500;
}
.markdown-content h1 {
font-size: 2rem;
font-weight: 700;
margin: 0 0 16px 0;
color: #fff;
border-bottom: 2px solid #333;
padding-bottom: 8px;
}
.markdown-content h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 24px 0 12px 0;
color: #fff;
}
.markdown-content h3 {
font-size: 1.25rem;
font-weight: 600;
margin: 20px 0 8px 0;
color: #fff;
}
.markdown-content p {
margin: 0 0 12px 0;
color: #d1d5db;
}
.markdown-content li {
margin: 4px 0;
color: #d1d5db;
list-style-type: disc;
margin-left: 20px;
}
.markdown-content strong {
font-weight: 600;
color: #fff;
}
.markdown-content br {
margin: 8px 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.asset-details-card {
padding: 16px;
}
.asset-preview {
min-height: 80px;
}
.asset-preview-image {
max-height: 80px;
}
.asset-title {
font-size: 1.1rem;
}
.viewer-header {
padding: 16px 20px 12px;
}
.viewer-title {
font-size: 1.25rem;
}
.viewer-content {
padding: 16px;
min-height: 300px;
}
.pdf-frame {
height: calc(100vh - 250px);
min-height: 400px;
}
.markdown-content {
max-height: calc(100vh - 250px);
}
}
/* Smooth animations */
.asset-viewer {
animation: slideIn 0.3s ease-out;
}
.asset-details-card {
animation: slideIn 0.3s ease-out 0.1s both;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Library item cards hover effect */
.library-item-card {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.library-item-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}

View File

@ -0,0 +1,125 @@
.members-view .view-title { /* Assuming h1 in component is styled this way */
color: var(--primary-accent); /* Common.css variable */
text-align: center;
margin-bottom: var(--spacing-xl); /* Was 30px */
font-size: 2em;
}
.member-ripples-container { /* Default flex layout */
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 35px; /* Increased gap for circular elements */
padding: 20px 0; /* Add some vertical padding to the container */
}
.member-ripples-container.geometric-layout { /* For 1-4 members */
position: relative; /* Context for absolute positioning */
min-height: 400px; /* Ensure container has space for positioned items */
/* Override flex properties if they were set directly on the class before */
display: block; /* Or whatever is appropriate to override flex if needed */
flex-wrap: nowrap; /* Override */
justify-content: initial; /* Override */
gap: 0; /* Override */
padding: 0; /* Override or adjust as needed */
}
/* Styles for .member-ripple when inside .geometric-layout */
.member-ripples-container.geometric-layout .member-ripple {
position: absolute;
/* left, top, and transform will be set by inline styles from Rust */
/* Other .member-ripple styles (size, border-radius, etc.) still apply */
}
/* End of rules for .member-ripples-container and its variants */
.member-ripple { /* Renamed from .member-card */
background-color: var(--surface-dark); /* Common.css variable */
width: 180px;
height: 180px;
border-radius: 50%; /* Circular shape */
padding: var(--spacing-md); /* Was 15px */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: var(--shadow-medium); /* Common.css variable, was 0 5px 15px rgba(0,0,0,0.35) */
border: 2px solid var(--border-color); /* Common.css variable */
transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease; /* Common.css variable */
position: relative; /* For potential future ripple pseudo-elements */
}
.member-ripple:hover {
transform: scale(1.05);
box-shadow: 0 0 25px color-mix(in srgb, var(--primary-accent) 40%, transparent); /* Glow with common primary */
}
.member-avatar {
position: relative; /* For status indicator positioning */
width: 90px; /* Slightly smaller to give more space for text */
height: 90px; /* Keep square */
margin-bottom: 10px; /* Adjusted margin for flex layout */
border-radius: 50%;
overflow: hidden; /* Ensures image stays within circle */
border: 3px solid var(--primary-accent); /* Common.css variable */
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.status-indicator { /* No changes needed for status indicator position relative to avatar */
position: absolute;
bottom: 5px; /* Relative to avatar */
right: 5px; /* Relative to avatar */
width: 18px; /* Slightly smaller to fit smaller avatar */
height: 18px;
border-radius: 50%;
border: 2px solid var(--surface-dark); /* Common.css variable, for contrast */
}
.status-online {
background-color: #4CAF50; /* Green */
}
.status-away {
background-color: #FFC107; /* Amber */
}
.status-offline {
background-color: #757575; /* Grey */
}
.member-info { /* Added to ensure text is constrained if needed */
max-width: 90%; /* Prevent text overflow if names are too long */
}
.member-info h3 {
margin: 5px 0 2px 0; /* Adjusted margins */
color: var(--text-primary); /* Common.css variable */
font-size: 1.0em; /* Slightly smaller */
line-height: 1.2;
word-break: break-word; /* Prevent long names from breaking layout */
}
.member-info .member-role {
font-size: 0.8em; /* Slightly smaller */
color: var(--text-secondary); /* Common.css variable */
font-style: italic;
line-height: 1.1;
word-break: break-word;
}
.members-view .empty-state {
text-align: center;
padding: 50px;
color: var(--text-secondary); /* Common.css variable */
}
.members-view .empty-state p {
margin-bottom: 10px;
font-size: 1.1em;
}

View File

@ -0,0 +1,97 @@
/* Navigation Island - from original Yew project CSS */
/* Navigation Island - simplified transform-based collapse/expand */
.nav-island {
position: fixed;
right: 28px;
bottom: 16px;
display: flex;
flex-direction: row;
align-items: flex-end;
background: var(--bg-medium); /* Was rgba(30,32,40,0.98), using common.css variable, assuming full opacity is acceptable */
border-radius: var(--border-radius-large); /* Was 14px, using common.css variable (16px) */
box-shadow: 0 8px 32px 0 rgba(0,0,0,0.18), 0 1.5px 8px 0 rgba(0,0,0,0.10); /* Keeping specific shadow */
padding: 4px 9px;
z-index: 9000;
overflow: hidden;
transition: width 0.42s ease;
will-change: width;
}
.nav-island.collapsed {
width: 88px;
}
.nav-island.collapsed:hover:not(.clicked),
.nav-island.collapsed:focus-within:not(.clicked) {
width: 720px; /* 9 buttons × (82px + 6px margin) + padding = ~816px */
}
.nav-island.clicked {
width: 88px !important;
}
.nav-island-buttons {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 0;
white-space: nowrap;
transition: transform 0.42s ease;
will-change: transform;
}
.nav-island.collapsed .nav-island-buttons {
transform: translateX(-6px); /* Small offset to keep first button visible */
}
.nav-island.collapsed:hover:not(.clicked) .nav-island-buttons,
.nav-island.collapsed:focus-within:not(.clicked) .nav-island-buttons {
transform: translateX(0);
}
.nav-island.clicked .nav-island-buttons {
transform: translateX(-6px) !important;
}
.nav-button {
width: 82px;
background-color: transparent;
color: var(--text-secondary); /* Common.css variable */
border: 1px solid transparent;
border-radius: var(--border-radius-medium); /* Was 10px, using common.css variable (8px) */
font-size: 0.85em;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
transition: background 0.2s, color 0.2s, border 0.2s;
margin-right: 6px;
outline: none;
padding: 10px 15px;
flex-shrink: 0;
}
.nav-button i {
font-size: 1.2em;
}
.nav-button:hover {
background-color: color-mix(in srgb, var(--primary-accent) 10%, transparent); /* Common.css primary with alpha */
color: var(--primary-accent); /* Common.css variable */
border-color: var(--primary-accent); /* Common.css variable */
}
.nav-button.active {
background-color: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Common.css variable */
font-weight: 600;
box-shadow: 0 0 10px var(--primary-accent); /* Glow with common primary */
}
.nav-button.active:hover {
background-color: var(--primary-accent); /* Common.css variable, keep active color on hover */
color: var(--bg-dark); /* Common.css variable */
}

View File

@ -0,0 +1,179 @@
/* Network Animation Styles - Ultra Minimal and Sleek */
.network-animation-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.network-overlay-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
/* Ensure the world map container has relative positioning and proper sizing */
.network-map-container {
position: relative !important;
width: 100% !important;
height: auto !important;
min-height: 400px;
aspect-ratio: 783.086 / 400.649; /* Maintain SVG aspect ratio */
}
.network-map-container svg {
position: relative;
z-index: 1;
width: 100%;
height: auto;
}
/* Make the world map darker */
.network-map-container svg path {
fill: #6c757d !important; /* Darker gray for the map */
opacity: 0.7;
}
/* Server Node Styles - Clean white and bright */
.server-node {
cursor: pointer;
}
.node-glow {
fill: rgba(255, 255, 255, 0.1);
opacity: 0.8;
}
.node-pin {
fill: #ffffff;
stroke: rgba(0, 123, 255, 0.3);
stroke-width: 1;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
filter: drop-shadow(0 2px 8px rgba(0, 123, 255, 0.2));
}
.node-pin:hover {
fill: #ffffff;
stroke: rgba(0, 123, 255, 0.6);
stroke-width: 2;
filter: drop-shadow(0 4px 12px rgba(0, 123, 255, 0.3));
transform: scale(1.1);
}
.node-core {
fill: #007bff;
opacity: 1;
}
/* Remove the ugly pulse - replace with subtle breathing effect */
.node-pulse {
fill: none;
stroke: rgba(0, 123, 255, 0.2);
stroke-width: 1;
opacity: 0;
animation: gentle-breathe 6s ease-in-out infinite;
}
@keyframes gentle-breathe {
0%, 100% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.2);
}
}
.node-label {
fill: #495057;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
font-weight: 500;
opacity: 0.9;
}
/* Transmission Styles - More visible with shadow */
.transmission-group {
opacity: 0.8;
}
.transmission-line {
fill: none;
stroke: rgba(0, 123, 255, 0.7);
stroke-width: 2;
stroke-linecap: round;
opacity: 0;
animation: fade-in-out 4s ease-in-out infinite;
filter: drop-shadow(0 0 4px rgba(0, 123, 255, 0.3));
}
@keyframes fade-in-out {
0%, 100% { opacity: 0; }
50% { opacity: 0.8; }
}
/* Responsive Design */
@media (max-width: 768px) {
.node-label {
font-size: 9px;
}
.network-map-container {
min-height: 300px;
}
}
@media (max-width: 480px) {
.node-label {
font-size: 8px;
}
.network-map-container {
min-height: 250px;
}
}
/* Dark theme compatibility */
@media (prefers-color-scheme: dark) {
.network-map-container svg path {
fill: #404040 !important;
}
.node-label {
fill: rgba(255, 255, 255, 0.8);
}
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.node-pulse,
.transmission-line {
animation: none;
}
.transmission-line {
opacity: 0.4;
}
.node-pin {
transition: none;
}
}
/* Performance optimizations */
.network-overlay-svg * {
will-change: transform, opacity;
}
.server-node {
/* Removed translateZ(0) as it was causing positioning issues */
}

View File

@ -0,0 +1,977 @@
/* Projects View - Game-like Minimalistic Design */
/* :root variables moved to common.css or are view-specific if necessary */
.projects-view-container {
/* Extends .view-container from common.css */
height: calc(100vh - 120px); /* Specific height */
margin: 100px 40px 60px 40px; /* Specific margins */
/* font-family will be inherited from common.css body */
/* Other .view-container properties are inherited or set by common.css */
}
/* Header */
.projects-header {
/* Extends .view-header from common.css */
/* .view-header provides: display, justify-content, align-items, padding-bottom, border-bottom, margin-bottom */
/* Original margin-bottom: 24px (var(--spacing-lg)); padding: 0 8px (var(--spacing-sm) on sides) */
/* common.css .view-header has margin-bottom: var(--spacing-md) (16px). Override if 24px is needed. */
margin-bottom: var(--spacing-lg); /* Explicitly use 24px equivalent */
padding: 0 var(--spacing-sm); /* Explicitly use 8px equivalent for side padding */
}
.projects-tabs {
display: flex;
gap: var(--spacing-sm); /* Was 8px */
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Was 12px */
padding: 6px; /* Specific padding */
}
.tab-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm); /* Was 8px */
background: transparent;
color: var(--text-secondary); /* Common.css variable */
border: none;
padding: 12px 16px; /* Specific padding */
border-radius: var(--border-radius-medium); /* Common.css variable */
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.tab-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s ease;
}
.tab-btn:hover::before {
left: 100%;
}
.tab-btn:hover {
color: var(--text-primary); /* Common.css variable */
background: var(--surface-medium); /* Common.css variable for hover */
transform: translateY(-1px);
}
.tab-btn.active {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
box-shadow: 0 0 20px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.tab-btn i {
font-size: 16px;
}
.projects-actions {
display: flex;
gap: 12px;
}
.action-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm); /* Was 8px */
background: var(--surface-dark); /* Common.css variable */
color: var(--text-primary); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
padding: 12px 20px; /* Specific padding */
border-radius: var(--border-radius-medium); /* Common.css variable */
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.action-btn.primary {
background: var(--primary-accent); /* Common.css variable */
border-color: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color for primary button */
box-shadow: 0 0 15px color-mix(in srgb, var(--primary-accent) 20%, transparent); /* Glow with common primary */
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); /* Specific shadow */
background: var(--surface-medium); /* Example: align hover bg with common */
border-color: var(--primary-accent); /* Example: align hover border with common */
}
.action-btn.primary:hover {
box-shadow: 0 4px 25px color-mix(in srgb, var(--primary-accent) 40%, transparent); /* Glow with common primary */
background: color-mix(in srgb, var(--primary-accent) 85%, white); /* Common primary button hover */
}
/* Content Area */
.projects-content {
flex: 1;
overflow: hidden;
border-radius: 12px;
}
/* Kanban Board */
.kanban-board {
display: flex;
gap: 20px;
height: 100%;
overflow-x: auto;
padding: 8px;
}
.kanban-column {
min-width: 300px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
display: flex;
flex-direction: column;
overflow: hidden;
}
.column-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color); /* Common.css variable */
display: flex;
justify-content: space-between;
align-items: center;
background: var(--surface-light); /* Common.css variable */
}
.column-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.task-count {
background: var(--surface-medium); /* Common.css variable */
color: var(--text-secondary); /* Common.css variable */
padding: 4px 8px;
border-radius: var(--border-radius-large); /* Common.css variable */
font-size: 12px;
font-weight: 600;
}
.column-content {
flex: 1;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Task Cards */
.task-card {
background: var(--surface-light); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Common.css variable */
padding: var(--spacing-md); /* Was 16px */
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.task-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.05), transparent);
opacity: 0;
transition: opacity 0.2s ease;
}
.task-card:hover::before {
opacity: 1;
}
.task-card:hover {
transform: translateY(-2px);
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* Specific shadow */
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-priority {
width: 8px;
height: 8px;
border-radius: 50%;
}
.task-id {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
}
.task-title {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
.task-description {
margin: 0 0 12px 0;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.task-assignee {
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
border: 2px solid var(--border-color); /* Common.css variable */
}
.task-assignee img {
width: 100%;
height: 100%;
object-fit: cover;
}
.task-assignee.unassigned {
background: var(--surface-medium); /* Common.css variable */
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 10px;
}
.story-points {
background: #06b6d4; /* Literal info color */
color: var(--bg-dark); /* Text on cyan */
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.task-tags {
display: flex;
gap: 4px;
}
.tag {
background: var(--surface-medium); /* Common.css variable */
color: var(--text-muted); /* Common.css variable */
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
}
.add-task-placeholder {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm); /* Was 8px */
padding: var(--spacing-md); /* Was 16px */
border: 2px dashed var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Common.css variable */
color: var(--text-muted); /* Common.css variable */
cursor: pointer;
transition: all 0.2s ease;
margin-top: auto;
}
.add-task-placeholder:hover {
border-color: var(--primary-accent); /* Common.css variable */
color: var(--primary-accent); /* Common.css variable */
background: color-mix(in srgb, var(--primary-accent) 5%, transparent); /* Use common primary with alpha */
}
/* Epics View */
.epics-view {
height: 100%;
overflow-y: auto;
padding: 8px;
}
.epics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.epic-card {
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
overflow: hidden;
transition: all 0.2s ease;
cursor: pointer;
}
.epic-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.epic-header {
padding: 20px;
color: white;
position: relative;
overflow: hidden;
}
.epic-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
}
.epic-header h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 700;
position: relative;
z-index: 1;
}
.epic-status {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
opacity: 0.9;
position: relative;
z-index: 1;
}
.epic-content {
padding: 20px;
}
.epic-description {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.epic-progress {
margin-bottom: 16px;
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--surface-medium); /* Common.css variable */
border-radius: var(--border-radius-small); /* Common.css variable */
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.epic-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.epic-owner {
display: flex;
align-items: center;
gap: 8px;
}
.epic-owner img,
.avatar-placeholder {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid var(--border-color); /* Common.css variable */
}
.avatar-placeholder {
background: var(--surface-medium); /* Common.css variable */
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 10px;
}
.epic-owner span {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
}
.epic-date {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
/* Sprints View */
.sprints-view {
height: 100%;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sprint-card {
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-lg); /* Was 20px */
transition: all 0.2s ease;
cursor: pointer;
}
.sprint-card:hover {
transform: translateY(-2px);
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* Specific shadow */
}
.sprint-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.sprint-info h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.sprint-goal {
margin: 0;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.4;
}
.sprint-badge {
background: var(--surface-medium); /* Common.css variable */
color: var(--text-secondary); /* Common.css variable */
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.sprint-badge.active {
background: #10b981; /* Literal success color */
color: white;
box-shadow: 0 0 15px color-mix(in srgb, #10b981 30%, transparent); /* Glow with literal success */
}
.sprint-metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.metric {
text-align: center;
}
.metric-value {
display: block;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.metric-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
font-weight: 500;
}
.sprint-progress {
margin-bottom: 16px;
}
.sprint-dates {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
/* Roadmap View */
.roadmap-view {
height: 100%;
overflow-y: auto;
padding: 8px 40px;
}
.roadmap-timeline {
position: relative;
padding-left: 40px;
}
.roadmap-timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border-color); /* Common.css variable */
}
.roadmap-item {
position: relative;
margin-bottom: 32px;
padding-left: 40px;
}
.roadmap-marker {
position: absolute;
left: -28px;
top: 8px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 4px solid var(--surface-dark); /* Common.css variable */
box-shadow: 0 0 0 2px var(--border-color); /* Common.css variable */
}
.roadmap-content {
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-lg); /* Was 20px */
transition: all 0.2s ease;
}
.roadmap-content:hover {
transform: translateX(8px);
border-color: var(--primary-accent); /* Common.css variable */
}
.roadmap-content h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.roadmap-content p {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.roadmap-progress {
display: flex;
align-items: center;
gap: 12px;
}
.roadmap-progress .progress-bar {
flex: 1;
}
.roadmap-progress span {
font-size: 12px;
color: var(--text-muted);
font-weight: 600;
}
/* Analytics View */
.analytics-view {
height: 100%;
overflow-y: auto;
padding: 8px;
}
.analytics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.analytics-card {
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-lg); /* Was 24px */
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.analytics-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.05), transparent);
opacity: 0;
transition: opacity 0.2s ease;
}
.analytics-card:hover::before {
opacity: 1;
}
.analytics-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
position: relative;
z-index: 1;
}
.card-icon.tasks { background: var(--primary-accent); } /* Common.css variable */
.card-icon.completed { background: #10b981; } /* Literal success color */
.card-icon.progress { background: #f59e0b; } /* Literal warning color */
.card-icon.epics { background: #8b5cf6; } /* Literal accent color */
.card-icon.sprints { background: #06b6d4; } /* Literal info color */
/* Text color for .card-icon.progress should be var(--bg-dark) for contrast */
.card-icon.progress { color: var(--bg-dark); }
.card-icon.sprints { color: var(--bg-dark); } /* Text on cyan */
.card-content h3 {
margin: 0 0 4px 0;
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.card-content p {
margin: 0;
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
/* Gantt View */
.gantt-view {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden; /* Allow internal scrolling for chart */
padding: 8px;
}
.gantt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 8px;
}
.gantt-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.gantt-controls {
display: flex;
gap: 8px;
}
.gantt-zoom-btn {
background: var(--surface-dark); /* Common.css variable */
color: var(--text-secondary); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
padding: 8px 12px;
border-radius: var(--border-radius-medium); /* Was 6px */
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
}
.gantt-zoom-btn:hover {
color: var(--text-primary); /* Common.css variable */
background: var(--surface-medium); /* Common.css variable */
border-color: var(--primary-accent); /* Common.css variable */
}
.gantt-zoom-btn.active {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text on primary accent */
border-color: var(--primary-accent); /* Common.css variable */
box-shadow: 0 0 10px color-mix(in srgb, var(--primary-accent) 30%, transparent); /* Glow with common primary */
}
.gantt-chart {
flex: 1;
overflow-x: auto; /* Horizontal scroll for timeline */
overflow-y: auto; /* Vertical scroll for rows */
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-md); /* Was 16px */
}
.gantt-timeline {
position: relative;
min-width: 1200px; /* Ensure there's enough space for a year view, adjust as needed */
}
.timeline-header {
position: sticky;
top: 0;
background: var(--surface-light); /* Common.css variable */
z-index: 10;
border-bottom: 1px solid var(--border-color); /* Common.css variable */
padding-bottom: var(--spacing-sm); /* Was 8px */
margin-bottom: 8px;
}
.timeline-months {
display: flex;
white-space: nowrap;
}
.timeline-month {
flex: 1 0 auto; /* Allow shrinking but prefer base size */
min-width: 80px; /* Approximate width for a month, adjust as needed */
text-align: center;
padding: 8px 0;
color: var(--text-secondary); /* Common.css variable */
font-size: 12px;
font-weight: 500;
border-right: 1px solid var(--border-color); /* Common.css variable */
}
.timeline-month:last-child {
border-right: none;
}
.gantt-rows {
position: relative;
}
.gantt-row {
display: flex;
align-items: stretch; /* Make label and timeline same height */
border-bottom: 1px solid var(--border-color); /* Common.css variable */
transition: background-color var(--transition-speed) ease;
}
.gantt-row:last-child {
border-bottom: none;
}
.gantt-row:hover {
background-color: var(--surface-medium); /* Common.css variable */
}
.gantt-row-label {
width: 250px; /* Fixed width for labels */
padding: 12px 16px;
border-right: 1px solid var(--border-color); /* Common.css variable */
background-color: var(--surface-light); /* Common.css variable */
flex-shrink: 0; /* Prevent label from shrinking */
display: flex;
flex-direction: column;
justify-content: center;
}
.epic-info h4 {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.epic-progress-text {
font-size: 11px;
color: var(--text-muted);
font-weight: 500;
}
.gantt-row-timeline {
flex: 1;
position: relative;
padding: 12px 0; /* Vertical padding for bars */
min-height: 40px; /* Ensure row has some height for the bar */
}
.gantt-bar {
position: absolute;
height: 24px; /* Height of the bar */
top: 50%;
transform: translateY(-50%);
border-radius: var(--border-radius-small); /* Common.css variable */
background-color: var(--primary-accent); /* Default bar color, Common.css variable */
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Specific shadow */
display: flex;
align-items: center;
overflow: hidden;
transition: all 0.2s ease;
}
.gantt-bar:hover {
opacity: 1 !important; /* Ensure hover is visible */
transform: translateY(-50%) scale(1.02);
}
.gantt-progress {
height: 100%;
background-color: #10b981; /* Literal success color */
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small); /* Common.css variable */
opacity: 0.7;
transition: width 0.3s ease;
}
.gantt-bar .gantt-progress[style*="width: 100%"] {
border-radius: var(--border-radius-small); /* Common.css variable */
}
/* Scrollbar styling is now handled globally by common.css */
/* Responsive design */
@media (max-width: 768px) {
.projects-view-container {
margin: 20px;
height: calc(100vh - 80px);
}
.projects-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.projects-tabs {
justify-content: center;
}
.kanban-board {
flex-direction: column;
gap: 16px;
}
.kanban-column {
min-width: auto;
max-height: 400px;
}
.epics-grid {
grid-template-columns: 1fr;
}
.sprint-metrics {
grid-template-columns: repeat(2, 1fr);
}
.analytics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.projects-tabs {
flex-wrap: wrap;
}
.tab-btn {
flex: 1;
min-width: 0;
justify-content: center;
}
.tab-btn span {
display: none;
}
.sprint-metrics,
.analytics-grid {
grid-template-columns: 1fr;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,320 @@
/* Timeline View - Ultra Minimalistic Design */
/* :root variables moved to common.css or are view-specific if necessary */
.timeline-view-container {
/* Extends .view-container from common.css but with flex-direction: row */
flex-direction: row; /* Specific direction for this view */
height: calc(100vh - 120px); /* Specific height */
margin: 60px 40px 60px 40px; /* Specific margins */
gap: var(--spacing-lg); /* Was 24px */
/* font-family will be inherited from common.css body */
/* Other .view-container properties are inherited or set by common.css */
}
.timeline-sidebar {
width: 280px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-lg); /* Was 24px */
overflow-y: auto;
flex-shrink: 0;
}
.timeline-sidebar h4 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--text-primary);
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.sidebar-section {
margin-bottom: 32px;
}
.sidebar-section:last-child {
margin-bottom: 0;
}
.filter-options,
.time-range-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-btn {
background: transparent;
color: var(--text-secondary); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
padding: 12px 16px;
border-radius: var(--border-radius-medium); /* Common.css variable */
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
text-align: left;
}
.filter-btn:hover {
background: var(--surface-medium); /* Common.css variable for hover */
color: var(--text-primary); /* Common.css variable */
border-color: var(--primary-accent); /* Common interaction color */
}
.filter-btn.active {
background: var(--primary-accent); /* Common.css variable */
color: var(--bg-dark); /* Text color on primary accent */
border-color: var(--primary-accent); /* Common.css variable */
}
.timeline-content {
flex: 1;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
overflow-y: auto;
padding: var(--spacing-lg); /* Was 24px */
}
.timeline-feed {
display: flex;
flex-direction: column;
gap: 32px;
}
.timeline-day-group {
display: flex;
flex-direction: column;
}
.timeline-date-header {
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color); /* Common.css variable */
}
.timeline-date-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.timeline-day-actions {
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}
.timeline-day-actions::before {
content: '';
position: absolute;
left: 32px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border-color); /* Common.css variable */
}
.timeline-action {
display: flex;
gap: 16px;
position: relative;
padding-left: 8px;
}
.timeline-action-avatar {
position: relative;
flex-shrink: 0;
}
.timeline-action-avatar img {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid var(--border-color); /* Common.css variable */
}
.timeline-action-icon {
position: absolute;
bottom: -4px;
right: -4px;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--surface-dark); /* Common.css variable */
}
.timeline-action-icon i {
font-size: 10px;
color: white;
}
.timeline-action-content {
flex: 1;
background: var(--surface-medium); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-large); /* Common.css variable */
padding: var(--spacing-md); /* Was 16px */
transition: all 0.2s ease;
}
.timeline-action-content:hover {
border-color: var(--primary-accent); /* Common interaction color */
}
.timeline-action-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.4;
}
.timeline-actor-name {
font-weight: 600;
color: var(--text-primary);
}
.timeline-action-title {
color: var(--text-secondary);
}
.timeline-circle-name {
color: var(--text-muted);
font-size: 13px;
}
.timeline-action-description {
margin: 12px 0;
font-size: 14px;
line-height: 1.5;
color: var(--text-secondary);
}
.timeline-action-target {
display: flex;
align-items: center;
gap: var(--spacing-sm); /* Was 8px */
margin: 12px 0;
padding: 8px 12px;
background: var(--surface-dark); /* Common.css variable */
border: 1px solid var(--border-color); /* Common.css variable */
border-radius: var(--border-radius-medium); /* Common.css variable */
font-size: 13px;
color: var(--text-secondary);
}
.timeline-action-target i {
color: var(--text-muted);
font-size: 12px;
}
.timeline-action-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color); /* Common.css variable */
}
.timeline-timestamp {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.timeline-metadata {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.metadata-tag {
background: var(--surface-dark); /* Common.css variable */
color: var(--text-muted); /* Common.css variable */
padding: 4px 8px;
border-radius: var(--border-radius-small); /* Common.css variable */
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border-color); /* Common.css variable */
}
.empty-state {
text-align: center;
color: var(--text-secondary);
font-size: 14px;
margin-top: 40px;
}
/* Action type color variations */
.timeline-action-icon.primary {
background-color: var(--primary-accent); /* Common.css variable */
}
.timeline-action-icon.secondary {
background-color: #6b7280; /* Literal secondary color */
}
.timeline-action-icon.success {
background-color: #10b981; /* Literal success color */
}
.timeline-action-icon.warning {
background-color: #f59e0b; /* Literal warning color */
}
.timeline-action-icon.warning i { color: var(--bg-dark); } /* Adjust icon color for contrast */
.timeline-action-icon.info {
background-color: #06b6d4; /* Literal info color */
}
.timeline-action-icon.info i { color: var(--bg-dark); } /* Adjust icon color for contrast */
.timeline-action-icon.accent {
background-color: #8b5cf6; /* Literal accent color */
}
/* Scrollbar styling is now handled globally by common.css */
/* Responsive design */
@media (max-width: 768px) {
.timeline-view-container {
flex-direction: column;
margin: 20px;
height: calc(100vh - 80px);
}
.timeline-sidebar {
width: 100%;
height: auto;
max-height: 200px;
}
.timeline-day-actions::before {
left: 24px;
}
.timeline-action-avatar img {
width: 40px;
height: 40px;
}
.timeline-action-icon {
width: 20px;
height: 20px;
}
.timeline-action-icon i {
font-size: 8px;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

45
src/app/static/styles.css Normal file
View File

@ -0,0 +1,45 @@
/* app/static/styles.css */
/* Contains remaining global variables or styles not covered by common.css or specific view CSS files. */
.auth-view-container {
position: absolute;
top: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 10px;
background-color: rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 5px;
}
.public-key {
color: #a0a0a0;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9em;
}
.logout-button {
background: none;
border: none;
color: #a0a0a0;
cursor: pointer;
font-size: 1.2em;
}
.logout-button:hover {
color: #ffffff;
}
:root {
/* Shadow for the navigation island, if still used and distinct from common shadows */
--island-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
/* Any other app-specific global variables that don't fit in common.css can go here. */
}
/*
Global reset (*), body styles, and most common variables have been moved to common.css.
Styles for .circles-view have been moved to circles_view.css.
Styles for .dashboard-view have been moved to dashboard_view.css.
*/

Some files were not shown because too many files have changed in this diff Show More