Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@ extension DependencyContainer {
userPreferencesRepository: userPreferencesRepository
)
}

func makeDragDropViewModel() -> DragDropViewModel {
DragDropViewModel(
transcriptionService: transcriptionService,
llmService: llmService,
userPreferencesRepository: userPreferencesRepository,
recordingFileManagerHelper: recordingFileManagerHelper
)
}
}
2 changes: 2 additions & 0 deletions Recap/DependencyContainer/DependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final class DependencyContainer {
lazy var appSelectionCoordinator: AppSelectionCoordinatorType = makeAppSelectionCoordinator()
lazy var keychainService: KeychainServiceType = makeKeychainService()
lazy var keychainAPIValidator: KeychainAPIValidatorType = makeKeychainAPIValidator()
lazy var dragDropViewModel: DragDropViewModel = makeDragDropViewModel()

init(inMemory: Bool = false) {
self.inMemory = inMemory
Expand All @@ -57,6 +58,7 @@ final class DependencyContainer {
onboardingViewModel: onboardingViewModel,
summaryViewModel: summaryViewModel,
generalSettingsViewModel: generalSettingsViewModel,
dragDropViewModel: dragDropViewModel,
userPreferencesRepository: userPreferencesRepository,
meetingDetectionService: meetingDetectionService
)
Expand Down
61 changes: 61 additions & 0 deletions Recap/MenuBar/Manager/MenuBarPanelManager+DragDrop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import AppKit
import SwiftUI

extension MenuBarPanelManager {
func createDragDropPanel() -> SlidingPanel? {
let contentView = DragDropView(
viewModel: dragDropViewModel
) { [weak self] in
self?.hideDragDropPanel()
}
let hostingController = NSHostingController(rootView: contentView)
hostingController.view.wantsLayer = true
hostingController.view.layer?.cornerRadius = 12

let newPanel = SlidingPanel(
contentViewController: hostingController,
shouldCloseOnOutsideClick: false
)
newPanel.panelDelegate = self
return newPanel
}

func positionDragDropPanel(_ panel: NSPanel) {
guard let statusButton = statusBarManager.statusButton,
let statusWindow = statusButton.window,
let screen = statusWindow.screen
else { return }

let screenFrame = screen.frame
let dragDropX = screenFrame.maxX - (initialSize.width * 2) - (panelOffset * 2) - panelSpacing
let panelY = screenFrame.maxY - menuBarHeight - initialSize.height - panelSpacing

panel.setFrame(
NSRect(x: dragDropX, y: panelY, width: initialSize.width, height: initialSize.height),
display: false
)
}

func showDragDropPanel() {
if dragDropPanel == nil {
dragDropPanel = createDragDropPanel()
}

guard let dragDropPanel = dragDropPanel else { return }

positionDragDropPanel(dragDropPanel)
dragDropPanel.contentView?.wantsLayer = true

PanelAnimator.slideIn(panel: dragDropPanel) { [weak self] in
self?.isDragDropVisible = true
}
}

func hideDragDropPanel() {
guard let dragDropPanel = dragDropPanel else { return }

PanelAnimator.slideOut(panel: dragDropPanel) { [weak self] in
self?.isDragDropVisible = false
}
}
}
19 changes: 19 additions & 0 deletions Recap/MenuBar/Manager/MenuBarPanelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
var settingsPanel: SlidingPanel?
var summaryPanel: SlidingPanel?
var recapsPanel: SlidingPanel?
var dragDropPanel: SlidingPanel?
var previousRecapsWindowManager: RecapsWindowManager?

var isVisible = false
var isSettingsVisible = false
var isSummaryVisible = false
var isRecapsVisible = false
var isDragDropVisible = false
var isPreviousRecapsVisible = false

let initialSize = CGSize(width: 485, height: 500)
Expand All @@ -34,6 +36,7 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
let onboardingViewModel: OnboardingViewModel
let summaryViewModel: SummaryViewModel
let generalSettingsViewModel: GeneralSettingsViewModel
let dragDropViewModel: DragDropViewModel
let userPreferencesRepository: UserPreferencesRepositoryType
let meetingDetectionService: any MeetingDetectionServiceType
private let logger = Logger(
Expand All @@ -51,6 +54,7 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
onboardingViewModel: OnboardingViewModel,
summaryViewModel: SummaryViewModel,
generalSettingsViewModel: GeneralSettingsViewModel,
dragDropViewModel: DragDropViewModel,
userPreferencesRepository: UserPreferencesRepositoryType,
meetingDetectionService: any MeetingDetectionServiceType
) {
Expand All @@ -62,6 +66,7 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
self.onboardingViewModel = onboardingViewModel
self.summaryViewModel = summaryViewModel
self.generalSettingsViewModel = generalSettingsViewModel
self.dragDropViewModel = dragDropViewModel
self.userPreferencesRepository = userPreferencesRepository
self.meetingDetectionService = meetingDetectionService
self.previousRecapsViewModel = previousRecapsViewModel
Expand Down Expand Up @@ -193,6 +198,7 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
if isSettingsVisible { hideSettingsPanel() }
if isSummaryVisible { hideSummaryPanel() }
if isRecapsVisible { hideRecapsPanel() }
if isDragDropVisible { hideDragDropPanel() }
if isPreviousRecapsVisible { hidePreviousRecapsWindow() }
}

Expand All @@ -210,6 +216,7 @@ final class MenuBarPanelManager: MenuBarPanelManagerType, ObservableObject {
panel = nil
settingsPanel = nil
recapsPanel = nil
dragDropPanel = nil
}
}

Expand Down Expand Up @@ -259,6 +266,18 @@ extension MenuBarPanelManager: StatusBarDelegate {
)
}

