feat(debug): DebugAIRunner DEBUG 自检入口挂到 MeView
按 W2 plan Task 7 落地,实现胜过 plan 原稿(强化 UX 减少 Xcode console 依赖): - 卡片显示 Application Support 路径 + 模型预期完整路径 - 一键复制路径到剪贴板,方便 `cp -R` 拷模型 - 模型就绪状态徽章(✓ 就绪 / ⚠ 未就绪),依赖 ModelStore.isReady - 跑一段 prompt 流式输出,顶部 tok/s 速率显示 - 全文件 #if DEBUG 包裹,Release 不打包 MeView 在 DEBUG 时挂 DebugAIRunner 在 placeholder 下方。 下一步用户手动:把 ~/tiji-models/Qwen3-1.7B-4bit 拷到模拟器沙盒 Application Support/Models/ 下,然后跑 App → Me 页点按钮验收。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
体己/Debug/DebugAIRunner.swift
Normal file
130
体己/Debug/DebugAIRunner.swift
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// DEBUG 自检:加载 LLM 并跑一段 prompt,流式显示 token + 速率。
|
||||||
|
/// 同时显示沙盒 Application Support 路径,方便把模型拷进去。
|
||||||
|
struct DebugAIRunner: View {
|
||||||
|
@State private var output: String = ""
|
||||||
|
@State private var status: String = "未开始"
|
||||||
|
@State private var rate: Double = 0
|
||||||
|
@State private var running = false
|
||||||
|
@State private var modelReady: Bool = false
|
||||||
|
|
||||||
|
private var appSupportPath: String {
|
||||||
|
(try? FileManager.default.url(
|
||||||
|
for: .applicationSupportDirectory,
|
||||||
|
in: .userDomainMask,
|
||||||
|
appropriateFor: nil,
|
||||||
|
create: false
|
||||||
|
).path) ?? "(无法获取)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var modelExpectedPath: String {
|
||||||
|
appSupportPath + "/Models/Qwen3-1.7B-4bit"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
Text("DEBUG · AI 自检")
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundStyle(Tj.Palette.text)
|
||||||
|
|
||||||
|
// 沙盒路径与模型状态卡
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("模型预期路径")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(Tj.Palette.text3)
|
||||||
|
Text(modelExpectedPath)
|
||||||
|
.font(.system(size: 10, design: .monospaced))
|
||||||
|
.foregroundStyle(Tj.Palette.text2)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.lineLimit(3)
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button("复制路径") {
|
||||||
|
UIPasteboard.general.string = modelExpectedPath
|
||||||
|
}
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(modelReady ? "✓ 模型就绪" : "⚠ 模型未就绪")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(modelReady ? Tj.Palette.leaf : Tj.Palette.brick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.black.opacity(0.03))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 推理状态
|
||||||
|
HStack {
|
||||||
|
Text("状态:\(status)")
|
||||||
|
Spacer()
|
||||||
|
Text(String(format: "%.1f tok/s", rate))
|
||||||
|
.foregroundStyle(Tj.Palette.text3)
|
||||||
|
.monospaced()
|
||||||
|
}
|
||||||
|
.font(.system(size: 12))
|
||||||
|
|
||||||
|
Button(running ? "推理中..." : "跑一段 prompt") {
|
||||||
|
Task { await run() }
|
||||||
|
}
|
||||||
|
.buttonStyle(TjPrimaryButton())
|
||||||
|
.disabled(running)
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
Text(output.isEmpty ? "(暂无输出)" : output)
|
||||||
|
.font(.system(.footnote, design: .monospaced))
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: 240)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.black.opacity(0.04))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Tj.Radius.md)
|
||||||
|
.fill(Color.yellow.opacity(0.08))
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.onAppear { refreshModelStatus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshModelStatus() {
|
||||||
|
modelReady = ModelStore.shared.isReady(.llm)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func run() async {
|
||||||
|
running = true
|
||||||
|
output = ""
|
||||||
|
rate = 0
|
||||||
|
status = "加载模型..."
|
||||||
|
do {
|
||||||
|
try await AIRuntime.shared.prepare()
|
||||||
|
status = "推理中..."
|
||||||
|
|
||||||
|
let prompt = "用中文一句话介绍肝功能里 ALT 这个指标。"
|
||||||
|
for try await chunk in await AIRuntime.shared.generate(
|
||||||
|
prompt: prompt,
|
||||||
|
maxTokens: 200
|
||||||
|
) {
|
||||||
|
output += chunk.text
|
||||||
|
rate = chunk.decodeRate
|
||||||
|
}
|
||||||
|
status = "完成 ✓"
|
||||||
|
} catch {
|
||||||
|
status = "失败:\(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
running = false
|
||||||
|
refreshModelStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -2,16 +2,17 @@ import SwiftUI
|
|||||||
|
|
||||||
struct MeView: View {
|
struct MeView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
ScrollView {
|
||||||
Spacer()
|
VStack(spacing: 16) {
|
||||||
TjPlaceholder(label: "me · 设置 / 模型 / 档案管理\n(尚未实现)")
|
TjPlaceholder(label: "我的 · 模型管理 / Face ID / 关于\n(W6 实现)")
|
||||||
.frame(width: 280, height: 180)
|
.frame(width: 280, height: 180)
|
||||||
Text("我的")
|
|
||||||
.font(.tjH2())
|
#if DEBUG
|
||||||
.foregroundStyle(Tj.Palette.text2)
|
DebugAIRunner()
|
||||||
Spacer()
|
#endif
|
||||||
|
}
|
||||||
|
.padding(.vertical, 24)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user