<script setup lang="ts">
import type {ServerSentEventMessage} from 'fetch-event-stream';

import type {ChatThinkingBlockStatus} from '@easyflow/common-ui';
import {ChatThinkingBlock, ChatTimeMarkdown} from '@easyflow/common-ui';
import type {BotInfo} from '@easyflow/types';

import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch,} from 'vue';
import {useRoute, useRouter} from 'vue-router';
import {LOGIN_PATH} from '@easyflow/constants';
import {IconifyIcon} from '@easyflow/icons';
import {$t} from '@easyflow/locales';
import {useAccessStore} from '@easyflow/stores';
import {createChatVariantSwitchController, uuid} from '@easyflow/utils';

import {useTitle} from '@vueuse/core';
import {ElAvatar, ElButton, ElInput, ElMessage, ElSkeleton} from 'element-plus';

import {refreshTokenApi} from '#/api/core';
import {baseRequestClient, sseClient} from '#/api/request';
import defaultBotAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
import ChatMessageActionBar from '#/components/chat-workspace/ChatMessageActionBar.vue';

type MessageRole = 'assistant' | 'user';
type ToolStatus = 'TOOL_CALL' | 'TOOL_RESULT';

interface ToolTraceItem {
  id: string;
  name: string;
  status: ToolStatus;
  arguments?: string;
  result?: string;
}

interface AssistantTextSegment {
  id: string;
  type: 'text';
  content: string;
}

interface AssistantToolSegment {
  id: string;
  type: 'tool';
  toolId: string;
}

interface AssistantThinkingSegment {
  id: string;
  type: 'thinking';
  content: string;
  expanded: boolean;
  status: ChatThinkingBlockStatus;
}

type AssistantSegment =
  | AssistantTextSegment
  | AssistantThinkingSegment
  | AssistantToolSegment;

interface BubbleMessage {
  id: string;
  role: MessageRole;
  content: string;
  createdAt: number;
  loading?: boolean;
  messageKind?: string;
  roundId?: string;
  roundNo?: number;
  selectedVariantIndex?: number;
  switchable?: boolean;
  toolCalls?: ToolTraceItem[];
  segments?: AssistantSegment[];
  variantCount?: number;
  variantIndex?: number;
}

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

interface PublicChatApiResponse<T> {
  data?: T;
  errorCode?: number;
  message?: string;
}

interface PublicChatLoginResult {
  token?: string;
}

interface PublicChatSessionContext {
  version: number;
  botId: string;
  accessToken: string;
  authenticatedAccess: boolean;
  conversationId: string;
  updatedAt: number;
}

interface PublicChatMessageRecord {
  id?: number | string;
  senderRole?: string;
  contentText?: string;
  contentPayload?: null | Record<string, any>;
  created?: number | string;
  messageKind?: string;
  roundId?: number | string;
  roundNo?: number;
  selectedVariantIndex?: number;
  switchable?: boolean;
  variantCount?: number;
  variantIndex?: number;
}

interface PublicChatSessionRestoreResult {
  sessionExists?: boolean;
  conversationId?: string;
  session?: null | Record<string, any>;
  messages?: PublicChatMessageRecord[];
}

type AuthResolutionStatus = 'authenticated' | 'error' | 'unauthenticated';

interface AuthResolutionResult {
  message?: string;
  nextToken?: string;
  status: AuthResolutionStatus;
}

const PUBLIC_CHAT_API_KEY_QUERY_KEYS = ['token', 'apikey', 'apiKey'] as const;
const PUBLIC_CHAT_CONTEXT_VERSION = 1;

const route = useRoute();
const router = useRouter();
const pageTitle = useTitle();
const accessStore = useAccessStore();

const botInfo = ref<BotInfo | null>(null);
const conversationId = ref<string>('');
const initializing = ref(true);
const blocked = ref(false);
const initError = ref('');
const sending = ref(false);
const userStoppedStreaming = ref(false);
const isComposing = ref(false);
const senderValue = ref('');
const messages = ref<BubbleMessage[]>([]);
const messageContainerRef = ref<HTMLElement | null>(null);
const dockRef = ref<HTMLElement | null>(null);
const shouldAutoScroll = ref(true);
const dockHeight = ref(170);
const collapsedToolIds = ref<Set<string>>(new Set());
const requestAccessToken = ref('');
const authenticatedAccess = ref(false);
const variantSwitchStateVersion = ref(0);
const variantSwitchController = createChatVariantSwitchController<
  PublicChatMessageRecord,
  BubbleMessage
>({
  mapRecords: (records) => records.map((record) => buildBubbleMessageFromRecord(record)),
  onError: () => ElMessage.error('答案版本切换失败'),
  onStateChange: () => {
    variantSwitchStateVersion.value += 1;
  },
  replaceRound(items, roundId, nextItems) {
    const assistantMessage = nextItems.find((item) => item.role === 'assistant');
    if (!assistantMessage) {
      return;
    }
    const targetIndex = items.findIndex(
      (item) => item.role === 'assistant' && item.roundId === roundId,
    );
    if (targetIndex >= 0) {
      items.splice(targetIndex, 1, assistantMessage);
    } else {
      items.push(assistantMessage);
    }
    const nextCollapsed = new Set(collapsedToolIds.value);
    assistantMessage.toolCalls?.forEach((item) => nextCollapsed.add(item.id));
    collapsedToolIds.value = nextCollapsed;
  },
});
const botId = computed(() => String(route.params.botId || ''));
const isEmbedMode = computed(() => String(route.query.embed || '') === '1');
const presetQuestions = computed(() =>
  (botInfo.value?.options?.presetQuestions || []).filter(
    (item: any) =>
      typeof item?.description === 'string' && item.description.trim() !== '',
  ),
);
const messageContainerStyle = computed(() => ({
  '--public-chat-dock-space': `${Math.max(dockHeight.value + 14, 132)}px`,
}));
let dockResizeObserver: null | ResizeObserver = null;

const SCROLL_FOLLOW_THRESHOLD = 48;
const setPageTitle = () => {
  const botTitle = String(botInfo.value?.title || '').trim();
  pageTitle.value = botTitle || $t('bot.publicChatTitle');
};

const getUnauthenticatedMessage = () => $t('bot.publicPageLoginRequired');

const normalizePublicChatErrorMessage = (message?: string) => {
  const trimmed = String(message || '').trim();
  if (!trimmed) {
    return $t('bot.publicChatInitError');
  }
  if (trimmed === '请登录' || trimmed === '用户未认证，请先登录') {
    return getUnauthenticatedMessage();
  }
  return trimmed;
};

const goToLogin = async () => {
  await router.push({
    path: LOGIN_PATH,
    query: {
      redirect: encodeURIComponent(route.fullPath),
    },
  });
};

const isAtBottom = () => {
  if (!messageContainerRef.value) return true;
  const { scrollTop, scrollHeight, clientHeight } = messageContainerRef.value;
  return scrollHeight - (scrollTop + clientHeight) <= SCROLL_FOLLOW_THRESHOLD;
};

const onMessageContainerScroll = () => {
  shouldAutoScroll.value = isAtBottom();
};

const updateDockHeight = () => {
  dockHeight.value = dockRef.value?.offsetHeight || 0;
};

const scrollToBottom = (force = false) => {
  nextTick(() => {
    if (!messageContainerRef.value) return;
    if (!force && !shouldAutoScroll.value) return;
    messageContainerRef.value.scrollTop =
      messageContainerRef.value.scrollHeight;
  });
};

const buildRequestHeaders = (
  token = requestAccessToken.value,
): Record<string, string> | undefined =>
  token
    ? {
        'easyflow-token': token,
      }
    : undefined;

const getResponseBody = async <T,>(request: Promise<any>) => {
  const response = await request;
  return (response?.data || {}) as PublicChatApiResponse<T>;
};

const getPublicChatContextKey = (botIdValue: string) =>
  `easyflow:public-chat:${botIdValue}`;

const readPublicChatContext = (
  botIdValue: string,
): null | PublicChatSessionContext => {
  if (!botIdValue || typeof window === 'undefined') {
    return null;
  }
  const raw = window.sessionStorage.getItem(
    getPublicChatContextKey(botIdValue),
  );
  if (!raw) {
    return null;
  }
  const parsed = safeJsonParse(raw);
  if (!parsed || typeof parsed !== 'object') {
    return null;
  }
  if (String(parsed.botId || '') !== botIdValue) {
    return null;
  }
  return {
    version: Number(parsed.version || PUBLIC_CHAT_CONTEXT_VERSION),
    botId: String(parsed.botId || botIdValue),
    accessToken: String(parsed.accessToken || ''),
    authenticatedAccess: Boolean(parsed.authenticatedAccess),
    conversationId: String(parsed.conversationId || ''),
    updatedAt: Number(parsed.updatedAt || Date.now()),
  };
};

