2.4 #6
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
+125
-97
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user