Files
kangkang/康康/Features/Me/InferenceSettingsView.swift
link2026 b3777d508d 根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
2026-06-16 00:01:48 +08:00

223 lines
8.5 KiB
Swift

import SwiftUI
/// : MNN(CPU/SME2,) MLX(GPU,), SME2
/// ; AI (prepare/generate)
struct InferenceSettingsView: View {
@AppStorage("kk.inferenceEngine") private var engineRaw = EnginePreference.auto.rawValue
@State private var modelService = ModelDownloadService.shared
private var selected: EnginePreference {
EnginePreference(rawValue: engineRaw) ?? .auto
}
/// (MNN MLX )
private var modelReady: Bool {
modelService.states[.mnnLLM]?.phase == .ready
|| 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)
}
sme2Card
selfTestSection
noteCard
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.onAppear { modelService.refreshStates() }
}
/// : prompt,
///
@ViewBuilder
private var selfTestSection: some View {
if modelReady {
NavigationLink {
ModelSelfTestView()
} label: {
HStack(spacing: 12) {
ZStack {
Circle().fill(Tj.Palette.sand2)
Image(systemName: "gauge.with.needle")
.font(.tjScaled(18))
.foregroundStyle(Tj.Palette.ink)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("性能自检")
.font(.tjScaled(15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("用上方选中的引擎跑固定 prompt,实测 prefill / 生成 tok/s")
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(2)
}
Spacer()
Image(systemName: "chevron.right")
.font(.tjScaled(13, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
.tjCard()
}
.buttonStyle(.plain)
} else {
HStack(spacing: 12) {
ZStack {
Circle().fill(Tj.Palette.sand2)
Image(systemName: "gauge.with.needle")
.font(.tjScaled(18))
.foregroundStyle(Tj.Palette.text2)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("性能自检")
.font(.tjScaled(15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("模型未就绪,前往「模型管理」下载后可用")
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(2)
}
Spacer()
}
.padding(14)
.tjCard()
.opacity(0.55)
}
}
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 ;
private func isAvailable(_ engine: EnginePreference) -> Bool {
switch engine {
case .auto: return true
case .mnn: return InferenceEngine.mnn.isAvailable
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:
// ,
let resolved = engine.resolved
if resolved == .mnn {
return InferenceEngine.cpuSupportsSME2
? String(appLoc: "按本机配置选择 · 当前 MNN + SME2")
: String(appLoc: "按本机配置选择 · 当前 MNN(NEON)")
} else {
return String(appLoc: "按本机配置选择 · 当前 MLX(MNN 不可用)")
}
case .mnn:
if !available { return String(appLoc: "本设备/模拟器不可用,自动回退 MLX") }
return InferenceEngine.cpuSupportsSME2
? String(appLoc: "端侧 CPU + SME2 加速 · 挑战赛考核路径")
: String(appLoc: "端侧 CPU(本机无 SME2,NEON 回退)")
case .mlx:
return String(appLoc: "Metal GPU · 兜底 / 对照")
}
}
private var sme2Card: some View {
let sme2 = InferenceEngine.cpuSupportsSME2
return HStack(spacing: 12) {
ZStack {
Circle().fill(sme2 ? Tj.Palette.leafSoft : Tj.Palette.sand2)
Image(systemName: sme2 ? "checkmark.seal.fill" : "minus.circle")
.font(.tjScaled(18))
.foregroundStyle(sme2 ? Tj.Palette.ink : Tj.Palette.text2)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("Arm SME2")
.font(.tjScaled(15, weight: .medium))
.foregroundStyle(Tj.Palette.text)
Text(sme2 ? String(appLoc: "本设备支持,MNN 已启用 SME2 加速")
: String(appLoc: "本设备不支持(需 A19/iPhone 17+)"))
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
}
.padding(14)
.tjCard()
}
private var noteCard: some View {
Text("MNN 在端侧 CPU 上以 Arm SME2 指令集加速 Qwen 推理(本地、不上云)。切换后下一次 AI 调用生效。")
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.tjCard()
}
}
#Preview {
InferenceSettingsView()
}