Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ora/Common/Constants/AppEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extension Notification.Name {
static let nextTab = Notification.Name("NextTab")
static let previousTab = Notification.Name("PreviousTab")
static let toggleToolbar = Notification.Name("ToggleToolbar")
static let toggleChatPanel = Notification.Name("ToggleChatPanel")
static let selectTabAtIndex = Notification.Name("SelectTabAtIndex") // userInfo: ["index": Int]

// Per-window settings/events
Expand Down
8 changes: 7 additions & 1 deletion ora/Common/Constants/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ enum KeyboardShortcuts {
category: "App",
defaultChord: KeyChord(keyEquivalent: .init("d"), modifiers: [.command, .shift])
)
static let toggleChatPanel = KeyboardShortcutDefinition(
id: "app.toggleChatPanel",
name: "Toggle Chat Panel",
category: "App",
defaultChord: KeyChord(keyEquivalent: .init("/"), modifiers: [.command])
)
}

/// All keyboard shortcut definitions
Expand Down Expand Up @@ -329,7 +335,7 @@ enum KeyboardShortcuts {
Developer.toggleDevTools, Developer.reloadIgnoringCache,

// App
App.quit, App.hide, App.preferences, App.toggleSidebar, App.toggleToolbar
App.quit, App.hide, App.preferences, App.toggleSidebar, App.toggleToolbar, App.toggleChatPanel
]

/// Get shortcuts grouped by category for settings display
Expand Down
3 changes: 0 additions & 3 deletions ora/Common/Utils/WindowFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,3 @@ enum WindowFactory {
return window
}
}



