229 lines
9.8 KiB
Rust
229 lines
9.8 KiB
Rust
use super::*;
|
||
use iced::widget::column;
|
||
|
||
impl App {
|
||
pub(super) fn chat_detail(&self) -> Element<'_, Message> {
|
||
let Some(item) = self.selected_project() else {
|
||
let open_project_content = row![icon(ICON_FOLDER_PLUS, 17), text("Open project…"),]
|
||
.spacing(8)
|
||
.align_y(Alignment::Center);
|
||
let open_project = if self.database.is_some() && !self.choosing_folder {
|
||
action_button(open_project_content).on_press(Message::ChooseProjectFolder)
|
||
} else {
|
||
action_button(open_project_content)
|
||
};
|
||
return container(
|
||
column![
|
||
icon(ICON_SPARK, 36),
|
||
text("Start a local coding session").size(28),
|
||
text("Choose a project folder to create your first session.").size(14),
|
||
Space::with_height(10),
|
||
open_project,
|
||
]
|
||
.spacing(10)
|
||
.align_x(Alignment::Center),
|
||
)
|
||
.center_x(Length::Fill)
|
||
.center_y(Length::Fill)
|
||
.into();
|
||
};
|
||
|
||
let project = &item.project;
|
||
let active_title = self.active_session_title(item);
|
||
let selected_title = active_title.unwrap_or(project.name.as_str());
|
||
let mut header = row![icon(ICON_FOLDER, 19), text(selected_title).size(18)];
|
||
if let Some(session) = self.selected_session(item) {
|
||
header = header.push(
|
||
button(icon(ICON_MORE, 18))
|
||
.on_press(Message::OpenSessionMenu(session.id))
|
||
.style(button::text),
|
||
);
|
||
}
|
||
let header = header
|
||
.push(Space::with_width(Length::Fill))
|
||
.spacing(10)
|
||
.align_y(Alignment::Center);
|
||
|
||
let body: Element<'_, Message> = if let Some(title) = active_title {
|
||
let mut messages = column![].spacing(12);
|
||
if self.conversation.is_empty() {
|
||
messages = messages.push(
|
||
column![
|
||
text(title).size(26),
|
||
text("Run DeepSeek locally with the Rust Metal engine.").size(14),
|
||
]
|
||
.spacing(8),
|
||
);
|
||
} else {
|
||
let markdown_style = markdown::Style::from_palette(app_theme().palette());
|
||
for (index, message) in self.conversation.iter().enumerate() {
|
||
let label = if message.user {
|
||
"You"
|
||
} else if message.tool {
|
||
"Tool"
|
||
} else {
|
||
"DS4"
|
||
};
|
||
let active = self.generating && index + 1 == self.conversation.len();
|
||
let mut body = column![text(label).size(11)].spacing(5);
|
||
if let Some(reasoning) = &message.reasoning {
|
||
let reasoning_label =
|
||
match (message.reasoning_open, message.reasoning_complete, active) {
|
||
(true, false, true) => "▾ Thinking",
|
||
(false, false, true) => "› Thinking",
|
||
(true, false, false) => "▾ Reasoning (stopped)",
|
||
(false, false, false) => "› Reasoning (stopped)",
|
||
(true, true, _) => "▾ Reasoning",
|
||
(false, true, _) => "› Reasoning",
|
||
};
|
||
body = body.push(
|
||
button(text(reasoning_label).size(12))
|
||
.padding(0)
|
||
.style(button::text)
|
||
.on_press(Message::ToggleReasoning(index)),
|
||
);
|
||
if message.reasoning_open {
|
||
body = body.push(
|
||
text(if reasoning.is_empty() && active {
|
||
"Thinking…"
|
||
} else {
|
||
reasoning
|
||
})
|
||
.size(13)
|
||
.color(muted_text()),
|
||
);
|
||
}
|
||
}
|
||
if !message.content.is_empty() {
|
||
if message.user || message.tool || message.markdown.is_empty() {
|
||
let content = if message.reasoning.is_some() {
|
||
crate::agent::visible_content(&message.content).trim_start()
|
||
} else {
|
||
crate::agent::visible_content(&message.content)
|
||
};
|
||
body = body.push(text(content).size(14));
|
||
} else {
|
||
body = body.push(
|
||
markdown::view(
|
||
&message.markdown,
|
||
markdown::Settings::with_text_size(14),
|
||
markdown_style,
|
||
)
|
||
.map(Message::OpenLink),
|
||
);
|
||
}
|
||
} else if active && message.reasoning.is_none() {
|
||
body = body.push(text("Loading model…").size(14));
|
||
}
|
||
if !message.user
|
||
&& !message.tool
|
||
&& let Some(model) = ModelChoice::from_id(&self.preferences.selected_model)
|
||
{
|
||
for summary in crate::agent::tool_summaries(model, &message.content) {
|
||
body = body.push(text(summary).size(13).color(muted_text()));
|
||
}
|
||
}
|
||
let user = message.user;
|
||
messages = messages.push(
|
||
container(body)
|
||
.padding(14)
|
||
.width(Length::Fill)
|
||
.style(move |theme| chat_message_style(theme, user)),
|
||
);
|
||
}
|
||
}
|
||
let composer = text_input("Ask DS4Server anything…", &self.composer)
|
||
.id(composer_id())
|
||
.on_input(Message::ComposerChanged)
|
||
.on_submit(Message::SubmitPrompt)
|
||
.padding(12)
|
||
.size(14);
|
||
let action = if self.generating {
|
||
action_button(text("Stop").size(12)).on_press(Message::StopGeneration)
|
||
} else if self.composer.trim().is_empty() {
|
||
action_button(icon(ICON_SEND, 18)).padding(8)
|
||
} else {
|
||
action_button(icon(ICON_SEND, 18))
|
||
.padding(8)
|
||
.on_press(Message::SubmitPrompt)
|
||
};
|
||
let context_fraction = if self.context_limit == 0 {
|
||
0.0
|
||
} else {
|
||
self.context_used.min(self.context_limit) as f32 / self.context_limit as f32
|
||
};
|
||
let conversation = column![
|
||
scrollable(messages)
|
||
.id(chat_scroll_id())
|
||
.height(Length::Fill),
|
||
container(
|
||
column![
|
||
composer,
|
||
progress_bar(0.0..=1.0, context_fraction).height(3),
|
||
row![
|
||
icon(ICON_PAPERCLIP, 19),
|
||
text(format!(
|
||
"{} / {} tokens ({:.0}%) • {}",
|
||
self.context_used,
|
||
self.context_limit,
|
||
context_fraction * 100.0,
|
||
self.tokens_per_second.map_or_else(
|
||
|| "— tok/s".to_owned(),
|
||
|speed| format!("{speed:.1} tok/s")
|
||
)
|
||
))
|
||
.size(11)
|
||
.color(muted_text()),
|
||
Space::with_width(Length::Fill),
|
||
icon(ICON_MODEL, 16),
|
||
text(
|
||
ModelChoice::from_id(&self.preferences.selected_model)
|
||
.unwrap_or_default()
|
||
.to_string(),
|
||
)
|
||
.size(12),
|
||
action,
|
||
]
|
||
.align_y(Alignment::Center),
|
||
]
|
||
.spacing(8),
|
||
)
|
||
.padding(16)
|
||
.width(Length::Fill)
|
||
.style(overview_style),
|
||
]
|
||
.height(Length::Fill)
|
||
.spacing(8);
|
||
container(conversation)
|
||
.max_width(860)
|
||
.center_x(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
} else {
|
||
container(
|
||
column![
|
||
text(if item.sessions.is_empty() {
|
||
"No sessions yet"
|
||
} else {
|
||
"Choose a session"
|
||
})
|
||
.size(24),
|
||
text("Use the new session icon next to the project, or pick one from the sidebar.")
|
||
.size(14),
|
||
]
|
||
.spacing(8)
|
||
.align_x(Alignment::Center),
|
||
)
|
||
.center_x(Length::Fill)
|
||
.center_y(Length::Fill)
|
||
.into()
|
||
};
|
||
|
||
container(column![header, body].spacing(24))
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.padding(24)
|
||
.into()
|
||
}
|
||
}
|