import SwiftUI /// 模型推理自检:加载 LLM 跑一段固定 prompt,流式显示输出 + tok/s。 /// 模型就绪后从「我的 · 模型管理」进入,用于现场快速验证本地推理是否正常。 struct ModelSelfTestView: View { @State private var output = "" @State private var phase: Phase = .idle @State private var rate: Double = 0 private enum Phase: Equatable { case idle, loading, running, done, failed(String) var label: String { switch self { case .idle: return String(appLoc: "未开始") case .loading: return String(appLoc: "加载模型…") case .running: return String(appLoc: "推理中…") case .done: return String(appLoc: "完成 ✓") case .failed(let m): return String(appLoc: "失败:\(m)") } } } private let prompt = "用中文一句话介绍肝功能里 ALT 这个指标。" private var isBusy: Bool { phase == .loading || phase == .running } private var statusColor: Color { switch phase { case .failed: return Tj.Palette.brick case .done: return Tj.Palette.leaf default: return Tj.Palette.text2 } } var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 6) { Text("测试 PROMPT") .font(.system(size: 11, weight: .semibold)) .tracking(0.5) .foregroundStyle(Tj.Palette.text3) Text(prompt) .font(.system(size: 14)) .foregroundStyle(Tj.Palette.text) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .tjCard() HStack { Text(phase.label) .font(.system(size: 13, weight: .medium)) .foregroundStyle(statusColor) .lineLimit(1) Spacer() if rate > 0 { Text(String(format: "%.1f tok/s", rate)) .font(.system(size: 12, design: .monospaced)) .foregroundStyle(Tj.Palette.text3) } } Button { Task { await run() } } label: { Text(isBusy ? "运行中…" : "运行推理自检").frame(maxWidth: .infinity) } .buttonStyle(TjPrimaryButton()) .disabled(isBusy) ScrollView { Text(output.isEmpty ? "(暂无输出)" : output) .font(.system(.footnote, design: .monospaced)) .foregroundStyle(Tj.Palette.text) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .padding(12) } .frame(maxHeight: 280) .background( RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous) .fill(Tj.Palette.paper) ) .overlay( RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous) .strokeBorder(Tj.Palette.lineSoft, lineWidth: 1) ) Spacer() } .padding(16) .background(Tj.Palette.sand.ignoresSafeArea()) .navigationTitle("推理自检") .navigationBarTitleDisplayMode(.inline) } @MainActor private func run() async { output = "" rate = 0 phase = .loading do { try await AIRuntime.shared.prepare() phase = .running for try await chunk in await AIRuntime.shared.generate(prompt: prompt, maxTokens: 200) { output += chunk.text rate = chunk.decodeRate } phase = .done } catch { phase = .failed(error.localizedDescription) } } } #Preview { NavigationStack { ModelSelfTestView() } }