Files
rune 75e5b40f45 Persist sidebar visibility; add language-matching rule to default prompt
- ContentView now reads/writes SettingsService.sidebarVisible so the
  NavigationSplitView sidebar's shown/hidden state survives relaunch,
  matching the existing window size/position persistence.
- Default system prompt gains a rule: always reply in the user's
  language, even for requests (e.g. translation) targeting another one.
2026-07-19 16:01:04 +02:00

177 lines
6.2 KiB
Swift

//
// ContentView.swift
// oAI
//
// Root navigation container — NavigationSplitView with collapsible sidebar
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import SwiftUI
#if os(macOS)
import Darwin // uname, sysctlbyname
#endif
struct ContentView: View {
@Environment(ChatViewModel.self) var chatViewModel
private var updateService = UpdateCheckService.shared
@State private var columnVisibility: NavigationSplitViewVisibility =
SettingsService.shared.sidebarVisible ? .all : .detailOnly
@State private var showIntelWarning = false
var body: some View {
@Bindable var vm = chatViewModel
NavigationSplitView(columnVisibility: $columnVisibility) {
SidebarView()
.navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 340)
} detail: {
ChatView(
onModelSelect: { chatViewModel.showModelSelector = true },
onProviderChange: { newProvider in
chatViewModel.changeProvider(newProvider)
}
)
}
.frame(minWidth: 860, minHeight: 560)
.onChange(of: columnVisibility) { _, newValue in
SettingsService.shared.sidebarVisible = newValue != .detailOnly
}
#if os(macOS)
.onAppear {
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
checkIntelWarning()
}
.onKeyPress(.return, phases: .down) { press in
if press.modifiers.contains(.command) {
chatViewModel.sendMessage()
return .handled
}
return .ignored
}
#endif
.sheet(isPresented: $vm.showModelSelector) {
ModelSelectorView(
models: chatViewModel.availableModels,
selectedModel: chatViewModel.selectedModel,
onSelect: { model in
let oldModel = chatViewModel.selectedModel
chatViewModel.selectModel(model)
chatViewModel.showModelSelector = false
Task {
await chatViewModel.onModelSwitch(from: oldModel, to: model)
}
}
)
.task {
if chatViewModel.availableModels.count <= 10 {
await chatViewModel.loadAvailableModels()
}
}
}
.sheet(isPresented: $vm.showSettings, onDismiss: {
chatViewModel.syncFromSettings()
}) {
SettingsView(chatViewModel: chatViewModel)
}
.sheet(isPresented: $vm.showStats) {
StatsView(
stats: chatViewModel.sessionStats,
model: chatViewModel.selectedModel,
provider: chatViewModel.currentProvider
)
}
.sheet(isPresented: $vm.showHelp) {
HelpView()
}
.sheet(isPresented: $vm.showCredits) {
CreditsView(provider: chatViewModel.currentProvider)
}
.sheet(isPresented: $vm.showConversations) {
ConversationListView(
onLoad: { conversation in
chatViewModel.loadConversation(conversation)
},
onRename: { id, newName in
chatViewModel.didRenameConversation(id: id, newName: newName)
}
)
}
.sheet(item: $vm.modelInfoTarget) { model in
ModelInfoView(model: model)
}
.sheet(isPresented: $vm.showHistory) {
HistoryView(onSelect: { input in
chatViewModel.inputText = input
})
}
.alert("Intel Mac Support Ending", isPresented: $showIntelWarning) {
Button("Got It") {
UserDefaults.standard.set(true, forKey: "hasShownIntelWarning")
}
} message: {
Text("oAI v2.4 is the last version to support Intel Macs and Rosetta. Starting with macOS 28, oAI will require Apple Silicon. Consider upgrading your Mac to continue receiving updates.")
}
.alert("Software Update", isPresented: Binding(
get: { updateService.manualCheckMessage != nil },
set: { if !$0 { updateService.manualCheckMessage = nil } }
)) {
if updateService.updateAvailable {
if let url = updateService.downloadURL {
Button("Download v\(updateService.latestVersion ?? "")") {
NSWorkspace.shared.open(url)
}
}
Button("Release Page") { updateService.openReleasesPage() }
Button("Later", role: .cancel) { }
} else {
Button("OK", role: .cancel) { }
}
} message: {
Text(updateService.manualCheckMessage ?? "")
}
}
#if os(macOS)
private func checkIntelWarning() {
guard !UserDefaults.standard.bool(forKey: "hasShownIntelWarning") else { return }
guard isIntelNative || isRosetta else { return }
showIntelWarning = true
}
private var isIntelNative: Bool {
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) {
String(cString: $0.bindMemory(to: CChar.self).baseAddress!)
}
return machine.contains("x86_64")
}
private var isRosetta: Bool {
var ret: Int32 = 0
var size = MemoryLayout<Int32>.size
sysctlbyname("sysctl.proc_translated", &ret, &size, nil, 0)
return ret == 1
}
#endif
}
#Preview {
ContentView()
.environment(ChatViewModel())
}