Split Rust code into domain modules

This commit is contained in:
Georg Bauer
2026-07-25 11:22:59 +02:00
parent 7843592400
commit 4292e0d3f9
22 changed files with 7650 additions and 7536 deletions

210
src/app/view/chat.rs Normal file
View File

@@ -0,0 +1,210 @@
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 selected_title = self
.selected_session(item)
.map(|session| session.title.as_str())
.unwrap_or(project.name.as_str());
let header = row![
icon(ICON_FOLDER, 19),
text(selected_title).size(18),
icon(ICON_MORE, 18),
Space::with_width(Length::Fill),
]
.spacing(10)
.align_y(Alignment::Center);
let body: Element<'_, Message> = if let Some(session) = self.selected_session(item) {
let mut messages = column![].spacing(12);
if self.conversation.is_empty() {
messages = messages.push(
column![
text(&session.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 { "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.markdown.is_empty() {
let content = if message.reasoning.is_some() {
message.content.trim_start()
} else {
&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));
}
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)
.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("Create a session above or select 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()
}
}

View File

@@ -0,0 +1,296 @@
use super::*;
use iced::widget::column;
impl App {
pub(super) fn model_manager(&self) -> Element<'_, Message> {
let busy = matches!(self.model_download, ModelDownload::Active(_));
let mut artifacts = column![];
for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() {
if index > 0 {
artifacts = artifacts.push(horizontal_rule(1));
}
artifacts = artifacts.push(model_artifact_row(artifact, busy));
}
let mut content = column![
text("Model Manager").size(26),
text("Download, verify, or remove locally stored model files.")
.size(14)
.color(muted_text()),
]
.spacing(8);
if !matches!(self.model_download, ModelDownload::Idle) {
content = content.push(
container(model_download_status(&self.model_download))
.padding(16)
.style(overview_style),
);
}
if let Some(error) = &self.error {
content = content.push(text(error).style(iced::widget::text::danger));
}
content = content.push(Space::with_height(8)).push(
container(scrollable(artifacts).height(Length::Fill))
.height(Length::Fill)
.style(overview_style),
);
let base: Element<'_, Message> = container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding(28)
.into();
let Some(artifact) = self.pending_model_delete else {
return base;
};
let confirmation = container(
column![
text("Delete model file?").size(22),
text(format!(
"Delete {artifact}, including any resumable partial download?"
))
.size(13),
row![
Space::with_width(Length::Fill),
action_button("Cancel").on_press(Message::CancelDeleteArtifact),
danger_button("Delete").on_press(Message::ConfirmDeleteArtifact),
]
.spacing(8),
]
.spacing(14),
)
.padding(22)
.width(460)
.style(overview_style);
stack![
base,
opaque(
container(confirmation)
.center_x(Length::Fill)
.center_y(Length::Fill)
.style(|_| container::Style::default()
.background(Color::from_rgba8(0, 0, 0, 0.68)))
)
]
.into()
}
}
fn model_artifact_row(artifact: &ManagedArtifact, busy: bool) -> Element<'static, Message> {
let status = match artifact.state {
ManagedArtifactState::Missing => "Not downloaded",
ManagedArtifactState::Partial => "Partial download",
ManagedArtifactState::NeedsVerification => "Downloaded; verification required",
ManagedArtifactState::Ready => "Ready and verified",
};
let download_label = match artifact.state {
ManagedArtifactState::Ready => "Downloaded",
_ if artifact.stored > 0 => "Resume",
_ => "Download",
};
let download = if artifact.state == ManagedArtifactState::Ready || busy {
action_button(download_label)
} else {
action_button(download_label).on_press(Message::DownloadArtifact(artifact.id))
};
let validate = if artifact.can_validate() && !busy {
action_button("Validate").on_press(Message::ValidateArtifact(artifact.id))
} else {
action_button("Validate")
};
let delete = if artifact.stored > 0 && !busy {
danger_button("Delete").on_press(Message::DeleteArtifact(artifact.id))
} else {
danger_button("Delete")
};
container(
row![
icon(ICON_MODEL, 28),
column![
text(artifact.id.to_string()).size(16),
text(status).size(13).color(muted_text()),
text(format!(
"{} on disk • {} expected",
format_bytes(artifact.stored),
format_bytes(artifact.expected),
))
.size(12)
.color(muted_text()),
]
.spacing(5),
Space::with_width(Length::Fill),
row![download, validate, delete]
.spacing(8)
.align_y(Alignment::Center),
]
.spacing(16)
.align_y(Alignment::Center),
)
.width(Length::Fill)
.padding([18, 20])
.into()
}
pub(super) fn download_status_bar(download: &ActiveDownload) -> Element<'_, Message> {
let progress = &download.progress;
let percent = progress.fraction() * 100.0;
let measurement = if progress.verification.is_some() {
format!(
"{} verified of {} ({percent:.1}%)",
format_bytes(progress.completed()),
format_bytes(progress.active_total()),
)
} else {
format!(
"{} of {} ({percent:.1}%)",
format_bytes(progress.completed()),
format_bytes(progress.active_total()),
)
};
let transfer = if download.bytes_per_second > 0.0 && progress.remaining() > 0 {
format!(
"{}/s • about {} remaining",
format_bytes(download.bytes_per_second as u64),
format_duration(progress.remaining() as f64 / download.bytes_per_second),
)
} else {
"Calculating time remaining…".to_owned()
};
let stop = if download.stopping {
danger_button("Stopping…")
} else {
danger_button("Stop").on_press(Message::StopModelDownload)
};
container(
row![
text(phase_text(progress.phase)).size(12),
progress_bar(0.0..=1.0, progress.fraction())
.width(180)
.height(7),
text(measurement).size(12),
text(transfer).size(12),
Space::with_width(Length::Fill),
stop,
]
.spacing(12)
.align_y(Alignment::Center),
)
.width(Length::Fill)
.padding([7, 14])
.style(sidebar_style)
.into()
}
fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
let (progress, heading, speed, failed) = match download {
ModelDownload::Idle => unreachable!("idle operations are not displayed"),
ModelDownload::Active(active) => (
&active.progress,
if active.stopping {
format!("Stopping {}", active.artifact)
} else {
phase_text(active.progress.phase)
},
active.bytes_per_second,
false,
),
ModelDownload::Complete(artifact, operation, progress) => (
progress,
match operation {
ModelOperation::Download => format!("{artifact} is downloaded and verified."),
ModelOperation::Validate => format!("{artifact} passed validation."),
},
0.0,
false,
),
ModelDownload::Failed(artifact, error, progress) => (
progress,
format!("{artifact} operation failed: {error}"),
0.0,
true,
),
};
let percent = progress.fraction() * 100.0;
let measurement = if progress.verification.is_some() {
format!(
"{} verified of {} ({percent:.1}%) • {} remaining",
format_bytes(progress.completed()),
format_bytes(progress.active_total()),
format_bytes(progress.remaining()),
)
} else {
format!(
"{} of {} ({percent:.1}%) • {} remaining",
format_bytes(progress.completed()),
format_bytes(progress.active_total()),
format_bytes(progress.remaining()),
)
};
let heading = if failed {
text(heading).size(12).style(iced::widget::text::danger)
} else {
text(heading).size(12)
};
let mut heading_row = row![heading, Space::with_width(Length::Fill)]
.align_y(Alignment::Center)
.spacing(8);
if let ModelDownload::Active(active) = download {
heading_row = heading_row.push(if active.stopping {
danger_button("Stopping…")
} else {
danger_button("Stop").on_press(Message::StopModelDownload)
});
}
let mut status = column![
heading_row,
progress_bar(0.0..=1.0, progress.fraction()).height(8),
text(measurement).size(12),
]
.spacing(6);
if speed > 0.0 && progress.remaining() > 0 {
status = status.push(
text(format!(
"{}/s • about {} remaining",
format_bytes(speed as u64),
format_duration(progress.remaining() as f64 / speed),
))
.size(12),
);
}
status.into()
}
fn phase_text(phase: DownloadPhase) -> String {
match phase {
DownloadPhase::Pending(artifact) => format!("Ready to download {artifact}."),
DownloadPhase::Downloading(artifact) => format!("Downloading {artifact}"),
DownloadPhase::Verifying(artifact) => format!("Verifying {artifact}"),
DownloadPhase::Complete => "Download complete.".to_owned(),
}
}
pub(super) fn format_bytes(bytes: u64) -> String {
const GB: f64 = 1_000_000_000.0;
const MB: f64 = 1_000_000.0;
if bytes >= 1_000_000_000 {
format!("{:.1} GB", bytes as f64 / GB)
} else {
format!("{:.1} MB", bytes as f64 / MB)
}
}
pub(super) fn format_duration(seconds: f64) -> String {
let seconds = seconds.max(0.0).round() as u64;
let hours = seconds / 3600;
let minutes = seconds % 3600 / 60;
if hours > 0 {
format!("{hours}h {minutes}m")
} else if minutes > 0 {
format!("{minutes}m {}s", seconds % 60)
} else {
format!("{seconds}s")
}
}

