feat(DiaryQuickSheet): 添加AI追问问答功能和底部协作入口 - 新增currentAnswer状态管理追问输入,添加answerFocused状态独立处理键盘避让 - 移除键盘工具条的关心条,将AI协作入口固定到底部按钮 - 添加完整的问答式追问卡片组件,支持自由回答输入和加入日记功能 - 修改prompt阶段行为,不再在正文区显示邀请横幅 - 更新recordCurrent为answerCurrent,实现问题+答案一同加入日记的逻辑 - 调整底部操作栏布局,间距和内边距优化 refactor(InferenceSettingsView): 性能自检改为内联展开模式 - 将性能自检视图从导航链接改为当前页就地展开 - 添加showSelfTest状态控制展开收起动画 - 支持ModelSelfTestView内联嵌入模式,去除外层导航和背景 chore(Localizable): 同步更新本地化字符串资源 - 添加新的UI文本:加入日记、在这儿写下你的回答、康康帮你一起填等 - 修复部分字符串位置调整和翻译映射问题 - 同步更新多语言版本的翻译内容 style(RootView): 优化记一笔标签页视觉设计 - 为记一笔标签添加语音识别角标标识 - 使用麦克风图标配合加号突出长按语音直达功能 ```
214 lines
8.2 KiB
Swift
214 lines
8.2 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
|
|
/// 性能自检改为当前页就地展开,不再 push 新页面。
|
|
@State private var showSelfTest = false
|
|
|
|
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() }
|
|
}
|
|
|
|
/// 性能自检入口:它是「动作/工具」而非引擎选择,所以做成描边动作按钮(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 永远可用;具体引擎看自身可用性。
|
|
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()
|
|
}
|