const writePublicChatContext = (context: PublicChatSessionContext) => {
  if (!context.botId || typeof window === 'undefined') {
    return;
  }
  window.sessionStorage.setItem(
    getPublicChatContextKey(context.botId),
    JSON.stringify(context),
  );
};

const clearPublicChatContext = (botIdValue = botId.value) => {
  if (!botIdValue || typeof window === 'undefined') {
    return;
  }
  window.sessionStorage.removeItem(getPublicChatContextKey(botIdValue));
};

const upsertPublicChatContext = (
  patch: Partial<PublicChatSessionContext>,
  options?: { resetConversation?: boolean },
) => {
  const current =
    readPublicChatContext(botId.value) ||
    ({
      version: PUBLIC_CHAT_CONTEXT_VERSION,
      botId: botId.value,
      accessToken: '',
      authenticatedAccess: false,
      conversationId: '',
      updatedAt: Date.now(),
    } as PublicChatSessionContext);
  const next: PublicChatSessionContext = {
    ...current,
    ...patch,
    version: PUBLIC_CHAT_CONTEXT_VERSION,
    botId: botId.value,
    updatedAt: Date.now(),
  };
  if (options?.resetConversation) {
    next.conversationId = '';
  }
  writePublicChatContext(next);
  return next;
};

const normalizeTimestamp = (value: any) => {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }
  const parsed = Date.parse(String(value || ''));
  return Number.isNaN(parsed) ? Date.now() : parsed;
};

const normalizeRoundId = (value: any) => {
  const normalized = String(value ?? '').trim();
  return normalized || undefined;
};

