Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, afterUpdate } from "svelte"; | |
| import gsap from "gsap"; | |
| import type { MessageSegment } from "../../../models/chat-data"; | |
| import type { TodoListView } from "../../../models/segment-view"; | |
| import { parseTodoList } from "../../../models/segment-view"; | |
| export let segment: MessageSegment; | |
| let todoList: TodoListView | null = null; | |
| let containerEl: HTMLDivElement; | |
| let progressBarEl: HTMLDivElement; | |
| let lastCompletedCount = 0; | |
| let taskElements: HTMLDivElement[] = []; | |
| $: { | |
| const content = segment.toolOutput || segment.content; | |
| todoList = parseTodoList(content); | |
| } | |
| $: progressPercentage = todoList | |
| ? (todoList.completedCount / todoList.totalCount) * 100 | |
| : 0; | |
| onMount(() => { | |
| if (containerEl) { | |
| gsap.from(containerEl, { | |
| opacity: 0, | |
| y: 5, | |
| duration: 0.3, | |
| ease: "power2.out", | |
| }); | |
| gsap.from(".todo-task", { | |
| opacity: 0, | |
| x: -10, | |
| duration: 0.2, | |
| stagger: 0.05, | |
| ease: "power2.out", | |
| }); | |
| } | |
| }); | |
| afterUpdate(() => { | |
| if (progressBarEl && todoList) { | |
| gsap.to(progressBarEl, { | |
| width: `${progressPercentage}%`, | |
| duration: 0.6, | |
| ease: "power2.out", | |
| }); | |
| if (todoList.completedCount > lastCompletedCount) { | |
| if (progressPercentage === 100) { | |
| gsap.to(containerEl, { | |
| borderColor: "rgba(76, 175, 80, 0.5)", | |
| duration: 0.3, | |
| yoyo: true, | |
| repeat: 2, | |
| ease: "power2.inOut", | |
| }); | |
| } else if (progressPercentage === 50 || progressPercentage === 75) { | |
| gsap.to(progressBarEl, { | |
| backgroundColor: "rgba(76, 175, 80, 0.6)", | |
| duration: 0.3, | |
| yoyo: true, | |
| repeat: 1, | |
| ease: "power2.inOut", | |
| }); | |
| } | |
| lastCompletedCount = todoList.completedCount; | |
| } | |
| } | |
| }); | |
| </script> | |
| {#if todoList} | |
| <div class="todo-segment" bind:this={containerEl}> | |
| <div class="todo-header"> | |
| <span class="todo-title">Tasks</span> | |
| <span class="todo-progress"> | |
| {todoList.completedCount}/{todoList.totalCount} | |
| </span> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-bar" bind:this={progressBarEl} style="width: {progressPercentage}%"></div> | |
| </div> | |
| <div class="todo-list"> | |
| {#each todoList.tasks as task, i} | |
| <div | |
| class="todo-task {task.status}" | |
| bind:this={taskElements[i]} | |
| > | |
| <span class="task-indicator"> | |
| {#if task.status === 'completed'} | |
| <span class="checkmark">✓</span> | |
| {:else if task.status === 'in_progress'} | |
| <span class="progress-dot">●</span> | |
| {:else} | |
| <span class="pending-circle">○</span> | |
| {/if} | |
| </span> | |
| <span class="task-text">{task.description}</span> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {:else} | |
| <pre class="raw-output">{segment.toolOutput || segment.content}</pre> | |
| {/if} | |
| <style> | |
| .todo-segment { | |
| background: rgba(255, 255, 255, 0.01); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin: 0.125rem 0; | |
| } | |
| .todo-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0.375rem 0.5rem; | |
| background: rgba(255, 255, 255, 0.02); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .todo-title { | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| .todo-progress { | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.4); | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 0.125rem 0.375rem; | |
| border-radius: 10px; | |
| } | |
| .progress-track { | |
| height: 3px; | |
| background: rgba(255, 255, 255, 0.05); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, | |
| rgba(76, 175, 80, 0.3), | |
| rgba(76, 175, 80, 0.5)); | |
| transition: width 0.6s ease-out; | |
| } | |
| .todo-list { | |
| padding: 0.375rem 0.5rem; | |
| } | |
| .todo-task { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.375rem; | |
| padding: 0.3rem 0; | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.7); | |
| transition: all 0.2s ease; | |
| } | |
| .task-indicator { | |
| width: 16px; | |
| height: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .checkmark { | |
| color: rgba(76, 175, 80, 0.9); | |
| font-weight: bold; | |
| animation: checkPop 0.3s ease-out; | |
| } | |
| @keyframes checkPop { | |
| 0% { transform: scale(0); } | |
| 50% { transform: scale(1.2); } | |
| 100% { transform: scale(1); } | |
| } | |
| .progress-dot { | |
| color: rgba(33, 150, 243, 0.9); | |
| animation: progressPulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes progressPulse { | |
| 0%, 100% { opacity: 0.4; transform: scale(0.8); } | |
| 50% { opacity: 1; transform: scale(1.2); } | |
| } | |
| .pending-circle { | |
| color: rgba(255, 255, 255, 0.3); | |
| font-size: 10px; | |
| } | |
| .task-text { | |
| flex: 1; | |
| } | |
| .todo-task.completed .task-text { | |
| text-decoration: line-through; | |
| opacity: 0.5; | |
| } | |
| .todo-task.in_progress { | |
| color: rgba(255, 255, 255, 0.9); | |
| background: rgba(33, 150, 243, 0.05); | |
| border-left: 2px solid rgba(33, 150, 243, 0.3); | |
| padding-left: 0.5rem; | |
| margin-left: -0.5rem; | |
| } | |
| .todo-task.in_progress .task-text { | |
| font-weight: 500; | |
| } | |
| .raw-output { | |
| margin: 0; | |
| padding: 0.25rem; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| font-family: "Monaco", "Menlo", "Consolas", monospace; | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| </style> |