import SwiftUI #if canImport(UIKit) import UIKit #endif // MARK: - Configurations 弹窗(iOS 版,适配 Theme 暖色系) struct ConfigurationsView: View { @Bindable var engine: AgentEngine @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL @State private var selectedTab = 0 // 0=Model Settings, 1=System Prompt, 2=Permissions // 本地编辑状态(确认后才应用) @State private var selectedModelID = MLXLocalLLMService.defaultModel.id @State private var maxTokens: Double = 4000 @State private var topK: Double = 64 @State private var topP: Double = 0.95 @State private var temperature: Double = 1.0 @State private var systemPrompt: String = "" @State private var permissionStatuses: [AppPermissionKind: AppPermissionStatus] = [:] @State private var requestingPermission: AppPermissionKind? private var isChineseSystem: Bool { Locale.preferredLanguages.contains { $0.hasPrefix("zh") } } var body: some View { NavigationStack { VStack(spacing: 0) { // Tab 切换 HStack(spacing: 0) { tabButton(localized("模型设置", "Model Settings"), tag: 0) tabButton(localized("系统提示词", "System Prompt"), tag: 1) tabButton(localized("权限", "Permissions"), tag: 2) } .padding(.horizontal) Rectangle().fill(Theme.border).frame(height: 1) Group { if selectedTab == 0 { modelConfigsTab } else if selectedTab == 1 { systemPromptTab } else { permissionsTab } } Rectangle().fill(Theme.border).frame(height: 1) // 底部按钮 HStack(spacing: 20) { Spacer() Button(localized("取消", "Cancel")) { dismiss() } .foregroundStyle(Theme.textSecondary) Button(localized("确定", "OK")) { applySettings() dismiss() } .font(.body.weight(.semibold)) .foregroundStyle(Theme.accent) .padding(.horizontal, 24) .padding(.vertical, 8) .background(Theme.accent.opacity(0.15), in: Capsule()) } .padding() } .navigationTitle(localized("配置", "Configurations")) .navigationBarTitleDisplayMode(.inline) .toolbarColorScheme(.dark, for: .navigationBar) .background(Theme.bgElevated) } .preferredColorScheme(.dark) .onAppear { loadCurrentSettings() } #if canImport(UIKit) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in refreshPermissionStatuses() } #endif } // MARK: - Tab 按钮 private func tabButton(_ title: String, tag: Int) -> some View { Button { withAnimation(.easeInOut(duration: 0.2)) { selectedTab = tag } } label: { VStack(spacing: 8) { Text(title) .font(.subheadline.weight(selectedTab == tag ? .semibold : .regular)) .foregroundStyle(selectedTab == tag ? Theme.textPrimary : Theme.textTertiary) Rectangle() .fill(selectedTab == tag ? Theme.accent : .clear) .frame(height: 2) } } .frame(maxWidth: .infinity) } // MARK: - Model Configs private var modelConfigsTab: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { modelSection configSlider( title: localized("最大 Token 数", "Max Tokens"), value: $maxTokens, range: 128...8192, displayValue: "\(Int(maxTokens))" ) configSlider( title: localized("采样 TopK", "TopK"), value: $topK, range: 1...128, displayValue: "\(Int(topK))" ) configSlider( title: localized("采样 TopP", "TopP"), value: $topP, range: 0...1, displayValue: String(format: "%.2f", topP) ) configSlider( title: localized("温度", "Temperature"), value: $temperature, range: 0...2, displayValue: String(format: "%.2f", temperature) ) } .padding() } } // MARK: - System Prompt private var systemPromptTab: some View { VStack(alignment: .leading, spacing: 12) { TextEditor(text: $systemPrompt) .font(.system(size: 13)) .foregroundStyle(Theme.textPrimary) .scrollContentBackground(.hidden) .padding(12) .background(Theme.bg, in: RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(Theme.border, lineWidth: 1) ) Button(localized("恢复默认", "Restore Default")) { systemPrompt = engine.defaultSystemPrompt } .font(.subheadline) .foregroundStyle(Theme.accent) } .padding() } private var permissionsTab: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { permissionsSection } .padding() } } // MARK: - 配置 Slider private var modelSection: some View { VStack(alignment: .leading, spacing: 12) { Text(localized("模型", "Model")) .font(.headline) .foregroundStyle(Theme.textPrimary) Text(engine.llm.isLoaded ? localized("当前已加载:", "Loaded: ") + engine.llm.modelDisplayName : engine.llm.statusMessage) .font(.subheadline) .foregroundStyle(Theme.textSecondary) VStack(spacing: 10) { ForEach(engine.availableModels) { model in Button { selectedModelID = model.id } label: { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(model.displayName) .font(.subheadline.weight(.semibold)) .foregroundStyle(Theme.textPrimary) Text(model.id) .font(.caption.monospaced()) .foregroundStyle(Theme.textTertiary) } Spacer() if selectedModelID == model.id { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.accent) } else { Image(systemName: "circle") .foregroundStyle(Theme.textTertiary) } } .padding(12) .background( RoundedRectangle(cornerRadius: 12) .fill(selectedModelID == model.id ? Theme.accentSubtle : Theme.bg) ) .overlay( RoundedRectangle(cornerRadius: 12) .strokeBorder( selectedModelID == model.id ? Theme.accent : Theme.border, lineWidth: 1 ) ) } .buttonStyle(.plain) } } Text(selectedModelID == engine.llm.selectedModelID ? localized("点击确定会保留当前模型。", "Tap OK to keep the current model.") : localized("点击确定后会卸载当前模型并重新加载新模型。", "Tap OK to unload the current model and reload the new one.")) .font(.caption) .foregroundStyle(Theme.textTertiary) } .padding(14) .background(Theme.bgElevated, in: RoundedRectangle(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) .strokeBorder(Theme.border, lineWidth: 1) ) } private var permissionsSection: some View { VStack(alignment: .leading, spacing: 12) { Text(localized("权限", "Permissions")) .font(.headline) .foregroundStyle(Theme.textPrimary) ForEach(AppPermissionKind.allCases) { kind in permissionRow(for: kind) } } .padding(14) .background(Theme.bgElevated, in: RoundedRectangle(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) .strokeBorder(Theme.border, lineWidth: 1) ) } private func permissionRow(for kind: AppPermissionKind) -> some View { let status = permissionStatuses[kind] ?? .notDetermined return VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 12) { Image(systemName: kind.icon) .font(.system(size: 15, weight: .medium)) .foregroundStyle(Theme.accent) .frame(width: 28, height: 28) .background(Theme.bg, in: RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 4) { HStack { Text(permissionTitle(kind)) .font(.subheadline.weight(.semibold)) .foregroundStyle(Theme.textPrimary) Spacer() Text(permissionStatusLabel(status)) .font(.caption.weight(.semibold)) .foregroundStyle(status.isGranted ? Theme.accentGreen : Theme.accent) .padding(.horizontal, 8) .padding(.vertical, 4) .background( (status.isGranted ? Theme.accentGreen : Theme.accent) .opacity(0.14), in: Capsule() ) } Text(permissionDescription(kind)) .font(.caption) .foregroundStyle(Theme.textSecondary) Text(permissionStatusDetail(status)) .font(.caption) .foregroundStyle(Theme.textTertiary) } } HStack(spacing: 10) { if !status.isGranted { Button(requestingPermission == kind ? localized("请求中...", "Requesting...") : localized("请求权限", "Request Access")) { requestPermission(kind) } .disabled(requestingPermission != nil) .font(.caption.weight(.semibold)) .foregroundStyle(Theme.accent) .padding(.horizontal, 12) .padding(.vertical, 8) .background(Theme.accent.opacity(0.15), in: Capsule()) } Button(localized("去设置", "Open Settings")) { openAppSettings() } .font(.caption.weight(.semibold)) .foregroundStyle(Theme.textSecondary) .padding(.horizontal, 12) .padding(.vertical, 8) .background(Theme.bg, in: Capsule()) } } .padding(.vertical, 4) } private func configSlider( title: String, value: Binding, range: ClosedRange, displayValue: String ) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.subheadline) .foregroundStyle(Theme.textSecondary) HStack(spacing: 12) { Slider(value: value, in: range) .tint(Theme.accent) Text(displayValue) .font(.body.monospaced()) .foregroundStyle(Theme.textPrimary.opacity(0.8)) .frame(width: 56) .padding(.horizontal, 8) .padding(.vertical, 5) .background(Theme.bg, in: RoundedRectangle(cornerRadius: 6)) } } } // MARK: - 加载 / 应用 private func localized(_ zh: String, _ en: String) -> String { isChineseSystem ? zh : en } private func permissionTitle(_ kind: AppPermissionKind) -> String { switch kind { case .calendar: return localized("日历", "Calendar") case .reminders: return localized("提醒事项", "Reminders") case .contacts: return localized("通讯录", "Contacts") } } private func permissionDescription(_ kind: AppPermissionKind) -> String { switch kind { case .calendar: return localized("允许创建和写入日历事项", "Allow creating and writing calendar events") case .reminders: return localized("允许创建提醒和待办", "Allow creating reminders and tasks") case .contacts: return localized("允许保存和更新联系人", "Allow saving and updating contacts") } } private func permissionStatusLabel(_ status: AppPermissionStatus) -> String { switch status { case .notDetermined: return localized("未请求", "Not Requested") case .denied: return localized("已拒绝", "Denied") case .restricted: return localized("受限制", "Restricted") case .granted: return localized("已授权", "Granted") } } private func permissionStatusDetail(_ status: AppPermissionStatus) -> String { switch status { case .notDetermined: return localized("首次使用时会弹出系统授权框", "The system permission dialog will appear on first use") case .denied: return localized("请到系统设置里手动开启权限", "Please enable this permission manually in Settings") case .restricted: return localized("当前设备限制了这项权限", "This permission is restricted on the current device") case .granted: return localized("可以直接执行相关 Skill", "Related skills can run directly") } } private func loadCurrentSettings() { selectedModelID = engine.config.selectedModelID maxTokens = Double(engine.config.maxTokens) topK = Double(engine.config.topK) topP = engine.config.topP temperature = engine.config.temperature systemPrompt = engine.config.systemPrompt refreshPermissionStatuses() } private func applySettings() { let modelChanged = engine.config.selectedModelID != selectedModelID engine.config.selectedModelID = selectedModelID engine.config.maxTokens = Int(maxTokens) engine.config.topK = Int(topK) engine.config.topP = topP engine.config.temperature = temperature engine.config.systemPrompt = systemPrompt // 同步采样参数到 LLM(下次生成立即生效) engine.applySamplingConfig() if modelChanged { engine.reloadModel() } } private func refreshPermissionStatuses() { permissionStatuses = engine.permissionStatuses() } private func requestPermission(_ kind: AppPermissionKind) { requestingPermission = kind Task { _ = await engine.requestPermission(kind) await MainActor.run { refreshPermissionStatuses() requestingPermission = nil } } } private func openAppSettings() { #if canImport(UIKit) guard let url = URL(string: UIApplication.openSettingsURLString) else { return } openURL(url) #endif } }