const normalizePositiveInteger = (value: any) => {
  if (value == null || value === '') {
    return undefined;
  }
  const parsed = Number.parseInt(String(value), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
};

const applyRoundMeta = (
  target: Partial<BubbleMessage>,
  source?: null | Partial<PublicChatMessageRecord> | SseRoundMeta,
) => {
  if (!source) {
    return;
  }
  const roundId = normalizeRoundId(source.roundId);
  if (roundId) {
    target.roundId = roundId;
  }
  const roundNo = normalizePositiveInteger(source.roundNo);
  if (roundNo) {
    target.roundNo = roundNo;
  }
  const variantIndex = normalizePositiveInteger(source.variantIndex);
  if (variantIndex) {
    target.variantIndex = variantIndex;
  }
  const variantCount = normalizePositiveInteger(source.variantCount);
  if (variantCount) {
    target.variantCount = variantCount;
  }
  const selectedVariantIndex = normalizePositiveInteger(
    source.selectedVariantIndex,
  );
  if (selectedVariantIndex) {
    target.selectedVariantIndex = selectedVariantIndex;
  }
  if (typeof source.switchable === 'boolean') {
    target.switchable = source.switchable;
  }
  if ('messageKind' in source) {
    const messageKind = String(source.messageKind ?? '').trim();
    if (messageKind) {
      target.messageKind = messageKind;
    }
  }
};

const bindLatestPendingUserMessage = (meta?: SseRoundMeta) => {
  const roundId = normalizeRoundId(meta?.roundId);
  if (!roundId) {
    return;
  }
  for (let index = messages.value.length - 1; index >= 0; index -= 1) {
    const item = messages.value[index];
    if (!item) {
      continue;
    }
    if (item.role !== 'user') {
      continue;
    }
    if (item.roundId) {
      return;
    }
    applyRoundMeta(item, {
      roundId,
      roundNo: meta?.roundNo,
    });
    return;
  }
};

const restoreToolTraceFromChain = (chain: Record<string, any>) => {
  const toolId = String(chain.id || '').trim() || uuid();
  const status =
    String(chain.status || '').toUpperCase() === 'TOOL_CALL'
      ? 'TOOL_CALL'
      : 'TOOL_RESULT';
  return {
    id: toolId,
    name: String(chain.name || $t('bot.publicChatToolUnknown')),
    status,
    arguments:
      status === 'TOOL_CALL'
        ? normalizeToolPayloadValue(chain.arguments ?? chain.result)
        : undefined,
    result:
      status === 'TOOL_RESULT'
        ? normalizeToolPayloadValue(chain.result)
        : undefined,
  } as ToolTraceItem;
};

const buildAssistantMessageFromRecord = (
  record: PublicChatMessageRecord,
): BubbleMessage => {
  const contentText = String(record.contentText || '');
  const toolCalls: ToolTraceItem[] = [];
  const segments: AssistantSegment[] = [];
  const chains = Array.isArray(record.contentPayload?.chains)
    ? record.contentPayload?.chains
    : [];

  for (const rawChain of chains) {
    const chain =
      rawChain && typeof rawChain === 'object'
        ? (rawChain as Record<string, any>)
        : null;
    if (!chain) {
      continue;
    }
    const reasoningContent = String(chain.reasoning_content || '').trim();
    if (reasoningContent) {
      segments.push({
        id: uuid(),
        type: 'thinking',
        content: reasoningContent,
        expanded: false,
        status:
          String(chain.thinkingStatus || '').toLowerCase() === 'thinking'
            ? 'thinking'
            : 'end',
      });
      continue;
    }
    if (
      chain.id ||
      chain.name ||
      chain.status ||
      chain.result ||
      chain.arguments
    ) {
      const tool = restoreToolTraceFromChain(chain);
      toolCalls.push(tool);
      segments.push({
        id: uuid(),
        type: 'tool',
        toolId: tool.id,
      });
    }
  }

  if (contentText.trim()) {
    segments.push({
      id: uuid(),
      type: 'text',
      content: contentText,
    });
  }

  return {
    id: String(record.id || uuid()),
    role: 'assistant',
    content: contentText,
    createdAt: normalizeTimestamp(record.created),
    loading: false,
    toolCalls,
    segments,
  };
};

const buildBubbleMessageFromRecord = (record: PublicChatMessageRecord) => {
  const role = String(record.senderRole || '')
    .trim()
    .toLowerCase();
  if (role === 'assistant') {
    const assistantMessage = buildAssistantMessageFromRecord(record);
    applyRoundMeta(assistantMessage, record);
    return assistantMessage;
  }
  const userMessage: BubbleMessage = {
    id: String(record.id || uuid()),
    role: 'user',
    content: String(record.contentText || ''),
    createdAt: normalizeTimestamp(record.created),
    loading: false,
  };
  applyRoundMeta(userMessage, record);
  return userMessage;
};

const restoreMessagesFromRecords = (records: PublicChatMessageRecord[]) => {
  const restoredMessages: BubbleMessage[] = [];
  const nextCollapsedToolIds = new Set<string>();

  for (const record of records) {
    const message = buildBubbleMessageFromRecord(record);
    if (message.role === 'assistant') {
      const assistantMessage = message;
      assistantMessage.toolCalls?.forEach((item) =>
        nextCollapsedToolIds.add(item.id),
      );
      restoredMessages.push(assistantMessage);
      continue;
    }
    restoredMessages.push(message);
  }

  messages.value = restoredMessages;
  collapsedToolIds.value = nextCollapsedToolIds;
  shouldAutoScroll.value = true;
  prefetchVisibleVariants();
};

const readUrlApiKey = () => {
  for (const key of PUBLIC_CHAT_API_KEY_QUERY_KEYS) {
    const value = route.query[key];
    if (Array.isArray(value)) {
      const first = String(value[0] || '').trim();
      if (first) return first;
      continue;
    }
    const apiKey = String(value || '').trim();
    if (apiKey) return apiKey;
  }
  return '';
};

const clearUrlApiKey = async () => {
  const nextQuery = {
    ...route.query,
  };
  let changed = false;
  for (const key of PUBLIC_CHAT_API_KEY_QUERY_KEYS) {
    if (key in nextQuery) {
      delete nextQuery[key];
      changed = true;
    }
  }
  if (!changed) {
    return;
  }
  await router.replace({
    hash: route.hash,
    path: route.path,
    query: nextQuery,
  });
};

const exchangeApiKeyToAccessToken = async (apiKey: string) => {
  const loginResp = await getResponseBody<PublicChatLoginResult>(
    baseRequestClient.post('/api/v1/auth/loginByApiKey', {
      apiKey,
    }),
  );
  const accessToken = String(loginResp.data?.token || '').trim();
  if (loginResp.errorCode !== 0 || !accessToken) {
    throw new Error(loginResp.message || $t('bot.publicChatTokenInvalid'));
  }
  return accessToken;
};

const extractErrorStatus = (error: any) => {
  const status = Number(error?.response?.status);
  return Number.isFinite(status) ? status : 0;
};

const extractErrorMessage = (error: any) => {
  const responseData = error?.response?.data ?? {};
  const message = String(
    responseData?.message || responseData?.error || error?.message || '',
  ).trim();
  return message;
};

const isUnauthorizedError = (error: any) => {
  const responseData = error?.response?.data ?? {};
  return (
    extractErrorStatus(error) === 401 || Number(responseData?.errorCode) === 401
  );
};

const probeAuthenticatedAccess = async (token: string) => {
  if (!token.trim()) {
    return { status: 'unauthenticated' } as AuthResolutionResult;
  }
  try {
    const profileResp = await getResponseBody(
      baseRequestClient.get('/api/v1/sysAccount/myProfile', {
        headers: buildRequestHeaders(token),
      }),
    );
    if (profileResp.errorCode === 0 && profileResp.data) {
      return {
        nextToken: token,
        status: 'authenticated',
      } as AuthResolutionResult;
    }
    if (profileResp.errorCode === 401) {
      return { status: 'unauthenticated' } as AuthResolutionResult;
    }
    return {
      message: normalizePublicChatErrorMessage(profileResp.message),
      status: 'error',
    } as AuthResolutionResult;
  } catch (error) {
    if (isUnauthorizedError(error)) {
      return { status: 'unauthenticated' } as AuthResolutionResult;
    }
    return {
      message: normalizePublicChatErrorMessage(extractErrorMessage(error)),
      status: 'error',
    } as AuthResolutionResult;
  }
};

const tryRefreshCurrentAccessToken = async () => {
  try {
    const refreshed = await refreshTokenApi();
    const nextToken = String(refreshed?.data || '').trim();
    if (!nextToken) {
      return { status: 'unauthenticated' } as AuthResolutionResult;
    }
    accessStore.setAccessToken(nextToken);
    return await probeAuthenticatedAccess(nextToken);
  } catch (error) {
    if (isUnauthorizedError(error)) {
      return { status: 'unauthenticated' } as AuthResolutionResult;
    }
    return {
      message: normalizePublicChatErrorMessage(extractErrorMessage(error)),
      status: 'error',
    } as AuthResolutionResult;
  }
};

const resolveAuthenticatedAccess = async (
  token: string,
  options?: { allowRefresh?: boolean },
) => {
  const initialResult = await probeAuthenticatedAccess(token);
  if (initialResult.status !== 'unauthenticated' || !options?.allowRefresh) {
    return initialResult;
  }
  return await tryRefreshCurrentAccessToken();
};

const ensureRequestAccessToken = async () => {
  requestAccessToken.value = '';
  authenticatedAccess.value = false;
  const urlApiKey = readUrlApiKey();
  if (urlApiKey) {
    const exchangedToken = await exchangeApiKeyToAccessToken(urlApiKey);
    const authResult = await resolveAuthenticatedAccess(exchangedToken);
    if (authResult.status === 'error') {
      throw new Error(authResult.message || $t('bot.publicChatInitError'));
    }
    if (authResult.status !== 'authenticated') {
      throw new Error($t('bot.publicChatTokenInvalid'));
    }
    requestAccessToken.value = authResult.nextToken || exchangedToken;
    authenticatedAccess.value = true;
    upsertPublicChatContext(
      {
        accessToken: requestAccessToken.value,
        authenticatedAccess: true,
      },
      { resetConversation: true },
    );
    await clearUrlApiKey();
    return;
  }

  const storedContext = readPublicChatContext(botId.value);
  if (storedContext?.accessToken) {
    const authResult = await resolveAuthenticatedAccess(
      storedContext.accessToken,
    );
    if (authResult.status === 'error') {
      throw new Error(authResult.message || $t('bot.publicChatInitError'));
    }
    if (authResult.status === 'authenticated') {
      requestAccessToken.value =
        authResult.nextToken || storedContext.accessToken;
      authenticatedAccess.value = true;
      upsertPublicChatContext({
        accessToken: requestAccessToken.value,
        authenticatedAccess: true,
        conversationId: storedContext.conversationId,
      });
      return;
    }
    clearPublicChatContext(botId.value);
  }
  const currentAccessToken = String(accessStore.accessToken || '').trim();
  if (currentAccessToken) {
    const authResult = await resolveAuthenticatedAccess(currentAccessToken, {
      allowRefresh: true,
    });
    if (authResult.status === 'error') {
      throw new Error(authResult.message || $t('bot.publicChatInitError'));
    }
    if (authResult.status === 'authenticated') {
      requestAccessToken.value = authResult.nextToken || currentAccessToken;
      authenticatedAccess.value = true;
      upsertPublicChatContext(
        {
          accessToken: requestAccessToken.value,
          authenticatedAccess: true,
        },
        { resetConversation: true },
      );
    }
  }
};

const resetConversationState = () => {
  botInfo.value = null;
  conversationId.value = '';
  sending.value = false;
  senderValue.value = '';
  messages.value = [];
  blocked.value = false;
  initError.value = '';
  shouldAutoScroll.value = true;
  collapsedToolIds.value = new Set();
};

const appendMessage = (role: MessageRole, content = '', loading = false) => {
  const item: BubbleMessage = {
    id: uuid(),
    role,
    content,
    createdAt: Date.now(),
    loading,
  };
  if (role === 'assistant' && content.trim()) {
    item.segments = [
      {
        id: uuid(),
        type: 'text',
        content,
      },
    ];
  }
  messages.value.push(item);
  scrollToBottom(true);
};

const findMessageIndexByRoundId = (
  role: MessageRole,
  roundId?: string,
) => {
  if (!roundId) {
    return -1;
  }
  for (let index = messages.value.length - 1; index >= 0; index -= 1) {
    const item = messages.value[index];
    if (!item) {
      continue;
    }
    if (item.role === role && item.roundId === roundId) {
      return index;
    }
  }
  return -1;
};

const resetAssistantMessageForRegenerate = (roundId: string) => {
  const targetIndex = findMessageIndexByRoundId('assistant', roundId);
  if (targetIndex < 0) {
    appendMessage('assistant', '', true);
    return;
  }
  const current = messages.value[targetIndex];
  if (!current) {
    appendMessage('assistant', '', true);
    return;
  }
  messages.value[targetIndex] = {
    ...current,
    content: '',
    createdAt: Date.now(),
    loading: true,
    segments: [],
    toolCalls: [],
  };
  scrollToBottom(true);
};

const updateLastAssistantMessage = (patch: Partial<BubbleMessage>) => {
  const lastIndex = messages.value.length - 1;
  if (lastIndex < 0) return;
  const last = messages.value[lastIndex];
  if (!last || last.role !== 'assistant') return;
  messages.value[lastIndex] = { ...last, ...patch };
  scrollToBottom();
};

const normalizeEventKey = (value: any) => String(value || '').toUpperCase();
const safeJsonParse = (value: string) => {
  try {
    return JSON.parse(value);
  } catch {
    return null;
  }
};
const safeJsonParseLoose = (value: string) => {
  const parsed = safeJsonParse(value);
  if (parsed) {
    return parsed;
  }
  const firstBrace = value.indexOf('{');
  const lastBrace = value.lastIndexOf('}');
  if (firstBrace === -1 || lastBrace <= firstBrace) {
    return null;
  }
  return safeJsonParse(value.slice(firstBrace, lastBrace + 1));
};

const normalizeToolPayloadValue = (value: any) => {
  if (value === null || value === undefined) return '';
  if (typeof value === 'string') return value;
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
};

const getLastAssistantMessage = () => {
  const last = messages.value[messages.value.length - 1];
  if (!last || last.role !== 'assistant') {
    return null;
  }
  return last;
};

const appendAssistantTextDelta = (delta: string, meta?: SseRoundMeta) => {
  if (!delta) return;
  const last = getLastAssistantMessage();
  if (!last) return;
  const segments = [...(last.segments || [])];
  const tail = segments[segments.length - 1];
  if (tail && tail.type === 'text') {
    segments[segments.length - 1] = {
      ...tail,
      content: `${tail.content}${delta}`,
    };
  } else {
    segments.push({
      id: uuid(),
      type: 'text',
      content: delta,
    });
  }
  updateLastAssistantMessage({
    content: `${last.content}${delta}`,
    segments,
    loading: true,
    ...meta,
  });
};

const appendAssistantThinkingDelta = (delta: string, meta?: SseRoundMeta) => {
  if (!delta) return;
  const last = getLastAssistantMessage();
  if (!last) return;
  const segments = [...(last.segments || [])];
  const thinkingIndex = [...segments]
    .reverse()
    .findIndex(
      (segment) => segment.type === 'thinking' && segment.status === 'thinking',
    );

  if (thinkingIndex === -1) {
    segments.push({
      id: uuid(),
      type: 'thinking',
      content: delta,
      expanded: false,
      status: 'thinking',
    });
  } else {
    const actualIndex = segments.length - 1 - thinkingIndex;
    const current = segments[actualIndex] as AssistantThinkingSegment;
    segments[actualIndex] = {
      ...current,
      content: `${current.content}${delta}`,
    };
  }

  updateLastAssistantMessage({
    segments,
    loading: true,
    ...meta,
  });
};

const stopAssistantThinking = () => {
  const last = getLastAssistantMessage();
  if (!last?.segments?.length) return;
  updateLastAssistantMessage({
    segments: last.segments.map((segment) => {
      if (segment.type !== 'thinking' || segment.status !== 'thinking') {
        return segment;
      }
      return {
        ...segment,
        status: 'end' as ChatThinkingBlockStatus,
      };
    }),
  });
};

const getToolById = (message: BubbleMessage, toolId: string) =>
  (message.toolCalls || []).find((item) => item.id === toolId);

const getRenderableSegments = (message: BubbleMessage) => {
  if (message.role !== 'assistant') {
    return [];
  }
  const segments = [...(message.segments || [])];
  const hasTextSegment = segments.some(
    (segment) => segment.type === 'text' && segment.content.trim() !== '',
  );
  if (!hasTextSegment && message.content.trim()) {
    segments.push({
      id: `${message.id}-fallback-text`,
      type: 'text',
      content: message.content,
    });
  }
  const textSegments = segments.filter(
    (segment): segment is AssistantTextSegment => segment.type === 'text',
  );
  if (textSegments.length <= 1) {
    return segments;
  }
  return [
    ...segments.filter((segment) => segment.type !== 'text'),
    {
      id: `${message.id}-merged-text`,
      type: 'text' as const,
      content: textSegments.map((segment) => segment.content).join(''),
    },
  ];
};

const hasAssistantRenderableContent = (message: BubbleMessage) => {
  const segments = getRenderableSegments(message);
  return segments.some((segment) => {
    if (segment.type === 'thinking') {
      return segment.content.trim() !== '';
    }
    if (segment.type === 'text') {
      return segment.content.trim() !== '';
    }
    return Boolean(getToolById(message, segment.toolId));
  });
};

const upsertToolTrace = (patch: Partial<ToolTraceItem> & { id?: string }) => {
  const last = getLastAssistantMessage();
  if (!last) {
    return;
  }
  const nextToolCalls = [...(last.toolCalls || [])];
  let id = String(patch.id || '');
  if (!id && patch.status === 'TOOL_RESULT') {
    const pending = [...nextToolCalls]
      .reverse()
      .find((item) => item.status !== 'TOOL_RESULT');
    if (pending) {
      id = pending.id;
    }
  }
  if (!id) {
    id = uuid();
  }
  const idx = nextToolCalls.findIndex((item) => item.id === id);
  const current = idx === -1 ? null : nextToolCalls[idx];
  const next: ToolTraceItem = {
    id,
    name: String(
      patch.name || current?.name || $t('bot.publicChatToolUnknown'),
    ),
    status: (patch.status || current?.status || 'TOOL_CALL') as ToolStatus,
    arguments: patch.arguments ?? current?.arguments,
    result: patch.result ?? current?.result,
  };
  if (idx === -1) {
    nextToolCalls.push(next);
  } else {
    nextToolCalls[idx] = next;
  }
  const segments = [...(last.segments || [])];
  const hasToolSegment = segments.some(
    (segment) => segment.type === 'tool' && segment.toolId === id,
  );
  if (!hasToolSegment) {
    segments.push({
      id: uuid(),
      type: 'tool',
      toolId: id,
    });
    const nextCollapsed = new Set(collapsedToolIds.value);
    nextCollapsed.add(id);
    collapsedToolIds.value = nextCollapsed;
  }
  updateLastAssistantMessage({
    toolCalls: nextToolCalls,
    segments,
    loading: true,
    roundId: last.roundId,
    roundNo: last.roundNo,
    selectedVariantIndex: last.selectedVariantIndex,
    switchable: last.switchable,
    variantCount: last.variantCount,
    variantIndex: last.variantIndex,
  });
};

const isToolCollapsed = (toolId: string) => collapsedToolIds.value.has(toolId);
const toggleToolCollapsed = (toolId: string) => {
  const next = new Set(collapsedToolIds.value);
  if (next.has(toolId)) {
    next.delete(toolId);
  } else {
    next.add(toolId);
  }
  collapsedToolIds.value = next;
};

const extractToolMetaFromPayload = (payload: any) => {
  if (!payload || typeof payload !== 'object') {
    return null;
  }
  const id = payload?.tool_call_id || payload?.toolCallId || payload?.id || '';
  const name = payload?.name || payload?.toolName || '';
  const hasArgs = payload?.arguments !== undefined;
  const hasResult =
    payload?.result !== undefined || payload?.content !== undefined;
  if (!id && !name && !hasArgs && !hasResult) {
    return null;
  }
  return {
    id: String(id || ''),
    name: String(name || ''),
    arguments: hasArgs
      ? normalizeToolPayloadValue(payload?.arguments)
      : undefined,
    result: hasResult
      ? normalizeToolPayloadValue(
          payload?.result === undefined ? payload?.content : payload?.result,
        )
      : undefined,
  };
};

const handleToolSseEvent = (sseData: any, normalizedType?: string) => {
  const payload = sseData?.payload || {};
  const streamMeta = normalizeSseRoundMeta(sseData?.meta);
  const inferred = extractToolMetaFromPayload(payload);
  const normalized = normalizedType || normalizeEventKey(sseData?.type);
  let status = normalized as ToolStatus;
  if (status !== 'TOOL_CALL' && status !== 'TOOL_RESULT') {
    if (inferred?.result && !inferred?.arguments) {
      status = 'TOOL_RESULT';
    } else if (inferred?.arguments || inferred?.name || inferred?.id) {
      status = 'TOOL_CALL';
    } else {
      return;
    }
  }
  if (!inferred) {
    return;
  }
  upsertToolTrace({
    id: inferred.id,
    name: inferred.name,
    status,
    arguments: status === 'TOOL_CALL' ? inferred.arguments : undefined,
    result: status === 'TOOL_RESULT' ? inferred.result : undefined,
  });
  if (Object.keys(streamMeta).length > 0) {
    updateLastAssistantMessage(streamMeta);
  }
};

const handleNeedSaveToolMessage = (role: string, content: string) => {
  if (role === 'assistant') {
    const parsed = safeJsonParseLoose(content);
    const toolCalls = Array.isArray(parsed?.toolCalls) ? parsed.toolCalls : [];
    for (const call of toolCalls) {
      upsertToolTrace({
        id: String(call?.id || uuid()),
        name: String(call?.name || ''),
        status: 'TOOL_CALL',
        arguments: normalizeToolPayloadValue(call?.arguments),
      });
    }
    return;
  }

  if (role === 'tool') {
    const parsed = safeJsonParseLoose(content);
    const toolCallId = String(parsed?.toolCallId || parsed?.tool_call_id || '');
    const resultText =
      typeof parsed?.content === 'string'
        ? parsed.content
        : normalizeToolPayloadValue(parsed || content);
    upsertToolTrace({
      id: toolCallId || uuid(),
      status: 'TOOL_RESULT',
      result: resultText,
    });
  }
};

const onSseMessage = (event: ServerSentEventMessage) => {
  if (event.event === 'done') {
    sending.value = false;
    stopAssistantThinking();
    updateLastAssistantMessage({ loading: false });
    return;
  }
  if (!event.data) {
    return;
  }

  let sseData: any = null;
  try {
    sseData = JSON.parse(event.data);
  } catch {
    return;
  }
  const streamMeta = normalizeSseRoundMeta(sseData?.meta);
  bindLatestPendingUserMessage(streamMeta);
  const eventDomain = normalizeEventKey(sseData?.domain);
  const eventType = normalizeEventKey(sseData?.type);
  if (eventDomain === 'SYSTEM' && sseData?.payload?.code === 'SYSTEM_ERROR') {
    sending.value = false;
    stopAssistantThinking();
    updateLastAssistantMessage({
      content: sseData.payload?.message || $t('bot.publicChatInitError'),
      loading: false,
      ...streamMeta,
    });
    return;
  }

  if (
    eventDomain === 'TOOL' ||
    eventType === 'TOOL_CALL' ||
    eventType === 'TOOL_RESULT'
  ) {
    stopAssistantThinking();
    handleToolSseEvent(sseData, eventType);
    return;
  }

  if (event.event === 'needSaveMessage') {
    const role = String(sseData?.payload?.role || '')
      .trim()
      .toLowerCase();
    const content = sseData?.payload?.content;
    if (
      (role === 'assistant' ||
        role === 'user' ||
        role === 'system' ||
        role === 'tool') &&
      typeof content === 'string'
    ) {
      handleNeedSaveToolMessage(role, content);
    }
    return;
  }

  if (eventType === 'MESSAGE' && eventDomain === 'LLM') {
    stopAssistantThinking();
    const deltaRaw = sseData?.payload?.delta ?? '';
    const delta =
      typeof deltaRaw === 'string'
        ? deltaRaw
        : normalizeToolPayloadValue(deltaRaw);
    appendAssistantTextDelta(delta, streamMeta);
    return;
  }

  if (eventType === 'THINKING' && eventDomain === 'LLM') {
    const deltaRaw = sseData?.payload?.delta ?? sseData?.payload?.content ?? '';
    const delta =
      typeof deltaRaw === 'string'
        ? deltaRaw
        : normalizeToolPayloadValue(deltaRaw);
    appendAssistantThinkingDelta(delta, streamMeta);
    return;
  }

  handleToolSseEvent(sseData, eventType);
};

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

const getLatestAssistantMessage = () => {
  for (let index = messages.value.length - 1; index >= 0; index -= 1) {
    const item = messages.value[index];
    if (!item) {
      continue;
    }
    if (item.role === 'assistant') {
      return item;
    }
  }
  return null;
};

const resolveRoundUserPrompt = (roundId?: string) => {
  if (!roundId) {
    return '';
  }
  const matched = messages.value.find(
    (item) => item.role === 'user' && item.roundId === roundId,
  );
  return matched ? String(matched.content || '').trim() : '';
};

const refreshConversationMessages = async () => {
  if (!conversationId.value) {
    return false;
  }
  const restoreResp = await getResponseBody<PublicChatSessionRestoreResult>(
    baseRequestClient.get('/api/v1/public-chat/session/restore', {
      params: {
        botId: botId.value,
        conversationId: conversationId.value,
      },
      headers: buildRequestHeaders(),
    }),
  );
  const restoreData = restoreResp.data;
  if (
    restoreResp.errorCode !== 0 ||
    !restoreData?.sessionExists ||
    !restoreData.conversationId
  ) {
    return false;
  }
  conversationId.value = String(restoreData.conversationId);
  restoreMessagesFromRecords(restoreData.messages || []);
  upsertPublicChatContext({
    accessToken: requestAccessToken.value,
    authenticatedAccess: authenticatedAccess.value,
    conversationId: conversationId.value,
  });
  return true;
};

const doSendMessage = (options?: {
  prompt?: string;
  regenerateRoundId?: string;
}) => {
  const prompt = String(options?.prompt ?? senderValue.value).trim();
  const regenerateRoundId = options?.regenerateRoundId
    ? String(options.regenerateRoundId)
    : '';
  const isRegenerate = !!regenerateRoundId;
  if (!prompt || sending.value || blocked.value || !conversationId.value) {
    return;
  }

  if (!isRegenerate) {
    appendMessage('user', prompt);
    appendMessage('assistant', '', true);
  } else {
    resetAssistantMessageForRegenerate(regenerateRoundId);
  }
  shouldAutoScroll.value = true;
  userStoppedStreaming.value = false;
  if (!options?.prompt) {
    senderValue.value = '';
  }
  sending.value = true;

  sseClient.post(
    '/api/v1/bot/chat',
    {
      botId: botId.value,
      prompt,
      conversationId: conversationId.value,
      regenerateRoundId: regenerateRoundId || undefined,
    },
    {
      headers: buildRequestHeaders(),
      onMessage: onSseMessage,
      onFinished: async () => {
        sending.value = false;
        stopAssistantThinking();
        updateLastAssistantMessage({ loading: false });
        await refreshConversationMessages();
      },
      onError: async (error: any) => {
        sending.value = false;
        stopAssistantThinking();
        const abortLike =
          userStoppedStreaming.value ||
          error?.name === 'AbortError' ||
          String(error?.message || '').includes('aborted');
        if (abortLike) {
          const last = messages.value[messages.value.length - 1];
          if (last?.role === 'assistant' && !last.content.trim()) {
            updateLastAssistantMessage({
              loading: false,
              content: $t('bot.publicChatStopped'),
            });
          } else {
            updateLastAssistantMessage({ loading: false });
          }
          await refreshConversationMessages();
          return;
        }
        updateLastAssistantMessage({
          loading: false,
          content: $t('bot.publicChatInitError'),
        });
        await refreshConversationMessages();
      },
    },
  );
};

const stopStreaming = () => {
  userStoppedStreaming.value = true;
  sseClient.abort();
  sending.value = false;
  stopAssistantThinking();
  const last = messages.value[messages.value.length - 1];
  if (last?.role === 'assistant' && !last.content.trim()) {
    updateLastAssistantMessage({
      loading: false,
      content: $t('bot.publicChatStopped'),
    });
    return;
  }
  updateLastAssistantMessage({ loading: false });
};

const onEnter = (evt: Event | KeyboardEvent) => {
  if (!(evt instanceof KeyboardEvent)) {
    return;
  }
  // 输入法组合输入期间，Enter 仅用于上屏，不触发发送
  if (isComposing.value || evt.isComposing || evt.keyCode === 229) {
    return;
  }
  if (evt.shiftKey) return;
  evt.preventDefault();
  doSendMessage();
};

const onCompositionStart = () => {
  isComposing.value = true;
};

const onCompositionEnd = () => {
  isComposing.value = false;
};

const usePresetQuestion = (question: string) => {
  senderValue.value = question;
  doSendMessage();
};

const createConversationId = async () => {
  const conversationResp = await getResponseBody<number | string>(
    baseRequestClient.get('/api/v1/bot/generateConversationId', {
      headers: buildRequestHeaders(),
    }),
  );
  if (conversationResp.errorCode !== 0 || !conversationResp.data) {
    throw new Error(conversationResp.message || $t('bot.publicChatInitError'));
  }
  conversationId.value = String(conversationResp.data);
  upsertPublicChatContext({
    accessToken: requestAccessToken.value,
    authenticatedAccess: authenticatedAccess.value,
    conversationId: conversationId.value,
  });
};

const restoreConversationMessages = async (conversationIdValue?: string) => {
  const storedContext = readPublicChatContext(botId.value);
  const storedConversationId = String(
    conversationIdValue || storedContext?.conversationId || '',
  ).trim();
  if (!storedConversationId) {
    return false;
  }

  const restoreResp = await getResponseBody<PublicChatSessionRestoreResult>(
    baseRequestClient.get('/api/v1/public-chat/session/restore', {
      params: {
        botId: botId.value,
        conversationId: storedConversationId,
      },
      headers: buildRequestHeaders(),
    }),
  );
  const restoreData = restoreResp.data;
  if (
    restoreResp.errorCode !== 0 ||
    !restoreData?.sessionExists ||
    !restoreData.conversationId
  ) {
    return false;
  }

  conversationId.value = String(restoreData.conversationId);
  restoreMessagesFromRecords(restoreData.messages || []);
  upsertPublicChatContext({
    accessToken: requestAccessToken.value,
    authenticatedAccess: authenticatedAccess.value,
    conversationId: conversationId.value,
  });
  return true;
};

const initPublicChat = async () => {
  resetConversationState();
  initializing.value = true;
  setPageTitle();

  try {
    await ensureRequestAccessToken();

    const botResp = await getResponseBody<BotInfo>(
      baseRequestClient.get('/api/v1/bot/getDetail', {
        params: { id: botId.value },
        headers: buildRequestHeaders(),
      }),
    );
    if (botResp.errorCode !== 0 || !botResp.data) {
      throw new Error(botResp.message || $t('bot.publicChatInitError'));
    }
    botInfo.value = botResp.data;
    setPageTitle();
    blocked.value =
      !botResp.data?.options?.anonymousEnabled && !authenticatedAccess.value;
    if (blocked.value) {
      clearPublicChatContext(botId.value);
      return;
    }

    const restored = await restoreConversationMessages();
    if (!restored) {
      await createConversationId();
      const welcomeMessage = botResp.data?.options?.welcomeMessage;
      if (typeof welcomeMessage === 'string' && welcomeMessage.trim() !== '') {
        appendMessage('assistant', welcomeMessage.trim());
      }
    }
  } catch (error) {
    console.error(error);
    initError.value =
      error instanceof Error && error.message
        ? normalizePublicChatErrorMessage(error.message)
        : $t('bot.publicChatInitError');
    setPageTitle();
  } finally {
    initializing.value = false;
  }
};

onMounted(() => {
  initPublicChat();
  nextTick(() => {
    messageContainerRef.value?.addEventListener(
      'scroll',
      onMessageContainerScroll,
      {
        passive: true,
      },
    );
    updateDockHeight();
    if (dockRef.value && typeof ResizeObserver !== 'undefined') {
      dockResizeObserver = new ResizeObserver(() => {
        updateDockHeight();
      });
      dockResizeObserver.observe(dockRef.value);
    }
  });
});

watch(
  () => route.params.botId,
  () => {
    initPublicChat();
  },
);

watch(
  () => messages.value.length,
  () => {
    scrollToBottom();
  },
);

watch(
  () => [
    initializing.value,
    initError.value,
    blocked.value,
    presetQuestions.value.length,
    sending.value,
  ],
  () => {
    nextTick(updateDockHeight);
  },
  { flush: 'post' },
);

onBeforeUnmount(() => {
  sseClient.abort();
  messageContainerRef.value?.removeEventListener(
    'scroll',
    onMessageContainerScroll,
  );
  dockResizeObserver?.disconnect();
  dockResizeObserver = null;
});

const canCopyMessage = (item: BubbleMessage) =>
  Boolean(String(item.content || '').trim());

const shouldShowVariantNavigator = (item: BubbleMessage) =>
  item.role === 'assistant' &&
  isFinalAssistantInRound(item) &&
  Number(item.variantCount || 0) > 1 &&
  Boolean(item.roundId);

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

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

const handleRegenerateMessage = async (item: BubbleMessage) => {
  if (sending.value || item.role !== 'assistant') {
    return;
  }
  const roundId = String(item.roundId || '');
  const prompt = resolveRoundUserPrompt(roundId);
  if (!roundId || !prompt) {
    ElMessage.warning('当前回答暂不支持重新生成');
    return;
  }
  doSendMessage({
    prompt,
    regenerateRoundId: roundId,
  });
};

const handleSelectVariant = async (
  item: BubbleMessage,
  direction: 'next' | 'previous',
) => {
  if (!conversationId.value || !item.roundId) {
    return;
  }
  const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
  const variantIndex = direction === 'previous' ? current - 1 : current + 1;
  await variantSwitchController.switchVariant({
    fetchVariants: () => fetchRoundVariants(conversationId.value, String(item.roundId)),
    items: messages.value,
    persistVariant: async () => {
      const res = await getResponseBody<PublicChatMessageRecord>(
        baseRequestClient.post(
          `/api/v1/public-chat/session/${conversationId.value}/rounds/${item.roundId}/selectVariant`,
          {
            variantIndex,
          },
          {
            params: {
              botId: botId.value,
            },
            headers: buildRequestHeaders(),
          },
        ),
      );
      if (res.errorCode !== 0 || !res.data) {
        throw new Error(res.message || '答案版本切换失败');
      }
      return res.data;
    },
    roundId: item.roundId,
    sessionId: conversationId.value,
    targetVariantIndex: variantIndex,
  });
};

async function fetchRoundVariants(sessionId: string, roundId: string) {
  const res = await getResponseBody<PublicChatMessageRecord[]>(
    baseRequestClient.get(
      `/api/v1/public-chat/session/${sessionId}/rounds/${roundId}/variants`,
      {
        params: {
          botId: botId.value,
        },
        headers: buildRequestHeaders(),
      },
    ),
  );
  if (res.errorCode !== 0) {
    throw new Error(res.message || '答案版本加载失败');
  }
  return res.data || [];
}

function isVariantSwitching(item: BubbleMessage) {
  variantSwitchStateVersion.value;
  return variantSwitchController.isSwitching(conversationId.value, item.roundId);
}

function prefetchVisibleVariants() {
  if (!conversationId.value) {
    return;
  }
  for (const item of messages.value) {
    if (
      item.role === 'assistant' &&
      item.roundId &&
      Number(item.variantCount || 0) > 1
    ) {
      variantSwitchController.prefetchVariants({
        fetchVariants: () => fetchRoundVariants(conversationId.value, String(item.roundId)),
        roundId: item.roundId,
        sessionId: conversationId.value,
      });
    }
  }
}
</script>

<template>
  <div
    class="public-chat-page"
    :class="[isEmbedMode ? 'public-chat-page-embed' : '']"
  >
    <div class="public-chat-shell">
      <header class="public-chat-header">
        <div class="public-chat-title-group">
          <ElAvatar :size="40" :src="botInfo?.icon || defaultBotAvatar" />
          <div class="public-chat-title-wrap">
            <h1 class="public-chat-title">
              {{ botInfo?.title || $t('bot.publicChatTitle') }}
            </h1>
            <p class="public-chat-subtitle">
              {{ $t('bot.publicChatSubtitle') }}
            </p>
          </div>
        </div>
      </header>

      <main
        ref="messageContainerRef"
        class="public-chat-messages"
        :style="messageContainerStyle"
      >
        <ElSkeleton :loading="initializing" animated :rows="5">
          <template #default>
            <div
              v-if="initError"
              class="public-chat-banner public-chat-banner-error"
            >
              {{ initError }}
            </div>
            <div
              v-else-if="blocked"
              class="public-chat-banner public-chat-banner-warning"
            >
              <p class="public-chat-banner-title">
                {{ $t('bot.publicPageLoginRequired') }}
              </p>
              <p>{{ $t('bot.publicPageLoginRequiredTip') }}</p>
              <ElButton
                type="primary"
                class="public-chat-banner-action"
                @click="goToLogin"
              >
                {{ $t('authentication.goToLogin') }}
              </ElButton>
            </div>
            <template v-else>
              <div
                v-for="item in messages"
                :key="item.id"
                class="public-chat-message-row"
                :class="[item.role === 'user' ? 'is-user' : 'is-assistant']"
              >
                <ElAvatar
                  v-if="item.role === 'assistant'"
                  :size="34"
                  :src="botInfo?.icon || defaultBotAvatar"
                  class="public-chat-message-avatar"
                />
                <div class="public-chat-message">
                  <div
                    class="public-chat-bubble"
                    :class="[
                      item.role === 'user'
                        ? 'public-chat-bubble-user'
                        : 'public-chat-bubble-assistant',
                    ]"
                  >
                    <template v-if="item.role === 'user'">
                      <span>{{ item.content }}</span>
                    </template>
                    <template v-else>
                      <template
                        v-if="
                          item.loading && !hasAssistantRenderableContent(item)
                        "
                      >
                        <span class="public-chat-waiting">
                          <span class="public-chat-waiting-spinner"></span>
                          <span>{{ $t('bot.publicChatThinking') }}</span>
                        </span>
                      </template>
                      <template v-else>
                        <div
                          v-for="segment in getRenderableSegments(item)"
                          :key="segment.id"
                          class="public-chat-segment"
                          :class="[
                            segment.type === 'tool'
                              ? 'is-tool'
                              : segment.type === 'thinking'
                                ? 'is-thinking'
                                : 'is-text',
                          ]"
                        >
                          <template v-if="segment.type === 'text'">
                            <ChatTimeMarkdown
                              class="public-chat-markdown"
                              :content="segment.content"
                              :streaming="item.loading"
                            />
                          </template>
                          <template v-else-if="segment.type === 'thinking'">
                            <ChatThinkingBlock
                              v-model:expanded="segment.expanded"
                              :content="segment.content"
                              :status="segment.status"
                              class="public-chat-thinking-block"
                            />
                          </template>
                          <template v-else>
                            <template v-if="getToolById(item, segment.toolId)">
                              <div class="public-chat-tool-segment">
                                <div class="public-chat-tool-item">
                                  <div class="public-chat-tool-header">
                                    <span class="public-chat-tool-name">
                                      <IconifyIcon
                                        icon="solar:widget-5-outline"
                                      />
                                      {{
                                        getToolById(item, segment.toolId)?.name
                                      }}
                                    </span>
                                    <div class="public-chat-tool-actions">
                                      <span
                                        class="public-chat-tool-status"
                                        :class="[
                                          getToolById(item, segment.toolId)
                                            ?.status === 'TOOL_RESULT'
                                            ? 'is-done'
                                            : 'is-calling',
                                        ]"
                                      >
                                        {{
                                          getToolById(item, segment.toolId)
                                            ?.status === 'TOOL_RESULT'
                                            ? $t('bot.publicChatToolDone')
                                            : $t('bot.publicChatToolCalling')
                                        }}
                                      </span>
                                      <button
                                        class="public-chat-tool-toggle"
                                        type="button"
                                        @click="
                                          toggleToolCollapsed(segment.toolId)
                                        "
                                      >
                                        {{
                                          isToolCollapsed(segment.toolId)
                                            ? $t('bot.publicChatToolExpand')
                                            : $t('bot.publicChatToolCollapse')
                                        }}
                                      </button>
                                    </div>
                                  </div>
                                  <pre
                                    v-if="
                                      !isToolCollapsed(segment.toolId) &&
                                      getToolById(item, segment.toolId)
                                        ?.arguments
                                    "
                                    class="public-chat-tool-content"
                                    v-text="
                                      getToolById(item, segment.toolId)
                                        ?.arguments
                                    "
                                  ></pre>
                                  <pre
                                    v-if="
                                      !isToolCollapsed(segment.toolId) &&
                                      getToolById(item, segment.toolId)
                                        ?.result &&
                                      getToolById(item, segment.toolId)
                                        ?.status === 'TOOL_RESULT'
                                    "
                                    class="public-chat-tool-content"
                                    v-text="
                                      getToolById(item, segment.toolId)?.result
                                    "
                                  ></pre>
                                </div>
                              </div>
                            </template>
                          </template>
                        </div>
                        <span
                          v-if="item.loading"
                          class="public-chat-dot-typing"
                        >
                          <i></i><i></i><i></i>
                        </span>
                      </template>
                    </template>
                  </div>

                  <ChatMessageActionBar
                    :align="item.role === 'user' ? 'end' : 'start'"
                    :allow-copy="canCopyMessage(item)"
                    :copy-error-message="$t('bot.publicChatCopyFail')"
                    :copy-text="String(item.content || '').trim()"
                    :allow-regenerate="
                      item.role === 'assistant' &&
                      getLatestAssistantMessage()?.id === item.id &&
                      Boolean(item.roundId)
                    "
                    :regenerate-disabled="sending"
                    :show-variant-navigator="shouldShowVariantNavigator(item)"
                    :variant-loading="isVariantSwitching(item)"
                    :disabled-variant-next="!canSwitchVariant(item, 'next')"
                    :disabled-variant-previous="
                      !canSwitchVariant(item, 'previous')
                    "
                    :variant-current="
                      Number(item.variantIndex || item.selectedVariantIndex || 1)
                    "
                    :variant-total="Number(item.variantCount || 1)"
                    @regenerate="handleRegenerateMessage(item)"
                    @select-next-variant="handleSelectVariant(item, 'next')"
                    @select-previous-variant="
                      handleSelectVariant(item, 'previous')
                    "
                  />
                </div>
              </div>
            </template>
          </template>
        </ElSkeleton>
      </main>

      <div ref="dockRef" class="public-chat-dock">
        <section
          v-if="!initializing && !initError && !blocked"
          class="public-chat-preset-list"
        >
          <button
            v-for="item in presetQuestions"
            :key="item.key"
            class="public-chat-preset-item"
            type="button"
            @click="usePresetQuestion(item.description)"
          >
            {{ item.description }}
          </button>
        </section>

        <footer class="public-chat-footer">
          <div class="public-chat-composer">
            <ElInput
              class="public-chat-input"
              v-model="senderValue"
              type="textarea"
              :rows="2"
              resize="none"
              :placeholder="$t('bot.publicChatPlaceholder')"
              :disabled="blocked || initializing || !!initError || sending"
              @keydown.enter="onEnter"
              @compositionstart="onCompositionStart"
              @compositionend="onCompositionEnd"
            />
            <span class="public-chat-footer-hint">
              {{ $t('bot.publicChatInputHint') }}
            </span>
            <ElButton
              v-if="sending"
              plain
              class="public-chat-input-send-btn is-stop"
              @click="stopStreaming"
            >
              <template #icon>
                <IconifyIcon icon="solar:stop-circle-outline" />
              </template>
              {{ $t('bot.publicChatStop') }}
            </ElButton>
            <ElButton
              v-else
              type="primary"
              class="public-chat-input-send-btn"
              :disabled="
                blocked || initializing || !!initError || !senderValue.trim()
              "
              @click="() => doSendMessage()"
            >
              <template #icon>
                <IconifyIcon icon="solar:plain-linear" />
              </template>
              {{ $t('bot.publicChatSend') }}
            </ElButton>
          </div>
        </footer>
      </div>
    </div>
  </div>
