1use std::rc::Rc;
4use std::sync::{Arc, Mutex};
5
6use ravenrs_app_i18n::{Language, Texts};
7use ravenrs_reactive::Binding;
8use ravenrs_widgets::{BuildCx, Chat, ChatMessage, List, ListItem, NodeId, UiWaker, View};
9
10use crate::assistant::{short_moment_row, whole_answers};
11use crate::search::{Answer, Moment, MovieLibrary};
12
13pub type MovieWriter = Rc<dyn Fn(&str, &[Moment], Arc<Mutex<String>>, UiWaker, String)>;
17
18const MOMENTS: usize = 3;
20
21#[derive(Clone, Debug)]
23#[non_exhaustive]
24#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
25pub struct MovieChatLabels {
26 pub placeholder: String,
28 pub intro: String,
30 pub found: String,
32 pub nothing_found: String,
34}
35
36impl Default for MovieChatLabels {
37 fn default() -> Self {
38 Self::from_texts(&Texts::new(Language::English))
39 }
40}
41
42#[cfg_attr(feature = "live", ravenrs_live::expose)]
43impl MovieChatLabels {
44 pub fn from_texts(texts: &Texts) -> Self {
46 Self {
47 placeholder: texts.get("movie.chat.placeholder"),
48 intro: texts.get("movie.chat.intro"),
49 found: texts.get("movie.chat.found"),
50 nothing_found: texts.get("movie.chat.nothing_found"),
51 }
52 }
53
54 pub fn pt_br() -> Self {
56 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
57 }
58}
59
60#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
78pub struct MovieChat {
79 library: MovieLibrary,
80 messages: Binding<Vec<ChatMessage>>,
81 on_open: Rc<dyn Fn(&Moment)>,
82 labels: MovieChatLabels,
83 examples: Vec<String>,
84 writer: Option<MovieWriter>,
85}
86
87impl std::fmt::Debug for MovieChat {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("MovieChat").field("library", &self.library).finish_non_exhaustive()
90 }
91}
92
93#[cfg_attr(feature = "live", ravenrs_live::expose)]
94impl MovieChat {
95 pub fn new(
97 library: MovieLibrary,
98 messages: Binding<Vec<ChatMessage>>,
99 on_open: impl Fn(&Moment) + 'static,
100 ) -> Self {
101 Self {
102 library,
103 messages,
104 on_open: Rc::new(on_open),
105 labels: MovieChatLabels::default(),
106 examples: Vec::new(),
107 writer: None,
108 }
109 }
110
111 #[must_use]
116 pub fn writer(mut self, writer: Option<MovieWriter>) -> Self {
117 self.writer = writer;
118 self
119 }
120
121 #[must_use]
123 pub fn labels(mut self, labels: MovieChatLabels) -> Self {
124 self.labels = labels;
125 self
126 }
127
128 #[must_use]
130 pub fn examples(mut self, examples: impl IntoIterator<Item = impl Into<String>>) -> Self {
131 self.examples = examples.into_iter().map(Into::into).collect();
132 self
133 }
134}
135
136#[cfg_attr(feature = "live", ravenrs_live::expose)]
137impl MovieAnswers {
138 pub fn answer(&self, question: &str, waker: &UiWaker) -> ChatMessage {
141 let answers: Vec<Answer> = whole_answers(self.library.ask(question, MOMENTS));
142 if answers.is_empty() {
143 return ChatMessage::assistant(self.labels.nothing_found.clone());
144 }
145 let message = if let Some(writer) = &self.writer {
146 let moments: Vec<Moment> = answers.iter().map(|answer| answer.moment.clone()).collect();
147 let text = Arc::new(Mutex::new(String::new()));
148 writer(question, &moments, text.clone(), waker.clone(), self.labels.found.clone());
149 ChatMessage::assistant_writing(text)
150 } else {
151 ChatMessage::assistant(answers[0].moment.text.clone())
153 };
154 let (library, on_open) = (self.library.clone(), self.on_open.clone());
155 message.card(move || {
156 let rows: Vec<ListItem> = answers
157 .iter()
158 .map(|answer| {
159 let on_open = on_open.clone();
160 short_moment_row(&library, answer, move |moment| on_open(moment))
161 })
162 .collect();
163 Box::new(List::new(rows))
164 })
165 }
166}
167
168#[derive(Clone)]
170#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
171pub struct MovieAnswers {
172 pub library: MovieLibrary,
174 pub labels: MovieChatLabels,
176 pub on_open: Rc<dyn Fn(&Moment)>,
178 pub writer: Option<MovieWriter>,
180}
181
182impl std::fmt::Debug for MovieAnswers {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.debug_struct("MovieAnswers").field("library", &self.library).finish_non_exhaustive()
185 }
186}
187
188impl View for MovieChat {
189 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
190 let answering = MovieAnswers {
191 library: self.library.clone(),
192 labels: self.labels.clone(),
193 on_open: self.on_open.clone(),
194 writer: self.writer.clone(),
195 };
196 let (messages, waker) = (self.messages.clone(), cx.ui_waker());
197 let chat = Chat::new(self.messages.clone())
198 .placeholder(self.labels.placeholder.clone())
199 .intro(self.labels.intro.clone())
200 .examples(self.examples.clone())
201 .on_send(move |question| {
202 let reply = answering.answer(&question, &waker);
203 messages.update(|all| all.push(reply));
204 });
205 chat.build(cx)
206 }
207}