Split Rust code into domain modules
This commit is contained in:
252
src/app/generation.rs
Normal file
252
src/app/generation.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ChatMessage {
|
||||
pub(super) id: i32,
|
||||
pub(super) user: bool,
|
||||
pub(super) reasoning: Option<String>,
|
||||
pub(super) reasoning_complete: bool,
|
||||
pub(super) reasoning_open: bool,
|
||||
pub(super) content: String,
|
||||
pub(super) markdown: Vec<markdown::Item>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub(super) fn append(&mut self, reasoning: bool, chunk: &str) {
|
||||
if reasoning {
|
||||
self.reasoning.get_or_insert_default().push_str(chunk);
|
||||
} else {
|
||||
self.reasoning_complete = true;
|
||||
self.content.push_str(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn refresh_markdown(&mut self) {
|
||||
if !self.user {
|
||||
let content = if self.reasoning.is_some() {
|
||||
self.content.trim_start()
|
||||
} else {
|
||||
&self.content
|
||||
};
|
||||
self.markdown = markdown::parse(content).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoredMessage> for ChatMessage {
|
||||
fn from(message: StoredMessage) -> Self {
|
||||
let mut message = Self {
|
||||
id: message.id,
|
||||
user: message.user,
|
||||
reasoning: message.reasoning,
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
reasoning_open: false,
|
||||
content: message.content,
|
||||
markdown: Vec::new(),
|
||||
};
|
||||
message.refresh_markdown();
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn start_generation(&mut self) {
|
||||
if self.generating || self.selected_session.is_none() {
|
||||
return;
|
||||
}
|
||||
let prompt = self.composer.trim().to_owned();
|
||||
if prompt.is_empty() {
|
||||
return;
|
||||
}
|
||||
let model = match ModelChoice::from_id(&self.preferences.selected_model) {
|
||||
Some(model) => model,
|
||||
None => {
|
||||
self.error = Some("The selected model is not supported.".into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let effective = self.preferences.generation().and_then(|generation| {
|
||||
self.preferences.runtime().and_then(|runtime| {
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
|
||||
})
|
||||
});
|
||||
let effective = match effective {
|
||||
Ok(settings) => settings,
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.expect("a selected session was checked");
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut messages = self
|
||||
.conversation
|
||||
.iter()
|
||||
.map(|message| ChatTurn {
|
||||
user: message.user,
|
||||
skip_previous_eos: false,
|
||||
reasoning: message.reasoning.clone(),
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
content: message.content.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
#[cfg(target_os = "macos")]
|
||||
messages.push(ChatTurn {
|
||||
user: true,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
content: prompt.clone(),
|
||||
});
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let Some(service) = &self.generation_service else {
|
||||
self.error = Some("The model runtime is unavailable.".into());
|
||||
return;
|
||||
};
|
||||
let Some(database) = &mut self.database else {
|
||||
return;
|
||||
};
|
||||
let saved = match database.start_chat_turn(session_id, &prompt, assistant_reasoning) {
|
||||
Ok(turn) => turn,
|
||||
Err(error) => {
|
||||
self.error = Some(format!("Could not save the chat turn: {error}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let idle_timeout =
|
||||
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
|
||||
self.active_generation = match service.generate(
|
||||
effective.engine,
|
||||
effective.turn,
|
||||
messages,
|
||||
CheckpointTarget::Local(session_checkpoint_path(session_id)),
|
||||
idle_timeout,
|
||||
) {
|
||||
Ok(active) => Some(active),
|
||||
Err(error) => {
|
||||
self.generation_service = None;
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let user = ChatMessage::from(saved.0);
|
||||
let mut assistant = ChatMessage::from(saved.1);
|
||||
assistant.reasoning_open = assistant_reasoning;
|
||||
self.composer.clear();
|
||||
self.conversation.push(user);
|
||||
self.conversation.push(assistant);
|
||||
self.generating = true;
|
||||
self.tokens_per_second = None;
|
||||
self.error = None;
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = effective;
|
||||
self.error = Some("Local Metal generation requires macOS.".into());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn poll_generation(&mut self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
let Some(active) = &mut self.active_generation else {
|
||||
self.generating = false;
|
||||
return false;
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut transcript_changed = false;
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut context_changed = false;
|
||||
#[cfg(target_os = "macos")]
|
||||
loop {
|
||||
match active.events.try_recv() {
|
||||
Ok(GenerationEvent::Loading) => {}
|
||||
Ok(GenerationEvent::Chunk { reasoning, content }) => {
|
||||
if let Some(message) = self.conversation.last_mut()
|
||||
&& !message.user
|
||||
{
|
||||
message.append(reasoning, &content);
|
||||
transcript_changed = true;
|
||||
}
|
||||
}
|
||||
Ok(GenerationEvent::Context {
|
||||
used,
|
||||
limit,
|
||||
tokens_per_second,
|
||||
}) => {
|
||||
self.context_used = used;
|
||||
self.context_limit = limit;
|
||||
self.tokens_per_second = tokens_per_second;
|
||||
context_changed = true;
|
||||
}
|
||||
Ok(GenerationEvent::Finished(result)) => {
|
||||
self.generating = false;
|
||||
if let Err(error) = result {
|
||||
self.error = Some(error);
|
||||
}
|
||||
self.active_generation = None;
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.generating = false;
|
||||
self.active_generation = None;
|
||||
self.error = Some("The model runtime stopped unexpectedly.".into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if transcript_changed && let Some(message) = self.conversation.last_mut() {
|
||||
message.refresh_markdown();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if transcript_changed
|
||||
&& let Some(message) = self.conversation.last()
|
||||
&& let Some(database) = &mut self.database
|
||||
&& let Err(error) = database.update_message(
|
||||
message.id,
|
||||
message.reasoning.as_deref(),
|
||||
message.reasoning_complete,
|
||||
&message.content,
|
||||
)
|
||||
{
|
||||
if let Some(active) = &self.active_generation {
|
||||
active.cancel.store(true, Ordering::Relaxed);
|
||||
}
|
||||
self.error = Some(format!("Could not save generated chat text: {error}"));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if context_changed
|
||||
&& let Some(session_id) = self.selected_session
|
||||
&& let Some(database) = &mut self.database
|
||||
{
|
||||
if let Err(error) = database.update_session_context(
|
||||
session_id,
|
||||
self.context_used,
|
||||
self.context_limit,
|
||||
self.tokens_per_second,
|
||||
) {
|
||||
self.error = Some(format!("Could not save context usage: {error}"));
|
||||
} else if let Some(session) = self
|
||||
.projects
|
||||
.iter_mut()
|
||||
.flat_map(|project| &mut project.sessions)
|
||||
.find(|session| session.id == session_id)
|
||||
{
|
||||
session.context_used = self.context_used as i32;
|
||||
session.context_limit = self.context_limit as i32;
|
||||
session.last_tokens_per_second = self.tokens_per_second;
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
return transcript_changed;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user