feat(AI): 优化AIRuntime任务取消机制并增强安全保护

- 在AI推理流中添加Task.checkCancellation()检查,使消费者取消时能快速退出
- 为异步流添加onTermination回调以取消内部Task,与LLMSession一致
- 实现SwiftData store的completeUnlessOpen文件保护,提升数据安全性
- 在store备份过程中同样应用加密保护

feat(home): 优化主页交互体验并统一详情查看功能

- 在主页"最近记录"中点击任意条目可打开只读详情sheet
- 将时间线详情解析逻辑统一收敛到TimelineDetail.resolve方法
- 修复血压条目的精确反查逻辑,避免时间窗匹配错误

feat(archive): 新增提醒任务汇总卡并完善档案库功能

- 在档案库页面新增提醒任务汇总卡,显示总数和启用状态
- 添加按更新时间倒序合并的提醒标题预览功能
- 实现RemindersListView导航路由,统一管理提醒任务
- 优化导出列表显示,优先使用中文标签展示

feat(me): 优化个人中心界面并改进语言设置体验

- 将个人中心标题改为内容文字渲染,解决导航栏背景问题
- 为语言选择器添加个性化图标,使用本族语代表字区分
- 修复语言设置视图的图标显示逻辑

feat(timeline): 新增记录详情页删除功能并优化图表显示

- 在时间线详情页添加永久删除按钮和确认弹窗
- 实现完整的删除逻辑,包括SwiftData硬删和Vault原图unlink
- 修复系列图表的数值范围计算,处理同值数据的对称留白
- 优化血压图表合并逻辑,只保留有数据点的线条

refactor(calendar): 修复DST切换导致的月份天数计算错误

- 使用calendar.range(of:.day,in:.month)替代日期间隔计算
- 避免在夏令时切换月份出现天数偏差问题

fix(ui): 修复多个UI组件的交互响应区域问题

