<script setup lang="ts">
import type {ChatTimeHistoryRecord, ChatTimeTimelineItem,} from '@easyflow/types';

import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {useRoute, useRouter} from 'vue-router';

import {
  ChatTimeHistoryMapper,
  ChatTimeTimelineBuilder,
  createChatVariantSwitchController,
  uuid,
} from '@easyflow/utils';

import {Delete, Plus, Promotion, Search,} from '@element-plus/icons-vue';
import {
  ElButton,
  ElEmpty,
  ElInput,
  ElMessage,
  ElMessageBox,
  ElScrollbar,
  ElTag,
} from 'element-plus';
import {tryit} from 'radash';

import {api, sseClient} from '#/api/request';
import ChatTimeMessageContent from '#/components/chat/ChatTimeMessageContent.vue';
import ChatContextCapsuleBar from '#/components/chat-workspace/ChatContextCapsuleBar.vue';
import ChatMessageActionBar from '#/components/chat-workspace/ChatMessageActionBar.vue';
import ChatWelcomeAssistantPicker from '#/components/chat-workspace/ChatWelcomeAssistantPicker.vue';

interface AssistantOption {
  label: string;
  raw: any;
  value: string;
}

interface KnowledgeOption {
  label: string;
  value: string;
}

interface KnowledgeView {
  alias?: string;
  id: string;
  title: string;
}

interface SessionItem {
  accessAt?: string;
  assistantCode?: string;
  assistantId: string;
  assistantName?: string;
  continuable?: boolean;
  lastMessageAt?: string;
  lastMessagePreview?: string;
  messageCount?: number;
  readOnlyReason?: null | string;
  sessionId: string;
  title?: string;
}

interface OpenSessionOptions {
  scrollToLatest?: boolean;
  silent?: boolean;
  syncRoute?: boolean;
}

interface WorkspaceConversationView {
  records?: ChatTimeHistoryRecord[];
  total?: number;
  variantsByRound?: Record<string, ChatTimeHistoryRecord[]>;
}

interface SseRoundMeta {
  regenerate?: boolean;
  regenerateRoundId?: string;
  roundId?: string;
  roundNo?: number;
  selectedVariantIndex?: number;
  switchable?: boolean;
  variantCount?: number;
  variantIndex?: number;
}

const SESSION_PAGE_SIZE = 30;
const MESSAGE_AUTO_SCROLL_THRESHOLD = 48;
const SIDEBAR_LOAD_MORE_THRESHOLD = 120;

const route = useRoute();
const router = useRouter();

const assistants = ref<AssistantOption[]>([]);
const knowledgeOptions = ref<KnowledgeOption[]>([]);
const knowledgeMap = ref(new Map<string, KnowledgeView>());
const sessions = ref<SessionItem[]>([]);
const loadingSessions = ref(false);
const loadingMoreSessions = ref(false);
const loadingMessages = ref(false);
const loadingAssistants = ref(false);
const loadingKnowledgeOptions = ref(false);
const sending = ref(false);
const sessionSearchKeyword = ref('');
const currentAssistantId = ref<string>();
const currentAssistantDetail = ref<any>();
const currentSession = ref<any>();
const messageList = ref<ChatTimeTimelineItem[]>([]);
const extraKnowledgeIds = ref<string[]>([]);
const boundKnowledges = ref<KnowledgeView[]>([]);
const senderValue = ref('');
const sessionPage = ref({
  pageNumber: 0,
  pageSize: SESSION_PAGE_SIZE,
  total: 0,
});
const openSessionRequestToken = ref(0);
const activeStreamSessionId = ref('');
const autoScrollEnabled = ref(true);
const optimisticSessionIds = ref<Set<string>>(new Set());
const variantSwitchStateVersion = ref(0);
const sessionContextMenuVisible = ref(false);
const sessionContextMenuTarget = ref<null | SessionItem>(null);
const sessionContextMenuX = ref(0);
const sessionContextMenuY = ref(0);

const variantSwitchController = createChatVariantSwitchController<
  ChatTimeHistoryRecord,
  ChatTimeTimelineItem
>({
  mapRecords: (records) => ChatTimeHistoryMapper.fromHistoryRecords(records),
  onError: () => ElMessage.error('答案版本切换失败'),
  onStateChange: () => {
    variantSwitchStateVersion.value += 1;
  },
  replaceRound: (items, roundId, nextItems) =>
    ChatTimeTimelineBuilder.replaceRoundMessages(items, roundId, nextItems),
});

const currentSessionId = computed(() => {
  const queryValue = route.query.sessionId;
  if (!queryValue) {
    return '';
  }
  return String(queryValue);
});

const hasMessages = computed(() => messageList.value.length > 0);
const currentDisplayAssistant = computed(
  () => currentSession.value?.assistant || currentAssistantDetail.value,
);
const composerAssistantName = computed(() => {
  if (currentAssistantDetail.value?.title) {
    return currentAssistantDetail.value.title;
  }
  return assistants.value.find((item) => item.value === currentAssistantId.value)?.label || '';
});
const activeAssistantName = computed(() => {
  if (currentDisplayAssistant.value?.title) {
    return currentDisplayAssistant.value.title;
  }
  if (currentDisplayAssistant.value?.name) {
    return currentDisplayAssistant.value.name;
  }
  if (currentSession.value?.assistantName) {
    return currentSession.value.assistantName;
  }
  return composerAssistantName.value;
});
const currentSessionTitle = computed(() => currentSession.value?.title || '新对话');
const isReadOnly = computed(() => currentSession.value?.continuable === false);
const isWelcomeComposerState = computed(() => {
  return !hasMessages.value && !currentSession.value?.sessionId;
});
const composerLocked = computed(() => isReadOnly.value || !currentAssistantId.value);
const contextSelectorDisabled = computed(() => isReadOnly.value || sending.value);
const canCreateSession = computed(() => !sending.value);
const canSend = computed(
  () =>
    !sending.value &&
    !composerLocked.value &&
    !!currentAssistantId.value &&
    !!senderValue.value.trim(),
);
const hasMoreSessionRecords = computed(
  () => sessions.value.length < (sessionPage.value.total || 0),
);
const canLoadMoreSessions = computed(
  () =>
    !loadingSessions.value &&
    !loadingMoreSessions.value &&
    hasMoreSessionRecords.value,
);
const selectedSessionKey = computed(() => {
  return String(currentSession.value?.sessionId || '');
});
const latestAssistantMessage = computed(() => {
  return [...messageList.value]
    .reverse()
    .find((item) => item.role === 'assistant');
});
const latestAssistantMessageId = computed(
  () => latestAssistantMessage.value?.id || '',
);
const displayedBoundKnowledges = computed(() => {
  if (!isReadOnly.value) {
    return boundKnowledges.value;
  }
  return (currentSession.value?.boundKnowledges || []).map((item: any) => ({
    alias: item.alias,
    id: String(item.id),
    title: item.title,
  }));
});
const displayedExtraKnowledgeIds = computed(() => {
  if (!isReadOnly.value) {
    return extraKnowledgeIds.value;
  }
  return (currentSession.value?.extraKnowledges || []).map((item: any) =>
    String(item.id),
  );
});
const displayedExtraKnowledges = computed(() => {
  if (isReadOnly.value) {
    return (currentSession.value?.extraKnowledges || []).map((item: any) => ({
      alias: item.alias,
      id: String(item.id),
      title: item.title,
    }));
  }
  return extraKnowledgeIds.value
    .map((id) => resolveKnowledgeView(id))
    .filter(Boolean) as KnowledgeView[];
});
const editableKnowledgeIds = computed(() => {
  return isReadOnly.value ? [] : extraKnowledgeIds.value;
});
const extraKnowledgeOptionList = computed(() => {
  const excluded = new Set(boundKnowledges.value.map((item) => item.id));
  for (const selectedId of extraKnowledgeIds.value) {
    excluded.add(String(selectedId));
  }
  return knowledgeOptions.value.filter((item) => !excluded.has(String(item.value)));
});
const filteredSessions = computed(() => {
  const keyword = sessionSearchKeyword.value.trim().toLowerCase();
  if (!keyword) {
    return sessions.value;
  }
  return sessions.value.filter((session) => {
    return [
      resolveSessionLabel(session),
      session.lastMessagePreview,
      session.assistantName,
    ].some((value) => String(value || '').toLowerCase().includes(keyword));
  });
});
const groupedSessions = computed(() => {
  const today: SessionItem[] = [];
  const recent: SessionItem[] = [];
  const earlier: SessionItem[] = [];
  const now = new Date();
  const startOfToday = new Date(
    now.getFullYear(),
    now.getMonth(),
    now.getDate(),
  ).getTime();
  const recentThreshold = now.getTime() - 30 * 24 * 60 * 60 * 1000;
  for (const session of filteredSessions.value) {
    const time = resolveSessionSortTime(session);
    if (!Number.isFinite(time)) {
      earlier.push(session);
      continue;
    }
    if (time >= startOfToday) {
      today.push(session);
      continue;
    }
    if (time >= recentThreshold) {
      recent.push(session);
      continue;
    }
    earlier.push(session);
  }
  return [
    { key: 'today', title: '今天', items: today },
    { key: 'recent', title: '最近一个月', items: recent },
    { key: 'earlier', title: '更早', items: earlier },
  ].filter((group) => group.items.length > 0);
});
const greetingPrefix = computed(() => {
  const hour = new Date().getHours();
  if (hour < 12) {
    return '上午好';
  }
  if (hour < 18) {
    return '下午好';
  }
  return '晚上好';
});
const emptyConversationTitle = computed(() => {
  if (activeAssistantName.value) {
    return `开始和 ${activeAssistantName.value} 对话`;
  }
  return '选择智能体后开始对话';
});
const emptyConversationText = computed(() => {
  if (activeAssistantName.value) {
    return '输入消息后开始第一轮协作。';
  }
  return '先在欢迎区选择一个可用智能体。';
});
const composerPlaceholder = computed(() => {
  if (isReadOnly.value) {
    return resolveReadOnlyText(currentSession.value?.readOnlyReason) || '当前会话只支持查看历史';
  }
  if (!currentAssistantId.value) {
    return '请先选择智能体';
  }
  return '发送消息';
});

