Skip to main content

fc_dioxus/behavior/
context.rs

1//! The context strip's state, ported from upstream's `context-strip.ts`.
2//!
3//! Provider filters model, and effort applies only to reasoning models. Both rules
4//! belong to the group, not to any one select: selects that each owned their own state
5//! would disagree, showing a model from one provider selected under another.
6
7use std::collections::HashMap;
8
9/// One model.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ModelChoice {
12    /// Stable id.
13    pub id: String,
14    /// What people read.
15    pub name: String,
16    /// Reasoning models accept an effort level; others do not.
17    pub reasoning: bool,
18    /// Read-only facts shown as chips, e.g. ("context", "200k"), in order.
19    pub parameters: Vec<(String, String)>,
20}
21
22/// One provider and the models it offers.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ProviderChoice {
25    /// Stable id.
26    pub id: String,
27    /// What people read.
28    pub name: String,
29    /// Its models, in the order offered.
30    pub models: Vec<ModelChoice>,
31}
32
33/// An effort level.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Effort {
36    /// Low.
37    Low,
38    /// Medium, the default when a reasoning model is chosen.
39    Medium,
40    /// High.
41    High,
42}
43impl Effort {
44    /// All levels, lowest first.
45    pub const ALL: [Self; 3] = [Self::Low, Self::Medium, Self::High];
46    /// The value written to `data-fc-effort`.
47    #[must_use]
48    pub const fn as_str(self) -> &'static str {
49        match self {
50            Self::Low => "low",
51            Self::Medium => "medium",
52            Self::High => "high",
53        }
54    }
55    /// What people read.
56    #[must_use]
57    pub const fn label(self) -> &'static str {
58        match self {
59            Self::Low => "Low",
60            Self::Medium => "Medium",
61            Self::High => "High",
62        }
63    }
64    /// Parse a value written by [`Effort::as_str`].
65    #[must_use]
66    pub fn from_value(value: &str) -> Option<Self> {
67        Self::ALL.into_iter().find(|e| e.as_str() == value)
68    }
69}
70
71/// The resolved state of the whole strip.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ContextState {
74    /// Provider id.
75    pub provider: String,
76    /// Model id.
77    pub model: String,
78    /// `None` when the current model has no effort control.
79    pub effort: Option<Effort>,
80    /// The current model's parameters.
81    pub parameters: Vec<(String, String)>,
82}
83
84/// The strip's model.
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct ContextModel {
87    providers: Vec<ProviderChoice>,
88    provider: String,
89    model: String,
90    effort: Option<Effort>,
91    /// The model last chosen under each provider, so switching back restores it.
92    remembered: HashMap<String, String>,
93}
94
95impl ContextModel {
96    /// Start on the first provider, so the strip is never half-set.
97    #[must_use]
98    pub fn new(providers: Vec<ProviderChoice>) -> Self {
99        let first = providers.first().map(|p| p.id.clone()).unwrap_or_default();
100        let mut model = Self {
101            providers,
102            ..Self::default()
103        };
104        model.set_provider(&first);
105        model
106    }
107    /// The providers.
108    #[must_use]
109    pub fn providers(&self) -> &[ProviderChoice] {
110        &self.providers
111    }
112    /// The current provider.
113    #[must_use]
114    pub fn provider(&self) -> Option<&ProviderChoice> {
115        self.providers.iter().find(|p| p.id == self.provider)
116    }
117    /// The current model.
118    #[must_use]
119    pub fn model(&self) -> Option<&ModelChoice> {
120        self.provider()?.models.iter().find(|m| m.id == self.model)
121    }
122    /// The effort the user set, even when the current model ignores it.
123    #[must_use]
124    pub fn stored_effort(&self) -> Option<Effort> {
125        self.effort
126    }
127    /// The resolved state.
128    #[must_use]
129    pub fn state(&self) -> ContextState {
130        let model = self.model();
131        ContextState {
132            provider: self.provider.clone(),
133            model: self.model.clone(),
134            effort: model.filter(|m| m.reasoning).and(self.effort),
135            parameters: model.map(|m| m.parameters.clone()).unwrap_or_default(),
136        }
137    }
138    /// Choose a provider and resolve the model against it: the model this provider had
139    /// last time, else the one just chosen if it is offered here too, else its first.
140    pub fn set_provider(&mut self, provider: &str) {
141        if !self.providers.iter().any(|p| p.id == provider) {
142            return;
143        }
144        if !self.provider.is_empty() {
145            self.remembered
146                .insert(self.provider.clone(), self.model.clone());
147        }
148        let previous = std::mem::take(&mut self.model);
149        provider.clone_into(&mut self.provider);
150        let available: Vec<String> = self
151            .provider()
152            .map(|p| p.models.iter().map(|m| m.id.clone()).collect())
153            .unwrap_or_default();
154        let candidates = [self.remembered.get(provider).cloned(), Some(previous)];
155        self.model = candidates
156            .into_iter()
157            .flatten()
158            .find(|id| !id.is_empty() && available.contains(id))
159            .or_else(|| available.first().cloned())
160            .unwrap_or_default();
161        self.default_effort();
162    }
163    /// Choose a model the current provider offers.
164    pub fn set_model(&mut self, model: &str) {
165        if self
166            .provider()
167            .is_some_and(|p| p.models.iter().any(|m| m.id == model))
168        {
169            model.clone_into(&mut self.model);
170            self.default_effort();
171        }
172    }
173    /// Set the effort. Ignored for a model with no effort control.
174    pub fn set_effort(&mut self, effort: Effort) {
175        if self.model().is_some_and(|m| m.reasoning) {
176            self.effort = Some(effort);
177        }
178    }
179    /// Medium when a reasoning model is chosen and nothing is set yet, so switching
180    /// between reasoning models keeps the user's choice.
181    fn default_effort(&mut self) {
182        if self.model().is_some_and(|m| m.reasoning) && self.effort.is_none() {
183            self.effort = Some(Effort::Medium);
184        }
185    }
186    /// What to say when the effort control does not apply, or `None` when it does.
187    #[must_use]
188    pub fn notice(&self) -> Option<&'static str> {
189        if self.model().is_some_and(|m| m.reasoning) {
190            None
191        } else if self.effort.is_some() {
192            Some("Effort is not used by this model")
193        } else {
194            Some("This model has no effort setting")
195        }
196    }
197}