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

138 lines
4.1 KiB
Swift

import SwiftUI
struct TaskPanelView: View {
@ObservedObject var taskStore: TaskStore
var body: some View {
ScrollView {
VStack(spacing: 12) {
if taskStore.sortedTasks.isEmpty {
emptyState
} else {
ForEach(taskStore.sortedTasks) { task in
TaskCardView(task: task)
}
}
}
.padding()
}
.navigationTitle("Background tasks")
#if DEBUG
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Sample", action: { taskStore.injectSampleEvents() })
.font(.caption)
}
}
#endif
}
private var emptyState: some View {
VStack(spacing: 8) {
Text("Nothing running")
.font(.headline)
Text("Ask Hope to research or compare something.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 48)
}
}
struct TaskCardView: View {
let task: TaskItem
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
statusDot
Text(task.description)
.font(.subheadline.weight(.semibold))
.lineLimit(2)
Spacer()
}
if !task.steps.isEmpty {
VStack(alignment: .leading, spacing: 4) {
ForEach(task.steps) { step in
HStack(alignment: .top, spacing: 6) {
Text(stepLabel(step.role))
.font(.caption2.weight(.semibold))
.foregroundStyle(stepColor(step.role))
Text(step.content ?? "")
.font(.caption)
.foregroundStyle(stepColor(step.role))
.lineLimit(3)
}
}
}
}
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(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 14))
}
private var statusDot: some View {
Circle()
.fill(dotColor)
.frame(width: 9, height: 9)
}
private var dotColor: Color {
switch task.status {
case .pending: return .gray
case .running: return .blue
case .completed: return .green
case .failed: return .red
}
}
private func block(_ text: String, color: Color, background: Color) -> some View {
Text(text)
.font(.subheadline)
.foregroundStyle(color)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(background, in: RoundedRectangle(cornerRadius: 10))
}
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
}())
}
}