feat: background tasks panel (Phase 7)
This commit is contained in:
1 parent
5c35fa9c65
commit
af296da6b8
7 files changed
+339
-5
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user