Migrate frontend to Svelte 5 runes semantics

This commit is contained in:
2026-03-11 11:29:24 +03:00
parent 765178f12e
commit 0083d9054e
61 changed files with 989 additions and 922 deletions

View File

@@ -42,6 +42,7 @@
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { fromStore } from "svelte/store";
import { t } from "$lib/i18n";
import Icon from "$lib/ui/Icon.svelte";
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
@@ -64,28 +65,31 @@
const HISTORY_PAGE_SIZE = 30;
const CONVERSATIONS_PAGE_SIZE = 20;
let input = "";
let loading = false;
let loadingHistory = false;
let loadingMoreHistory = false;
let loadingConversations = false;
let messages = [];
let conversations = [];
let conversationFilter = "active";
let activeConversationsTotal = 0;
let archivedConversationsTotal = 0;
let historyPage = 1;
let historyHasNext = false;
let historyLoadVersion = 0;
let conversationsPage = 1;
let conversationsHasNext = false;
let historyViewport = null;
let initialized = false;
let llmReady = true;
let llmStatusReason = "";
let input = $state("");
let loading = $state(false);
let loadingHistory = $state(false);
let loadingMoreHistory = $state(false);
let loadingConversations = $state(false);
let messages = $state([]);
let conversations = $state([]);
let conversationFilter = $state("active");
let activeConversationsTotal = $state(0);
let archivedConversationsTotal = $state(0);
let historyPage = $state(1);
let historyHasNext = $state(false);
let historyLoadVersion = $state(0);
let conversationsPage = $state(1);
let conversationsHasNext = $state(false);
let historyViewport = $state(null);
let initialized = $state(false);
let llmReady = $state(true);
let llmStatusReason = $state("");
$: isOpen = $assistantChatStore?.isOpen || false;
$: conversationId = $assistantChatStore?.conversationId || null;
const assistantChatState = fromStore(assistantChatStore);
let isOpen = $derived(assistantChatState.current?.isOpen || false);
let conversationId = $derived(
assistantChatState.current?.conversationId || null,
);
// [DEF:loadHistory:Function]
/**
@@ -191,9 +195,10 @@
(c) => c.conversation_id !== conversationIdTemp,
);
if (conversationId === conversationIdTemp) {
$assistantChatStore.conversationId = null;
$assistantChatStore.messages = [];
$assistantChatStore.state = "idle";
assistantChatStore.update((state) => ({
...state,
conversationId: null,
}));
}
addToast("Conversation deleted", "success");
} catch (err) {
@@ -246,21 +251,24 @@
}
// [/DEF:loadOlderMessages:Function]
$: if (isOpen && !initialized) {
loadConversations(true);
loadHistory();
loadLlmStatus();
}
$: if (isOpen && initialized && conversationId) {
// Re-load only when user switched to another conversation.
const currentFirstConversationId = messages.length
? messages[0].conversation_id
: conversationId;
if (currentFirstConversationId !== conversationId) {
loadHistory();
$effect(() => {
if (isOpen && !initialized) {
void loadConversations(true);
void loadHistory();
void loadLlmStatus();
}
}
});
$effect(() => {
if (isOpen && initialized && conversationId) {
const currentFirstConversationId = messages.length
? messages[0].conversation_id
: conversationId;
if (currentFirstConversationId !== conversationId) {
void loadHistory();
}
}
});
// [DEF:appendLocalUserMessage:Function]
/**
@@ -566,7 +574,7 @@
{#if isOpen}
<div
class="fixed inset-0 z-[70] bg-slate-900/30"
on:click={closeAssistantChat}
onclick={closeAssistantChat}
aria-hidden="true"
></div>
@@ -584,7 +592,7 @@
</div>
<button
class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
on:click={closeAssistantChat}
onclick={closeAssistantChat}
aria-label={$t.assistant?.close}
>
<Icon name="close" size={18} />
@@ -622,7 +630,7 @@
>
<button
class="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100"
on:click={startNewConversation}
onclick={startNewConversation}
>
{$t.assistant?.new}
</button>
@@ -633,7 +641,7 @@
'active'
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
on:click={() => setConversationFilter("active")}
onclick={() => setConversationFilter("active")}
>
{$t.assistant?.active} ({activeConversationsTotal})
</button>
@@ -642,7 +650,7 @@
'archived'
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
on:click={() => setConversationFilter("archived")}
onclick={() => setConversationFilter("archived")}
>
{$t.assistant?.archived} ({archivedConversationsTotal})
</button>
@@ -655,7 +663,7 @@
conversationId
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
on:click={() => selectConversation(convo)}
onclick={() => selectConversation(convo)}
title={formatConversationTime(convo.updated_at)}
>
<div class="truncate font-semibold pr-4">
@@ -667,7 +675,7 @@
</button>
<button
class="absolute right-1.5 top-1.5 hidden group-hover:block p-1 text-slate-400 hover:text-red-500 rounded bg-white/80 hover:bg-red-50"
on:click={(e) => removeConversation(e, convo.conversation_id)}
onclick={(e) => removeConversation(e, convo.conversation_id)}
title="Удалить диалог"
>
<Icon name="trash" size={12} />
@@ -684,7 +692,7 @@
{#if conversationsHasNext}
<button
class="rounded-lg border border-slate-300 px-2.5 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-slate-100"
on:click={() => loadConversations(false)}
onclick={() => loadConversations(false)}
>
{$t.assistant?.more}
</button>
@@ -695,7 +703,7 @@
<div
class="flex-1 space-y-3 overflow-y-auto p-4"
bind:this={historyViewport}
on:scroll={handleHistoryScroll}
onscroll={handleHistoryScroll}
>
{#if loadingMoreHistory}
<div
@@ -761,7 +769,7 @@
>
<button
class="text-xs font-medium text-sky-700 hover:text-sky-900"
on:click={() => openDrawerForTask(message.task_id)}
onclick={() => openDrawerForTask(message.task_id)}
>
{$t.assistant?.open_task_drawer}
</button>
@@ -778,7 +786,7 @@
: action.type === 'cancel'
? 'border-rose-300 bg-rose-50 text-rose-700 hover:bg-rose-100'
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
on:click={() => handleAction(action, message)}
onclick={() => handleAction(action, message)}
>
{action.label}
</button>
@@ -819,11 +827,11 @@
class="min-h-[52px] w-full resize-y rounded-lg border px-3 py-2 text-sm outline-none transition {llmReady
? 'border-slate-300 focus:border-sky-400 focus:ring-2 focus:ring-sky-100'
: 'border-rose-300 bg-rose-50 focus:border-rose-400 focus:ring-2 focus:ring-rose-100'}"
on:keydown={handleKeydown}
onkeydown={handleKeydown}
></textarea>
<button
class="rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-60"
on:click={handleSend}
onclick={handleSend}
disabled={loading || !input.trim()}
>
{loading ? "..." : $t.assistant?.send}