add Chat GPT

This commit is contained in:
陈连辰
2023-03-10 01:19:32 +08:00
parent 9f93e390a1
commit 8228ca2fd9
17 changed files with 634 additions and 221 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
xcuserdata
OSXChatGPT/OSXChatGPT.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
*.xcuserstate
OSXChatGPT/OSXChatGPT.xcodeproj/xcuserdata/lianchen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist

View File

@@ -12,6 +12,16 @@
CB1DCAC929B4F09F00B1D4E1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB1DCAC829B4F09F00B1D4E1 /* Preview Assets.xcassets */; };
CB1DCAD129B4F0C800B1D4E1 /* ChatMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1DCAD029B4F0C800B1D4E1 /* ChatMessageView.swift */; };
CB1DCAD329B4F0EF00B1D4E1 /* ChatGPTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1DCAD229B4F0EF00B1D4E1 /* ChatGPTManager.swift */; };
CBC4B0FF29B8BF9600650296 /* OSXChatGPT.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B0FD29B8BF9600650296 /* OSXChatGPT.xcdatamodeld */; };
CBC4B11629B8CB5300650296 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11529B8CB5300650296 /* HTTPClient.swift */; };
CBC4B11A29B8D11500650296 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11929B8D11500650296 /* CoreDataManager.swift */; };
CBC4B11C29B8D23C00650296 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11B29B8D23C00650296 /* ViewModel.swift */; };
CBC4B12329B8D28D00650296 /* Message+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11D29B8D28D00650296 /* Message+CoreDataClass.swift */; };
CBC4B12429B8D28D00650296 /* Message+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11E29B8D28D00650296 /* Message+CoreDataProperties.swift */; };
CBC4B12529B8D28D00650296 /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B11F29B8D28D00650296 /* Conversation+CoreDataClass.swift */; };
CBC4B12629B8D28D00650296 /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B12029B8D28D00650296 /* Conversation+CoreDataProperties.swift */; };
CBC4B12729B8D28D00650296 /* UserRole+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B12129B8D28D00650296 /* UserRole+CoreDataClass.swift */; };
CBC4B12829B8D28D00650296 /* UserRole+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC4B12229B8D28D00650296 /* UserRole+CoreDataProperties.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -22,6 +32,16 @@
CB1DCACA29B4F09F00B1D4E1 /* OSXChatGPT.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OSXChatGPT.entitlements; sourceTree = "<group>"; };
CB1DCAD029B4F0C800B1D4E1 /* ChatMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageView.swift; sourceTree = "<group>"; };
CB1DCAD229B4F0EF00B1D4E1 /* ChatGPTManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatGPTManager.swift; sourceTree = "<group>"; };
CBC4B0FE29B8BF9600650296 /* OSXChatGPT.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = OSXChatGPT.xcdatamodel; sourceTree = "<group>"; };
CBC4B11529B8CB5300650296 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
CBC4B11929B8D11500650296 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
CBC4B11B29B8D23C00650296 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
CBC4B11D29B8D28D00650296 /* Message+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataClass.swift"; sourceTree = "<group>"; };
CBC4B11E29B8D28D00650296 /* Message+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataProperties.swift"; sourceTree = "<group>"; };
CBC4B11F29B8D28D00650296 /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = "<group>"; };
CBC4B12029B8D28D00650296 /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = "<group>"; };
CBC4B12129B8D28D00650296 /* UserRole+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserRole+CoreDataClass.swift"; sourceTree = "<group>"; };
CBC4B12229B8D28D00650296 /* UserRole+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserRole+CoreDataProperties.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -54,9 +74,14 @@
CB1DCAC029B4F09D00B1D4E1 /* OSXChatGPT */ = {
isa = PBXGroup;
children = (
CBC4B11429B8CB1B00650296 /* Models */,
CB1DCAC129B4F09D00B1D4E1 /* OSXChatGPTApp.swift */,
CB1DCAD029B4F0C800B1D4E1 /* ChatMessageView.swift */,
CB1DCAD229B4F0EF00B1D4E1 /* ChatGPTManager.swift */,
CBC4B11929B8D11500650296 /* CoreDataManager.swift */,
CBC4B11B29B8D23C00650296 /* ViewModel.swift */,
CBC4B11529B8CB5300650296 /* HTTPClient.swift */,
CBC4B0FD29B8BF9600650296 /* OSXChatGPT.xcdatamodeld */,
CB1DCAC529B4F09F00B1D4E1 /* Assets.xcassets */,
CB1DCACA29B4F09F00B1D4E1 /* OSXChatGPT.entitlements */,
CB1DCAC729B4F09F00B1D4E1 /* Preview Content */,
@@ -72,6 +97,19 @@
path = "Preview Content";
sourceTree = "<group>";
};
CBC4B11429B8CB1B00650296 /* Models */ = {
isa = PBXGroup;
children = (
CBC4B11D29B8D28D00650296 /* Message+CoreDataClass.swift */,
CBC4B11E29B8D28D00650296 /* Message+CoreDataProperties.swift */,
CBC4B11F29B8D28D00650296 /* Conversation+CoreDataClass.swift */,
CBC4B12029B8D28D00650296 /* Conversation+CoreDataProperties.swift */,
CBC4B12129B8D28D00650296 /* UserRole+CoreDataClass.swift */,
CBC4B12229B8D28D00650296 /* UserRole+CoreDataProperties.swift */,
);
path = Models;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -142,9 +180,19 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CBC4B12829B8D28D00650296 /* UserRole+CoreDataProperties.swift in Sources */,
CBC4B12729B8D28D00650296 /* UserRole+CoreDataClass.swift in Sources */,
CBC4B11A29B8D11500650296 /* CoreDataManager.swift in Sources */,
CBC4B0FF29B8BF9600650296 /* OSXChatGPT.xcdatamodeld in Sources */,
CBC4B11629B8CB5300650296 /* HTTPClient.swift in Sources */,
CB1DCAD329B4F0EF00B1D4E1 /* ChatGPTManager.swift in Sources */,
CB1DCAD129B4F0C800B1D4E1 /* ChatMessageView.swift in Sources */,
CBC4B12429B8D28D00650296 /* Message+CoreDataProperties.swift in Sources */,
CB1DCAC229B4F09D00B1D4E1 /* OSXChatGPTApp.swift in Sources */,
CBC4B12629B8D28D00650296 /* Conversation+CoreDataProperties.swift in Sources */,
CBC4B11C29B8D23C00650296 /* ViewModel.swift in Sources */,
CBC4B12529B8D28D00650296 /* Conversation+CoreDataClass.swift in Sources */,
CBC4B12329B8D28D00650296 /* Message+CoreDataClass.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -283,6 +331,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = MustangYM.OSXChatGPT.OSXChatGPT;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -310,6 +359,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = MustangYM.OSXChatGPT.OSXChatGPT;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -340,6 +390,19 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
CBC4B0FD29B8BF9600650296 /* OSXChatGPT.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
CBC4B0FE29B8BF9600650296 /* OSXChatGPT.xcdatamodel */,
);
currentVersion = CBC4B0FE29B8BF9600650296 /* OSXChatGPT.xcdatamodel */;
path = OSXChatGPT.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = CB1DCAB629B4F09D00B1D4E1 /* Project object */;
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "A2D88682-0CD0-40F2-B2A2-DF64B1716221"
type = "1"
version = "2.0">
</Bucket>

View File

@@ -7,159 +7,66 @@
import Foundation
class Conversation: Identifiable, Codable, Equatable, ObservableObject {
static func == (lhs: Conversation, rhs: Conversation) -> Bool {
return lhs.sessionId == rhs.sessionId
}
var id = UUID()
@Published var name: String = "会话标题"
var message: String {
get {
return messages.first?.content ?? ""
}
}
var sessionId: String = ""
@Published var messages: [Message] = []
init(id: UUID = UUID(), name: String, sessionId: String, messages: [Message]) {
self.id = id
self.name = name
self.sessionId = sessionId
self.messages = messages
}
enum CodingKeys: String, CodingKey {
case id
case sessionId
case messages
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
sessionId = try container.decode(String.self, forKey: .sessionId)
messages = try container.decode([Message].self, forKey: .messages)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(sessionId, forKey: .sessionId)
try container.encode(messages, forKey: .messages)
}
}
enum MessageRole: Codable {
case mine
case chatGpt
}
class Message: Identifiable, Codable, ObservableObject{
var id = UUID()
var content: String = ""
var role: MessageRole = .mine
init(id: UUID = UUID(), content: String, role: MessageRole) {
self.id = id
self.content = content
self.role = role
}
enum CodingKeys: String, CodingKey {
case id
case content
case role
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
content = try container.decode(String.self, forKey: .content)
role = try container.decode(MessageRole.self, forKey: .role)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(content, forKey: .content)
try container.encode(role, forKey: .role)
}
}
class ConversationData: Codable, ObservableObject {
@Published var datas: [Conversation]
init(datas: [Conversation]) {
self.datas = datas
}
enum CodingKeys: String, CodingKey {
case datas
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
datas = try container.decode([Conversation].self, forKey: .datas)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(datas, forKey: .datas)
}
}
class ChatGPTManager {
static let shared = ChatGPTManager()
var allConversations: ConversationData = ConversationData(datas: [])
private var messagesDict: [String:[Message]] = [:]
private let httpClient: HTTPClient = HTTPClient()
private let apiKey : String = "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"
let gptRoleString: String = "assistant"
private init() {
allConversations = getDataFromUserDefaults()
}
///
func updateConversation(conversation: Conversation) {
if allConversations.datas.contains(conversation) {
//
if let index = allConversations.datas.firstIndex(where: { $0.sessionId == conversation.sessionId }) {
allConversations.datas[index] = conversation
}
}else {
//
if (conversation.messages.count > 0) {
allConversations.datas.append(conversation)
}
}
//
saveDataToUserDefaults(data: allConversations)
}
func createNewSessionId() -> String {
let timestamp = String(Date().timeIntervalSince1970)
return timestamp
}
}
/// Chat GTP
extension ChatGPTManager {
///
private func getDataFromUserDefaults() -> ConversationData {
let decoder = JSONDecoder()
let defaults = UserDefaults.standard
if let savedCustomDict = defaults.object(forKey: "ChatGPTSessions_key") as? Data {
if let loadedCustomDict = try? decoder.decode(ConversationData.self, from: savedCustomDict) {
loadedCustomDict.datas.removeAll { con in
con.messages.count == 0
///
func askChatGPT(messages: [Message], complete:(([String: Any]?, String?) -> ())?) {
var arr: [Message] = messages
arr = checkMaxAskMsgCount(maxCount: 6, messages: arr)
var temp: [[String: String]] = []
arr.forEach { msg in
temp.append(["role": msg.role ?? "user", "content": msg.text ?? ""])
}
let parameters = [
"model": "gpt-3.5-turbo-0301",
"messages": temp
] as [String : Any]
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
complete?(nil, "URL无效")
return
}
let headers = [
"Content-Type": "application/json",
"Authorization": "Bearer \(apiKey)"
]
httpClient.post(url: url, headers: headers, parameters: parameters) { data, error, resCode in
if let err = error {
complete?(nil, err.domain)
return
}else if let da = data {
guard let json = try? JSONSerialization.jsonObject(with: da) as? [String: Any] else {
DispatchQueue.main.async {
complete?(nil, "无效数据")
}
return
}
return loadedCustomDict
complete?(json, nil)
}else {
complete?(nil, "无效数据")
}
}
return ConversationData(datas: [])
}
///
private func saveDataToUserDefaults(data: ConversationData) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(data) {
UserDefaults.standard.set(encoded, forKey: "ChatGPTSessions_key")
private func checkMaxAskMsgCount(maxCount: Int, messages: [Message]) -> [Message] {
var msg = messages
if msg.count > maxCount {
msg.remove(at: 0)
return checkMaxAskMsgCount(maxCount: maxCount, messages: msg)
}else {
return msg
}
}
}

View File

@@ -9,8 +9,7 @@ import SwiftUI
import AppKit
/// main View
struct ContentView: View {
@EnvironmentObject var chats: ConversationData
@EnvironmentObject var viewModel: ViewModel
@State private var searchText = ""
@State private var shouldNavigate = false
@@ -21,8 +20,7 @@ struct ContentView: View {
SearchBar(text: $searchText)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 200, minHeight: 40, idealHeight: 40, maxHeight: 40)
.padding(.init(top: 10, leading: 10, bottom: 0, trailing: 0))
NavigationLink(destination: ChatView(chat: Conversation(name: "New", sessionId: ChatGPTManager.shared.createNewSessionId(), messages: [])), isActive: $shouldNavigate) {
NavigationLink(destination: ChatView(sesstionId: viewModel.createSesstionId()).environmentObject(viewModel), isActive: $shouldNavigate) {
Button {
self.shouldNavigate = true
} label: {
@@ -30,16 +28,16 @@ struct ContentView: View {
.padding(5)
Spacer()
}.background(Color.clear)
}.frame(minWidth: 30, idealWidth: 30, maxWidth: 30, minHeight: 30, idealHeight: 30, maxHeight: 30)
.padding(.init(top: 10, leading: 0, bottom: 0, trailing: 10))
}.frame(minHeight: 40, idealHeight: 40, maxHeight: 50)
Divider()
List(chats.datas.filter({ searchText.isEmpty ? true : $0.name.localizedCaseInsensitiveContains(searchText) })) { chat in
NavigationLink(destination: ChatView(chat: chat)) {
ChatRow(chat: chat)
List(viewModel.conversations.filter({ searchText.isEmpty ? true : $0.sesstionId.localizedCaseInsensitiveContains(searchText) })) { chat in
NavigationLink(destination: ChatView(sesstionId: chat.sesstionId).environmentObject(viewModel)) {
ChatRowContent(chat: chat)
}
}
.listStyle(SidebarListStyle())
@@ -81,32 +79,107 @@ struct SearchBar: View {
}
}
///
struct ChatRowContent: View {
@State var chat: Conversation
var body: some View {
MyView(chat: chat)
.frame(minHeight: 50, idealHeight: 50, maxHeight: 50)
}
}
///
struct ChatRow: View {
var chat: Conversation
@State var chat: Conversation
var body: some View {
HStack {
Image("openAI_icon")
.resizable()
.frame(width: 30, height: 30)
VStack(alignment: .leading) {
Text(chat.message)
Text(chat.lastMessage?.text ?? "newChat")
.font(.headline)
// Text(chat.message)
// .font(.subheadline)
// .foregroundColor(.gray)
// Text(chat.message)
// .font(.subheadline)
// .foregroundColor(.gray)
}
Spacer()
}
.padding(.vertical, 4)
}
}
struct MyView: NSViewRepresentable {
@State var chat: Conversation
func updateNSView(_ nsView: NSView, context: Context) {
// Update view properties and state here.
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: MyView
init(_ parent: MyView) {
self.parent = parent
super.init()
}
@objc func handleRightClick(_ sender: NSClickGestureRecognizer) {
if sender.state == .ended {
print("Right mouse button clicked!")
}
}
}
func makeNSView(context: Context) -> NSView {
let view = NSView()
view.wantsLayer = true
view.layer?.cornerRadius = 3
let swiftUIView = ChatRow(chat: chat)
.frame(width: 300, height: 50)
let hostingView = NSHostingView(rootView: swiftUIView)
view.addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
let gestureRecognizer = NSClickGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.handleRightClick(_:)))
gestureRecognizer.buttonMask = 0x2 //
view.addGestureRecognizer(gestureRecognizer)
return view
}
}
struct MessageView: View {
let message: Message
var body: some View {
HStack {
if message.role != ChatGPTManager.shared.gptRoleString {
Spacer()
Text(message.text ?? "")
.padding(12)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(6)
} else {
Text(message.text ?? "")
.padding(12)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(6)
Spacer()
}
}.padding(.trailing, (message.role == ChatGPTManager.shared.gptRoleString) ? 70 : 0)
.padding(.leading, (message.role != ChatGPTManager.shared.gptRoleString) ? 70 : 0)
}
}
///
struct ChatView: View {
let chat: Conversation
var sesstionId: String
@EnvironmentObject var viewModel: ViewModel
@State private var newMessageText = ""
@State private var scrollView: ScrollViewProxy?
@State private var scrollID = UUID()
@@ -117,15 +190,15 @@ struct ChatView: View {
ScrollView {
ScrollViewReader { scrollView in
VStack(alignment: .trailing, spacing: 8) {
ForEach(chat.messages) { message in
ForEach(viewModel.messages) { message in
MessageView(message: message)
.id(message.id) //
.padding(.trailing, 30)
.padding(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20))
}
}
.onChange(of: scrollID) { _ in
//
scrollView.scrollTo(chat.messages.last?.id, anchor: .bottom)
scrollView.scrollTo(viewModel.messages.last?.id, anchor: .bottom)
}
.background(Color.clear)
.onAppear {
@@ -135,10 +208,11 @@ struct ChatView: View {
}
.background(Color.clear)
.onAppear {
viewModel.fetchMessage(sesstionId: sesstionId)
// ScrollView
self.scrollView = scrollView
}
.onChange(of: chat.messages.count) { _ in
.onChange(of: viewModel.messages.count) { _ in
// ID 便
self.scrollID = UUID()
}
@@ -148,25 +222,20 @@ struct ChatView: View {
Divider()
HStack {
GeometryReader { geometry in
if #available(macOS 13.0, *) {
TextEditor(text: $newMessageText)
.font(.title3)
.lineSpacing(5)
.disableAutocorrection(true)
.padding()
.background(Color.clear)
.scrollContentBackground(.hidden)
.cornerRadius(10)
.frame(maxHeight: geometry.size.height)
.onChange(of: newMessageText) { _ in
if newMessageText.contains("\n") {
sendMessage(scrollView: scrollView)
}
TextEditor(text: $newMessageText)
.font(.title3)
.lineSpacing(5)
.disableAutocorrection(true)
.padding()
.background(Color.clear)
.scrollContentBackground(.hidden)
.cornerRadius(10)
.frame(maxHeight: geometry.size.height)
.onChange(of: newMessageText) { _ in
if newMessageText.contains("\n") {
sendMessage(scrollView: scrollView)
}
} else {
// Fallback on earlier versions
}
}
}
}
.padding(0)
@@ -174,10 +243,14 @@ struct ChatView: View {
}
.onAppear {
print("View appeared!")
viewModel.fetchMessage(sesstionId: sesstionId)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
scrollView?.scrollTo(viewModel.messages.last?.id, anchor: .bottom)
}
}
.onDisappear {
print("View disappeared!")
ChatGPTManager.shared.updateConversation(conversation: chat)
}
}
@@ -186,40 +259,21 @@ struct ChatView: View {
let temp = NSMutableString(string: newMessageText)
let replaceStr = temp.replacingOccurrences(of: "\n", with: "")
chat.messages.append(Message(content: replaceStr, role: .mine))
if chat.messages.count == 1 {
viewModel.addNewMessage(sesstionId: sesstionId, text: replaceStr, role: "user") {
scrollView?.scrollTo(viewModel.messages.last?.id, anchor: .bottom)
}
if viewModel.messages.count == 1 {
//
viewModel.fetchConversations()
}
ChatGPTManager.shared.updateConversation(conversation: chat)//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
newMessageText = ""
scrollView?.scrollTo(chat.messages.last?.id, anchor: .bottom)
scrollView?.scrollTo(viewModel.messages.last?.id, anchor: .bottom)
}
}
}
struct MessageView: View {
let message: Message
var body: some View {
HStack {
if message.role == .mine {
Spacer()
Text(message.content)
.padding(12)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(14)
} else {
Text(message.content)
.padding(12)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(14)
Spacer()
}
}
}
}
struct ChatView_Previews: PreviewProvider {

View File

@@ -0,0 +1,69 @@
//
// CoreDataManager.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
import SwiftUI
import CoreData
class CoreDataManager {
static var shared = CoreDataManager()
let container: NSPersistentCloudKitContainer!
private init() {
container = NSPersistentCloudKitContainer(name: "OSXChatGPT")
let storeURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("OSXChatGPT.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
func fetch<R: NSFetchRequestResult>(_ entityName: String, sorting: [NSSortDescriptor]?) -> [R] {
let request = NSFetchRequest<R>(entityName: entityName)
request.sortDescriptors = sorting
do {
return try container.viewContext.fetch(request)
} catch let error {
print("\(error)")
}
return []
}
func fetch<R: NSFetchRequestResult>(request: NSFetchRequest<R>) -> [R] {
do {
return try container.viewContext.fetch(request)
} catch let error {
print("\(error)")
}
return []
}
func delete(object: NSManagedObject) {
withAnimation {
container.viewContext.delete(object)
saveData()
}
}
func delete(objects: [NSManagedObject]) {
withAnimation {
objects.forEach { container.viewContext.delete($0) }
saveData()
}
}
func saveData() {
do {
try container.viewContext.save()
} catch {
print("Error saving (error)")
}
}
}

View File

@@ -0,0 +1,65 @@
//
// HTTPClient.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
import Foundation
class HTTPClient {
// static let shared = HTTPClient()
fileprivate var urlSession: URLSession!
fileprivate var sessionConfiguration: URLSessionConfiguration!
init() {
sessionConfiguration = URLSessionConfiguration.default
urlSession = URLSession(configuration: sessionConfiguration)
}
func setAdditionalHeaders(_ headers: Dictionary<String, AnyObject>) {
sessionConfiguration.httpAdditionalHeaders = headers
}
func post(url: URL, headers: [String: String], parameters: [String: Any], callback:@escaping (_ data: Data?, _ error: NSError?, _ resCode: Int?) -> Void) {
var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers
guard let postData = try? JSONSerialization.data(withJSONObject: parameters, options: []) else {
let statusError = NSError(domain:"Request parameters Error", code:-1, userInfo:[NSLocalizedDescriptionKey: "请求参数错误"])
DispatchQueue.main.async {
callback(nil, statusError, -1)
}
return
}
request.httpMethod = "POST"
request.httpBody = postData
let task = urlSession.dataTask(with: request) { data, response, error in
if let responseError = error {
DispatchQueue.main.async {
callback(nil, responseError as NSError?, -1)
}
print("HTTP request Error: \(responseError)")
} else if let httpResponse = response as? HTTPURLResponse {
let resCode = httpResponse.statusCode
print("HTTP Status Code: \(resCode)")
if resCode != 200 {
let statusError = NSError(domain:"Response Error", code:resCode, userInfo:[NSLocalizedDescriptionKey: "HTTP status code: \(resCode)"])
DispatchQueue.main.async {
callback(nil, statusError, resCode)
}
} else {
DispatchQueue.main.async {
callback(data, nil, 200)
}
}
}else {
let statusError = NSError(domain:"Response Error", code:-1, userInfo:[NSLocalizedDescriptionKey: "响应失败"])
DispatchQueue.main.async {
callback(nil, statusError, -1)
}
print("HTTP request Failure: \(statusError)")
}
}
task.resume()
}
}

View File

@@ -0,0 +1,15 @@
//
// Conversation+CoreDataClass.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
public class Conversation: NSManagedObject {
}

View File

@@ -0,0 +1,27 @@
//
// Conversation+CoreDataProperties.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
extension Conversation {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Conversation> {
return NSFetchRequest<Conversation>(entityName: "Conversation")
}
@NSManaged public var sesstionId: String
@NSManaged public var updateData: Date?
@NSManaged public var id: UUID?
@NSManaged public var lastMessage: Message?
}
extension Conversation : Identifiable {
}

View File

@@ -0,0 +1,15 @@
//
// Message+CoreDataClass.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
public class Message: NSManagedObject {
}

View File

@@ -0,0 +1,30 @@
//
// Message+CoreDataProperties.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
extension Message {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Message> {
return NSFetchRequest<Message>(entityName: "Message")
}
@NSManaged public var sesstionId: String
@NSManaged public var id: UUID?
@NSManaged public var type: Int16
@NSManaged public var text: String?
@NSManaged public var role: String?
@NSManaged public var createdDate: Date?
}
extension Message : Identifiable {
}

View File

@@ -0,0 +1,15 @@
//
// UserRole+CoreDataClass.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
public class UserRole: NSManagedObject {
}

View File

@@ -0,0 +1,25 @@
//
// UserRole+CoreDataProperties.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
//
import Foundation
import CoreData
extension UserRole {
@nonobjc public class func fetchRequest() -> NSFetchRequest<UserRole> {
return NSFetchRequest<UserRole>(entityName: "UserRole")
}
@NSManaged public var role: String
}
extension UserRole : Identifiable {
}

View File

@@ -2,9 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Conversation" representedClassName=".Conversation" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="sesstionId" attributeType="String"/>
<attribute name="updateData" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="lastMessage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Message"/>
</entity>
<entity name="Message" representedClassName=".Message" syncable="YES">
<attribute name="createdDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="role" optional="YES" attributeType="String"/>
<attribute name="sesstionId" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>

View File

@@ -9,9 +9,11 @@ import SwiftUI
@main
struct OSXChatGPTApp: App {
@StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(ChatGPTManager.shared.allConversations)
ContentView().environmentObject(viewModel)
// ContentView().environment(\.managedObjectContext, CoreDataManager.shared.container.viewContext).environmentObject(viewModel)
}
}
}

View File

@@ -0,0 +1,112 @@
//
// ViewModel.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/3/8.
//
import Foundation
import SwiftUI
import CoreData
@MainActor class ViewModel: ObservableObject {
@Published var conversations: [Conversation] = []
@Published var messages: [Message] = []
init() {
fetchConversations()
}
func updateConversation(sesstionId: String, message: Message?) {
var con = fetchConversation(sesstionId: sesstionId)
if con == nil {
con = Conversation(context: CoreDataManager.shared.container.viewContext)
con?.sesstionId = sesstionId
con?.id = UUID()
}
con?.lastMessage = message
con?.updateData = Date()
CoreDataManager.shared.saveData()
// if let idx = conversations.firstIndex(where:{ $0.sesstionId == sesstionId }) {
// conversations[idx].lastMessage = message
// }
}
func addConversation() {
let con = Conversation(context: CoreDataManager.shared.container.viewContext)
con.sesstionId = createSesstionId()
con.updateData = Date()
con.id = UUID()
CoreDataManager.shared.saveData()
fetchConversations()
}
func deleteConversation(_ conversation: Conversation) {
}
func fetchMessage(sesstionId: String) {
let request: NSFetchRequest<Message> = Message.fetchRequest()
request.predicate = NSPredicate(format: "sesstionId == %@", sesstionId)
let timestampSortDescriptor = NSSortDescriptor(key: "createdDate", ascending: true)
request.sortDescriptors = [timestampSortDescriptor]
let results: [Message] = CoreDataManager.shared.fetch(request: request)
messages = results
}
func addNewMessage(sesstionId: String, text: String, role: String, updateBlock: @escaping(() -> ())) {
let msg = Message(context: CoreDataManager.shared.container.viewContext)
msg.sesstionId = sesstionId
msg.text = text
msg.role = role
msg.id = UUID()
msg.createdDate = Date()
CoreDataManager.shared.saveData()
messages.append(msg)
updateConversation(sesstionId: sesstionId, message:messages.last)
ChatGPTManager.shared.askChatGPT(messages: messages) { json, error in
if let err = error {
print("\(err)")
return
}
let choices = json?["choices"] as? [Any]
let choice = choices?[0] as? [String: Any]
let myMessage = choice?["message"] as? [String: Any]
let content = myMessage?["content"] as? String
let roleStr = myMessage?["role"] as? String
let msg = Message(context: CoreDataManager.shared.container.viewContext)
msg.sesstionId = sesstionId
msg.role = roleStr ?? ""
msg.text = content ?? ""
msg.createdDate = Date()
msg.id = UUID()
CoreDataManager.shared.saveData()
self.messages.append(msg)
self.updateConversation(sesstionId: sesstionId, message: self.messages.last)
updateBlock()
print("回答的内容:\(String(describing: content))")
}
}
func createSesstionId() -> String {
let aa = Date.now.timeIntervalSince1970 * 1000
return String(aa)
}
}
extension ViewModel {
private func getNowData() -> Int64 {
return Int64(Date.now.timeIntervalSince1970)
}
}
extension ViewModel {
func fetchConversations() {
let completedDateSort = NSSortDescriptor(keyPath: \Conversation.updateData, ascending: false)
conversations = CoreDataManager.shared.fetch("Conversation", sorting: [completedDateSort])
}
private func fetchConversation(sesstionId: String) -> Conversation? {
let request: NSFetchRequest<Conversation> = Conversation.fetchRequest()
request.predicate = NSPredicate(format: "sesstionId == %@", sesstionId)
let results: [Conversation] = CoreDataManager.shared.fetch(request: request)
return results.first
}
}