Files
Unlucky-Launcher/crates/app/src/conv/agent.rs
T
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00

104 lines
3.8 KiB
Rust

//! AI 页与覆盖层:会话消息、对话框(一次一条)、Toast。
use crate::{ChatRole, DialogKind, UiChatMessage, UiDialog, UiToast};
use unlucky_presenter::dialogs as dlg;
use unlucky_presenter::dto::ChatRole as DtoChatRole;
use unlucky_presenter::state::AppState;
use unlucky_presenter::toast as toast_mod;
/// AI 会话消息。
pub fn chat(state: &AppState) -> Vec<UiChatMessage> {
state
.ai
.messages
.iter()
.map(|message| UiChatMessage {
role: match message.role {
DtoChatRole::User => ChatRole::User,
DtoChatRole::Assistant => ChatRole::Assistant,
DtoChatRole::Tool => ChatRole::Tool,
DtoChatRole::System => ChatRole::System,
},
text: message.text.clone().into(),
tool_name: message
.tool
.as_ref()
.map(|tool| tool.name.clone())
.unwrap_or_default()
.into(),
tool_ok: message.tool.as_ref().is_none_or(|tool| tool.ok),
streaming: message.streaming,
})
.collect()
}
/// 当前对话框(一次一条)+ 是否显示 + 队列剩余条数。
pub fn dialog(state: &AppState) -> (UiDialog, bool, i32) {
let queued = i32::try_from(state.dialogs.pending_len()).unwrap_or(0);
match state.dialogs.current() {
None => (UiDialog::default(), false, queued),
Some(request) => {
let (kind, primary, secondary) = match &request.kind {
dlg::DialogKind::Confirm { primary, secondary } => {
(DialogKind::Confirm, primary.clone(), secondary.clone())
}
dlg::DialogKind::Input { primary, .. } => {
(DialogKind::Input, primary.clone(), String::new())
}
dlg::DialogKind::Progress { .. } => {
(DialogKind::Progress, "取消".to_string(), String::new())
}
dlg::DialogKind::Error { primary, .. } => {
(DialogKind::Error, primary.clone(), "忽略".to_string())
}
};
let progress = match &request.kind {
dlg::DialogKind::Progress { progress, .. } => *progress,
_ => 0.0,
};
let input_label = match &request.kind {
dlg::DialogKind::Input { label, .. } => label.clone(),
_ => String::new(),
};
let input_value = match &request.kind {
dlg::DialogKind::Input { value, .. } => value.clone(),
_ => String::new(),
};
(
UiDialog {
id: request.id.to_string().into(),
kind,
title: request.title.clone().into(),
body: request.body.clone().into(),
primary: primary.into(),
secondary: secondary.into(),
progress,
input_label: input_label.into(),
input_value: input_value.into(),
},
true,
queued,
)
}
}
}
/// Toast 列表。
pub fn toasts(state: &AppState) -> Vec<UiToast> {
state
.toasts
.items()
.iter()
.map(|toast| UiToast {
id: toast.id.to_string().into(),
kind: match toast.kind {
toast_mod::ToastKind::Info => crate::ToastKind::Info,
toast_mod::ToastKind::Success => crate::ToastKind::Success,
toast_mod::ToastKind::Warn => crate::ToastKind::Warn,
toast_mod::ToastKind::Error => crate::ToastKind::Error,
},
text: toast.text.clone().into(),
})
.collect()
}