</template>

<style scoped>
.public-chat-page {
  --pc-primary: #2563eb;
  --pc-primary-soft: #dbeafe;
  --pc-ink-strong: #0f172a;
  --pc-ink: #1e293b;
  --pc-ink-soft: #475569;
  --pc-line: #e2e8f0;
  --pc-line-soft: #edf2f7;
  --pc-surface: #fff;
  --pc-surface-soft: #f8fbff;
  --pc-surface-muted: #f8fafc;

  position: relative;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 24px;
  overflow: hidden;
  background:
    radial-gradient(
      1200px 360px at 100% -20%,
      rgb(37 99 235 / 14%) 0%,
      transparent 60%
    ),
    radial-gradient(
      900px 300px at -10% 120%,
      rgb(14 165 233 / 12%) 0%,
      transparent 62%
    ),
    linear-gradient(180deg, #f8fbff 0%, #f3f7fc 100%);
  isolation: isolate;
}

.public-chat-page::before {
  position: absolute;
  inset: -30% auto auto -20%;
  z-index: -1;
  width: 440px;
  height: 440px;
  pointer-events: none;
  content: '';
  background: radial-gradient(
    circle,
    rgb(59 130 246 / 16%) 0%,
    transparent 68%
  );
  filter: blur(8px);
}

.public-chat-page-embed {
  min-height: 100%;
  padding: 0;
  background: var(--pc-surface-soft);
}

.public-chat-page-embed::before {
  content: none;
}

.public-chat-shell {
  position: relative;
  display: flex;
  flex-direction: column;
  max-width: 980px;
  height: calc(100vh - 48px);
  margin: 0 auto;
  overflow: hidden;
  background: var(--pc-surface);
  border: 1px solid var(--pc-line);
  border-radius: 20px;
  box-shadow:
    0 2px 6px rgb(15 23 42 / 5%),
    0 24px 48px rgb(15 23 42 / 8%);
}

.public-chat-page-embed .public-chat-shell {
  height: 100vh;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.public-chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px;
  background: rgb(255 255 255 / 92%);
  border-bottom: 1px solid var(--pc-line-soft);
  backdrop-filter: saturate(130%) blur(6px);
}

.public-chat-title-group {
  display: flex;
  gap: 12px;
  align-items: center;
}

.public-chat-title-wrap {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.public-chat-title {
  margin: 0;
  font-family: Manrope, 'Noto Sans SC', sans-serif;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--pc-ink-strong);
  letter-spacing: 0.01em;
}

.public-chat-subtitle {
  margin: 0;
  font-size: 12px;
  color: #64748b;
  letter-spacing: 0.02em;
}

.public-chat-messages {
  flex: 1;
  padding: 20px 24px var(--public-chat-dock-space, 180px);
  overflow-y: auto;
  background: rgb(255 255 255);
}

.public-chat-banner {
  padding: 14px 16px;
  font-size: 14px;
  line-height: 1.5;
  border-radius: 10px;
}

.public-chat-banner-title {
  margin: 0 0 4px;
  font-weight: 600;
}

.public-chat-banner-error {
  color: #b42318;
  background: #fff1f2;
  border: 1px solid #fecdd3;
}

.public-chat-banner-warning {
  color: #9a3412;
  background: #fffbeb;
  border: 1px solid #fed7aa;
}

.public-chat-banner-action {
  margin-top: 12px;
}

.public-chat-message-row {
  display: flex;
  margin-bottom: 18px;
  animation: message-fade-in 0.22s ease;
}

.public-chat-message-row.is-user {
  justify-content: flex-end;
}

.public-chat-message-row.is-assistant {
  gap: 10px;
  align-items: flex-start;
  justify-content: flex-start;
}

.public-chat-message-avatar {
  flex: 0 0 auto;
  margin-top: 1px;
}

.public-chat-message {
  min-width: 0;
  max-width: min(88%, 760px);
}

.public-chat-bubble {
  display: inline-block;
  min-width: 72px;
  min-height: 44px;
  padding: 11px 14px;
  font-size: 14px;
  line-height: 1.7;
  white-space: pre-wrap;
  border-radius: 12px;
}

.public-chat-bubble-user {
  color: #fff;
  background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
  border-top-right-radius: 4px;
  box-shadow: 0 6px 16px rgb(37 99 235 / 24%);
}

.public-chat-bubble-assistant {
  display: block;
  width: 100%;
  min-width: 0;
  min-height: auto;
  padding: 0;
  color: #0f172a;
  background: transparent;
  border: 0;
  border-radius: 0;
}

.public-chat-dot-typing {
  display: inline-flex;
  gap: 4px;
  margin-left: 6px;
}

.public-chat-dot-typing i {
  width: 4px;
  height: 4px;
  background: #64748b;
  border-radius: 50%;
  animation: dot-pulse 1.2s infinite ease-in-out;
}

.public-chat-dot-typing i:nth-child(2) {
  animation-delay: 0.2s;
}

.public-chat-dot-typing i:nth-child(3) {
  animation-delay: 0.4s;
}

.public-chat-segment {
  margin-bottom: 10px;
}

.public-chat-segment:last-child {
  margin-bottom: 0;
}

.public-chat-segment.is-text {
  white-space: normal;
}

.public-chat-segment.is-thinking {
  display: block;
}

.public-chat-segment.is-tool {
  display: block;
  margin: 12px 0;
}

.public-chat-waiting {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  color: var(--pc-ink-soft);
}

.public-chat-waiting-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid #bfdbfe;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: waiting-spin 0.8s linear infinite;
}

