<!-- #region AssistantChatPanel [C:3] [TYPE Component] [SEMANTICS assistant, chat, panel, conversation, confirmation] -->
<!-- @BRIEF Component component: lib/components/assistant/AssistantChatPanel.svelte -->
<!-- @LAYER UI -->
<script lang="ts">
  /**
   * @PURPOSE: Assistant chat workspace for mixed-initiative dataset review clarification, contextual actions, and task tracking.
   * @LAYER UI
   * @RELATION BINDS_TO -> assistantChat
   * @RELATION CALLS -> AssistantApi
   * @RELATION BINDS_TO -> DatasetReviewWorkspace
   * @RELATION DISPATCHES -> [EXT:frontend:taskDrawer]
   * @SEMANTICS: assistant-chat, confirmation, long-running-task, progress-tracking
   * @PRE: Assistant chat store, API adapters, and optional dataset review session context are initialized before the panel becomes interactive.
   * @POST: User and assistant message state, conversation history, and task affordances stay synchronized with the active conversation scope.
   * @SIDE_EFFECT: Reads and mutates assistant conversation state, may navigate to review routes, opens task drawer affordances, and emits toast feedback.
   * @DATA_CONTRACT: Input[{variant,className,assistantChatStore,datasetReviewSessionId?}] -> Output[{messages,conversations,conversationId,llmReady,panelVisible}]
   * @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 when variant is drawer.
   * @UX_STATE: Embedded -> Panel mounts inline as the primary workspace canvas.
   * @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.
   */

  import { onMount } from "svelte";
  import { goto } from "$app/navigation";
  import { t } from "$lib/i18n/index.svelte.js";
  import AssistantClarificationCard from "$lib/components/assistant/AssistantClarificationCard.svelte";
  import Icon from "$lib/ui/Icon.svelte";
  import { openDrawerForTask } from "$lib/stores/taskDrawer.svelte.js";
  import { normalizeDatasetReviewDetail } from "$lib/api/datasetReview.js";
  import { setSession as setDatasetReviewSession } from "$lib/stores/datasetReviewSession.svelte.js";
  import {
    assistantChatStore,
    closeAssistantChat,
    setAssistantConversationId,
    setAssistantSeedMessage,
    setAssistantFocusTarget,
  } from "$lib/stores/assistantChat.svelte.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";
  import { addToast } from "$lib/toasts.svelte.js";

  let { variant = "drawer", className = "" } = $props();

  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("");
  let llmStatusPayload = $state(null);
  let clarificationBannerDismissed = $state(false);

  let isOpen = $derived(assistantChatStore.value?.isOpen || false);
  let conversationId = $derived(assistantChatStore.value?.conversationId || null);
  let datasetReviewSessionId = $derived(
    assistantChatStore.value?.datasetReviewSessionId || null,
  );
  let seedMessage = $derived(assistantChatStore.value?.seedMessage || "");
  let focusTarget = $derived(assistantChatStore.value?.focusTarget || null);
  const focusTargetKey = $derived(String(focusTarget?.target || ""));
  const panelVisible = $derived(variant === "embedded" || isOpen);

  async function loadHistory(targetConversationId = conversationId) {
    if (loadingHistory || !panelVisible) 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;
      console.log("[EXT:frontend:AssistantChatPanel][history][loadHistory:SUCCESS] History loaded");
    } catch (err) {
      console.error(
        "[EXT:frontend:AssistantChatPanel][history][loadHistory:FAILED] Failed to load history",
        err,
      );
    } finally {
      loadingHistory = false;
    }
  }

  async function loadConversations(reset = false) {
    if (loadingConversations || !panelVisible) 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;
      console.log("[EXT:frontend:AssistantChatPanel][conversations][loadConversations:SUCCESS]");
    } catch (err) {
      console.error(
        "[EXT:frontend:AssistantChatPanel][conversations][loadConversations:FAILED]",
        err,
      );
    } finally {
      loadingConversations = false;
    }
  }

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

  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("[EXT:frontend:AssistantChatPanel][history][loadOlderMessages:SUCCESS]");
    } catch (err) {
      console.error(
        "[EXT:frontend:AssistantChatPanel][history][loadOlderMessages:FAILED]",
        err,
      );
    } finally {
      loadingMoreHistory = false;
    }
  }

  $effect(() => {
    if (panelVisible && !initialized) {
      void loadConversations(true);
      void loadHistory();
      void loadLlmStatus();
    }
  });

  $effect(() => {
    if (!panelVisible) {
      return;
    }
    // Seed message is now shown as a welcome card in the chat area,
    // not pre-filled into the textarea. This keeps the input clean
    // and makes the dialog feel more natural.
    // Users can click "Ask about this" to send the seed message.
  });

  $effect(() => {
    if (panelVisible && initialized && conversationId && !loading) {
      const currentFirstConversationId = messages.length
        ? messages[0].conversation_id
        : conversationId;
      if (currentFirstConversationId !== conversationId) {
        void loadHistory();
      }
    }
  });

  function appendLocalUserMessage(text, targetConversationId = conversationId) {
    console.log("[EXT:frontend:AssistantChatPanel][message][appendLocalUserMessage][START]");
    messages = [
      ...messages,
      {
        message_id: `local-${Date.now()}`,
        role: "user",
        text,
        created_at: new Date().toISOString(),
        conversation_id: targetConversationId || null,
      },
    ];
  }

  function appendAssistantResponse(response, targetConversationId = conversationId) {
    console.log("[EXT:frontend: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,
        conversation_id: response.conversation_id || targetConversationId || null,
      },
    ];
  }

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

  async function handleSend() {
    console.log("[EXT:frontend:AssistantChatPanel][message][handleSend][START]");
    const text = input.trim();
    if (!text || loading) return;
    const requestConversationId = conversationId;

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

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

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

      appendAssistantResponse(
        response,
        response.conversation_id || requestConversationId,
      );
      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;
      if (seedMessage === text) {
        setAssistantSeedMessage("");
      }
    }
  }

  async function selectConversation(conversation) {
    console.log("[EXT:frontend:AssistantChatPanel][conversation][selectConversation][START]");
    if (!conversation?.conversation_id) return;
    if (conversation.conversation_id === conversationId) return;
    historyLoadVersion += 1;
    setAssistantConversationId(conversation.conversation_id);
    messages = [];
    historyPage = 1;
    historyHasNext = false;
    await loadHistory(conversation.conversation_id);
  }

  function startNewConversation() {
    console.log("[EXT:frontend:AssistantChatPanel][conversation][startNewConversation][START]");
    const newId =
      typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
        ? crypto.randomUUID()
        : `conv-${Date.now()}`;
    historyLoadVersion += 1;
    setAssistantConversationId(newId);
    messages = [];
    historyPage = 1;
    historyHasNext = false;
    initialized = true;
  }

  async function handleAction(action, message) {
    console.log("[EXT:frontend: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 === "focus_target" && action.target) {
        setAssistantFocusTarget({ target: action.target, source: "assistant_action" });
        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) {
        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) {
        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: [],
      });
    }
  }

  function handleKeydown(event) {
    console.log("[EXT:frontend:AssistantChatPanel][input][handleKeydown][START]");
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      handleSend();
    }
  }

  function stateClass(state) {
    console.log("[EXT:frontend: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";
  }

  function handleHistoryScroll(event) {
    console.log("[EXT:frontend:AssistantChatPanel][scroll][handleHistoryScroll][START]");
    const el = event.currentTarget;
    if (!el || typeof el.scrollTop !== "number") return;
    if (el.scrollTop <= 16) {
      loadOlderMessages();
    }
  }

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

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

  function clearFocusTarget() {
    setAssistantFocusTarget(null);
  }

  function buildContextSummary() {
    const parts = [];
    if (datasetReviewSessionId) {
      parts.push($t.assistant?.session_scope_title || "Активный review-контекст");
    }
    if (focusTarget?.target) {
      const targetLabel = String(focusTarget.target)
        .replace(/^(field|mapping|finding|filter|clarification):/, "$1 ")
        .replace(/_/g, " ");
      parts.push(`${$t.assistant?.focus_target_label || "Фокус"}: ${targetLabel}`);
    }
    return parts.join(" • ");
  }

  const contextSummary = $derived(buildContextSummary());
  const clarificationMessage = $derived(
    messages.find((message) => message.state === "needs_clarification") || null,
  );
  const clarificationBannerVisible = $derived(
    Boolean(datasetReviewSessionId && clarificationMessage && !clarificationBannerDismissed),
  );

  function isConfirmationCard(message) {
    return message.role === "assistant" && message.state === "needs_confirmation";
  }

  function getMessageFocusTarget(message) {
    const actionFocus = message.actions?.find(
      (action) => action.type === "focus_target" && action.target,
    )?.target;
    if (actionFocus) {
      return actionFocus;
    }

    const text = String(message.text || "");
    const patterns = [/field:([\w-]+)/i, /mapping:([\w-]+)/i, /finding:([\w-]+)/i, /filter:([\w-]+)/i];
    for (const pattern of patterns) {
      const matched = text.match(pattern);
      if (matched?.[0]) {
        return matched[0].toLowerCase();
      }
    }

    return null;
  }

  function syncMessageFocus(message) {
    const target = getMessageFocusTarget(message);
    if (!target) {
      return;
    }
    setAssistantFocusTarget({ target, source: "assistant_message" });
  }

  function renderConfirmationScope(message) {
    const blockers = [];
    if (focusTarget?.target) {
      blockers.push(`${$t.assistant?.focus_target_label || "Focus"}: ${focusTarget.target}`);
    }
    if (message.intent?.remaining_blockers?.length) {
      blockers.push(...message.intent.remaining_blockers);
    }
    if (message.intent?.open_warning_refs?.length) {
      blockers.push(...message.intent.open_warning_refs);
    }
    return blockers;
  }

  function hasConfirmationScope(message) {
    return renderConfirmationScope(message).length > 0;
  }

  function dismissClarificationBanner() {
    clarificationBannerDismissed = true;
  }

  function resumeClarificationFromBanner() {
    clarificationBannerDismissed = false;
    if (clarificationMessage) {
      syncMessageFocus(clarificationMessage);
      input = clarificationMessage.text || input;
    }
  }

  async function refreshDatasetReviewSession() {
    if (!datasetReviewSessionId) {
      return;
    }

    try {
      const detail = await api.fetchApi(`/dataset-orchestration/sessions/${datasetReviewSessionId}`);
      setDatasetReviewSession(normalizeDatasetReviewDetail(detail));
    } catch (_error) {
      // Keep assistant clarification usable even if workspace refresh fails.
    }
  }

  function handleClarificationUpdated(result) {
    const nextQuestionId =
      result?.clarification_state?.current_question?.question_id ||
      result?.current_question?.question_id ||
      "";
    setAssistantFocusTarget({
      target: nextQuestionId ? `clarification:${nextQuestionId}` : "clarification",
      source: "assistant_clarification",
    });
    void refreshDatasetReviewSession();
  }

  $effect(() => {
    clarificationBannerDismissed = false;
  });
</script>

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

  <aside
    class={variant === "embedded"
      ? `flex min-h-[48rem] w-full min-w-0 flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm ${className}`
      : "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 ${variant === "embedded" ? "bg-slate-50/80" : ""}`}
    >
      <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>
      {#if variant === "drawer"}
        <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>
      {:else}
        <span class="rounded-full border border-violet-200 bg-violet-50 px-2.5 py-1 text-[11px] font-medium text-violet-700">
          {$t.assistant?.session_scope_title || "Active review context"}
        </span>
      {/if}
    </div>

    <div class={variant === "embedded" ? "flex min-h-0 flex-1 flex-col" : "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"}
              No active LLM provider is enabled for runtime. Activate one provider in Admin → LLM Settings or verify that this runtime points to the same database where providers were configured.
            {: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>
          {#if llmStatusPayload?.reason}
            <div class="mt-2 text-[11px] text-rose-700/90">
              Runtime status: {llmStatusPayload.reason}
            </div>
          {/if}
        </div>
      {/if}

      {#if contextSummary}
        <div class="mx-3 mt-3 rounded-xl border border-violet-200 bg-violet-50 px-3 py-3 text-xs text-violet-900">
          <div class="flex items-start justify-between gap-3">
            <div>
              <div class="font-semibold">{$t.assistant?.session_scope_title || "Active review context"}</div>
              <div class="mt-1 text-violet-700">{contextSummary}</div>
            </div>
            {#if focusTarget?.target}
              <button
                type="button"
                class="rounded-lg border border-violet-300 bg-white px-2 py-1 text-[11px] font-medium text-violet-700 transition hover:bg-violet-100"
                onclick={clearFocusTarget}
              >
                {$t.assistant?.clear_focus_action || "Clear focus"}
              </button>
            {/if}
          </div>
        </div>
      {/if}

      {#if clarificationBannerVisible}
        <div class="mx-3 mt-3 rounded-xl border border-violet-300 bg-violet-50 px-3 py-3 text-xs text-violet-900">
          <div class="flex items-start justify-between gap-3">
            <div>
              <div class="font-semibold">
                {$t.assistant?.clarification_banner_title || "Clarification queue is active"}
              </div>
              <div class="mt-1 text-violet-700">{clarificationMessage?.text}</div>
              {#if focusTargetKey}
                <div class="mt-2 text-[11px] text-violet-700">
                  {$t.assistant?.focus_target_label || "Focus"}: {focusTargetKey}
                </div>
              {/if}
            </div>
            <div class="flex shrink-0 gap-2">
              <button
                type="button"
                class="rounded-lg border border-violet-300 bg-white px-2 py-1 text-[11px] font-medium text-violet-700 transition hover:bg-violet-100"
                onclick={resumeClarificationFromBanner}
              >
                {$t.assistant?.clarification_resume_action || "Resume"}
              </button>
              <button
                type="button"
                class="rounded-lg border border-violet-300 bg-white px-2 py-1 text-[11px] font-medium text-violet-700 transition hover:bg-violet-100"
                onclick={dismissClarificationBanner}
              >
                {$t.assistant?.clarification_dismiss_action || "Dismiss"}
              </button>
            </div>
          </div>
        </div>
      {/if}

      <AssistantClarificationCard
        sessionId={datasetReviewSessionId || ""}
        visible={Boolean(datasetReviewSessionId)}
        disabled={loading}
        onupdated={handleClarificationUpdated}
        onfocus={(nextFocusTarget) => setAssistantFocusTarget(nextFocusTarget)}
      />

      <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="group relative 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 pr-4 font-semibold">{buildConversationTitle(convo)}</div>
                <div class="truncate pr-4 text-[10px] text-slate-500">{convo.last_message || ""}</div>
              </button>
              <button
                class="absolute right-1.5 top-1.5 hidden rounded bg-white/80 p-1 text-slate-400 hover:bg-destructive-light hover:text-destructive group-hover:block"
                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}
          <!-- Welcome card for dataset review context -->
          {#if datasetReviewSessionId && seedMessage}
            <div class="rounded-2xl border border-violet-200 bg-gradient-to-br from-violet-50 to-white p-5 shadow-sm">
              <div class="flex items-start gap-3">
                <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-violet-100 text-violet-600">
                  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>
                </div>
                <div class="min-w-0 flex-1">
                  <h3 class="text-sm font-semibold text-slate-900">{$t.assistant?.welcome_title || "Готов к работе"}</h3>
                  <p class="mt-1.5 text-sm leading-relaxed text-slate-600">{seedMessage}</p>
                  <div class="mt-4 flex flex-wrap gap-2">
                    <button
                      type="button"
                      class="inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3 py-2 text-xs font-medium text-white transition hover:bg-violet-700"
                      onclick={() => { input = seedMessage; handleSend(); }}
                    >
                      <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
                      {$t.assistant?.welcome_start_action || "Начать review"}
                    </button>
                    <button
                      type="button"
                      class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-medium text-slate-700 transition hover:bg-slate-50"
                      onclick={() => { input = ""; }}
                    >
                      {$t.assistant?.welcome_custom_action || "Задать свой вопрос"}
                    </button>
                  </div>
                </div>
              </div>
            </div>
          {:else}
            <div class="rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-4 text-sm text-slate-600">
              <div class="font-semibold text-slate-900">
                {$t.assistant?.try_commands || "Начните диалог с ассистентом."}
              </div>
              <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}
        {/if}

        {#each messages as message (message.message_id)}
          <div class={message.role === "user" ? "ml-8" : "mr-8"}>
            <div
              data-focus-target={getMessageFocusTarget(message) || undefined}
              class={`rounded-xl border p-3 ${message.role === "user"
                ? "border-sky-200 bg-sky-50"
                : isConfirmationCard(message)
                  ? "border-amber-300 bg-amber-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 getMessageFocusTarget(message)}
                <button
                  type="button"
                  class="mt-3 inline-flex items-center justify-center rounded-lg border border-violet-300 bg-violet-50 px-3 py-2 text-xs font-medium text-violet-700 transition hover:bg-violet-100"
                  onclick={() => syncMessageFocus(message)}
                >
                  {$t.assistant?.highlight_workspace_action || "Highlight in workspace"}
                </button>
              {/if}

              {#if isConfirmationCard(message)}
                <div class="mt-3 rounded-xl border border-amber-200 bg-white/90 p-3 text-xs text-slate-700">
                  <div class="font-semibold text-slate-900">
                    {$t.assistant?.confirmation_card_title || "Confirmation required"}
                  </div>
                  <div class="mt-1 text-slate-600">
                    {datasetReviewSessionId
                      ? `${$t.assistant?.session_scope_label || "Session"}: ${datasetReviewSessionId}`
                      : $t.assistant?.confirmation_scope_fallback || "This action affects the current assistant scope."}
                  </div>
                  {#if message.intent?.operation}
                    <div class="mt-2 rounded-lg bg-amber-50 px-2 py-2 text-slate-700">
                      <span class="font-medium">{$t.assistant?.confirmation_operation_label || "Operation"}:</span>
                      {$t.assistant?.operation_labels?.[message.intent.operation] || message.intent.operation}
                    </div>
                  {/if}
                  {#if hasConfirmationScope(message)}
                    <ul class="mt-2 space-y-1 rounded-lg bg-amber-50 px-2 py-2 text-slate-700">
                      {#each renderConfirmationScope(message) as item}
                        <li>• {item}</li>
                      {/each}
                    </ul>
                  {/if}
                </div>
              {/if}

              {#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}

<!-- #endregion AssistantChatPanel -->

<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>