onMounted(async () => {
  window.addEventListener('click', closeSessionContextMenu);
  window.addEventListener('resize', closeSessionContextMenu);
  window.addEventListener('scroll', closeSessionContextMenu, true);
  window.addEventListener('keydown', handleWindowKeydown);
  await Promise.all([fetchAssistants(), fetchKnowledgeOptions(), fetchSessions()]);
  if (currentSessionId.value) {
    await openSessionById(currentSessionId.value, {
      scrollToLatest: false,
      silent: true,
    });
  }
});

watch(
  () => route.query.sessionId,
  async (sessionId) => {
    const normalized = sessionId ? String(sessionId) : '';
    if (!normalized) {
      return;
    }
    if (selectedSessionKey.value === normalized) {
      return;
    }
    await openSessionById(normalized, {
      scrollToLatest: false,
      silent: true,
    });
  },
);

onBeforeUnmount(() => {
  window.removeEventListener('click', closeSessionContextMenu);
  window.removeEventListener('resize', closeSessionContextMenu);
  window.removeEventListener('scroll', closeSessionContextMenu, true);
  window.removeEventListener('keydown', handleWindowKeydown);
});

async function fetchAssistants() {
  loadingAssistants.value = true;
  const [, res] = await tryit(api.get)('/api/v1/bot/list', {
    params: {
      publishedOnly: true,
      status: 1,
    },
  });
  loadingAssistants.value = false;
  if (res?.errorCode === 0) {
    assistants.value = (res.data || []).map((item: any) => ({
      label: item.title,
      raw: item,
      value: String(item.id),
    }));
    await selectDefaultAssistantIfNeeded();
  }
}

async function selectDefaultAssistantIfNeeded() {
  if (
    currentSessionId.value ||
    currentSession.value?.sessionId ||
    currentAssistantId.value ||
    assistants.value.length === 0
  ) {
    return;
  }
  await handleAssistantChange(assistants.value[0]?.value);
}

async function fetchKnowledgeOptions() {
  loadingKnowledgeOptions.value = true;
  const [, res] = await tryit(api.get)('/api/v1/documentCollection/list', {
    params: {
      publishedOnly: true,
    },
  });
  loadingKnowledgeOptions.value = false;
  if (res?.errorCode === 0) {
    knowledgeOptions.value = (res.data || []).map((item: any) => ({
      label: item.title,
      value: String(item.id),
    }));
    knowledgeMap.value = new Map(
      (res.data || []).map((item: any) => [
        String(item.id),
        {
          alias: item.alias,
          id: String(item.id),
          title: item.title,
        } satisfies KnowledgeView,
      ]),
    );
    if (currentAssistantDetail.value && !isReadOnly.value) {
      boundKnowledges.value = resolveBoundKnowledgesFromBot(currentAssistantDetail.value);
      extraKnowledgeIds.value = extraKnowledgeIds.value.filter(
        (id) => !boundKnowledges.value.some((item) => item.id === id),
      );
    }
  }
}

async function fetchSessions(options: { append?: boolean } = {}) {
  const append = options.append === true;
  if (append) {
    if (!canLoadMoreSessions.value) {
      return;
    }
    loadingMoreSessions.value = true;
  } else {
    loadingSessions.value = true;
  }
  const nextPageNumber = append ? sessionPage.value.pageNumber + 1 : 1;
  const [, res] = await tryit(api.get)('/api/v1/chatWorkspace/sessions', {
    params: {
      pageNumber: nextPageNumber,
      pageSize: sessionPage.value.pageSize,
    },
  });
  if (append) {
    loadingMoreSessions.value = false;
  } else {
    loadingSessions.value = false;
  }
  if (res?.errorCode === 0) {
    const records = (res.data?.records || []) as SessionItem[];
    sessions.value = append
      ? mergeSessionRecords(sessions.value, records)
      : mergeFreshSessionRecords(records);
    sessionPage.value.pageNumber = Number(res.data?.pageNumber || nextPageNumber);
    sessionPage.value.total = Math.max(
      Number(res.data?.total || sessions.value.length),
      sessions.value.length,
    );
  }
}

function mergeFreshSessionRecords(records: SessionItem[]) {
  const freshIds = new Set(records.map((item) => String(item.sessionId)));
  const optimisticIds = new Set(optimisticSessionIds.value);
  for (const sessionId of optimisticIds) {
    if (freshIds.has(sessionId)) {
      optimisticIds.delete(sessionId);
    }
  }
  optimisticSessionIds.value = optimisticIds;
  const preserved = sessions.value.filter((item) => {
    const sessionId = String(item.sessionId);
    return optimisticIds.has(sessionId) && !freshIds.has(sessionId);
  });
  return sortSessionRecords([...records, ...preserved]);
}

function mergeSessionRecords(current: SessionItem[], incoming: SessionItem[]) {
  const merged = new Map<string, SessionItem>();
  for (const item of [...current, ...incoming]) {
    const sessionId = String(item.sessionId);
    if (!merged.has(sessionId)) {
      merged.set(sessionId, {
        ...item,
        sessionId,
      });
    }
  }
  return sortSessionRecords(Array.from(merged.values()));
}

function upsertSessionRecord(
  session: SessionItem,
  options: { incrementTotal?: boolean } = {},
) {
  const normalizedSession = {
    ...session,
    assistantId: String(session.assistantId || ''),
    sessionId: String(session.sessionId),
  } satisfies SessionItem;
  const next = [...sessions.value];
  const currentIndex = next.findIndex(
    (item) => String(item.sessionId) === normalizedSession.sessionId,
  );
  const existed = currentIndex >= 0;
  if (existed) {
    next.splice(currentIndex, 1);
  }
  next.push({
    ...(existed ? sessions.value[currentIndex] : {}),
    ...normalizedSession,
  });
  sessions.value = sortSessionRecords(next);
  if (!existed && options.incrementTotal) {
    sessionPage.value.total += 1;
  } else {
    sessionPage.value.total = Math.max(sessionPage.value.total, sessions.value.length);
  }
}

function resolveSessionSortTime(session: SessionItem) {
  const time = new Date(session.lastMessageAt || '').getTime();
  return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
}

function sortSessionRecords(records: SessionItem[]) {
  return [...records].sort((a, b) => {
    const timeDiff = resolveSessionSortTime(b) - resolveSessionSortTime(a);
    if (timeDiff !== 0) {
      return timeDiff;
    }
    return String(b.sessionId || '').localeCompare(String(a.sessionId || ''));
  });
}

function createOptimisticSessionRecord(sessionId: string, prompt: string) {
  const now = new Date().toISOString();
  const normalizedPrompt = prompt.trim();
  return {
    accessAt: now,
    assistantCode: currentSession.value?.assistantCode || currentAssistantDetail.value?.alias,
    assistantId: String(currentAssistantId.value || ''),
    assistantName: activeAssistantName.value || composerAssistantName.value,
    continuable: true,
    lastMessageAt: now,
    lastMessagePreview: normalizedPrompt,
    messageCount: Math.max(Number(currentSession.value?.messageCount || 0), 0) + 1,
    readOnlyReason: null,
    sessionId,
    title: currentSession.value?.title || '新对话',
  } satisfies SessionItem;
}

