feat: tasks

This commit is contained in:
Timothy Jaeryang Baek
2026-03-29 18:01:04 -05:00
parent 66c9bf57da
commit bcb71bb520
12 changed files with 382 additions and 13 deletions
+9
View File
@@ -159,6 +159,8 @@
let chat = null;
let tags = [];
let chatTasks = [];
let history = {
messages: {},
currentId: null
@@ -449,6 +451,8 @@
message.content = data.content;
} else if (type === 'chat:message:files' || type === 'files') {
message.files = data.files;
} else if (type === 'chat:message:tasks') {
chatTasks = data.tasks;
} else if (type === 'chat:message:embeds' || type === 'embeds') {
message.embeds = data.embeds;
@@ -1156,6 +1160,7 @@
chatFiles = [];
params = {};
taskIds = null;
chatTasks = [];
if ($page.url.searchParams.get('youtube')) {
await uploadWeb(`https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`);
@@ -1268,6 +1273,9 @@
params = chatContent?.params ?? {};
chatFiles = chatContent?.files ?? [];
// Load tasks from chat-level DB field
chatTasks = chat?.tasks ?? [];
autoScroll = true;
await tick();
@@ -2863,6 +2871,7 @@
{createMessagePair}
{onUpload}
messageQueue={$chatRequestQueues[$chatId] ?? []}
{chatTasks}
onQueueSendNow={async (id) => {
const queue = $chatRequestQueues[$chatId] ?? [];
const item = queue.find((m) => m.id === id);
@@ -99,6 +99,7 @@
import InputModal from '../common/InputModal.svelte';
import Expand from '../icons/Expand.svelte';
import QueuedMessageItem from './MessageInput/QueuedMessageItem.svelte';
import TaskList from './Messages/ResponseMessage/TaskList.svelte';
const i18n = getContext('i18n');
@@ -140,6 +141,8 @@
export let onQueueEdit: (id: string) => void = () => {};
export let onQueueDelete: (id: string) => void = () => {};
export let chatTasks = [];
let inputContent = null;
let showInputVariablesModal = false;
@@ -1217,6 +1220,13 @@
on:click={() => createMessagePair(prompt)}
/>
<!-- Task list display -->
{#if chatTasks.length > 0}
<div class="mx-1">
<TaskList tasks={chatTasks} />
</div>
{/if}
<!-- Queued messages display -->
{#if messageQueue.length > 0}
<div
@@ -43,6 +43,7 @@
export let readOnly = false;
export let editCodeBlock = true;
export let topPadding = false;
</script>
<div
@@ -0,0 +1,84 @@
<script lang="ts">
import { getContext } from 'svelte';
import { slide } from 'svelte/transition';
import TaskListIcon from '$lib/components/icons/TaskList.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
const i18n = getContext('i18n');
export let tasks: Array<{ id: string; content: string; status: string }> = [];
let collapsed = false;
$: completedCount = tasks.filter((t) => t.status === 'completed').length;
$: totalCount = tasks.length;
$: hasActive = tasks.some((t) => t.status === 'pending' || t.status === 'in_progress');
</script>
{#if tasks.length > 0 && hasActive}
<div
class="my-2 rounded-xl border border-gray-50 dark:border-gray-850 bg-white dark:bg-gray-900"
transition:slide={{ duration: 200 }}
>
<!-- Header -->
<div class="flex items-center justify-between px-3.5 py-2">
<div class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<TaskListIcon className="w-3.5 h-3.5" />
<span>
{completedCount} {$i18n.t('out of')} {totalCount} {$i18n.t('tasks completed')}
</span>
</div>
<button
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
on:click={() => (collapsed = !collapsed)}
aria-label={collapsed ? 'Expand' : 'Collapse'}
>
{#if collapsed}
<ChevronDown className="w-2.5 h-2.5" />
{:else}
<ChevronUp className="w-2.5 h-2.5" />
{/if}
</button>
</div>
<!-- Task list -->
{#if !collapsed}
<div class="px-3.5 pb-2.5 space-y-0.5" transition:slide={{ duration: 150 }}>
{#each tasks as task, idx (task.id)}
<div class="flex items-start gap-2 py-0.5 text-xs">
<span class="flex-shrink-0 mt-0.5 text-gray-400 dark:text-gray-500">
{#if task.status === 'completed'}
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{:else if task.status === 'in_progress'}
<svg class="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 3a9 9 0 1 0 9 9" stroke-linecap="round" />
</svg>
{:else if task.status === 'cancelled'}
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9" stroke-dasharray="4 3" />
</svg>
{:else}
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9" />
</svg>
{/if}
</span>
<span
class="line-clamp-2 {task.status === 'completed'
? 'line-through text-gray-400 dark:text-gray-500'
: task.status === 'cancelled'
? 'line-through text-gray-400 dark:text-gray-600'
: 'text-gray-700 dark:text-gray-300'}"
>
{idx + 1}. {task.content}
</span>
</div>
{/each}
</div>
{/if}
</div>
{/if}
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg
class={className}
stroke-width="1.5"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M20 20L15 15M15 15V19M15 15H19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M4 20L9 15M9 15V19M9 15H5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M20 4L15 9M15 9V5M15 9H19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M4 4L9 9M9 9V5M9 9H5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
+9 -13
View File
@@ -1,21 +1,17 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
><path d="M9 9L4 4M4 4V8M4 4H8" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M15 9L20 4M20 4V8M20 4H16"
stroke-linecap="round"
stroke-linejoin="round"
></path><path d="M9 15L4 20M4 20V16M4 20H8" stroke-linecap="round" stroke-linejoin="round"
></path><path d="M15 15L20 20M20 20V16M20 20H16" stroke-linecap="round" stroke-linejoin="round"
></path></svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M9 9L4 4M4 4V8M4 4H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M15 9L20 4M20 4V8M20 4H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9 15L4 20M4 20V16M4 20H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M15 15L20 20M20 20V16M20 20H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg
class={className}
stroke-width="1.5"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9 6L20 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M3.80002 5.79999L4.60002 6.59998L6.60001 4.59999" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M3.80002 11.8L4.60002 12.6L6.60001 10.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M3.80002 17.8L4.60002 18.6L6.60001 16.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9 12L20 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9 18L20 18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
@@ -42,6 +42,10 @@
code_interpreter: {
label: $i18n.t('Code Interpreter'),
description: $i18n.t('Execute code')
},
tasks: {
label: $i18n.t('Task Management'),
description: $i18n.t('Break down complex requests into trackable steps')
}
};