.public-chat-tool-segment {
  width: 560px;
  max-width: 560px;
}

.public-chat-message-row.is-assistant .public-chat-tool-segment {
  max-width: min(560px, 100%);
}

.public-chat-tool-item {
  width: 100%;
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.45;
  background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
  border: 1px solid #d7e2f2;
  border-left: 3px solid var(--pc-primary);
  border-radius: 12px;
  box-shadow: 0 2px 6px rgb(15 23 42 / 5%);
}

.public-chat-thinking-block {
  margin-bottom: 8px;
}

.public-chat-tool-header {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
}

.public-chat-tool-actions {
  display: inline-flex;
  gap: 8px;
  align-items: center;
}

.public-chat-tool-name {
  display: inline-flex;
  gap: 6px;
  align-items: center;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  font-weight: 600;
  color: var(--pc-ink-strong);
  white-space: nowrap;
}

.public-chat-tool-status {
  flex-shrink: 0;
  padding: 2px 8px;
  font-size: 11px;
  line-height: 1.25;
  border: 1px solid transparent;
  border-radius: 999px;
}

.public-chat-tool-status.is-calling {
  color: #1d4ed8;
  background: #eff6ff;
  border-color: #bfdbfe;
}

.public-chat-tool-status.is-done {
  color: #15803d;
  background: #ecfdf3;
  border-color: #bbf7d0;
}

