feat(ios): IOS-M0-01 RAG Chat 接入真实 API
- 新增 RagChatService: createSession/listSessions/getMessages/sendMessage/deleteSession - 新增 ChatSession/ChatMessage/SendMessageResponse 等数据模型 - AIChatViewModel 重写:移除 mock,调用真实 /rag-chat API - 自动选择知识库创建对话 session - AIChatPage 增加加载态(创建 session 中)和错误态(无知识库) - send() → POST /rag-chat/sessions/:id/messages 返回真实 AI 回复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
481f513b11
commit
444f1842d0
@ -392,6 +392,52 @@ struct BatchDeleteResponse: Codable {
|
||||
let count: Int?
|
||||
}
|
||||
|
||||
// MARK: - RAG Chat
|
||||
|
||||
struct ChatSession: Codable, Identifiable {
|
||||
let id: String
|
||||
let userId: String?
|
||||
let knowledgeBaseId: String?
|
||||
let title: String?
|
||||
let createdAt: String?
|
||||
let updatedAt: String?
|
||||
}
|
||||
|
||||
struct ChatMessage: Codable, Identifiable {
|
||||
let id: String
|
||||
let sessionId: String?
|
||||
let role: String
|
||||
let content: String
|
||||
let tokens: Int?
|
||||
let createdAt: String?
|
||||
let citations: [ChatCitation]?
|
||||
}
|
||||
|
||||
struct ChatCitation: Codable, Identifiable {
|
||||
let id: String
|
||||
let chunkId: String?
|
||||
let content: String?
|
||||
let score: Double?
|
||||
}
|
||||
|
||||
struct CreateSessionRequest: Codable {
|
||||
let knowledgeBaseId: String
|
||||
let title: String?
|
||||
}
|
||||
|
||||
struct SendMessageRequest: Codable {
|
||||
let content: String
|
||||
}
|
||||
|
||||
struct SendMessageResponse: Codable {
|
||||
let id: String?
|
||||
let role: String?
|
||||
let content: String?
|
||||
let tokens: Int?
|
||||
let blocked: Bool?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
// MARK: - File Upload (COS presigned URL flow)
|
||||
|
||||
struct FileUploadUrlRequest: Codable {
|
||||
|
||||
@ -273,6 +273,36 @@ class FeedbackService {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RAG Chat
|
||||
|
||||
@MainActor
|
||||
class RagChatService {
|
||||
static let shared = RagChatService()
|
||||
private let client = APIClient.shared
|
||||
|
||||
func createSession(knowledgeBaseId: String, title: String? = nil) async throws -> ChatSession {
|
||||
let body = CreateSessionRequest(knowledgeBaseId: knowledgeBaseId, title: title)
|
||||
return try await client.request("/rag-chat/sessions", method: "POST", body: body)
|
||||
}
|
||||
|
||||
func listSessions() async throws -> [ChatSession] {
|
||||
return try await client.request("/rag-chat/sessions")
|
||||
}
|
||||
|
||||
func getMessages(sessionId: String) async throws -> [ChatMessage] {
|
||||
return try await client.request("/rag-chat/sessions/\(sessionId)/messages")
|
||||
}
|
||||
|
||||
func sendMessage(sessionId: String, content: String) async throws -> SendMessageResponse {
|
||||
let body = SendMessageRequest(content: content)
|
||||
return try await client.request("/rag-chat/sessions/\(sessionId)/messages", method: "POST", body: body)
|
||||
}
|
||||
|
||||
func deleteSession(id: String) async throws {
|
||||
let _: GenericSuccessResponse = try await client.request("/rag-chat/sessions/\(id)", method: "DELETE")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -7,12 +7,22 @@ struct AIChatPage: View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
if vm.isCreatingSession {
|
||||
VStack(spacing: 12) {
|
||||
ProgressView().tint(Color.zxPurple)
|
||||
Text("正在准备 AI 助手...").font(.system(size: 13)).foregroundColor(Color.zxF04)
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = vm.sessionError {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle").font(.system(size: 36)).foregroundColor(Color.zxF04)
|
||||
Text(error).font(.system(size: 14)).foregroundColor(Color.zxF04)
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(vm.messages) { m in
|
||||
chatBubble(m)
|
||||
.id(m.id)
|
||||
chatBubble(m).id(m.id)
|
||||
}
|
||||
if vm.isSending {
|
||||
HStack(spacing: 8) {
|
||||
@ -20,11 +30,9 @@ struct AIChatPage: View {
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
ZXDotLoader(color: Color.zxPurple)
|
||||
.padding(.leading, 4)
|
||||
ZXDotLoader(color: Color.zxPurple).padding(.leading, 4)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
|
||||
@ -38,8 +46,10 @@ struct AIChatPage: View {
|
||||
.padding(.horizontal, 20).padding(.bottom, 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline).animatedTabBarHide()
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.task { await vm.load() }
|
||||
}
|
||||
|
||||
private func chatBubble(_ m: AIMessage) -> some View {
|
||||
|
||||
@ -13,30 +13,72 @@ enum AIMessageRole {
|
||||
|
||||
@MainActor
|
||||
final class AIChatViewModel: ObservableObject {
|
||||
@Published var messages: [AIMessage] = [
|
||||
AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手。")
|
||||
]
|
||||
@Published var messages: [AIMessage] = []
|
||||
@Published var inputText = ""
|
||||
@Published var isSending = false
|
||||
@Published var isCreatingSession = false
|
||||
@Published var sessionError: String?
|
||||
|
||||
private var sessionId: String?
|
||||
private var knowledgeBaseId: String?
|
||||
|
||||
var canSend: Bool {
|
||||
!inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending
|
||||
!inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending && sessionId != nil
|
||||
}
|
||||
|
||||
var isReady: Bool { sessionId != nil }
|
||||
|
||||
func load() async {
|
||||
guard sessionId == nil else { return }
|
||||
|
||||
// 自动选一个知识库
|
||||
if knowledgeBaseId == nil {
|
||||
do {
|
||||
let kbs = try await KnowledgeBaseService.shared.list(page: 1, limit: 1)
|
||||
knowledgeBaseId = kbs.first?.id
|
||||
} catch {}
|
||||
}
|
||||
|
||||
guard let kbId = knowledgeBaseId else {
|
||||
sessionError = "请先创建一个知识库,才能使用 AI 对话"
|
||||
return
|
||||
}
|
||||
|
||||
isCreatingSession = true
|
||||
do {
|
||||
let session = try await RagChatService.shared.createSession(knowledgeBaseId: kbId, title: nil)
|
||||
sessionId = session.id
|
||||
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于你的知识库回答问题。")]
|
||||
isCreatingSession = false
|
||||
} catch {
|
||||
sessionError = "创建对话失败"
|
||||
isCreatingSession = false
|
||||
}
|
||||
}
|
||||
|
||||
func send() {
|
||||
guard canSend else { return }
|
||||
guard canSend, let sid = sessionId else { return }
|
||||
let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
messages.append(AIMessage(role: .user, content: text))
|
||||
inputText = ""
|
||||
isSending = true
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_200_000_000)
|
||||
messages.append(AIMessage(
|
||||
role: .ai,
|
||||
content: "好的,我理解你的问题。需要我帮你制定学习计划吗?"
|
||||
))
|
||||
do {
|
||||
let resp = try await RagChatService.shared.sendMessage(sessionId: sid, content: text)
|
||||
if resp.blocked == true {
|
||||
messages.append(AIMessage(role: .ai, content: resp.message ?? "内容违规,请修改后重试"))
|
||||
} else {
|
||||
messages.append(AIMessage(role: .ai, content: resp.content ?? "抱歉,AI 暂时无法回复"))
|
||||
}
|
||||
} catch {
|
||||
messages.append(AIMessage(role: .ai, content: "发送失败: \(error.localizedDescription)"))
|
||||
}
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
|
||||
func setKnowledgeBase(_ id: String) {
|
||||
knowledgeBaseId = id
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user