use std::cell::RefCell;
use std::rc::Rc;
use crate::base::Rect;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::theme::TokenSet;
use crate::ui::{Element, EventCtx, Key, Mods, MouseButton, MouseKind, Phase, Role, UiEvent, View};
use super::overlays::Overlays;
use super::select::core::{
resolve_overlays, trigger_view, TriggerLabel, TypeAhead, DEFAULT_MAX_VISIBLE,
};
use super::viewport::use_viewport;
#[path = "reasoning_open.rs"]
mod popup;
use popup::{open_popup, ChangeFn, ChangeSlot, Session, Shared};
pub const REASONING_LADDER: [&str; 6] = ["none", "minimal", "low", "medium", "high", "xhigh"];
pub const REASONING_AUTO: &str = "auto";
const LOCK_GLYPH: &str = "\u{2298}";
const WHY_NON_REASONING: &str = "model does not reason";
const WHY_UNKNOWN: &str = "capability unknown";
const OVERRIDE_ROW: &str = "set anyway (capability unknown — passed verbatim)";
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum LockState {
Unlocked,
Locked,
}
pub fn reasoning_label(value: &str, state: LockState) -> String {
match state {
LockState::Unlocked => format!("r: {value}"),
LockState::Locked => format!("r: {value} (locked)"),
}
}
pub fn reasoning_label_glyph(value: &str, state: LockState) -> String {
match state {
LockState::Unlocked => format!("r: {value}"),
LockState::Locked => format!("r: {value} {LOCK_GLYPH}"),
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReasoningFacts {
pub support: Option<bool>,
pub levels: Vec<String>,
}
impl ReasoningFacts {
pub fn unknown() -> ReasoningFacts {
ReasoningFacts::default()
}
pub fn capable<I, S>(levels: I) -> ReasoningFacts
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
ReasoningFacts {
support: Some(true),
levels: levels.into_iter().map(Into::into).collect(),
}
}
pub fn non_reasoning() -> ReasoningFacts {
ReasoningFacts {
support: Some(false),
levels: Vec::new(),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum Mode {
Capable,
NonReasoning,
Unknown,
}
pub struct ReasoningSelect {
facts: ReasoningFacts,
value: Option<Signal<String>>,
max_visible: usize,
layout: Option<LayoutStyle>,
overlays: Option<Overlays>,
on_change: Option<ChangeFn>,
}
impl ReasoningSelect {
pub fn new(facts: ReasoningFacts) -> ReasoningSelect {
ReasoningSelect {
facts,
value: None,
max_visible: DEFAULT_MAX_VISIBLE,
layout: None,
overlays: None,
on_change: None,
}
}
pub fn value(mut self, value: Signal<String>) -> ReasoningSelect {
self.value = Some(value);
self
}
pub fn max_visible(mut self, n: usize) -> ReasoningSelect {
self.max_visible = n.max(1);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> ReasoningSelect {
self.layout = Some(layout);
self
}
pub fn overlays(mut self, overlays: &Overlays) -> ReasoningSelect {
self.overlays = Some(overlays.clone());
self
}
pub fn on_change(mut self, f: impl FnMut(&str) + 'static) -> ReasoningSelect {
self.on_change = Some(Box::new(f));
self
}
pub fn view(self, cx: Scope) -> View {
let t = crate::widgets::theme_tokens(cx);
self.element(cx, &t).build()
}
pub fn element(self, cx: Scope, t: &TokenSet) -> Element {
let mode = match self.facts.support {
Some(true) => Mode::Capable,
Some(false) => Mode::NonReasoning,
None => Mode::Unknown,
};
let mut offered: Vec<String> = vec![REASONING_AUTO.to_string(), "none".to_string()];
for level in &self.facts.levels {
if !level.is_empty() && !offered.iter().any(|v| v == level) {
offered.push(level.clone());
}
}
let value = self
.value
.unwrap_or_else(|| cx.signal(REASONING_AUTO.to_string()));
let overridden: Signal<bool> = cx.signal(false);
let display: Signal<Vec<usize>> = cx.signal(Vec::new());
let highlight: Signal<usize> = cx.signal(0);
let on_change: ChangeSlot = Rc::new(RefCell::new(self.on_change));
let session = Rc::new(RefCell::new(Session {
popup: None,
type_ahead: TypeAhead::default(),
unlocked: false,
anchor: Rect::new(0, 0, 0, 0),
}));
let focused = cx.signal(false);
let hovered = cx.signal(false);
let shared: Option<Rc<Shared>> = resolve_overlays(cx, self.overlays).map(|overlays| {
Rc::new(Shared {
cx,
tokens: *t,
mode,
offered,
max_visible: self.max_visible,
value,
overridden,
display,
highlight,
on_change,
session,
overlays,
viewport: use_viewport(cx),
})
});
let locked = matches!(mode, Mode::NonReasoning);
let mut el = Element::new()
.style(self.layout.unwrap_or_else(|| {
LayoutStyle::default()
.height(Dimension::Cells(1))
.grow(1.0)
.shrink(0.0)
}))
.role(Role::Button)
.access_label("reasoning")
.access_value(move || match mode {
Mode::NonReasoning => format!("none (locked — {WHY_NON_REASONING})"),
Mode::Unknown if !overridden.get_untracked() => {
format!("none (locked — {WHY_UNKNOWN})")
}
Mode::Unknown => format!(
"{} ({WHY_UNKNOWN} — passed verbatim)",
value.get_untracked()
),
Mode::Capable => value.get_untracked(),
})
.hover_signal(hovered)
.focus_signal(focused);
if !locked {
if let Some(shared) = shared.clone() {
let open = move |ctx: &mut EventCtx| {
let anchor = ctx.current_rect_screen();
open_popup(&shared, anchor);
};
el = el.focusable().on(Phase::Bubble, move |ctx, ev| match ev {
UiEvent::Key(k)
if (k.key == Key::Enter || k.key == Key::Char(' '))
&& k.mods == Mods::NONE =>
{
if focused.get_untracked() {
open(ctx);
ctx.stop_propagation();
}
}
UiEvent::Mouse(m) if matches!(m.kind, MouseKind::Down(MouseButton::Left)) => {
open(ctx);
ctx.stop_propagation();
}
_ => {}
});
} else {
debug_assert!(
false,
"ReasoningSelect: no Overlays available — build inside an App \
(context) or pass .overlays(..) explicitly"
);
}
}
el.child(trigger_view(
t,
focused,
hovered,
locked,
Rc::new(move || match mode {
Mode::NonReasoning => TriggerLabel {
text: format!(
"{} — {WHY_NON_REASONING}",
reasoning_label("none", LockState::Locked)
),
short: Some(reasoning_label("none", LockState::Locked)),
placeholder: true,
},
Mode::Unknown if !overridden.get() => TriggerLabel {
text: format!(
"{} — {WHY_UNKNOWN}",
reasoning_label("none", LockState::Locked)
),
short: Some(reasoning_label("none", LockState::Locked)),
placeholder: false,
},
_ => TriggerLabel {
text: reasoning_label(&value.get(), LockState::Unlocked),
short: None,
placeholder: false,
},
}),
))
}
}
#[cfg(test)]
#[path = "reasoning_tests.rs"]
mod tests;