function syncCurrentSessionRecord(partial: Partial<SessionItem> & { sessionId: string }) {
  const currentId = String(partial.sessionId);
  const existing = sessions.value.find((item) => String(item.sessionId) === currentId);
  if (!existing) {
    upsertSessionRecord(
      {
        accessAt:
          partial.accessAt ||
          currentSession.value?.accessAt ||
          currentSession.value?.lastMessageAt ||
          new Date().toISOString(),
        assistantId: String(partial.assistantId || currentAssistantId.value || ''),
        assistantName: partial.assistantName || activeAssistantName.value,
        continuable: partial.continuable ?? currentSession.value?.continuable ?? true,
        lastMessageAt:
          partial.lastMessageAt ||
          currentSession.value?.lastMessageAt ||
          new Date().toISOString(),
        lastMessagePreview: partial.lastMessagePreview || '',
        messageCount: Number(partial.messageCount || currentSession.value?.messageCount || 0),
        readOnlyReason: partial.readOnlyReason ?? currentSession.value?.readOnlyReason,
        sessionId: currentId,
        title: partial.title || currentSession.value?.title || '新对话',
      } as SessionItem,
      { incrementTotal: true },
    );
    return;
  }
  upsertSessionRecord(
    {
      ...existing,
      ...partial,
      assistantId: String(partial.assistantId || existing.assistantId || ''),
      sessionId: currentId,
    },
  );
}

function removeSessionRecord(sessionId: string) {
  const currentId = String(sessionId);
  optimisticSessionIds.value = new Set(
    [...optimisticSessionIds.value].filter((item) => item !== currentId),
  );
  const next = sessions.value.filter((item) => String(item.sessionId) !== currentId);
  if (next.length === sessions.value.length) {
    return;
  }
  sessions.value = next;
  sessionPage.value.total = Math.max(sessionPage.value.total - 1, sessions.value.length);
}

function resetComposerState(options?: { resetAssistant?: boolean }) {
  abortActiveStream();
  autoScrollEnabled.value = true;
  if (options?.resetAssistant !== false) {
    currentAssistantId.value = undefined;
    currentAssistantDetail.value = undefined;
    boundKnowledges.value = [];
    extraKnowledgeIds.value = [];
  }
  currentSession.value = undefined;
  messageList.value = [];
  senderValue.value = '';
}

async function enterWelcomeState() {
  resetComposerState();
  await router.replace({ query: {} });
}

async function handleAssistantChange(assistantId?: string) {
  const normalized = assistantId ? String(assistantId) : undefined;
  if (!isReadOnly.value && normalized === currentAssistantId.value) {
    return;
  }
  if (!normalized) {
    await enterWelcomeState();
    return;
  }
  abortActiveStream();
  const assistant = await fetchAssistantDetail(normalized);
  if (!assistant?.id) {
    ElMessage.warning('聊天助手不存在或已下架');
    await enterWelcomeState();
    return;
  }
  currentAssistantId.value = normalized;
  const assistantName =
    assistant.title ||
    assistant.name ||
    assistants.value.find((item) => item.value === normalized)?.label;
  currentAssistantDetail.value = assistant;
  boundKnowledges.value = resolveBoundKnowledgesFromBot(assistant);
  extraKnowledgeIds.value = [];
  currentSession.value = undefined;
  messageList.value = [];
  senderValue.value = '';
  if (assistantName) {
    currentAssistantDetail.value = {
      ...assistant,
      title: assistantName,
    };
  }
  await router.replace({ query: {} });
}

async function fetchAssistantDetail(assistantId: string) {
  const [, res] = await tryit(api.get)('/api/v1/bot/getDetail', {
    params: {
      id: assistantId,
      publishedOnly: true,
    },
  });
  if (res?.errorCode !== 0 || !res?.data) {
    return null;
  }
  return res.data;
}

async function ensureConversationSession() {
  if (currentSession.value?.sessionId) {
    return String(currentSession.value.sessionId);
  }
  const [, res] = await tryit(api.get)('/api/v1/bot/generateConversationId');
  if (res?.errorCode !== 0 || !res.data) {
    throw new Error('暂时无法创建会话，请稍后再试');
  }
  const sessionId = String(res.data);
  currentSession.value = {
    assistant: currentAssistantDetail.value
      ? {
          alias: currentAssistantDetail.value.alias,
          description: currentAssistantDetail.value.description,
          icon: currentAssistantDetail.value.icon,
          id: currentAssistantId.value,
          title: composerAssistantName.value,
        }
      : undefined,
    assistantId: currentAssistantId.value,
    assistantName: composerAssistantName.value,
    boundKnowledges: [...boundKnowledges.value],
    continuable: true,
    extraKnowledges: displayedExtraKnowledges.value,
    readOnlyReason: null,
    sessionId,
    title: '新对话',
  };
  return sessionId;
}

async function handleCreateSession() {
  await enterWelcomeState();
}

async function syncSessionRoute(sessionId?: string) {
  const normalized = String(sessionId || '');
  if (!normalized || currentSessionId.value === normalized) {
    return;
  }
  await router.replace({
    query: {
      sessionId: normalized,
    },
  });
}

async function requestSessionDetail(sessionId: string) {
  const [, detailRes] = await tryit(api.get)(
    `/api/v1/chatWorkspace/sessions/${sessionId}`,
  );
  if (detailRes?.errorCode !== 0) {
    return null;
  }
  return detailRes.data;
}

async function requestConversation(sessionId: string) {
  const [, res] = await tryit(api.get)(
    `/api/v1/chatWorkspace/sessions/${sessionId}/conversation`,
  );
  if (res?.errorCode !== 0) {
    return null;
  }
  return (res.data || {}) as WorkspaceConversationView;
}

function applyConversation(
  conversation: WorkspaceConversationView | null,
  sessionId: string,
) {
  const records = Array.isArray(conversation?.records)
    ? conversation.records
    : [];
  messageList.value = ChatTimeHistoryMapper.fromHistoryRecords(records);
  seedVariantCache(sessionId, conversation?.variantsByRound || {});
}

function seedVariantCache(
  sessionId: string,
  variantsByRound: Record<string, ChatTimeHistoryRecord[]>,
) {
  for (const [roundId, variants] of Object.entries(variantsByRound || {})) {
    if (!roundId || !Array.isArray(variants)) {
      continue;
    }
    variantSwitchController.cacheVariants(sessionId, roundId, variants);
  }
}

function applySessionDetail(detail: any, options?: { resetSender?: boolean }) {
  currentSession.value = detail;
  if (options?.resetSender !== false) {
    senderValue.value = '';
  }
  if (detail.continuable !== false) {
    currentAssistantId.value = detail.assistant?.id
      ? String(detail.assistant.id)
      : undefined;
    currentAssistantDetail.value = detail.assistant;
    boundKnowledges.value = (detail.boundKnowledges || []).map((item: any) => ({
      alias: item.alias,
      id: String(item.id),
      title: item.title,
    }));
    extraKnowledgeIds.value = (detail.extraKnowledges || []).map((item: any) =>
      String(item.id),
    );
  } else {
    currentAssistantId.value = undefined;
    currentAssistantDetail.value = undefined;
    boundKnowledges.value = [];
    extraKnowledgeIds.value = [];
  }
}

async function openSession(item: SessionItem) {
  closeSessionContextMenu();
  await openSessionById(String(item.sessionId), {
    scrollToLatest: true,
    syncRoute: true,
  });
}

function closeSessionContextMenu() {
  sessionContextMenuVisible.value = false;
  sessionContextMenuTarget.value = null;
}

function openSessionContextMenu(event: MouseEvent, session: SessionItem) {
  event.preventDefault();
  sessionContextMenuTarget.value = session;
  sessionContextMenuX.value = Math.min(event.clientX, window.innerWidth - 188);
  sessionContextMenuY.value = Math.min(event.clientY, window.innerHeight - 116);
  sessionContextMenuVisible.value = true;
}

function openSessionContextMenuByKeyboard(
  event: KeyboardEvent,
  session: SessionItem,
) {
  const target = event.currentTarget as HTMLElement | null;
  if (!target) {
    return;
  }
  const rect = target.getBoundingClientRect();
  sessionContextMenuTarget.value = session;
  sessionContextMenuX.value = Math.min(rect.right - 12, window.innerWidth - 188);
  sessionContextMenuY.value = Math.min(rect.top + 12, window.innerHeight - 116);
  sessionContextMenuVisible.value = true;
}

function handleSessionItemKeydown(event: KeyboardEvent, session: SessionItem) {
  if (event.key === 'Enter') {
    event.preventDefault();
    openSession(session);
    return;
  }
  if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) {
    event.preventDefault();
    openSessionContextMenuByKeyboard(event, session);
  }
}

function handleWindowKeydown(event: KeyboardEvent) {
  if (event.key === 'Escape') {
    closeSessionContextMenu();
  }
}

