Skip to content

Commit b9be729

Browse files
committed
add a chatbox example
1 parent 17195a7 commit b9be729

21 files changed

+2248
-0
lines changed

Examples/Package.resolved

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ let package = Package(
2222
url: "https://github.com/stackotter/swift-bundler",
2323
revision: "d42d7ffda684cfed9edcfd3581b8127f1dc55c2e"
2424
),
25+
.package(
26+
url: "https://github.com/MacPaw/OpenAI",
27+
from: "0.4.4"
28+
),
2529
],
2630
targets: [
2731
.executableTarget(
@@ -72,6 +76,12 @@ let package = Package(
7276
.executableTarget(
7377
name: "WebViewExample",
7478
dependencies: exampleDependencies
79+
),
80+
.executableTarget(
81+
name: "ChatbotExample",
82+
dependencies: exampleDependencies + [
83+
.product(name: "OpenAI", package: "OpenAI")
84+
]
7585
)
7686
]
7787
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import DefaultBackend
2+
import SwiftCrossUI
3+
4+
#if canImport(SwiftBundlerRuntime)
5+
import SwiftBundlerRuntime
6+
#endif
7+
8+
// MARK: - Main App
9+
10+
@main
11+
@HotReloadable
12+
struct ChatbotApp: App {
13+
@State private var viewModel = ChatbotViewModel()
14+
15+
var body: some Scene {
16+
WindowGroup("ChatBot") {
17+
#hotReloadable {
18+
ZStack {
19+
// Main content with sidebar
20+
HStack(spacing: 0) {
21+
// Thread Sidebar - conditionally shown
22+
if viewModel.showSidebar {
23+
ThreadSidebarView(
24+
threads: Binding(
25+
get: { viewModel.threads },
26+
set: { viewModel.threads = $0 }
27+
),
28+
selectedThread: Binding(
29+
get: { viewModel.selectedThread },
30+
set: { viewModel.selectedThread = $0 }
31+
),
32+
showSidebar: Binding(
33+
get: { viewModel.showSidebar },
34+
set: { viewModel.showSidebar = $0 }
35+
),
36+
onNewThread: viewModel.createNewThread,
37+
onSelectThread: viewModel.selectThread,
38+
onDeleteThread: viewModel.deleteThread
39+
)
40+
.frame(width: 300)
41+
}
42+
43+
// Main chat area
44+
MainChatView(viewModel: viewModel)
45+
}
46+
.frame(maxWidth: .infinity, maxHeight: .infinity)
47+
48+
// Settings Overlay
49+
if viewModel.showSettings {
50+
ChatSettingsDialog(
51+
isPresented: Binding(
52+
get: { viewModel.showSettings },
53+
set: { viewModel.showSettings = $0 }
54+
),
55+
selectedModel: Binding(
56+
get: { viewModel.selectedLLM },
57+
set: { viewModel.selectedLLM = $0 }
58+
),
59+
openAIService: viewModel.openAIService,
60+
apiKeyStorage: viewModel.apiKeyStorage,
61+
onSave: viewModel.reloadAPIKey
62+
)
63+
}
64+
}
65+
}
66+
}
67+
.defaultSize(width: 1200, height: 800)
68+
}
69+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
// MARK: - Error Types
4+
5+
enum ChatError: Error, LocalizedError {
6+
case missingAPIKey
7+
case invalidURL
8+
case encodingError
9+
case decodingError
10+
case invalidResponse
11+
case apiError(Int)
12+
13+
var errorDescription: String? {
14+
switch self {
15+
case .missingAPIKey:
16+
return "Please enter your OpenAI API key"
17+
case .invalidURL:
18+
return "Invalid URL"
19+
case .encodingError:
20+
return "Failed to encode request"
21+
case .decodingError:
22+
return "Failed to decode response"
23+
case .invalidResponse:
24+
return "Invalid response from server"
25+
case .apiError(let code):
26+
switch code {
27+
case 401:
28+
return "API Error 401: Invalid API key. Please check your OpenAI API key and make sure it's valid and has sufficient credits."
29+
case 429:
30+
return "API Error 429: Rate limit exceeded. Please wait a moment and try again."
31+
case 500, 502, 503:
32+
return "API Error \(code): OpenAI server error. Please try again later."
33+
default:
34+
return "API error: \(code)"
35+
}
36+
}
37+
}
38+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
// MARK: - Data Models
4+
5+
struct ChatMessage: Identifiable {
6+
let id = UUID()
7+
let content: String
8+
let isUser: Bool
9+
let timestamp: Date
10+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Foundation
2+
3+
// MARK: - Thread Models
4+
5+
struct ChatThread: Identifiable, Codable {
6+
let id: String
7+
let title: String
8+
let createdAt: Date
9+
let lastMessageAt: Date
10+
let openAIThreadId: String? // OpenAI Thread ID for API integration
11+
12+
init(id: String = UUID().uuidString, title: String, openAIThreadId: String? = nil) {
13+
self.id = id
14+
self.title = title
15+
self.createdAt = Date()
16+
self.lastMessageAt = Date()
17+
self.openAIThreadId = openAIThreadId
18+
}
19+
20+
func updated(with lastMessageTime: Date = Date()) -> ChatThread {
21+
return ChatThread(
22+
id: self.id,
23+
title: self.title,
24+
openAIThreadId: self.openAIThreadId
25+
)
26+
}
27+
}
28+
29+
// MARK: - Thread Message
30+
31+
struct ThreadMessage: Identifiable, Codable {
32+
let id: String
33+
let threadId: String
34+
let content: String
35+
let isUser: Bool
36+
let timestamp: Date
37+
let openAIMessageId: String? // OpenAI Message ID for API integration
38+
39+
init(id: String = UUID().uuidString, threadId: String, content: String, isUser: Bool, openAIMessageId: String? = nil) {
40+
self.id = id
41+
self.threadId = threadId
42+
self.content = content
43+
self.isUser = isUser
44+
self.timestamp = Date()
45+
self.openAIMessageId = openAIMessageId
46+
}
47+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import OpenAI
2+
3+
// Type alias to avoid confusion between OpenAI's Model and our ViewModels
4+
typealias LLM = Model
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Foundation
2+
3+
// MARK: - API Key Storage
4+
5+
class APIKeyStorage {
6+
private let userDefaults = UserDefaults.standard
7+
private let apiKeyKey = "OpenAI_API_Key"
8+
9+
func saveAPIKey(_ key: String) {
10+
userDefaults.set(key, forKey: apiKeyKey)
11+
userDefaults.synchronize() // Force immediate synchronization
12+
print("🔑 API key saved to disk")
13+
}
14+
15+
func loadAPIKey() -> String? {
16+
let key = userDefaults.string(forKey: apiKeyKey)
17+
if let key = key, !key.isEmpty {
18+
print("🔑 API key loaded from disk successfully")
19+
return key
20+
}
21+
return nil
22+
}
23+
24+
func deleteAPIKey() {
25+
userDefaults.removeObject(forKey: apiKeyKey)
26+
userDefaults.synchronize() // Force immediate synchronization
27+
print("🗑️ API key deleted from disk")
28+
}
29+
}

0 commit comments

Comments
 (0)