Files
Unlucky-Launcher/crates/app-iced/src/pages/agent/conversation/banners.rs
T
AskaEth b5e96c61ac fix(app-iced): M119 出图抓到的两处:右栏文案没跟 lang 走、确认条文字被压成竖缝
两张验收图(`DevDocs/artifacts/m119/`)暴露的问题,都是测试抓不住、只有图能看出来的那类:

1. **同屏两语**:`pages/agent/mod.rs` 里还留着 M104 的 `const LANG: Lang = Lang::En`,所以右栏的
   标题/按钮恒英文,而 M119 新加的失败横幅走 `App::lang()`(这台机器是 zh-CN)⇒ 一屏里
   「Assistant / Collapse」和「连不上助手后端」并排。现在删掉那个常量,`docked`/`page` 收一个
   `Catalog` 形参,由 `App::lang().catalog()` 给——与设置页/账户页同一条口径(M106 的真值源)。

2. **确认条没有字**:`chip + 文字 + 两个按钮` 挤在同一条 `Row` 里,而右栗只有 280~360:320 宽下
   文字列被压成一条竖缝,标题整段看不见(图 05 第一版就是这个样子)。改成文字行与动作**分行**,
   动作右对齐——窄栏里才是可读的 M3 告警形态。

顺带:`Script::WriteOp` 补一句增量,免得那条助手气泡是空壳(出图更接近真后端)。

`cargo test -p unlucky-app-iced --lib`:599 passed / 0 failed;14 张图重出后逐张看过。
2026-11-05 08:11:14 +08:00

164 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 会话里的两条**横幅**(M119 从 `conversation.rs` 拆出来守 400 行硬线):
//! 失败态(具名错误)与写操作确认。
//!
//! ## 为什么必须存在
//!
//! 任务书第 4 条:「presenter 返回错误时 UI 必须显示**具名**错误而不是静默无语」。M104 的壳没有
//! 任何错误位——`Submit` 只在本地数组追加一句「没发给模型」的提示。现在两条来源都有了落点:
//!
//! | 来源 | 触发 | 画什么 |
//! |---|---|---|
//! | `AgentHost::last_failure` | `agent_submit` 返回 `Err`(端点不可达 / 路由未实现) | 标题 = [`Key::AgentErrorTitle`],正文 = 原因码对应的 catalog 译文 |
//! | `AgentRail::error` | 后端发 `AgentEvent::Failed`(这一轮回复中止) | 标题同上,正文 = presenter 已脱敏的那句原因 |
//!
//! 两者都不把后端诊断原文直接上屏:诊断串只进日志(M114 复审 r1 立的口径)。
//!
//! ## 确认条
//!
//! `rail.confirm` 非空 ⇒ 一轮写操作在等用户裁决,而 `busy` 会一直锁着输入(presenter 的
//! `sync_busy` 语义:`busy = streaming || confirm.is_some()`)。所以这段 UI 不是装饰:
//! **不按就不动**。两个按钮都带文字标签(M104 的「不得只有图标」同一条规矩)。
use iced::widget::{column, container, row, Row};
use iced::{Alignment, Background, Element, Length};
use unlucky_design::color::Role;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::icons::IconKind;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::{Catalog, Key};
use unlucky_presenter::dto::ConfirmRequest;
use crate::app::components::buttons::{self, Button};
use crate::theme::Ui;
use super::super::{Failure, Message};
use super::text_role_owned;
/// 失败横幅:`ErrorContainer` 图标块 + 标题/正文(M3 内联 alert 的形状,与账户页横幅同源)。
pub(super) fn failure<'a>(
ui: &'a Ui,
failure: Failure<'a>,
catalog: Catalog,
) -> Element<'a, Message> {
banner(
ui,
catalog.text(failure.title).to_string(),
failure.body.to_string(),
Vec::new(),
)
}
/// 写操作确认条:一行请求摘要 + Allow / Deny。
pub(super) fn confirm<'a>(
ui: &'a Ui,
request: &'a ConfirmRequest,
catalog: Catalog,
) -> Element<'a, Message> {
// 摘要来自 presenter(已过 `scrub_text`):它是「要改什么」的唯一描述,必须显示,
// 否则用户是在盲签。句框走 catalog,摘要本身是正文。
let title = catalog.render(
Key::AgentConfirmRequest,
&[("summary", request.summary.as_str())],
);
let actions: Vec<Element<'a, Message>> = vec![
buttons::outlined(
ui,
Button::new(catalog.text(Key::AgentConfirmDeny), Message::Confirm(false))
.leading(IconKind::Close),
),
buttons::filled(
ui,
Button::new(catalog.text(Key::AgentConfirmAllow), Message::Confirm(true))
.leading(IconKind::Check),
),
];
banner(ui, title, String::new(), actions)
}
/// 横幅本体:图标块 + 标题/正文 + 右侧动作。
///
/// 形状与 `pages::account::widgets::alert` 同源(面板底走卡片层 + 1dp 描边,
/// `ErrorContainer` / `OnErrorContainer` 只作左侧图标块的强调,不是通栏红条)。
fn banner<'a>(
ui: &'a Ui,
title: String,
body: String,
actions: Vec<Element<'a, Message>>,
) -> Element<'a, Message> {
let side = ui.space(Space::X8);
let glyph_px = ui.space(Space::X6);
let chip = container(crate::icons::view_at(
IconKind::Error,
ui.color(Role::Error),
glyph_px,
))
.width(Length::Fixed(side))
.height(Length::Fixed(side))
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.color(Role::ErrorContainer))),
border: iced::Border {
radius: ui.radius(ShapeScale::Full, side),
..iced::Border::default()
},
..container::Style::default()
});
let mut lines = column![text_role_owned(
ui,
TypeRole::TitleSmall,
true,
title,
ui.color(Role::OnSurface),
Length::Fill,
)]
.spacing(ui.space(Space::X1));
if !body.is_empty() {
lines = lines.push(text_role_owned(
ui,
TypeRole::BodySmall,
false,
body,
ui.color(Role::OnSurfaceVariant),
Length::Fill,
));
}
// 文字行与动作**分行**:右栗只有 280~360,chip + 两个按钮已占掉大半宽,
// 把文字挤在同一条 Row 里会被压成一条竖缝(M119 实测:320 宽下标题整段看不见)。
let head = Row::new()
.spacing(ui.space(Space::X3))
.align_y(Alignment::Center)
.width(Length::Fill)
.push(chip)
.push(container(lines).width(Length::Fill));
let mut content = column![Element::from(head)].spacing(ui.space(Space::X2));
if !actions.is_empty() {
content = content.push(
container(
row(actions)
.spacing(ui.space(Space::X2))
.align_y(Alignment::Center),
)
.width(Length::Fill)
.align_x(Alignment::End),
);
}
container(content)
.width(Length::Fill)
.padding(ui.metrics().card_padding)
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: ui.outlined(ShapeScale::Medium),
..container::Style::default()
})
.into()
}