Skip to main content

fc_dioxus/components/
context_strip.rs

1//! Provider, model, and effort as one piece of state, from context-strip.css. The rules
2//! are [`crate::behavior::context`], ported from upstream's `context-strip.ts`.
3use crate::{
4    behavior::context::{ContextModel, ContextState, Effort, ProviderChoice},
5    core::{
6        assets,
7        attrs::{applicable_attribute, component_attributes, context_attributes},
8    },
9    prelude::*,
10    theme::use_stylesheets,
11};
12const BEM: Bem = Bem::new("fc-context-strip");
13
14/// The controls above a composer: provider, model, effort, and the model's parameters.
15///
16/// Changing provider resolves the model (restoring the one this provider had last time).
17/// Effort applies only to reasoning models; for others the control is dimmed and
18/// labelled rather than removed, and a value the user set is kept, not cleared.
19#[component]
20pub fn ContextStrip(
21    providers: Vec<ProviderChoice>,
22    #[props(default)] onchange: Option<EventHandler<ContextState>>,
23    /// Hide the provider control when there is only one provider.
24    #[props(default = true)]
25    show_provider: bool,
26    #[props(default)] class: Option<String>,
27    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
28) -> Element {
29    let mut strip = use_signal(|| ContextModel::new(providers.clone()));
30    if strip.peek().providers() != providers.as_slice() {
31        strip.set(ContextModel::new(providers.clone()));
32    }
33    let model = strip.read().clone();
34    let state = model.state();
35    let reasoning = model.model().is_some_and(|m| m.reasoning);
36    let mut attributes = component_attributes(
37        attributes,
38        ClassList::new(BEM.block()).extend(class.as_deref()),
39        &[],
40    );
41    attributes.extend(context_attributes(
42        &state.provider,
43        &state.model,
44        state.effort.map(Effort::as_str),
45    ));
46    let mut update = move |change: &dyn Fn(&mut ContextModel)| {
47        let before = strip.peek().state();
48        change(&mut strip.write());
49        let after = strip.peek().state();
50        if after != before {
51            if let Some(onchange) = onchange {
52                onchange.call(after);
53            }
54        }
55    };
56    let provider_options: Vec<SelectChoice> = model
57        .providers()
58        .iter()
59        .map(|p| SelectChoice::new(p.id.clone(), p.name.clone()))
60        .collect();
61    let model_options: Vec<SelectChoice> = model
62        .provider()
63        .map(|p| {
64            p.models
65                .iter()
66                .map(|m| SelectChoice::new(m.id.clone(), m.name.clone()))
67                .collect()
68        })
69        .unwrap_or_default();
70    let effort_options: Vec<SelectChoice> = Effort::ALL
71        .iter()
72        .map(|e| SelectChoice::new(e.as_str(), e.label()))
73        .collect();
74    // The selects are controlled by the strip, which alone resolves their values.
75    let provider_value = use_memo(move || Some(strip.read().state().provider));
76    let model_value = use_memo(move || Some(strip.read().state().model));
77    let effort_value =
78        use_memo(move || strip.read().stored_effort().map(|e| e.as_str().to_owned()));
79    rsx! {
80        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CONTEXT_STRIP_CSS } }
81        div { ..attributes,
82            if show_provider {
83                div { class: BEM.element("field"),
84                    span { class: BEM.element("label"), "Provider" }
85                    Select { label: crate::name!("Provider"), options: provider_options,
86                        value: ReadSignal::from(provider_value),
87                        onchange: move |v: String| update(&|m| m.set_provider(&v)) }
88                }
89            }
90            div { class: BEM.element("field"),
91                span { class: BEM.element("label"), "Model" }
92                Select { label: crate::name!("Model"), options: model_options, value: ReadSignal::from(model_value),
93                    onchange: move |v: String| update(&|m| m.set_model(&v)) }
94            }
95            // Dimmed, never removed: a row that reflows when the model changes makes the
96            // strip jump, and a missing control is not announced at all.
97            div { class: BEM.element("field"), ..crate::spread![applicable_attribute(reasoning)],
98                span { class: BEM.element("label"), "Effort" }
99                Select { label: crate::name!("Effort"), options: effort_options, placeholder: "n/a",
100                    value: ReadSignal::from(effort_value),
101                    availability: if reasoning { Availability::Enabled } else { Availability::Disabled },
102                    onchange: move |v: String| if let Some(e) = Effort::from_value(&v) { update(&|m| m.set_effort(e)) } }
103            }
104            span { class: BEM.element("spacer") }
105            div { class: BEM.element("params"),
106                for (name, value) in state.parameters.iter() {
107                    span { key: "{name}", class: BEM.element("param"),
108                        span { class: BEM.element("param-name"), "{name}" }
109                        span { class: BEM.element("param-value"), "{value}" }
110                    }
111                }
112            }
113            span { class: BEM.element("notice"), role: "status", hidden: model.notice().is_none(),
114                "{model.notice().unwrap_or_default()}"
115            }
116        }
117    }
118}