Skip to content
Open
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
2 changes: 1 addition & 1 deletion Wisp/App/WispApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ struct WispApp: App {
browserCoordinator.authToken = apiClient.spritesToken
}
}
.modelContainer(for: [SpriteChat.self, SpriteSession.self])
.modelContainer(for: [SpriteChat.self, SpriteSession.self, QuickMessage.self])
}
}
15 changes: 15 additions & 0 deletions Wisp/Models/Local/QuickMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation
import SwiftData

@Model
final class QuickMessage {
var id: UUID
var text: String
var createdAt: Date

init(text: String) {
self.id = UUID()
self.text = text
self.createdAt = Date()
}
}
121 changes: 121 additions & 0 deletions Wisp/Views/Settings/QuickMessagesSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import SwiftData
import SwiftUI

struct QuickMessagesSettingsView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \QuickMessage.createdAt) private var quickMessages: [QuickMessage]
@State private var editingMessage: QuickMessage?
@State private var showingAddSheet = false

var body: some View {
List {
if quickMessages.isEmpty {
ContentUnavailableView(
"No Quick Messages",
systemImage: "text.bubble",
description: Text("Tap + to add messages you send often.")
)
.listRowBackground(Color.clear)
} else {
ForEach(quickMessages) { message in
Button {
editingMessage = message
} label: {
Text(message.text)
.foregroundStyle(.primary)
.lineLimit(2)
}
.swipeActions(edge: .trailing) {
Button("Delete", role: .destructive) {
modelContext.delete(message)
}
}
.contextMenu {
Button("Edit") {
editingMessage = message
}
Button("Delete", role: .destructive) {
modelContext.delete(message)
}
}
}
}
}
.navigationTitle("Quick Messages")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showingAddSheet = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
QuickMessageEditSheet(message: nil)
}
.sheet(item: $editingMessage) { message in
QuickMessageEditSheet(message: message)
}
}
}

struct QuickMessageEditSheet: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
let message: QuickMessage?

@State private var text: String = ""
@FocusState private var isTextFocused: Bool

private var isNew: Bool { message == nil }

var body: some View {
NavigationStack {
TextEditor(text: $text)
.focused($isTextFocused)
.padding()
.navigationTitle(isNew ? "New Quick Message" : "Edit Quick Message")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") { save() }
.disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
.onAppear {
text = message?.text ?? ""
isTextFocused = true
}
}

private func save() {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
if let message {
message.text = trimmed
} else {
modelContext.insert(QuickMessage(text: trimmed))
}
dismiss()
}
}

#Preview("List") {
NavigationStack {
QuickMessagesSettingsView()
.modelContainer(for: QuickMessage.self, inMemory: true)
}
}

#Preview("Edit Sheet") {
QuickMessageEditSheet(message: nil)
.modelContainer(for: QuickMessage.self, inMemory: true)
}
11 changes: 11 additions & 0 deletions Wisp/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct SettingsView: View {
gitIdentitySection
claudeSection
instructionsSection
quickMessagesSection
appearanceSection
#if DEBUG
developerSection
Expand Down Expand Up @@ -203,6 +204,16 @@ struct SettingsView: View {
}
}

private var quickMessagesSection: some View {
Section {
NavigationLink {
QuickMessagesSettingsView()
} label: {
Label("Quick Messages", systemImage: "text.bubble")
}
}
}

private var appearanceSection: some View {
Section("Appearance") {
Picker("Theme", selection: $theme) {
Expand Down
9 changes: 9 additions & 0 deletions Wisp/Views/SpriteDetail/Chat/ChatAttachmentButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct ChatAttachmentButton: View {
let onBrowseSpriteFiles: () -> Void
let onPickPhoto: () -> Void
let onPickFile: () -> Void
var onQuickMessages: (() -> Void)? = nil
var body: some View {
if isUploading {
ProgressView()
Expand All @@ -30,6 +31,14 @@ struct ChatAttachmentButton: View {
Label("Choose File", systemImage: "doc")
}

if let onQuickMessages {
Button {
onQuickMessages()
} label: {
Label("Quick Messages", systemImage: "text.bubble")
}
}

} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
Expand Down
4 changes: 3 additions & 1 deletion Wisp/Views/SpriteDetail/Chat/ChatInputBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct ChatInputBar: View {
var lastUploadedFileName: String? = nil
var onStash: (() -> Void)? = nil
var onSideChat: (() -> Void)? = nil
var onQuickMessages: (() -> Void)? = nil
var isFocused: FocusState<Bool>.Binding

@State private var showStopConfirmation = false
Expand Down Expand Up @@ -57,7 +58,8 @@ struct ChatInputBar: View {
isDisabled: hasQueuedMessage,
onBrowseSpriteFiles: onBrowseSpriteFiles,
onPickPhoto: onPickPhoto,
onPickFile: onPickFile
onPickFile: onPickFile,
onQuickMessages: onQuickMessages
)
}

Expand Down
9 changes: 8 additions & 1 deletion Wisp/Views/SpriteDetail/Chat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct ChatView: View {

// Quick Actions
@State private var quickActionsViewModel: QuickActionsViewModel?
@State private var showingQuickMessages = false

var body: some View {
ScrollViewReader { proxy in
Expand Down Expand Up @@ -168,6 +169,7 @@ struct ChatView: View {
},
lastUploadedFileName: viewModel.lastUploadedFileName,
onStash: { viewModel.stashDraft() },
onQuickMessages: { showingQuickMessages = true },
onSideChat: {
quickActionsViewModel = QuickActionsViewModel(
spriteName: viewModel.spriteName,
Expand All @@ -192,6 +194,11 @@ struct ChatView: View {
}
}
}
.sheet(isPresented: $showingQuickMessages) {
QuickMessagePickerSheet { text in
viewModel.inputText += (viewModel.inputText.isEmpty ? "" : "\n") + text
}
}
.sheet(item: $quickActionsViewModel) { vm in
QuickActionsView(
viewModel: vm,
Expand Down Expand Up @@ -401,6 +408,6 @@ struct ChatView: View {
NavigationStack {
ChatView(viewModel: viewModel)
.environment(SpritesAPIClient())
.modelContainer(for: [SpriteChat.self, SpriteSession.self], inMemory: true)
.modelContainer(for: [SpriteChat.self, SpriteSession.self, QuickMessage.self], inMemory: true)
}
}
52 changes: 52 additions & 0 deletions Wisp/Views/SpriteDetail/Chat/QuickMessagePickerSheet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import SwiftData
import SwiftUI

struct QuickMessagePickerSheet: View {
@Environment(\.dismiss) private var dismiss
@Query(sort: \QuickMessage.createdAt) private var quickMessages: [QuickMessage]
let onSelect: (String) -> Void

var body: some View {
NavigationStack {
if quickMessages.isEmpty {
ContentUnavailableView(
"No Quick Messages",
systemImage: "text.bubble",
description: Text("Add quick messages in Settings.")
)
.navigationTitle("Quick Messages")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
} else {
List(quickMessages) { message in
Button {
onSelect(message.text)
dismiss()
} label: {
Text(message.text)
.foregroundStyle(.primary)
.lineLimit(3)
}
}
.navigationTitle("Quick Messages")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}

#Preview {
QuickMessagePickerSheet { _ in }
.modelContainer(for: QuickMessage.self, inMemory: true)
}
Loading