52 changes: 43 additions & 9 deletions ora/Modules/Browser/BrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ struct BrowserView: View {
@State private var isMouseOverSidebar = false
@StateObject private var sidebarFraction = FractionHolder.usingUserDefaults(0.2, key: "ui.sidebar.fraction")
@StateObject private var sidebarVisibility = SideHolder.usingUserDefaults(key: "ui.sidebar.visibility")
@StateObject private var chatPanelFraction = FractionHolder.usingUserDefaults(0.2, key: "ui.chatPanel.fraction")
@StateObject private var chatPanelVisibility = SideHolder.usingUserDefaults(key: "ui.chatPanel.visibility")

private func toggleSidebar() {
withAnimation(.spring(response: 0.2, dampingFraction: 1.0)) {
sidebarVisibility.toggle(.primary)
}
}

private func toggleChatPanel() {
withAnimation(.spring(response: 0.2, dampingFraction: 1.0)) {
chatPanelVisibility.toggle(.secondary)
}
}

private func toggleMaximizeWindow() {
window?.toggleMaximized()
}
Expand All @@ -32,16 +40,25 @@ struct BrowserView: View {
SidebarView(isFullscreen: isFullscreen)
},
right: {
if tabManager.activeTab != nil {
BrowserContentContainer(isFullscreen: isFullscreen, hideState: sidebarVisibility) {
webView
// Main content with optional chat panel
HSplit(
left: {
mainContentArea
},
right: {
ChatPanel()
.frame(minWidth: 295, maxWidth: 500)
}
} else {
// Start page (visible when no tab is active)
BrowserContentContainer(isFullscreen: isFullscreen, hideState: sidebarVisibility) {
HomeView(sidebarToggle: toggleSidebar)
}
}
)
.splitter { Splitter.invisible() }
.fraction(chatPanelFraction)
.hide(chatPanelVisibility)
.constraints(
minPFraction: 0.4,
minSFraction: 0.33,
priority: .left
)
.styling(hideSplitter: false)
}
)
.hide(sidebarVisibility)
Expand Down Expand Up @@ -150,6 +167,9 @@ struct BrowserView: View {
.onReceive(NotificationCenter.default.publisher(for: .toggleSidebar)) { _ in
toggleSidebar()
}
.onReceive(NotificationCenter.default.publisher(for: .toggleChatPanel)) { _ in
toggleChatPanel()
}
.onChange(of: downloadManager.isDownloadsPopoverOpen) { isOpen in
if sidebarVisibility.side == .primary {
if isOpen {
Expand Down Expand Up @@ -192,6 +212,20 @@ struct BrowserView: View {
}
}

@ViewBuilder
private var mainContentArea: some View {
if tabManager.activeTab != nil {
BrowserContentContainer(isFullscreen: isFullscreen, hideState: sidebarVisibility) {
webView
}
} else {
// Start page (visible when no tab is active)
BrowserContentContainer(isFullscreen: isFullscreen, hideState: sidebarVisibility) {
HomeView(sidebarToggle: toggleSidebar)
}
}
}

@ViewBuilder
private var webView: some View {
VStack(alignment: .leading, spacing: 0) {
Expand Down
179 changes: 179 additions & 0 deletions ora/Modules/Settings/Sections/AISettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import SwiftUI

struct AISettingsView: View {
@Environment(\.theme) var theme
@StateObject private var providerManager = AIProviderManager.shared
@State private var openAIAPIKey: String = ""
@State private var showAPIKeyField = false
@State private var isAPIKeyVisible = false

var body: some View {
SettingsContainer(maxContentWidth: 760) {
Form {
VStack(alignment: .leading, spacing: 20) {
// Header
VStack(alignment: .leading, spacing: 8) {
Text("AI Integration")
.font(.headline)

Text("Configure AI providers to enable chat functionality")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(12)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 8))

// Provider Status
VStack(alignment: .leading, spacing: 12) {
Text("Provider Status")
.font(.subheadline)
.fontWeight(.medium)

ForEach(providerManager.providers, id: \.name) { provider in
providerStatusRow(provider)
}
}

// OpenAI Configuration
VStack(alignment: .leading, spacing: 12) {
Text("OpenAI Configuration")
.font(.subheadline)
.fontWeight(.medium)

VStack(alignment: .leading, spacing: 8) {
Text("API Key")
.font(.caption)
.foregroundColor(.secondary)

HStack {
if isAPIKeyVisible {
TextField("", text: $openAIAPIKey)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
} else {
SecureField("", text: $openAIAPIKey)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
}

Button(action: { isAPIKeyVisible.toggle() }) {
Image(systemName: isAPIKeyVisible ? "eye.slash" : "eye")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())

Button(action: saveOpenAIKey) {
Text("Save")
}
.disabled(openAIAPIKey.isEmpty)

if KeychainService.shared.hasOpenAIKey() {
Button(action: clearOpenAIKey) {
Text("Remove")
}
.foregroundColor(.red)
}
}

Text("Get your API key from [OpenAI Platform](https://platform.openai.com/api-keys)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(12)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 8))
}

// Usage Information
VStack(alignment: .leading, spacing: 8) {
Text("About API Usage")
.font(.subheadline)
.fontWeight(.medium)

VStack(alignment: .leading, spacing: 4) {
Text("• Your API keys are stored securely in macOS Keychain")
Text("• Keys are encrypted and never sent to our servers")
Text("• API requests are made directly from your browser to the provider")
Text("• You will be charged by the provider based on your usage")
Text("• Remove your API key anytime to disable AI features")
}
.font(.caption)
.foregroundColor(.secondary)
}
.padding(12)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 8))
}
.padding(.vertical, 8)
}
}
.onAppear {
loadAPIKeys()
}
}

private func providerStatusRow(_ provider: AIProvider) -> some View {
HStack {
Image(systemName: provider.isConfigured ? "checkmark.circle.fill" : "exclamationmark.circle.fill")
.foregroundColor(provider.isConfigured ? .green : .orange)

VStack(alignment: .leading, spacing: 4) {
Text(provider.name)
.font(.subheadline)

Text(provider.isConfigured ? "Configured" : "Requires API key")
.font(.caption)
.foregroundColor(.secondary)

if provider.isConfigured {
Text("Models: " + provider.models.map(\.displayName).joined(separator: ", "))
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(2)
}
}

Spacer()

if provider.isConfigured {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.system(size: 16))
}
}
.padding(8)
.background(
provider.isConfigured ? Color.green.opacity(0.1) : Color.orange.opacity(0.1),
in: RoundedRectangle(cornerRadius: 6)
)
}

private func loadAPIKeys() {
openAIAPIKey = KeychainService.shared.getOpenAIKey() ?? ""
}

private func saveOpenAIKey() {
let success = KeychainService.shared.storeOpenAIKey(openAIAPIKey)

if success {
providerManager.refreshProviders()
// Show success feedback
NSSound(named: NSSound.Name("Glass"))?.play()
} else {
// Show error feedback
NSSound.beep()
print("Failed to save API key to keychain")
}
}

private func clearOpenAIKey() {
let success = KeychainService.shared.deleteOpenAIKey()

if success {
openAIAPIKey = ""
providerManager.refreshProviders()
NSSound(named: NSSound.Name("Glass"))?.play()
} else {
NSSound.beep()
print("Failed to remove API key from keychain")
}
}
}
5 changes: 2 additions & 3 deletions ora/Modules/Settings/Sections/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ struct GeneralSettingsView: View {
.padding(12)
.background(theme.solidWindowBackgroundColor)
.cornerRadius(8)

if !defaultBrowserManager.isDefault {


if !defaultBrowserManager.isDefault {
HStack {
Text("Born for your Mac. Make Ora your default browser.")
Spacer()
Expand Down
8 changes: 7 additions & 1 deletion ora/Modules/Settings/SettingsContentView.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

enum SettingsTab: Hashable {
case general, spaces, privacySecurity, shortcuts, searchEngines
case general, spaces, privacySecurity, shortcuts, searchEngines, ai

var title: String {
switch self {
Expand All @@ -10,6 +10,7 @@ enum SettingsTab: Hashable {
case .privacySecurity: return "Privacy"
case .shortcuts: return "Shortcuts"
case .searchEngines: return "Search"
case .ai: return "AI"
}
}

Expand All @@ -20,6 +21,7 @@ enum SettingsTab: Hashable {
case .privacySecurity: return "lock.shield"
case .shortcuts: return "command"
case .searchEngines: return "magnifyingglass"
case .ai: return "brain"
}
}
}
Expand Down Expand Up @@ -50,6 +52,10 @@ struct SettingsContentView: View {
SearchEngineSettingsView()
.tabItem { Label(SettingsTab.searchEngines.title, systemImage: SettingsTab.searchEngines.symbol) }
.tag(SettingsTab.searchEngines)

AISettingsView()
.tabItem { Label(SettingsTab.ai.title, systemImage: SettingsTab.ai.symbol) }
.tag(SettingsTab.ai)
}
.tabViewStyle(.automatic)
.frame(width: 600, height: 350)
Expand Down
5 changes: 5 additions & 0 deletions ora/OraCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ struct OraCommands: Commands {
}
.keyboardShortcut(KeyboardShortcuts.App.toggleSidebar.keyboardShortcut)

Button("Toggle Chat Panel") {
NotificationCenter.default.post(name: .toggleChatPanel, object: NSApp.keyWindow)
}
.keyboardShortcut(KeyboardShortcuts.App.toggleChatPanel.keyboardShortcut)

Divider()

Button("Toggle Full URL") { NotificationCenter.default.post(name: .toggleFullURL, object: NSApp.keyWindow) }
Expand Down
Loading
Loading