<!-- [DEF:AssistantChatPanel:Component] -->
<script>
  /**
   * @TIER: CRITICAL
   * @PURPOSE: Slide-out assistant chat panel for natural language command execution and task tracking.
   * @LAYER: UI
   * @RELATION: BINDS_TO -> assistantChatStore
   * @RELATION: CALLS -> frontend.src.lib.api.assistant
   * @RELATION: DISPATCHES -> taskDrawerStore
   * @SEMANTICS: assistant-chat, confirmation, long-running-task, progress-tracking
   * @INVARIANT: User commands and assistant responses are appended in chronological order.
   * @INVARIANT: Risky operations are executed only through explicit confirm action.
   *
   * @UX_STATE: Closed -> Panel is hidden.
   * @UX_STATE: LoadingHistory -> Existing conversation history is loading.
   * @UX_STATE: Idle -> Input is available and no request in progress.
   * @UX_STATE: Sending -> Input locked while request is pending.
   * @UX_STATE: Error -> Failed action rendered as assistant failed message.
   * @UX_FEEDBACK: Started operation surfaces task_id and quick action to open task drawer.
   * @UX_RECOVERY: User can retry command or action from input and action buttons.
   * @UX_TEST: LoadingHistory -> {openPanel: true, expected: loading block visible}
   * @UX_TEST: Sending -> {sendMessage: "branch", expected: send button disabled}
   * @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id}
   * @TEST_DATA: assistant_llm_ready -> {"llmStatus":{"configured":true,"reason":"ok"},"messages":[{"role":"assistant","text":"Ready","state":"success"}]}
   * @TEST_DATA: assistant_llm_not_configured -> {"llmStatus":{"configured":false,"reason":"invalid_api_key"}}
   *
   * @TEST_CONTRACT Component_AssistantChatPanel ->
   * {
   *   required_props: {},
   *   optional_props: {},
   *   invariants: [
   *       "Loads history and LLM status on mount/open",
   *       "Appends messages and sends to API correctly",
   *       "Handles action buttons (confirm, open task) properly"
   *   ]
   * }
   * @TEST_FIXTURE chat_open -> {"isOpen": true, "messages": [{"role": "assistant", "text": "Hello"}]}
   * @TEST_EDGE server_error -> Appends error message with "failed" state
   * @TEST_EDGE llm_not_ready -> Renders LLM config warning banner
   * @TEST_INVARIANT action_handling -> verifies: [chat_open]
   */

  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";
  import {
    assistantChatStore,
    closeAssistantChat,
    setAssistantConversationId,
  } from "$lib/stores/assistantChat.js";
  import {
    sendAssistantMessage,
    confirmAssistantOperation,
    cancelAssistantOperation,
    getAssistantHistory,
    getAssistantConversations,
    deleteAssistantConversation,
  } from "$lib/api/assistant.js";
  import { api } from "$lib/api.js";
  import { gitService } from "../../../services/gitService.js";

  const HISTORY_PAGE_SIZE = 30;
  const CONVERSATIONS_PAGE_SIZE = 20;

  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("");

  const assistantChatState = fromStore(assistantChatStore);
  let isOpen = $derived(assistantChatState.current?.isOpen || false);
  let conversationId = $derived(
    assistantChatState.current?.conversationId || null,
  );

  // [DEF:loadHistory:Function]
  /**
   * @PURPOSE: Load current conversation history when panel becomes visible.
   * @PRE: Panel is open and history request is not already running.
   * @POST: messages are populated from persisted history and conversation id is synchronized.
   * @SIDE_EFFECT: Performs API call to assistant history endpoint.
   */
  async function loadHistory(targetConversationId = conversationId) {
    if (loadingHistory || !isOpen) return;
    const requestVersion = ++historyLoadVersion;
    loadingHistory = true;
    try {
      const history = await getAssistantHistory(
        1,
        HISTORY_PAGE_SIZE,
        targetConversationId,
        true,
      );
      if (requestVersion !== historyLoadVersion) {
        return;
      }
      messages = (history.items || []).map((msg) => ({
        ...msg,
        actions: msg.actions || msg.metadata?.actions || [],
      }));
      historyPage = 1;
      historyHasNext = Boolean(history.has_next);
      if (
        !targetConversationId &&
        history.conversation_id &&
        history.conversation_id !== conversationId
      ) {
        setAssistantConversationId(history.conversation_id);
      }
      initialized = true;
      // prettier-ignore
      console.log("[AssistantChatPanel][history][loadHistory:SUCCESS] History loaded");
    } catch (err) {
      console.error(
        "[AssistantChatPanel][history][loadHistory:FAILED] Failed to load history",
        err,
      );
    } finally {
      loadingHistory = false;
    }
  }
  // [/DEF:loadHistory:Function]

  // [DEF:loadConversations:Function]
  /**
   * @PURPOSE: Load paginated conversation summaries for quick switching UI.
   * @PRE: Panel is open and request not already running.
   * @POST: conversations list refreshed or appended based on page.
   */
  async function loadConversations(reset = false) {
    if (loadingConversations || !isOpen) return;
    loadingConversations = true;
    try {
      const page = reset ? 1 : conversationsPage + 1;
      const includeArchived = conversationFilter === "archived";
      const archivedOnly = conversationFilter === "archived";
      const response = await getAssistantConversations(
        page,
        CONVERSATIONS_PAGE_SIZE,
        includeArchived,
        "",
        archivedOnly,
      );
      const rows = response.items || [];
      conversations = reset ? rows : [...conversations, ...rows];
      conversationsPage = page;
      conversationsHasNext = Boolean(response.has_next);
      activeConversationsTotal =
        response.active_total ?? activeConversationsTotal;
      archivedConversationsTotal =
        response.archived_total ?? archivedConversationsTotal;
      // prettier-ignore
      console.log("[AssistantChatPanel][conversations][loadConversations:SUCCESS]");
    } catch (err) {
      console.error(
        "[AssistantChatPanel][conversations][loadConversations:FAILED]",
        err,
      );
    } finally {
      loadingConversations = false;
    }
  }
  // [/DEF:loadConversations:Function]

  // [DEF:removeConversation:Function]
  // @PURPOSE: Removes a conversation from the list and deletes it from the backend.
  // @PRE: conversationId string is provided.
  // @POST: It is soft-deleted from the API and removed from local UI. If active, reset state.
  async function removeConversation(e, conversationIdTemp) {
    if (e) {
      e.stopPropagation();
      e.preventDefault();
    }
    try {
      await deleteAssistantConversation(conversationIdTemp);
      conversations = conversations.filter(
        (c) => c.conversation_id !== conversationIdTemp,
      );
      if (conversationId === conversationIdTemp) {
        assistantChatStore.update((state) => ({
          ...state,
          conversationId: null,
        }));
      }
      addToast("Conversation deleted", "success");
    } catch (err) {
      addToast("Failed to delete conversation: " + err.message, "error");
    }
  }
  // [/DEF:removeConversation:Function]

  // [DEF:loadOlderMessages:Function]
  /**
   * @PURPOSE: Lazy-load older messages for active conversation when user scrolls to top.
   * @PRE: History has next page and active conversation is known.
   * @POST: Older messages are prepended while preserving order.
   */
  async function loadOlderMessages() {
    if (
      loadingMoreHistory ||
      loadingHistory ||
      !historyHasNext ||
      !conversationId
    )
      return;
    loadingMoreHistory = true;
    try {
      const nextPage = historyPage + 1;
      const history = await getAssistantHistory(
        nextPage,
        HISTORY_PAGE_SIZE,
        conversationId,
        true,
      );
      const chunk = (history.items || []).map((msg) => ({
        ...msg,
        actions: msg.actions || msg.metadata?.actions || [],
      }));
      const existingIds = new Set(messages.map((m) => m.message_id));
      const uniqueChunk = chunk.filter((m) => !existingIds.has(m.message_id));
      messages = [...uniqueChunk, ...messages];
      historyPage = nextPage;
      historyHasNext = Boolean(history.has_next);
      console.log("[AssistantChatPanel][history][loadOlderMessages:SUCCESS]");
    } catch (err) {
      console.error(
        "[AssistantChatPanel][history][loadOlderMessages:FAILED]",
        err,
      );
    } finally {
      loadingMoreHistory = false;
    }
  }
  // [/DEF:loadOlderMessages:Function]

  $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]
  /**
   * @PURPOSE: Add optimistic local user message before backend response.
   * @PRE: text is non-empty command text.
   * @POST: user message appears at the end of messages list.
   */
  function appendLocalUserMessage(text) {
    console.log("[AssistantChatPanel][message][appendLocalUserMessage][START]");
    messages = [
      ...messages,
      {
        message_id: `local-${Date.now()}`,
        role: "user",
        text,
        created_at: new Date().toISOString(),
      },
    ];
  }
  // [/DEF:appendLocalUserMessage:Function]

  // [DEF:appendAssistantResponse:Function]
  /**
   * @PURPOSE: Normalize and append assistant response payload to chat list.
   * @PRE: response follows assistant message response contract.
   * @POST: assistant message appended with state/task/actions metadata.
   */
  function appendAssistantResponse(response) {
    // prettier-ignore
    console.log("[AssistantChatPanel][message][appendAssistantResponse][START]");
    messages = [
      ...messages,
      {
        message_id: response.response_id,
        role: "assistant",
        text: response.text,
        state: response.state,
        task_id: response.task_id || null,
        confirmation_id: response.confirmation_id || null,
        actions: response.actions || [],
        created_at: response.created_at,
      },
    ];
  }
  // [/DEF:appendAssistantResponse:Function]

  function buildConversationTitle(conversation) {
    if (conversation?.title?.trim()) return conversation.title.trim();
    if (!conversation?.conversation_id) return $t.assistant?.conversation;
    return `${$t.assistant?.conversation} ${conversation.conversation_id.slice(0, 8)}`;
  }

  function setConversationFilter(filter) {
    if (filter !== "active" && filter !== "archived") return;
    if (conversationFilter === filter) return;
    conversationFilter = filter;
    conversations = [];
    conversationsPage = 1;
    conversationsHasNext = false;
    loadConversations(true);
  }

  function formatConversationTime(iso) {
    if (!iso) return "";
    const dt = new Date(iso);
    if (Number.isNaN(dt.getTime())) return "";
    return dt.toLocaleString();
  }

  // [DEF:handleSend:Function]
  /**
   * @PURPOSE: Submit user command to assistant orchestration API.
   * @PRE: input contains a non-empty command and current request is not loading.
   * @POST: assistant response is rendered and conversation id is persisted in store.
   * @SIDE_EFFECT: Triggers backend command execution pipeline.
   */
  async function handleSend() {
    console.log("[AssistantChatPanel][message][handleSend][START]");
    const text = input.trim();
    if (!text || loading) return;

    appendLocalUserMessage(text);
    input = "";
    loading = true;

    try {
      const response = await sendAssistantMessage({
        conversation_id: conversationId,
        message: text,
      });

      if (response.conversation_id) {
        setAssistantConversationId(response.conversation_id);
      }

      appendAssistantResponse(response);
      await loadConversations(true);
    } catch (err) {
      appendAssistantResponse({
        response_id: `error-${Date.now()}`,
        text: err.message || $t.assistant?.request_failed,
        state: "failed",
        created_at: new Date().toISOString(),
        actions: [],
      });
    } finally {
      loading = false;
    }
  }
  // [/DEF:handleSend:Function]

  // [DEF:selectConversation:Function]
  /**
   * @PURPOSE: Switch active chat context to selected conversation item.
   * @PRE: conversation carries valid conversation_id.
   * @POST: conversationId updated and history reloaded.
   */
  async function selectConversation(conversation) {
    // prettier-ignore
    console.log("[AssistantChatPanel][conversation][selectConversation][START]");
    if (!conversation?.conversation_id) return;
    if (conversation.conversation_id === conversationId) return;
    // Invalidate any in-flight history request to avoid stale conversation overwrite.
    historyLoadVersion += 1;
    setAssistantConversationId(conversation.conversation_id);
    messages = [];
    historyPage = 1;
    historyHasNext = false;
    await loadHistory(conversation.conversation_id);
  }
  // [/DEF:selectConversation:Function]

  // [DEF:startNewConversation:Function]
  /**
   * @PURPOSE: Create local empty chat context that will be persisted on first message.
   * @PRE: Panel is open.
   * @POST: Messages reset and new conversation id bound.
   */
  function startNewConversation() {
    // prettier-ignore
    console.log("[AssistantChatPanel][conversation][startNewConversation][START]");
    const newId =
      typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
        ? crypto.randomUUID()
        : `conv-${Date.now()}`;
    setAssistantConversationId(newId);
    messages = [];
    historyPage = 1;
    historyHasNext = false;
    initialized = true;
  }
  // [/DEF:startNewConversation:Function]

  // [DEF:handleAction:Function]
  /**
   * @PURPOSE: Execute assistant action button behavior (open task/reports, confirm, cancel).
   * @PRE: action object is produced by assistant response contract.
   * @POST: UI navigation or follow-up assistant response is appended.
   * @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints.
   */
  async function handleAction(action, message) {
    console.log("[AssistantChatPanel][action][handleAction][START]");
    try {
      if (action.type === "open_task" && action.target) {
        openDrawerForTask(action.target);
        return;
      }

      if (action.type === "open_reports") {
        goto("/reports");
        return;
      }

      if (action.type === "open_route" && action.target) {
        goto(action.target);
        return;
      }

      if (action.type === "open_diff" && action.target) {
        const dashboardId = Number(action.target);
        if (!Number.isFinite(dashboardId) || dashboardId <= 0) {
          throw new Error("Invalid dashboard id for diff");
        }
        const diffPayload = await gitService.getDiff(dashboardId);
        const diffText =
          typeof diffPayload === "string"
            ? diffPayload
            : diffPayload?.diff || JSON.stringify(diffPayload, null, 2);
        appendAssistantResponse({
          response_id: `diff-${Date.now()}`,
          text: diffText
            ? `Diff для дашборда ${dashboardId}:\n\n${diffText}`
            : `Diff для дашборда ${dashboardId} пуст.`,
          state: "success",
          created_at: new Date().toISOString(),
          actions: [],
        });
        return;
      }

      if (action.type === "confirm" && message.confirmation_id) {
        // Hide buttons immediately to prevent repeated clicks
        messages = messages.map((m) =>
          m.message_id === message.message_id ? { ...m, actions: [] } : m,
        );
        const response = await confirmAssistantOperation(
          message.confirmation_id,
        );
        appendAssistantResponse(response);
        return;
      }

      if (action.type === "cancel" && message.confirmation_id) {
        // Hide buttons immediately to prevent repeated clicks
        messages = messages.map((m) =>
          m.message_id === message.message_id ? { ...m, actions: [] } : m,
        );
        const response = await cancelAssistantOperation(
          message.confirmation_id,
        );
        appendAssistantResponse(response);
      }
    } catch (err) {
      appendAssistantResponse({
        response_id: `action-error-${Date.now()}`,
        text: err.message || $t.assistant?.action_failed,
        state: "failed",
        created_at: new Date().toISOString(),
        actions: [],
      });
    }
  }
  // [/DEF:handleAction:Function]

  // [DEF:handleKeydown:Function]
  /**
   * @PURPOSE: Submit command by Enter while preserving multiline input with Shift+Enter.
   * @PRE: Keyboard event received from chat input.
   * @POST: handleSend is invoked when Enter is pressed without shift modifier.
   */
  function handleKeydown(event) {
    console.log("[AssistantChatPanel][input][handleKeydown][START]");
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      handleSend();
    }
  }
  // [/DEF:handleKeydown:Function]

  // [DEF:stateClass:Function]
  /**
   * @PURPOSE: Map assistant state to visual badge style class.
   * @PRE: state is a nullable assistant state string.
   * @POST: Tailwind class string returned for badge rendering.
   */
  function stateClass(state) {
    console.log("[AssistantChatPanel][ui][stateClass][START]");
    if (state === "started") return "bg-sky-100 text-sky-700 border-sky-200";
    if (state === "success")
      return "bg-emerald-100 text-emerald-700 border-emerald-200";
    if (state === "needs_confirmation")
      return "bg-amber-100 text-amber-700 border-amber-200";
    if (state === "denied" || state === "failed")
      return "bg-rose-100 text-rose-700 border-rose-200";
    if (state === "needs_clarification")
      return "bg-violet-100 text-violet-700 border-violet-200";
    return "bg-slate-100 text-slate-700 border-slate-200";
  }
  // [/DEF:stateClass:Function]

  // [DEF:handleHistoryScroll:Function]
  /**
   * @PURPOSE: Trigger lazy history fetch when user scroll reaches top boundary.
   * @PRE: Scroll event emitted by history viewport container.
   * @POST: loadOlderMessages called when boundary and more pages available.
   */
  function handleHistoryScroll(event) {
    console.log("[AssistantChatPanel][scroll][handleHistoryScroll][START]");
    const el = event.currentTarget;
    if (!el || typeof el.scrollTop !== "number") return;
    if (el.scrollTop <= 16) {
      loadOlderMessages();
    }
  }
  // [/DEF:handleHistoryScroll:Function]

  onMount(() => {
    initialized = false;
  });

  async function loadLlmStatus() {
    try {
      const status = await api.getLlmStatus();
      llmReady = Boolean(status?.configured);
      llmStatusReason = status?.reason || "";
    } catch (_err) {
      llmReady = false;
      llmStatusReason = "status_unavailable";
    }
  }
