Files
kangkang/康康/Debug/DebugAIRunner.swift
link2026 bb08243aa9 chore(preview): add #Preview to RecordSheet + DebugAIRunner
之前 HomeView/MeView/TrendsView/ArchiveListView/RootView/SymptomStartSheet
都有 #Preview,只剩这两个。补完后所有主屏 View 都能在 Xcode Canvas 直接
预览,改 UI 不用 build & run。
2026-05-25 23:37:55 +08:00

212 lines
7.1 KiB
Swift

#if DEBUG
import SwiftUI
import UIKit
import UniformTypeIdentifiers
/// 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
@State private var importingModel = false
@State private var importError: String?
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)
Button(importingModel ? "导入中..." : "导入模型") {
importingModel = true
}
.font(.system(size: 11))
.buttonStyle(.borderless)
.disabled(importingModel)
Spacer()
Text(modelReady ? "✓ 模型就绪" : "⚠ 模型未就绪")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(modelReady ? Tj.Palette.leaf : Tj.Palette.brick)
}
if let importError {
Text(importError)
.font(.system(size: 10))
.foregroundStyle(Tj.Palette.brick)
.lineLimit(3)
}
}
.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() }
.fileImporter(
isPresented: $importingModel,
allowedContentTypes: [.folder],
allowsMultipleSelection: false
) { result in
importModelFolder(from: result)
}
}
private func refreshModelStatus() {
modelReady = ModelStore.shared.isReady(.llm)
}
private func importModelFolder(from result: Result<[URL], Error>) {
defer {
refreshModelStatus()
importingModel = false
}
do {
guard let pickedURL = try result.get().first else {
importError = "未选择模型文件夹"
return
}
let securityScoped = pickedURL.startAccessingSecurityScopedResource()
defer {
if securityScoped {
pickedURL.stopAccessingSecurityScopedResource()
}
}
let sourceURL = resolvedModelSourceURL(from: pickedURL)
guard FileManager.default.fileExists(
atPath: sourceURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
) else {
importError = "请选择包含 config.json 的 Qwen3-1.7B-4bit 文件夹"
return
}
let targetURL = ModelStore.shared.localURL(for: .llm)
let parentURL = targetURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: parentURL, withIntermediateDirectories: true)
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
}
try FileManager.default.copyItem(at: sourceURL, to: targetURL)
importError = nil
} catch {
importError = "导入失败:\(error.localizedDescription)"
}
}
private func resolvedModelSourceURL(from pickedURL: URL) -> URL {
let nestedURL = pickedURL.appendingPathComponent(ModelKind.llm.rawValue, isDirectory: true)
if FileManager.default.fileExists(
atPath: nestedURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
) {
return nestedURL
}
return pickedURL
}
@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()
}
}
#Preview("DebugAIRunner") {
ScrollView {
DebugAIRunner()
.padding(.vertical, 24)
}
.background(Tj.Palette.sand.ignoresSafeArea())
}
#endif