async function openSessionById(
  sessionId: string,
  options: OpenSessionOptions = {},
) {
  abortActiveStream();
  const requestToken = ++openSessionRequestToken.value;
  loadingMessages.value = true;
  const detail = await requestSessionDetail(sessionId);
  if (requestToken !== openSessionRequestToken.value) {
    return false;
  }
  if (!detail) {
    loadingMessages.value = false;
    if (!options.silent) {
      ElMessage.warning('会话不存在或已删除');
    }
    if (currentSessionId.value === sessionId) {
      await router.replace({ query: {} });
    }
    return false;
  }
  applySessionDetail(detail);
  syncCurrentSessionRecord({
    accessAt: detail.accessAt,
    assistantCode: detail.assistantCode,
    assistantId: detail.assistantId ? String(detail.assistantId) : undefined,
    assistantName: detail.assistantName,
    continuable: detail.continuable,
    lastMessageAt: detail.lastMessageAt,
    lastMessagePreview: detail.lastMessagePreview,
    messageCount: detail.messageCount,
    readOnlyReason: detail.readOnlyReason,
    sessionId,
    title: detail.title || '新对话',
  });
  if ((detail.removedExtraKnowledgeNames || []).length > 0) {
    ElMessage.warning(
      `以下知识库已失效并移除：${detail.removedExtraKnowledgeNames.join('、')}`,
    );
  }
  const conversation = await requestConversation(sessionId);
  if (requestToken !== openSessionRequestToken.value) {
    return false;
  }
  if (!conversation) {
    loadingMessages.value = false;
    if (!options.silent) {
      ElMessage.warning('会话内容加载失败');
    }
    return false;
  }
  applyConversation(conversation, sessionId);
  loadingMessages.value = false;
  if (options.syncRoute) {
    await syncSessionRoute(sessionId);
  }
  if (options.scrollToLatest !== false) {
    scrollToBottom(true);
  }
  return true;
}

function getMessageScrollWrap() {
  return document.querySelector(
    '.chat-workspace__message-scroll .el-scrollbar__wrap',
  ) as HTMLElement | null;
}

function getSidebarScrollWrap() {
  return document.querySelector(
    '.chat-workspace__sidebar-scroll .el-scrollbar__wrap',
  ) as HTMLElement | null;
}

function isNearMessageBottom(element: HTMLElement) {
  const distanceToBottom =
    element.scrollHeight - (element.scrollTop + element.clientHeight);
  return distanceToBottom <= MESSAGE_AUTO_SCROLL_THRESHOLD;
}

function handleMessageScroll() {
  const element = getMessageScrollWrap();
  if (!element) {
    autoScrollEnabled.value = true;
    return;
  }
  autoScrollEnabled.value = isNearMessageBottom(element);
}

function handleSidebarScroll() {
  closeSessionContextMenu();
  const element = getSidebarScrollWrap();
  if (!element || !canLoadMoreSessions.value) {
    return;
  }
  const distanceToBottom =
    element.scrollHeight - (element.scrollTop + element.clientHeight);
  if (distanceToBottom <= SIDEBAR_LOAD_MORE_THRESHOLD) {
    fetchSessions({ append: true });
  }
}

function scrollToBottom(force = false) {
  requestAnimationFrame(() => {
    const element = getMessageScrollWrap();
    if (element) {
      if (!force && !autoScrollEnabled.value) {
        return;
      }
      element.scrollTop = element.scrollHeight;
      autoScrollEnabled.value = true;
    }
  });
}

function resolveKnowledgeView(knowledgeId: string) {
  const view = knowledgeMap.value.get(knowledgeId);
  if (view) {
    return view;
  }
  const option = knowledgeOptions.value.find(
    (item) => String(item.value) === knowledgeId,
  );
  if (!option) {
    return null;
  }
  return {
    id: knowledgeId,
    title: option.label,
  } satisfies KnowledgeView;
}

function resolveBoundKnowledgesFromBot(bot: any): KnowledgeView[] {
  const bindings = bot?.publishedSnapshotJson?.knowledgeBindings;
  if (!Array.isArray(bindings)) {
    return [];
  }
  const list: KnowledgeView[] = [];
  const seen = new Set<string>();
  for (const binding of bindings) {
    const knowledgeId = String(binding?.knowledgeId || '');
    if (!knowledgeId || seen.has(knowledgeId)) {
      continue;
    }
    const knowledge = resolveKnowledgeView(knowledgeId);
    if (!knowledge) {
      continue;
    }
    seen.add(knowledgeId);
    list.push(knowledge);
  }
  return list;
}

function resolveReadOnlyText(reason?: null | string) {
  if (reason === 'NO_PERMISSION') {
    return '当前会话对应的聊天助手已无权限，仅支持查看历史记录';
  }
  if (reason === 'ASSISTANT_OFFLINE') {
    return '当前会话对应的聊天助手已下架，仅支持查看历史记录';
  }
  if (reason === 'ASSISTANT_DELETED') {
    return '当前会话对应的聊天助手已删除，仅支持查看历史记录';
  }
  return '';
}

function resolveSessionTimeLabel(session: SessionItem) {
  return formatTime(session.lastMessageAt || session.accessAt);
}

function resolveSessionLabel(session: SessionItem) {
  return session.title || session.lastMessagePreview || '未命名会话';
}

function resolveSessionSubtitle(session: SessionItem) {
  return session.lastMessagePreview || session.assistantName || '暂无消息';
}

