/// Valence Framework — Patterns 1-4 Extracted
/// Reusable component patterns with full interaction protocol
///
/// Patterns 1-4 are extracted from real use in Helios.
/// Each pattern is a function that returns (visual, narrative, actions) or (visual, narrative)
/// depending on whether it has interactive elements.
using @element { div, span, text, text_dyn, type DomNode, events }
using @resource { signal, memo, effect, type Signal }
// ============================================================================
// CORE DATA TYPES (shared with apps that use these patterns)
// ============================================================================
pub struct Event {
pub timestamp: String
pub actor: String
pub kind: String // event type: "tool_call", "commit", "heartbeat", "gesture_tap", ...
pub action: String
pub target: String
pub source: String
pub message: String
pub data: String // kind-specific JSON payload (optional, empty = none)
}
// ============================================================================
// ACTION TYPES FOR INTERACTIVE PATTERNS
// ============================================================================
/// Pattern 2: Event Stream Actions
/// Users can focus on a specific actor or clear the focus to see all events
pub enum EventStreamAction {
FocusEntity(String) // Focus on actor: "haiku", "opus", etc.
ClearFocus // Show all events
}
/// Pattern 1: Instance Panel Actions
/// Users can select an instance to see its details
pub enum InstancePanelAction {
SelectInstance(String) // Select actor by name
ClearSelection // Deselect
}
/// Pattern 4: Nav Bar Actions
/// Users can switch between tabs
pub enum NavBarAction {
SwitchTab(String) // Switch to tab name: "Events", "Instances", etc.
}
/// Standard return from on_action for all patterns
/// new_narrative is always detail-level, so caller confirms what happened
pub struct PatternActionResult {
pub success : Bool
pub message : String // "Focused on haiku" or "Cannot select: no events"
pub new_narrative : String // detail-level narrative after action
}
// ============================================================================
// PATTERN 1 — REACTIVE VALUE / GROWING LIST
// Status bar — simple metrics display, no actions
// ============================================================================
pub fn status_bar_pattern(
all_events: Signal[Array[Event]],
) -> (DomNode, () -> String) {
let event_count = memo(fn() { all_events.get().length() })
let instance_count_active = 4
let instance_count_total = 55
let visual = div(class="status-bar-fixed") <| [
div(class="status-metric") <| [
text("📊 Events: "),
text_dyn(fn() { event_count().to_string() })
],
div(class="status-metric") <| [
text("🤖 Instances: "),
text(instance_count_active.to_string() + "/" + instance_count_total.to_string())
],
div(class="status-metric") <| [
text("⚡ Status: ok")
]
]
let narrative = fn() {
event_count().to_string() + " events, " + instance_count_active.to_string() + "/" + instance_count_total.to_string() + " instances"
}
(visual, narrative)
}
// ============================================================================
// PATTERN 1 — REACTIVE VALUE / GROWING LIST
// Instance panel — tiles with selection capability and action protocol
// ============================================================================
pub struct InstancePanelActions {
pub available : () -> Array[InstancePanelAction]
pub can_do : (InstancePanelAction) -> Bool
pub on_action : (InstancePanelAction) -> PatternActionResult
}
pub fn instance_panel_pattern(
all_events: Signal[Array[Event]],
on_select: (String) -> Unit,
) -> (DomNode, () -> String, InstancePanelActions) {
let actors = ["haiku", "paladin", "proto", "opus", "vesper", "river"]
let selected = signal("")
let actor_summaries = memo(fn() {
let events = all_events.get()
let result : Array[(String, Int, String)] = []
for i = 0; i < actors.length(); i = i + 1 {
let actor = actors[i]
let actor_events = events.filter(fn(e) { e.actor == actor })
let count = actor_events.length()
let action = if count > 0 { actor_events[0].action } else { "—" }
let _ = result.push((actor, count, action))
}
result
})
let _rebuild = effect(fn() {
let summaries = actor_summaries()
let html = {
let mut h = ""
for i = 0; i < summaries.length(); i = i + 1 {
let item = summaries[i]
let actor = item.0
let count = item.1
let last_action = item.2
h = h + "" +
"" + actor + " " +
"" + count.to_string() + " " +
"" + last_action + "" +
"
"
}
h
}
update_element_html("instance-panel-tiles", html)
})
let visual = div(class="instance-panel") <| [
div(id="instance-panel-tiles") <| []
]
let narrative = fn() {
let summaries = actor_summaries()
let sel = selected.get()
if sel.is_empty() {
summaries.length().to_string() + " instances"
} else {
summaries.length().to_string() + " instances (selected: " + sel + ")"
}
}
let actions = InstancePanelActions::{
available: fn() {
[SelectInstance("haiku"), SelectInstance("paladin"), SelectInstance("proto"),
SelectInstance("opus"), SelectInstance("vesper"), SelectInstance("river"),
ClearSelection]
},
can_do: fn(action) {
match action {
SelectInstance(id) => not(id.is_empty())
ClearSelection => true
}
},
on_action: fn(action) {
match action {
SelectInstance(id) => {
selected.set(id)
on_select(id)
PatternActionResult::{
success: true,
message: "Selected " + id,
new_narrative: narrative()
}
}
ClearSelection => {
selected.set("")
on_select("")
PatternActionResult::{
success: true,
message: "Cleared selection",
new_narrative: narrative()
}
}
}
}
}
(visual, narrative, actions)
}
// ============================================================================
// PATTERN 2 — EVENT STREAM (ZOOM-AWARE)
// Live feed with zoom levels and focus filtering
// ============================================================================
pub struct EventStreamActions {
pub available : () -> Array[EventStreamAction]
pub can_do : (EventStreamAction) -> Bool
pub on_action : (EventStreamAction) -> PatternActionResult
}
fn render_event_row_html(e: Event, zoom: ZoomLevel) -> String {
let zoom_class = zoom_to_css_class(zoom)
match zoom {
ZoomLevel::ZoomedOut => {
let cls = if zoom_class.is_empty() { "event-row" } else { "event-row " + zoom_class }
"" +
"" + e.timestamp + " " +
"" + e.actor + " " +
"" + truncate_string(e.message, 40) + "" +
"
"
}
ZoomLevel::Default => {
"" +
"" + e.timestamp + " " +
"" + e.actor + " " +
"" + e.action + " " +
"" + e.message + "" +
"
"
}
ZoomLevel::ZoomedIn => {
"" +
"
" + e.timestamp + "
" +
"
" + e.actor + "
" +
"
" + e.action + " → " + e.target + "
" +
"
" + e.source + "
" +
"
" + e.message + "
" +
"
"
}
}
}
pub fn event_stream_pattern(
all_events: Signal[Array[Event]],
zoom: Signal[ZoomLevel],
focus_entity: Signal[String],
) -> (DomNode, NarrativeRender, EventStreamActions) {
let visible_events = memo(fn() {
let events = all_events.get()
let focus = focus_entity.get()
if focus.is_empty() {
events
} else {
events.filter(fn(e) { e.actor == focus })
}
})
let _rebuild = effect(fn() {
let events = visible_events()
let z = zoom.get()
let html = events
.map(fn(e) { render_event_row_html(e, z) })
.join("")
update_element_html("event-stream-rows", html)
})
let visual = div(class="event-stream") <| [
div(id="event-stream-rows") <| []
]
let narrative_render = NarrativeRender::{
summary: fn() {
visible_events().length().to_string() + " events"
},
detail: fn() {
let count = visible_events().length()
let focus = focus_entity.get()
if focus.is_empty() {
count.to_string() + " events"
} else {
count.to_string() + " events (focused: " + focus + ")"
}
},
full: fn() {
let count = visible_events().length()
let focus = focus_entity.get()
let zoom_level = match zoom.get() {
ZoomLevel::ZoomedOut => "zoomed out (dense)"
ZoomLevel::Default => "default zoom"
ZoomLevel::ZoomedIn => "zoomed in (detailed)"
}
if focus.is_empty() {
count.to_string() + " events, " + zoom_level
} else {
count.to_string() + " events (focused: " + focus + "), " + zoom_level
}
}
}
let actions = EventStreamActions::{
available: fn() {
let focus = focus_entity.get()
if focus.is_empty() {
[] // No actions available when showing all
} else {
[ClearFocus]
}
},
can_do: fn(action) {
match action {
FocusEntity(entity) => not(entity.is_empty())
ClearFocus => not(focus_entity.get().is_empty())
}
},
on_action: fn(action) {
match action {
FocusEntity(entity) => {
if not(entity.is_empty()) {
focus_entity.set(entity)
PatternActionResult::{
success: true,
message: "Focused on " + entity,
new_narrative: (narrative_render.detail)()
}
} else {
PatternActionResult::{
success: false,
message: "Cannot focus: entity name is empty",
new_narrative: (narrative_render.detail)()
}
}
}
ClearFocus => {
focus_entity.set("")
PatternActionResult::{
success: true,
message: "Cleared focus, showing all events",
new_narrative: (narrative_render.detail)()
}
}
}
}
}
(visual, narrative_render, actions)
}
// ============================================================================
// PATTERN 4 — PANELS AND ROOMS
// Navigation bar with tab switching
// ============================================================================
pub struct NavBarActions {
pub available : () -> Array[NavBarAction]
pub can_do : (NavBarAction) -> Bool
pub on_action : (NavBarAction) -> PatternActionResult
}
fn is_valid_tab(tab: String) -> Bool {
let valid_tabs = ["Events", "Instances", "Knowledge", "Peripherals", "Labs", "Config"]
for i = 0; i < valid_tabs.length(); i = i + 1 {
if valid_tabs[i] == tab {
return true
}
}
false
}
pub fn nav_bar_pattern(
current_tab: Signal[String],
) -> (DomNode, () -> String, NavBarActions) {
let tabs = ["Events", "Instances", "Knowledge", "Peripherals", "Labs", "Config"]
let visual = div(class="nav-bar-fixed") <| [
div(class="tabs-container") <| tabs.map(fn(tab_name) {
let is_active = memo(fn() { current_tab.get() == tab_name })
div(
dyn_class=fn() {
if is_active() { "tab active" } else { "tab" }
},
on=events().click(fn(_) {
current_tab.set(tab_name)
})
) <| [
text(tab_name)
]
})
]
let narrative = fn() {
"Nav: " + current_tab.get()
}
let actions = NavBarActions::{
available: fn() {
tabs.map(fn(tab) { SwitchTab(tab) })
},
can_do: fn(action) {
match action {
SwitchTab(tab) => is_valid_tab(tab)
}
},
on_action: fn(action) {
match action {
SwitchTab(tab) => {
if is_valid_tab(tab) {
current_tab.set(tab)
PatternActionResult::{
success: true,
message: "Switched to " + tab,
new_narrative: "Nav: " + tab
}
} else {
PatternActionResult::{
success: false,
message: "Cannot switch: invalid tab",
new_narrative: narrative()
}
}
}
}
}
}
(visual, narrative, actions)
}