feat(ios): IOS-M1-04 AI 对话增强
- AIChatPage 新增引用来源展示(AI回答下方胶囊标签) - 新增复制/重新生成按钮 - 右上角会话列表按钮 - SendMessageResponse 新增 citations 字段 - AIMessage 新增 citations 属性 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2fcf68a64b
commit
5ff979cc97
@ -441,6 +441,7 @@ struct SendMessageResponse: Codable {
|
||||
let tokens: Int?
|
||||
let blocked: Bool?
|
||||
let message: String?
|
||||
let citations: [ChatCitation]?
|
||||
}
|
||||
|
||||
// MARK: - Document Import
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct AIChatPage: View {
|
||||
@StateObject private var vm = AIChatViewModel()
|
||||
@State private var showSessions = false
|
||||
@State private var sessions: [ChatSession] = []
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@ -22,33 +25,58 @@ struct AIChatPage: View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(vm.messages) { m in
|
||||
chatBubble(m).id(m.id)
|
||||
VStack(alignment: m.role == .user ? .trailing : .leading, spacing: 6) {
|
||||
chatBubble(m)
|
||||
// Citations below AI messages
|
||||
if m.role == .ai, let citations = m.citations, !citations.isEmpty {
|
||||
VStack(spacing: 4) {
|
||||
ForEach(citations.prefix(3)) { c in
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "doc.text").font(.system(size: 9)).foregroundColor(Color.zxF04)
|
||||
Text(c.content?.prefix(60).description ?? "").font(.system(size: 10)).foregroundColor(Color.zxF04).lineLimit(1)
|
||||
}.padding(.horizontal, 8).padding(.vertical, 4)
|
||||
.background(Color.zxFill004).clipShape(Capsule())
|
||||
}
|
||||
}.padding(.leading, 36)
|
||||
}
|
||||
// Actions for AI messages
|
||||
if m.role == .ai {
|
||||
HStack(spacing: 16) {
|
||||
Button { UIPasteboard.general.string = m.content } label: {
|
||||
Label("复制", systemImage: "doc.on.doc").font(.system(size: 11)).foregroundColor(Color.zxF04)
|
||||
}
|
||||
Button { Task { vm.send() } } label: {
|
||||
Label("重新生成", systemImage: "arrow.clockwise").font(.system(size: 11)).foregroundColor(Color.zxF04)
|
||||
}
|
||||
}.padding(.leading, 36)
|
||||
}
|
||||
}.id(m.id).padding(.horizontal, 20)
|
||||
}
|
||||
if vm.isSending {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
ZXDotLoader(color: Color.zxPurple).padding(.leading, 4)
|
||||
Spacer()
|
||||
.frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle())
|
||||
ZXDotLoader(color: Color.zxPurple).padding(.leading, 4); Spacer()
|
||||
}.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
|
||||
}.padding(.top, 8).padding(.bottom, 100)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.onChange(of: vm.messages.count) { _ in
|
||||
withAnimation { proxy.scrollTo(vm.messages.last?.id) }
|
||||
}
|
||||
.onChange(of: vm.messages.count) { _ in withAnimation { proxy.scrollTo(vm.messages.last?.id) } }
|
||||
}
|
||||
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() })
|
||||
.padding(.horizontal, 20).padding(.bottom, 34)
|
||||
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() }).padding(.horizontal, 20).padding(.bottom, 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline).animatedTabBarHide()
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button { Task { sessions = (try? await RagChatService.shared.listSessions()) ?? []; showSessions = true } } label: {
|
||||
Image(systemName: "list.bullet.rectangle").font(.system(size: 16)).foregroundColor(Color.zxF05)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { await vm.load() }
|
||||
}
|
||||
|
||||
@ -56,21 +84,16 @@ struct AIChatPage: View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
if m.role == .ai {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
.frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle())
|
||||
}
|
||||
Text(m.content).zxFontScaled(size: 14)
|
||||
.foregroundColor(m.role == .user ? .white : Color.zxF007)
|
||||
.padding(12)
|
||||
.foregroundColor(m.role == .user ? .white : Color.zxF007).padding(12)
|
||||
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
if m.role == .user {
|
||||
Circle().frame(width: 28, height: 28)
|
||||
.foregroundColor(Color.zxPurpleBG(0.2))
|
||||
.overlay(Text("我").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
|
||||
.foregroundColor(Color.zxPurpleBG(0.2)).overlay(Text("我").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
|
||||
}.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,13 @@ struct AIMessage: Identifiable {
|
||||
let id = UUID()
|
||||
let role: AIMessageRole
|
||||
let content: String
|
||||
let citations: [ChatCitation]?
|
||||
}
|
||||
|
||||
struct AIMessageCitation: Identifiable {
|
||||
let id: String
|
||||
let title: String?
|
||||
let content: String?
|
||||
}
|
||||
|
||||
enum AIMessageRole {
|
||||
@ -59,7 +66,7 @@ final class AIChatViewModel: ObservableObject {
|
||||
func send() {
|
||||
guard canSend, let sid = sessionId else { return }
|
||||
let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
messages.append(AIMessage(role: .user, content: text))
|
||||
messages.append(AIMessage(role: .user, content: text, citations: nil))
|
||||
inputText = ""
|
||||
isSending = true
|
||||
|
||||
@ -67,9 +74,9 @@ final class AIChatViewModel: ObservableObject {
|
||||
do {
|
||||
let resp = try await RagChatService.shared.sendMessage(sessionId: sid, content: text)
|
||||
if resp.blocked == true {
|
||||
messages.append(AIMessage(role: .ai, content: resp.message ?? "内容违规,请修改后重试"))
|
||||
messages.append(AIMessage(role: .ai, content: resp.message ?? "内容违规,请修改后重试", citations: nil))
|
||||
} else {
|
||||
messages.append(AIMessage(role: .ai, content: resp.content ?? "抱歉,AI 暂时无法回复"))
|
||||
messages.append(AIMessage(role: .ai, content: resp.content ?? "抱歉,AI 暂时无法回复", citations: resp.citations))
|
||||
}
|
||||
} catch {
|
||||
messages.append(AIMessage(role: .ai, content: "发送失败: \(error.localizedDescription)"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user