.public-chat-tool-toggle {
  padding: 3px 10px;
  font-size: 11px;
  line-height: 1.4;
  color: #334155;
  cursor: pointer;
  background: #fff;
  border: 1px solid #d7e2f2;
  border-radius: 999px;
  transition: all 0.15s ease;
}

.public-chat-tool-toggle:hover {
  color: #1d4ed8;
  border-color: #bfdbfe;
}

.public-chat-tool-content {
  max-height: 160px;
  padding: 9px 10px;
  margin: 8px 0 0;
  overflow: auto;
  font-family:
    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
    'Courier New', monospace;
  font-size: 12px;
  line-height: 1.5;
  color: #334155;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
}

.public-chat-dock {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 12;
  padding: 0 24px 18px;
  pointer-events: none;
}

.public-chat-preset-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0 4px;
  margin-bottom: 10px;
  pointer-events: auto;
  background: transparent;
  border-top: 0;
}

.public-chat-preset-item {
  max-width: 100%;
  padding: 8px 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  color: #334155;
  white-space: nowrap;
  cursor: pointer;
  background: rgb(255 255 255 / 96%);
  border: 1px solid #dbe3ef;
  border-radius: 999px;
  box-shadow: 0 4px 12px rgb(15 23 42 / 8%);
  transition: all 0.18s ease;
}

