First public release v2.3.1
This commit is contained in:
@@ -126,7 +126,12 @@ struct ChatView: View {
|
||||
)
|
||||
|
||||
// Footer
|
||||
FooterView(stats: viewModel.sessionStats)
|
||||
FooterView(
|
||||
stats: viewModel.sessionStats,
|
||||
conversationName: viewModel.currentConversationName,
|
||||
hasUnsavedChanges: viewModel.hasUnsavedChanges,
|
||||
onQuickSave: viewModel.quickSave
|
||||
)
|
||||
}
|
||||
.background(Color.oaiBackground)
|
||||
.sheet(isPresented: $viewModel.showShortcuts) {
|
||||
|
||||
@@ -46,6 +46,9 @@ struct ContentView: View {
|
||||
}
|
||||
.frame(minWidth: 640, minHeight: 400)
|
||||
#if os(macOS)
|
||||
.onAppear {
|
||||
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
|
||||
}
|
||||
.onKeyPress(.return, phases: .down) { press in
|
||||
if press.modifiers.contains(.command) {
|
||||
chatViewModel.sendMessage()
|
||||
@@ -157,8 +160,7 @@ struct ContentView: View {
|
||||
Button(action: { chatViewModel.showStats = true }) {
|
||||
ToolbarLabel(title: "Stats", systemImage: "chart.bar", showLabels: showLabels, scale: scale)
|
||||
}
|
||||
.keyboardShortcut("s", modifiers: .command)
|
||||
.help("Session statistics (Cmd+S)")
|
||||
.help("Session statistics")
|
||||
|
||||
Button(action: { chatViewModel.showCredits = true }) {
|
||||
ToolbarLabel(title: "Credits", systemImage: "creditcard", showLabels: showLabels, scale: scale)
|
||||
|
||||
@@ -27,7 +27,20 @@ import SwiftUI
|
||||
|
||||
struct FooterView: View {
|
||||
let stats: SessionStats
|
||||
|
||||
let conversationName: String?
|
||||
let hasUnsavedChanges: Bool
|
||||
let onQuickSave: (() -> Void)?
|
||||
|
||||
init(stats: SessionStats,
|
||||
conversationName: String? = nil,
|
||||
hasUnsavedChanges: Bool = false,
|
||||
onQuickSave: (() -> Void)? = nil) {
|
||||
self.stats = stats
|
||||
self.conversationName = conversationName
|
||||
self.hasUnsavedChanges = hasUnsavedChanges
|
||||
self.onQuickSave = onQuickSave
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 20) {
|
||||
// Session summary
|
||||
@@ -58,9 +71,18 @@ struct FooterView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
// Save indicator (only when chat has messages)
|
||||
if stats.messageCount > 0 {
|
||||
SaveIndicator(
|
||||
conversationName: conversationName,
|
||||
hasUnsavedChanges: hasUnsavedChanges,
|
||||
onSave: onQuickSave
|
||||
)
|
||||
}
|
||||
|
||||
// Shortcuts hint
|
||||
#if os(macOS)
|
||||
Text("⌘M Model • ⌘K Clear • ⌘S Stats")
|
||||
Text("⌘N New • ⌘M Model • ⌘⇧S Save")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.oaiSecondary)
|
||||
#endif
|
||||
@@ -77,6 +99,62 @@ struct FooterView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SaveIndicator: View {
|
||||
let conversationName: String?
|
||||
let hasUnsavedChanges: Bool
|
||||
let onSave: (() -> Void)?
|
||||
|
||||
private let guiSize = SettingsService.shared.guiTextSize
|
||||
|
||||
private var isSaved: Bool { conversationName != nil }
|
||||
private var isModified: Bool { isSaved && hasUnsavedChanges }
|
||||
private var isUnsaved: Bool { !isSaved }
|
||||
|
||||
private var label: String {
|
||||
if let name = conversationName {
|
||||
return name.count > 20 ? String(name.prefix(20)) + "…" : name
|
||||
}
|
||||
return "Unsaved"
|
||||
}
|
||||
|
||||
private var icon: String {
|
||||
if isModified { return "circle.fill" }
|
||||
if isSaved { return "checkmark.circle.fill" }
|
||||
return "exclamationmark.circle"
|
||||
}
|
||||
|
||||
private var color: Color {
|
||||
if isModified { return .orange }
|
||||
if isSaved { return .green }
|
||||
return .secondary
|
||||
}
|
||||
|
||||
private var tooltip: String {
|
||||
if isModified { return "Click to re-save \"\(conversationName ?? "")\"" }
|
||||
if isSaved { return "Saved — no changes" }
|
||||
return "Not saved — use /save <name>"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: { if isModified { onSave?() } }) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: guiSize - 3))
|
||||
.foregroundColor(color)
|
||||
Text(label)
|
||||
.font(.system(size: guiSize - 2))
|
||||
.foregroundColor(isUnsaved ? .secondary : .oaiPrimary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(tooltip)
|
||||
.disabled(!isModified)
|
||||
.opacity(isUnsaved ? 0.6 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.2), value: conversationName)
|
||||
.animation(.easeInOut(duration: 0.2), value: hasUnsavedChanges)
|
||||
}
|
||||
}
|
||||
|
||||
struct FooterItem: View {
|
||||
let icon: String
|
||||
let label: String
|
||||
@@ -177,14 +255,17 @@ struct SyncStatusFooter: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
let stats = SessionStats(
|
||||
totalInputTokens: 1250,
|
||||
totalOutputTokens: 3420,
|
||||
totalCost: 0.0152,
|
||||
messageCount: 12
|
||||
)
|
||||
return VStack(spacing: 0) {
|
||||
Spacer()
|
||||
FooterView(stats: SessionStats(
|
||||
totalInputTokens: 1250,
|
||||
totalOutputTokens: 3420,
|
||||
totalCost: 0.0152,
|
||||
messageCount: 12
|
||||
))
|
||||
FooterView(stats: stats, conversationName: nil, hasUnsavedChanges: true)
|
||||
FooterView(stats: stats, conversationName: "My Project", hasUnsavedChanges: true)
|
||||
FooterView(stats: stats, conversationName: "My Project", hasUnsavedChanges: false)
|
||||
}
|
||||
.background(Color.oaiBackground)
|
||||
}
|
||||
|
||||
@@ -279,6 +279,9 @@ struct MessageRow: View {
|
||||
private func loadStarredState() {
|
||||
if let metadata = try? DatabaseService.shared.getMessageMetadata(messageId: message.id) {
|
||||
isStarred = metadata.user_starred == 1
|
||||
} else {
|
||||
// Unsaved message — use in-memory flag on the Message struct
|
||||
isStarred = message.isStarred
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user