</script>

{#if isOpen}
  <div
    class="fixed inset-0 z-[70] bg-slate-900/30"
    onclick={closeAssistantChat}
    aria-hidden="true"
  ></div>

  <aside
    class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl"
  >
    <div
      class="flex h-14 items-center justify-between border-b border-slate-200 px-4"
    >
      <div class="flex items-center gap-2 text-slate-800">
        <Icon name="clipboard" size={18} />
        <h2 class="text-sm font-semibold">
          {$t.assistant?.title}
        </h2>
      </div>
      <button
        class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
        onclick={closeAssistantChat}
        aria-label={$t.assistant?.close}
      >
        <Icon name="close" size={18} />
      </button>
    </div>

    <div class="flex h-[calc(100%-56px)] flex-col">
      {#if !llmReady}
        <div
          class="mx-3 mt-3 rounded-lg border border-rose-300 bg-rose-50 px-3 py-2 text-xs text-rose-800"
        >
          <div class="font-semibold">
            {$t.dashboard?.llm_not_configured || "LLM is not configured"}
          </div>
          <div class="mt-1 text-rose-700">
            {#if llmStatusReason === "no_active_provider"}
              {$t.dashboard?.llm_configure_provider ||
                "No active LLM provider. Configure it in Admin -> LLM Settings."}
            {:else if llmStatusReason === "invalid_api_key"}
              {$t.dashboard?.llm_configure_key ||
                "Invalid LLM API key. Update and save a real key in Admin -> LLM Settings."}
            {:else}
              {$t.dashboard?.llm_status_unavailable ||
                "LLM status is unavailable. Check settings and backend logs."}
            {/if}
          </div>
        </div>
      {/if}

      <div class="border-b border-slate-200 px-3 py-2">
        <div class="mb-2 flex items-center justify-between">
          <span
            class="text-xs font-semibold uppercase tracking-wide text-slate-500"
            >{$t.assistant?.conversations}</span
          >
          <button
            class="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100"
            onclick={startNewConversation}
          >
            {$t.assistant?.new}
          </button>
        </div>
        <div class="mb-2 flex items-center gap-1">
          <button
            class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
            'active'
              ? 'border-sky-300 bg-sky-50 text-sky-900'
              : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
            onclick={() => setConversationFilter("active")}
          >
            {$t.assistant?.active} ({activeConversationsTotal})
          </button>
          <button
            class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
            'archived'
              ? 'border-sky-300 bg-sky-50 text-sky-900'
              : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
            onclick={() => setConversationFilter("archived")}
          >
            {$t.assistant?.archived} ({archivedConversationsTotal})
          </button>
        </div>
        <div class="flex gap-2 overflow-x-auto pb-1">
          {#each conversations as convo (convo.conversation_id)}
            <div class="relative group min-w-[140px] max-w-[220px]">
              <button
                class="w-full rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id ===
                conversationId
                  ? 'border-sky-300 bg-sky-50 text-sky-900'
                  : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
                onclick={() => selectConversation(convo)}
                title={formatConversationTime(convo.updated_at)}
              >
                <div class="truncate font-semibold pr-4">
                  {buildConversationTitle(convo)}
                </div>
                <div class="truncate text-[10px] text-slate-500 pr-4">
                  {convo.last_message || ""}
                </div>
              </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"
                onclick={(e) => removeConversation(e, convo.conversation_id)}
                title="Удалить диалог"
              >
                <Icon name="trash" size={12} />
              </button>
            </div>
          {/each}
          {#if loadingConversations}
            <div
              class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs text-slate-500"
            >
              ...
            </div>
          {/if}
          {#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"
              onclick={() => loadConversations(false)}
            >
              {$t.assistant?.more}
            </button>
          {/if}
        </div>
      </div>

      <div
        class="flex-1 space-y-3 overflow-y-auto p-4"
        bind:this={historyViewport}
        onscroll={handleHistoryScroll}
      >
        {#if loadingMoreHistory}
          <div
            class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center text-xs text-slate-500"
          >
            {$t.assistant?.loading_older}
          </div>
        {/if}
        {#if loadingHistory}
          <div
            class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
          >
            {$t.assistant?.loading_history}
          </div>
        {:else if messages.length === 0}
          <div
            class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
          >
            {$t.assistant?.try_commands}
            <div class="mt-2 space-y-1 text-xs">
              <div>• {$t.assistant?.sample_command_branch}</div>
              <div>• {$t.assistant?.sample_command_migration}</div>
              <div>• {$t.assistant?.sample_command_status}</div>
            </div>
          </div>
        {/if}

        {#each messages as message (message.message_id)}
          <div class={message.role === "user" ? "ml-8" : "mr-8"}>
            <div
              class="rounded-xl border p-3 {message.role === 'user'
                ? 'border-sky-200 bg-sky-50'
                : 'border-slate-200 bg-white'}"
            >
              <div class="mb-1 flex items-center justify-between gap-2">
                <span
                  class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
                >
                  {message.role === "user"
                    ? $t.assistant?.you
                    : $t.assistant?.assistant}
                </span>
                {#if message.state}
                  <span
                    class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(
                      message.state,
                    )}"
                  >
                    {$t.assistant?.states?.[message.state] || message.state}
                  </span>
                {/if}
              </div>

              <div class="whitespace-pre-wrap text-sm text-slate-800">
                {message.text}
              </div>

              {#if message.task_id}
                <div class="mt-2 flex items-center gap-2">
                  <span
                    class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700"
                    >{$t.assistant?.task_id}: {message.task_id}</span
                  >
                  <button
                    class="text-xs font-medium text-sky-700 hover:text-sky-900"
                    onclick={() => openDrawerForTask(message.task_id)}
                  >
                    {$t.assistant?.open_task_drawer}
                  </button>
                </div>
              {/if}

              {#if message.actions?.length}
                <div class="mt-3 flex flex-wrap gap-2">
                  {#each message.actions as action}
                    <button
                      class="rounded-md border px-3 py-1.5 text-xs font-semibold transition
                        {action.type === 'confirm'
                        ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
                        : 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'}"
                      onclick={() => handleAction(action, message)}
                    >
                      {action.label}
                    </button>
                  {/each}
                </div>
              {/if}
            </div>
          </div>
        {/each}

        {#if loading}
          <div class="mr-8">
            <div class="rounded-xl border border-slate-200 bg-white p-3">
              <div class="mb-1 flex items-center justify-between gap-2">
                <span
                  class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
                >
                  {$t.assistant?.assistant}
                </span>
              </div>
              <div class="flex items-center gap-2 text-sm text-slate-700">
                <span>{$t.assistant?.thinking}</span>
                <span class="thinking-dots" aria-hidden="true">
                  <span></span><span></span><span></span>
                </span>
              </div>
            </div>
          </div>
        {/if}
      </div>

      <div class="border-t border-slate-200 p-3">
        <div class="flex items-end gap-2">
          <textarea
            bind:value={input}
            rows="2"
            placeholder={$t.assistant?.input_placeholder}
            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'}"
            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"
            onclick={handleSend}
            disabled={loading || !input.trim()}
          >
            {loading ? "..." : $t.assistant?.send}
          </button>
        </div>
      </div>
    </div>
  </aside>
{/if}

<!-- [/DEF:AssistantChatPanel:Component] -->

<style>
  .thinking-dots {
    display: inline-flex;
    gap: 0.25rem;
    align-items: center;
  }

  .thinking-dots span {
    width: 0.35rem;
    height: 0.35rem;
    border-radius: 9999px;
    background: #0ea5e9;
    opacity: 0.35;
    animation: assistant-thinking-bounce 0.9s infinite ease-in-out;
  }

  .thinking-dots span:nth-child(2) {
    animation-delay: 0.12s;
  }

  .thinking-dots span:nth-child(3) {
    animation-delay: 0.24s;
  }

  @keyframes assistant-thinking-bounce {
    0%,
    80%,
    100% {
      transform: translateY(0);
      opacity: 0.35;
    }
    40% {
      transform: translateY(-0.2rem);
      opacity: 1;
    }
  }
</style>