440
src/app/view/preferences.rs Normal file
View File

@@ -0,0 +1,440 @@
use super::*;
use iced::widget::column;
impl App {
pub(super) fn preferences_panel(&self) -> Element<'_, Message> {
let dspark_toggle: Option<fn(bool) -> Message> = self
.preference_draft
.model
.supports_dspark()
.then_some(Message::PreferenceDsparkChanged);
let dspark = checkbox(
"Enable DSpark for this model",
self.preference_draft.dspark_enabled,
)
.on_toggle_maybe(dspark_toggle);
let glm_mtp_toggle: Option<fn(bool) -> Message> = (self.preference_draft.model
== ModelChoice::Glm52)
.then_some(Message::PreferenceGlmMtpChanged);
let glm_mtp_timing_toggle: Option<fn(bool) -> Message> = (self.preference_draft.model
== ModelChoice::Glm52)
.then_some(Message::PreferenceGlmMtpTimingChanged);
let dspark_strict_toggle: Option<fn(bool) -> Message> = self
.preference_draft
.model
.supports_dspark()
.then_some(Message::PreferenceDsparkStrictChanged);
let effective = self
.preference_draft
.generation()
.and_then(|generation| {
self.preference_draft.runtime().and_then(|runtime| {
crate::settings::effective_settings(
self.preference_draft.model,
&generation,
&runtime,
&models_path(),
)
})
})
.ok();
let engine = effective.as_ref().map(|settings| &settings.engine);
let turn = effective.as_ref().map(|settings| &settings.turn);
let mut power = text_input("100", &self.preference_draft.power_percent);
let mut prefill = text_input("Automatic", &self.preference_draft.prefill_chunk);
let mut ssd_full_layers = text_input("Automatic", &self.preference_draft.ssd_full_layers);
let mut steering_file = text_input(
"Direction-vector file path",
&self.preference_draft.directional_steering_file,
);
let mut steering_ffn =
text_input("Automatic", &self.preference_draft.directional_steering_ffn);
let mut steering_attn = text_input("0", &self.preference_draft.directional_steering_attn);
let mut dspark_confidence = text_input(
"0.9 (DS4 default)",
&self.preference_draft.dspark_confidence_threshold,
);
if self.preference_draft.model != ModelChoice::Glm52 {
power = power.on_input(Message::PreferencePowerChanged);
prefill = prefill.on_input(Message::PreferencePrefillChunkChanged);
steering_file = steering_file.on_input(Message::PreferenceSteeringFileChanged);
steering_ffn = steering_ffn.on_input(Message::PreferenceSteeringFfnChanged);
steering_attn = steering_attn.on_input(Message::PreferenceSteeringAttnChanged);
} else {
ssd_full_layers = ssd_full_layers.on_input(Message::PreferenceSsdFullLayersChanged);
}
if self.preference_draft.model.supports_dspark() {
dspark_confidence =
dspark_confidence.on_input(Message::PreferenceDsparkConfidenceChanged);
}
let model_group = preference_group(
"MODEL & LIFECYCLE",
column![
pick_list(
&MODEL_CHOICES[..],
Some(self.preference_draft.model),
Message::PreferenceModelChanged,
)
.width(Length::Fill),
text(format!(
"Main: {}{}",
engine.map_or_else(
|| "Invalid settings".to_owned(),
|engine| engine.artifacts.model.display().to_string(),
),
engine
.and_then(|engine| engine.artifacts.mtp.as_ref())
.map_or_else(String::new, |path| format!(
" • support: {}",
path.display()
)),
))
.size(12),
row![
text_input("10", &self.preference_draft.idle_timeout_minutes)
.on_input(Message::PreferenceTimeoutChanged)
.width(90)
.padding(9),
text("minutes before unloading the model").size(13),
]
.spacing(10)
.align_y(Alignment::Center),
text("Enter a whole number from 1 to 1440.").size(12),
]
.spacing(10),
);
let endpoint_group = preference_group(
"LOCAL ENDPOINT",
column![
preference_input_row(
"Port",
text_input("4000", &self.preference_draft.endpoint_port)
.on_input(Message::PreferenceEndpointPortChanged),
),
text("Listens on 127.0.0.1. Saving a changed port restarts the local endpoint.")
.size(12),
]
.spacing(10),
);
let generation_group = preference_group(
"GENERATION",
column![
preference_input_row(
"Context tokens",
text_input("32768", &self.preference_draft.context_tokens)
.on_input(Message::PreferenceContextChanged),
),
preference_input_row(
"Maximum generated tokens",
text_input("50000", &self.preference_draft.max_generated_tokens)
.on_input(Message::PreferenceMaxTokensChanged),
),
text("System prompt").size(13),
text_input(
"You are a helpful assistant",
&self.preference_draft.system_prompt,
)
.on_input(Message::PreferenceSystemPromptChanged)
.padding(9),
Space::with_height(4),
text("SAMPLING & REASONING").size(11).color(muted_text()),
preference_input_row(
"Temperature",
text_input("DS4 default", &self.preference_draft.temperature)
.on_input(Message::PreferenceTemperatureChanged),
),
preference_input_row(
"Top-p",
text_input("DS4 default", &self.preference_draft.top_p)
.on_input(Message::PreferenceTopPChanged),
),
preference_input_row(
"Min-p",
text_input("DS4 default", &self.preference_draft.min_p)
.on_input(Message::PreferenceMinPChanged),
),
preference_input_row(
"Seed",
text_input("Random", &self.preference_draft.seed)
.on_input(Message::PreferenceSeedChanged),
),
row![
text("Reasoning").size(13).width(Length::Fill),
pick_list(
&REASONING_MODES[..],
Some(self.preference_draft.reasoning_mode),
Message::PreferenceReasoningChanged,
)
.width(240),
]
.spacing(12)
.align_y(Alignment::Center),
text("Blank sampling values retain DS4's model-family defaults. Think Max needs at least 393216 context tokens.")
.size(12),
text(turn.map_or_else(
|| "Effective settings will appear after valid values are entered.".to_owned(),
|settings| format!(
"Effective: {} context • {} max • temp {} • top-p {} • min-p {} • seed {}{} • system prompt {}",
settings.context_tokens,
settings.max_generated_tokens,
settings.temperature,
settings.top_p,
settings.min_p,
settings.seed.map_or_else(|| "random".to_owned(), |seed| seed.to_string()),
settings.reasoning_mode,
if settings.system_prompt.is_empty() { "off" } else { "on" },
),
))
.size(12),
]
.spacing(10),
);
let execution_group = preference_group(
"EXECUTION",
column![
preference_input_row(
"CPU helper threads",
text_input("Automatic", &self.preference_draft.cpu_threads)
.on_input(Message::PreferenceCpuThreadsChanged),
),
preference_input_row("GPU power percent", power),
preference_input_row("Prefill chunk", prefill),
checkbox("Prefer exact quality kernels", self.preference_draft.quality)
.on_toggle(Message::PreferenceQualityChanged),
checkbox("Warm mapped weights at load time", self.preference_draft.warm_weights)
.on_toggle(Message::PreferenceWarmWeightsChanged),
text(if self.preference_draft.model == ModelChoice::Glm52 {
"GLM 5.2 uses full GPU power and selects prefill chunks automatically."
} else {
"Blank numeric values preserve DS4's automatic engine behavior."
})
.size(12),
text(engine.as_ref().map_or_else(
|| "Effective execution settings will appear after valid values are entered."
.to_owned(),
|engine| {
let settings = engine.execution;
format!(
"Metal engine: threads {} • power {}% • prefill {} • quality {} • warm weights {}",
if settings.cpu_threads == 0 { "auto".to_owned() } else { settings.cpu_threads.to_string() },
if settings.power_percent == 0 { 100 } else { settings.power_percent },
if settings.prefill_chunk == 0 { "auto".to_owned() } else { settings.prefill_chunk.to_string() },
if settings.quality { "on" } else { "off" },
if settings.warm_weights { "on" } else { "off" },
)
},
))
.size(12),
]
.spacing(10),
);
let acceleration_group = preference_group(
"ACCELERATION & MEMORY",
column![
text("SPECULATIVE DECODING").size(11).color(muted_text()),
preference_input_row(
"MTP draft tokens",
text_input("1", &self.preference_draft.mtp_draft_tokens)
.on_input(Message::PreferenceMtpDraftChanged),
),
preference_input_row(
"MTP verifier margin",
text_input("3", &self.preference_draft.mtp_margin)
.on_input(Message::PreferenceMtpMarginChanged),
),
checkbox("Enable integrated GLM MTP", self.preference_draft.glm_mtp)
.on_toggle_maybe(glm_mtp_toggle),
checkbox(
"Log GLM MTP timing counters",
self.preference_draft.glm_mtp_timing,
)
.on_toggle_maybe(glm_mtp_timing_toggle),
dspark,
preference_input_row("DSpark confidence threshold", dspark_confidence),
checkbox(
"DSpark target-only decode",
self.preference_draft.dspark_strict,
)
.on_toggle_maybe(dspark_strict_toggle),
text(if self.preference_draft.model.supports_dspark() {
"DSpark uses the managed support artifact; entering a threshold or enabling strict mode also enables DSpark."
} else if self.preference_draft.model == ModelChoice::Glm52 {
"GLM MTP is integrated; DSpark is unavailable for this model."
} else {
"No managed MTP support artifact is available for this model."
})
.size(12),
text(engine.as_ref().map_or_else(
|| "Effective speculative settings will appear after valid values are entered."
.to_owned(),
|engine| {
let settings = engine.speculative;
format!(
"Engine: MTP draft {} • margin {} • GLM MTP {} • timing {} • DSpark {} • confidence {}{} • target-only {}",
settings.mtp_draft_tokens,
settings.mtp_margin,
if settings.glm_mtp { "on" } else { "off" },
if settings.glm_mtp_timing { "on" } else { "off" },
if settings.dspark { "on" } else { "off" },
settings.dspark_confidence_threshold,
if settings.dspark_confidence_threshold_set { " explicit" } else { " default" },
if settings.dspark_strict { "on" } else { "off" },
)
},
))
.size(12),
Space::with_height(6),
text("SSD STREAMING").size(11).color(muted_text()),
checkbox("Enable SSD-backed model streaming", self.preference_draft.ssd_streaming)
.on_toggle(Message::PreferenceSsdChanged),
checkbox("Skip automatic expert preload", self.preference_draft.ssd_streaming_cold)
.on_toggle(Message::PreferenceSsdColdChanged),
preference_input_row(
"Expert cache count or GiB",
text_input("Automatic, 128, or 64GB", &self.preference_draft.ssd_cache)
.on_input(Message::PreferenceSsdCacheChanged),
),
preference_input_row("Fully resident GLM layers", ssd_full_layers),
preference_input_row(
"Explicit expert preload count",
text_input("Automatic", &self.preference_draft.ssd_preload_experts)
.on_input(Message::PreferenceSsdPreloadChanged),
),
text("A blank full-layer value is automatic; an explicit 0 disables fully resident GLM layers. SSD streaming and DSpark are mutually exclusive.")
.size(12),
text(engine.as_ref().map_or_else(
|| "Effective SSD settings will appear after valid values are entered."
.to_owned(),
|engine| {
let settings = engine.ssd;
let cache = if settings.cache_bytes > 0 {
format!("{} GiB", settings.cache_bytes / GIB)
} else if settings.cache_experts > 0 {
format!("{} experts", settings.cache_experts)
} else {
"auto".to_owned()
};
format!(
"Engine: streaming {} • cold {} • cache {} • full layers {}{} • preload {}",
if settings.enabled { "on" } else { "off" },
if settings.cold { "on" } else { "off" },
cache,
settings.full_layers,
if settings.full_layers_set { " explicit" } else { " auto" },
if settings.preload_experts == 0 { "auto".to_owned() } else { settings.preload_experts.to_string() },
)
},
))
.size(12),
]
.spacing(10),
);
let steering_group = preference_group(
"STEERING & DIAGNOSTICS",
column![
text("DIRECTIONAL STEERING").size(11).color(muted_text()),
text("Direction-vector file").size(13),
steering_file.padding(9),
preference_input_row("FFN scale", steering_ffn),
preference_input_row("Attention scale", steering_attn),
text(if self.preference_draft.model == ModelChoice::Glm52 {
"Directional steering is not supported for GLM 5.2."
} else {
"With a file and no explicit scale, DS4 defaults the FFN scale to 1. Scales accept -100 through 100."
})
.size(12),
text(engine.as_ref().map_or_else(
|| "Effective steering settings will appear after valid values are entered."
.to_owned(),
|engine| format!(
"Engine: file {} • FFN scale {} • attention scale {}",
if engine.steering.file.is_some() { "set" } else { "off" },
engine.steering.ffn_scale,
engine.steering.attention_scale,
),
))
.size(12),
Space::with_height(6),
text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()),
preference_input_row(
"Simulated used memory (GiB)",
text_input("Disabled", &self.preference_draft.simulated_used_memory_gib)
.on_input(Message::PreferenceSimulatedMemoryChanged),
),
text("Routed expert profile output").size(13),
text_input("Output file path", &self.preference_draft.expert_profile_path)
.on_input(Message::PreferenceExpertProfileChanged)
.padding(9),
text(engine.as_ref().map_or_else(
|| "Effective diagnostic settings will appear after valid values are entered."
.to_owned(),
|engine| format!(
"{} load: simulated memory {} • expert profile {}",
engine.model,
if engine.diagnostics.simulated_used_memory_bytes == 0 {
"off".to_owned()
} else {
format!("{} GiB", engine.diagnostics.simulated_used_memory_bytes / GIB)
},
if engine.diagnostics.expert_profile_path.is_some() { "set" } else { "off" },
),
))
.size(12),
]
.spacing(10),
);
let mut fields = column![
model_group,
endpoint_group,
generation_group,
execution_group,
acceleration_group,
steering_group,
]
.spacing(12);
if let Some(error) = &self.preference_error {
fields = fields.push(text(error).style(iced::widget::text::danger));
}
let header = row![
icon(ICON_SETTINGS, 22),
text("Preferences").size(24),
Space::with_width(Length::Fill),
text("⌘,").size(12),
]
.spacing(10)
.align_y(Alignment::Center);
let footer = row![
action_button("Reset DS4 defaults").on_press(Message::ResetPreferences),
Space::with_width(Length::Fill),
action_button("Cancel").on_press(Message::DismissPanel),
action_button("Save").on_press(Message::SavePreferences),
]
.spacing(8);
let panel = container(
column![
header,
scrollable(container(fields).padding(iced::Padding::ZERO.right(18)))
.height(Length::Fill),
footer
]
.spacing(16),
)
.padding(24)
.width(700)
.height(Length::Fill)
.max_height(660)
.style(overview_style);
opaque(
container(panel)
.padding(24)
.center_x(Length::Fill)
.center_y(Length::Fill)
.style(|_| {
container::Style::default().background(Color::from_rgba8(0, 0, 0, 0.68))
}),
)
}
}