func dragDropRequested() {
// Hide main panel and show only drag & drop panel
if isVisible {
hidePanel()
}
toggleSidePanel(
isVisible: isDragDropVisible,
show: showDragDropPanel,
hide: hideDragDropPanel
)
}

func quitRequested() {
NSApplication.shared.terminate(nil)
}
Expand Down
13 changes: 13 additions & 0 deletions Recap/MenuBar/Manager/StatusBar/StatusBarManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ protocol StatusBarDelegate: AnyObject {
func stopRecordingRequested()
func settingsRequested()
func recapsRequested()
func dragDropRequested()
}

final class StatusBarManager: StatusBarManagerType {
Expand Down Expand Up @@ -138,6 +139,11 @@ final class StatusBarManager: StatusBarManagerType {
title: "Recaps", action: #selector(recapsMenuItemClicked), keyEquivalent: "")
recapsItem.target = self

// Drag & Drop menu item
let dragDropItem = NSMenuItem(
title: "Drag & Drop", action: #selector(dragDropMenuItemClicked), keyEquivalent: "")
dragDropItem.target = self

// Settings menu item
let settingsItem = NSMenuItem(
title: "Settings", action: #selector(settingsMenuItemClicked), keyEquivalent: "")
Expand All @@ -150,6 +156,7 @@ final class StatusBarManager: StatusBarManagerType {

mainMenu.addItem(recordingItem)
mainMenu.addItem(recapsItem)
mainMenu.addItem(dragDropItem)
mainMenu.addItem(settingsItem)
mainMenu.addItem(NSMenuItem.separator())
mainMenu.addItem(quitItem)
Expand Down Expand Up @@ -197,6 +204,12 @@ final class StatusBarManager: StatusBarManagerType {
}
}

@objc private func dragDropMenuItemClicked() {
DispatchQueue.main.async { [weak self] in
self?.delegate?.dragDropRequested()
}
}

@objc private func quitMenuItemClicked() {
DispatchQueue.main.async { [weak self] in
self?.delegate?.quitRequested()
Expand Down
5 changes: 4 additions & 1 deletion Recap/MenuBar/SlidingPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ protocol SlidingPanelDelegate: AnyObject {
final class SlidingPanel: NSPanel, SlidingPanelType {
weak var panelDelegate: SlidingPanelDelegate?
private var eventMonitor: Any?
var shouldCloseOnOutsideClick: Bool = true

init(contentViewController: NSViewController) {
init(contentViewController: NSViewController, shouldCloseOnOutsideClick: Bool = true) {
self.shouldCloseOnOutsideClick = shouldCloseOnOutsideClick
super.init(
contentRect: .zero,
styleMask: [.borderless, .nonactivatingPanel],
Expand Down Expand Up @@ -78,6 +80,7 @@ final class SlidingPanel: NSPanel, SlidingPanelType {
}

private func handleGlobalClick(_ event: NSEvent) {
guard shouldCloseOnOutsideClick else { return }
let globalLocation = NSEvent.mouseLocation
if !self.frame.contains(globalLocation) {
panelDelegate?.panelDidReceiveClickOutside()
Expand Down
56 changes: 54 additions & 2 deletions Recap/Services/LLM/LLMService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ final class LLMService: LLMServiceType {

func initializeProviders() {
let ollamaProvider = OllamaProvider()
let openRouterProvider = OpenRouterProvider()

// Get OpenAI credentials from keychain
// Get credentials from keychain
let keychainService = KeychainService()
let openRouterApiKey = try? keychainService.retrieveOpenRouterAPIKey()
let openAIApiKey = try? keychainService.retrieveOpenAIAPIKey()
let openAIEndpoint = try? keychainService.retrieveOpenAIEndpoint()

let openRouterProvider = OpenRouterProvider(apiKey: openRouterApiKey)
let openAIProvider = OpenAIProvider(
apiKey: openAIApiKey,
endpoint: openAIEndpoint ?? "https://api.openai.com/v1"
Expand Down Expand Up @@ -73,6 +75,56 @@ final class LLMService: LLMServiceType {
}
}

func reinitializeProviders() {
// Cancel any existing subscriptions
cancellables.removeAll()

// Get fresh credentials from keychain
let keychainService = KeychainService()
let openRouterApiKey = try? keychainService.retrieveOpenRouterAPIKey()
let openAIApiKey = try? keychainService.retrieveOpenAIAPIKey()
let openAIEndpoint = try? keychainService.retrieveOpenAIEndpoint()

// Create new provider instances with updated credentials
let ollamaProvider = OllamaProvider()
let openRouterProvider = OpenRouterProvider(apiKey: openRouterApiKey)
let openAIProvider = OpenAIProvider(
apiKey: openAIApiKey,
endpoint: openAIEndpoint ?? "https://api.openai.com/v1"
)

availableProviders = [ollamaProvider, openRouterProvider, openAIProvider]

// Update current provider
Task {
do {
let preferences = try await userPreferencesRepository.getOrCreatePreferences()
setCurrentProvider(preferences.selectedProvider)
} catch {
setCurrentProvider(.default)
}
}

// Re-setup availability monitoring
Publishers.CombineLatest3(
ollamaProvider.availabilityPublisher,
openRouterProvider.availabilityPublisher,
openAIProvider.availabilityPublisher
)
.map { ollamaAvailable, openRouterAvailable, openAIAvailable in
ollamaAvailable || openRouterAvailable || openAIAvailable
}
.sink { [weak self] isAnyProviderAvailable in
self?.isProviderAvailable = isAnyProviderAvailable
}
.store(in: &cancellables)

// Refresh models from providers
Task {
try? await refreshModelsFromProviders()
}
}

func refreshModelsFromProviders() async throws {
var allModelInfos: [LLMModelInfo] = []

Expand Down
1 change: 1 addition & 0 deletions Recap/Services/LLM/LLMServiceType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocol LLMServiceType: AnyObject {
var providerAvailabilityPublisher: AnyPublisher<Bool, Never> { get }

func initializeProviders()
func reinitializeProviders()
func refreshModelsFromProviders() async throws
func getAvailableModels() async throws -> [LLMModelInfo]
func getSelectedModel() async throws -> LLMModelInfo?
Expand Down
Loading
Loading