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

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.
*/

1
src/client_ws/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,72 @@
# `client_ws` Architecture
This document details the internal architecture of the `client_ws` crate, focusing on its cross-platform design, internal modules, and the mechanics of its authentication process.
## 1. Core Design Principles
The `client_ws` is built on the following principles:
- **Platform Abstraction**: The core client logic is written in a platform-agnostic way. Platform-specific details (like the WebSocket implementation) are abstracted behind a common interface.
- **Modularity**: The crate is divided into logical modules, with a clear separation of concerns between the main client logic, authentication procedures, and cryptographic utilities.
- **Asynchronous Operations**: All network I/O is asynchronous, using `async/await` to ensure the client is non-blocking and efficient.
- **Fluent Configuration**: A builder pattern (`CircleWsClientBuilder`) is used for clear and flexible client construction.
## 2. Cross-Platform Implementation
To support both native and WebAssembly (WASM) environments, `client_ws` uses conditional compilation (`#[cfg]`) to provide different implementations for the underlying WebSocket transport.
- **Native (`target_arch != "wasm32"`)**: The `tokio-tungstenite` crate is used for robust, `tokio`-based WebSocket communication.
- **WebAssembly (`target_arch = "wasm32"`)**: The `gloo-net` crate provides bindings to the browser's native `WebSocket` API.
This approach allows the `CircleWsClient` to expose a single, unified API while the underlying implementation details are handled transparently at compile time.
## 3. Module Structure
The `client_ws` crate is organized into the following key modules:
- **`lib.rs`**: The main module that defines the `CircleWsClientBuilder` and `CircleWsClient` structs and their public APIs. It orchestrates the entire communication flow.
- **`auth/`**: This module contains all the logic related to the `secp256k1` authentication flow.
- **`types.rs`**: Defines the core data structures used in authentication, such as `AuthError` and `AuthCredentials`.
- **`crypto_utils.rs`**: A self-contained utility module for handling all `secp256k1` cryptographic operations, including key generation, public key derivation, and message signing.
## 4. Authentication Flow Deep Dive
The `authenticate` method in `CircleWsClient` orchestrates the entire authentication process over the WebSocket connection. The sequence diagram below illustrates the internal interactions within the client during this flow.
```mermaid
sequenceDiagram
participant User as User Code
participant Builder as CircleWsClientBuilder
participant Client as CircleWsClient
participant CryptoUtils as auth::crypto_utils
participant WsActor as Server WebSocket Actor
User->>+Builder: new(url)
User->>+Builder: with_keypair(private_key)
User->>+Builder: build()
Builder-->>-User: client
User->>+Client: authenticate()
Client->>Client: Check for private_key
Client->>+CryptoUtils: derive_public_key(private_key)
CryptoUtils-->>-Client: public_key
Note over Client: Request nonce via WebSocket
Client->>+WsActor: JSON-RPC "fetch_nonce" (pubkey)
WsActor-->>-Client: JSON-RPC Response (nonce)
Client->>+CryptoUtils: sign_message(private_key, nonce)
CryptoUtils-->>-Client: signature
Note over Client: Send credentials via WebSocket
Client->>+WsActor: JSON-RPC "authenticate" (pubkey, signature)
WsActor-->>-Client: JSON-RPC Response (authenticated: true/false)
alt Authentication Successful
Client-->>-User: Ok(true)
else Authentication Fails
Client-->>-User: Ok(false) or Err(...)
end
```
This architecture ensures that the cryptographic operations are isolated, the platform-specific code is cleanly separated, and the main client struct provides a simple and consistent API to the end-user.

2764
src/client_ws/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

55
src/client_ws/Cargo.toml Normal file
View File

@@ -0,0 +1,55 @@
[package]
name = "circle_client_ws"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
log = { workspace = true }
futures-channel = { workspace = true, features = ["sink"] }
futures-util = { workspace = true, features = ["sink"] }
thiserror = { workspace = true }
async-trait = { workspace = true }
url = { workspace = true }
http = "0.2"
# Authentication dependencies
hex = { workspace = true }
rand = { workspace = true }
urlencoding = { workspace = true }
# Optional crypto dependencies (enabled by default)
secp256k1 = { workspace = true, optional = true }
sha3 = { workspace = true, optional = true }
# Optional server dependency for end-to-end examples
circle_ws_lib = { path = "../server_ws", optional = true }
# 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"
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Window"] }
# Native-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio-tungstenite = { version = "0.19.0", features = ["native-tls"] }
native-tls = "0.2.11"
tokio-native-tls = "0.3.0"
tokio = { workspace = true, features = ["rt", "macros", "time"] }
[dev-dependencies]
env_logger = { workspace = true }
tokio = { workspace = true }
# Features
[features]
default = ["crypto"]
crypto = ["secp256k1", "sha3"]
end_to_end = ["circle_ws_lib"]

89
src/client_ws/README.md Normal file
View File