.public-chat-preset-item:hover {
  color: #1d4ed8;
  background: #eff6ff;
  border-color: #bfdbfe;
}

.public-chat-footer {
  padding: 0 10px;
  pointer-events: auto;
  background: transparent;
  border-top: 0;
}

.public-chat-composer {
  position: relative;
  padding: 0 0 4px;
  overflow: hidden;
  background: rgb(255 255 255);
  border: 1px solid #dce5f1;
  border-radius: 22px;
  box-shadow:
    0 8px 26px rgb(15 23 42 / 10%),
    0 1px 0 rgb(255 255 255 / 95%) inset;
}

.public-chat-footer-hint {
  position: absolute;
  bottom: 16px;
  left: 18px;
  z-index: 1;
  font-size: 12px;
  color: #94a3b8;
  pointer-events: none;
  opacity: 0.9;
}

.public-chat-input-send-btn {
  position: absolute;
  right: 14px;
  bottom: 14px;
  z-index: 2;
  height: 40px;
  padding: 0 15px;
  border-radius: 999px;
}

.public-chat-input-send-btn.is-stop {
  padding-right: 12px;
  padding-left: 12px;
}

.public-chat-input :deep(.el-textarea__inner) {
  min-height: 126px !important;
  padding: 16px 124px 48px 18px;
  font-size: 14px;
  line-height: 1.6;
  background: rgb(255 255 255);
  border-color: transparent;
  border-radius: 20px;
  box-shadow: none;
  transition:
    border-color 0.2s ease,
    box-shadow 0.2s ease;
}

.public-chat-input :deep(.el-textarea) {
  background: rgb(255 255 255);
}

.public-chat-input :deep(.el-textarea__inner:focus) {
  border-color: transparent;
  box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
}

@keyframes message-fade-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes dot-pulse {
  0%,
  80%,
  100% {
    opacity: 0.5;
    transform: scale(0.75);
  }

  40% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes waiting-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (width <= 768px) {
  .public-chat-page {
    padding: 0;
  }

  .public-chat-shell {
    height: 100vh;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .public-chat-header,
  .public-chat-messages {
    padding-right: 14px;
    padding-left: 14px;
  }

  .public-chat-dock {
    padding-right: 14px;
    padding-bottom: 12px;
    padding-left: 14px;
  }

  .public-chat-preset-list {
    gap: 6px;
    padding-right: 2px;
    padding-left: 2px;
    margin-bottom: 8px;
  }

  .public-chat-footer {
    padding-right: 6px;
    padding-left: 6px;
  }

  .public-chat-message {
    max-width: 94%;
  }

  .public-chat-input :deep(.el-textarea__inner) {
    min-height: 118px !important;
    padding-right: 118px;
    padding-bottom: 46px;
  }
}
</style>
