1use tuile::prelude::*;
9
10const BRAND: ThemeSpec = ThemeSpec::new("brand", true, Rgb(124, 58, 237));
12
13const ORBIT: SpinnerDef = SpinnerDef::new("orbit", 120, &["●∙∙", "∙●∙", "∙∙●", "∙●∙"]);
15
16const REPLY: &str = "Sure - I'll switch the cache to an LRU with a 1 000 entry cap and add a hit/miss counter so we can see whether it earns its keep.";
17
18struct Demo {
19 chat: ChatState,
20 composer: ComposerState,
21 started: Instant,
22 stream_from: Option<Instant>,
23}
24
25impl App for Demo {
26 fn draw(&mut self, frame: &mut Frame, now: Instant) {
27 let area = frame.area();
28 let buf = frame.buffer_mut();
29 let th = theme::current();
30 fill(buf, area, th.background);
31
32 if let Some(t0) = self.stream_from {
34 let shown = revealed(REPLY, elapsed(t0, now), 45.0);
35 if let Some(last) = self.chat.messages.last_mut() {
36 last.text.clear();
37 last.text.push_str(shown);
38 }
39 if shown.len() == REPLY.len() {
40 self.chat.finish_stream();
41 self.stream_from = None;
42 }
43 }
44
45 let [chat, status, composer] = Layout::vertical([
46 Constraint::Fill(1),
47 Constraint::Length(1),
48 Constraint::Length(5),
49 ])
50 .areas(pad(area, 1, 0));
51 ChatView::new()
52 .show_time(true)
53 .now(now)
54 .render(chat, buf, &mut self.chat);
55
56 let [left, right] =
58 Layout::horizontal([Constraint::Length(28), Constraint::Fill(1)]).areas(status);
59 if self.stream_from.is_some() {
60 Thinking::new("Generating")
61 .spinner(&ORBIT)
62 .started(self.started)
63 .now(now)
64 .render(left, buf);
65 } else {
66 Spinner::new(&spinners::SPARKLE)
67 .label("idle - Enter to send")
68 .now(now)
69 .render(left, buf);
70 }
71 let used = self
72 .chat
73 .messages
74 .iter()
75 .map(|m| m.text.len() as u32 / 4)
76 .sum();
77 ContextGauge::new(TokenUsage {
78 prompt: used,
79 completion: 0,
80 limit: 8_000,
81 })
82 .compact(true)
83 .render(right, buf);
84
85 PromptComposer::new()
86 .model("brand-1")
87 .focused(true)
88 .now(now)
89 .render(composer, buf, &mut self.composer);
90 }
91
92 fn event(&mut self, ev: Event, now: Instant) -> Flow {
93 if let Event::Key(k) = &ev
94 && (ctrl(k, 'c') || k.code == KeyCode::Esc)
95 {
96 return Flow::Quit;
97 }
98 self.chat.handle(&ev);
99 self.composer.handle(&ev);
100 if let Some(text) = self.composer.take_submitted() {
101 self.chat.push(ChatMessage::new(Role::User, text));
102 self.chat
103 .push(ChatMessage::new(Role::Assistant, "").streaming(true));
104 self.chat.scroll_to_end();
105 self.started = now;
106 self.stream_from = Some(now);
107 }
108 Flow::Continue
109 }
110
111 fn animating(&self, _now: Instant) -> bool {
112 true }
114}
115
116fn main() -> std::io::Result<()> {
117 theme::set(Theme::resolve(&BRAND, None));
118 let mut chat = ChatState::new();
119 chat.push(ChatMessage::new(
120 Role::System,
121 "brand-1 · custom theme · custom spinner",
122 ));
123 chat.push(ChatMessage::new(
124 Role::Assistant,
125 "Ask me anything. Replies are canned, the widgets are not.",
126 ));
127 run(&mut Demo {
128 chat,
129 composer: ComposerState::default(),
130 started: Instant::now(),
131 stream_from: None,
132 })
133}