- 为纯描边按钮和胶囊添加contentShape以扩大点击区域
- 修复提醒行展开按钮尺寸,保证不同提醒类型的垂直对齐
```
This commit is contained in:
link2026
2026-05-31 09:25:49 +08:00
parent 7ad41c5f09
commit 40155de709
19 changed files with 424 additions and 47 deletions

View File

@@ -73,7 +73,7 @@ actor AIRuntime {
let snapshotSession = llmSession
return AsyncThrowingStream { continuation in
Task {
let task = Task {
guard snapshotStatus == .ready, let session = snapshotSession else {
continuation.finish(throwing: AIRuntimeError.notReady)
return
@@ -82,6 +82,9 @@ actor AIRuntime {
// session.generate actor , await
let stream = await session.generate(prompt: prompt, maxTokens: maxTokens)
for try await chunk in stream {
// (UI)/, checkCancellation Task 退,
// session onTermination, MLX , GPU
try Task.checkCancellation()
// Task generate() , AIRuntime actor ;
// actor recordRate await
self.recordRate(chunk.decodeRate)
@@ -92,6 +95,8 @@ actor AIRuntime {
continuation.finish(throwing: AIRuntimeError.inferenceFailed("\(error)"))
}
}
// / Task( LLMSession / HealthExportService )
continuation.onTermination = { _ in task.cancel() }
}
}

View File

@@ -20,8 +20,14 @@ struct KangkangApp: App {
CustomReminder.self,
])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
// store .completeUnlessOpen (§6),
func makeContainer() throws -> ModelContainer {
let container = try ModelContainer(for: schema, configurations: [config])
KangkangApp.protectStore(at: config.url)
return container
}
do {
return try ModelContainer(for: schema, configurations: [config])
return try makeContainer()
} catch {
// Demo schema : SwiftData
// (: @Model ),
@@ -32,13 +38,27 @@ struct KangkangApp: App {
print("⚠️ ModelContainer 创建失败,备份旧 store 后重建: \(error)")
KangkangApp.backupIncompatibleStore(at: config.url)
do {
return try ModelContainer(for: schema, configurations: [config])
return try makeContainer()
} catch {
fatalError("Could not create ModelContainer even after store reset: \(error)")
}
}
}()
/// SwiftData store( `-wal`/`-shm`) `.completeUnlessOpen` :
/// , SQLite ,
/// `.complete` /Live Activity 访 store CLAUDE.md §6
/// ( iOS CompleteUntilFirstUserAuthentication,)
private static func protectStore(at storeURL: URL) {
let fm = FileManager.default
for suffix in ["", "-wal", "-shm"] {
let path = storeURL.path + suffix
guard fm.fileExists(atPath: path) else { continue }
try? fm.setAttributes([.protectionKey: FileProtectionType.completeUnlessOpen],
ofItemAtPath: path)
}
}
/// schema store( `-wal` / `-shm`)
/// `Application Support/StoreBackups/<>/`,
/// ,;
@@ -51,12 +71,17 @@ struct KangkangApp: App {
let backupDir = storeURL.deletingLastPathComponent()
.appendingPathComponent("StoreBackups/\(stamp)", isDirectory: true)
try? fm.createDirectory(at: backupDir, withIntermediateDirectories: true)
// ()
try? fm.setAttributes([.protectionKey: FileProtectionType.completeUnlessOpen],
ofItemAtPath: backupDir.path)
for suffix in ["", "-wal", "-shm"] {
let src = URL(fileURLWithPath: storeURL.path + suffix)
guard fm.fileExists(atPath: src.path) else { continue }
let dst = backupDir.appendingPathComponent(src.lastPathComponent)
do {
try fm.moveItem(at: src, to: dst)
try? fm.setAttributes([.protectionKey: FileProtectionType.completeUnlessOpen],
ofItemAtPath: dst.path)
} catch {
try? fm.removeItem(at: src) // ,
}

View File

@@ -26,6 +26,23 @@ enum AppLanguage: String, CaseIterable, Identifiable {
var localeIdentifier: String? {
self == .system ? nil : rawValue
}
/// ( / A / / ),
/// , `displayName`
enum PickerIcon: Equatable {
case symbol(String) // SF Symbol
case glyph(String) //
}
var pickerIcon: PickerIcon {
switch self {
case .system: return .symbol("globe")
case .zhHans: return .glyph("")
case .en: return .glyph("A")
case .ja: return .glyph("")
case .ko: return .glyph("")
}
}
}
/// App : lproj bundle locale

View File

@@ -17,11 +17,21 @@ struct ArchiveListView: View {
@Query(sort: \HealthExport.createdAt, order: .reverse)
private var exports: [HealthExport]
@Query(sort: \CustomReminder.updatedAt, order: .reverse)
private var customReminders: [CustomReminder]
@Query(sort: \MetricReminder.updatedAt, order: .reverse)
private var metricReminders: [MetricReminder]
/// push `navigationDestination(item:)`
/// `navigationDestination(isPresented:)` SwiftUI ()
private enum Route: Hashable { case exports, reminders }
@State private var filter: TimelineKind? = nil
@State private var endingSymptom: Symptom?
@State private var selectedEntry: TimelineEntry?
@State private var showExportSheet = false
@State private var showExportList = false
@State private var route: Route?
@MainActor
private var allEntries: [TimelineEntry] {
@@ -43,8 +53,11 @@ struct ArchiveListView: View {
var body: some View {
NavigationStack {
content
.navigationDestination(isPresented: $showExportList) {
HealthExportListView()
.navigationDestination(item: $route) { route in
switch route {
case .exports: HealthExportListView()
case .reminders: RemindersListView()
}
}
}
}
@@ -56,6 +69,12 @@ struct ArchiveListView: View {
.padding(.top, 8)
.padding(.bottom, 14)
if reminderTotal > 0 {
reminderBoard
.padding(.horizontal, 20)
.padding(.bottom, 14)
}
filterChips
.padding(.bottom, 14)
@@ -118,33 +137,11 @@ struct ArchiveListView: View {
}
}
/// 线(id `<kind>-<persistentModelID>` / `bp-<sys>-<dia>`)
/// 线 `TimelineDetail.resolve`(/)
private func detail(for entry: TimelineEntry) -> TimelineDetail? {
switch entry.kind {
case .report:
return reports.first { "report-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.report)
case .diary:
return diaries.first { "diary-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.diary)
case .symptom:
return symptoms.first { "symptom-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.symptom)
case .indicator:
if let i = indicators.first(where: { "indicator-\($0.persistentModelID)" == entry.id }) {
return .indicator(i)
}
// :bp-<sysID>-<diaID>
if entry.id.hasPrefix("bp-"),
let sys = indicators.first(where: { entry.id.hasPrefix("bp-\($0.persistentModelID)-") }) {
let dia = indicators.first {
$0.seriesKey == "bp.diastolic" &&
abs($0.capturedAt.timeIntervalSince(sys.capturedAt)) <= 5
}
return .bloodPressure(sys: sys, dia: dia)
}
return nil
}
TimelineDetail.resolve(for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms)
}
private var header: some View {
@@ -164,7 +161,7 @@ struct ArchiveListView: View {
}
if !exports.isEmpty {
Button {
showExportList = true
route = .exports
} label: {
Label("我的导出 · \(exports.count)", systemImage: "clock.arrow.circlepath")
}
@@ -173,7 +170,7 @@ struct ArchiveListView: View {
HStack(spacing: 6) {
Image(systemName: "doc.text.below.ecg")
.font(.system(size: 12, weight: .semibold))
Text("导出")
Text("导出身体档案")
.font(.system(size: 13, weight: .semibold))
Image(systemName: "chevron.down")
.font(.system(size: 9, weight: .semibold))
@@ -186,6 +183,71 @@ struct ArchiveListView: View {
}
}
// MARK: -
/// ( + ),
private var reminderTotal: Int { customReminders.count + metricReminders.count }
private var reminderEnabledCount: Int {
customReminders.filter(\.enabled).count + metricReminders.filter(\.enabled).count
}
/// updatedAt , 3 (,)
private var reminderTitlePreview: [String] {
let merged: [(title: String, at: Date)] =
customReminders.map { ($0.title, $0.updatedAt) } +
metricReminders.map { ($0.displayName, $0.updatedAt) }
return merged.sorted { $0.at > $1.at }.prefix(3).map(\.title)
}
private var reminderCountLabel: String {
reminderEnabledCount == reminderTotal
? String(appLoc: "\(reminderTotal) 个提醒任务")
: String(appLoc: "\(reminderTotal) 个提醒任务 · \(reminderEnabledCount) 个开启中")
}
private var reminderTitleLine: String {
let joined = reminderTitlePreview.joined(separator: " · ")
return reminderTotal > reminderTitlePreview.count ? joined + "" : joined
}
/// (RemindersListView);
private var reminderBoard: some View {
Button { route = .reminders } label: {
HStack(spacing: 12) {
ZStack {
Circle().fill(reminderEnabledCount > 0 ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.system(size: 16))
.foregroundStyle(reminderEnabledCount > 0 ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminderCountLabel)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
if !reminderTitlePreview.isEmpty {
Text(reminderTitleLine)
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
}
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
.contentShape(Rectangle())
.tjCard()
}
.buttonStyle(.plain)
}
private var filterChips: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {

View File

@@ -125,6 +125,7 @@ struct HealthExportDetailView: View {
.padding(.horizontal, 14)
.frame(height: 44)
.background(Capsule().strokeBorder(Tj.Palette.ink, lineWidth: 1))
.contentShape(Capsule()) // :
}
Spacer()

View File

@@ -107,7 +107,7 @@ struct HealthExportRow: View {
.foregroundStyle(Tj.Palette.leaf)
}
Spacer()
if let label = export.inferredIntent {
if let label = export.inferredLabelCN ?? export.inferredIntent {
TjBadge(text: label, style: .neutral)
}
}

View File

@@ -274,6 +274,7 @@ struct HealthExportSheet: View {
.padding(.horizontal, 14)
.frame(height: 44)
.background(Capsule().strokeBorder(Tj.Palette.ink, lineWidth: 1))
.contentShape(Capsule()) // :
}
Spacer()
@@ -297,6 +298,7 @@ struct HealthExportSheet: View {
guard !p.isEmpty else { return }
promptFocused = false
content = ""
rate = 0 // , tok/s
error = nil
completed = false
phase = .extractingIntent

View File

@@ -293,6 +293,8 @@ struct DiaryQuickSheet: View {
style: StrokeStyle(lineWidth: 1, dash: enabled ? [] : [3, 3])
)
)
// : contentShape (+)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(!enabled)

View File

@@ -16,6 +16,9 @@ struct HomeView: View {
@Query(sort: \Symptom.startedAt, order: .reverse)
private var symptoms: [Symptom]
/// sheet( C1 )
@State private var selectedEntry: TimelineEntry?
@MainActor
private var recentEntries: [TimelineEntry] {
let all =
@@ -51,6 +54,15 @@ struct HomeView: View {
.padding(.bottom, 20)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.sheet(item: $selectedEntry) { entry in
if let d = TimelineDetail.resolve(
for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms
) {
TimelineEntryDetailView(detail: d)
}
}
}
private var greeting: some View {
@@ -110,8 +122,19 @@ struct HomeView: View {
.foregroundStyle(Tj.Palette.text3)
VStack(spacing: 10) {
ForEach(group.items) { entry in
Button {
if TimelineDetail.resolve(
for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms
) != nil {
selectedEntry = entry
}
} label: {
TimelineRow(entry: entry)
}
.buttonStyle(.plain)
}
}
}
}

View File

@@ -35,9 +35,7 @@ struct LanguageSettingsView: View {
HStack(spacing: 12) {
ZStack {
Circle().fill(selected ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "character.bubble")
.font(.system(size: 16))
.foregroundStyle(selected ? Tj.Palette.ink : Tj.Palette.text2)
icon(option, selected: selected)
}
.frame(width: 40, height: 40)
@@ -58,6 +56,22 @@ struct LanguageSettingsView: View {
}
.buttonStyle(.plain)
}
/// :(/A//),
@ViewBuilder
private func icon(_ option: AppLanguage, selected: Bool) -> some View {
let fg = selected ? Tj.Palette.ink : Tj.Palette.text2
switch option.pickerIcon {
case .symbol(let name):
Image(systemName: name)
.font(.system(size: 16))
.foregroundStyle(fg)
case .glyph(let g):
Text(verbatim: g)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(fg)
}
}
}
#Preview {

View File

@@ -24,6 +24,15 @@ struct MeView: View {
NavigationStack {
ScrollView {
VStack(spacing: 12) {
HStack {
Text("我的")
.font(.tjTitle())
.foregroundStyle(Tj.Palette.text)
Spacer()
}
.padding(.top, 4)
.padding(.bottom, 6)
profileCard
customMetricsCard
modelManagementCard
@@ -42,8 +51,8 @@ struct MeView: View {
.padding(.vertical, 20)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle("我的")
.navigationBarTitleDisplayMode(.large)
// ( // ), .navigationTitle:
// , App
.onAppear {
if profiles.isEmpty {
_ = UserProfileStore.loadOrCreate(in: ctx)

View File

@@ -171,9 +171,11 @@ private struct CustomReminderRow: View {
.tint(Tj.Palette.ink)
.onChange(of: reminder.enabled) { _, _ in onToggle() }
// 28×28 , Toggle
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(width: 28, height: 28)
}
.padding(14)
.background(

View File

@@ -10,19 +10,56 @@ enum TimelineDetail {
case report(Report)
case diary(DiaryEntry)
case symptom(Symptom)
/// 线(id `<kind>-<persistentModelID>` / `bp-<sysID>-<diaID>`)
/// C1 , nil
static func resolve(for entry: TimelineEntry,
indicators: [Indicator],
reports: [Report],
diaries: [DiaryEntry],
symptoms: [Symptom]) -> TimelineDetail? {
switch entry.kind {
case .report:
return reports.first { "report-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.report)
case .diary:
return diaries.first { "diary-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.diary)
case .symptom:
return symptoms.first { "symptom-\($0.persistentModelID)" == entry.id }
.map(TimelineDetail.symptom)
case .indicator:
if let i = indicators.first(where: { "indicator-\($0.persistentModelID)" == entry.id }) {
return .indicator(i)
}
// :bp-<sysID>-<diaID>
if entry.id.hasPrefix("bp-"),
let sys = indicators.first(where: { entry.id.hasPrefix("bp-\($0.persistentModelID)-") }) {
// id diaID , ±5s
//()
let dia = indicators.first { entry.id.hasSuffix("-\($0.persistentModelID)") }
return .bloodPressure(sys: sys, dia: dia)
}
return nil
}
}
}
/// 线:,
struct TimelineEntryDetailView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var ctx
let detail: TimelineDetail
@State private var showDeleteConfirm = false
var body: some View {
VStack(spacing: 0) {
header
ScrollView {
VStack(alignment: .leading, spacing: 16) {
bodyContent
deleteButton
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
@@ -34,6 +71,63 @@ struct TimelineEntryDetailView: View {
.presentationDragIndicator(.visible)
.presentationBackground(Tj.Palette.sand)
.presentationCornerRadius(Tj.Radius.xl)
.alert(String(appLoc: "永久删除这条记录?"), isPresented: $showDeleteConfirm) {
Button(String(appLoc: "删除"), role: .destructive) { performDelete() }
Button(String(appLoc: "取消"), role: .cancel) { }
} message: {
Text("删除后无法恢复。")
}
}
// MARK: - (:SwiftData + Vault unlink, CLAUDE.md §6)
private var deleteButton: some View {
Button(role: .destructive) { showDeleteConfirm = true } label: {
Label(String(appLoc: "永久删除"), systemImage: "trash")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Tj.Palette.brick.opacity(0.8))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.brick.opacity(0.3), lineWidth: 1)
)
// : contentShape ()
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.padding(.top, 8)
}
private func performDelete() {
switch detail {
case .indicator(let i):
deleteIndicator(i)
case .bloodPressure(let sys, let dia):
deleteIndicator(sys)
if let dia { deleteIndicator(dia) }
case .report(let r):
// cascade Asset/Indicator ,Vault JPEG unlink
var paths = Set(r.assets.map(\.relativePath))
paths.formUnion(r.indicators.compactMap { $0.asset?.relativePath })
for p in paths { try? FileVault.shared.remove(relativePath: p) }
ctx.delete(r)
case .diary(let d):
ctx.delete(d)
case .symptom(let s):
ctx.delete(s)
}
try? ctx.save()
dismiss()
}
/// : unlink + Asset ( nullify,),
private func deleteIndicator(_ i: Indicator) {
if let asset = i.asset {
try? FileVault.shared.remove(relativePath: asset.relativePath)
ctx.delete(asset)
}
ctx.delete(i)
}
// MARK: - Header

View File

@@ -47,7 +47,8 @@ private struct MiniMonth: View {
private var days: [Date] {
guard let interval = calendar.dateInterval(of: .month, for: anchor) else { return [] }
let count = calendar.dateComponents([.day], from: interval.start, to: interval.end).day ?? 30
// range(of:.day,in:.month) , DST 1
let count = calendar.range(of: .day, in: .month, for: anchor)?.count ?? 30
return (0..<count).compactMap { calendar.date(byAdding: .day, value: $0, to: interval.start) }
}

View File

@@ -46,8 +46,7 @@ extension SeriesBucket {
//
let bpKeys: Set<String> = ["bp.systolic", "bp.diastolic"]
let bpIndicators = bpKeys.flatMap { buckets[$0] ?? [] }
let bpHasEnoughPoints = bpIndicators.filter { $0.seriesKey == "bp.systolic" }.count >= minPoints
let bpHasEnoughPoints = (buckets["bp.systolic"]?.count ?? 0) >= minPoints
var results: [SeriesBucket] = []
@@ -142,11 +141,13 @@ extension SeriesBucket {
diaItems.last?.capturedAt ?? .distantPast
)
// 线:(/)线 +
let lines = [sysLine, diaLine].filter { !$0.points.isEmpty }
return SeriesBucket(
id: "bp",
title: String(appLoc: "血压"),
unit: "mmHg",
lines: [sysLine, diaLine],
lines: lines,
latestDate: latest
)
}

View File

@@ -34,8 +34,11 @@ struct SeriesChartCard: View {
hi = max(hi, r.upperBound)
}
}
guard lo < hi else { return nil }
let pad = max(1, (hi - lo) * 0.12)
// lo>hi nil;(lo==hi),
// 0...1
guard lo <= hi else { return nil }
let span = hi - lo
let pad = span > 0 ? max(1, span * 0.12) : max(1, abs(lo) * 0.1)
return (lo - pad)...(hi + pad)
}

View File

@@ -395,6 +395,50 @@
}
}
},
"%lld 个提醒任务" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld reminders"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 件のリマインダー"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "리마인더 %lld개"
}
}
}
},
"%lld 个提醒任务 · %lld 个开启中" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld reminders · %lld on"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 件のリマインダー · %lld 件オン"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "리마인더 %lld개 · %lld개 켜짐"
}
}
}
},
"%lld 个建议" : {
"localizations" : {
"en" : {
@@ -3402,6 +3446,28 @@
}
}
},
"删除后无法恢复。" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This can't be undone."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "削除すると元に戻せません。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "삭제하면 복구할 수 없습니다."
}
}
}
},
"删除后无法恢复。源记录(指标、症状等)不受影响。" : {
"localizations" : {
"en" : {
@@ -4867,6 +4933,7 @@
}
},
"导出" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -8873,6 +8940,28 @@
},
"每月%lld日" : {
},
"永久删除" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete Permanently"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "完全に削除"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "영구 삭제"
}
}
}
},
"永久删除这份导出?" : {
"localizations" : {
@@ -8896,6 +8985,28 @@
}
}
},
"永久删除这条记录?" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete this record permanently?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "この記録を完全に削除しますか?"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이 기록을 영구 삭제할까요?"
}
}
}
},
"没有指标 — 点上方「加一项」补一行,或直接保存只存图片" : {
"localizations" : {
"en" : {

View File

@@ -24,6 +24,8 @@ final class HealthExport {
var inferredTimeFromDate: Date?
var inferredTimeToDate: Date?
var inferredIntent: String?
/// (), badge ;,
var inferredLabelCN: String?
// demo
/// tag, "Qwen3-1.7B-4bit"
@@ -41,6 +43,7 @@ final class HealthExport {
inferredTimeFromDate: Date? = nil,
inferredTimeToDate: Date? = nil,
inferredIntent: String? = nil,
inferredLabelCN: String? = nil,
modelTag: String = "Qwen3-1.7B-4bit",
decodeRate: Double = 0) {
self.prompt = prompt
@@ -53,6 +56,7 @@ final class HealthExport {
self.inferredTimeFromDate = inferredTimeFromDate
self.inferredTimeToDate = inferredTimeToDate
self.inferredIntent = inferredIntent
self.inferredLabelCN = inferredLabelCN
self.modelTag = modelTag
self.decodeRate = decodeRate
}

View File

@@ -146,6 +146,7 @@ struct HealthExportService {
inferredTimeFromDate: snapshot.fromDate,
inferredTimeToDate: snapshot.toDate,
inferredIntent: intent.intent,
inferredLabelCN: intent.labelCN,
decodeRate: lastRate
)
modelContext.insert(export)