///|
/// 设计智能:引擎不仅执行设计——它理解设计。
///
/// - infer_page_type:从节点结构推断页面类型(login/dashboard/list...)
/// - infer_missing_elements:基于页面类型推断缺失的常见元素
/// - suggest_improvements:基于设计原则给出改进建议
/// - design_quality_score:综合评分(层次/一致性/密度/可访问性)
///
/// 这是"AI-native"的核心差异化:引擎有设计常识,能主动告诉 Agent
/// "你做的这个页面缺了什么"和"怎么做会更好"。
// ---------------------------------------------------------------------------
// 页面类型推断
// ---------------------------------------------------------------------------
pub(all) enum PageType {
AuthPage // 登录/注册
DashboardPage // 仪表盘
ListPage // 列表页
DetailPage // 详情页
ProfilePage // 个人主页
SettingsPage // 设置页
FormPage // 表单页
NavigationPage // 导航/引导
EmptyStatePage // 空状态
UnknownPage // 未识别
}
///|
pub fn PageType::name(self : PageType) -> String {
match self {
AuthPage => "auth"
DashboardPage => "dashboard"
ListPage => "list"
DetailPage => "detail"
ProfilePage => "profile"
SettingsPage => "settings"
FormPage => "form"
NavigationPage => "navigation"
EmptyStatePage => "empty_state"
UnknownPage => "unknown"
}
}
///|
/// 从画板节点结构推断页面类型
/// 信号:输入框数量 + 按钮文字 + 卡片密度 + 布局模式
pub fn Project::infer_page_type(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) => {
let doc = ab.doc
let mut input_count = 0
let mut button_count = 0
let mut card_count = 0
let mut text_count = 0
let mut all_text = ""
for id in doc.nodes.keys() {
match doc.nodes.get(id) {
Some(n) => {
match n.component {
"text_input" => input_count = input_count + 1
"button" => button_count = button_count + 1
"card" => card_count = card_count + 1
"heading" | "body_text" => text_count = text_count + 1
_ => ()
}
all_text = all_text + " " + n.text + " " + n.name
}
None => ()
}
}
let lower = all_text.to_lower()
let has_avatar = lower.contains("avatar") || lower.contains("头像")
let has_chart = lower.contains("chart") ||
lower.contains("图表") ||
card_count >= 4
// 推断逻辑
let page_type : String = if input_count >= 2 && button_count >= 1 {
"auth"
} else if card_count >= 4 && has_chart {
"dashboard"
} else if card_count >= 3 && text_count <= 2 {
"list"
} else if has_avatar && text_count >= 2 {
"profile"
} else if all_text.contains("Settings") || all_text.contains("设置") {
"settings"
} else if input_count >= 1 && button_count >= 1 {
"form"
} else if text_count >= 3 && card_count == 0 {
if all_text.to_lower().contains("no ") ||
all_text.to_lower().contains("empty") {
"empty_state"
} else {
"navigation"
}
} else {
"unknown"
}
let _ = has_avatar
Ok(
"{\"page_type\":\"\{page_type}\",\"signals\":{\"inputs\":\{input_count},\"buttons\":\{button_count},\"cards\":\{card_count},\"texts\":\{text_count}}}",
)
}
}
}
// ---------------------------------------------------------------------------
// 缺失元素推断
// ---------------------------------------------------------------------------
///|
/// 基于页面类型推断常见缺失元素
/// Agent 得到"你的登录页还差一个忘记密码链接"这种建议
pub fn Project::infer_missing(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.infer_page_type(artboard~) {
Err(e) => Err(e)
Ok(type_json) => {
let page_type = extract_field(type_json, "page_type")
let doc = match self.artboards.get(artboard) {
Some(ab) => ab.doc
None => return Err("unknown_artboard")
}
// 收集所有文字
let mut all_text = ""
for id in doc.nodes.keys() {
match doc.nodes.get(id) {
Some(n) => all_text = all_text + " " + n.text + " " + n.name
None => ()
}
}
let lower = all_text.to_lower()
let missing : Array[String] = []
match page_type {
"auth" => {
if !lower.contains("forgot") && !lower.contains("忘记") {
missing.push(
"{\"type\":\"missing_link\",\"element\":\"forgot_password\",\"suggestion\":\"添加'忘记密码'链接,通常在密码输入框下方\"}",
)
}
if !lower.contains("sign up") &&
!lower.contains("create account") &&
!lower.contains("注册") {
missing.push(
"{\"type\":\"missing_link\",\"element\":\"signup_link\",\"suggestion\":\"添加'创建账号'链接,通常在登录按钮下方\"}",
)
}
if !lower.contains("remember") && !lower.contains("记住") {
missing.push(
"{\"type\":\"missing_checkbox\",\"element\":\"remember_me\",\"suggestion\":\"考虑添加'记住我'复选框\"}",
)
}
}
"dashboard" =>
if !lower.contains("refresh") && !lower.contains("刷新") {
missing.push(
"{\"type\":\"missing_action\",\"element\":\"refresh\",\"suggestion\":\"考虑添加刷新/更新数据的操作\"}",
)
}
"list" => {
if !lower.contains("search") && !lower.contains("搜索") {
missing.push(
"{\"type\":\"missing_input\",\"element\":\"search_bar\",\"suggestion\":\"列表页通常需要搜索栏\"}",
)
}
if !lower.contains("filter") && !lower.contains("筛选") {
missing.push(
"{\"type\":\"missing_action\",\"element\":\"filter\",\"suggestion\":\"考虑添加筛选/排序功能\"}",
)
}
}
"form" =>
missing.push(
"{\"type\":\"missing_validation\",\"element\":\"error_states\",\"suggestion\":\"表单页应为每个输入框添加错误提示状态\"}",
)
"profile" =>
if !lower.contains("edit") && !lower.contains("编辑") {
missing.push(
"{\"type\":\"missing_action\",\"element\":\"edit_profile\",\"suggestion\":\"个人主页通常需要'编辑资料'入口\"}",
)
}
"settings" =>
if !lower.contains("logout") && !lower.contains("退出") {
missing.push(
"{\"type\":\"missing_action\",\"element\":\"logout\",\"suggestion\":\"设置页通常需要退出登录按钮\"}",
)
}
_ => ()
}
// 通用检查
if !lower.contains("loading") && !lower.contains("加载") {
missing.push(
"{\"type\":\"missing_state\",\"element\":\"loading_state\",\"suggestion\":\"考虑添加加载状态(骨架屏或旋转器)\"}",
)
}
let j = jstr14(missing)
Ok(
"{\"page_type\":\"\{page_type}\",\"missing_count\":\{missing.length()},\"missing\":[\{j}]}",
)
}
}
}
// ---------------------------------------------------------------------------
// 设计改进建议
// ---------------------------------------------------------------------------
///|
pub(all) struct DesignSuggestion {
category : String // "hierarchy" | "spacing" | "alignment" | "contrast" | "consistency"
severity : String // "high" | "medium" | "low"
description : String
fix_command : String // Agent 可以直接执行的建议命令
}
///|
/// 基于设计原则给出改进建议
pub fn Project::suggest_improvements(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) => {
let suggestions : Array[String] = []
let layout = solve(ab.doc)
let tokens = self.tokens
// 1. 层次检查:是否有清晰的视觉层次(至少 2 种字号)
let font_sizes : Map[Double, Int] = Map::new()
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
match font_sizes.get(n.style.font_size) {
Some(c) => font_sizes.set(n.style.font_size, c + 1)
None => font_sizes.set(n.style.font_size, 1)
}
None => ()
}
}
if font_sizes.size() < 2 {
suggestions.push(
"{\"category\":\"hierarchy\",\"severity\":\"high\",\"description\":\"所有文字字号相同,缺少视觉层次\",\"fix_command\":\"为标题使用 heading h2,正文使用 body_text\"}",
)
}
// 2. 间距一致性:检查子元素间距是否统一
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
if n.children.length() > 2 {
match n.layout {
Some(l) => {
let is_std = l.gap == 4.0 ||
l.gap == 8.0 ||
l.gap == 12.0 ||
l.gap == 16.0 ||
l.gap == 24.0
if !is_std {
suggestions.push(
"{\"category\":\"consistency\",\"severity\":\"medium\",\"description\":\"间距 \{l.gap} 不是标准令牌值\",\"fix_command\":\"snap_to_grid(grid=8)\"}",
)
}
}
None => ()
}
}
None => ()
}
}
// 3. 颜色一致性:检查是否有非令牌的硬编码颜色
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
if n.style.fill != "none" &&
n.style.fill != "" &&
!is_token_color(tokens, n.style.fill) {
match find_nearest_token_color(tokens, n.style.fill) {
Some((token_name, _hex)) =>
suggestions.push(
"{\"category\":\"consistency\",\"severity\":\"low\",\"description\":\"硬编码颜色接近令牌\",\"fix_command\":\"替换为令牌 \{token_name}\"}",
)
None => ()
}
}
None => ()
}
}
// 4. 密度检查:元素是否过于密集或稀疏
let node_count = ab.doc.nodes.size() - 1
let area = ab.width * ab.height
let density = node_count.to_double() / (area / 10000.0) // 节点/万px²
if density > 8.0 {
suggestions.push(
"{\"category\":\"density\",\"severity\":\"medium\",\"description\":\"画板元素密度过高(\{density.to_string()} 节点/万px²),考虑分组或精简\",\"fix_command\":\"auto_arrange\"}",
)
} else if density < 0.5 && node_count > 3 {
suggestions.push(
"{\"category\":\"density\",\"severity\":\"low\",\"description\":\"画板元素较少,可能内容不够丰富\",\"fix_command\":\"infer_missing\"}",
)
}
let j = jstr14(suggestions)
Ok("{\"suggestions\":\{suggestions.length()},\"detail\":[\{j}]}")
}
}
}
// ---------------------------------------------------------------------------
// 设计质量综合评分
// ---------------------------------------------------------------------------
///|
pub fn Project::design_quality_score(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) => {
let doc = ab.doc
let layout = solve(doc)
// 维度 1:层次分(字号种类越多越好,上限 5 种)
let font_set : Map[Double, Bool] = Map::new()
for id in doc.nodes.keys() {
match doc.nodes.get(id) {
Some(n) => font_set.set(n.style.font_size, true)
None => ()
}
}
let hierarchy_score = if font_set.size() >= 3 {
25
} else if font_set.size() == 2 {
18
} else {
8
}
// 维度 2:一致性分(颜色是否使用令牌)
let mut token_colors = 0
let mut total_colors = 0
for id in doc.nodes.keys() {
match doc.nodes.get(id) {
Some(n) =>
if n.style.fill != "none" && n.style.fill != "" {
total_colors = total_colors + 1
if is_token_color(self.tokens, n.style.fill) {
token_colors = token_colors + 1
}
}
None => ()
}
}
let consistency_score = if total_colors == 0 {
15
} else {
token_colors * 25 / total_colors
}
// 维度 3:稳定性分(无违规 = 满分)
let violations = validate(doc).length()
let stability_score = if violations == 0 {
25
} else if violations <= 2 {
15
} else {
5
}
// 维度 4:丰富度分(节点数量,考虑层次)
let node_count = doc.nodes.size() - 1
let richness_score = if node_count >= 10 {
25
} else if node_count >= 5 {
18
} else if node_count >= 3 {
10
} else {
3
}
let total = hierarchy_score +
consistency_score +
stability_score +
richness_score
let grade = if total >= 85 {
"A"
} else if total >= 70 {
"B"
} else if total >= 50 {
"C"
} else {
"D"
}
Ok(
"{\"total\":\{total},\"grade\":\"\{grade}\",\"hierarchy\":\{hierarchy_score},\"consistency\":\{consistency_score},\"stability\":\{stability_score},\"richness\":\{richness_score},\"violations\":\{violations},\"nodes\":\{node_count}}",
)
}
}
}
// ---------------------------------------------------------------------------
// 辅助函数
// ---------------------------------------------------------------------------
///|
fn is_token_color(tokens : DesignTokens, hex : String) -> Bool {
for k in tokens.colors.keys() {
match tokens.colors.get(k) {
Some(v) => if v == hex { return true }
None => ()
}
}
false
}
///|
fn find_nearest_token_color(
tokens : DesignTokens,
hex : String,
) -> (String, String)? {
let target = parse_hex_rgb(hex)
let mut best : (String, String)? = None
let mut best_dist = 999999.0
for k in tokens.colors.keys() {
match tokens.colors.get(k) {
Some(v) => {
let c = parse_hex_rgb(v)
let dist = color_distance(target, c)
if dist < best_dist {
best_dist = dist
best = Some((k, v))
}
}
None => ()
}
}
// 只有距离足够近才建议替换
if best_dist < 50.0 {
best
} else {
None
}
}
///|
fn parse_hex_rgb(hex : String) -> (Double, Double, Double) {
let body = if hex.length() > 0 && hex.get_char(0) == Some('#') {
hex.substring(start=1)
} else {
hex
}
let r = hex_part(body, 0)
let g = hex_part(body, 2)
let b = hex_part(body, 4)
(r, g, b)
}
///|
fn hex_part(s : String, offset : Int) -> Double {
if s.length() < offset + 2 {
return 0.0
}
let hi = hex_digit_val2(s.get_char(offset))
let lo = hex_digit_val2(s.get_char(offset + 1))
(hi * 16 + lo).to_double()
}
///|
fn hex_digit_val2(c : Char?) -> Int {
match c {
Some(ch) => {
let code = ch.to_int()
if code >= 48 && code <= 57 {
code - 48
} else if code >= 97 && code <= 102 {
code - 87
} else if code >= 65 && code <= 70 {
code - 55
} else {
0
}
}
None => 0
}
}
///|
fn color_distance(
a : (Double, Double, Double),
b : (Double, Double, Double),
) -> Double {
let dr = a.0 - b.0
let dg = a.1 - b.1
let db = a.2 - b.2
Double::sqrt(dr * dr + dg * dg + db * db)
}
///|
fn extract_field(json : String, field : String) -> String {
let key = "\"\{field}\":\""
match json.find(key) {
Some(idx) => {
let start = idx + key.length()
let rest = json.substring(start~)
match rest.find("\"") {
Some(end) => rest.substring(start=0, end~)
None => ""
}
}
None => ""
}
}
///|
fn jstr14(parts : Array[String]) -> String {
let sb = StringBuilder::new()
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string(parts[i])
}
sb.to_string()
}