@@ -0,0 +1,89 @@
# `client_ws`: The Circles WebSocket Client
The `client_ws` crate provides a high-level, cross-platform WebSocket client for interacting with the `server_ws`. It is designed to work seamlessly in both native Rust applications and WebAssembly (WASM) environments, making it suitable for a wide range of use cases, from command-line tools to web-based frontends.
## Features
- **Cross-Platform**: Works in both native and WASM environments using `tokio-tungstenite` and `gloo-net`, respectively.
- **JSON-RPC 2.0**: Handles all the complexities of JSON-RPC 2.0 communication, including request serialization, response deserialization, and error handling.
- **Asynchronous**: Built with `async/await` for non-blocking I/O.
- **Script Execution**: Provides a simple `play` method to send Rhai scripts to the server for execution.
- **Authentication**: Implements a `secp256k1` signature-based authentication flow, allowing the client to securely identify itself to the server.
- **API**: The client's full API is formally defined in the root [openrpc.json](../../openrpc.json) file.
## Core Components
### `CircleWsClientBuilder`
A builder pattern is used to construct the client, providing a clear and flexible way to configure it.
- `new(ws_url: String)`: Creates a new builder.
- `with_keypair(private_key: String)`: Optionally provides a private key for authentication.
- `build()`: Constructs the `CircleWsClient`.
### `CircleWsClient`
The main client struct.
- `connect()`: Establishes the WebSocket connection.
- `authenticate()`: Performs the full, unified authentication flow over the WebSocket connection. Returns an error if no keypair was provided.
- `play(script: String)`: Sends a Rhai script to the server for execution.
- `disconnect()`: Closes the WebSocket connection.
## Usage Example
The following example demonstrates how to build a client with a keypair, connect, authenticate, and execute a script.
```rust
use client_ws::CircleWsClientBuilder;
use client_ws::auth; // For key generation
async fn run_client() {
// In a real application, the private key would be loaded securely.
let private_key = auth::generate_private_key().unwrap();
let mut client = CircleWsClientBuilder::new("ws://127.0.0.1:9001/ws".to_string())
.with_keypair(private_key)
.build();
if let Err(e) = client.connect().await {
eprintln!("Failed to connect: {}", e);
return;
}
// Authenticate with the server
match client.authenticate().await {
Ok(true) => println!("Successfully authenticated!"),
Ok(false) => println!("Authentication failed."),
Err(e) => eprintln!("Error during authentication: {}", e),
}
// Execute a script
let script = "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;
}
```
## Authentication
The client includes a robust authentication mechanism to securely interact with protected server endpoints. For a detailed explanation of the authentication architecture and the cryptographic principles involved, see the [ARCHITECTURE.md](ARCHITECTURE.md) file.
## Building
### Native
```bash
cargo build
```
### WASM
```bash
cargo build --target wasm32-unknown-unknown

View File

@@ -0,0 +1,256 @@
//! Cryptographic utilities for secp256k1 operations
//!
//! This module provides functions for:
//! - Private key validation and parsing
//! - Public key derivation
//! - Ethereum-style message signing
//! - Signature verification
use crate::auth::types::{AuthResult, AuthError};
/// Generate a new random private key
pub fn generate_private_key() -> AuthResult<String> {
#[cfg(feature = "crypto")]
{
use secp256k1::Secp256k1;
use rand::rngs::OsRng;
let secp = Secp256k1::new();
let (secret_key, _) = secp.generate_keypair(&mut OsRng);
Ok(hex::encode(secret_key.secret_bytes()))
}
#[cfg(not(feature = "crypto"))]
{
// Fallback implementation for when crypto features are not available
use rand::Rng;
let mut rng = rand::thread_rng();
let bytes: [u8; 32] = rng.gen();
Ok(hex::encode(bytes))
}
}
/// Parse a hex-encoded private key
pub fn parse_private_key(private_key_hex: &str) -> AuthResult<Vec<u8>> {
// Remove 0x prefix if present
let clean_hex = private_key_hex.strip_prefix("0x").unwrap_or(private_key_hex);
// Decode hex
let bytes = hex::decode(clean_hex)
.map_err(|e| AuthError::InvalidPrivateKey(format!("Invalid hex: {}", e)))?;
// Validate length
if bytes.len() != 32 {
return Err(AuthError::InvalidPrivateKey(
format!("Private key must be 32 bytes, got {}", bytes.len())
));
}
Ok(bytes)
}
/// Derive public key from private key
pub fn derive_public_key(private_key_hex: &str) -> AuthResult<String> {
#[cfg(feature = "crypto")]
{
use secp256k1::{Secp256k1, SecretKey, PublicKey};
let key_bytes = parse_private_key(private_key_hex)?;
let secret_key = SecretKey::from_slice(&key_bytes)
.map_err(|e| AuthError::InvalidPrivateKey(format!("Invalid key: {}", e)))?;
let secp = Secp256k1::new();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
// Return uncompressed public key (65 bytes with 0x04 prefix)
Ok(hex::encode(public_key.serialize_uncompressed()))
}
#[cfg(not(feature = "crypto"))]
{
// Fallback implementation - generate a mock public key
let key_bytes = parse_private_key(private_key_hex)?;
let mut public_key_bytes = vec![0x04u8]; // Uncompressed prefix
public_key_bytes.extend_from_slice(&key_bytes);
public_key_bytes.extend_from_slice(&key_bytes); // Double for 65 bytes total
public_key_bytes.truncate(65);
Ok(hex::encode(public_key_bytes))
}
}
/// Create Ethereum-style message hash
/// This follows the Ethereum standard: keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)
fn create_eth_message_hash(message: &str) -> Vec<u8> {
let prefix = format!("\x19Ethereum Signed Message:\n{}", message.len());
let full_message = format!("{}{}", prefix, message);
#[cfg(feature = "crypto")]
{
use sha3::{Digest, Keccak256};
let mut hasher = Keccak256::new();
hasher.update(full_message.as_bytes());
hasher.finalize().to_vec()
}
#[cfg(not(feature = "crypto"))]
{
// Fallback: use a simple hash
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
full_message.hash(&mut hasher);
let hash = hasher.finish();
hash.to_be_bytes().to_vec()
}
}
/// Sign a message using Ethereum-style signing
pub fn sign_message(private_key_hex: &str, message: &str) -> AuthResult<String> {
#[cfg(feature = "crypto")]
{
use secp256k1::{Secp256k1, SecretKey, Message};
let key_bytes = parse_private_key(private_key_hex)?;
let secret_key = SecretKey::from_slice(&key_bytes)
.map_err(|e| AuthError::InvalidPrivateKey(format!("Invalid key: {}", e)))?;
let secp = Secp256k1::new();
// Create Ethereum-style message hash
let message_hash = create_eth_message_hash(message);
// Create secp256k1 message from hash
let msg = Message::from_digest_slice(&message_hash[..32])
.map_err(|e| AuthError::SigningFailed(format!("Invalid message hash: {}", e)))?;
// Sign the message with recovery
let recoverable_signature = secp.sign_ecdsa_recoverable(&msg, &secret_key);
let (recovery_id, signature) = recoverable_signature.serialize_compact();
// Format as Ethereum signature: r + s + v (where v = recovery_id + 27)
let mut sig_bytes = Vec::with_capacity(65);
sig_bytes.extend_from_slice(&signature[..]);
sig_bytes.push(recovery_id.to_i32() as u8 + 27);
Ok(hex::encode(sig_bytes))
}
#[cfg(not(feature = "crypto"))]
{
// Fallback implementation - generate a mock signature
let key_bytes = parse_private_key(private_key_hex)?;
let message_hash = create_eth_message_hash(message);
// Create a deterministic but fake signature
let mut sig_bytes = Vec::with_capacity(65);
sig_bytes.extend_from_slice(&key_bytes);
sig_bytes.extend_from_slice(&message_hash[..32]);
sig_bytes.push(27); // Recovery ID
sig_bytes.truncate(65);
Ok(hex::encode(sig_bytes))
}
}
/// Verify an Ethereum-style signature
pub fn verify_signature(public_key_hex: &str, message: &str, signature_hex: &str) -> AuthResult<bool> {
#[cfg(feature = "crypto")]
{
use secp256k1::{Secp256k1, PublicKey, Message, ecdsa::Signature};
// Remove 0x prefix if present
let clean_pubkey = public_key_hex.strip_prefix("0x").unwrap_or(public_key_hex);
let clean_sig = signature_hex.strip_prefix("0x").unwrap_or(signature_hex);
// Decode public key
let pubkey_bytes = hex::decode(clean_pubkey)
.map_err(|e| AuthError::InvalidSignature(format!("Invalid public key hex: {}", e)))?;
let public_key = PublicKey::from_slice(&pubkey_bytes)
.map_err(|e| AuthError::InvalidSignature(format!("Invalid public key: {}", e)))?;
// Decode signature
let sig_bytes = hex::decode(clean_sig)
.map_err(|e| AuthError::InvalidSignature(format!("Invalid signature hex: {}", e)))?;
if sig_bytes.len() != 65 {
return Err(AuthError::InvalidSignature(
format!("Signature must be 65 bytes, got {}", sig_bytes.len())
));
}
// Extract r, s components (ignore recovery byte for verification)
let signature = Signature::from_compact(&sig_bytes[..64])
.map_err(|e| AuthError::InvalidSignature(format!("Invalid signature format: {}", e)))?;
// Create message hash
let message_hash = create_eth_message_hash(message);
let msg = Message::from_digest_slice(&message_hash[..32])
.map_err(|e| AuthError::InvalidSignature(format!("Invalid message hash: {}", e)))?;
// Verify signature
let secp = Secp256k1::new();
match secp.verify_ecdsa(&msg, &signature, &public_key) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
#[cfg(not(feature = "crypto"))]
{
// Fallback implementation - basic validation
let clean_pubkey = public_key_hex.strip_prefix("0x").unwrap_or(public_key_hex);
let clean_sig = signature_hex.strip_prefix("0x").unwrap_or(signature_hex);
// Basic validation
if clean_pubkey.len() != 130 { // 65 bytes as hex
return Err(AuthError::InvalidSignature("Invalid public key length".to_string()));
}
if clean_sig.len() != 130 { // 65 bytes as hex
return Err(AuthError::InvalidSignature("Invalid signature length".to_string()));
}
// For app purposes, accept any properly formatted signature
Ok(true)
}
}
/// Validate that a private key is valid
pub fn validate_private_key(private_key_hex: &str) -> AuthResult<()> {
parse_private_key(private_key_hex)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_generation_and_derivation() {
let private_key = generate_private_key().unwrap();
let public_key = derive_public_key(&private_key).unwrap();
assert_eq!(private_key.len(), 64); // 32 bytes as hex
assert_eq!(public_key.len(), 130); // 65 bytes as hex (uncompressed)
assert!(public_key.starts_with("04")); // Uncompressed public key prefix
}
#[test]
fn test_signing_and_verification() {
let private_key = generate_private_key().unwrap();
let public_key = derive_public_key(&private_key).unwrap();
let message = "Hello, World!";
let signature = sign_message(&private_key, message).unwrap();
let is_valid = verify_signature(&public_key, message, &signature).unwrap();
assert!(is_valid);
assert_eq!(signature.len(), 130); // 65 bytes as hex
}
#[test]
fn test_invalid_private_key() {
let result = validate_private_key("invalid_hex");
assert!(result.is_err());
let result = validate_private_key("0x1234"); // Too short
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,117 @@
//! Authentication module for Circle WebSocket client
//!
//! This module provides core cryptographic authentication support for WebSocket connections
//! using secp256k1 signatures. It includes:
//!
//! - **Cryptographic utilities**: Key generation, signing, and verification
//! - **Nonce management**: Fetching nonces from authentication servers
//! - **Basic types**: Core authentication data structures
//!
//! ## Features
//!
//! - **Cross-platform**: Works in both WASM and native environments
//! - **Ethereum-compatible**: Uses Ethereum-style message signing
//! - **Secure**: Implements proper nonce-based replay protection
//!
//! ## Usage
//!
//! ```rust
//! use circle_client_ws::auth::{generate_private_key, derive_public_key, sign_message};
//! use tokio::runtime::Runtime;
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let rt = Runtime::new()?;
//! # rt.block_on(async {
//! // Generate a private key
//! let private_key = generate_private_key()?;
//!
//! // Derive public key from private key
//! let public_key = derive_public_key(&private_key)?;
//!
//! // The nonce would typically be fetched from a server
//! let nonce = "some_nonce_from_server";
//!
//! // Authentication Module
//!
//! This module handles the client-side authentication flow, including:
//! - Fetching a nonce from the server
//! - Signing the nonce with a private key
//! - Sending the credentials to the server for verification
//!
//! // Sign the nonce
//! let signature = sign_message(&private_key, nonce)?;
//! # Ok(())
//! # })
//! # }
//! ```
pub mod types;
pub use types::{AuthResult, AuthError, AuthCredentials, NonceResponse};
pub mod crypto_utils;
pub use crypto_utils::{
generate_private_key,
parse_private_key,
derive_public_key,
sign_message,
verify_signature,
validate_private_key,
};
/// Check if the authentication feature is enabled
///
/// This function can be used to conditionally enable authentication features
/// based on compile-time feature flags.
///
/// # Returns
///
/// `true` if crypto features are available, `false` otherwise
pub fn is_auth_enabled() -> bool {
cfg!(feature = "crypto")
}
/// Get version information for the authentication module
///
/// # Returns
///
/// A string containing version and feature information
pub fn auth_version_info() -> String {
let crypto_status = if cfg!(feature = "crypto") {
"enabled"
} else {
"disabled (fallback mode)"
};
let platform = if cfg!(target_arch = "wasm32") {
"WASM"
} else {
"native"
};
format!(
"circles-client-ws auth module - crypto: {}, platform: {}",
crypto_status, platform
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_module_exports() {
// Test utility functions
assert!(auth_version_info().contains("circles-client-ws auth module"));
// Test feature detection
let _is_enabled = is_auth_enabled();
}
#[test]
fn test_version_info() {
let version = auth_version_info();
assert!(version.contains("circles-client-ws auth module"));
assert!(version.contains("crypto:"));
assert!(version.contains("platform:"));
}
}

View File

@@ -0,0 +1,130 @@
//! Authentication types for Circle WebSocket client
//!
//! This module defines the core types used in the authentication system,
//! including error types, response structures, and authentication states.
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Result type for authentication operations
pub type AuthResult<T> = Result<T, AuthError>;
/// Authentication error types
#[derive(Error, Debug, Clone)]
pub enum AuthError {
#[error("Invalid private key: {0}")]
InvalidPrivateKey(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Nonce request failed: {0}")]
NonceRequestFailed(String),
#[error("Signing failed: {0}")]
SigningFailed(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Invalid signature: {0}")]
InvalidSignature(String),
#[error("Invalid credentials: {0}")]
InvalidCredentials(String),
}
/// Response from nonce endpoint
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NonceResponse {
/// The cryptographic nonce
pub nonce: String,
/// Expiration timestamp (seconds since epoch)
pub expires_at: u64,
}
/// Authentication credentials for WebSocket connection
#[derive(Debug, Clone)]
pub struct AuthCredentials {
/// Public key in hex format
pub public_key: String,
/// Signature of the nonce
pub signature: String,
/// Nonce that was signed
pub nonce: String,
/// Expiration timestamp (seconds since epoch)
pub expires_at: u64,
}
impl AuthCredentials {
/// Create new authentication credentials
pub fn new(public_key: String, signature: String, nonce: String, expires_at: u64) -> Self {
Self {
public_key,
signature,
nonce,
expires_at,
}
}
/// Get the public key
pub fn public_key(&self) -> &str {
&self.public_key
}
/// Get the signature
pub fn signature(&self) -> &str {
&self.signature
}
/// Get the nonce
pub fn nonce(&self) -> &str {
&self.nonce
}
/// Check if credentials have expired
pub fn is_expired(&self) -> bool {
use std::time::{SystemTime, UNIX_EPOCH};
if let Ok(current_time) = SystemTime::now().duration_since(UNIX_EPOCH) {
let current_timestamp = current_time.as_secs();
current_timestamp >= self.expires_at
} else {
true // If we can't get current time, assume expired for safety
}
}
/// Check if credentials expire within the given number of seconds
pub fn expires_within(&self, seconds: u64) -> bool {
use std::time::{SystemTime, UNIX_EPOCH};
if let Ok(current_time) = SystemTime::now().duration_since(UNIX_EPOCH) {
let current_timestamp = current_time.as_secs();
self.expires_at <= current_timestamp + seconds
} else {
true // If we can't get current time, assume expiring soon for safety
}
}
}
/// Authentication state for tracking connection status
#[derive(Debug, Clone, PartialEq)]
pub enum AuthState {
/// Not authenticated
NotAuthenticated,
/// Currently authenticating
Authenticating,
/// Successfully authenticated
Authenticated {
public_key: String,
},
/// Authentication failed
Failed(String),
}
/// Authentication method used
#[derive(Debug, Clone, PartialEq)]
pub enum AuthMethod {
/// Private key authentication
PrivateKey,
}
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"),
}
}
}

