import SwiftUI /// 推理引擎设置:本项目已切 Gemma-3n(端侧 4bit),主模型只走 MLX/GPU。 /// MNN/SME2 路径已停用(Gemma-3n 无 MNN 转换模型),引擎行保留但 MNN 置灰。 /// 切换只改持久化选择;下一次 AI 调用(prepare/generate)按新引擎加载。 struct InferenceSettingsView: View { @AppStorage("kk.inferenceEngine") private var engineRaw = EnginePreference.auto.rawValue // 云端 AI(Gemini)开关与 key —— 键名与 CloudAI 对齐,@AppStorage 写入即被后端读到。 @AppStorage("cloud_ai_gemini_enabled") private var cloudEnabled = false @AppStorage("cloud_ai_gemini_key") private var cloudKey = "" @State private var modelService = ModelDownloadService.shared /// 性能自检改为当前页就地展开,不再 push 新页面。 @State private var showSelfTest = false private var selected: EnginePreference { EnginePreference(rawValue: engineRaw) ?? .auto } /// 性能自检需要主模型(Gemma-3n,MLX)就绪。 private var modelReady: Bool { modelService.states[.llm]?.phase == .ready } var body: some View { ScrollView { VStack(spacing: 12) { HStack { Text("推理引擎") .font(.tjTitle()) .foregroundStyle(Tj.Palette.text) Spacer() } .padding(.top, 4) .padding(.bottom, 6) ForEach(EnginePreference.allCases, id: \.self) { engine in engineRow(engine) } selfTestSection cloudSection noteCard } .padding(.horizontal, 16) .padding(.vertical, 20) } .background(Tj.Palette.sand.ignoresSafeArea()) .onAppear { modelService.refreshStates() } } /// 性能自检入口:它是「动作/工具」而非引擎选择,所以做成描边动作按钮(TjGhostButton), /// 从视觉类别上和上方引擎/状态卡区分开。模型未就绪时禁用并给出下载提示而非死链。 @ViewBuilder private var selfTestSection: some View { if modelReady { VStack(spacing: 12) { Button { withAnimation(.easeInOut(duration: 0.22)) { showSelfTest.toggle() } } label: { HStack(spacing: 8) { Image(systemName: "gauge.with.needle") .font(.tjScaled(15, weight: .semibold)) Text("性能自检") Image(systemName: "chevron.down") .font(.tjScaled(13, weight: .semibold)) .rotationEffect(.degrees(showSelfTest ? 180 : 0)) } .frame(maxWidth: .infinity) } .buttonStyle(TjGhostButton()) if showSelfTest { ModelSelfTestView(embedded: true) .transition(.opacity.combined(with: .move(edge: .top))) } } .padding(.top, 4) } else { VStack(spacing: 8) { HStack(spacing: 8) { Image(systemName: "gauge.with.needle") .font(.tjScaled(15, weight: .semibold)) Text("性能自检") } .font(.tjScaled(15, weight: .semibold)) .foregroundStyle(Tj.Palette.text3) .frame(maxWidth: .infinity) .frame(height: 48) .background(Capsule().strokeBorder(Tj.Palette.line, lineWidth: 1)) .opacity(0.6) Text("模型未就绪,前往「模型管理」下载后可用") .font(.tjScaled(12)) .foregroundStyle(Tj.Palette.text3) } .padding(.top, 4) } } private func engineRow(_ engine: EnginePreference) -> some View { let available = isAvailable(engine) let isOn = (selected == engine) return Button { guard available else { return } engineRaw = engine.rawValue } label: { HStack(spacing: 12) { ZStack { Circle().fill(isOn ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2) Image(systemName: iconName(engine)) .font(.tjScaled(18)) .foregroundStyle(isOn ? Tj.Palette.ink : Tj.Palette.text2) } .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text(engine.displayName) .font(.tjScaled(15, weight: .semibold)) .foregroundStyle(Tj.Palette.text) Text(subtitle(engine, available: available)) .font(.tjScaled(12)) .foregroundStyle(Tj.Palette.text3) .lineLimit(2) } Spacer() if isOn { Image(systemName: "checkmark.circle.fill") .font(.tjScaled(18)) .foregroundStyle(Tj.Palette.leaf) } } .padding(14) .tjCard() .opacity(available ? 1 : 0.45) } .buttonStyle(.plain) .disabled(!available) } /// .auto 永远可用;MLX 看自身可用性。MNN 已停用(Gemma-3n 无 MNN 模型),恒置灰。 private func isAvailable(_ engine: EnginePreference) -> Bool { switch engine { case .auto: return true case .mnn: return false case .mlx: return InferenceEngine.mlx.isAvailable } } private func iconName(_ engine: EnginePreference) -> String { switch engine { case .auto: return "wand.and.stars" case .mnn: return "cpu.fill" case .mlx: return "bolt.fill" } } private func subtitle(_ engine: EnginePreference, available: Bool) -> String { switch engine { case .auto: // 已切 Gemma-3n,auto 恒解析为 MLX。 return String(appLoc: "按本机配置选择 · 当前 MLX · GPU") case .mnn: return String(appLoc: "已停用:Gemma-3n 无 MNN 转换模型,统一走 MLX") case .mlx: return String(appLoc: "Metal GPU · 端侧推理 Gemma-3n E2B") } } private var cloudConfigured: Bool { cloudEnabled && !cloudKey.trimmingCharacters(in: .whitespaces).isEmpty } /// 云端 AI(Gemini)开关区。hybrid 的「云端」一侧:默认关(隐私优先), /// 开启并填入 AI Studio 的 key 后,「读报告原图 / 深度解读 / 多语言」走 Google Gemini。 private var cloudSection: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("云端 AI · Gemini") .font(.tjTitle()) .foregroundStyle(Tj.Palette.text) Spacer() } .padding(.top, 10) VStack(alignment: .leading, spacing: 12) { Toggle(isOn: $cloudEnabled) { VStack(alignment: .leading, spacing: 2) { Text("启用云端增强") .font(.tjScaled(15, weight: .semibold)) .foregroundStyle(Tj.Palette.text) Text("默认端侧 Gemma-3n;开启后「读报告原图 / 深度解读 / 多语言」走 Google Gemini。") .font(.tjScaled(12)) .foregroundStyle(Tj.Palette.text3) } } .tint(Tj.Palette.leaf) if cloudEnabled { SecureField(String(appLoc: "粘贴 Google AI Studio 的 API Key"), text: $cloudKey) .font(.tjScaled(14)) .textInputAutocapitalization(.never) .autocorrectionDisabled() .padding(12) .background(RoundedRectangle(cornerRadius: Tj.Radius.md).fill(Tj.Palette.sand2)) HStack(spacing: 6) { Image(systemName: cloudConfigured ? "checkmark.seal.fill" : "exclamationmark.circle") .font(.tjScaled(13)) .foregroundStyle(cloudConfigured ? Tj.Palette.leaf : Tj.Palette.text3) Text(cloudConfigured ? String(appLoc: "已就绪 · \(AIRuntime.cloudLabel)") : String(appLoc: "请填入 API Key(aistudio.google.com 免费获取)")) .font(.tjScaled(12)) .foregroundStyle(Tj.Palette.text3) } Text("Key 仅存本机;断网或额度用尽时自动回退端侧 Gemma-3n,功能不中断。") .font(.tjScaled(11)) .foregroundStyle(Tj.Palette.text3) } } .padding(14) .tjCard() } } private var noteCard: some View { Text("隐私优先:默认端侧 Gemma-3n(MLX · Metal GPU)推理,数据不出设备;仅在你开启「云端 AI」后,深度任务才走 Google Gemini。切换后下一次 AI 调用生效。") .font(.tjScaled(12)) .foregroundStyle(Tj.Palette.text3) .frame(maxWidth: .infinity, alignment: .leading) .padding(14) .tjCard() } } #Preview { InferenceSettingsView() }