├── .gitignore ├── LICENSE ├── README.MD ├── Shared ├── ChatGPTAPI.swift ├── ChatGPTAPIModels.swift ├── CodeBlockView.swift ├── ContentView.swift ├── DotLoadingView.swift ├── LLM Provider │ ├── LLMClient.swift │ ├── LLMConfig.swift │ ├── LLMProvider.swift │ └── PaLMChatAPI.swift ├── LLMConfigView.swift ├── MarkdownAttributedStringParser.swift ├── MessageRow.swift ├── MessageRowView.swift ├── ParserResult.swift ├── ResponseParsingTask.swift └── ViewModel.swift ├── XCAChatGPT.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── XCAChatGPT ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── openai.imageset │ │ ├── Contents.json │ │ └── openai.png │ ├── palm.imageset │ │ ├── Contents.json │ │ └── palm.png │ └── profile.imageset │ │ ├── Contents.json │ │ └── unnamed.jpg ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── TextView.swift ├── TokenizerView.swift ├── TokenizerViewModel.swift └── XCAChatGPTApp.swift ├── XCAChatGPTMac ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── icon.imageset │ │ ├── Contents.json │ │ └── Shape.png │ ├── openai.imageset │ │ ├── Contents.json │ │ └── openai.png │ ├── palm.imageset │ │ ├── Contents.json │ │ └── palm.png │ └── profile.imageset │ │ ├── Contents.json │ │ └── unnamed.jpg ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── XCAChatGPTMac.entitlements └── XCAChatGPTMacApp.swift ├── XCAChatGPTTV ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Top Shelf Image.imageset │ │ │ └── Contents.json │ ├── Contents.json │ ├── openai.imageset │ │ ├── Contents.json │ │ └── openai.png │ ├── palm.imageset │ │ ├── Contents.json │ │ └── palm.png │ └── profile.imageset │ │ ├── Contents.json │ │ └── unnamed.jpg ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ScrollView.swift └── XCAChatGPTTVApp.swift └── XCAChatGPTWatch Watch App ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json ├── openai.imageset │ ├── Contents.json │ └── openai.png ├── palm.imageset │ ├── Contents.json │ └── palm.png └── profile.imageset │ ├── Contents.json │ └── unnamed.jpg ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── XCAChatGPTWatchApp.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, 4 | .DS_Store 5 | Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting 11 | Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting 16 | Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift 44 | Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | # Package.resolved 48 | # *.xcodeproj 49 | # 50 | # Xcode automatically generates this directory with a .xcworkspacedata 51 | file and xcuserdata 52 | # hence it is not needed unless you have added a package configuration 53 | file to your project 54 | # .swiftpm 55 | 56 | .build/ 57 | 58 | # CocoaPods 59 | # 60 | # We recommend against adding the Pods directory to your .gitignore. 61 | However 62 | # you should judge for yourself, the pros and cons are mentioned at: 63 | # 64 | https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | # 66 | # Pods/ 67 | # 68 | # Add this line if you want to avoid checking in source code from the 69 | Xcode workspace 70 | # *.xcworkspace 71 | 72 | # Carthage 73 | # 74 | # Add this line if you want to avoid checking in source code from Carthage 75 | dependencies. 76 | # Carthage/Checkouts 77 | 78 | Carthage/Build/ 79 | 80 | # Accio dependency management 81 | Dependencies/ 82 | .accio/ 83 | 84 | # fastlane 85 | # 86 | # It is recommended to not store the screenshots in the git repo. 87 | # Instead, use fastlane to re-generate the screenshots whenever they are 88 | needed. 89 | # For more information about the recommended setup visit: 90 | # 91 | https://docs.fastlane.tools/best-practices/source-control/#source-control 92 | 93 | fastlane/report.xml 94 | fastlane/Preview.html 95 | fastlane/screenshots/**/*.png 96 | fastlane/test_output 97 | 98 | # Code Injection 99 | # 100 | # After new code Injection tools there's a generated folder 101 | /iOSInjectionProject 102 | # https://github.com/johnno1962/injectionforxcode 103 | 104 | iOSInjectionProject/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alfian Losari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # PaLMChat & ChatGPT SwiftUI iOS, macOS, watchOS, tvOS App 2 | 3 | ![Alt text](https://imagizer.imageshack.com/v2/640x480q70/924/4Qgrta.jpg "image") 4 | 5 | This is a native iOS, macOS, watchOS, tvOS App for interacting with PaLM API & ChatGPT LLM Chatbots built using SwiftUI, OpenAPI Official ChatGPT API, & Google Generative AI SDK SPM. 6 | 7 | It is also able to render response with markdown and code syntax highlighting. 8 | 9 | ## Video tutorial 10 | - [iOS YouTube](https://youtu.be/PLEgTCT20zU) 11 | - [macOS YouTube](https://youtu.be/Wl1cDvwpJoE) 12 | - [watchOS YouTube](https://youtu.be/DwXy0gKz1GY) 13 | - [tvOS YouTube](https://youtu.be/7RQHG7GXJ_U) 14 | - [Upgrade to Official API YouTube](https://youtu.be/9byLhs5hQjI) 15 | 16 | ## Requirements 17 | - Xcode 14 18 | - Register at openai.com/api 19 | - Create API Key from either OpenAI or PaLM API MakerSuite 20 | 21 | ## ChatGPTSwift API Lib 22 | You can use this standalone api client to access ChatGPT API, you can add dependency for [ChatGPTSwift](https://github.com/alfianlosari/ChatGPTSwift) to access the API only if you want to integrate into your own app. 23 | 24 | ## GPT Encoder Lib 25 | I've also created [GPTEncoder](https://github.com/alfianlosari/GPTEncoder) Swift BPE Encoder/Decoder for OpenAI GPT Models. A programmatic interface for tokenizing text for OpenAI GPT API. 26 | 27 | ## GPT Tokenizer UI Lib 28 | I've also created [GPTTokenizerUI](https://github.com/alfianlosari/GPTTokenizerUI), a SPM lib you can integrate in your app for providing GUI to input text and show the tokenization results used by GPT API. 29 | 30 | ![Alt text](https://imagizer.imageshack.com/v2/640x480q70/922/CEVvrE.png "image") 31 | -------------------------------------------------------------------------------- /Shared/ChatGPTAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatGPTAPI.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 01/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class ChatGPTAPI: LLMClient, @unchecked Sendable { 11 | 12 | var provider: LLMProvider { .chatGPT } 13 | 14 | private let systemMessage: Message 15 | private let temperature: Double 16 | private let model: String 17 | 18 | private let apiKey: String 19 | private var historyList = [Message]() 20 | private let urlSession = URLSession.shared 21 | private var urlRequest: URLRequest { 22 | let url = URL(string: "https://api.openai.com/v1/chat/completions")! 23 | var urlRequest = URLRequest(url: url) 24 | urlRequest.httpMethod = "POST" 25 | headers.forEach { urlRequest.setValue($1, forHTTPHeaderField: $0) } 26 | return urlRequest 27 | } 28 | 29 | let dateFormatter: DateFormatter = { 30 | let df = DateFormatter() 31 | df.dateFormat = "YYYY-MM-dd" 32 | return df 33 | }() 34 | 35 | private let jsonDecoder: JSONDecoder = { 36 | let jsonDecoder = JSONDecoder() 37 | jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 38 | return jsonDecoder 39 | }() 40 | 41 | private var headers: [String: String] { 42 | [ 43 | "Content-Type": "application/json", 44 | "Authorization": "Bearer \(apiKey)" 45 | ] 46 | } 47 | 48 | 49 | init(apiKey: String, model: String = "gpt-3.5-turbo", systemPrompt: String = "You are a helpful assistant", temperature: Double = 0.5) { 50 | self.apiKey = apiKey 51 | self.model = model 52 | self.systemMessage = .init(role: "system", content: systemPrompt) 53 | self.temperature = temperature 54 | } 55 | 56 | private func generateMessages(from text: String) -> [Message] { 57 | var messages = [systemMessage] + historyList + [Message(role: "user", content: text)] 58 | 59 | if messages.contentCount > (4000 * 4) { 60 | _ = historyList.removeFirst() 61 | messages = generateMessages(from: text) 62 | } 63 | return messages 64 | } 65 | 66 | private func jsonBody(text: String, stream: Bool = true) throws -> Data { 67 | let request = Request(model: model, temperature: temperature, 68 | messages: generateMessages(from: text), stream: stream) 69 | return try JSONEncoder().encode(request) 70 | } 71 | 72 | private func appendToHistoryList(userText: String, responseText: String) { 73 | self.historyList.append(.init(role: "user", content: userText)) 74 | self.historyList.append(.init(role: "assistant", content: responseText)) 75 | } 76 | 77 | func sendMessageStream(text: String) async throws -> AsyncThrowingStream { 78 | var urlRequest = self.urlRequest 79 | urlRequest.httpBody = try jsonBody(text: text) 80 | 81 | let (result, response) = try await urlSession.bytes(for: urlRequest) 82 | try Task.checkCancellation() 83 | 84 | guard let httpResponse = response as? HTTPURLResponse else { 85 | throw "Invalid response" 86 | } 87 | 88 | guard 200...299 ~= httpResponse.statusCode else { 89 | var errorText = "" 90 | for try await line in result.lines { 91 | try Task.checkCancellation() 92 | errorText += line 93 | } 94 | 95 | if let data = errorText.data(using: .utf8), let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error { 96 | errorText = "\n\(errorResponse.message)" 97 | } 98 | 99 | throw "Bad Response: \(httpResponse.statusCode), \(errorText)" 100 | } 101 | 102 | var responseText = "" 103 | let streams: AsyncThrowingStream = AsyncThrowingStream { continuation in 104 | Task { 105 | do { 106 | for try await line in result.lines { 107 | try Task.checkCancellation() 108 | continuation.yield(line) 109 | } 110 | continuation.finish() 111 | } catch { 112 | continuation.finish(throwing: error) 113 | } 114 | } 115 | } 116 | 117 | return AsyncThrowingStream { [weak self] in 118 | guard let self else { return nil } 119 | for try await line in streams { 120 | try Task.checkCancellation() 121 | if line.hasPrefix("data: "), 122 | let data = line.dropFirst(6).data(using: .utf8), 123 | let response = try? self.jsonDecoder.decode(StreamCompletionResponse.self, from: data), 124 | let text = response.choices.first?.delta.content { 125 | responseText += text 126 | return text 127 | } 128 | } 129 | self.appendToHistoryList(userText: text, responseText: responseText) 130 | return nil 131 | } 132 | } 133 | 134 | func sendMessage(_ text: String) async throws -> String { 135 | var urlRequest = self.urlRequest 136 | urlRequest.httpBody = try jsonBody(text: text, stream: false) 137 | 138 | let (data, response) = try await urlSession.data(for: urlRequest) 139 | try Task.checkCancellation() 140 | guard let httpResponse = response as? HTTPURLResponse else { 141 | throw "Invalid response" 142 | } 143 | 144 | guard 200...299 ~= httpResponse.statusCode else { 145 | var error = "Bad Response: \(httpResponse.statusCode)" 146 | if let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error { 147 | error.append("\n\(errorResponse.message)") 148 | } 149 | throw error 150 | } 151 | 152 | do { 153 | let completionResponse = try self.jsonDecoder.decode(CompletionResponse.self, from: data) 154 | let responseText = completionResponse.choices.first?.message.content ?? "" 155 | self.appendToHistoryList(userText: text, responseText: responseText) 156 | return responseText 157 | } catch { 158 | throw error 159 | } 160 | } 161 | 162 | func deleteHistoryList() { 163 | self.historyList.removeAll() 164 | } 165 | } 166 | 167 | extension String: CustomNSError { 168 | 169 | public var errorUserInfo: [String : Any] { 170 | [ 171 | NSLocalizedDescriptionKey: self 172 | ] 173 | } 174 | } 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /Shared/ChatGPTAPIModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatGPTAPIModels.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/03/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ChatGPTModel: String, Identifiable, CaseIterable { 11 | 12 | var id: Self { self } 13 | 14 | case gpt3Turbo = "gpt-3.5-turbo" 15 | case gpt4 = "gpt-4" 16 | 17 | var text: String { 18 | switch self { 19 | case .gpt3Turbo: 20 | return "GPT-3.5" 21 | case .gpt4: 22 | return "GPT-4" 23 | } 24 | } 25 | } 26 | 27 | struct Message: Codable { 28 | let role: String 29 | let content: String 30 | } 31 | 32 | extension Array where Element == Message { 33 | 34 | var contentCount: Int { reduce(0, { $0 + $1.content.count })} 35 | } 36 | 37 | struct Request: Codable { 38 | let model: String 39 | let temperature: Double 40 | let messages: [Message] 41 | let stream: Bool 42 | } 43 | 44 | struct ErrorRootResponse: Decodable { 45 | let error: ErrorResponse 46 | } 47 | 48 | struct ErrorResponse: Decodable { 49 | let message: String 50 | let type: String? 51 | } 52 | 53 | struct StreamCompletionResponse: Decodable { 54 | let choices: [StreamChoice] 55 | } 56 | 57 | struct CompletionResponse: Decodable { 58 | let choices: [Choice] 59 | let usage: Usage? 60 | } 61 | 62 | struct Usage: Decodable { 63 | let promptTokens: Int? 64 | let completionTokens: Int? 65 | let totalTokens: Int? 66 | } 67 | 68 | struct Choice: Decodable { 69 | let message: Message 70 | let finishReason: String? 71 | } 72 | 73 | struct StreamChoice: Decodable { 74 | let finishReason: String? 75 | let delta: StreamMessage 76 | } 77 | 78 | struct StreamMessage: Decodable { 79 | let role: String? 80 | let content: String? 81 | } 82 | 83 | -------------------------------------------------------------------------------- /Shared/CodeBlockView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeBlockView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 19/04/23. 6 | // 7 | 8 | import SwiftUI 9 | import Markdown 10 | 11 | enum HighlighterConstants { 12 | static let color = Color(red: 38/255, green: 38/255, blue: 38/255) 13 | } 14 | 15 | struct CodeBlockView: View { 16 | 17 | let parserResult: ParserResult 18 | @State var isCopied = false 19 | 20 | var body: some View { 21 | VStack(alignment: .leading) { 22 | header 23 | .padding(.horizontal) 24 | .padding(.vertical, 8) 25 | .background(Color(red: 9/255, green: 49/255, blue: 69/255)) 26 | 27 | ScrollView(.horizontal, showsIndicators: true) { 28 | Text(parserResult.attributedString) 29 | .padding(.horizontal, 16) 30 | .textSelection(.enabled) 31 | } 32 | } 33 | .background(HighlighterConstants.color) 34 | .cornerRadius(8) 35 | } 36 | 37 | var header: some View { 38 | HStack { 39 | if let codeBlockLanguage = parserResult.codeBlockLanguage { 40 | Text(codeBlockLanguage.capitalized) 41 | .font(.headline.monospaced()) 42 | .foregroundColor(.white) 43 | } 44 | Spacer() 45 | button 46 | } 47 | } 48 | 49 | @ViewBuilder 50 | var button: some View { 51 | if isCopied { 52 | HStack { 53 | Text("Copied") 54 | .foregroundColor(.white) 55 | .font(.subheadline.monospaced().bold()) 56 | Image(systemName: "checkmark.circle.fill") 57 | .imageScale(.large) 58 | .symbolRenderingMode(.multicolor) 59 | } 60 | .frame(alignment: .trailing) 61 | } else { 62 | Button { 63 | let string = NSAttributedString(parserResult.attributedString).string 64 | UIPasteboard.general.string = string 65 | withAnimation { 66 | isCopied = true 67 | } 68 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 69 | withAnimation { 70 | isCopied = false 71 | } 72 | } 73 | } label: { 74 | Image(systemName: "doc.on.doc") 75 | } 76 | .foregroundColor(.white) 77 | } 78 | } 79 | } 80 | 81 | struct CodeBlockView_Previews: PreviewProvider { 82 | 83 | static var markdownString = """ 84 | ```swift 85 | let api = ChatGPTAPI(apiKey: "API_KEY") 86 | 87 | Task { 88 | do { 89 | let stream = try await api.sendMessageStream(text: "What is ChatGPT?") 90 | for try await line in stream { 91 | print(line) 92 | } 93 | } catch { 94 | print(error.localizedDescription) 95 | } 96 | } 97 | ``` 98 | """ 99 | 100 | static let parserResult: ParserResult = { 101 | let document = Document(parsing: markdownString) 102 | var parser = MarkdownAttributedStringParser() 103 | return parser.parserResults(from: document)[0] 104 | }() 105 | 106 | static var previews: some View { 107 | CodeBlockView(parserResult: parserResult) 108 | } 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 01/02/23. 6 | // 7 | 8 | import SwiftUI 9 | import AVKit 10 | 11 | struct ContentView: View { 12 | 13 | @Environment(\.colorScheme) var colorScheme 14 | @ObservedObject var vm: ViewModel 15 | @FocusState var isTextFieldFocused: Bool 16 | 17 | var body: some View { 18 | chatListView 19 | .navigationTitle(vm.navigationTitle) 20 | } 21 | 22 | var chatListView: some View { 23 | ScrollViewReader { proxy in 24 | VStack(spacing: 0) { 25 | ScrollView { 26 | LazyVStack(spacing: 0) { 27 | ForEach(vm.messages) { message in 28 | MessageRowView(message: message) { message in 29 | Task { @MainActor in 30 | await vm.retry(message: message) 31 | } 32 | } 33 | } 34 | } 35 | .onTapGesture { 36 | isTextFieldFocused = false 37 | } 38 | } 39 | #if os(iOS) || os(macOS) 40 | Divider() 41 | bottomView(image: "profile", proxy: proxy) 42 | Spacer() 43 | #endif 44 | } 45 | .onChange(of: vm.messages.last?.responseText) { _ in scrollToBottom(proxy: proxy) 46 | } 47 | } 48 | .background(colorScheme == .light ? .white : Color(red: 52/255, green: 53/255, blue: 65/255, opacity: 0.5)) 49 | } 50 | 51 | func bottomView(image: String, proxy: ScrollViewProxy) -> some View { 52 | HStack(alignment: .top, spacing: 8) { 53 | if image.hasPrefix("http"), let url = URL(string: image) { 54 | AsyncImage(url: url) { image in 55 | image 56 | .resizable() 57 | .frame(width: 30, height: 30) 58 | } placeholder: { 59 | ProgressView() 60 | } 61 | 62 | } else { 63 | Image(image) 64 | .resizable() 65 | .frame(width: 30, height: 30) 66 | } 67 | 68 | TextField("Send message", text: $vm.inputMessage, axis: .vertical) 69 | .autocorrectionDisabled() 70 | #if os(iOS) || os(macOS) 71 | .textFieldStyle(.roundedBorder) 72 | #endif 73 | .focused($isTextFieldFocused) 74 | .disabled(vm.isInteracting) 75 | 76 | if vm.isInteracting { 77 | #if os(iOS) 78 | Button { 79 | vm.cancelStreamingResponse() 80 | } label: { 81 | Image(systemName: "stop.circle.fill") 82 | .font(.system(size: 30)) 83 | .symbolRenderingMode(.multicolor) 84 | .foregroundColor(.red) 85 | } 86 | #else 87 | DotLoadingView().frame(width: 60, height: 30) 88 | #endif 89 | } else { 90 | Button { 91 | Task { @MainActor in 92 | isTextFieldFocused = false 93 | scrollToBottom(proxy: proxy) 94 | await vm.sendTapped() 95 | } 96 | } label: { 97 | Image(systemName: "paperplane.circle.fill") 98 | .rotationEffect(.degrees(45)) 99 | .font(.system(size: 30)) 100 | } 101 | #if os(macOS) 102 | .buttonStyle(.borderless) 103 | .keyboardShortcut(.defaultAction) 104 | .foregroundColor(.accentColor) 105 | #endif 106 | .disabled(vm.inputMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 107 | } 108 | } 109 | .padding(.horizontal, 16) 110 | .padding(.top, 12) 111 | } 112 | 113 | private func scrollToBottom(proxy: ScrollViewProxy) { 114 | guard let id = vm.messages.last?.id else { return } 115 | proxy.scrollTo(id, anchor: .bottomTrailing) 116 | } 117 | } 118 | 119 | struct ContentView_Previews: PreviewProvider { 120 | static var previews: some View { 121 | NavigationStack { 122 | ContentView(vm: ViewModel(api: ChatGPTAPI(apiKey: "PROVIDE_API_KEY"))) 123 | } 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /Shared/DotLoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotLoadingView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 02/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DotLoadingView: View { 11 | 12 | @State private var showCircle1 = false 13 | @State private var showCircle2 = false 14 | @State private var showCircle3 = false 15 | 16 | var body: some View { 17 | HStack { 18 | Circle() 19 | .opacity(showCircle1 ? 1 : 0) 20 | Circle() 21 | .opacity(showCircle2 ? 1 : 0) 22 | Circle() 23 | .opacity(showCircle3 ? 1 : 0) 24 | } 25 | .foregroundColor(.gray.opacity(0.5)) 26 | .onAppear { performAnimation() } 27 | } 28 | 29 | func performAnimation() { 30 | let animation = Animation.easeInOut(duration: 0.4) 31 | withAnimation(animation) { 32 | self.showCircle1 = true 33 | self.showCircle3 = false 34 | } 35 | 36 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 37 | withAnimation(animation) { 38 | self.showCircle2 = true 39 | self.showCircle1 = false 40 | } 41 | } 42 | 43 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { 44 | withAnimation(animation) { 45 | self.showCircle2 = false 46 | self.showCircle3 = true 47 | } 48 | } 49 | 50 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { 51 | self.performAnimation() 52 | } 53 | } 54 | } 55 | 56 | struct DotLoadingView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | DotLoadingView() 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Shared/LLM Provider/LLMClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LLMClient.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LLMClient { 11 | 12 | var provider: LLMProvider { get } 13 | 14 | func sendMessageStream(text: String) async throws -> AsyncThrowingStream 15 | func sendMessage(_ text: String) async throws -> String 16 | func deleteHistoryList() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Shared/LLM Provider/LLMConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LLMConfig.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LLMConfig: Identifiable, Hashable { 11 | 12 | var id: String { apiKey } 13 | 14 | let apiKey: String 15 | let type: ConfigType 16 | 17 | enum ConfigType: Hashable { 18 | case chatGPT(ChatGPTModel) 19 | case palm 20 | } 21 | 22 | func createClient() -> LLMClient { 23 | switch self.type { 24 | case .chatGPT(let model): 25 | return ChatGPTAPI(apiKey: apiKey, model: model.rawValue) 26 | case .palm: 27 | return PaLMChatAPI(apiKey: apiKey) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Shared/LLM Provider/LLMProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LLMProvider.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LLMProvider: Identifiable, CaseIterable { 11 | 12 | case chatGPT 13 | case palm 14 | 15 | var id: Self { self } 16 | 17 | var text: String { 18 | switch self { 19 | case .chatGPT: 20 | return "OpenAI ChatGPT" 21 | case .palm: 22 | return "Google PaLM" 23 | } 24 | } 25 | 26 | var footerInfo: String { 27 | switch self { 28 | case .chatGPT: 29 | return """ 30 | ChatGPT is an artificial intelligence (AI) chatbot developed by OpenAI and released in November 2022. The name "ChatGPT" combines "Chat", referring to its chatbot functionality, and "GPT", which stands for Generative Pre-trained Transformer, a type of large language model (LLM). ChatGPT is built upon OpenAI's foundational GPT models, specifically GPT-3.5 and GPT-4, and has been fine-tuned (an approach to transfer learning) for conversational applications using a combination of supervised and reinforcement learning techniques. 31 | """ 32 | case .palm: 33 | return """ 34 | PaLM (Pathways Language Model) is a 540 billion parameter transformer-based large language model developed by Google AI.Researchers also trained smaller versions of PaLM, 8 and 62 billion parameter models, to test the effects of model scale. 35 | 36 | PaLM is capable of a wide range of tasks, including commonsense reasoning, arithmetic reasoning, joke explanation, code generation, and translation. When combined with chain-of-thought prompting, PaLM achieved significantly better performance on datasets requiring reasoning of multiple steps, such as word problems and logic-based questions. 37 | """ 38 | } 39 | } 40 | 41 | var navigationTitle: String { 42 | switch self { 43 | case .chatGPT: 44 | return "XCA ChatGPT" 45 | 46 | case .palm: 47 | return "XCA PaLMChat" 48 | } 49 | } 50 | 51 | var imageName: String { 52 | switch self { 53 | case .chatGPT: 54 | return "openai" 55 | case .palm: 56 | return "palm" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Shared/LLM Provider/PaLMChatAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaLMChatAPI.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/06/23. 6 | // 7 | 8 | import Foundation 9 | import GoogleGenerativeAI 10 | 11 | class PaLMChatAPI: LLMClient { 12 | 13 | var provider: LLMProvider { .palm } 14 | private let palmClient: GenerativeLanguage 15 | private var history = [GoogleGenerativeAI.Message]() 16 | 17 | init(apiKey: String) { 18 | self.palmClient = .init(apiKey: apiKey) 19 | } 20 | 21 | func sendMessage(_ text: String) async throws -> String { 22 | let response = try await palmClient.chat(message: text, history: history) 23 | if let candidate = response.candidates?.first, let responseText = candidate.content { 24 | if let historicMessages = response.messages { 25 | self.history = historicMessages 26 | self.history.append(candidate) 27 | } 28 | return responseText 29 | } else { 30 | throw "No response" 31 | } 32 | } 33 | 34 | func sendMessageStream(text: String) async throws -> AsyncThrowingStream { 35 | fatalError("Not supported") 36 | } 37 | 38 | func deleteHistoryList() { 39 | self.history = [] 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Shared/LLMConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LLMConfigView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 03/06/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LLMConfigView: View { 11 | 12 | let onStartChatTapped: (_ result: LLMConfig) -> Void 13 | @State var apiKey = "" 14 | @State var llmProvider = LLMProvider.chatGPT 15 | @State var chatGPTModel = ChatGPTModel.gpt3Turbo 16 | 17 | var body: some View { 18 | #if os(macOS) 19 | ScrollView { sectionsView } 20 | .padding(.horizontal) 21 | #else 22 | List { sectionsView } 23 | #endif 24 | } 25 | 26 | @ViewBuilder 27 | var sectionsView: some View { 28 | Section("LLM Provider") { 29 | Picker("Provider", selection: $llmProvider) { 30 | ForEach(LLMProvider.allCases) { 31 | Text($0.text).id($0) 32 | } 33 | } 34 | #if !os(watchOS) 35 | .pickerStyle(.segmented) 36 | #endif 37 | } 38 | 39 | Section("Configuration") { 40 | TextField("Enter API Key", text: $apiKey) 41 | .autocorrectionDisabled() 42 | #if os(macOS) 43 | .textCase(.none) 44 | .textFieldStyle(.roundedBorder) 45 | #else 46 | .textInputAutocapitalization(.never) 47 | #endif 48 | 49 | if llmProvider == .chatGPT { 50 | Picker("Model", selection: $chatGPTModel) { 51 | ForEach(ChatGPTModel.allCases) { 52 | Text($0.text).id($0) 53 | } 54 | } 55 | #if !os(watchOS) 56 | .pickerStyle(.segmented) 57 | #endif 58 | } 59 | } 60 | 61 | Section { 62 | Button("Start Chat") { 63 | let type: LLMConfig.ConfigType 64 | switch llmProvider { 65 | case .chatGPT: 66 | type = .chatGPT(chatGPTModel) 67 | case .palm: 68 | type = .palm 69 | } 70 | 71 | self.onStartChatTapped(.init(apiKey: apiKey, type: type)) 72 | } 73 | #if os(macOS) 74 | .buttonStyle(.borderedProminent) 75 | #endif 76 | .frame(maxWidth: .infinity) 77 | .disabled(apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 78 | 79 | } footer: { 80 | Text(llmProvider.footerInfo) 81 | .padding(.vertical) 82 | } 83 | } 84 | } 85 | 86 | struct LLMConfigView_Previews: PreviewProvider { 87 | static var previews: some View { 88 | NavigationStack { 89 | LLMConfigView { result in 90 | 91 | } 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Shared/MarkdownAttributedStringParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownAttributedStringParser.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 19/04/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Markdown 11 | import Highlighter 12 | 13 | /// Based on the source code from Christian Selig 14 | /// https://github.com/christianselig/Markdownosaur/blob/main/Sources/Markdownosaur/Markdownosaur.swift 15 | 16 | public struct MarkdownAttributedStringParser: MarkupVisitor { 17 | let baseFontSize: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize 18 | let highlighter: Highlighter = { 19 | let highlighter = Highlighter()! 20 | highlighter.setTheme("stackoverflow-dark") 21 | return highlighter 22 | }() 23 | 24 | let newLineFontSize: CGFloat = 12 25 | 26 | public init() {} 27 | 28 | public mutating func attributedString(from document: Document) -> NSAttributedString { 29 | return visit(document) 30 | } 31 | 32 | mutating func parserResults(from document: Document) -> [ParserResult] { 33 | var results = [ParserResult]() 34 | var currentAttrString = NSMutableAttributedString() 35 | 36 | func appendCurrentAttrString() { 37 | if !currentAttrString.string.isEmpty { 38 | let currentAttrStringToAppend = (try? AttributedString(currentAttrString, including: \.uiKit)) ?? AttributedString(stringLiteral: currentAttrString.string) 39 | results.append(.init(attributedString: currentAttrStringToAppend, isCodeBlock: false, codeBlockLanguage: nil)) 40 | } 41 | } 42 | 43 | document.children.forEach { markup in 44 | let attrString = visit(markup) 45 | if let codeBlock = markup as? CodeBlock { 46 | appendCurrentAttrString() 47 | let attrStringToAppend = (try? AttributedString(attrString, including: \.uiKit)) ?? AttributedString(stringLiteral: attrString.string) 48 | results.append(.init(attributedString: attrStringToAppend, isCodeBlock: true, codeBlockLanguage: codeBlock.language)) 49 | currentAttrString = NSMutableAttributedString() 50 | } else { 51 | currentAttrString.append(attrString) 52 | } 53 | } 54 | 55 | appendCurrentAttrString() 56 | return results 57 | } 58 | 59 | mutating public func defaultVisit(_ markup: Markup) -> NSAttributedString { 60 | let result = NSMutableAttributedString() 61 | 62 | for child in markup.children { 63 | result.append(visit(child)) 64 | } 65 | 66 | return result 67 | } 68 | 69 | mutating public func visitText(_ text: Text) -> NSAttributedString { 70 | return NSAttributedString(string: text.plainText, attributes: [.font: UIFont.systemFont(ofSize: baseFontSize, weight: .regular)]) 71 | } 72 | 73 | mutating public func visitEmphasis(_ emphasis: Emphasis) -> NSAttributedString { 74 | let result = NSMutableAttributedString() 75 | 76 | for child in emphasis.children { 77 | result.append(visit(child)) 78 | } 79 | 80 | result.applyEmphasis() 81 | 82 | return result 83 | } 84 | 85 | mutating public func visitStrong(_ strong: Strong) -> NSAttributedString { 86 | let result = NSMutableAttributedString() 87 | 88 | for child in strong.children { 89 | result.append(visit(child)) 90 | } 91 | 92 | result.applyStrong() 93 | 94 | return result 95 | } 96 | 97 | mutating public func visitParagraph(_ paragraph: Paragraph) -> NSAttributedString { 98 | let result = NSMutableAttributedString() 99 | 100 | for child in paragraph.children { 101 | result.append(visit(child)) 102 | } 103 | 104 | if paragraph.hasSuccessor { 105 | result.append(paragraph.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize)) 106 | } 107 | 108 | return result 109 | } 110 | 111 | mutating public func visitHeading(_ heading: Heading) -> NSAttributedString { 112 | let result = NSMutableAttributedString() 113 | 114 | for child in heading.children { 115 | result.append(visit(child)) 116 | } 117 | 118 | result.applyHeading(withLevel: heading.level) 119 | 120 | if heading.hasSuccessor { 121 | result.append(.doubleNewline(withFontSize: newLineFontSize)) 122 | } 123 | 124 | return result 125 | } 126 | 127 | mutating public func visitLink(_ link: Link) -> NSAttributedString { 128 | let result = NSMutableAttributedString() 129 | 130 | for child in link.children { 131 | result.append(visit(child)) 132 | } 133 | 134 | let url = link.destination != nil ? URL(string: link.destination!) : nil 135 | 136 | result.applyLink(withURL: url) 137 | 138 | return result 139 | } 140 | 141 | mutating public func visitInlineCode(_ inlineCode: InlineCode) -> NSAttributedString { 142 | return NSAttributedString(string: inlineCode.code, attributes: [.font: UIFont.monospacedSystemFont(ofSize: baseFontSize - 1.0, weight: .regular), .foregroundColor: UIColor.systemPink]) 143 | } 144 | 145 | public func visitCodeBlock(_ codeBlock: CodeBlock) -> NSAttributedString { 146 | let result = NSMutableAttributedString(attributedString: highlighter.highlight(codeBlock.code, as: codeBlock.language) ?? NSAttributedString(string: codeBlock.code)) 147 | 148 | if codeBlock.hasSuccessor { 149 | result.append(.singleNewline(withFontSize: newLineFontSize)) 150 | } 151 | 152 | return result 153 | } 154 | 155 | mutating public func visitStrikethrough(_ strikethrough: Strikethrough) -> NSAttributedString { 156 | let result = NSMutableAttributedString() 157 | 158 | for child in strikethrough.children { 159 | result.append(visit(child)) 160 | } 161 | 162 | result.applyStrikethrough() 163 | 164 | return result 165 | } 166 | 167 | mutating public func visitUnorderedList(_ unorderedList: UnorderedList) -> NSAttributedString { 168 | let result = NSMutableAttributedString() 169 | 170 | let font = UIFont.systemFont(ofSize: baseFontSize, weight: .regular) 171 | 172 | for listItem in unorderedList.listItems { 173 | var listItemAttributes: [NSAttributedString.Key: Any] = [:] 174 | 175 | let listItemParagraphStyle = NSMutableParagraphStyle() 176 | 177 | let baseLeftMargin: CGFloat = 15.0 178 | let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(unorderedList.listDepth)) 179 | let spacingFromIndex: CGFloat = 8.0 180 | let bulletWidth = ceil(NSAttributedString(string: "•", attributes: [.font: font]).size().width) 181 | let firstTabLocation = leftMarginOffset + bulletWidth 182 | let secondTabLocation = firstTabLocation + spacingFromIndex 183 | 184 | listItemParagraphStyle.tabStops = [ 185 | NSTextTab(textAlignment: .right, location: firstTabLocation), 186 | NSTextTab(textAlignment: .left, location: secondTabLocation) 187 | ] 188 | 189 | listItemParagraphStyle.headIndent = secondTabLocation 190 | 191 | listItemAttributes[.paragraphStyle] = listItemParagraphStyle 192 | listItemAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .regular) 193 | listItemAttributes[.listDepth] = unorderedList.listDepth 194 | 195 | let listItemAttributedString = visit(listItem).mutableCopy() as! NSMutableAttributedString 196 | listItemAttributedString.insert(NSAttributedString(string: "\t•\t", attributes: listItemAttributes), at: 0) 197 | 198 | result.append(listItemAttributedString) 199 | } 200 | 201 | if unorderedList.hasSuccessor { 202 | result.append(.doubleNewline(withFontSize: newLineFontSize)) 203 | } 204 | 205 | return result 206 | } 207 | 208 | mutating public func visitListItem(_ listItem: ListItem) -> NSAttributedString { 209 | let result = NSMutableAttributedString() 210 | 211 | for child in listItem.children { 212 | result.append(visit(child)) 213 | } 214 | 215 | if listItem.hasSuccessor { 216 | result.append(.singleNewline(withFontSize: newLineFontSize)) 217 | } 218 | 219 | return result 220 | } 221 | 222 | mutating public func visitOrderedList(_ orderedList: OrderedList) -> NSAttributedString { 223 | let result = NSMutableAttributedString() 224 | 225 | for (index, listItem) in orderedList.listItems.enumerated() { 226 | var listItemAttributes: [NSAttributedString.Key: Any] = [:] 227 | 228 | let font = UIFont.systemFont(ofSize: baseFontSize, weight: .regular) 229 | let numeralFont = UIFont.monospacedDigitSystemFont(ofSize: baseFontSize, weight: .regular) 230 | 231 | let listItemParagraphStyle = NSMutableParagraphStyle() 232 | 233 | // Implement a base amount to be spaced from the left side at all times to better visually differentiate it as a list 234 | let baseLeftMargin: CGFloat = 15.0 235 | let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(orderedList.listDepth)) 236 | 237 | // Grab the highest number to be displayed and measure its width (yes normally some digits are wider than others but since we're using the numeral mono font all will be the same width in this case) 238 | let highestNumberInList = orderedList.childCount 239 | let numeralColumnWidth = ceil(NSAttributedString(string: "\(highestNumberInList).", attributes: [.font: numeralFont]).size().width) 240 | 241 | let spacingFromIndex: CGFloat = 8.0 242 | let firstTabLocation = leftMarginOffset + numeralColumnWidth 243 | let secondTabLocation = firstTabLocation + spacingFromIndex 244 | 245 | listItemParagraphStyle.tabStops = [ 246 | NSTextTab(textAlignment: .right, location: firstTabLocation), 247 | NSTextTab(textAlignment: .left, location: secondTabLocation) 248 | ] 249 | 250 | listItemParagraphStyle.headIndent = secondTabLocation 251 | 252 | listItemAttributes[.paragraphStyle] = listItemParagraphStyle 253 | listItemAttributes[.font] = font 254 | listItemAttributes[.listDepth] = orderedList.listDepth 255 | 256 | let listItemAttributedString = visit(listItem).mutableCopy() as! NSMutableAttributedString 257 | 258 | // Same as the normal list attributes, but for prettiness in formatting we want to use the cool monospaced numeral font 259 | var numberAttributes = listItemAttributes 260 | numberAttributes[.font] = numeralFont 261 | 262 | let numberAttributedString = NSAttributedString(string: "\t\(index + 1).\t", attributes: numberAttributes) 263 | listItemAttributedString.insert(numberAttributedString, at: 0) 264 | 265 | result.append(listItemAttributedString) 266 | } 267 | 268 | if orderedList.hasSuccessor { 269 | result.append(orderedList.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize)) 270 | } 271 | 272 | return result 273 | } 274 | 275 | mutating public func visitBlockQuote(_ blockQuote: BlockQuote) -> NSAttributedString { 276 | let result = NSMutableAttributedString() 277 | 278 | for child in blockQuote.children { 279 | var quoteAttributes: [NSAttributedString.Key: Any] = [:] 280 | 281 | let quoteParagraphStyle = NSMutableParagraphStyle() 282 | 283 | let baseLeftMargin: CGFloat = 15.0 284 | let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(blockQuote.quoteDepth)) 285 | 286 | quoteParagraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: leftMarginOffset)] 287 | 288 | quoteParagraphStyle.headIndent = leftMarginOffset 289 | 290 | quoteAttributes[.paragraphStyle] = quoteParagraphStyle 291 | quoteAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .regular) 292 | quoteAttributes[.listDepth] = blockQuote.quoteDepth 293 | 294 | let quoteAttributedString = visit(child).mutableCopy() as! NSMutableAttributedString 295 | quoteAttributedString.insert(NSAttributedString(string: "\t", attributes: quoteAttributes), at: 0) 296 | 297 | quoteAttributedString.addAttribute(.foregroundColor, value: UIColor.systemGray) 298 | 299 | result.append(quoteAttributedString) 300 | } 301 | 302 | if blockQuote.hasSuccessor { 303 | result.append(.doubleNewline(withFontSize: newLineFontSize)) 304 | } 305 | 306 | return result 307 | } 308 | } 309 | 310 | // MARK: - Extensions Land 311 | 312 | extension NSMutableAttributedString { 313 | func applyEmphasis() { 314 | enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in 315 | guard let font = value as? UIFont else { return } 316 | 317 | let newFont = font.apply(newTraits: .traitItalic) 318 | addAttribute(.font, value: newFont, range: range) 319 | } 320 | } 321 | 322 | func applyStrong() { 323 | enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in 324 | guard let font = value as? UIFont else { return } 325 | 326 | let newFont = font.apply(newTraits: .traitBold) 327 | addAttribute(.font, value: newFont, range: range) 328 | } 329 | } 330 | 331 | func applyLink(withURL url: URL?) { 332 | addAttribute(.foregroundColor, value: UIColor.systemBlue) 333 | 334 | if let url = url { 335 | addAttribute(.link, value: url) 336 | } 337 | } 338 | 339 | func applyBlockquote() { 340 | addAttribute(.foregroundColor, value: UIColor.systemGray) 341 | } 342 | 343 | func applyHeading(withLevel headingLevel: Int) { 344 | enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in 345 | guard let font = value as? UIFont else { return } 346 | 347 | let newFont = font.apply(newTraits: .traitBold, newPointSize: 28.0 - CGFloat(headingLevel * 2)) 348 | addAttribute(.font, value: newFont, range: range) 349 | } 350 | } 351 | 352 | func applyStrikethrough() { 353 | addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue) 354 | } 355 | } 356 | 357 | extension UIFont { 358 | func apply(newTraits: UIFontDescriptor.SymbolicTraits, newPointSize: CGFloat? = nil) -> UIFont { 359 | var existingTraits = fontDescriptor.symbolicTraits 360 | existingTraits.insert(newTraits) 361 | 362 | guard let newFontDescriptor = fontDescriptor.withSymbolicTraits(existingTraits) else { return self } 363 | return UIFont(descriptor: newFontDescriptor, size: newPointSize ?? pointSize) 364 | } 365 | } 366 | 367 | extension ListItemContainer { 368 | /// Depth of the list if nested within others. Index starts at 0. 369 | var listDepth: Int { 370 | var index = 0 371 | 372 | var currentElement = parent 373 | 374 | while currentElement != nil { 375 | if currentElement is ListItemContainer { 376 | index += 1 377 | } 378 | 379 | currentElement = currentElement?.parent 380 | } 381 | 382 | return index 383 | } 384 | } 385 | 386 | extension BlockQuote { 387 | /// Depth of the quote if nested within others. Index starts at 0. 388 | var quoteDepth: Int { 389 | var index = 0 390 | 391 | var currentElement = parent 392 | 393 | while currentElement != nil { 394 | if currentElement is BlockQuote { 395 | index += 1 396 | } 397 | 398 | currentElement = currentElement?.parent 399 | } 400 | 401 | return index 402 | } 403 | } 404 | 405 | extension NSAttributedString.Key { 406 | static let listDepth = NSAttributedString.Key("ListDepth") 407 | static let quoteDepth = NSAttributedString.Key("QuoteDepth") 408 | } 409 | 410 | extension NSMutableAttributedString { 411 | func addAttribute(_ name: NSAttributedString.Key, value: Any) { 412 | addAttribute(name, value: value, range: NSRange(location: 0, length: length)) 413 | } 414 | 415 | func addAttributes(_ attrs: [NSAttributedString.Key : Any]) { 416 | addAttributes(attrs, range: NSRange(location: 0, length: length)) 417 | } 418 | } 419 | 420 | extension Markup { 421 | /// Returns true if this element has sibling elements after it. 422 | var hasSuccessor: Bool { 423 | guard let childCount = parent?.childCount else { return false } 424 | return indexInParent < childCount - 1 425 | } 426 | 427 | var isContainedInList: Bool { 428 | var currentElement = parent 429 | 430 | while currentElement != nil { 431 | if currentElement is ListItemContainer { 432 | return true 433 | } 434 | 435 | currentElement = currentElement?.parent 436 | } 437 | 438 | return false 439 | } 440 | } 441 | 442 | extension NSAttributedString { 443 | static func singleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString { 444 | return NSAttributedString(string: "\n", attributes: [.font: UIFont.systemFont(ofSize: fontSize, weight: .regular)]) 445 | } 446 | 447 | static func doubleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString { 448 | return NSAttributedString(string: "\n\n", attributes: [.font: UIFont.systemFont(ofSize: fontSize, weight: .regular)]) 449 | } 450 | } 451 | 452 | -------------------------------------------------------------------------------- /Shared/MessageRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageRow.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 02/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AttributedOutput { 11 | let string: String 12 | let results: [ParserResult] 13 | } 14 | 15 | enum MessageRowType { 16 | case attributed(AttributedOutput) 17 | case rawText(String) 18 | 19 | var text: String { 20 | switch self { 21 | case .attributed(let attributedOutput): 22 | return attributedOutput.string 23 | case .rawText(let string): 24 | return string 25 | } 26 | } 27 | } 28 | 29 | struct MessageRow: Identifiable { 30 | 31 | let id = UUID() 32 | 33 | var isInteracting: Bool 34 | 35 | let sendImage: String 36 | var send: MessageRowType 37 | var sendText: String { 38 | send.text 39 | } 40 | 41 | let responseImage: String 42 | var response: MessageRowType? 43 | var responseText: String? { 44 | response?.text 45 | } 46 | 47 | var responseError: String? 48 | 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /Shared/MessageRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageRowView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 02/02/23. 6 | // 7 | 8 | import SwiftUI 9 | #if os(iOS) 10 | import Markdown 11 | #endif 12 | 13 | struct MessageRowView: View { 14 | 15 | @Environment(\.colorScheme) private var colorScheme 16 | let message: MessageRow 17 | let retryCallback: (MessageRow) -> Void 18 | 19 | var imageSize: CGSize { 20 | #if os(iOS) || os(macOS) 21 | CGSize(width: 25, height: 25) 22 | #elseif os(watchOS) 23 | CGSize(width: 20, height: 20) 24 | #else 25 | CGSize(width: 80, height: 80) 26 | #endif 27 | } 28 | 29 | var body: some View { 30 | VStack(spacing: 0) { 31 | messageRow(rowType: message.send, image: message.sendImage, bgColor: colorScheme == .light ? .white : Color(red: 52/255, green: 53/255, blue: 65/255, opacity: 0.5)) 32 | 33 | if let response = message.response { 34 | Divider() 35 | messageRow(rowType: response, image: message.responseImage, bgColor: colorScheme == .light ? .gray.opacity(0.1) : Color(red: 52/255, green: 53/255, blue: 65/255, opacity: 1), responseError: message.responseError, showDotLoading: message.isInteracting) 36 | Divider() 37 | } 38 | } 39 | } 40 | 41 | func messageRow(rowType: MessageRowType, image: String, bgColor: Color, responseError: String? = nil, showDotLoading: Bool = false) -> some View { 42 | #if os(watchOS) 43 | VStack(alignment: .leading, spacing: 8) { 44 | messageRowContent(rowType: rowType, image: image, responseError: responseError, showDotLoading: showDotLoading) 45 | } 46 | 47 | .padding(16) 48 | .frame(maxWidth: .infinity, alignment: .leading) 49 | .background(bgColor) 50 | #else 51 | HStack(alignment: .top, spacing: 24) { 52 | messageRowContent(rowType: rowType, image: image, responseError: responseError, showDotLoading: showDotLoading) 53 | } 54 | #if os(tvOS) 55 | .padding(32) 56 | #else 57 | .padding(16) 58 | #endif 59 | .frame(maxWidth: .infinity, alignment: .leading) 60 | .background(bgColor) 61 | #endif 62 | } 63 | 64 | @ViewBuilder 65 | func messageRowContent(rowType: MessageRowType, image: String, responseError: String? = nil, showDotLoading: Bool = false) -> some View { 66 | if image.hasPrefix("http"), let url = URL(string: image) { 67 | AsyncImage(url: url) { image in 68 | image 69 | .resizable() 70 | .frame(width: imageSize.width, height: imageSize.height) 71 | } placeholder: { 72 | ProgressView() 73 | } 74 | 75 | } else { 76 | Image(image) 77 | .resizable() 78 | .frame(width: imageSize.width, height: imageSize.height) 79 | } 80 | 81 | VStack(alignment: .leading) { 82 | switch rowType { 83 | case .attributed(let attributedOutput): 84 | attributedView(results: attributedOutput.results) 85 | 86 | case .rawText(let text): 87 | if !text.isEmpty { 88 | #if os(tvOS) 89 | responseTextView(text: text) 90 | #else 91 | Text(text) 92 | .multilineTextAlignment(.leading) 93 | #if os(iOS) || os(macOS) 94 | .textSelection(.enabled) 95 | #endif 96 | #endif 97 | } 98 | } 99 | 100 | if let error = responseError { 101 | Text("Error: \(error)") 102 | .foregroundColor(.red) 103 | .multilineTextAlignment(.leading) 104 | 105 | Button("Regenerate response") { 106 | retryCallback(message) 107 | } 108 | .foregroundColor(.accentColor) 109 | .padding(.top) 110 | } 111 | 112 | if showDotLoading { 113 | #if os(tvOS) 114 | ProgressView() 115 | .progressViewStyle(.circular) 116 | .padding() 117 | #else 118 | DotLoadingView() 119 | .frame(width: 60, height: 30) 120 | #endif 121 | 122 | } 123 | } 124 | } 125 | 126 | func attributedView(results: [ParserResult]) -> some View { 127 | VStack(alignment: .leading, spacing: 0) { 128 | ForEach(results) { parsed in 129 | if parsed.isCodeBlock { 130 | #if os(iOS) 131 | CodeBlockView(parserResult: parsed) 132 | .padding(.bottom, 24) 133 | #else 134 | Text(parsed.attributedString) 135 | #if os(iOS) || os(macOS) 136 | .textSelection(.enabled) 137 | #endif 138 | #endif 139 | } else { 140 | Text(parsed.attributedString) 141 | #if os(iOS) || os(macOS) 142 | .textSelection(.enabled) 143 | #endif 144 | } 145 | } 146 | } 147 | } 148 | 149 | #if os(tvOS) 150 | private func rowsFor(text: String) -> [String] { 151 | var rows = [String]() 152 | let maxLinesPerRow = 8 153 | var currentRowText = "" 154 | var currentLineSum = 0 155 | 156 | for char in text { 157 | currentRowText += String(char) 158 | if char == "\n" { 159 | currentLineSum += 1 160 | } 161 | 162 | if currentLineSum >= maxLinesPerRow { 163 | rows.append(currentRowText) 164 | currentLineSum = 0 165 | currentRowText = "" 166 | } 167 | } 168 | 169 | rows.append(currentRowText) 170 | return rows 171 | } 172 | 173 | 174 | func responseTextView(text: String) -> some View { 175 | ForEach(rowsFor(text: text), id: \.self) { text in 176 | Text(text) 177 | .focusable() 178 | .multilineTextAlignment(.leading) 179 | } 180 | } 181 | #endif 182 | 183 | } 184 | 185 | struct MessageRowView_Previews: PreviewProvider { 186 | 187 | static let message = MessageRow( 188 | isInteracting: true, sendImage: "profile", 189 | send: .rawText("What is SwiftUI?"), 190 | responseImage: "openai", 191 | response: responseMessageRowType) 192 | 193 | static let message2 = MessageRow( 194 | isInteracting: false, sendImage: "profile", 195 | send: .rawText("What is SwiftUI?"), 196 | responseImage: "openai", 197 | response: .rawText(""), 198 | responseError: "ChatGPT is currently not available") 199 | 200 | static var previews: some View { 201 | NavigationStack { 202 | ScrollView { 203 | MessageRowView(message: message, retryCallback: { messageRow in 204 | 205 | }) 206 | 207 | MessageRowView(message: message2, retryCallback: { messageRow in 208 | 209 | }) 210 | 211 | } 212 | .previewLayout(.sizeThatFits) 213 | } 214 | } 215 | 216 | static var responseMessageRowType: MessageRowType { 217 | #if os(iOS) 218 | let document = Document(parsing: rawString) 219 | var parser = MarkdownAttributedStringParser() 220 | let results = parser.parserResults(from: document) 221 | return MessageRowType.attributed(.init(string: rawString, results: results)) 222 | #else 223 | MessageRowType.rawText(rawString) 224 | #endif 225 | } 226 | 227 | static var rawString: String { 228 | #if os(iOS) 229 | """ 230 | ## Supported Platforms 231 | 232 | - iOS/tvOS 15 and above 233 | - macOS 12 and above 234 | - watchOS 8 and above 235 | - Linux 236 | 237 | ## Installation 238 | 239 | ### Swift Package Manager 240 | - File > Swift Packages > Add Package Dependency 241 | - Add https://github.com/alfianlosari/ChatGPTSwift.git 242 | 243 | ### Cocoapods 244 | ```ruby 245 | platform :ios, '15.0' 246 | use_frameworks! 247 | 248 | target 'MyApp' do 249 | pod 'ChatGPTSwift', '~> 1.3.1' 250 | end 251 | ``` 252 | 253 | ## Requirement 254 | 255 | Register for API key from [OpenAI](https://openai.com/api). Initialize with api key 256 | 257 | ```swift 258 | let api = ChatGPTAPI(apiKey: "API_KEY") 259 | ``` 260 | 261 | ## Usage 262 | 263 | There are 2 APIs: stream and normal 264 | 265 | ### Stream 266 | 267 | The server will stream chunks of data until complete, the method `AsyncThrowingStream` which you can loop using For-Loop like so: 268 | 269 | ```swift 270 | Task { 271 | do { 272 | let stream = try await api.sendMessageStream(text: "What is ChatGPT?") 273 | for try await line in stream { 274 | print(line) 275 | } 276 | } catch { 277 | print(error.localizedDescription) 278 | } 279 | } 280 | ``` 281 | 282 | ### Normal 283 | A normal HTTP request and response lifecycle. Server will send the complete text (it will take more time to response) 284 | 285 | ```swift 286 | Task { 287 | do { 288 | let response = try await api.sendMessage(text: "What is ChatGPT?") 289 | print(response) 290 | } catch { 291 | print(error.localizedDescription) 292 | } 293 | } 294 | ``` 295 | """ 296 | #else 297 | "SwiftUI is a user interface framework that allows developers to design and develop user interfaces for iOS, macOS, watchOS, and tvOS applications using Swift, a programming language developed by Apple Inc." 298 | #endif 299 | } 300 | } 301 | 302 | 303 | -------------------------------------------------------------------------------- /Shared/ParserResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParserResult.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 19/04/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ParserResult: Identifiable { 11 | 12 | let id = UUID() 13 | let attributedString: AttributedString 14 | let isCodeBlock: Bool 15 | let codeBlockLanguage: String? 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Shared/ResponseParsingTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseParsingTask.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 19/04/23. 6 | // 7 | 8 | import Foundation 9 | import Markdown 10 | 11 | actor ResponseParsingTask { 12 | 13 | func parse(text: String) async -> AttributedOutput { 14 | let document = Document(parsing: text) 15 | var markdownParser = MarkdownAttributedStringParser() 16 | let results = markdownParser.parserResults(from: document) 17 | return AttributedOutput(string: text, results: results) 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Shared/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 02/02/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import AVKit 11 | 12 | class ViewModel: ObservableObject { 13 | 14 | @Published var isInteracting = false 15 | @Published var messages: [MessageRow] = [] 16 | @Published var inputMessage: String = "" 17 | var task: Task? 18 | 19 | #if !os(watchOS) 20 | private var synthesizer: AVSpeechSynthesizer? 21 | #endif 22 | 23 | private var api: LLMClient 24 | 25 | var title: String { 26 | "XCA LLM Chatbot" 27 | } 28 | 29 | var navigationTitle: String { 30 | api.provider.navigationTitle 31 | } 32 | 33 | init(api: LLMClient, enableSpeech: Bool = false) { 34 | self.api = api 35 | #if !os(watchOS) 36 | if enableSpeech { 37 | synthesizer = .init() 38 | } 39 | #endif 40 | } 41 | 42 | func updateClient(_ client: LLMClient) { 43 | self.messages = [] 44 | self.api = client 45 | } 46 | 47 | @MainActor 48 | func sendTapped() async { 49 | #if os(iOS) 50 | self.task = Task { 51 | let text = inputMessage 52 | inputMessage = "" 53 | if api.provider == .chatGPT { 54 | await sendAttributed(text: text) 55 | } else { 56 | await sendAttributedWithoutStream(text: text) 57 | } 58 | } 59 | #else 60 | let text = inputMessage 61 | inputMessage = "" 62 | if api.provider == .chatGPT { 63 | await send(text: text) 64 | } else { 65 | await sendWithoutStream(text: text) 66 | } 67 | #endif 68 | } 69 | 70 | @MainActor 71 | func clearMessages() { 72 | stopSpeaking() 73 | api.deleteHistoryList() 74 | withAnimation { [weak self] in 75 | self?.messages = [] 76 | } 77 | } 78 | 79 | @MainActor 80 | func retry(message: MessageRow) async { 81 | #if os(iOS) 82 | self.task = Task { 83 | guard let index = messages.firstIndex(where: { $0.id == message.id }) else { 84 | return 85 | } 86 | self.messages.remove(at: index) 87 | if api.provider == .chatGPT { 88 | await sendAttributed(text: message.sendText) 89 | } else { 90 | await sendAttributedWithoutStream(text: message.sendText) 91 | } 92 | } 93 | #else 94 | guard let index = messages.firstIndex(where: { $0.id == message.id }) else { 95 | return 96 | } 97 | self.messages.remove(at: index) 98 | if api.provider == .chatGPT { 99 | await send(text: message.sendText) 100 | } else { 101 | await sendWithoutStream(text: message.sendText) 102 | } 103 | #endif 104 | } 105 | 106 | func cancelStreamingResponse() { 107 | self.task?.cancel() 108 | self.task = nil 109 | } 110 | 111 | #if os(iOS) 112 | @MainActor 113 | private func sendAttributed(text: String) async { 114 | isInteracting = true 115 | var streamText = "" 116 | 117 | var messageRow = MessageRow( 118 | isInteracting: true, 119 | sendImage: "profile", 120 | send: .rawText(text), 121 | responseImage: api.provider.imageName, 122 | response: .rawText(streamText), 123 | responseError: nil) 124 | 125 | do { 126 | let parsingTask = ResponseParsingTask() 127 | let attributedSend = await parsingTask.parse(text: text) 128 | try Task.checkCancellation() 129 | messageRow.send = .attributed(attributedSend) 130 | 131 | self.messages.append(messageRow) 132 | 133 | let parserThresholdTextCount = 64 134 | var currentTextCount = 0 135 | var currentOutput: AttributedOutput? 136 | 137 | let stream = try await api.sendMessageStream(text: text) 138 | for try await text in stream { 139 | streamText += text 140 | currentTextCount += text.count 141 | 142 | if currentTextCount >= parserThresholdTextCount || text.contains("```") { 143 | currentOutput = await parsingTask.parse(text: streamText) 144 | try Task.checkCancellation() 145 | currentTextCount = 0 146 | } 147 | 148 | if let currentOutput = currentOutput, !currentOutput.results.isEmpty { 149 | let suffixText = streamText.trimmingPrefix(currentOutput.string) 150 | var results = currentOutput.results 151 | let lastResult = results[results.count - 1] 152 | var lastAttrString = lastResult.attributedString 153 | if lastResult.isCodeBlock { 154 | lastAttrString.append(AttributedString(String(suffixText), attributes: .init([.font: UIFont.systemFont(ofSize: 12).apply(newTraits: .traitMonoSpace), .foregroundColor: UIColor.white]))) 155 | } else { 156 | lastAttrString.append(AttributedString(String(suffixText))) 157 | } 158 | results[results.count - 1] = ParserResult(attributedString: lastAttrString, isCodeBlock: lastResult.isCodeBlock, codeBlockLanguage: lastResult.codeBlockLanguage) 159 | messageRow.response = .attributed(.init(string: streamText, results: results)) 160 | } else { 161 | messageRow.response = .attributed(.init(string: streamText, results: [ 162 | ParserResult(attributedString: AttributedString(stringLiteral: streamText), isCodeBlock: false, codeBlockLanguage: nil) 163 | ])) 164 | } 165 | 166 | self.messages[self.messages.count - 1] = messageRow 167 | if let currentString = currentOutput?.string, currentString != streamText { 168 | let output = await parsingTask.parse(text: streamText) 169 | try Task.checkCancellation() 170 | messageRow.response = .attributed(output) 171 | } 172 | } 173 | } catch is CancellationError { 174 | messageRow.responseError = "The response was cancelled" 175 | } catch { 176 | messageRow.responseError = error.localizedDescription 177 | } 178 | 179 | if messageRow.response == nil { 180 | messageRow.response = .rawText(streamText) 181 | } 182 | 183 | messageRow.isInteracting = false 184 | self.messages[self.messages.count - 1] = messageRow 185 | isInteracting = false 186 | speakLastResponse() 187 | } 188 | 189 | @MainActor 190 | private func sendAttributedWithoutStream(text: String) async { 191 | isInteracting = true 192 | var messageRow = MessageRow( 193 | isInteracting: true, 194 | sendImage: "profile", 195 | send: .rawText(text), 196 | responseImage: api.provider.imageName, 197 | response: .rawText(""), 198 | responseError: nil) 199 | 200 | self.messages.append(messageRow) 201 | 202 | do { 203 | let responseText = try await api.sendMessage(text) 204 | try Task.checkCancellation() 205 | 206 | let parsingTask = ResponseParsingTask() 207 | let output = await parsingTask.parse(text: responseText) 208 | try Task.checkCancellation() 209 | 210 | messageRow.response = .attributed(output) 211 | 212 | } catch { 213 | messageRow.responseError = error.localizedDescription 214 | } 215 | 216 | messageRow.isInteracting = false 217 | self.messages[self.messages.count - 1] = messageRow 218 | isInteracting = false 219 | speakLastResponse() 220 | 221 | } 222 | #endif 223 | 224 | @MainActor 225 | private func send(text: String) async { 226 | isInteracting = true 227 | var streamText = "" 228 | var messageRow = MessageRow( 229 | isInteracting: true, 230 | sendImage: "profile", 231 | send: .rawText(text), 232 | responseImage: api.provider.imageName, 233 | response: .rawText(streamText), 234 | responseError: nil) 235 | 236 | self.messages.append(messageRow) 237 | 238 | do { 239 | let stream = try await api.sendMessageStream(text: text) 240 | for try await text in stream { 241 | streamText += text 242 | messageRow.response = .rawText(streamText.trimmingCharacters(in: .whitespacesAndNewlines)) 243 | self.messages[self.messages.count - 1] = messageRow 244 | } 245 | } catch { 246 | messageRow.responseError = error.localizedDescription 247 | } 248 | 249 | messageRow.isInteracting = false 250 | self.messages[self.messages.count - 1] = messageRow 251 | isInteracting = false 252 | speakLastResponse() 253 | 254 | } 255 | 256 | @MainActor 257 | private func sendWithoutStream(text: String) async { 258 | isInteracting = true 259 | var messageRow = MessageRow( 260 | isInteracting: true, 261 | sendImage: "profile", 262 | send: .rawText(text), 263 | responseImage: api.provider.imageName, 264 | response: .rawText(""), 265 | responseError: nil) 266 | 267 | self.messages.append(messageRow) 268 | 269 | do { 270 | let responseText = try await api.sendMessage(text) 271 | try Task.checkCancellation() 272 | messageRow.response = .rawText(responseText) 273 | } catch { 274 | messageRow.responseError = error.localizedDescription 275 | } 276 | 277 | messageRow.isInteracting = false 278 | self.messages[self.messages.count - 1] = messageRow 279 | isInteracting = false 280 | speakLastResponse() 281 | } 282 | 283 | func speakLastResponse() { 284 | #if !os(watchOS) 285 | guard let synthesizer, let responseText = self.messages.last?.responseText, !responseText.isEmpty else { 286 | return 287 | } 288 | stopSpeaking() 289 | let utterance = AVSpeechUtterance(string: responseText) 290 | utterance.voice = .init(language: "en-US") 291 | utterance.rate = 0.5 292 | utterance.pitchMultiplier = 0.8 293 | utterance.postUtteranceDelay = 0.2 294 | synthesizer.speak(utterance ) 295 | #endif 296 | } 297 | 298 | func stopSpeaking() { 299 | #if !os(watchOS) 300 | synthesizer?.stopSpeaking(at: .immediate) 301 | #endif 302 | } 303 | 304 | } 305 | 306 | 307 | 308 | -------------------------------------------------------------------------------- /XCAChatGPT.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8B057585298E52C000A56C9A /* XCAChatGPTMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B057584298E52C000A56C9A /* XCAChatGPTMacApp.swift */; }; 11 | 8B057589298E52C000A56C9A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B057588298E52C000A56C9A /* Assets.xcassets */; }; 12 | 8B05758C298E52C000A56C9A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B05758B298E52C000A56C9A /* Preview Assets.xcassets */; }; 13 | 8B057592298E52D900A56C9A /* DotLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */; }; 14 | 8B057593298E52D900A56C9A /* ChatGPTAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */; }; 15 | 8B057594298E52D900A56C9A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C005298AD09E0079AF26 /* ContentView.swift */; }; 16 | 8B057595298E52D900A56C9A /* MessageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */; }; 17 | 8B057596298E52DD00A56C9A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C013298ADC560079AF26 /* ViewModel.swift */; }; 18 | 8B057597298E52DD00A56C9A /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C015298ADC9D0079AF26 /* MessageRow.swift */; }; 19 | 8B0575F1298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 8B0575F0298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 20 | 8B0575F6298FA9D800A56C9A /* XCAChatGPTWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B0575F5298FA9D800A56C9A /* XCAChatGPTWatchApp.swift */; }; 21 | 8B0575FA298FA9DA00A56C9A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B0575F9298FA9DA00A56C9A /* Assets.xcassets */; }; 22 | 8B0575FD298FA9DA00A56C9A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B0575FC298FA9DA00A56C9A /* Preview Assets.xcassets */; }; 23 | 8B057605298FA9E900A56C9A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C005298AD09E0079AF26 /* ContentView.swift */; }; 24 | 8B057606298FA9E900A56C9A /* MessageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */; }; 25 | 8B057607298FA9E900A56C9A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C013298ADC560079AF26 /* ViewModel.swift */; }; 26 | 8B057608298FA9E900A56C9A /* DotLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */; }; 27 | 8B057609298FA9E900A56C9A /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C015298ADC9D0079AF26 /* MessageRow.swift */; }; 28 | 8B05760A298FA9E900A56C9A /* ChatGPTAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */; }; 29 | 8B057614298FBDB600A56C9A /* XCAChatGPTTVApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B057613298FBDB600A56C9A /* XCAChatGPTTVApp.swift */; }; 30 | 8B057618298FBDB700A56C9A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B057617298FBDB700A56C9A /* Assets.xcassets */; }; 31 | 8B05761B298FBDB700A56C9A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B05761A298FBDB700A56C9A /* Preview Assets.xcassets */; }; 32 | 8B05761F298FBE0400A56C9A /* ChatGPTAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */; }; 33 | 8B057620298FBE0400A56C9A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C005298AD09E0079AF26 /* ContentView.swift */; }; 34 | 8B057621298FBE0400A56C9A /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C015298ADC9D0079AF26 /* MessageRow.swift */; }; 35 | 8B057622298FBE0400A56C9A /* MessageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */; }; 36 | 8B057623298FBE0400A56C9A /* DotLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */; }; 37 | 8B057624298FBE0400A56C9A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C013298ADC560079AF26 /* ViewModel.swift */; }; 38 | 8B05764829909A9200A56C9A /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05764729909A9200A56C9A /* ScrollView.swift */; }; 39 | 8B612E2529D68CC9008DF5AF /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B612E2229D68CC9008DF5AF /* TextView.swift */; }; 40 | 8B612E2629D68CC9008DF5AF /* TokenizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B612E2329D68CC9008DF5AF /* TokenizerView.swift */; }; 41 | 8B612E2729D68CC9008DF5AF /* TokenizerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B612E2429D68CC9008DF5AF /* TokenizerViewModel.swift */; }; 42 | 8B612E2A29D68CE3008DF5AF /* GPTEncoder in Frameworks */ = {isa = PBXBuildFile; productRef = 8B612E2929D68CE3008DF5AF /* GPTEncoder */; }; 43 | 8B75A1B12A2CB7C600E8810E /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 8B75A1B02A2CB7C600E8810E /* GoogleGenerativeAI */; }; 44 | 8B75A1B42A2CB7D200E8810E /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 8B75A1B32A2CB7D200E8810E /* GoogleGenerativeAI */; }; 45 | 8B75A1B62A2CB7D500E8810E /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 8B75A1B52A2CB7D500E8810E /* GoogleGenerativeAI */; }; 46 | 8B75A1B82A2CB7DA00E8810E /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 8B75A1B72A2CB7DA00E8810E /* GoogleGenerativeAI */; }; 47 | 8B75A1BE2A2CB83900E8810E /* LLMClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */; }; 48 | 8B75A1BF2A2CB83900E8810E /* LLMClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */; }; 49 | 8B75A1C02A2CB83900E8810E /* LLMClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */; }; 50 | 8B75A1C12A2CB83900E8810E /* LLMClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */; }; 51 | 8B75A1C22A2CB83900E8810E /* PaLMChatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */; }; 52 | 8B75A1C32A2CB83900E8810E /* PaLMChatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */; }; 53 | 8B75A1C42A2CB83900E8810E /* PaLMChatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */; }; 54 | 8B75A1C52A2CB83900E8810E /* PaLMChatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */; }; 55 | 8B75A1C62A2CB83900E8810E /* LLMConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */; }; 56 | 8B75A1C72A2CB83900E8810E /* LLMConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */; }; 57 | 8B75A1C82A2CB83900E8810E /* LLMConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */; }; 58 | 8B75A1C92A2CB83900E8810E /* LLMConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */; }; 59 | 8B75A1CA2A2CB83900E8810E /* LLMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */; }; 60 | 8B75A1CB2A2CB83900E8810E /* LLMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */; }; 61 | 8B75A1CC2A2CB83900E8810E /* LLMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */; }; 62 | 8B75A1CD2A2CB83900E8810E /* LLMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */; }; 63 | 8B75A1CF2A2CB8DC00E8810E /* LLMConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */; }; 64 | 8B75A1D02A2CB8DC00E8810E /* LLMConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */; }; 65 | 8B75A1D12A2CB8DC00E8810E /* LLMConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */; }; 66 | 8B75A1D22A2CB8DC00E8810E /* LLMConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */; }; 67 | 8B82463429B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; }; 68 | 8B82463529B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; }; 69 | 8B82463629B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; }; 70 | 8B82463729B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; }; 71 | 8B91C004298AD09E0079AF26 /* XCAChatGPTApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C003298AD09E0079AF26 /* XCAChatGPTApp.swift */; }; 72 | 8B91C006298AD09E0079AF26 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C005298AD09E0079AF26 /* ContentView.swift */; }; 73 | 8B91C008298AD09F0079AF26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B91C007298AD09F0079AF26 /* Assets.xcassets */; }; 74 | 8B91C00B298AD09F0079AF26 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B91C00A298AD09F0079AF26 /* Preview Assets.xcassets */; }; 75 | 8B91C012298AD0CE0079AF26 /* ChatGPTAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */; }; 76 | 8B91C014298ADC560079AF26 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C013298ADC560079AF26 /* ViewModel.swift */; }; 77 | 8B91C016298ADC9D0079AF26 /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C015298ADC9D0079AF26 /* MessageRow.swift */; }; 78 | 8B91C018298ADF4E0079AF26 /* DotLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */; }; 79 | 8B91C01A298ADF7F0079AF26 /* MessageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */; }; 80 | 8BC0F8D029F157C800D6B492 /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 8BC0F8CF29F157C800D6B492 /* Markdown */; }; 81 | 8BC0F8D329F157E600D6B492 /* Highlighter in Frameworks */ = {isa = PBXBuildFile; productRef = 8BC0F8D229F157E600D6B492 /* Highlighter */; }; 82 | 8BC0F8D529F1581800D6B492 /* MarkdownAttributedStringParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8D429F1581800D6B492 /* MarkdownAttributedStringParser.swift */; }; 83 | 8BC0F8D729F1583300D6B492 /* ParserResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8D629F1583300D6B492 /* ParserResult.swift */; }; 84 | 8BC0F8D829F1583300D6B492 /* ParserResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8D629F1583300D6B492 /* ParserResult.swift */; }; 85 | 8BC0F8D929F1583300D6B492 /* ParserResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8D629F1583300D6B492 /* ParserResult.swift */; }; 86 | 8BC0F8DA29F1583300D6B492 /* ParserResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8D629F1583300D6B492 /* ParserResult.swift */; }; 87 | 8BC0F8DC29F1587600D6B492 /* ResponseParsingTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8DB29F1587600D6B492 /* ResponseParsingTask.swift */; }; 88 | 8BC0F8DE29F1589600D6B492 /* CodeBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0F8DD29F1589600D6B492 /* CodeBlockView.swift */; }; 89 | /* End PBXBuildFile section */ 90 | 91 | /* Begin PBXContainerItemProxy section */ 92 | 8B0575F2298FA9D800A56C9A /* PBXContainerItemProxy */ = { 93 | isa = PBXContainerItemProxy; 94 | containerPortal = 8B91BFF8298AD09E0079AF26 /* Project object */; 95 | proxyType = 1; 96 | remoteGlobalIDString = 8B0575EF298FA9D800A56C9A; 97 | remoteInfo = "XCAChatGPTWatch Watch App"; 98 | }; 99 | /* End PBXContainerItemProxy section */ 100 | 101 | /* Begin PBXCopyFilesBuildPhase section */ 102 | 8B057600298FA9DA00A56C9A /* Embed Watch Content */ = { 103 | isa = PBXCopyFilesBuildPhase; 104 | buildActionMask = 2147483647; 105 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; 106 | dstSubfolderSpec = 16; 107 | files = ( 108 | 8B0575F1298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app in Embed Watch Content */, 109 | ); 110 | name = "Embed Watch Content"; 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | /* End PBXCopyFilesBuildPhase section */ 114 | 115 | /* Begin PBXFileReference section */ 116 | 8B057582298E52C000A56C9A /* XCAChatGPTMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCAChatGPTMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 117 | 8B057584298E52C000A56C9A /* XCAChatGPTMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAChatGPTMacApp.swift; sourceTree = ""; }; 118 | 8B057588298E52C000A56C9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 119 | 8B05758B298E52C000A56C9A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 120 | 8B05758D298E52C000A56C9A /* XCAChatGPTMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = XCAChatGPTMac.entitlements; sourceTree = ""; }; 121 | 8B0575EB298FA9D800A56C9A /* XCAChatGPTWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCAChatGPTWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 122 | 8B0575F0298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "XCAChatGPTWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 123 | 8B0575F5298FA9D800A56C9A /* XCAChatGPTWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAChatGPTWatchApp.swift; sourceTree = ""; }; 124 | 8B0575F9298FA9DA00A56C9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 125 | 8B0575FC298FA9DA00A56C9A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 126 | 8B057611298FBDB600A56C9A /* XCAChatGPTTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCAChatGPTTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 127 | 8B057613298FBDB600A56C9A /* XCAChatGPTTVApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAChatGPTTVApp.swift; sourceTree = ""; }; 128 | 8B057617298FBDB700A56C9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 129 | 8B05761A298FBDB700A56C9A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 130 | 8B05764729909A9200A56C9A /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = ""; }; 131 | 8B612E2229D68CC9008DF5AF /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; 132 | 8B612E2329D68CC9008DF5AF /* TokenizerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenizerView.swift; sourceTree = ""; }; 133 | 8B612E2429D68CC9008DF5AF /* TokenizerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenizerViewModel.swift; sourceTree = ""; }; 134 | 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMClient.swift; sourceTree = ""; }; 135 | 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaLMChatAPI.swift; sourceTree = ""; }; 136 | 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMConfig.swift; sourceTree = ""; }; 137 | 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMProvider.swift; sourceTree = ""; }; 138 | 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMConfigView.swift; sourceTree = ""; }; 139 | 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatGPTAPIModels.swift; sourceTree = ""; }; 140 | 8B91C000298AD09E0079AF26 /* XCAChatGPT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCAChatGPT.app; sourceTree = BUILT_PRODUCTS_DIR; }; 141 | 8B91C003298AD09E0079AF26 /* XCAChatGPTApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAChatGPTApp.swift; sourceTree = ""; }; 142 | 8B91C005298AD09E0079AF26 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 143 | 8B91C007298AD09F0079AF26 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 144 | 8B91C00A298AD09F0079AF26 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 145 | 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatGPTAPI.swift; sourceTree = ""; }; 146 | 8B91C013298ADC560079AF26 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 147 | 8B91C015298ADC9D0079AF26 /* MessageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRow.swift; sourceTree = ""; }; 148 | 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotLoadingView.swift; sourceTree = ""; }; 149 | 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRowView.swift; sourceTree = ""; }; 150 | 8BC0F8D429F1581800D6B492 /* MarkdownAttributedStringParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownAttributedStringParser.swift; sourceTree = ""; }; 151 | 8BC0F8D629F1583300D6B492 /* ParserResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserResult.swift; sourceTree = ""; }; 152 | 8BC0F8DB29F1587600D6B492 /* ResponseParsingTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseParsingTask.swift; sourceTree = ""; }; 153 | 8BC0F8DD29F1589600D6B492 /* CodeBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlockView.swift; sourceTree = ""; }; 154 | /* End PBXFileReference section */ 155 | 156 | /* Begin PBXFrameworksBuildPhase section */ 157 | 8B05757F298E52C000A56C9A /* Frameworks */ = { 158 | isa = PBXFrameworksBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 8B75A1B42A2CB7D200E8810E /* GoogleGenerativeAI in Frameworks */, 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | }; 165 | 8B0575ED298FA9D800A56C9A /* Frameworks */ = { 166 | isa = PBXFrameworksBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 8B75A1B62A2CB7D500E8810E /* GoogleGenerativeAI in Frameworks */, 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | 8B05760E298FBDB600A56C9A /* Frameworks */ = { 174 | isa = PBXFrameworksBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | 8B75A1B82A2CB7DA00E8810E /* GoogleGenerativeAI in Frameworks */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | 8B91BFFD298AD09E0079AF26 /* Frameworks */ = { 182 | isa = PBXFrameworksBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | 8B612E2A29D68CE3008DF5AF /* GPTEncoder in Frameworks */, 186 | 8BC0F8D329F157E600D6B492 /* Highlighter in Frameworks */, 187 | 8BC0F8D029F157C800D6B492 /* Markdown in Frameworks */, 188 | 8B75A1B12A2CB7C600E8810E /* GoogleGenerativeAI in Frameworks */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXFrameworksBuildPhase section */ 193 | 194 | /* Begin PBXGroup section */ 195 | 8B057583298E52C000A56C9A /* XCAChatGPTMac */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 8B057584298E52C000A56C9A /* XCAChatGPTMacApp.swift */, 199 | 8B057588298E52C000A56C9A /* Assets.xcassets */, 200 | 8B05758D298E52C000A56C9A /* XCAChatGPTMac.entitlements */, 201 | 8B05758A298E52C000A56C9A /* Preview Content */, 202 | ); 203 | path = XCAChatGPTMac; 204 | sourceTree = ""; 205 | }; 206 | 8B05758A298E52C000A56C9A /* Preview Content */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 8B05758B298E52C000A56C9A /* Preview Assets.xcassets */, 210 | ); 211 | path = "Preview Content"; 212 | sourceTree = ""; 213 | }; 214 | 8B057591298E52CB00A56C9A /* Shared */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 8B75A1B92A2CB81400E8810E /* LLM Provider */, 218 | 8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */, 219 | 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */, 220 | 8B91C005298AD09E0079AF26 /* ContentView.swift */, 221 | 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */, 222 | 8B91C019298ADF7F0079AF26 /* MessageRowView.swift */, 223 | 8B91C013298ADC560079AF26 /* ViewModel.swift */, 224 | 8B91C015298ADC9D0079AF26 /* MessageRow.swift */, 225 | 8BC0F8D429F1581800D6B492 /* MarkdownAttributedStringParser.swift */, 226 | 8BC0F8D629F1583300D6B492 /* ParserResult.swift */, 227 | 8BC0F8DB29F1587600D6B492 /* ResponseParsingTask.swift */, 228 | 8BC0F8DD29F1589600D6B492 /* CodeBlockView.swift */, 229 | 8B75A1CE2A2CB8DC00E8810E /* LLMConfigView.swift */, 230 | ); 231 | path = Shared; 232 | sourceTree = ""; 233 | }; 234 | 8B0575F4298FA9D800A56C9A /* XCAChatGPTWatch Watch App */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 8B0575F5298FA9D800A56C9A /* XCAChatGPTWatchApp.swift */, 238 | 8B0575F9298FA9DA00A56C9A /* Assets.xcassets */, 239 | 8B0575FB298FA9DA00A56C9A /* Preview Content */, 240 | ); 241 | path = "XCAChatGPTWatch Watch App"; 242 | sourceTree = ""; 243 | }; 244 | 8B0575FB298FA9DA00A56C9A /* Preview Content */ = { 245 | isa = PBXGroup; 246 | children = ( 247 | 8B0575FC298FA9DA00A56C9A /* Preview Assets.xcassets */, 248 | ); 249 | path = "Preview Content"; 250 | sourceTree = ""; 251 | }; 252 | 8B057612298FBDB600A56C9A /* XCAChatGPTTV */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 8B057613298FBDB600A56C9A /* XCAChatGPTTVApp.swift */, 256 | 8B057617298FBDB700A56C9A /* Assets.xcassets */, 257 | 8B057619298FBDB700A56C9A /* Preview Content */, 258 | 8B05764729909A9200A56C9A /* ScrollView.swift */, 259 | ); 260 | path = XCAChatGPTTV; 261 | sourceTree = ""; 262 | }; 263 | 8B057619298FBDB700A56C9A /* Preview Content */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | 8B05761A298FBDB700A56C9A /* Preview Assets.xcassets */, 267 | ); 268 | path = "Preview Content"; 269 | sourceTree = ""; 270 | }; 271 | 8B75A1B22A2CB7CE00E8810E /* Frameworks */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | ); 275 | name = Frameworks; 276 | sourceTree = ""; 277 | }; 278 | 8B75A1B92A2CB81400E8810E /* LLM Provider */ = { 279 | isa = PBXGroup; 280 | children = ( 281 | 8B75A1BA2A2CB83900E8810E /* LLMClient.swift */, 282 | 8B75A1BC2A2CB83900E8810E /* LLMConfig.swift */, 283 | 8B75A1BD2A2CB83900E8810E /* LLMProvider.swift */, 284 | 8B75A1BB2A2CB83900E8810E /* PaLMChatAPI.swift */, 285 | ); 286 | path = "LLM Provider"; 287 | sourceTree = ""; 288 | }; 289 | 8B91BFF7298AD09E0079AF26 = { 290 | isa = PBXGroup; 291 | children = ( 292 | 8B057591298E52CB00A56C9A /* Shared */, 293 | 8B91C002298AD09E0079AF26 /* XCAChatGPT */, 294 | 8B057583298E52C000A56C9A /* XCAChatGPTMac */, 295 | 8B0575F4298FA9D800A56C9A /* XCAChatGPTWatch Watch App */, 296 | 8B057612298FBDB600A56C9A /* XCAChatGPTTV */, 297 | 8B91C001298AD09E0079AF26 /* Products */, 298 | 8B75A1B22A2CB7CE00E8810E /* Frameworks */, 299 | ); 300 | sourceTree = ""; 301 | }; 302 | 8B91C001298AD09E0079AF26 /* Products */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | 8B91C000298AD09E0079AF26 /* XCAChatGPT.app */, 306 | 8B057582298E52C000A56C9A /* XCAChatGPTMac.app */, 307 | 8B0575EB298FA9D800A56C9A /* XCAChatGPTWatch.app */, 308 | 8B0575F0298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app */, 309 | 8B057611298FBDB600A56C9A /* XCAChatGPTTV.app */, 310 | ); 311 | name = Products; 312 | sourceTree = ""; 313 | }; 314 | 8B91C002298AD09E0079AF26 /* XCAChatGPT */ = { 315 | isa = PBXGroup; 316 | children = ( 317 | 8B91C003298AD09E0079AF26 /* XCAChatGPTApp.swift */, 318 | 8B612E2229D68CC9008DF5AF /* TextView.swift */, 319 | 8B612E2329D68CC9008DF5AF /* TokenizerView.swift */, 320 | 8B612E2429D68CC9008DF5AF /* TokenizerViewModel.swift */, 321 | 8B91C007298AD09F0079AF26 /* Assets.xcassets */, 322 | 8B91C009298AD09F0079AF26 /* Preview Content */, 323 | ); 324 | path = XCAChatGPT; 325 | sourceTree = ""; 326 | }; 327 | 8B91C009298AD09F0079AF26 /* Preview Content */ = { 328 | isa = PBXGroup; 329 | children = ( 330 | 8B91C00A298AD09F0079AF26 /* Preview Assets.xcassets */, 331 | ); 332 | path = "Preview Content"; 333 | sourceTree = ""; 334 | }; 335 | /* End PBXGroup section */ 336 | 337 | /* Begin PBXNativeTarget section */ 338 | 8B057581298E52C000A56C9A /* XCAChatGPTMac */ = { 339 | isa = PBXNativeTarget; 340 | buildConfigurationList = 8B057590298E52C000A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTMac" */; 341 | buildPhases = ( 342 | 8B05757E298E52C000A56C9A /* Sources */, 343 | 8B05757F298E52C000A56C9A /* Frameworks */, 344 | 8B057580298E52C000A56C9A /* Resources */, 345 | ); 346 | buildRules = ( 347 | ); 348 | dependencies = ( 349 | ); 350 | name = XCAChatGPTMac; 351 | packageProductDependencies = ( 352 | 8B75A1B32A2CB7D200E8810E /* GoogleGenerativeAI */, 353 | ); 354 | productName = XCAChatGPTMac; 355 | productReference = 8B057582298E52C000A56C9A /* XCAChatGPTMac.app */; 356 | productType = "com.apple.product-type.application"; 357 | }; 358 | 8B0575EA298FA9D800A56C9A /* XCAChatGPTWatch */ = { 359 | isa = PBXNativeTarget; 360 | buildConfigurationList = 8B057604298FA9DA00A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTWatch" */; 361 | buildPhases = ( 362 | 8B0575E9298FA9D800A56C9A /* Resources */, 363 | 8B057600298FA9DA00A56C9A /* Embed Watch Content */, 364 | ); 365 | buildRules = ( 366 | ); 367 | dependencies = ( 368 | 8B0575F3298FA9D800A56C9A /* PBXTargetDependency */, 369 | ); 370 | name = XCAChatGPTWatch; 371 | productName = XCAChatGPTWatch; 372 | productReference = 8B0575EB298FA9D800A56C9A /* XCAChatGPTWatch.app */; 373 | productType = "com.apple.product-type.application.watchapp2-container"; 374 | }; 375 | 8B0575EF298FA9D800A56C9A /* XCAChatGPTWatch Watch App */ = { 376 | isa = PBXNativeTarget; 377 | buildConfigurationList = 8B057603298FA9DA00A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTWatch Watch App" */; 378 | buildPhases = ( 379 | 8B0575EC298FA9D800A56C9A /* Sources */, 380 | 8B0575ED298FA9D800A56C9A /* Frameworks */, 381 | 8B0575EE298FA9D800A56C9A /* Resources */, 382 | ); 383 | buildRules = ( 384 | ); 385 | dependencies = ( 386 | ); 387 | name = "XCAChatGPTWatch Watch App"; 388 | packageProductDependencies = ( 389 | 8B75A1B52A2CB7D500E8810E /* GoogleGenerativeAI */, 390 | ); 391 | productName = "XCAChatGPTWatch Watch App"; 392 | productReference = 8B0575F0298FA9D800A56C9A /* XCAChatGPTWatch Watch App.app */; 393 | productType = "com.apple.product-type.application"; 394 | }; 395 | 8B057610298FBDB600A56C9A /* XCAChatGPTTV */ = { 396 | isa = PBXNativeTarget; 397 | buildConfigurationList = 8B05761C298FBDB700A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTTV" */; 398 | buildPhases = ( 399 | 8B05760D298FBDB600A56C9A /* Sources */, 400 | 8B05760E298FBDB600A56C9A /* Frameworks */, 401 | 8B05760F298FBDB600A56C9A /* Resources */, 402 | ); 403 | buildRules = ( 404 | ); 405 | dependencies = ( 406 | ); 407 | name = XCAChatGPTTV; 408 | packageProductDependencies = ( 409 | 8B75A1B72A2CB7DA00E8810E /* GoogleGenerativeAI */, 410 | ); 411 | productName = XCAChatGPTTV; 412 | productReference = 8B057611298FBDB600A56C9A /* XCAChatGPTTV.app */; 413 | productType = "com.apple.product-type.application"; 414 | }; 415 | 8B91BFFF298AD09E0079AF26 /* XCAChatGPT */ = { 416 | isa = PBXNativeTarget; 417 | buildConfigurationList = 8B91C00E298AD09F0079AF26 /* Build configuration list for PBXNativeTarget "XCAChatGPT" */; 418 | buildPhases = ( 419 | 8B91BFFC298AD09E0079AF26 /* Sources */, 420 | 8B91BFFD298AD09E0079AF26 /* Frameworks */, 421 | 8B91BFFE298AD09E0079AF26 /* Resources */, 422 | ); 423 | buildRules = ( 424 | ); 425 | dependencies = ( 426 | ); 427 | name = XCAChatGPT; 428 | packageProductDependencies = ( 429 | 8B612E2929D68CE3008DF5AF /* GPTEncoder */, 430 | 8BC0F8CF29F157C800D6B492 /* Markdown */, 431 | 8BC0F8D229F157E600D6B492 /* Highlighter */, 432 | 8B75A1B02A2CB7C600E8810E /* GoogleGenerativeAI */, 433 | ); 434 | productName = XCAChatGPT; 435 | productReference = 8B91C000298AD09E0079AF26 /* XCAChatGPT.app */; 436 | productType = "com.apple.product-type.application"; 437 | }; 438 | /* End PBXNativeTarget section */ 439 | 440 | /* Begin PBXProject section */ 441 | 8B91BFF8298AD09E0079AF26 /* Project object */ = { 442 | isa = PBXProject; 443 | attributes = { 444 | BuildIndependentTargetsInParallel = 1; 445 | LastSwiftUpdateCheck = 1420; 446 | LastUpgradeCheck = 1420; 447 | TargetAttributes = { 448 | 8B057581298E52C000A56C9A = { 449 | CreatedOnToolsVersion = 14.2; 450 | }; 451 | 8B0575EA298FA9D800A56C9A = { 452 | CreatedOnToolsVersion = 14.2; 453 | }; 454 | 8B0575EF298FA9D800A56C9A = { 455 | CreatedOnToolsVersion = 14.2; 456 | }; 457 | 8B057610298FBDB600A56C9A = { 458 | CreatedOnToolsVersion = 14.2; 459 | }; 460 | 8B91BFFF298AD09E0079AF26 = { 461 | CreatedOnToolsVersion = 14.2; 462 | }; 463 | }; 464 | }; 465 | buildConfigurationList = 8B91BFFB298AD09E0079AF26 /* Build configuration list for PBXProject "XCAChatGPT" */; 466 | compatibilityVersion = "Xcode 14.0"; 467 | developmentRegion = en; 468 | hasScannedForEncodings = 0; 469 | knownRegions = ( 470 | en, 471 | Base, 472 | ); 473 | mainGroup = 8B91BFF7298AD09E0079AF26; 474 | packageReferences = ( 475 | 8B612E2829D68CE3008DF5AF /* XCRemoteSwiftPackageReference "GPTEncoder" */, 476 | 8BC0F8CE29F157C800D6B492 /* XCRemoteSwiftPackageReference "swift-markdown" */, 477 | 8BC0F8D129F157E600D6B492 /* XCRemoteSwiftPackageReference "HighlighterSwift" */, 478 | 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */, 479 | ); 480 | productRefGroup = 8B91C001298AD09E0079AF26 /* Products */; 481 | projectDirPath = ""; 482 | projectRoot = ""; 483 | targets = ( 484 | 8B91BFFF298AD09E0079AF26 /* XCAChatGPT */, 485 | 8B057581298E52C000A56C9A /* XCAChatGPTMac */, 486 | 8B0575EA298FA9D800A56C9A /* XCAChatGPTWatch */, 487 | 8B0575EF298FA9D800A56C9A /* XCAChatGPTWatch Watch App */, 488 | 8B057610298FBDB600A56C9A /* XCAChatGPTTV */, 489 | ); 490 | }; 491 | /* End PBXProject section */ 492 | 493 | /* Begin PBXResourcesBuildPhase section */ 494 | 8B057580298E52C000A56C9A /* Resources */ = { 495 | isa = PBXResourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | 8B05758C298E52C000A56C9A /* Preview Assets.xcassets in Resources */, 499 | 8B057589298E52C000A56C9A /* Assets.xcassets in Resources */, 500 | ); 501 | runOnlyForDeploymentPostprocessing = 0; 502 | }; 503 | 8B0575E9298FA9D800A56C9A /* Resources */ = { 504 | isa = PBXResourcesBuildPhase; 505 | buildActionMask = 2147483647; 506 | files = ( 507 | ); 508 | runOnlyForDeploymentPostprocessing = 0; 509 | }; 510 | 8B0575EE298FA9D800A56C9A /* Resources */ = { 511 | isa = PBXResourcesBuildPhase; 512 | buildActionMask = 2147483647; 513 | files = ( 514 | 8B0575FD298FA9DA00A56C9A /* Preview Assets.xcassets in Resources */, 515 | 8B0575FA298FA9DA00A56C9A /* Assets.xcassets in Resources */, 516 | ); 517 | runOnlyForDeploymentPostprocessing = 0; 518 | }; 519 | 8B05760F298FBDB600A56C9A /* Resources */ = { 520 | isa = PBXResourcesBuildPhase; 521 | buildActionMask = 2147483647; 522 | files = ( 523 | 8B05761B298FBDB700A56C9A /* Preview Assets.xcassets in Resources */, 524 | 8B057618298FBDB700A56C9A /* Assets.xcassets in Resources */, 525 | ); 526 | runOnlyForDeploymentPostprocessing = 0; 527 | }; 528 | 8B91BFFE298AD09E0079AF26 /* Resources */ = { 529 | isa = PBXResourcesBuildPhase; 530 | buildActionMask = 2147483647; 531 | files = ( 532 | 8B91C00B298AD09F0079AF26 /* Preview Assets.xcassets in Resources */, 533 | 8B91C008298AD09F0079AF26 /* Assets.xcassets in Resources */, 534 | ); 535 | runOnlyForDeploymentPostprocessing = 0; 536 | }; 537 | /* End PBXResourcesBuildPhase section */ 538 | 539 | /* Begin PBXSourcesBuildPhase section */ 540 | 8B05757E298E52C000A56C9A /* Sources */ = { 541 | isa = PBXSourcesBuildPhase; 542 | buildActionMask = 2147483647; 543 | files = ( 544 | 8B75A1D02A2CB8DC00E8810E /* LLMConfigView.swift in Sources */, 545 | 8B057597298E52DD00A56C9A /* MessageRow.swift in Sources */, 546 | 8B75A1CB2A2CB83900E8810E /* LLMProvider.swift in Sources */, 547 | 8B057593298E52D900A56C9A /* ChatGPTAPI.swift in Sources */, 548 | 8B75A1C32A2CB83900E8810E /* PaLMChatAPI.swift in Sources */, 549 | 8B82463529B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */, 550 | 8BC0F8D829F1583300D6B492 /* ParserResult.swift in Sources */, 551 | 8B75A1BF2A2CB83900E8810E /* LLMClient.swift in Sources */, 552 | 8B057594298E52D900A56C9A /* ContentView.swift in Sources */, 553 | 8B057592298E52D900A56C9A /* DotLoadingView.swift in Sources */, 554 | 8B057595298E52D900A56C9A /* MessageRowView.swift in Sources */, 555 | 8B75A1C72A2CB83900E8810E /* LLMConfig.swift in Sources */, 556 | 8B057585298E52C000A56C9A /* XCAChatGPTMacApp.swift in Sources */, 557 | 8B057596298E52DD00A56C9A /* ViewModel.swift in Sources */, 558 | ); 559 | runOnlyForDeploymentPostprocessing = 0; 560 | }; 561 | 8B0575EC298FA9D800A56C9A /* Sources */ = { 562 | isa = PBXSourcesBuildPhase; 563 | buildActionMask = 2147483647; 564 | files = ( 565 | 8B75A1D12A2CB8DC00E8810E /* LLMConfigView.swift in Sources */, 566 | 8B057608298FA9E900A56C9A /* DotLoadingView.swift in Sources */, 567 | 8B75A1CC2A2CB83900E8810E /* LLMProvider.swift in Sources */, 568 | 8B057606298FA9E900A56C9A /* MessageRowView.swift in Sources */, 569 | 8B75A1C42A2CB83900E8810E /* PaLMChatAPI.swift in Sources */, 570 | 8B82463629B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */, 571 | 8BC0F8D929F1583300D6B492 /* ParserResult.swift in Sources */, 572 | 8B75A1C02A2CB83900E8810E /* LLMClient.swift in Sources */, 573 | 8B057609298FA9E900A56C9A /* MessageRow.swift in Sources */, 574 | 8B057605298FA9E900A56C9A /* ContentView.swift in Sources */, 575 | 8B05760A298FA9E900A56C9A /* ChatGPTAPI.swift in Sources */, 576 | 8B75A1C82A2CB83900E8810E /* LLMConfig.swift in Sources */, 577 | 8B057607298FA9E900A56C9A /* ViewModel.swift in Sources */, 578 | 8B0575F6298FA9D800A56C9A /* XCAChatGPTWatchApp.swift in Sources */, 579 | ); 580 | runOnlyForDeploymentPostprocessing = 0; 581 | }; 582 | 8B05760D298FBDB600A56C9A /* Sources */ = { 583 | isa = PBXSourcesBuildPhase; 584 | buildActionMask = 2147483647; 585 | files = ( 586 | 8B057621298FBE0400A56C9A /* MessageRow.swift in Sources */, 587 | 8B057622298FBE0400A56C9A /* MessageRowView.swift in Sources */, 588 | 8B82463729B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */, 589 | 8BC0F8DA29F1583300D6B492 /* ParserResult.swift in Sources */, 590 | 8B75A1CD2A2CB83900E8810E /* LLMProvider.swift in Sources */, 591 | 8B75A1C92A2CB83900E8810E /* LLMConfig.swift in Sources */, 592 | 8B75A1C52A2CB83900E8810E /* PaLMChatAPI.swift in Sources */, 593 | 8B05761F298FBE0400A56C9A /* ChatGPTAPI.swift in Sources */, 594 | 8B057623298FBE0400A56C9A /* DotLoadingView.swift in Sources */, 595 | 8B057614298FBDB600A56C9A /* XCAChatGPTTVApp.swift in Sources */, 596 | 8B05764829909A9200A56C9A /* ScrollView.swift in Sources */, 597 | 8B057620298FBE0400A56C9A /* ContentView.swift in Sources */, 598 | 8B75A1C12A2CB83900E8810E /* LLMClient.swift in Sources */, 599 | 8B057624298FBE0400A56C9A /* ViewModel.swift in Sources */, 600 | 8B75A1D22A2CB8DC00E8810E /* LLMConfigView.swift in Sources */, 601 | ); 602 | runOnlyForDeploymentPostprocessing = 0; 603 | }; 604 | 8B91BFFC298AD09E0079AF26 /* Sources */ = { 605 | isa = PBXSourcesBuildPhase; 606 | buildActionMask = 2147483647; 607 | files = ( 608 | 8B91C012298AD0CE0079AF26 /* ChatGPTAPI.swift in Sources */, 609 | 8B91C006298AD09E0079AF26 /* ContentView.swift in Sources */, 610 | 8BC0F8DC29F1587600D6B492 /* ResponseParsingTask.swift in Sources */, 611 | 8B75A1C62A2CB83900E8810E /* LLMConfig.swift in Sources */, 612 | 8B75A1CF2A2CB8DC00E8810E /* LLMConfigView.swift in Sources */, 613 | 8B612E2629D68CC9008DF5AF /* TokenizerView.swift in Sources */, 614 | 8B612E2729D68CC9008DF5AF /* TokenizerViewModel.swift in Sources */, 615 | 8B82463429B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */, 616 | 8BC0F8DE29F1589600D6B492 /* CodeBlockView.swift in Sources */, 617 | 8B91C014298ADC560079AF26 /* ViewModel.swift in Sources */, 618 | 8BC0F8D729F1583300D6B492 /* ParserResult.swift in Sources */, 619 | 8B91C018298ADF4E0079AF26 /* DotLoadingView.swift in Sources */, 620 | 8B91C004298AD09E0079AF26 /* XCAChatGPTApp.swift in Sources */, 621 | 8B75A1C22A2CB83900E8810E /* PaLMChatAPI.swift in Sources */, 622 | 8B612E2529D68CC9008DF5AF /* TextView.swift in Sources */, 623 | 8B91C01A298ADF7F0079AF26 /* MessageRowView.swift in Sources */, 624 | 8BC0F8D529F1581800D6B492 /* MarkdownAttributedStringParser.swift in Sources */, 625 | 8B75A1BE2A2CB83900E8810E /* LLMClient.swift in Sources */, 626 | 8B75A1CA2A2CB83900E8810E /* LLMProvider.swift in Sources */, 627 | 8B91C016298ADC9D0079AF26 /* MessageRow.swift in Sources */, 628 | ); 629 | runOnlyForDeploymentPostprocessing = 0; 630 | }; 631 | /* End PBXSourcesBuildPhase section */ 632 | 633 | /* Begin PBXTargetDependency section */ 634 | 8B0575F3298FA9D800A56C9A /* PBXTargetDependency */ = { 635 | isa = PBXTargetDependency; 636 | target = 8B0575EF298FA9D800A56C9A /* XCAChatGPTWatch Watch App */; 637 | targetProxy = 8B0575F2298FA9D800A56C9A /* PBXContainerItemProxy */; 638 | }; 639 | /* End PBXTargetDependency section */ 640 | 641 | /* Begin XCBuildConfiguration section */ 642 | 8B05758E298E52C000A56C9A /* Debug */ = { 643 | isa = XCBuildConfiguration; 644 | buildSettings = { 645 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 646 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 647 | CODE_SIGN_ENTITLEMENTS = XCAChatGPTMac/XCAChatGPTMac.entitlements; 648 | CODE_SIGN_STYLE = Automatic; 649 | COMBINE_HIDPI_IMAGES = YES; 650 | CURRENT_PROJECT_VERSION = 1; 651 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTMac/Preview Content\""; 652 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 653 | ENABLE_HARDENED_RUNTIME = YES; 654 | ENABLE_PREVIEWS = YES; 655 | GENERATE_INFOPLIST_FILE = YES; 656 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 657 | LD_RUNPATH_SEARCH_PATHS = ( 658 | "$(inherited)", 659 | "@executable_path/../Frameworks", 660 | ); 661 | MACOSX_DEPLOYMENT_TARGET = 13.1; 662 | MARKETING_VERSION = 1.0; 663 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTMac; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SDKROOT = macosx; 666 | SWIFT_EMIT_LOC_STRINGS = YES; 667 | SWIFT_VERSION = 5.0; 668 | }; 669 | name = Debug; 670 | }; 671 | 8B05758F298E52C000A56C9A /* Release */ = { 672 | isa = XCBuildConfiguration; 673 | buildSettings = { 674 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 675 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 676 | CODE_SIGN_ENTITLEMENTS = XCAChatGPTMac/XCAChatGPTMac.entitlements; 677 | CODE_SIGN_STYLE = Automatic; 678 | COMBINE_HIDPI_IMAGES = YES; 679 | CURRENT_PROJECT_VERSION = 1; 680 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTMac/Preview Content\""; 681 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 682 | ENABLE_HARDENED_RUNTIME = YES; 683 | ENABLE_PREVIEWS = YES; 684 | GENERATE_INFOPLIST_FILE = YES; 685 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 686 | LD_RUNPATH_SEARCH_PATHS = ( 687 | "$(inherited)", 688 | "@executable_path/../Frameworks", 689 | ); 690 | MACOSX_DEPLOYMENT_TARGET = 13.1; 691 | MARKETING_VERSION = 1.0; 692 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTMac; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SDKROOT = macosx; 695 | SWIFT_EMIT_LOC_STRINGS = YES; 696 | SWIFT_VERSION = 5.0; 697 | }; 698 | name = Release; 699 | }; 700 | 8B0575FE298FA9DA00A56C9A /* Debug */ = { 701 | isa = XCBuildConfiguration; 702 | buildSettings = { 703 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 704 | CODE_SIGN_STYLE = Automatic; 705 | CURRENT_PROJECT_VERSION = 1; 706 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 707 | INFOPLIST_KEY_CFBundleDisplayName = XCAChatGPTWatch; 708 | MARKETING_VERSION = 1.0; 709 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTWatch; 710 | PRODUCT_NAME = "$(TARGET_NAME)"; 711 | SWIFT_VERSION = 5.0; 712 | }; 713 | name = Debug; 714 | }; 715 | 8B0575FF298FA9DA00A56C9A /* Release */ = { 716 | isa = XCBuildConfiguration; 717 | buildSettings = { 718 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 719 | CODE_SIGN_STYLE = Automatic; 720 | CURRENT_PROJECT_VERSION = 1; 721 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 722 | INFOPLIST_KEY_CFBundleDisplayName = XCAChatGPTWatch; 723 | MARKETING_VERSION = 1.0; 724 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTWatch; 725 | PRODUCT_NAME = "$(TARGET_NAME)"; 726 | SWIFT_VERSION = 5.0; 727 | }; 728 | name = Release; 729 | }; 730 | 8B057601298FA9DA00A56C9A /* Debug */ = { 731 | isa = XCBuildConfiguration; 732 | buildSettings = { 733 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 734 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 735 | CODE_SIGN_STYLE = Automatic; 736 | CURRENT_PROJECT_VERSION = 1; 737 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTWatch Watch App/Preview Content\""; 738 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 739 | ENABLE_PREVIEWS = YES; 740 | GENERATE_INFOPLIST_FILE = YES; 741 | INFOPLIST_KEY_CFBundleDisplayName = XCAChatGPTWatch; 742 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 743 | INFOPLIST_KEY_WKWatchOnly = YES; 744 | LD_RUNPATH_SEARCH_PATHS = ( 745 | "$(inherited)", 746 | "@executable_path/Frameworks", 747 | ); 748 | MARKETING_VERSION = 1.0; 749 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTWatch.watchkitapp; 750 | PRODUCT_NAME = "$(TARGET_NAME)"; 751 | SDKROOT = watchos; 752 | SKIP_INSTALL = YES; 753 | SWIFT_EMIT_LOC_STRINGS = YES; 754 | SWIFT_VERSION = 5.0; 755 | TARGETED_DEVICE_FAMILY = 4; 756 | WATCHOS_DEPLOYMENT_TARGET = 9.1; 757 | }; 758 | name = Debug; 759 | }; 760 | 8B057602298FA9DA00A56C9A /* Release */ = { 761 | isa = XCBuildConfiguration; 762 | buildSettings = { 763 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 764 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 765 | CODE_SIGN_STYLE = Automatic; 766 | CURRENT_PROJECT_VERSION = 1; 767 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTWatch Watch App/Preview Content\""; 768 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 769 | ENABLE_PREVIEWS = YES; 770 | GENERATE_INFOPLIST_FILE = YES; 771 | INFOPLIST_KEY_CFBundleDisplayName = XCAChatGPTWatch; 772 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 773 | INFOPLIST_KEY_WKWatchOnly = YES; 774 | LD_RUNPATH_SEARCH_PATHS = ( 775 | "$(inherited)", 776 | "@executable_path/Frameworks", 777 | ); 778 | MARKETING_VERSION = 1.0; 779 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTWatch.watchkitapp; 780 | PRODUCT_NAME = "$(TARGET_NAME)"; 781 | SDKROOT = watchos; 782 | SKIP_INSTALL = YES; 783 | SWIFT_EMIT_LOC_STRINGS = YES; 784 | SWIFT_VERSION = 5.0; 785 | TARGETED_DEVICE_FAMILY = 4; 786 | WATCHOS_DEPLOYMENT_TARGET = 9.1; 787 | }; 788 | name = Release; 789 | }; 790 | 8B05761D298FBDB700A56C9A /* Debug */ = { 791 | isa = XCBuildConfiguration; 792 | buildSettings = { 793 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 794 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 795 | CODE_SIGN_STYLE = Automatic; 796 | CURRENT_PROJECT_VERSION = 1; 797 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTTV/Preview Content\""; 798 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 799 | ENABLE_PREVIEWS = YES; 800 | GENERATE_INFOPLIST_FILE = YES; 801 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 802 | INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; 803 | LD_RUNPATH_SEARCH_PATHS = ( 804 | "$(inherited)", 805 | "@executable_path/Frameworks", 806 | ); 807 | MARKETING_VERSION = 1.0; 808 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTTV; 809 | PRODUCT_NAME = "$(TARGET_NAME)"; 810 | SDKROOT = appletvos; 811 | SWIFT_EMIT_LOC_STRINGS = YES; 812 | SWIFT_VERSION = 5.0; 813 | TARGETED_DEVICE_FAMILY = 3; 814 | TVOS_DEPLOYMENT_TARGET = 16.1; 815 | }; 816 | name = Debug; 817 | }; 818 | 8B05761E298FBDB700A56C9A /* Release */ = { 819 | isa = XCBuildConfiguration; 820 | buildSettings = { 821 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 822 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 823 | CODE_SIGN_STYLE = Automatic; 824 | CURRENT_PROJECT_VERSION = 1; 825 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPTTV/Preview Content\""; 826 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 827 | ENABLE_PREVIEWS = YES; 828 | GENERATE_INFOPLIST_FILE = YES; 829 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 830 | INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; 831 | LD_RUNPATH_SEARCH_PATHS = ( 832 | "$(inherited)", 833 | "@executable_path/Frameworks", 834 | ); 835 | MARKETING_VERSION = 1.0; 836 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPTTV; 837 | PRODUCT_NAME = "$(TARGET_NAME)"; 838 | SDKROOT = appletvos; 839 | SWIFT_EMIT_LOC_STRINGS = YES; 840 | SWIFT_VERSION = 5.0; 841 | TARGETED_DEVICE_FAMILY = 3; 842 | TVOS_DEPLOYMENT_TARGET = 16.1; 843 | }; 844 | name = Release; 845 | }; 846 | 8B91C00C298AD09F0079AF26 /* Debug */ = { 847 | isa = XCBuildConfiguration; 848 | buildSettings = { 849 | ALWAYS_SEARCH_USER_PATHS = NO; 850 | CLANG_ANALYZER_NONNULL = YES; 851 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 852 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 853 | CLANG_ENABLE_MODULES = YES; 854 | CLANG_ENABLE_OBJC_ARC = YES; 855 | CLANG_ENABLE_OBJC_WEAK = YES; 856 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 857 | CLANG_WARN_BOOL_CONVERSION = YES; 858 | CLANG_WARN_COMMA = YES; 859 | CLANG_WARN_CONSTANT_CONVERSION = YES; 860 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 861 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 862 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 863 | CLANG_WARN_EMPTY_BODY = YES; 864 | CLANG_WARN_ENUM_CONVERSION = YES; 865 | CLANG_WARN_INFINITE_RECURSION = YES; 866 | CLANG_WARN_INT_CONVERSION = YES; 867 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 868 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 869 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 870 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 871 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 872 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 873 | CLANG_WARN_STRICT_PROTOTYPES = YES; 874 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 875 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 876 | CLANG_WARN_UNREACHABLE_CODE = YES; 877 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 878 | COPY_PHASE_STRIP = NO; 879 | DEBUG_INFORMATION_FORMAT = dwarf; 880 | ENABLE_STRICT_OBJC_MSGSEND = YES; 881 | ENABLE_TESTABILITY = YES; 882 | GCC_C_LANGUAGE_STANDARD = gnu11; 883 | GCC_DYNAMIC_NO_PIC = NO; 884 | GCC_NO_COMMON_BLOCKS = YES; 885 | GCC_OPTIMIZATION_LEVEL = 0; 886 | GCC_PREPROCESSOR_DEFINITIONS = ( 887 | "DEBUG=1", 888 | "$(inherited)", 889 | ); 890 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 891 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 892 | GCC_WARN_UNDECLARED_SELECTOR = YES; 893 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 894 | GCC_WARN_UNUSED_FUNCTION = YES; 895 | GCC_WARN_UNUSED_VARIABLE = YES; 896 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 897 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 898 | MTL_FAST_MATH = YES; 899 | ONLY_ACTIVE_ARCH = YES; 900 | SDKROOT = iphoneos; 901 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 902 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 903 | }; 904 | name = Debug; 905 | }; 906 | 8B91C00D298AD09F0079AF26 /* Release */ = { 907 | isa = XCBuildConfiguration; 908 | buildSettings = { 909 | ALWAYS_SEARCH_USER_PATHS = NO; 910 | CLANG_ANALYZER_NONNULL = YES; 911 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 912 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 913 | CLANG_ENABLE_MODULES = YES; 914 | CLANG_ENABLE_OBJC_ARC = YES; 915 | CLANG_ENABLE_OBJC_WEAK = YES; 916 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 917 | CLANG_WARN_BOOL_CONVERSION = YES; 918 | CLANG_WARN_COMMA = YES; 919 | CLANG_WARN_CONSTANT_CONVERSION = YES; 920 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 921 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 922 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 923 | CLANG_WARN_EMPTY_BODY = YES; 924 | CLANG_WARN_ENUM_CONVERSION = YES; 925 | CLANG_WARN_INFINITE_RECURSION = YES; 926 | CLANG_WARN_INT_CONVERSION = YES; 927 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 928 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 929 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 930 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 931 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 932 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 933 | CLANG_WARN_STRICT_PROTOTYPES = YES; 934 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 935 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 936 | CLANG_WARN_UNREACHABLE_CODE = YES; 937 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 938 | COPY_PHASE_STRIP = NO; 939 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 940 | ENABLE_NS_ASSERTIONS = NO; 941 | ENABLE_STRICT_OBJC_MSGSEND = YES; 942 | GCC_C_LANGUAGE_STANDARD = gnu11; 943 | GCC_NO_COMMON_BLOCKS = YES; 944 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 945 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 946 | GCC_WARN_UNDECLARED_SELECTOR = YES; 947 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 948 | GCC_WARN_UNUSED_FUNCTION = YES; 949 | GCC_WARN_UNUSED_VARIABLE = YES; 950 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 951 | MTL_ENABLE_DEBUG_INFO = NO; 952 | MTL_FAST_MATH = YES; 953 | SDKROOT = iphoneos; 954 | SWIFT_COMPILATION_MODE = wholemodule; 955 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 956 | VALIDATE_PRODUCT = YES; 957 | }; 958 | name = Release; 959 | }; 960 | 8B91C00F298AD09F0079AF26 /* Debug */ = { 961 | isa = XCBuildConfiguration; 962 | buildSettings = { 963 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 964 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 965 | CODE_SIGN_STYLE = Automatic; 966 | CURRENT_PROJECT_VERSION = 1; 967 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPT/Preview Content\""; 968 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 969 | ENABLE_PREVIEWS = YES; 970 | GENERATE_INFOPLIST_FILE = YES; 971 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 972 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 973 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 974 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 975 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 976 | LD_RUNPATH_SEARCH_PATHS = ( 977 | "$(inherited)", 978 | "@executable_path/Frameworks", 979 | ); 980 | MARKETING_VERSION = 1.0; 981 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPT; 982 | PRODUCT_NAME = "$(TARGET_NAME)"; 983 | SWIFT_EMIT_LOC_STRINGS = YES; 984 | SWIFT_VERSION = 5.0; 985 | TARGETED_DEVICE_FAMILY = "1,2"; 986 | }; 987 | name = Debug; 988 | }; 989 | 8B91C010298AD09F0079AF26 /* Release */ = { 990 | isa = XCBuildConfiguration; 991 | buildSettings = { 992 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 993 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 994 | CODE_SIGN_STYLE = Automatic; 995 | CURRENT_PROJECT_VERSION = 1; 996 | DEVELOPMENT_ASSET_PATHS = "\"XCAChatGPT/Preview Content\""; 997 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 998 | ENABLE_PREVIEWS = YES; 999 | GENERATE_INFOPLIST_FILE = YES; 1000 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 1001 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 1002 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 1003 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 1004 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 1005 | LD_RUNPATH_SEARCH_PATHS = ( 1006 | "$(inherited)", 1007 | "@executable_path/Frameworks", 1008 | ); 1009 | MARKETING_VERSION = 1.0; 1010 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.XCAChatGPT; 1011 | PRODUCT_NAME = "$(TARGET_NAME)"; 1012 | SWIFT_EMIT_LOC_STRINGS = YES; 1013 | SWIFT_VERSION = 5.0; 1014 | TARGETED_DEVICE_FAMILY = "1,2"; 1015 | }; 1016 | name = Release; 1017 | }; 1018 | /* End XCBuildConfiguration section */ 1019 | 1020 | /* Begin XCConfigurationList section */ 1021 | 8B057590298E52C000A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTMac" */ = { 1022 | isa = XCConfigurationList; 1023 | buildConfigurations = ( 1024 | 8B05758E298E52C000A56C9A /* Debug */, 1025 | 8B05758F298E52C000A56C9A /* Release */, 1026 | ); 1027 | defaultConfigurationIsVisible = 0; 1028 | defaultConfigurationName = Release; 1029 | }; 1030 | 8B057603298FA9DA00A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTWatch Watch App" */ = { 1031 | isa = XCConfigurationList; 1032 | buildConfigurations = ( 1033 | 8B057601298FA9DA00A56C9A /* Debug */, 1034 | 8B057602298FA9DA00A56C9A /* Release */, 1035 | ); 1036 | defaultConfigurationIsVisible = 0; 1037 | defaultConfigurationName = Release; 1038 | }; 1039 | 8B057604298FA9DA00A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTWatch" */ = { 1040 | isa = XCConfigurationList; 1041 | buildConfigurations = ( 1042 | 8B0575FE298FA9DA00A56C9A /* Debug */, 1043 | 8B0575FF298FA9DA00A56C9A /* Release */, 1044 | ); 1045 | defaultConfigurationIsVisible = 0; 1046 | defaultConfigurationName = Release; 1047 | }; 1048 | 8B05761C298FBDB700A56C9A /* Build configuration list for PBXNativeTarget "XCAChatGPTTV" */ = { 1049 | isa = XCConfigurationList; 1050 | buildConfigurations = ( 1051 | 8B05761D298FBDB700A56C9A /* Debug */, 1052 | 8B05761E298FBDB700A56C9A /* Release */, 1053 | ); 1054 | defaultConfigurationIsVisible = 0; 1055 | defaultConfigurationName = Release; 1056 | }; 1057 | 8B91BFFB298AD09E0079AF26 /* Build configuration list for PBXProject "XCAChatGPT" */ = { 1058 | isa = XCConfigurationList; 1059 | buildConfigurations = ( 1060 | 8B91C00C298AD09F0079AF26 /* Debug */, 1061 | 8B91C00D298AD09F0079AF26 /* Release */, 1062 | ); 1063 | defaultConfigurationIsVisible = 0; 1064 | defaultConfigurationName = Release; 1065 | }; 1066 | 8B91C00E298AD09F0079AF26 /* Build configuration list for PBXNativeTarget "XCAChatGPT" */ = { 1067 | isa = XCConfigurationList; 1068 | buildConfigurations = ( 1069 | 8B91C00F298AD09F0079AF26 /* Debug */, 1070 | 8B91C010298AD09F0079AF26 /* Release */, 1071 | ); 1072 | defaultConfigurationIsVisible = 0; 1073 | defaultConfigurationName = Release; 1074 | }; 1075 | /* End XCConfigurationList section */ 1076 | 1077 | /* Begin XCRemoteSwiftPackageReference section */ 1078 | 8B612E2829D68CE3008DF5AF /* XCRemoteSwiftPackageReference "GPTEncoder" */ = { 1079 | isa = XCRemoteSwiftPackageReference; 1080 | repositoryURL = "https://github.com/alfianlosari/GPTEncoder.git"; 1081 | requirement = { 1082 | kind = upToNextMajorVersion; 1083 | minimumVersion = 1.0.0; 1084 | }; 1085 | }; 1086 | 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */ = { 1087 | isa = XCRemoteSwiftPackageReference; 1088 | repositoryURL = "https://github.com/google/generative-ai-swift"; 1089 | requirement = { 1090 | kind = upToNextMajorVersion; 1091 | minimumVersion = 0.2.0; 1092 | }; 1093 | }; 1094 | 8BC0F8CE29F157C800D6B492 /* XCRemoteSwiftPackageReference "swift-markdown" */ = { 1095 | isa = XCRemoteSwiftPackageReference; 1096 | repositoryURL = "https://github.com/apple/swift-markdown"; 1097 | requirement = { 1098 | branch = main; 1099 | kind = branch; 1100 | }; 1101 | }; 1102 | 8BC0F8D129F157E600D6B492 /* XCRemoteSwiftPackageReference "HighlighterSwift" */ = { 1103 | isa = XCRemoteSwiftPackageReference; 1104 | repositoryURL = "https://github.com/alfianlosari/HighlighterSwift"; 1105 | requirement = { 1106 | branch = main; 1107 | kind = branch; 1108 | }; 1109 | }; 1110 | /* End XCRemoteSwiftPackageReference section */ 1111 | 1112 | /* Begin XCSwiftPackageProductDependency section */ 1113 | 8B612E2929D68CE3008DF5AF /* GPTEncoder */ = { 1114 | isa = XCSwiftPackageProductDependency; 1115 | package = 8B612E2829D68CE3008DF5AF /* XCRemoteSwiftPackageReference "GPTEncoder" */; 1116 | productName = GPTEncoder; 1117 | }; 1118 | 8B75A1B02A2CB7C600E8810E /* GoogleGenerativeAI */ = { 1119 | isa = XCSwiftPackageProductDependency; 1120 | package = 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */; 1121 | productName = GoogleGenerativeAI; 1122 | }; 1123 | 8B75A1B32A2CB7D200E8810E /* GoogleGenerativeAI */ = { 1124 | isa = XCSwiftPackageProductDependency; 1125 | package = 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */; 1126 | productName = GoogleGenerativeAI; 1127 | }; 1128 | 8B75A1B52A2CB7D500E8810E /* GoogleGenerativeAI */ = { 1129 | isa = XCSwiftPackageProductDependency; 1130 | package = 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */; 1131 | productName = GoogleGenerativeAI; 1132 | }; 1133 | 8B75A1B72A2CB7DA00E8810E /* GoogleGenerativeAI */ = { 1134 | isa = XCSwiftPackageProductDependency; 1135 | package = 8B75A1AF2A2CB7C600E8810E /* XCRemoteSwiftPackageReference "generative-ai-swift" */; 1136 | productName = GoogleGenerativeAI; 1137 | }; 1138 | 8BC0F8CF29F157C800D6B492 /* Markdown */ = { 1139 | isa = XCSwiftPackageProductDependency; 1140 | package = 8BC0F8CE29F157C800D6B492 /* XCRemoteSwiftPackageReference "swift-markdown" */; 1141 | productName = Markdown; 1142 | }; 1143 | 8BC0F8D229F157E600D6B492 /* Highlighter */ = { 1144 | isa = XCSwiftPackageProductDependency; 1145 | package = 8BC0F8D129F157E600D6B492 /* XCRemoteSwiftPackageReference "HighlighterSwift" */; 1146 | productName = Highlighter; 1147 | }; 1148 | /* End XCSwiftPackageProductDependency section */ 1149 | }; 1150 | rootObject = 8B91BFF8298AD09E0079AF26 /* Project object */; 1151 | } 1152 | -------------------------------------------------------------------------------- /XCAChatGPT.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XCAChatGPT.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /XCAChatGPT.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "generative-ai-swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/google/generative-ai-swift", 7 | "state" : { 8 | "revision" : "c16f7c335a45b5541c85bebcdc1443305c10926f", 9 | "version" : "0.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "get", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/kean/Get", 16 | "state" : { 17 | "revision" : "12830cc64f31789ae6f4352d2d51d03a25fc3741", 18 | "version" : "2.1.6" 19 | } 20 | }, 21 | { 22 | "identity" : "gptencoder", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/alfianlosari/GPTEncoder.git", 25 | "state" : { 26 | "revision" : "8d2057a7073fdbf8fb3ac7b3006e1514cf2f92fb", 27 | "version" : "1.0.3" 28 | } 29 | }, 30 | { 31 | "identity" : "highlighterswift", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/alfianlosari/HighlighterSwift", 34 | "state" : { 35 | "branch" : "main", 36 | "revision" : "c15eea4e5e8f18b0a2d2173830fcaa250f742bc0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-cmark", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-cmark.git", 43 | "state" : { 44 | "branch" : "gfm", 45 | "revision" : "86aeb491675de6f077a3a6df6cbcac1a25dcbee1" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-markdown", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-markdown", 52 | "state" : { 53 | "branch" : "main", 54 | "revision" : "528d49cb5130e9c84081e13e7bd639a011b9f89d" 55 | } 56 | }, 57 | { 58 | "identity" : "urlqueryencoder", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/CreateAPI/URLQueryEncoder", 61 | "state" : { 62 | "revision" : "4ce950479707ea109f229d7230ec074a133b15d7", 63 | "version" : "0.2.1" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/openai.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "openai.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/openai.imageset/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPT/Assets.xcassets/openai.imageset/openai.png -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/palm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "palm.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/palm.imageset/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPT/Assets.xcassets/palm.imageset/palm.png -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "unnamed.jpg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPT/Assets.xcassets/profile.imageset/unnamed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPT/Assets.xcassets/profile.imageset/unnamed.jpg -------------------------------------------------------------------------------- /XCAChatGPT/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPT/TextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 28/03/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct TextView: UIViewRepresentable { 12 | 13 | let colors = [ 14 | UIColor(red: 199/255, green: 195/255, blue: 212/255, alpha: 1), 15 | UIColor(red: 202/255, green: 236/255, blue: 202/255, alpha: 1), 16 | UIColor(red: 241/255, green: 218/255, blue: 181/255, alpha: 1), 17 | UIColor(red: 236/255, green: 180/255, blue: 180/255, alpha: 1), 18 | UIColor(red: 183/255, green: 219/255, blue: 241/255, alpha: 1) 19 | ] 20 | 21 | let output: TokenOutput 22 | 23 | func updateUIView(_ textView: UITextView, context: Context) { 24 | let attributedText = NSMutableAttributedString() 25 | output.stringTokens.enumerated().forEach { index, string in 26 | let attributes: [NSAttributedString.Key: Any] = [ 27 | .font: UIFont.preferredFont(forTextStyle: .body), 28 | .kern: 1, 29 | .backgroundColor: colors[index % colors.count], 30 | ] 31 | 32 | let attributedTokenText = NSAttributedString(string: string, attributes: attributes) 33 | attributedText.append(attributedTokenText) 34 | 35 | } 36 | textView.attributedText = attributedText 37 | } 38 | 39 | func makeUIView(context: Context) -> UITextView { 40 | let tv = UITextView() 41 | tv.isEditable = false 42 | return tv 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /XCAChatGPT/TokenizerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenizerView.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 28/03/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TokenizerView: View { 11 | 12 | @StateObject var vm = TokenizerViewModel() 13 | @FocusState private var isFocused: Bool 14 | 15 | var body: some View { 16 | List { 17 | inputSection 18 | outputSection 19 | } 20 | .navigationTitle("GPT Tokenizer") 21 | } 22 | 23 | var inputSection: some View { 24 | Section { 25 | TextField("Enter text to tokenize", text: $vm.inputText, axis: .vertical) 26 | .focused($isFocused) 27 | .lineLimit(4...12) 28 | .toolbar { 29 | ToolbarItem(placement: .keyboard) { 30 | HStack { 31 | Spacer() 32 | Button("Done") { 33 | isFocused = false 34 | } 35 | } 36 | } 37 | } 38 | 39 | HStack { 40 | Button("Clear") { 41 | withAnimation { 42 | vm.inputText = "" 43 | } 44 | } 45 | .buttonStyle(.borderedProminent) 46 | .disabled(vm.inputText.isEmpty) 47 | 48 | Button("Show example") { 49 | withAnimation { 50 | vm.inputText = exampleText 51 | isFocused = false 52 | } 53 | 54 | } 55 | .buttonStyle(.borderedProminent) 56 | .disabled(vm.inputText == exampleText) 57 | 58 | Spacer() 59 | 60 | if vm.isTokenizing { 61 | ProgressView() 62 | } 63 | 64 | } 65 | .padding(.vertical, 2) 66 | } header: { 67 | Text("Input") 68 | } 69 | 70 | } 71 | 72 | var outputSection: some View { 73 | Section { 74 | if let output = vm.output { 75 | VStack(alignment: .leading) { 76 | HStack { 77 | VStack { 78 | Text("Tokens").font(.subheadline) 79 | Text("\(output.tokens.count)").font(.headline) 80 | } 81 | 82 | Divider() 83 | .frame(height: 32) 84 | 85 | VStack { 86 | Text("Characters").font(.subheadline) 87 | Text("\(output.text.count)").font(.headline) 88 | } 89 | } 90 | .frame(maxWidth: .infinity, alignment: .center) 91 | 92 | Picker("Output Type", selection: $vm.outputType) { 93 | Text("Text").tag(OutputType.text) 94 | Text("Token Ids").tag(OutputType.tokenIds) 95 | } 96 | .pickerStyle(.segmented) 97 | 98 | switch vm.outputType { 99 | case .text: 100 | TextView(output: output) 101 | .frame(height: 240) 102 | 103 | case .tokenIds: 104 | Text("\(output.tokens.description)") 105 | .textSelection(.enabled) 106 | .multilineTextAlignment(.leading) 107 | .padding(.vertical) 108 | } 109 | } 110 | } 111 | } header: { 112 | if vm.output != nil { 113 | Text("Output") 114 | } 115 | } footer: { 116 | Text(footerText).padding(.top, vm.output != nil ? 8 : 0) 117 | } 118 | } 119 | } 120 | 121 | struct TokenizerView_Previews: PreviewProvider { 122 | static var previews: some View { 123 | NavigationStack { 124 | TokenizerView() 125 | } 126 | } 127 | } 128 | 129 | 130 | 131 | let exampleText = """ 132 | Many words map to one token, but some don't: indivisible. 133 | 134 | Unicode characters like emojis may be split into many tokens containing the underlying bytes: 🤚🏾 135 | 136 | Sequences of characters commonly found next to each other may be grouped together: 1234567890 137 | """ 138 | 139 | 140 | let footerText = """ 141 | The GPT family of models process text using tokens, which are common sequences of characters found in text. The models understand the statistical relationships between these tokens, and excel at producing the next token in a sequence of tokens. 142 | 143 | You can use this tool to understand how a piece of text would be tokenized by the API, and the total count of tokens in that piece of text. 144 | 145 | A helpful rule of thumb is that one token generally corresponds to ~4 characters of text for common English text. This translates to roughly ¾ of a word (so 100 tokens ~= 75 words). 146 | 147 | if your input contains one or more unicode characters that map to multiple tokens. The output visualization may display the bytes in each token in a non-standard way. 148 | 149 | If you need a programmatic interface for tokenizing text, check out the GPTEncoder SPM or Cocoapods lib for Swift. 150 | """ 151 | -------------------------------------------------------------------------------- /XCAChatGPT/TokenizerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenizerViewModel.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 28/03/23. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | import GPTEncoder 12 | 13 | struct TokenOutput { 14 | let text: String 15 | let stringTokens: [String] 16 | let tokens: [Int] 17 | } 18 | 19 | enum OutputType: Identifiable { 20 | case text, tokenIds 21 | var id: Self { self } 22 | } 23 | 24 | class TokenizerViewModel: ObservableObject, @unchecked Sendable { 25 | 26 | let tokenizer = GPTEncoder() 27 | 28 | @Published var inputText = "" 29 | @Published var output: TokenOutput? 30 | @Published var isTokenizing = false 31 | @Published var error: String? 32 | @Published var isShowingError = false 33 | @Published var outputType = OutputType.text 34 | 35 | var cancellables = Set() 36 | var task: Task<(), Never>? 37 | 38 | init() { 39 | startObserve() 40 | } 41 | 42 | func startObserve() { 43 | $inputText 44 | .filter { !$0.isEmpty } 45 | .debounce(for: 0.3, scheduler: DispatchQueue.main) 46 | .sink { [weak self] value in 47 | guard let self = self else { return } 48 | self.task?.cancel() 49 | if self.inputText.isEmpty { 50 | return 51 | } 52 | self.task = Task { await self.tokenize(value: value) } 53 | }.store(in: &cancellables) 54 | 55 | 56 | $inputText 57 | .filter { $0.isEmpty } 58 | .sink { [weak self] _ in 59 | withAnimation { self?.output = nil } 60 | }.store(in: &cancellables) 61 | } 62 | 63 | func tokenize(value: String) async { 64 | if Task.isCancelled { return } 65 | 66 | Task { @MainActor [weak self] in 67 | withAnimation { 68 | self?.isTokenizing = true 69 | } 70 | } 71 | 72 | let tokens = self.tokenizer.encode(text: value) 73 | let stringTokens = tokens.map { tokenizer.decode(tokens: [$0]) } 74 | 75 | Task { @MainActor [weak self] in 76 | if Task.isCancelled { return } 77 | withAnimation { 78 | self?.output = .init(text: value, stringTokens: stringTokens, tokens: tokens) 79 | self?.isTokenizing = false 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /XCAChatGPT/XCAChatGPTApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAChatGPTApp.swift 3 | // XCAChatGPT 4 | // 5 | // Created by Alfian Losari on 01/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct XCAChatGPTApp: App { 12 | 13 | @StateObject var vm = ViewModel(api: ChatGPTAPI(apiKey: "PROVIDE_API_KEY")) 14 | @State var isShowingTokenizer = false 15 | @State var llmConfig: LLMConfig? 16 | 17 | var body: some Scene { 18 | WindowGroup { 19 | NavigationStack { 20 | LLMConfigView { config in 21 | vm.updateClient(config.createClient()) 22 | llmConfig = config 23 | } 24 | .navigationTitle(vm.title) 25 | } 26 | .fullScreenCover(item: $llmConfig) { config in 27 | NavigationStack { 28 | ContentView(vm: vm) 29 | .toolbar { 30 | ToolbarItemGroup(placement: .navigationBarTrailing) { 31 | if case .chatGPT = llmConfig?.type { 32 | Button("Tokenizer") { 33 | self.isShowingTokenizer = true 34 | } 35 | .disabled(vm.isInteracting) 36 | } 37 | 38 | Button("Clear", role: .destructive) { 39 | vm.clearMessages() 40 | } 41 | .disabled(vm.isInteracting) 42 | } 43 | 44 | ToolbarItem(placement: .navigationBarLeading) { 45 | Button("Switch LLM", role: .destructive) { 46 | llmConfig = nil 47 | } 48 | } 49 | } 50 | } 51 | .fullScreenCover(isPresented: $isShowingTokenizer) { 52 | NavigationTokenView() 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | 60 | struct NavigationTokenView: View { 61 | 62 | @Environment(\.dismiss) var dismiss 63 | 64 | var body: some View { 65 | NavigationStack { 66 | TokenizerView() 67 | .toolbar { 68 | ToolbarItem(placement: .navigationBarTrailing) { 69 | Button("Close") { 70 | dismiss() 71 | } 72 | } 73 | } 74 | } 75 | .interactiveDismissDisabled() 76 | } 77 | } 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Shape.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/icon.imageset/Shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTMac/Assets.xcassets/icon.imageset/Shape.png -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/openai.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "openai.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/openai.imageset/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTMac/Assets.xcassets/openai.imageset/openai.png -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/palm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "palm.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/palm.imageset/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTMac/Assets.xcassets/palm.imageset/palm.png -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "unnamed.jpg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTMac/Assets.xcassets/profile.imageset/unnamed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTMac/Assets.xcassets/profile.imageset/unnamed.jpg -------------------------------------------------------------------------------- /XCAChatGPTMac/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTMac/XCAChatGPTMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /XCAChatGPTMac/XCAChatGPTMacApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAChatGPTMacApp.swift 3 | // XCAChatGPTMac 4 | // 5 | // Created by Alfian Losari on 04/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct XCAChatGPTMacApp: App { 12 | 13 | @StateObject var vm = ViewModel(api: ChatGPTAPI(apiKey: "API_KEY")) 14 | @State var llmConfig: LLMConfig? 15 | 16 | var body: some Scene { 17 | MenuBarExtra(vm.title, systemImage: "bubbles.and.sparkles") { 18 | VStack(spacing: 16) { 19 | HStack { 20 | Text(vm.title).font(.title) 21 | Spacer() 22 | 23 | Button { 24 | exit(0) 25 | } label: { 26 | Image(systemName: "xmark.circle.fill") 27 | .symbolRenderingMode(.multicolor) 28 | .font(.system(size: 24)) 29 | } 30 | 31 | .buttonStyle(.borderless) 32 | }.padding() 33 | 34 | LLMConfigView { config in 35 | vm.updateClient(config.createClient()) 36 | llmConfig = config 37 | } 38 | } 39 | .frame(width: 480, height: 648) 40 | 41 | .sheet(item: $llmConfig) { _ in 42 | VStack(spacing: 0) { 43 | HStack { 44 | Text(vm.navigationTitle) 45 | .font(.title) 46 | Spacer() 47 | 48 | Button("Switch LLM", role: .destructive) { 49 | llmConfig = nil 50 | } 51 | 52 | Button { 53 | guard !vm.isInteracting else { return } 54 | vm.clearMessages() 55 | } label: { 56 | Image(systemName: "trash") 57 | .symbolRenderingMode(.multicolor) 58 | .font(.system(size: 24)) 59 | } 60 | 61 | .buttonStyle(.borderless) 62 | } 63 | .padding() 64 | 65 | ContentView(vm: vm) 66 | } 67 | .frame(width: 480, height: 648) 68 | } 69 | }.menuBarExtraStyle(.window) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/openai.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "openai.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/openai.imageset/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTTV/Assets.xcassets/openai.imageset/openai.png -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/palm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "palm.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/palm.imageset/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTTV/Assets.xcassets/palm.imageset/palm.png -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "unnamed.jpg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTTV/Assets.xcassets/profile.imageset/unnamed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTTV/Assets.xcassets/profile.imageset/unnamed.jpg -------------------------------------------------------------------------------- /XCAChatGPTTV/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTTV/ScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollView.swift 3 | // XCAChatGPTTV 4 | // 5 | // Created by Alfian Losari on 06/02/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import SwiftUI 11 | 12 | //struct ScrollViewWrapper: UIViewRepresentable { 13 | // 14 | // 15 | // func makeUIView(context: Context) -> some UIView { 16 | // return UIScrollView() 17 | // } 18 | // 19 | // func updateUIView(_ uiView: UIScrollView, context: Context) { 20 | // 21 | // } 22 | // 23 | //} 24 | -------------------------------------------------------------------------------- /XCAChatGPTTV/XCAChatGPTTVApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAChatGPTTVApp.swift 3 | // XCAChatGPTTV 4 | // 5 | // Created by Alfian Losari on 05/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct XCAChatGPTTVApp: App { 12 | 13 | @StateObject var vm = ViewModel(api: ChatGPTAPI(apiKey: "API_KEY"), enableSpeech: true) 14 | @State var llmConfig: LLMConfig? 15 | 16 | @FocusState var isTextFieldFocused: Bool 17 | 18 | var body: some Scene { 19 | WindowGroup { 20 | VStack(alignment: .center) { 21 | Text(vm.title).font(.largeTitle) 22 | LLMConfigView { config in 23 | vm.updateClient(config.createClient()) 24 | llmConfig = config 25 | } 26 | .frame(width: 1280) 27 | } 28 | .opacity(llmConfig == nil ? 1 : 0) 29 | .fullScreenCover(item: $llmConfig) { _ in 30 | VStack { 31 | Text(vm.navigationTitle).font(.largeTitle) 32 | HStack(alignment: .top) { 33 | ContentView(vm: vm) 34 | .cornerRadius(32) 35 | .overlay { 36 | if vm.messages.isEmpty { 37 | Text("Click send to start interacting with ChatGPT") 38 | .multilineTextAlignment(.center) 39 | .font(.headline) 40 | .foregroundColor(Color(UIColor.placeholderText)) 41 | } else { 42 | EmptyView() 43 | } 44 | } 45 | 46 | VStack { 47 | TextField("Send", text: $vm.inputMessage) 48 | .multilineTextAlignment(.center) 49 | .frame(width: 176) 50 | .focused($isTextFieldFocused) 51 | .disabled(vm.isInteracting) 52 | .onSubmit { 53 | Task { @MainActor in 54 | await vm.sendTapped() 55 | isTextFieldFocused = true 56 | } 57 | } 58 | .onChange(of: isTextFieldFocused) { _ in 59 | vm.inputMessage = "" 60 | } 61 | 62 | Button("Clear", role: .destructive) { 63 | vm.clearMessages() 64 | } 65 | .frame(width: 176) 66 | .disabled(vm.isInteracting || vm.messages.isEmpty) 67 | 68 | Button("Switch LLM", role: .destructive) { 69 | llmConfig = nil 70 | } 71 | .padding(.vertical) 72 | 73 | ProgressView() 74 | .progressViewStyle(.circular) 75 | .padding() 76 | .opacity(vm.isInteracting ? 1 : 0) 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | 84 | 85 | } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemTealColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "watchos", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/openai.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "openai.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/openai.imageset/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTWatch Watch App/Assets.xcassets/openai.imageset/openai.png -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/palm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "palm.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/palm.imageset/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTWatch Watch App/Assets.xcassets/palm.imageset/palm.png -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "unnamed.jpg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Assets.xcassets/profile.imageset/unnamed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/ChatGPTSwiftUI/bda026e3c931235a1dbbaf560e5ac7ffd178bc5b/XCAChatGPTWatch Watch App/Assets.xcassets/profile.imageset/unnamed.jpg -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCAChatGPTWatch Watch App/XCAChatGPTWatchApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAChatGPTWatchApp.swift 3 | // XCAChatGPTWatch Watch App 4 | // 5 | // Created by Alfian Losari on 05/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct XCAChatGPTWatch_Watch_AppApp: App { 12 | 13 | @StateObject var vm = ViewModel(api: ChatGPTAPI(apiKey: "API_KEY")) 14 | @State private var presentedConfigs = [LLMConfig]() 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | NavigationStack(path: $presentedConfigs) { 19 | LLMConfigView { config in 20 | vm.updateClient(config.createClient()) 21 | presentedConfigs.append(config) 22 | } 23 | .navigationTitle(vm.title) 24 | .navigationDestination(for: LLMConfig.self) { _ in 25 | ContentView(vm: vm) 26 | .edgesIgnoringSafeArea([.horizontal, .bottom]) 27 | .navigationBarTitleDisplayMode(.inline) 28 | .overlay { 29 | if vm.messages.isEmpty { 30 | Text("Scroll to top and tap send to Chat") 31 | } 32 | } 33 | .toolbar { 34 | ToolbarItemGroup { 35 | HStack { 36 | Button("Send") { 37 | self.presentInputController(withSuggestions: []) { result in 38 | Task { @MainActor in 39 | guard !result.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } 40 | vm.inputMessage = result.trimmingCharacters(in: .whitespacesAndNewlines) 41 | await vm.sendTapped() 42 | } 43 | } 44 | } 45 | 46 | Button("Clear", role: .destructive) { 47 | vm.clearMessages() 48 | } 49 | .tint(.red) 50 | .disabled(vm.isInteracting || vm.messages.isEmpty) 51 | } 52 | .padding(.bottom) 53 | } 54 | } 55 | } 56 | 57 | } 58 | 59 | } 60 | } 61 | } 62 | 63 | extension App { 64 | typealias StringCompletion = (String) -> Void 65 | 66 | func presentInputController(withSuggestions suggestions: [String], completion: @escaping StringCompletion) { 67 | WKExtension.shared() 68 | .visibleInterfaceController? 69 | .presentTextInputController(withSuggestions: suggestions, 70 | allowedInputMode: .plain) { result in 71 | 72 | guard let result = result as? [String], let firstElement = result.first else { 73 | completion("") 74 | return 75 | } 76 | 77 | completion(firstElement) 78 | } 79 | } 80 | } 81 | 82 | --------------------------------------------------------------------------------