Files
hope-voice-api/Sources/Features/Tasks/TaskPanelView.swift
T

142 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct TaskPanelView: View {
@ObservedObject var taskStore: TaskStore
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 20) {
if taskStore.sortedTasks.isEmpty {
emptyState
} else {
Text(taskStore.activeCount > 0 ? "Hope is working on \(taskStore.activeCount) task\(taskStore.activeCount == 1 ? "" : "s"). You can keep talking." : "Results from the things you’ve asked Hope to work on.")
.font(.subheadline).foregroundStyle(.secondary)
ForEach(taskStore.sortedTasks) { task in
TaskCardView(task: task)
}
}
}
.padding()
}
.navigationTitle("Activity")
.background(Color(.systemGroupedBackground))
}
private var emptyState: some View {
ContentUnavailableView {
Label("A little help, in the background", systemImage: "checklist")
} description: {
Text("Ask Hope to research a topic or compare your options. Follow the progress and read the results here.")
}
.padding(.top, 48)
}
}
struct TaskCardView: View {
let task: TaskItem
@State private var showSteps = false
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Label(statusTitle, systemImage: statusSymbol)
.font(.subheadline.weight(.medium))
.foregroundStyle(dotColor)
Spacer()
Text(task.createdAt, style: .time).font(.caption).foregroundStyle(.secondary)
}
Text(task.description).font(.headline).fixedSize(horizontal: false, vertical: true)
if !task.steps.isEmpty {
DisclosureGroup("\(task.steps.count) progress updates", isExpanded: $showSteps) {
VStack(alignment: .leading, spacing: 12) {
ForEach(task.steps) { step in
VStack(alignment: .leading, spacing: 4) {
Text(stepLabel(step.role))
.font(.caption.weight(.semibold)).foregroundStyle(.secondary)
Text(step.content ?? "")
.font(.subheadline).textSelection(.enabled)
}
}
}.padding(.top, 8)
}
.font(.subheadline)
}
if let result = task.result {
block(result, color: .green, background: Color.green.opacity(0.12))
}
if let error = task.error {
block(error, color: .red, background: Color.red.opacity(0.12))
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 16))
}
private var statusTitle: String {
switch task.status {
case .pending: "Queued"
case .running: "In progress"
case .completed: "Complete"
case .failed: "Needs attention"
}
}
private var statusSymbol: String {
switch task.status {
case .pending: "clock"
case .running: "arrow.triangle.2.circlepath"
case .completed: "checkmark.circle.fill"
case .failed: "exclamationmark.circle"
}
}
private var dotColor: Color {
switch task.status {
case .pending: return .gray
case .running: return HopeStyle.tint
case .completed: return .green
case .failed: return .red
}
}
private func block(_ text: String, color: Color, background: Color) -> some View {
Text(text)
.font(.body)
.foregroundStyle(.primary)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
private func stepLabel(_ role: String?) -> String {
switch role {
case "thinking": return "Thinking"
case "tool_call": return "Tool"
case "tool_result": return "Result"
case "text": return "Note"
default: return role ?? "Step"
}
}
private func stepColor(_ role: String?) -> Color {
switch role {
case "thinking": return .blue
case "tool_call": return .indigo
case "tool_result": return .secondary
default: return .primary
}
}
}
#Preview {
NavigationStack {
TaskPanelView(taskStore: {
let store = TaskStore()
#if DEBUG
store.injectSampleEvents()
#endif
return store
}())
}
}