#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