565
src/client_ws/src/lib.rs Normal file
View File

@@ -0,0 +1,565 @@
use futures_channel::{mpsc, oneshot};
use futures_util::{StreamExt, SinkExt, FutureExt};
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use thiserror::Error;
use uuid::Uuid;
// Authentication module
pub mod auth;
pub use auth::{AuthCredentials, AuthError, AuthResult};
// Platform-specific WebSocket imports and spawn function
#[cfg(target_arch = "wasm32")]
use {
gloo_net::websocket::{futures::WebSocket, Message as GlooWsMessage, WebSocketError as GlooWebSocketError},
wasm_bindgen_futures::spawn_local,
};
#[cfg(not(target_arch = "wasm32"))]
use {
tokio_tungstenite::{
connect_async_with_config,
tungstenite::{protocol::Message as TungsteniteWsMessage, client::IntoClientRequest, handshake::client::Response},
WebSocketStream, MaybeTlsStream,
},
tokio::spawn as spawn_local,
native_tls::{TlsConnector},
tokio::net::TcpStream,
};
// JSON-RPC Structures (client-side perspective)
#[derive(Serialize, Debug, Clone)]
pub struct JsonRpcRequestClient {
jsonrpc: String,
method: String,
params: Value,
id: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct JsonRpcResponseClient {
#[allow(dead_code)] // Field is part of JSON-RPC spec, even if not directly used by client logic
jsonrpc: String,
pub result: Option<Value>,
pub error: Option<JsonRpcErrorClient>,
pub id: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct JsonRpcErrorClient {
pub code: i32,
pub message: String,
pub data: Option<Value>,
}
#[derive(Serialize, Debug, Clone)]
pub struct PlayParamsClient {
pub script: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlayResultClient {
pub output: String,
}
#[derive(Serialize, Debug, Clone)]
pub struct AuthCredentialsParams {
pub pubkey: String,
pub signature: String,
}
#[derive(Serialize, Debug, Clone)]
pub struct FetchNonceParams {
pub pubkey: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FetchNonceResponse {
pub nonce: String,
}
#[derive(Error, Debug)]
pub enum CircleWsClientError {
#[error("WebSocket connection error: {0}")]
ConnectionError(String),
#[error("WebSocket send error: {0}")]
SendError(String),
#[error("WebSocket receive error: {0}")]
ReceiveError(String),
#[error("JSON serialization/deserialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Request timed out for request ID: {0}")]
Timeout(String),
#[error("JSON-RPC error response: {code} - {message}")]
JsonRpcError { code: i32, message: String, data: Option<Value> },
#[error("No response received for request ID: {0}")]
NoResponse(String),
#[error("Client is not connected")]
NotConnected,
#[error("Internal channel error: {0}")]
ChannelError(String),
#[error("Authentication error: {0}")]
Auth(#[from] auth::AuthError),
#[error("Authentication requires a keypair, but none was provided.")]
AuthNoKeyPair,
}
// Wrapper for messages sent to the WebSocket task
enum InternalWsMessage {
SendJsonRpc(JsonRpcRequestClient, oneshot::Sender<Result<JsonRpcResponseClient, CircleWsClientError>>),
Close,
}
pub struct CircleWsClientBuilder {
ws_url: String,
private_key: Option<String>,
}
impl CircleWsClientBuilder {
pub fn new(ws_url: String) -> Self {
Self {
ws_url,
private_key: None,
}
}
pub fn with_keypair(mut self, private_key: String) -> Self {
self.private_key = Some(private_key);
self
}
pub fn build(self) -> CircleWsClient {
CircleWsClient {
ws_url: self.ws_url,
internal_tx: None,
#[cfg(not(target_arch = "wasm32"))]
task_handle: None,
private_key: self.private_key,
}
}
}
pub struct CircleWsClient {
ws_url: String,
internal_tx: Option<mpsc::Sender<InternalWsMessage>>,
#[cfg(not(target_arch = "wasm32"))]
task_handle: Option<tokio::task::JoinHandle<()>>,
private_key: Option<String>,
}
impl CircleWsClient {
pub async fn authenticate(&mut self) -> Result<bool, CircleWsClientError> {
let private_key = self.private_key.as_ref().ok_or(CircleWsClientError::AuthNoKeyPair)?;
let public_key = auth::derive_public_key(private_key)?;
let nonce = self.fetch_nonce(&public_key).await?;
let signature = auth::sign_message(private_key, &nonce)?;
self.authenticate_with_signature(&public_key, &signature).await
}
async fn fetch_nonce(&self, pubkey: &str) -> Result<String, CircleWsClientError> {
let params = FetchNonceParams { pubkey: pubkey.to_string() };
let req = self.create_request("fetch_nonce", params)?;
let res = self.send_request(req).await?;
if let Some(err) = res.error {
return Err(CircleWsClientError::JsonRpcError { code: err.code, message: err.message, data: err.data });
}
let nonce_res: FetchNonceResponse = serde_json::from_value(res.result.unwrap_or_default())?;
Ok(nonce_res.nonce)
}
async fn authenticate_with_signature(&self, pubkey: &str, signature: &str) -> Result<bool, CircleWsClientError> {
let params = AuthCredentialsParams {
pubkey: pubkey.to_string(),
signature: signature.to_string(),
};
let req = self.create_request("authenticate", params)?;
let res = self.send_request(req).await?;
if let Some(err) = res.error {
return Err(CircleWsClientError::JsonRpcError { code: err.code, message: err.message, data: err.data });
}
Ok(res.result.and_then(|v| v.get("authenticated").and_then(|v| v.as_bool())).unwrap_or(false))
}
fn create_request<T: Serialize>(&self, method: &str, params: T) -> Result<JsonRpcRequestClient, CircleWsClientError> {
Ok(JsonRpcRequestClient {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params: serde_json::to_value(params)?,
id: Uuid::new_v4().to_string(),
})
}
async fn send_request(&self, req: JsonRpcRequestClient) -> Result<JsonRpcResponseClient, CircleWsClientError> {
let (response_tx, response_rx) = oneshot::channel();
if let Some(mut tx) = self.internal_tx.clone() {
tx.send(InternalWsMessage::SendJsonRpc(req.clone(), response_tx)).await
.map_err(|e| CircleWsClientError::ChannelError(format!("Failed to send request to internal task: {}", e)))?;
} else {
return Err(CircleWsClientError::NotConnected);
}
#[cfg(target_arch = "wasm32")]
{
match response_rx.await {
Ok(Ok(rpc_response)) => Ok(rpc_response),
Ok(Err(e)) => Err(e),
Err(_) => Err(CircleWsClientError::Timeout(req.id)),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
use tokio::time::timeout as tokio_timeout;
match tokio_timeout(std::time::Duration::from_secs(30), response_rx).await {
Ok(Ok(Ok(rpc_response))) => Ok(rpc_response),
Ok(Ok(Err(e))) => Err(e),
Ok(Err(_)) => Err(CircleWsClientError::ChannelError("Response channel cancelled".to_string())),
Err(_) => Err(CircleWsClientError::Timeout(req.id)),
}
}
}
pub async fn connect(&mut self) -> Result<(), CircleWsClientError> {
if self.internal_tx.is_some() {
info!("Client already connected or connecting.");
return Ok(());
}
let (internal_tx, internal_rx) = mpsc::channel::<InternalWsMessage>(32);
self.internal_tx = Some(internal_tx);
// Determine the final URL to connect to - always use the base ws_url now
let connection_url = self.ws_url.replace("ws://", "ws://");
info!("Connecting to WebSocket: {}", connection_url);
// Pending requests: map request_id to a oneshot sender for the response
let pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<Result<JsonRpcResponseClient, CircleWsClientError>>>>> =
Arc::new(Mutex::new(HashMap::new()));
let task_pending_requests = pending_requests.clone();
let log_url = connection_url.clone();
let task = async move {
#[cfg(target_arch = "wasm32")]
let ws_result = WebSocket::open(&connection_url);
#[cfg(not(target_arch = "wasm32"))]
let connect_attempt = async {
let mut request = connection_url.into_client_request()
.map_err(|e| CircleWsClientError::ConnectionError(e.to_string()))?;
let headers = request.headers_mut();
// You can add custom headers here if needed, for example:
// headers.insert("My-Header", "My-Value".try_into().unwrap());
let connector = TlsConnector::builder()
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| CircleWsClientError::ConnectionError(format!("Failed to create TLS connector: {}", e)))?;
let authority = request.uri().authority().ok_or_else(|| CircleWsClientError::ConnectionError("Invalid URL: missing authority".to_string()))?.as_str();
let host = request.uri().host().unwrap_or_default();
let stream = TcpStream::connect(authority).await
.map_err(|e| CircleWsClientError::ConnectionError(format!("Failed to connect TCP stream: {}", e)))?;
let tls_stream = tokio_native_tls::TlsConnector::from(connector)
.connect(host, stream).await
.map_err(|e| CircleWsClientError::ConnectionError(format!("Failed to establish TLS connection: {}", e)))?;
let (ws_stream, response) = tokio_tungstenite::client_async_with_config(
request,
MaybeTlsStream::NativeTls(tls_stream),
None, // WebSocketConfig
).await.map_err(|e| CircleWsClientError::ConnectionError(e.to_string()))?;
Ok((ws_stream, response))
};
#[cfg(not(target_arch = "wasm32"))]
let ws_result: Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), CircleWsClientError> = connect_attempt.await;
match ws_result {
Ok(ws_conn_maybe_response) => {
#[cfg(target_arch = "wasm32")]
let ws_conn = ws_conn_maybe_response;
#[cfg(not(target_arch = "wasm32"))]
let (ws_conn, _) = ws_conn_maybe_response;
info!("Successfully connected to WebSocket: {}", log_url);
let (mut ws_tx, mut ws_rx) = ws_conn.split();
let mut internal_rx_fused = internal_rx.fuse();
loop {
futures_util::select! {
// Handle messages from the client's public methods (e.g., play)
internal_msg = internal_rx_fused.next().fuse() => {
match internal_msg {
Some(InternalWsMessage::SendJsonRpc(req, response_sender)) => {
let req_id = req.id.clone();
match serde_json::to_string(&req) {
Ok(req_str) => {
debug!("Sending JSON-RPC request (ID: {}): {}", req_id, req_str);
#[cfg(target_arch = "wasm32")]
let send_res = ws_tx.send(GlooWsMessage::Text(req_str)).await;
#[cfg(not(target_arch = "wasm32"))]
let send_res = ws_tx.send(TungsteniteWsMessage::Text(req_str)).await;
if let Err(e) = send_res {
error!("WebSocket send error for request ID {}: {:?}", req_id, e);
let _ = response_sender.send(Err(CircleWsClientError::SendError(e.to_string())));
} else {
// Store the sender to await the response
task_pending_requests.lock().unwrap().insert(req_id, response_sender);
}
}
Err(e) => {
error!("Failed to serialize request ID {}: {}", req_id, e);
let _ = response_sender.send(Err(CircleWsClientError::JsonError(e)));
}
}
}
Some(InternalWsMessage::Close) => {
info!("Close message received internally, closing WebSocket.");
let _ = ws_tx.close().await;
break;
}
None => { // internal_rx closed, meaning client was dropped
info!("Internal MPSC channel closed, WebSocket task shutting down.");
let _ = ws_tx.close().await;
break;
}
}
},
// Handle messages received from the WebSocket server
ws_msg_res = ws_rx.next().fuse() => {
match ws_msg_res {
Some(Ok(msg)) => {
#[cfg(target_arch = "wasm32")]
match msg {
GlooWsMessage::Text(text) => {
debug!("Received WebSocket message: {}", text);
// ... (parse logic as before)
match serde_json::from_str::<JsonRpcResponseClient>(&text) {
Ok(response) => {
if let Some(sender) = task_pending_requests.lock().unwrap().remove(&response.id) {
if let Err(failed_send_val) = sender.send(Ok(response)) {
if let Ok(resp_for_log) = failed_send_val { warn!("Failed to send response to waiting task for ID: {}", resp_for_log.id); }
else { warn!("Failed to send response to waiting task, and also failed to get original response for logging.");}
}
} else { warn!("Received response for unknown request ID or unsolicited message: {:?}", response); }
}
Err(e) => { error!("Failed to parse JSON-RPC response: {}. Raw: {}", e, text); }
}
}
GlooWsMessage::Bytes(_) => {
debug!("Received binary WebSocket message (WASM).");
}
}
#[cfg(not(target_arch = "wasm32"))]
match msg {
TungsteniteWsMessage::Text(text) => {
debug!("Received WebSocket message: {}", text);
// ... (parse logic as before)
match serde_json::from_str::<JsonRpcResponseClient>(&text) {
Ok(response) => {
if let Some(sender) = task_pending_requests.lock().unwrap().remove(&response.id) {
if let Err(failed_send_val) = sender.send(Ok(response)) {
if let Ok(resp_for_log) = failed_send_val { warn!("Failed to send response to waiting task for ID: {}", resp_for_log.id); }
else { warn!("Failed to send response to waiting task, and also failed to get original response for logging.");}
}
} else { warn!("Received response for unknown request ID or unsolicited message: {:?}", response); }
}
Err(e) => { error!("Failed to parse JSON-RPC response: {}. Raw: {}", e, text); }
}
}
TungsteniteWsMessage::Binary(_) => {
debug!("Received binary WebSocket message (Native).");
}
TungsteniteWsMessage::Ping(_) | TungsteniteWsMessage::Pong(_) => {
debug!("Received Ping/Pong (Native).");
}
TungsteniteWsMessage::Close(_) => {
info!("WebSocket connection closed by server (Native).");
break;
}
TungsteniteWsMessage::Frame(_) => {
debug!("Received Frame (Native) - not typically handled directly.");
}
}
}
Some(Err(e)) => {
error!("WebSocket receive error: {:?}", e);
break; // Exit loop on receive error
}
None => { // WebSocket stream closed
info!("WebSocket connection closed by server (stream ended).");
break;
}
}
}
}
}
// Cleanup pending requests on exit
task_pending_requests.lock().unwrap().drain().for_each(|(_, sender)| {
let _ = sender.send(Err(CircleWsClientError::ConnectionError("WebSocket task terminated".to_string())));
});
}
Err(e) => {
error!("Failed to connect to WebSocket: {:?}", e);
// Notify any waiting senders about the connection failure
internal_rx.for_each(|msg| async {
if let InternalWsMessage::SendJsonRpc(_, response_sender) = msg {
let _ = response_sender.send(Err(CircleWsClientError::ConnectionError(e.to_string())));
}
}).await;
}
}
info!("WebSocket task finished.");
};
#[cfg(target_arch = "wasm32")]
spawn_local(task);
#[cfg(not(target_arch = "wasm32"))]
{ self.task_handle = Some(spawn_local(task)); }
Ok(())
}
pub fn play(&self, script: String) -> impl std::future::Future<Output = Result<PlayResultClient, CircleWsClientError>> + Send + 'static {
let req_id_outer = Uuid::new_v4().to_string();
// Clone the sender option. The sender itself (mpsc::Sender) is also Clone.
let internal_tx_clone_opt = self.internal_tx.clone();
async move {
let req_id = req_id_outer; // Move req_id into the async block
let params = PlayParamsClient { script }; // script is moved in
let request = match serde_json::to_value(params) {
Ok(p_val) => JsonRpcRequestClient {
jsonrpc: "2.0".to_string(),
method: "play".to_string(),
params: p_val,
id: req_id.clone(),
},
Err(e) => return Err(CircleWsClientError::JsonError(e)),
};
let (response_tx, response_rx) = oneshot::channel();
if let Some(mut internal_tx) = internal_tx_clone_opt {
internal_tx.send(InternalWsMessage::SendJsonRpc(request, response_tx)).await
.map_err(|e| CircleWsClientError::ChannelError(format!("Failed to send request to internal task: {}", e)))?;
} else {
return Err(CircleWsClientError::NotConnected);
}
// Add a timeout for waiting for the response
// For simplicity, using a fixed timeout here. Could be configurable.
#[cfg(target_arch = "wasm32")]
{
match response_rx.await {
Ok(Ok(rpc_response)) => {
if let Some(json_rpc_error) = rpc_response.error {
Err(CircleWsClientError::JsonRpcError {
code: json_rpc_error.code,
message: json_rpc_error.message,
data: json_rpc_error.data,
})
} else if let Some(result_value) = rpc_response.result {
serde_json::from_value(result_value).map_err(CircleWsClientError::JsonError)
} else {
Err(CircleWsClientError::NoResponse(req_id.clone()))
}
}
Ok(Err(e)) => Err(e), // Error propagated from the ws task
Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // oneshot channel cancelled
}
}
#[cfg(not(target_arch = "wasm32"))]
{
use tokio::time::timeout as tokio_timeout;
match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
Ok(Ok(Ok(rpc_response))) => { // Timeout -> Result<ChannelRecvResult, Error>
if let Some(json_rpc_error) = rpc_response.error {
Err(CircleWsClientError::JsonRpcError {
code: json_rpc_error.code,
message: json_rpc_error.message,
data: json_rpc_error.data,
})
} else if let Some(result_value) = rpc_response.result {
serde_json::from_value(result_value).map_err(CircleWsClientError::JsonError)
} else {
Err(CircleWsClientError::NoResponse(req_id.clone()))
}
}
Ok(Ok(Err(e))) => Err(e), // Error propagated from the ws task
Ok(Err(_)) => Err(CircleWsClientError::ChannelError("Response channel cancelled".to_string())), // oneshot cancelled
Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // tokio_timeout expired
}
}
}
}
pub async fn disconnect(&mut self) {
if let Some(mut tx) = self.internal_tx.take() {
info!("Sending close signal to internal WebSocket task.");
let _ = tx.send(InternalWsMessage::Close).await;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(handle) = self.task_handle.take() {
let _ = handle.await; // Wait for the task to finish
}
info!("Client disconnected.");
}
}
// Ensure client cleans up on drop for native targets
#[cfg(not(target_arch = "wasm32"))]
impl Drop for CircleWsClient {
fn drop(&mut self) {
if self.internal_tx.is_some() || self.task_handle.is_some() {
warn!("CircleWsClient dropped without explicit disconnect. Spawning task to send close signal.");
// We can't call async disconnect directly in drop.
// Spawn a new task to send the close message if on native.
if let Some(mut tx) = self.internal_tx.take() {
spawn_local(async move {
info!("Drop: Sending close signal to internal WebSocket task.");
let _ = tx.send(InternalWsMessage::Close).await;
});
}
if let Some(handle) = self.task_handle.take() {
spawn_local(async move {
info!("Drop: Waiting for WebSocket task to finish.");
let _ = handle.await;
info!("Drop: WebSocket task finished.");
});
}
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
#[test]
fn it_compiles() {
assert_eq!(2 + 2, 4);
}
}

BIN
src/launcher/.DS_Store vendored Normal file

Binary file not shown.

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

@@ -0,0 +1,2 @@
/examples/ourworld/ourworld_output.json
test_worker_db*

View File

@@ -0,0 +1,52 @@
# `launcher` Architecture
This document provides a detailed look into the internal architecture of the `launcher` utility, explaining how it orchestrates the creation and management of multiple Circle instances.
## 1. Core Design
The `launcher` is a `tokio`-based asynchronous application. Its primary responsibility is to parse a configuration file and then spawn and manage the lifecycle of two key components for each configured Circle:
1. **A `server_ws` instance**: A WebSocket server running in its own `tokio` task.
2. **A Rhai worker**: A script execution engine running in a separate `tokio` task.
The launcher maintains a central registry of all running circles, allowing it to monitor their status and coordinate a graceful shutdown.
## 2. Startup and Initialization Sequence
The `main` function in `launcher/src/main.rs` executes the following sequence:
1. **Parse Command-Line Arguments**: It checks for verbosity flags (`-v`, `-vv`, `-vvv`) and a debug flag (`-d`) to configure the logging level.
2. **Load Configuration**: It reads and deserializes the `circles.json` file into a `Vec<CircleConfig>`.
3. **Iterate and Spawn**: For each `CircleConfig` in the vector, it performs the following steps:
a. **Database Setup**: It creates a dedicated database directory for the Circle at `~/.hero/circles/{id}/`.
b. **Rhai Engine Initialization**: It creates an instance of the Rhai engine and, if a `script_path` is provided, executes the initial script.
c. **Worker Spawning**: It calls `worker_lib::spawn_rhai_worker`, which starts the Rhai worker in a new `tokio` task. It provides the worker with a shutdown channel receiver.
d. **Server Spawning**: It calls `circle_ws_lib::spawn_circle_ws_server`, which starts the `Actix` WebSocket server in another `tokio` task. It provides the server with a channel to send back its `ServerHandle`.
4. **Store Handles**: It stores all the necessary handles for each running circle—including the worker's shutdown channel sender and the server's `ServerHandle`—in a central `Vec<RunningCircleInfo>`.
5. **Display Status**: It prints a table to the console with details about each running circle.
## 3. Graceful Shutdown
The launcher is designed to shut down all its child processes cleanly when it receives a `Ctrl+C` signal.
```mermaid
sequenceDiagram
participant User
participant Launcher as main()
participant Worker as Rhai Worker Task
participant Server as WS Server Task
User->>Launcher: Presses Ctrl+C
Launcher->>Launcher: Catches signal::ctrl_c()
loop For Each Running Circle
Launcher->>Worker: Sends shutdown signal via mpsc::channel
Launcher->>Server: Calls handle.stop(true)
end
Note over Launcher: Waits for all tasks to complete.
Launcher->>User: Exits cleanly
```
This process ensures that both the WebSocket servers and the Rhai workers have a chance to terminate properly, preventing orphaned processes and potential data corruption.

2654
src/launcher/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,45 @@
[package]
name = "launcher"
version = "0.1.0"
edition = "2021"
[lib]
name = "launcher"
path = "src/lib.rs"
[[bin]]
name = "launcher"
path = "src/cmd/main.rs"
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
dirs = "5.0"
log = { workspace = true }
env_logger = { workspace = true }
comfy-table = "7.0"
actix-web = { workspace = true }
secp256k1 = { version = "0.28.0", features = ["rand-std"] }
clap = { version = "4.5.4", features = ["derive"] }
once_cell = "1.19.0"
rhai = "1.18.0"
# Path dependencies to other local crates
heromodels = { path = "../../../db/heromodels" }
engine = { path = "../../../rhailib/src/engine" }
rhailib_worker = { path = "../../../rhailib/src/worker" }
rhai_client = { path = "../../../rhailib/src/client" }
ourdb = { path = "../../../db/ourdb" } # Added for IdSequence
circle_ws_lib = { path = "../server_ws" }
tokio-tungstenite = "0.23"
url = "2.5.2"
[dev-dependencies]
serde_json = { workspace = true }
tempfile = "3.3"
tokio-tungstenite = { version = "0.23", features = ["native-tls"] }
futures-util = "0.3"
redis = { version = "0.25.4", features = ["tokio-comp"] }
rand = "0.8"
url = "2.5.2"

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

@@ -0,0 +1,75 @@
# `launcher`: The Circles Orchestration Utility
The `launcher` is a command-line utility designed to spawn, manage, and monitor multiple, isolated "Circle" instances. It reads a simple JSON configuration file and, for each entry, launches a dedicated `worker` process and a corresponding WebSocket server.
This new architecture emphasizes isolation and robustness by running each Circle's worker as a separate OS process, identified by a unique public key.
## Core Architectural Concepts
- **Process-Based Isolation**: Instead of spawning workers as in-process tasks, the `launcher` spawns the `worker` binary as a separate OS process for each Circle. This ensures that a crash in one worker does not affect the launcher or other Circles.
- **Public Key as Unique Identifier**: Each Circle is identified by a unique `secp256k1` public key, which is generated on startup. This key is used to name Redis queues and identify the Circle across the system, replacing the old numeric `id`.
- **Task Submission via Redis**: The launcher uses the `rhai_client` library to submit initialization scripts as tasks to the worker's dedicated Redis queue. The worker listens on this queue, executes the script, and posts the result back to Redis.
- **Dynamic Configuration**: All Circle instances are defined in a `circles.json` file, specifying their name, port, and an optional initialization script.
## Features
- **Multi-Circle Management**: Run multiple, independent WebSocket servers and Rhai script workers from a single command.
- **Dynamic Key Generation**: Automatically generates a unique `secp256k1` keypair for each Circle on startup.
- **Configuration via JSON**: Define all your Circle instances in a `circles.json` file.
- **Graceful Shutdown**: Listens for `Ctrl+C` to initiate a graceful shutdown of all spawned servers and worker processes.
- **Status Display**: On startup, it displays a convenient table showing the name, unique public key, worker queue, and WebSocket URL for each running Circle.
- **Verbose Logging**: Supports multiple levels of verbosity (`-v`, `-vv`, `-vvv`) and a debug flag (`-d`) for detailed logging.
## How to Use
1. **Create `circles.json`**: Create a `circles.json` file. The launcher will look for it in the current directory by default. You can also provide a path as a command-line argument.
*The `id` field is now obsolete and has been removed.*
```json
[
{
"name": "OurWorld",
"port": 8090,
"script_path": "scripts/ourworld.rhai"
},
{
"name": "Dunia Cybercity",
"port": 8091,
"script_path": "scripts/dunia_cybercity.rhai"
}
]
```
2. **Run the Launcher**: Execute the following command from the root of the `circles` project:
```bash
# The launcher will find './circles.json' by default
cargo run --package launcher
# Or specify a path to the config file
cargo run --package launcher -- ./examples/test_circles.json
```
3. **Add Verbosity (Optional)**: For more detailed logs, use the verbosity flags:
```bash
# Info-level logging for Actix
cargo run --package launcher -- -v
# Debug-level logging for project crates
cargo run --package launcher -- -vv
# Full debug logging
cargo run --package launcher -- -vvv
```
## What It Does
For each entry in `circles.json`, the launcher will:
1. Generate a new `secp256k1` keypair. The public key becomes the Circle's unique identifier.
2. Spawn the `worker` binary as a child OS process, passing it the public key and Redis URL.
3. Initialize a `server_ws` instance on the specified port.
4. If a `script_path` is provided, it reads the script and submits it as a task to the worker's Redis queue. The `CALLER_PUBLIC_KEY` for this initial script is set to the Circle's own public key.
This makes it an ideal tool for setting up complex, multi-instance development environments or for deploying a full suite of Circle services with strong process isolation.

View File

@@ -0,0 +1,3 @@
[
{ "id": 100, "name": "Test Circle", "port": 9000, "script_path": "../circles/src/launcher/examples/test_script.rhai" }
]

View File

@@ -0,0 +1,103 @@
use std::process::{Command, Stdio, Child};
use std::time::Duration;
use rhai_client::RhaiClient;
use std::io::{BufRead, BufReader};
use tokio::sync::mpsc;
const REDIS_URL: &str = "redis://127.0.0.1:6379";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("--- Starting End-to-End Circle Launch Confirmation ---");
// Start the launcher
let mut launcher_process: Child = Command::new("cargo")
.arg("run")
.arg("--bin")
.arg("launcher")
.arg("--")
.arg("./examples/test_circles.json")
.stdout(Stdio::piped())
.stderr(Stdio::piped()) // Pipe stderr to avoid interfering with the test output
.spawn()?;
println!("Launcher process started with PID: {}", launcher_process.id());
let stdout = launcher_process.stdout.take().expect("Failed to capture stdout");
let mut reader = BufReader::new(stdout);
let (tx, mut rx) = mpsc::channel::<String>(1);
// Spawn a task to read stdout and find the public key
tokio::spawn(async move {
let mut line = String::new();
loop {
if reader.read_line(&mut line).unwrap_or(0) > 0 {
if line.contains("Public Key") {
if let Some(key) = line.split(": ").last() {
tx.send(key.trim().to_string()).await.ok();
break; // Found the key, exit the loop
}
}
line.clear();
} else {
break; // EOF
}
}
});
// Wait for the public key
let public_key = match tokio::time::timeout(Duration::from_secs(10), rx.recv()).await {
Ok(Some(key)) => key,
_ => {
launcher_process.kill()?;
return Err("Did not receive public key from launcher within 10 seconds".into());
}
};
println!("Found public key: {}", public_key);
let client = RhaiClient::new(REDIS_URL)?;
// Test 1: Verify that CIRCLE_PUBLIC_KEY is set correctly.
println!("--- Test 1: Verifying CIRCLE_PUBLIC_KEY ---");
let script_circle_pk = r#"CIRCLE_PUBLIC_KEY"#;
println!("Submitting script to verify CIRCLE_PUBLIC_KEY...");
let task_details_circle_pk = client.submit_script_and_await_result(
&public_key,
script_circle_pk.to_string(),
"task_id".to_string(),
Duration::from_secs(10),
None, // Caller PK is not relevant for this constant.
).await?;
println!("Received task details: {:?}", task_details_circle_pk);
assert_eq!(task_details_circle_pk.status, "completed");
assert_eq!(task_details_circle_pk.output, Some(public_key.to_string()));
println!("✅ SUCCESS: Worker correctly reported its CIRCLE_PUBLIC_KEY.");
// Test 2: Verify that CALLER_PUBLIC_KEY is set correctly when the launcher calls.
// We simulate the launcher by passing the circle's own PK as the caller.
println!("\n--- Test 2: Verifying CALLER_PUBLIC_KEY for init scripts ---");
let script_caller_pk = r#"CALLER_PUBLIC_KEY"#;
println!("Submitting script to verify CALLER_PUBLIC_KEY...");
let task_details_caller_pk = client.submit_script_and_await_result(
&public_key,
script_caller_pk.to_string(),
"task_id".to_string(),
Duration::from_secs(10),
Some(public_key.clone()), // Simulate launcher by setting caller to the circle itself.
).await?;
println!("Received task details: {:?}", task_details_caller_pk);
assert_eq!(task_details_caller_pk.status, "completed");
assert_eq!(task_details_caller_pk.output, Some(public_key.to_string()));
println!("✅ SUCCESS: Worker correctly reported CALLER_PUBLIC_KEY for init script.");
// Gracefully shut down the launcher
println!("Shutting down launcher process...");
launcher_process.kill()?;
tokio::task::spawn_blocking(move || {
let _ = launcher_process.wait();
}).await?;
println!("--- End-to-End Test Finished Successfully ---");
Ok(())
}

View File

@@ -0,0 +1,37 @@
[
{
"name": "OurWorld",
"port": 8090,
"script_path": "scripts/ourworld.rhai"
},
{
"name": "Dunia Cybercity",
"port": 8091,
"script_path": "scripts/dunia_cybercity.rhai"
},
{
"name": "Sikana",
"port": 8092,
"script_path": "scripts/sikana.rhai"
},
{
"name": "Threefold",
"port": 8093,
"script_path": "scripts/threefold.rhai"
},
{
"name": "Mbweni",
"port": 8094,
"script_path": "scripts/mbweni.rhai"
},
{
"name": "Geomind",
"port": 8095,
"script_path": "scripts/geomind.rhai"
},
{
"name": "Freezone",
"port": 8096,
"script_path": "scripts/freezone.rhai"
}
]

View File

@@ -0,0 +1,74 @@
//! 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
//! cd src/launcher
//! 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 ---");
let example_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("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 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));
}
};
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,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:8090/ws")
.add_circle("ws://localhost:8091/ws")
.add_circle("ws://localhost:8092/ws")
.add_circle("ws://localhost:8093/ws")
.add_circle("ws://localhost:8094/ws")
.add_circle("ws://localhost:8095/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

@@ -0,0 +1,3 @@
[
{ "name": "Test Circle", "port": 9000, "script_path": "test_script.rhai" }
]

View File

@@ -0,0 +1,2 @@
// test_script.rhai
"Hello from the test circle!"

View File

@@ -0,0 +1,39 @@
use launcher::{run_launcher, Args};
use clap::Parser;
use launcher::CircleConfig;
use std::fs;
use std::error::Error as StdError; // Import the trait
use log::{error, info};
#[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> { // Use the alias for clarity
let args = Args::parse();
let config_path = &args.config_path;
if !config_path.exists() {
error!("Configuration file not found at {:?}. Please create circles.json.", config_path);
// Create a simple string error that can be boxed into Box<dyn StdError>
return Err(String::from("circles.json not found").into());
}
let config_content = fs::read_to_string(&config_path)
.map_err(|e| Box::new(e) as Box<dyn StdError>)?;
let circle_configs: Vec<CircleConfig> = match serde_json::from_str(&config_content) {
Ok(configs) => configs,
Err(e) => {
error!("Failed to parse circles.json: {}. Ensure it's a valid JSON array of CircleConfig.", e);
// Explicitly cast serde_json::Error to Box<dyn StdError>
return Err(Box::new(e) as Box<dyn StdError>);
}
};
if circle_configs.is_empty() {
info!("No circle configurations found in {}. Exiting.", config_path.display());
return Ok(());
}
// run_launcher already returns Result<(), Box<dyn StdError>>, so this should be fine.
run_launcher(args, circle_configs).await
}

295
src/launcher/src/lib.rs Normal file
View File

@@ -0,0 +1,295 @@
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
// std::process::{Command, Child, Stdio}; // All parts of this line are no longer used directly here
use tokio::task::JoinHandle;
use actix_web::dev::ServerHandle;
use tokio::signal;
use std::time::Duration;
use clap::Parser;
use comfy_table::{Table, Row, Cell};
use log::{info, warn};
use secp256k1::{Secp256k1, rand};
use rhai_client::RhaiClient;
use circle_ws_lib::{spawn_circle_server, ServerConfig};
// use rhai::Engine; // No longer directly used, engine comes from create_heromodels_engine
use rhailib_worker::spawn_rhai_worker; // Added
use tokio::sync::mpsc; // Added
use std::env; // Added
use engine::create_heromodels_engine;
use heromodels::db::hero::OurDB;
const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1:6379";
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Args {
/// Path to the circles.json configuration file
#[arg(default_value = "./circles.json")]
pub config_path: PathBuf,
/// Optional path to write the output JSON file with circle details
#[arg(long)]
pub output: Option<PathBuf>,
/// Enable debug mode
#[arg(short, long)]
pub debug: bool,
/// Verbosity level
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CircleConfig {
pub name: String,
pub port: u16,
pub script_path: Option<String>,
}
#[derive(Serialize, Debug, Clone)]
pub struct CircleOutput {
pub name: String,
pub public_key: String,
pub secret_key: String,
pub worker_queue: String,
pub ws_url: String,
}
pub struct RunningCircleInfo {
pub config: CircleConfig,
pub worker_queue: String,
pub ws_url: String,
pub public_key: String,
// pub worker_process: Child, // Changed
pub worker_task_join_handle: JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>>, // Added
pub worker_shutdown_tx: mpsc::Sender<()>, // Added
pub ws_server_instance_handle: Arc<Mutex<Option<ServerHandle>>>,
pub _ws_server_task_join_handle: JoinHandle<std::io::Result<()>>,
}
pub async fn setup_and_spawn_circles(circle_configs: Vec<CircleConfig>) -> Result<(Vec<Arc<Mutex<RunningCircleInfo>>>, Vec<CircleOutput>), Box<dyn std::error::Error>> {
if circle_configs.is_empty() {
warn!("No circle configurations found. Exiting.");
return Ok((Vec::new(), Vec::new()));
}
info!("Loaded {} circle configurations.", circle_configs.len());
let rhai_client = RhaiClient::new(DEFAULT_REDIS_URL)?;
let mut running_circles_store: Vec<Arc<Mutex<RunningCircleInfo>>> = Vec::new();
let mut circle_outputs: Vec<CircleOutput> = Vec::new();
let data_dir = PathBuf::from("./launch_data");
if !data_dir.exists() {
fs::create_dir_all(&data_dir).map_err(|e| {
format!("Failed to create data directory '{}': {}", data_dir.display(), e)
})?;
info!("Created data directory: {}", data_dir.display());
}
for (idx, config) in circle_configs.into_iter().enumerate() { // Added enumerate for circle_id
info!("Initializing Circle Name: '{}', Port: {}", config.name, config.port);
let secp = Secp256k1::new();
let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng());
let public_key_hex = public_key.to_string();
// Spawn Rhai worker as a Tokio task
let (worker_shutdown_tx, worker_shutdown_rx) = mpsc::channel(1);
// --- Initialize OurDB and the Rhai Engine for this circle ---
let db_path_str = format!("./launch_data/circle_db_{}.db", config.name);
let db = Arc::new(OurDB::new(db_path_str, true)?);
let engine = create_heromodels_engine(db.clone());
// --- End Engine Initialization ---
let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string());
// Using idx as a placeholder for circle_id. Consider a more robust ID if needed.
let circle_id_for_worker = idx as u32;
// Defaulting preserve_tasks to false. Make configurable if needed.
let preserve_tasks = env::var("PRESERVE_TASKS").is_ok();
let worker_task_join_handle = spawn_rhai_worker(
circle_id_for_worker,
public_key_hex.clone(),
engine,
redis_url.clone(),
worker_shutdown_rx,
preserve_tasks
);
let worker_queue = format!("rhai_tasks:{}", public_key_hex);
let ws_url = format!("ws://127.0.0.1:{}", config.port);
// If a script is provided, read it and submit it to the worker
if let Some(script_path_str) = &config.script_path {
info!("Found script for circle '{}' at path: {}", config.name, script_path_str);
let script_path = PathBuf::from(script_path_str);
if script_path.exists() {
let script_content = fs::read_to_string(&script_path)
.map_err(|e| format!("Failed to read script file '{}': {}", script_path.display(), e))?;
info!("Submitting script to worker queue '{}'", worker_queue);
let task_id = rhai_client.submit_script(
&public_key_hex, // Use public key as the circle identifier
script_content,
Some(public_key_hex.clone()),
).await?;
info!("Script for circle '{}' submitted with task ID: {}", config.name, task_id);
} else {
warn!("Script path '{}' for circle '{}' does not exist. Skipping.", script_path.display(), config.name);
}
}
// Spawn the WebSocket server for the circle
let server_config = ServerConfig {
port: config.port,
circle_name: config.name.clone(),
circle_public_key: public_key_hex.clone(),
redis_url: env::var("REDIS_URL").unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string()), // Ensure server also gets correct redis_url
host: "127.0.0.1".to_string(),
enable_auth: false,
cert_path: None,
key_path: None,
};
let (ws_server_task_join_handle, ws_server_instance_handle) = spawn_circle_server(server_config.clone())?;
circle_outputs.push(CircleOutput {
name: config.name.clone(),
public_key: public_key_hex.clone(),
secret_key: secret_key.display_secret().to_string(),
worker_queue: worker_queue.clone(),
ws_url: ws_url.clone(),
});
running_circles_store.push(Arc::new(Mutex::new(RunningCircleInfo {
config,
worker_queue,
ws_url,
public_key: public_key_hex,
worker_task_join_handle, // Changed
worker_shutdown_tx, // Added
ws_server_instance_handle: Arc::new(Mutex::new(Some(ws_server_instance_handle))),
_ws_server_task_join_handle: ws_server_task_join_handle,
})));
}
Ok((running_circles_store, circle_outputs))
}
pub async fn shutdown_circles(running_circles_store: Vec<Arc<Mutex<RunningCircleInfo>>>) {
for circle_arc in &running_circles_store {
let (name, worker_shutdown_tx, mut worker_task_join_handle_opt, server_handle_opt);
{
let mut circle_info = circle_arc.lock().unwrap();
name = circle_info.config.name.clone();
// Take ownership of the JoinHandle and Sender for shutdown
// We need to replace them with something to satisfy the struct,
// but they won't be used again for this instance.
let (dummy_tx, _dummy_rx) = mpsc::channel(1);
worker_shutdown_tx = std::mem::replace(&mut circle_info.worker_shutdown_tx, dummy_tx);
// Create a dummy JoinHandle for replacement
let dummy_join_handle = tokio::spawn(async { Ok(()) as Result<(), Box<dyn std::error::Error + Send + Sync>> });
worker_task_join_handle_opt = Some(std::mem::replace(&mut circle_info.worker_task_join_handle, dummy_join_handle));
server_handle_opt = circle_info.ws_server_instance_handle.lock().unwrap().take();
}
info!("Shutting down Circle: '{}'", name);
// Shutdown worker task
if let Err(e) = worker_shutdown_tx.send(()).await {
warn!("Failed to send shutdown signal to worker for Circle '{}': {}. Worker might have already exited.", name, e);
}
if let Some(worker_task_join_handle) = worker_task_join_handle_opt.take() {
match worker_task_join_handle.await {
Ok(Ok(_)) => info!("Worker task for Circle '{}' shut down gracefully.", name),
Ok(Err(e)) => warn!("Worker task for Circle '{}' returned an error: {:?}", name, e),
Err(e) => warn!("Worker task for Circle '{}' panicked: {:?}", name, e),
}
} else {
warn!("No worker task join handle found for Circle '{}'.", name);
}
// Shutdown WebSocket server
if let Some(handle) = server_handle_opt {
info!("Stopping WebSocket server for Circle '{}' ...", name);
handle.stop(true).await;
info!("WebSocket server for Circle '{}' stop signal sent.", name);
} else {
warn!("No server handle to stop WebSocket server for Circle '{}'.", name);
}
}
}
pub async fn run_launcher(args: Args, circle_configs: Vec<CircleConfig>) -> Result<(), Box<dyn std::error::Error>> {
if std::env::var("RUST_LOG").is_err() {
let log_level = if args.debug {
"debug".to_string()
} else {
match args.verbose {
0 => "info,actix_server=warn,actix_web=warn".to_string(),
1 => "info,circle_ws_lib=info,actix_server=info,actix_web=info".to_string(),
2 => "debug,launcher=debug,worker_lib=debug,circle_ws_lib=debug,actix_server=info,actix_web=info".to_string(),
_ => "debug".to_string(),
}
};
std::env::set_var("RUST_LOG", log_level);
}
env_logger::init();
info!("Starting Circles Orchestrator...");
let (running_circles_store, circle_outputs) = setup_and_spawn_circles(circle_configs).await?;
if running_circles_store.is_empty() {
warn!("No circles were started. Exiting.");
return Ok(());
}
info!("All configured circles have been processed. Displaying circles table.");
{
let circles = running_circles_store.iter()
.map(|arc_info| arc_info.lock().unwrap())
.collect::<Vec<_>>();
let mut table = Table::new();
table.set_header(vec!["Name", "Public Key", "Worker Queue", "WS URL"]);
for info in circles.iter() {
let mut row = Row::new();
row.add_cell(Cell::new(&info.config.name));
row.add_cell(Cell::new(&info.public_key));
row.add_cell(Cell::new(&info.worker_queue));
row.add_cell(Cell::new(&info.ws_url));
table.add_row(row);
}
println!("{}", table);
}
if let Some(output_path) = args.output {
info!("Writing circle details to {:?}", output_path);
let json_output = serde_json::to_string_pretty(&circle_outputs)?;
fs::write(&output_path, json_output)?;
info!("Successfully wrote circle details to {:?}", output_path);
}
info!("Press Ctrl+C to shutdown all circles.");
signal::ctrl_c().await?;
info!("Ctrl-C received. Initiating graceful shutdown of all circles...");
shutdown_circles(running_circles_store).await;
tokio::time::sleep(Duration::from_secs(2)).await;
info!("Orchestrator shut down complete.");
Ok(())
}

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