function formatTime(value?: string) {
  if (!value) {
    return '';
  }
  const time = new Date(value);
  if (Number.isNaN(time.getTime())) {
    return value;
  }
  return `${time.getMonth() + 1}-${time.getDate()} ${String(
    time.getHours(),
  ).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
}

function resolveLastUserPrompt() {
  const reversed = [...messageList.value].reverse();
  const item = reversed.find((entry) => entry.role === 'user');
  return item?.content?.trim() || '';
}

function resolveRoundUserPrompt(roundId?: string) {
  if (!roundId) {
    return resolveLastUserPrompt();
  }
  const reversed = [...messageList.value].reverse();
  const item = reversed.find(
    (entry) => entry.role === 'user' && entry.roundId === roundId,
  );
  return item?.content?.trim() || '';
}

function appendUserMessage(content: string) {
  const userMessageId = uuid();
  ChatTimeTimelineBuilder.appendUserMessage(messageList.value, {
    content,
    created: Date.now(),
    id: userMessageId,
  });
  return userMessageId;
}

function removeMessageById(messageId: string) {
  const index = messageList.value.findIndex((item) => item.id === messageId);
  if (index >= 0) {
    messageList.value.splice(index, 1);
  }
}

async function sendMessage(prompt?: string) {
  await sendChatRequest({
    prompt,
  });
}

async function sendChatRequest(options: {
  prompt?: string;
  regenerateRoundId?: string;
}) {
  const content = (options.prompt || senderValue.value).trim();
  if (!content || !currentAssistantId.value || composerLocked.value) {
    return;
  }
  const regenerateRoundId = options.regenerateRoundId
    ? String(options.regenerateRoundId)
    : '';
  const isRegenerate = !!regenerateRoundId;
  if (!isRegenerate && latestAssistantMessage.value?.roundId) {
    ChatTimeTimelineBuilder.setRoundSwitchable(
      messageList.value,
      latestAssistantMessage.value.roundId,
      false,
    );
  }
  const hadSessionBeforeSend = !!currentSession.value?.sessionId;
  let conversationId = '';
  try {
    conversationId = await ensureConversationSession();
  } catch (error: any) {
    ElMessage.error(error?.message || '暂时无法创建会话，请稍后再试');
    return;
  }
  if (!isRegenerate) {
    optimisticSessionIds.value = new Set([
      ...optimisticSessionIds.value,
      conversationId,
    ]);
    upsertSessionRecord(createOptimisticSessionRecord(conversationId, content), {
      incrementTotal: !hadSessionBeforeSend,
    });
    currentSession.value = {
      ...currentSession.value,
      accessAt: new Date().toISOString(),
      assistantId: currentAssistantId.value,
      assistantName: activeAssistantName.value || composerAssistantName.value,
      continuable: true,
      lastMessageAt: new Date().toISOString(),
      lastMessagePreview: content,
      messageCount: Math.max(Number(currentSession.value?.messageCount || 0), 0) + 1,
      readOnlyReason: null,
      sessionId: conversationId,
      title: currentSession.value?.title || '新对话',
    };
    await syncSessionRoute(conversationId);
  }
  sending.value = true;
  activeStreamSessionId.value = conversationId;
  const userMessageId = isRegenerate ? '' : appendUserMessage(content);
  let receivedAssistantPayload = false;
  let streamSettled = false;
  if (!options.prompt) {
    senderValue.value = '';
  }
  scrollToBottom(true);

  await sseClient.post(
    '/api/v1/bot/chat',
    {
      botId: currentAssistantId.value,
      conversationId,
      extraKnowledgeIds: extraKnowledgeIds.value,
      prompt: content,
      publishedOnly: true,
      regenerateRoundId: regenerateRoundId || undefined,
    },
    {
      onMessage(message) {
        if (activeStreamSessionId.value !== conversationId) {
          return;
        }
        if (message.event === 'done') {
          streamSettled = true;
          sending.value = false;
          ChatTimeTimelineBuilder.finalize(messageList.value);
          activeStreamSessionId.value = '';
          refreshAfterSend(conversationId);
          return;
        }
        if (!message.data) {
          return;
        }
        const sseData = JSON.parse(message.data);
        const streamMeta = normalizeSseRoundMeta(sseData?.meta);
        ChatTimeTimelineBuilder.bindLatestPendingUserMessage(
          messageList.value,
          streamMeta,
        );
        const delta = sseData.payload?.delta;

        if (
          sseData?.domain === 'SYSTEM' &&
          sseData.payload?.code === 'SYSTEM_ERROR'
        ) {
          if (!isRegenerate && userMessageId) {
            removeMessageById(userMessageId);
          }
          if (!options.prompt && !isRegenerate) {
            senderValue.value = content;
          }
          sending.value = false;
          if (isRegenerate && !receivedAssistantPayload) {
            ElMessage.error(sseData.payload.message || '重新生成失败');
          } else {
            ChatTimeTimelineBuilder.applySystemError(
              messageList.value,
              sseData.payload.message,
              Date.now(),
            );
          }
          return;
        }

        if (sseData?.domain === 'TOOL') {
          receivedAssistantPayload = true;
          if (sseData?.type === 'TOOL_CALL') {
            ChatTimeTimelineBuilder.upsertToolCall(messageList.value, {
              created: Date.now(),
              ...streamMeta,
              name: sseData?.payload?.name,
              toolCallId: sseData?.payload?.tool_call_id,
              value: sseData?.payload?.arguments,
            });
          } else {
            ChatTimeTimelineBuilder.upsertToolResult(messageList.value, {
              created: Date.now(),
              ...streamMeta,
              name: sseData?.payload?.name,
              result: sseData?.payload?.result,
              toolCallId: sseData?.payload?.tool_call_id,
            });
          }
          scrollToBottom();
          return;
        }

        if (sseData.type === 'THINKING') {
          receivedAssistantPayload = true;
          ChatTimeTimelineBuilder.appendThinkingDelta(
            messageList.value,
            delta,
            Date.now(),
            streamMeta,
          );
        } else if (sseData.type === 'MESSAGE') {
          receivedAssistantPayload = true;
          ChatTimeTimelineBuilder.appendMessageDelta(
            messageList.value,
            delta,
            Date.now(),
            streamMeta,
          );
        }
        scrollToBottom();
      },
      async onError(error) {
        if (activeStreamSessionId.value !== conversationId) {
          return;
        }
        console.error(error);
        if (!receivedAssistantPayload) {
          if (!isRegenerate && userMessageId) {
            removeMessageById(userMessageId);
          }
          if (!options.prompt && !isRegenerate) {
            senderValue.value = content;
          }
        }
        const errorMessage = String(error?.message || '').trim();
        if (errorMessage && errorMessage !== 'This operation was aborted') {
          ElMessage.error(errorMessage);
        }
        sending.value = false;
        ChatTimeTimelineBuilder.finalize(messageList.value);
        await refreshAfterSend(conversationId);
        activeStreamSessionId.value = '';
      },
      async onFinished() {
        if (activeStreamSessionId.value !== conversationId) {
          return;
        }
        sending.value = false;
        ChatTimeTimelineBuilder.finalize(messageList.value);
        if (streamSettled) {
          return;
        }
        await refreshAfterSend(conversationId);
        activeStreamSessionId.value = '';
      },
    },
  );
}

async function refreshAfterSend(sessionId?: string) {
  const currentId = String(sessionId || currentSession.value?.sessionId || '');
  if (!currentId) {
    return;
  }
  for (const delay of [0, 240, 480]) {
    if (delay > 0) {
      await sleep(delay);
    }
    const detail = await requestSessionDetail(currentId);
    if (!detail) {
      continue;
    }
    applySessionDetail(
      {
        ...detail,
        title: detail.title || currentSession.value?.title || '新对话',
      },
      { resetSender: false },
    );
    upsertSessionRecord(
      {
        accessAt: detail.accessAt,
        assistantCode: detail.assistantCode,
        assistantId: String(detail.assistantId || currentAssistantId.value || ''),
        assistantName: detail.assistantName,
        continuable: detail.continuable,
        lastMessageAt: detail.lastMessageAt,
        lastMessagePreview: detail.lastMessagePreview,
        messageCount: detail.messageCount,
        readOnlyReason: detail.readOnlyReason,
        sessionId: currentId,
        title: detail.title || currentSession.value?.title || '新对话',
      },
    );
    const conversation = await requestConversation(currentId);
    if (!conversation) {
      continue;
    }
    applyConversation(conversation, currentId);
    await syncSessionRoute(currentId);
    scrollToBottom();
    return;
  }
}

function sleep(delay: number) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

function stopSending() {
  abortActiveStream();
}

function abortActiveStream() {
  const shouldFinalize = sending.value || sseClient.isActive();
  activeStreamSessionId.value = '';
  sseClient.abort();
  sending.value = false;
  if (shouldFinalize) {
    ChatTimeTimelineBuilder.finalize(messageList.value);
  }
}

function canCopyMessage(item: ChatTimeTimelineItem) {
  if (item.role === 'tool') {
    return false;
  }
  if (item.role === 'assistant' && !isFinalAssistantInRound(item)) {
    return false;
  }
  return !!String(item.content || '').trim();
}

function canRegenerateMessage(item: ChatTimeTimelineItem) {
  if (item.role !== 'assistant') {
    return false;
  }
  if (isReadOnly.value || isWelcomeComposerState.value || sending.value) {
    return false;
  }
  if (!isFinalAssistantInRound(item)) {
    return false;
  }
  if (item.id !== latestAssistantMessageId.value) {
    return false;
  }
  const roundId = String(item.roundId || '');
  return !!roundId && !!resolveRoundUserPrompt(roundId);
}

async function handleRegenerateMessage() {
  if (sending.value || isReadOnly.value) {
    return;
  }
  const roundId = String(latestAssistantMessage.value?.roundId || '');
  const prompt = resolveRoundUserPrompt(roundId);
  if (!prompt) {
    ElMessage.warning('暂无可重新生成的问题');
    return;
  }
  if (!roundId) {
    ElMessage.warning('当前回答暂不支持重新生成');
    return;
  }
  await sendChatRequest({
    prompt,
    regenerateRoundId: roundId,
  });
}

function shouldShowVariantNavigator(item: ChatTimeTimelineItem) {
  return (
    item.role === 'assistant' &&
    isFinalAssistantInRound(item) &&
    Number(item.variantCount || 0) > 1 &&
    item.switchable === true &&
    !!item.roundId
  );
}

function canSwitchVariant(item: ChatTimeTimelineItem, direction: 'next' | 'previous') {
  if (item.role !== 'assistant') {
    return false;
  }
  if (!isFinalAssistantInRound(item)) {
    return false;
  }
  const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
  const total = Number(item.variantCount || 1);
  if (!item.switchable) {
    return false;
  }
  if (isVariantSwitching(item)) {
    return false;
  }
  if (direction === 'previous') {
    return current > 1;
  }
  return current < total;
}

function isVariantSwitching(item: ChatTimeTimelineItem) {
  variantSwitchStateVersion.value;
  return variantSwitchController.isSwitching(
    currentSession.value?.sessionId,
    item.roundId,
  );
}

function isFinalAssistantInRound(item: ChatTimeTimelineItem) {
  if (item.role !== 'assistant') {
    return false;
  }
  if (!item.roundId) {
    return true;
  }
  for (let index = messageList.value.length - 1; index >= 0; index -= 1) {
    const candidate = messageList.value[index];
    if (
      candidate?.role === 'assistant' &&
      candidate.roundId === item.roundId &&
      String(candidate.variantIndex || '') === String(item.variantIndex || '')
    ) {
      return candidate.id === item.id;
    }
  }
  return true;
}

async function selectVariantForMessage(
  item: ChatTimeTimelineItem,
  direction: 'next' | 'previous',
) {
  if (item.role !== 'assistant' || !item.roundId || !currentSession.value?.sessionId) {
    return;
  }
  const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
  const nextVariantIndex = direction === 'previous' ? current - 1 : current + 1;
  if (!canSwitchVariant(item, direction)) {
    return;
  }
  const sessionId = String(currentSession.value.sessionId);
  const roundId = String(item.roundId);
  const previousPreview = currentSession.value?.lastMessagePreview;
  const selected = await variantSwitchController.switchVariant({
    fetchVariants: async () => {
      const [, res] = await tryit(api.get)(
        `/api/v1/chatWorkspace/sessions/${sessionId}/rounds/${roundId}/variants`,
      );
      if (res?.errorCode !== 0 || !Array.isArray(res?.data)) {
        throw new Error('答案版本加载失败');
      }
      return res.data;
    },
    items: messageList.value,
    onLocalSwitch(record) {
      const preview = String(record.contentText || record.content || '').trim();
      if (currentSession.value) {
        currentSession.value.lastMessagePreview = preview;
      }
    },
    persistVariant: async () => {
      const [, res] = await tryit(api.post)(
        `/api/v1/chatWorkspace/sessions/${currentSession.value.sessionId}/rounds/${item.roundId}/selectVariant`,
        {
          variantIndex: nextVariantIndex,
        },
      );
      if (res?.errorCode !== 0 || !res?.data) {
        throw new Error(res?.message || '答案版本切换失败');
      }
      return res.data;
    },
    roundId,
    sessionId,
    targetVariantIndex: nextVariantIndex,
  });
  if (!selected && currentSession.value) {
    currentSession.value.lastMessagePreview = previousPreview;
  }
}

function normalizeSseRoundMeta(meta: any): SseRoundMeta {
  if (!meta || typeof meta !== 'object') {
    return {};
  }
  const variantIndex = meta.variantIndex ? Number(meta.variantIndex) : undefined;
  return {
    regenerate: Boolean(meta.regenerate),
    regenerateRoundId: meta.regenerateRoundId
      ? String(meta.regenerateRoundId)
      : undefined,
    roundId: meta.roundId ? String(meta.roundId) : undefined,
    roundNo: meta.roundNo ? Number(meta.roundNo) : undefined,
    selectedVariantIndex: meta.selectedVariantIndex
      ? Number(meta.selectedVariantIndex)
      : variantIndex,
    switchable:
      typeof meta.switchable === 'boolean'
        ? meta.switchable
        : variantIndex != null
          ? true
          : undefined,
    variantCount: meta.variantCount ? Number(meta.variantCount) : variantIndex,
    variantIndex,
  };
}

function handleExtraKnowledgeIdsChange(value: string[]) {
  extraKnowledgeIds.value = value.map((item) => String(item));
}

function handleEnter(event: Event | KeyboardEvent) {
  if (event instanceof KeyboardEvent && event.shiftKey) {
    return;
  }
  event.preventDefault();
  sendMessage();
}

async function deleteSession(targetSession?: SessionItem) {
  const session = targetSession || sessionContextMenuTarget.value || currentSession.value;
  if (!session?.sessionId) {
    return;
  }
  closeSessionContextMenu();
  const [, confirmRes] = await tryit(ElMessageBox.confirm)(
    '删除后该会话将不再出现在历史列表中，是否继续？',
    '删除会话',
    {
      cancelButtonText: '取消',
      confirmButtonText: '删除',
      type: 'warning',
    },
  );
  if (!confirmRes) {
    return;
  }
  const [, res] = await tryit(api.post)(
    `/api/v1/chatWorkspace/sessions/${session.sessionId}/delete`,
    {},
  );
  if (res?.errorCode === 0) {
    ElMessage.success('删除成功');
    removeSessionRecord(String(session.sessionId));
    if (String(currentSession.value?.sessionId || '') === String(session.sessionId)) {
      resetComposerState();
      await router.replace({ query: {} });
    }
  }
}
</script>

<template>
  <div class="chat-workspace">
    <aside class="chat-workspace__sidebar">
      <div class="chat-workspace__sidebar-header">
        <ElButton
          class="chat-workspace__new-session"
          :icon="Plus"
          :disabled="!canCreateSession"
          @click="handleCreateSession"
        >
          新建会话
        </ElButton>

        <ElInput
          v-model="sessionSearchKeyword"
          class="chat-workspace__session-search"
          clearable
          :prefix-icon="Search"
          placeholder="搜索会话"
        />
      </div>

      <ElScrollbar
        class="chat-workspace__sidebar-scroll"
        @scroll="handleSidebarScroll"
      >
        <div class="chat-workspace__sidebar-body">
          <div class="chat-workspace__session-list-head">
            <span class="chat-workspace__panel-caption">会话</span>
          </div>

          <div
            v-if="groupedSessions.length > 0"
            class="chat-workspace__session-groups"
          >
            <section
              v-for="group in groupedSessions"
              :key="group.key"
              class="chat-workspace__session-group"
            >
              <h2 class="chat-workspace__group-title">{{ group.title }}</h2>
              <div
                v-for="session in group.items"
                :key="session.sessionId"
                role="button"
                tabindex="0"
                class="chat-workspace__session-item"
                :class="{
                  'is-active': selectedSessionKey === String(session.sessionId),
                  'is-context-open':
                    sessionContextMenuVisible &&
                    sessionContextMenuTarget?.sessionId === session.sessionId,
                  'is-readonly': session.continuable === false,
                }"
                @click="openSession(session)"
                @contextmenu="openSessionContextMenu($event, session)"
                @keydown="handleSessionItemKeydown($event, session)"
              >
                <div class="chat-workspace__session-main">
                  <div class="chat-workspace__session-row">
                    <span class="chat-workspace__session-name">
                      {{ resolveSessionLabel(session) }}
                    </span>
                    <span class="chat-workspace__session-time">
                      {{ resolveSessionTimeLabel(session) }}
                    </span>
                  </div>
                  <div class="chat-workspace__session-row is-sub">
                    <span class="chat-workspace__session-subtitle">
                      {{ resolveSessionSubtitle(session) }}
                    </span>
                    <ElTag
                      v-if="session.continuable === false"
                      size="small"
                      effect="plain"
                      type="info"
                    >
                      只读
                    </ElTag>
                  </div>
                </div>
              </div>
            </section>

            <div
              v-if="loadingMoreSessions || canLoadMoreSessions"
              class="chat-workspace__session-loadmore"
            >
              <span v-if="loadingMoreSessions">加载更多会话中...</span>
              <span v-else>向下滚动加载更多</span>
            </div>
          </div>

          <ElEmpty
            v-else-if="loadingSessions"
            description="会话加载中"
            class="chat-workspace__empty"
          />
          <ElEmpty
            v-else-if="sessionSearchKeyword"
            description="未找到匹配会话"
            class="chat-workspace__empty"
          />
          <ElEmpty
            v-else
            description="当前用户暂无会话"
            class="chat-workspace__empty"
          />
        </div>
      </ElScrollbar>

      <div
        v-if="sessionContextMenuVisible && sessionContextMenuTarget"
        class="chat-workspace__context-menu"
        :style="{
          left: `${sessionContextMenuX}px`,
          top: `${sessionContextMenuY}px`,
        }"
        @click.stop
      >
        <button
          type="button"
          class="chat-workspace__context-menu-item is-danger"
          @click="deleteSession(sessionContextMenuTarget)"
        >
          <Delete class="chat-workspace__context-menu-icon" />
          <span>删除</span>
        </button>
      </div>
    </aside>

    <main class="chat-workspace__main">
      <header class="chat-workspace__topbar">
        <div class="chat-workspace__topbar-content">
          <div class="chat-workspace__topbar-main">
            <h1 class="chat-workspace__topbar-title">
              {{ currentSessionTitle }}
            </h1>
            <div class="chat-workspace__topbar-meta">
              <span>{{ activeAssistantName || '未选择智能体' }}</span>
              <ElTag
                v-if="isReadOnly"
                type="info"
                effect="plain"
                size="small"
              >
                只读
              </ElTag>
            </div>
            <p
              v-if="isReadOnly"
              class="chat-workspace__readonly-tip"
            >
              {{ resolveReadOnlyText(currentSession?.readOnlyReason) }}
            </p>
          </div>
        </div>
      </header>

      <section
        class="chat-workspace__conversation"
        :class="{ 'is-welcome-layout': isWelcomeComposerState }"
      >
        <div v-if="isWelcomeComposerState" class="chat-workspace__welcome-stage">
          <div class="chat-workspace__conversation-content">
            <div class="chat-workspace__welcome is-centered">
              <div class="chat-workspace__welcome-title chat-workspace__welcome-title--inline">
                <span>{{ greetingPrefix }}，和</span>
                <ChatWelcomeAssistantPicker
                  :assistant-id="currentAssistantId"
                  :assistant-name="activeAssistantName || composerAssistantName"
                  :assistant-options="assistants"
                  :disabled="sending"
                  :loading="loadingAssistants"
                  @update:assistant-id="handleAssistantChange"
                />
                <span>一起协作</span>
              </div>
            </div>
          </div>
        </div>

        <ElScrollbar
          v-else
          class="chat-workspace__message-scroll"
          @scroll="handleMessageScroll"
        >
          <div class="chat-workspace__message-shell">
            <div class="chat-workspace__conversation-content">
              <div
                v-if="hasMessages"
                class="chat-workspace__message-list"
              >
                <article
                  v-for="item in messageList"
                  :key="item.id"
                  class="chat-workspace__message"
                  :class="`is-${item.role}`"
                >
                  <div class="chat-workspace__message-stack" :class="`is-${item.role}`">
                    <div class="chat-workspace__message-bubble" :class="`is-${item.role}`">
                      <ChatTimeMessageContent :item="item" />
                    </div>
                    <ChatMessageActionBar
                      :align="item.role === 'user' ? 'end' : 'start'"
                      :allow-copy="canCopyMessage(item)"
                      :allow-regenerate="canRegenerateMessage(item)"
                      :copy-text="String(item.content || '').trim()"
                      :disabled-variant-next="!canSwitchVariant(item, 'next')"
                      :disabled-variant-previous="!canSwitchVariant(item, 'previous')"
                      :regenerate-disabled="sending || isReadOnly"
                      :show-variant-navigator="shouldShowVariantNavigator(item)"
                      :variant-loading="false"
                      :variant-current="
                        Number(item.variantIndex || item.selectedVariantIndex || 1)
                      "
                      :variant-total="Number(item.variantCount || 1)"
                      @regenerate="handleRegenerateMessage"
                      @select-next-variant="selectVariantForMessage(item, 'next')"
                      @select-previous-variant="
                        selectVariantForMessage(item, 'previous')
                      "
                    />
                  </div>
                </article>
              </div>

              <div v-else class="chat-workspace__welcome">
                <div class="chat-workspace__welcome-badge">聊天</div>
                <h2 class="chat-workspace__welcome-title">
                  {{ emptyConversationTitle }}
                </h2>
                <p class="chat-workspace__welcome-text">
                  {{ emptyConversationText }}
                </p>
              </div>
            </div>
          </div>
        </ElScrollbar>

        <footer
          class="chat-workspace__composer-shell"
          :class="{ 'is-centered': isWelcomeComposerState }"
        >
          <div class="chat-workspace__conversation-content">
            <div
              class="chat-workspace__composer"
              :class="{ 'is-disabled': composerLocked }"
            >
              <ElInput
                v-model="senderValue"
                class="chat-workspace__textarea"
                :autosize="{ minRows: 1, maxRows: 6 }"
                :disabled="composerLocked"
                resize="none"
                type="textarea"
                :placeholder="composerPlaceholder"
                @keydown.enter="handleEnter"
              />

              <div class="chat-workspace__composer-footer">
                <div class="chat-workspace__composer-actions">
                  <ElButton
                    v-if="sending"
                    aria-label="停止"
                    class="chat-workspace__send-button is-stop"
                    @click="stopSending"
                  >
                    <span class="chat-workspace__stop-glyph" />
                  </ElButton>
                  <ElButton
                    v-else
                    :icon="Promotion"
                    aria-label="发送"
                    class="chat-workspace__send-button"
                    :disabled="!canSend"
                    @click="sendMessage()"
                  />
                </div>
              </div>
            </div>

            <ChatContextCapsuleBar
              class="chat-workspace__context-rail-shell"
              :bound-knowledges="displayedBoundKnowledges"
              :selected-extra-knowledges="displayedExtraKnowledges"
              :extra-knowledge-ids="
                isReadOnly ? displayedExtraKnowledgeIds : editableKnowledgeIds
              "
              :extra-knowledge-options="extraKnowledgeOptionList"
              :disabled="contextSelectorDisabled"
              :knowledge-disabled="!currentAssistantId || composerLocked"
              :loading="loadingKnowledgeOptions"
              :mode="isReadOnly ? 'readonly' : 'editable'"
              @update:extra-knowledge-ids="handleExtraKnowledgeIdsChange"
            />

            <div
              v-if="isReadOnly"
              class="chat-workspace__composer-disabled"
            >
              {{ resolveReadOnlyText(currentSession?.readOnlyReason) }}
            </div>
          </div>
        </footer>
      </section>
    </main>
  </div>
</template>

<style scoped>
.chat-workspace {
  --chat-content-width: 840px;
  --chat-sidebar-width: 272px;
  display: grid;
  grid-template-columns: var(--chat-sidebar-width) minmax(0, 1fr);
  gap: 14px;
  height: var(--easyflow-content-height, 100%);
  max-height: var(--easyflow-content-height, 100%);
  min-height: 0;
  padding: 16px;
  overflow: hidden;
  box-sizing: border-box;
  background:
    radial-gradient(circle at top left, hsl(var(--nav-ambient) / 0.08), transparent 20%),
    linear-gradient(180deg, hsl(var(--background)) 0%, hsl(var(--background-deep)) 100%);
}

.chat-workspace__sidebar,
.chat-workspace__main {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  border: 1px solid hsl(var(--divider-faint) / 0.7);
  border-radius: 18px;
  background: hsl(var(--surface-panel) / 0.94);
  box-shadow: 0 10px 28px -26px hsl(var(--foreground) / 0.18);
}

.chat-workspace__sidebar-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 14px 10px;
  border-bottom: 1px solid hsl(var(--divider-faint) / 0.64);
}

.chat-workspace__new-session {
  justify-content: flex-start;
  width: 100%;
  height: 36px;
  padding-inline: 12px;
  font-size: 13px;
  font-weight: 600;
  color: hsl(var(--text-strong));
  border-color: hsl(var(--divider-faint) / 0.86);
  border-radius: 12px;
  background: hsl(var(--surface-subtle));
  box-shadow: none;
}

.chat-workspace__new-session:hover:not(.is-disabled) {
  background: hsl(var(--surface-panel));
}

.chat-workspace__new-session:focus-visible,
.chat-workspace__session-item:focus-visible,
.chat-workspace__session-more:focus-visible {
  outline: 2px solid hsl(var(--primary) / 0.32);
  outline-offset: 2px;
}

.chat-workspace__session-search :deep(.el-input__wrapper) {
  min-height: 36px;
  padding-inline: 12px;
  border-radius: 12px;
  border: 1px solid transparent;
  box-shadow: none;
  background: hsl(var(--surface-subtle));
}

.chat-workspace__session-search :deep(.el-input__wrapper.is-focus) {
  border-color: hsl(var(--primary) / 0.18);
}

.chat-workspace__sidebar-scroll,
.chat-workspace__message-scroll {
  flex: 1;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

.chat-workspace__sidebar-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 10px 10px 14px;
}

.chat-workspace__panel-caption {
  font-size: 11px;
  font-weight: 600;
  color: hsl(var(--text-muted));
  letter-spacing: 0.02em;
}

.chat-workspace__session-list-head {
  padding-inline: 4px;
}

.chat-workspace__session-groups {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chat-workspace__session-loadmore {
  display: flex;
  justify-content: center;
  padding: 8px 4px 2px;
  font-size: 11px;
  color: hsl(var(--text-muted));
}

.chat-workspace__session-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.chat-workspace__group-title {
  margin: 0;
  padding: 0 4px;
  font-size: 11px;
  font-weight: 600;
  color: hsl(var(--text-muted));
  letter-spacing: 0.02em;
}

.chat-workspace__session-item {
  position: relative;
  display: flex;
  gap: 8px;
  align-items: flex-start;
  justify-content: space-between;
  padding: 9px 10px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: transparent;
  transition:
    border-color 0.18s ease,
    background-color 0.18s ease;
}

.chat-workspace__session-item:hover {
  background: hsl(var(--surface-subtle));
}

.chat-workspace__session-item.is-active {
  border-color: hsl(var(--divider-faint) / 0.92);
  background: hsl(var(--surface-subtle) / 0.96);
}

.chat-workspace__session-item.is-context-open {
  background: hsl(var(--surface-subtle));
}

.chat-workspace__session-item.is-readonly {
  color: hsl(var(--text-muted));
}

.chat-workspace__session-main {
  flex: 1;
  min-width: 0;
}

.chat-workspace__session-row {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  min-width: 0;
}

.chat-workspace__session-row.is-sub {
  margin-top: 2px;
}

.chat-workspace__session-name,
.chat-workspace__session-subtitle {
  overflow: hidden;
  min-width: 0;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-workspace__session-name {
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--text-strong));
}

.chat-workspace__session-subtitle,
.chat-workspace__session-time {
  font-size: 11px;
  color: hsl(var(--text-muted));
}

.chat-workspace__context-menu {
  position: fixed;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 144px;
  padding: 6px;
  border: 1px solid hsl(var(--divider-faint) / 0.84);
  border-radius: 14px;
  background: hsl(var(--surface-panel) / 0.98);
  box-shadow: 0 16px 36px -24px hsl(var(--foreground) / 0.28);
  backdrop-filter: blur(12px);
}

.chat-workspace__context-menu-item {
  display: flex;
  gap: 8px;
  align-items: center;
  width: 100%;
  min-height: 32px;
  padding: 0 10px;
  font-size: 12px;
  font-weight: 500;
  color: hsl(var(--text-strong));
  text-align: left;
  border: none;
  border-radius: 10px;
  background: transparent;
  transition: background-color 0.18s ease;
}

.chat-workspace__context-menu-item:hover {
  background: hsl(var(--surface-subtle));
}

.chat-workspace__context-menu-item.is-danger {
  color: hsl(var(--destructive));
}

.chat-workspace__context-menu-item.is-danger:hover {
  background: hsl(var(--destructive) / 0.08);
}

.chat-workspace__context-menu-icon {
  flex: none;
  width: 14px;
  height: 14px;
}

.chat-workspace__empty {
  padding-top: 40px;
}

.chat-workspace__topbar {
  flex: none;
  padding: 14px 24px 10px;
  border-bottom: 1px solid hsl(var(--divider-faint) / 0.64);
}

.chat-workspace__topbar-content {
  max-width: var(--chat-content-width);
  margin: 0 auto;
}

.chat-workspace__topbar-main {
  min-width: 0;
}

.chat-workspace__topbar-title {
  margin: 0;
  font-size: 17px;
  font-weight: 650;
  color: hsl(var(--text-strong));
  letter-spacing: -0.01em;
}

.chat-workspace__topbar-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-top: 4px;
  font-size: 12px;
  color: hsl(var(--text-muted));
}

.chat-workspace__readonly-tip {
  margin: 6px 0 0;
  font-size: 11px;
  color: hsl(var(--text-muted));
}

.chat-workspace__conversation {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.chat-workspace__conversation.is-welcome-layout {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 32px 24px;
}

.chat-workspace__welcome-stage {
  flex: none;
}

.chat-workspace__message-shell {
  min-height: 100%;
  padding: 0 24px 196px;
}

.chat-workspace__conversation-content {
  width: 100%;
  max-width: var(--chat-content-width);
  margin: 0 auto;
}

.chat-workspace__more {
  display: flex;
  justify-content: center;
  padding-top: 20px;
}

.chat-workspace__message-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px 0 36px;
}

.chat-workspace__message {
  display: flex;
  width: 100%;
}

.chat-workspace__message.is-user {
  justify-content: flex-end;
}

.chat-workspace__message-stack {
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-width: 100%;
}

.chat-workspace__message-stack.is-user {
  align-items: flex-end;
  width: 100%;
}

.chat-workspace__message-bubble {
  min-width: 0;
}

.chat-workspace__message-bubble.is-assistant {
  width: 100%;
  max-width: 100%;
  padding: 0;
  color: hsl(var(--text-strong));
  border: none;
  border-radius: 0;
  background: transparent;
}

.chat-workspace__message-bubble.is-user {
  display: block;
  align-self: flex-end;
  width: fit-content;
  max-width: min(72%, 720px);
  min-width: 0;
  padding: 0;
  text-align: left;
  color: hsl(var(--text-strong));
  white-space: pre-wrap;
  border: none;
  border-radius: 0;
  background: transparent;
  overflow-wrap: anywhere;
}

.chat-workspace__message-bubble.is-tool {
  width: 100%;
  max-width: min(100%, 720px);
  padding: 0;
  border: none;
  border-radius: 0;
  background: transparent;
}

.chat-workspace__message-bubble.is-user :deep(.chat-time-markdown),
.chat-workspace__message-bubble.is-user :deep(.elx-xmarkdown-container),
.chat-workspace__message-bubble.is-user :deep(.elx-xmarkdown-provider) {
  width: auto;
  max-width: 100%;
}

.chat-workspace__message-bubble.is-assistant :deep(pre),
.chat-workspace__message-bubble.is-user :deep(pre),
.chat-workspace__message-bubble.is-tool :deep(pre) {
  overflow: auto;
  border-radius: 14px;
}

.chat-workspace__message-bubble.is-tool :deep(.chat-tool-panel) {
  border-color: hsl(var(--divider-faint) / 0.82);
  border-radius: 12px;
  background: hsl(var(--surface-panel));
}

.chat-workspace__welcome {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  min-height: 100%;
  padding: 36px 0 72px;
}

.chat-workspace__welcome-badge {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 9px;
  font-size: 11px;
  font-weight: 600;
  color: hsl(var(--text-muted));
  border: 1px solid hsl(var(--divider-faint) / 0.88);
  border-radius: 999px;
  background: hsl(var(--surface-subtle));
}

.chat-workspace__welcome-title {
  max-width: 480px;
  margin: 14px 0 0;
  font-size: 26px;
  font-weight: 680;
  line-height: 1.18;
  color: hsl(var(--text-strong));
  letter-spacing: -0.03em;
}

.chat-workspace__welcome-title--inline {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: center;
  max-width: 720px;
  line-height: 1.3;
}

.chat-workspace__welcome-text {
  max-width: 440px;
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.65;
  color: hsl(var(--text-muted));
}

.chat-workspace__welcome.is-centered {
  align-items: center;
  justify-content: flex-end;
  min-height: 0;
  padding: 0;
  text-align: center;
}

.chat-workspace__welcome.is-centered .chat-workspace__welcome-title,
.chat-workspace__welcome.is-centered .chat-workspace__welcome-text {
  max-width: 720px;
}

.chat-workspace__welcome.is-centered .chat-workspace__welcome-title {
  margin-top: 0;
}

.chat-workspace__composer-shell {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 0 24px 20px;
  background:
    linear-gradient(180deg, hsl(var(--surface-panel) / 0) 0%, hsl(var(--surface-panel) / 0.78) 30%, hsl(var(--surface-panel) / 0.96) 62%, hsl(var(--surface-panel)) 100%);
}

.chat-workspace__composer-shell.is-centered {
  position: static;
  padding: 28px 0 0;
  background: transparent;
}

.chat-workspace__composer {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px 10px;
  border: 1px solid hsl(var(--divider-faint) / 0.82);
  border-radius: 24px;
  background: hsl(var(--surface-panel) / 0.985);
  box-shadow: 0 24px 44px -36px hsl(var(--foreground) / 0.18);
}

.chat-workspace__composer.is-disabled {
  opacity: 0.74;
}

.chat-workspace__textarea :deep(.el-textarea__wrapper) {
  padding: 0;
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
}

.chat-workspace__textarea :deep(.el-textarea__inner) {
  padding: 0;
  min-height: 52px !important;
  border: none;
  outline: none;
  box-shadow: none;
  font-size: 15px;
  line-height: 1.58;
  color: hsl(var(--text-strong));
  background: transparent;
  resize: none;
}

.chat-workspace__textarea :deep(.el-textarea__inner::placeholder) {
  color: hsl(var(--text-muted));
}

.chat-workspace__composer-footer {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: flex-end;
  min-height: 42px;
}

.chat-workspace__composer-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex: none;
}

.chat-workspace__context-rail-shell {
  margin-top: 10px;
}

.chat-workspace__send-button {
  width: 42px;
  min-width: 42px;
  height: 42px;
  padding: 0;
  color: hsl(var(--background));
  border: none;
  border-radius: 999px;
  background: hsl(var(--foreground));
  box-shadow: none;
}

.chat-workspace__send-button:hover:not(.is-disabled) {
  background: hsl(var(--foreground) / 0.88);
}

.chat-workspace__send-button :deep(.el-icon) {
  font-size: 15px;
}

.chat-workspace__send-button.is-stop {
  color: hsl(var(--background));
  border: none;
  background: hsl(var(--foreground));
}

.chat-workspace__send-button.is-stop:hover:not(.is-disabled) {
  background: hsl(var(--foreground) / 0.88);
}

.chat-workspace__stop-glyph {
  display: block;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: currentColor;
}

.chat-workspace__composer-disabled {
  margin-top: 8px;
  font-size: 12px;
  color: hsl(var(--text-muted));
}

@media (max-width: 1180px) {
  .chat-workspace {
    --chat-sidebar-width: 252px;
  }
}

@media (max-width: 1024px) {
  .chat-workspace {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(128px, 30%) minmax(0, 1fr);
    height: 100%;
    min-height: 0;
    overflow: hidden;
  }

  .chat-workspace__sidebar {
    min-height: 0;
    max-height: none;
  }

  .chat-workspace__conversation {
    min-height: 0;
  }

  .chat-workspace__conversation.is-welcome-layout {
    padding-inline: 16px;
  }
}

@media (max-width: 768px) {
  .chat-workspace {
    padding: 12px;
  }

  .chat-workspace__topbar,
  .chat-workspace__message-shell,
  .chat-workspace__composer-shell {
    padding-inline: 16px;
  }

  .chat-workspace__message-shell {
    padding-bottom: 236px;
  }

  .chat-workspace__welcome-title {
    font-size: 24px;
  }

  .chat-workspace__welcome-title--inline {
    gap: 8px;
  }

  .chat-workspace__conversation.is-welcome-layout {
    padding: 24px 16px;
  }

  .chat-workspace__composer-footer {
    justify-content: flex-end;
  }

  .chat-workspace__message-bubble.is-user {
    max-width: 86%;
  }
}
</style>
