use std::cell::Cell;
use std::rc::Rc;
use crate::layout::Style as LayoutStyle;
use crate::reactive::{Scope, Signal};
use crate::theme::TokenSet;
use crate::ui::View;
use super::disclosure::Disclosure;
use super::feed::{Feed, FeedItem, FeedState};
use super::spinner::SpinnerKind;
const BODY_KEY: &str = "thinking";
#[derive(Clone)]
pub struct ThinkingFoldState {
feed: FeedState,
appends: Rc<Cell<u64>>,
completed: Rc<Cell<bool>>,
streaming: Signal<bool>,
detail: Signal<String>,
header: Signal<String>,
}
impl ThinkingFoldState {
pub fn new(cx: Scope) -> ThinkingFoldState {
let feed = FeedState::new(cx);
feed.push_stream(BODY_KEY);
ThinkingFoldState {
feed,
appends: Rc::new(Cell::new(0)),
completed: Rc::new(Cell::new(false)),
streaming: cx.signal(false),
detail: cx.signal(String::new()),
header: cx.signal(String::new()),
}
}
pub fn append(&self, fragment: &str) -> bool {
if self.completed.get() {
return false;
}
self.appends.set(self.appends.get() + 1);
if !self.streaming.get_untracked() {
self.streaming.set(true);
}
self.feed.stream_append(BODY_KEY, fragment);
self.recompose_header();
true
}
pub fn complete(&self, aggregate: &str) {
self.completed.set(true);
self.feed.update(BODY_KEY, FeedItem::markdown(aggregate));
if self.streaming.get_untracked() {
self.streaming.set(false);
}
self.recompose_header();
}
pub fn set_detail(&self, detail: impl Into<String>) {
self.detail.set(detail.into());
self.recompose_header();
}
pub fn is_streaming(&self) -> bool {
self.streaming.get()
}
pub fn is_completed(&self) -> bool {
self.completed.get()
}
fn recompose_header(&self) {
let detail = self.detail.get_untracked();
let composed = if self.streaming.get_untracked() {
let frames = SpinnerKind::Dots.frames();
let frame = frames[(self.appends.get() % frames.len() as u64) as usize];
if detail.is_empty() {
frame.to_string()
} else {
format!("{frame} {detail}")
}
} else {
detail
};
if self.header.get_untracked() != composed {
self.header.set(composed);
}
}
}
pub struct ThinkingFold {
state: ThinkingFoldState,
title: String,
max_body_rows: i32,
folded: Option<Signal<bool>>,
layout: Option<LayoutStyle>,
}
impl ThinkingFold {
pub fn new(state: &ThinkingFoldState) -> ThinkingFold {
ThinkingFold {
state: state.clone(),
title: String::from("Thinking"),
max_body_rows: 8,
folded: None,
layout: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> ThinkingFold {
self.title = title.into();
self
}
pub fn max_body_rows(mut self, rows: i32) -> ThinkingFold {
self.max_body_rows = rows;
self
}
pub fn folded(mut self, folded: Signal<bool>) -> ThinkingFold {
self.folded = Some(folded);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> ThinkingFold {
self.layout = Some(layout);
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) -> crate::ui::Element {
let tokens = *t;
let folded = self.folded.unwrap_or_else(|| cx.signal(true));
let feed = self.state.feed.clone();
let mut card = Disclosure::new(self.title)
.title_muted(true)
.detail_signal(self.state.header)
.folded(folded)
.max_body_rows(self.max_body_rows)
.body(move |gcx: Scope| Feed::new(&feed).gap(0).element(gcx, &tokens).build());
if let Some(layout) = self.layout {
card = card.layout(layout);
}
card.element(cx, t)
}
}
#[cfg(test)]
#[path = "thinking_fold_tests.rs"]
mod tests;