mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
121 lines
4.4 KiB
Swift
121 lines
4.4 KiB
Swift
import HapiClient
|
|
import HapiProtocol
|
|
import HapiUI
|
|
import SwiftUI
|
|
|
|
/// Tool groups contribute only their displayed summary to transcript diffs.
|
|
enum TranscriptRow: Identifiable, Equatable {
|
|
case history(ChatHistoryPagingState.Phase, Bool)
|
|
case message(VisibleChatBlock)
|
|
case group(ToolGroupPresentation)
|
|
|
|
static let historyID = "chat-history-control"
|
|
var role: TranscriptRowRole {
|
|
switch self {
|
|
case .history: .history
|
|
case .group: .tool
|
|
case .message(.toolGroup): .tool
|
|
case .message(.block(.userText)): .user
|
|
case .message(.block(.toolCall)): .tool
|
|
default: .content
|
|
}
|
|
}
|
|
var id: String {
|
|
switch self {
|
|
case .history: Self.historyID
|
|
case .message(let block): block.stableId
|
|
case .group(let group): group.id
|
|
}
|
|
}
|
|
|
|
func spacing(after previous: TranscriptRow?) -> CGFloat {
|
|
return role.spacing(after: previous?.role)
|
|
}
|
|
}
|
|
|
|
struct ChatTranscriptView: View {
|
|
let model: ChatModel
|
|
|
|
private var rows: [TranscriptRow] {
|
|
var rows: [TranscriptRow] = [.history(model.historyPaging.phase, model.hasMore)]
|
|
for block in model.blocks {
|
|
if case .toolGroup(let group) = block {
|
|
rows.append(.group(ToolGroupPresentation(group)))
|
|
} else {
|
|
rows.append(.message(block))
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
var body: some View {
|
|
AnchoredTranscriptList(
|
|
items: rows,
|
|
historyVersion: model.historyVersion,
|
|
jumpToken: model.jumpToLatestToken,
|
|
historyControlID: TranscriptRow.historyID,
|
|
isInspectionPresented: model.isInspectingContent,
|
|
onViewport: { viewport in
|
|
model.readingViewportChanged(followsTail: viewport.followsTail, needsOlder: viewport.needsOlder)
|
|
},
|
|
onLayout: { version, progress in model.historyLaidOut(version: version, madeProgress: progress) },
|
|
spacingBefore: { previous, row in row.spacing(after: previous) }
|
|
) { row in
|
|
// The list bridges the current environment into its hosting roots.
|
|
AnyView(rowView(row))
|
|
}
|
|
.overlay(alignment: .bottomTrailing) {
|
|
if !model.followsTail || model.hasTrimmedTail || model.isJumpingToLatest {
|
|
Button(action: model.jumpToLatest) {
|
|
HStack(spacing: 6) {
|
|
if model.isJumpingToLatest { ProgressView().controlSize(.small) }
|
|
Text("Back to latest")
|
|
Image(systemName: "arrow.down")
|
|
}
|
|
.font(.footnote.weight(.medium))
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 9)
|
|
.background(.regularMaterial, in: Capsule())
|
|
}
|
|
.disabled(model.isJumpingToLatest)
|
|
.accessibilityIdentifier("chat-latest")
|
|
.padding(12)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func rowView(_ row: TranscriptRow) -> some View {
|
|
switch row {
|
|
case .history(let phase, let hasMore):
|
|
Button(action: model.retryHistory) {
|
|
HStack(spacing: 8) {
|
|
if !hasMore {
|
|
Text("Beginning of conversation")
|
|
} else {
|
|
switch phase {
|
|
case .loading, .retrying, .awaitingLayout:
|
|
ProgressView().controlSize(.small)
|
|
Text("Loading older messages…")
|
|
case .failed: Text("Couldn't load history. Tap to retry.")
|
|
case .paused: Text("Continue loading history")
|
|
default: Text("Load older messages")
|
|
}
|
|
}
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, minHeight: 44)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(!hasMore || model.isLoadingOlder || model.isSyncingTail)
|
|
.accessibilityIdentifier("chat-history")
|
|
case .message(let block):
|
|
ChatBlockCard(block: block, basePath: model.basePath)
|
|
case .group(let presentation):
|
|
ToolGroupBlockView(presentation: presentation)
|
|
}
|
|
}
|
|
}
|