371
src/app/view/stats.rs Normal file
View File

@@ -0,0 +1,371 @@
use super::*;
use iced::widget::column;
impl App {
pub(super) fn stats_dashboard(&self) -> Element<'_, Message> {
let stats = &self.metrics_snapshot;
let context_fraction = if stats.context_limit == 0 {
0.0
} else {
stats.context_used.min(stats.context_limit) as f32 / stats.context_limit as f32
};
let cache_fraction = if stats.last_prompt_tokens == 0 {
0.0
} else {
stats.last_cached_tokens as f32 / stats.last_prompt_tokens as f32
};
let cache_hit_fraction = if stats.kv_lookups == 0 {
0.0
} else {
stats.kv_hits as f32 / stats.kv_lookups as f32
};
let endpoint = if stats.server_listening {
format!("Listening · 127.0.0.1:{}", stats.server_port)
} else {
"Stopped".to_owned()
};
let phase_color = match stats.phase {
crate::metrics::RuntimePhase::Generating => Color::from_rgb8(84, 170, 255),
crate::metrics::RuntimePhase::Prefilling | crate::metrics::RuntimePhase::Loading => {
Color::from_rgb8(240, 180, 70)
}
crate::metrics::RuntimePhase::Ready => Color::from_rgb8(72, 176, 112),
crate::metrics::RuntimePhase::Failed => Color::from_rgb8(220, 80, 86),
crate::metrics::RuntimePhase::Unloaded => muted_text(),
};
let heading = container(
row![
column![
text("Runtime observability").size(24),
text(format!(
"{} · {} · uptime {}",
stats.model,
stats.source.label(),
format_duration(stats.uptime_seconds as f64)
))
.size(12)
.color(muted_text()),
]
.spacing(5),
Space::with_width(Length::Fill),
container(text(stats.phase.label()).size(12).color(phase_color))
.padding([7, 11])
.style(move |_| status_badge_style(phase_color)),
]
.align_y(Alignment::Center),
)
.padding(16)
.style(overview_style);
let headline = column![
row![
metric_card(
"DECODE",
format!("{:.1} tok/s", stats.decode_tokens_per_second),
format!(
"{} completion tokens total",
format_count(stats.completion_tokens)
),
),
metric_card(
"PREFILL",
format!("{:.1} tok/s", stats.prefill_tokens_per_second),
format!("{} prompt tokens total", format_count(stats.prompt_tokens)),
),
]
.spacing(10),
row![
metric_card(
"CONTEXT",
format!(
"{} / {}",
format_count(u64::from(stats.context_used)),
format_count(u64::from(stats.context_limit))
),
format!("{:.0}% occupied", context_fraction * 100.0),
),
metric_card(
"WORK",
format!(
"{} active · {} queued",
stats.http_active, stats.queue_depth
),
format!("{} runtime requests", format_count(stats.runtime_requests)),
),
]
.spacing(10),
]
.spacing(10);
let throughput = stats_panel(
"MODEL ACTIVITY · LAST 24 SECONDS",
column![
mini_chart(
&self.metrics_history,
|point| point.decode_tokens_per_second,
Color::from_rgb8(84, 170, 255),
),
row![
text("Decode")
.size(12)
.color(Color::from_rgb8(84, 170, 255)),
Space::with_width(Length::Fill),
text(format!("{:.1} tok/s", stats.decode_tokens_per_second))
.size(12)
.color(muted_text()),
],
mini_chart(
&self.metrics_history,
|point| point.prefill_tokens_per_second,
Color::from_rgb8(157, 119, 255),
),
row![
text("Prefill")
.size(12)
.color(Color::from_rgb8(157, 119, 255)),
Space::with_width(Length::Fill),
text(format!("{:.1} tok/s", stats.prefill_tokens_per_second))
.size(12)
.color(muted_text()),
],
]
.spacing(7)
.into(),
);
let requests = stats_panel(
"SERVER REQUEST RATE · LAST 24 SECONDS",
column![
mini_chart(
&self.metrics_history,
|point| point.http_requests_per_second,
Color::from_rgb8(72, 176, 112),
),
row![
text(format!("{} requests", format_count(stats.http_requests))).size(12),
Space::with_width(Length::Fill),
text(format!(
"{} errors · {} streaming",
stats.http_errors, stats.http_streaming_requests
))
.size(12)
.color(muted_text()),
],
]
.spacing(7)
.into(),
);
let latest = self.metrics_history.back().copied().unwrap_or_default();
let kv_io = stats_panel(
"KV CHECKPOINT I/O · LAST 24 SECONDS",
column![
mini_chart(
&self.metrics_history,
|point| point.kv_read_bytes_per_second,
Color::from_rgb8(67, 194, 203),
),
row![
text(if stats.kv_read_active {
"Disk read · active"
} else {
"Disk read"
})
.size(12)
.color(Color::from_rgb8(67, 194, 203)),
Space::with_width(Length::Fill),
text(format_rate(latest.kv_read_bytes_per_second))
.size(12)
.color(muted_text()),
],
mini_chart(
&self.metrics_history,
|point| point.kv_write_bytes_per_second,
Color::from_rgb8(240, 180, 70),
),
row![
text(if stats.kv_write_active {
"Disk write · active"
} else {
"Disk write"
})
.size(12)
.color(Color::from_rgb8(240, 180, 70)),
Space::with_width(Length::Fill),
text(format_rate(latest.kv_write_bytes_per_second))
.size(12)
.color(muted_text()),
],
]
.spacing(7)
.into(),
);
let model = stats_panel(
"MODEL CORE",
column![
metric_row("State", stats.phase.label()),
metric_row("Loaded model", stats.model),
metric_row("Mapped weights", format_bytes(stats.model_bytes)),
metric_row("Tensors", format_count(stats.tensor_count)),
metric_row("Vocabulary", format_count(stats.vocabulary_size)),
metric_row("Last load", format_milliseconds(stats.model_load_ms)),
metric_row(
"Lifecycle",
format!(
"{} loads · {} unloads",
stats.model_loads, stats.model_unloads
),
),
]
.spacing(9)
.into(),
);
let runtime = stats_panel(
"GENERATION",
column![
metric_row("Last runtime", format_milliseconds(stats.last_runtime_ms)),
metric_row(
"Average runtime",
format_milliseconds(stats.average_runtime_ms)
),
metric_row("Last prompt", format_count(stats.last_prompt_tokens)),
metric_row("Last reused", format_count(stats.last_cached_tokens)),
metric_row(
"Last completion",
format_count(stats.last_completion_tokens)
),
metric_row("Cached tokens total", format_count(stats.cached_tokens)),
metric_row(
"Cache reuse",
format!("{:.0}% of last prompt", cache_fraction * 100.0),
),
metric_row(
"Results",
format!(
"{} completed · {} failed",
stats.completed_requests, stats.failed_requests
),
),
]
.spacing(9)
.into(),
);
let cache = stats_panel(
"KV CACHE",
column![
metric_row(
"Total",
format!(
"{} · {} files",
format_bytes(stats.kv_bytes),
stats.kv_files
)
),
metric_row(
"Local sessions",
format!(
"{} · {} files",
format_bytes(stats.local_kv_bytes),
stats.local_kv_files
),
),
metric_row(
"HTTP transient",
format!(
"{} · {} files",
format_bytes(stats.http_kv_bytes),
stats.http_kv_files
),
),
metric_row("Checkpoint writes", format_count(stats.checkpoint_writes)),
metric_row(
"Exact hits",
format!(
"{} · {} memory / {} disk",
stats.kv_hits, stats.kv_memory_hits, stats.kv_disk_hits
),
),
metric_row(
"Misses",
format!("{} · {} invalid", stats.kv_misses, stats.kv_invalid),
),
metric_row("Lookups", format_count(stats.kv_lookups)),
metric_row(
"Exact hit rate",
format!("{:.1}%", cache_hit_fraction * 100.0)
),
metric_row("Prefix hits", format_count(stats.kv_prefix_hits)),
metric_row(
"Reads",
format!(
"{} · {} · {} errors · last {}",
stats.kv_read_operations,
format_bytes(stats.kv_read_bytes),
stats.kv_read_errors,
format_milliseconds(stats.last_kv_read_ms),
),
),
metric_row(
"Writes",
format!(
"{} · {} · {} errors · last {}",
stats.kv_write_operations,
format_bytes(stats.kv_write_bytes),
stats.kv_write_errors,
format_milliseconds(stats.last_kv_write_ms),
),
),
progress_bar(0.0..=1.0, cache_fraction.min(1.0)).height(4),
]
.spacing(9)
.into(),
);
let server = stats_panel(
"LOCAL SERVER",
column![
metric_row("Endpoint", endpoint),
metric_row("Active", format_count(u64::from(stats.http_active))),
metric_row("Completed", format_count(stats.http_completed)),
metric_row("Chat completions", format_count(stats.http_chat_requests)),
metric_row("Model queries", format_count(stats.http_model_requests)),
metric_row(
"Runtime sources",
format!(
"{} local · {} HTTP",
stats.local_requests, stats.endpoint_generations
),
),
metric_row("Received", format_bytes(stats.http_bytes_received)),
metric_row("Last latency", format_milliseconds(stats.last_http_ms)),
metric_row(
"Average latency",
format_milliseconds(stats.average_http_ms)
),
]
.spacing(9)
.into(),
);
scrollable(
container(
column![
heading,
headline,
throughput,
kv_io,
requests,
row![model, runtime].spacing(10),
row![cache, server].spacing(10),
text("Counters are published by the runtime with relaxed atomics and sampled by the UI every 200 ms.")
.size(11)
.color(muted_text()),
]
.spacing(12),
)
.padding(24)
.max_width(960)
.center_x(Length::Fill),
)
.height(Length::Fill)
.into()
}
}