bcode_hyperchad_ui 0.0.1-alpha.0

Portable HyperChad UI components for Bcode sessions
//! Session composer and turn controls.

use super::theme::{accent, color, radius, space, surface, typeface};
use crate::context::{PresentationAction, PresentationContext};
use bcode_session_view_models::SessionViewSnapshot;
use hyperchad::template::{Containers, container};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ComposerPresentationState {
    Ready,
    Pending,
    Error,
    Disabled,
}

fn composer_presentation_state(snapshot: &SessionViewSnapshot) -> ComposerPresentationState {
    if snapshot.composer.can_submit {
        return ComposerPresentationState::Ready;
    }
    let reason = snapshot
        .composer
        .disabled_reason
        .as_deref()
        .unwrap_or_default()
        .to_ascii_lowercase();
    if ["error", "failed", "invalid", "cannot", "unavailable"]
        .iter()
        .any(|term| reason.contains(term))
    {
        ComposerPresentationState::Error
    } else if ["pending", "sending", "submitting", "waiting", "queued"]
        .iter()
        .any(|term| reason.contains(term))
    {
        ComposerPresentationState::Pending
    } else {
        ComposerPresentationState::Disabled
    }
}

pub(super) fn composer(
    snapshot: &SessionViewSnapshot,
    context: &impl PresentationContext,
) -> Containers {
    let action = context.action_target(PresentationAction::SubmitMessage);
    let presentation_state = composer_presentation_state(snapshot);
    let (state_label, state_color) = match presentation_state {
        ComposerPresentationState::Ready => ("Ready", color::SUCCESS),
        ComposerPresentationState::Pending => ("Pending", color::WARNING),
        ComposerPresentationState::Error => ("Error", color::ERROR),
        ComposerPresentationState::Disabled => ("Disabled", color::MUTED),
    };
    let state_detail = snapshot.composer.disabled_reason.as_deref().unwrap_or({
        if snapshot.composer.can_submit {
            "Ready to send"
        } else {
            "Message submission is unavailable"
        }
    });
    container! {
        div #composer-region {
            form hx-post=(action) hx-target="#bcode-web-shell" hx-swap=this background=(surface::APP) border=((1, surface::BORDER)) border-radius=((radius::CARD)) padding=((space::MD)) {
                input type=hidden name="submission_id" value=(bcode_session_models::SessionId::new().to_string());
                @if let Some(session_id) = snapshot.session_id {
                    input type=hidden name="session_id" value=(session_id.to_string());
                }
                div data-composer-state=(state_label.to_ascii_lowercase()) color=(state_color) font-size=((typeface::DETAIL)) margin-bottom=((space::SM)) {
                    (state_label) ": " (state_detail)
                }
                div #composer-message-label color=(color::MUTED) font-size=((typeface::DETAIL)) margin-bottom=((space::XS)) { "Message" }
                @if let Some(session_id) = snapshot.session_id {
                    textarea #composer-message name="text" rows="5" data-label-id="composer-message-label" placeholder="Send a message to this session" hx-post=(context.action_target(PresentationAction::UpdateDraft { session_id })) hx-trigger="change" hx-target="#bcode-web-shell" hx-swap=this width=100% padding=((space::S10)) border=((1, surface::BORDER)) border-radius=((radius::CONTROL)) background=(surface::INSET) color=(color::TEXT) {
                        (snapshot.composer.draft)
                    }
                } @else {
                    textarea #composer-message name="text" rows="5" data-label-id="composer-message-label" placeholder="Send a message to start a session" width=100% padding=((space::S10)) border=((1, surface::BORDER)) border-radius=((radius::CONTROL)) background=(surface::INSET) color=(color::TEXT) {
                        (snapshot.composer.draft)
                    }
                }
                div direction=row justify-content=space-between align-items=center gap=((space::S10)) margin-top=((space::S10)) {
                    div {
                        div #message-placement-label color=(color::MUTED) font-size=((typeface::DETAIL)) margin-bottom=((space::XS)) { "Message placement" }
                        select #message-placement name="placement" selected="steering" data-label-id="message-placement-label" padding=((space::S7)) border=((1, surface::BORDER)) border-radius=((radius::CONTROL)) background=(surface::INSET) color=(color::TEXT) {
                            option value="steering" { "Steer the active turn" }
                            option value="follow_up" { "Queue as a follow-up" }
                        }
                    }
                    button type=submit disabled=[!snapshot.composer.can_submit] background=(if snapshot.composer.can_submit { accent::POSITIVE } else { surface::DISABLED }) color=(color::ON_ACCENT) border-radius=((radius::CONTROL)) padding="8, 14" {
                        (if snapshot.composer.can_submit { "Send message" } else { "Sending unavailable" })
                    }
                }
            }
            @if let Some(session_id) = snapshot.session_id {
                form hx-post=(context.action_target(PresentationAction::CancelTurn)) hx-target="#bcode-web-shell" hx-swap=this margin-top=((space::S10)) {
                    input type=hidden name="session_id" value=(session_id.to_string());
                    input type=hidden name="clear_queue" value="false";
                    button type=submit background=(accent::DESTRUCTIVE) color=(color::ON_ACCENT) border-radius=((radius::CONTROL)) padding="8, 14" {
                        "Cancel active turn"
                    }
                }
            }
        }
    }
}