diff --git a/oAI/Views/Main/ChatView.swift b/oAI/Views/Main/ChatView.swift index 74c6943..48b01b7 100644 --- a/oAI/Views/Main/ChatView.swift +++ b/oAI/Views/Main/ChatView.swift @@ -38,7 +38,10 @@ struct ChatView: View { provider: viewModel.currentProvider, model: viewModel.selectedModel, onModelSelect: onModelSelect, - onProviderChange: onProviderChange + onProviderChange: onProviderChange, + conversationName: viewModel.currentConversationName, + hasUnsavedChanges: viewModel.hasUnsavedChanges, + onQuickSave: viewModel.quickSave ) // Messages diff --git a/oAI/Views/Main/FooterView.swift b/oAI/Views/Main/FooterView.swift index 178be1d..85085d7 100644 --- a/oAI/Views/Main/FooterView.swift +++ b/oAI/Views/Main/FooterView.swift @@ -93,15 +93,6 @@ struct FooterView: View { } #endif - // Save indicator (only when chat has messages) - if stats.messageCount > 0 { - SaveIndicator( - conversationName: conversationName, - hasUnsavedChanges: hasUnsavedChanges, - onSave: onQuickSave - ) - } - // Update available badge (shows only when an update exists — no version number) #if os(macOS) UpdateBadge() diff --git a/oAI/Views/Main/HeaderView.swift b/oAI/Views/Main/HeaderView.swift index 4280a9d..b2b8087 100644 --- a/oAI/Views/Main/HeaderView.swift +++ b/oAI/Views/Main/HeaderView.swift @@ -31,109 +31,24 @@ struct HeaderView: View { let model: ModelInfo? let onModelSelect: () -> Void let onProviderChange: (Settings.Provider) -> Void + var conversationName: String? = nil + var hasUnsavedChanges: Bool = false + var onQuickSave: (() -> Void)? = nil private let settings = SettingsService.shared private let registry = ProviderRegistry.shared var body: some View { - HStack(spacing: 12) { - // Provider picker dropdown — only shows configured providers - Menu { - ForEach(registry.configuredProviders, id: \.self) { p in - Button { - onProviderChange(p) - } label: { - HStack { - Image(systemName: p.iconName) - Text(p.displayName) - if p == provider { - Image(systemName: "checkmark") - } - } - } - } - } label: { - HStack(spacing: 4) { - Image(systemName: provider.iconName) - .font(.system(size: settings.guiTextSize - 2)) - Text(provider.displayName) - .font(.system(size: settings.guiTextSize - 2, weight: .medium)) - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 8)) - .opacity(0.7) - } - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.providerColor(provider)) - .cornerRadius(4) - } - .menuStyle(.borderlessButton) - .fixedSize() - .help("Switch provider") - - // Model name (clickable → model selector) - Button(action: onModelSelect) { - if let model = model { - HStack(spacing: 6) { - Text(model.name) - .font(.system(size: settings.guiTextSize, weight: .medium)) - .foregroundColor(.oaiPrimary) - - // Capability badges - HStack(spacing: 3) { - if model.capabilities.vision { - Image(systemName: "eye") - .font(.system(size: 9)) - .foregroundColor(.oaiSecondary) - } - if model.capabilities.tools { - Image(systemName: "wrench") - .font(.system(size: 9)) - .foregroundColor(.oaiSecondary) - } - if model.capabilities.online { - Image(systemName: "globe") - .font(.system(size: 9)) - .foregroundColor(.oaiSecondary) - } - if model.capabilities.imageGeneration { - Image(systemName: "paintbrush") - .font(.system(size: 9)) - .foregroundColor(.oaiSecondary) - } - } - - Image(systemName: "chevron.down") - .font(.caption2) - .foregroundColor(.oaiSecondary) - } - } else { - HStack(spacing: 4) { - Text("No model selected") - .font(.system(size: settings.guiTextSize)) - .foregroundColor(.oaiSecondary) - Image(systemName: "chevron.down") - .font(.caption2) - .foregroundColor(.oaiSecondary) - } - } - } - .buttonStyle(.plain) - .help("Select model") - - // Favourite star - if let model = model { - let isFav = settings.favoriteModelIds.contains(model.id) - Button(action: { settings.toggleFavoriteModel(model.id) }) { - Image(systemName: isFav ? "star.fill" : "star") - .font(.system(size: settings.guiTextSize - 3)) - .foregroundColor(isFav ? .yellow : .oaiSecondary) - } - .buttonStyle(.plain) - .help(isFav ? "Remove from favorites" : "Add to favorites") + ZStack { + // Left: provider + model + star + HStack(spacing: 12) { + providerMenu + modelButton + starButton + Spacer() } - Spacer() + // Center: conversation title (macOS document-title style) + conversationTitle } .padding(.horizontal, 16) .padding(.vertical, 8) @@ -145,6 +60,119 @@ struct HeaderView: View { alignment: .bottom ) } + + // MARK: - Conversation title (center) + + @ViewBuilder + private var conversationTitle: some View { + if let name = conversationName { + Button(action: { if hasUnsavedChanges { onQuickSave?() } }) { + HStack(spacing: 5) { + if hasUnsavedChanges { + Circle() + .fill(Color.orange) + .frame(width: 6, height: 6) + } + Text(name) + .font(.system(size: settings.guiTextSize - 1, weight: .medium)) + .foregroundColor(.oaiPrimary) + .lineLimit(1) + .frame(maxWidth: 300) + } + } + .buttonStyle(.plain) + .disabled(!hasUnsavedChanges) + .help(hasUnsavedChanges ? "Unsaved changes — click to save" : "Saved") + .animation(.easeInOut(duration: 0.2), value: hasUnsavedChanges) + .animation(.easeInOut(duration: 0.2), value: name) + } + } + + // MARK: - Subviews (extracted so ZStack stays readable) + + private var providerMenu: some View { + Menu { + ForEach(registry.configuredProviders, id: \.self) { p in + Button { + onProviderChange(p) + } label: { + HStack { + Image(systemName: p.iconName) + Text(p.displayName) + if p == provider { Image(systemName: "checkmark") } + } + } + } + } label: { + HStack(spacing: 4) { + Image(systemName: provider.iconName) + .font(.system(size: settings.guiTextSize - 2)) + Text(provider.displayName) + .font(.system(size: settings.guiTextSize - 2, weight: .medium)) + Image(systemName: "chevron.up.chevron.down") + .font(.system(size: 8)) + .opacity(0.7) + } + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.providerColor(provider)) + .cornerRadius(4) + } + .menuStyle(.borderlessButton) + .fixedSize() + .help("Switch provider") + } + + private var modelButton: some View { + Button(action: onModelSelect) { + if let model = model { + HStack(spacing: 6) { + Text(model.name) + .font(.system(size: settings.guiTextSize, weight: .medium)) + .foregroundColor(.oaiPrimary) + HStack(spacing: 3) { + if model.capabilities.vision { + Image(systemName: "eye").font(.system(size: 9)).foregroundColor(.oaiSecondary) + } + if model.capabilities.tools { + Image(systemName: "wrench").font(.system(size: 9)).foregroundColor(.oaiSecondary) + } + if model.capabilities.online { + Image(systemName: "globe").font(.system(size: 9)).foregroundColor(.oaiSecondary) + } + if model.capabilities.imageGeneration { + Image(systemName: "paintbrush").font(.system(size: 9)).foregroundColor(.oaiSecondary) + } + } + Image(systemName: "chevron.down").font(.caption2).foregroundColor(.oaiSecondary) + } + } else { + HStack(spacing: 4) { + Text("No model selected") + .font(.system(size: settings.guiTextSize)) + .foregroundColor(.oaiSecondary) + Image(systemName: "chevron.down").font(.caption2).foregroundColor(.oaiSecondary) + } + } + } + .buttonStyle(.plain) + .help("Select model") + } + + @ViewBuilder + private var starButton: some View { + if let model = model { + let isFav = settings.favoriteModelIds.contains(model.id) + Button(action: { settings.toggleFavoriteModel(model.id) }) { + Image(systemName: isFav ? "star.fill" : "star") + .font(.system(size: settings.guiTextSize - 3)) + .foregroundColor(isFav ? .yellow : .oaiSecondary) + } + .buttonStyle(.plain) + .help(isFav ? "Remove from favorites" : "Add to favorites") + } + } } // MARK: - Status Pills (used by SidebarView)