<template>
	<div ref="chatContentRef" class="chat-content">
		<BubbleList
			v-if="props.messages.length > 0"
			ref="bubbleListRef"
			class="item-list"
			:list="bubbleListItems"
			:auto-scroll="autoScrollEnabled"
			max-height="100%"
			show-back-button
			:btn-loading="loading"
			:back-button-threshold="120"
		>
			<template #avatar="{ item }">
				<div class="avatar-wrapper">
					<Icon v-if="item.role === 'ai'" icon="mingcute:ai-line" :width="22" />
					<Icon v-if="item.role === 'user'" icon="ri:user-line" :width="22" />
				</div>
			</template>

			<template #header>
				<div class="header-wrapper">
					<div class="header-name"></div>
				</div>
			</template>

			<template #content="{ item }">
				<div class="msg-content-wrapper">
					<div v-if="item.role === 'ai' && hasStructuredBlocks(item)" class="structured-blocks">
						<Thinking
							v-if="shouldShowThinking(item)"
							:content="getThinkingText(item)"
							:status="getThinkingStatus(item)"
							:model-value="isThinkingOpen(item)"
							auto-collapse
							max-width="100%"
							background-color="var(--bg-container)"
							color="var(--text-primary)"
						>
							<template #content="{ content }">
								<SMarkdown :markdown="content" allow-html enableLatex enableBreaks />
							</template>
						</Thinking>

						<div v-if="getToolCalls(item).length" class="structured-block">
							<div class="structured-block__head">
								<span class="structured-block__title">{{ t('pages.chat.toolCallTitle') }}</span>
								<el-tag size="small" type="warning" effect="plain">{{
									getToolCalls(item).length
								}}</el-tag>
							</div>
							<div class="tool-list">
								<div
									v-for="(toolCall, index) in getToolCalls(item)"
									:key="`${item.id || item.key}-tool-call-${index}`"
									class="tool-card"
								>
									<div class="tool-card__head">
										<div class="tool-card__meta">
											<el-tag size="small" type="warning" effect="dark">
												{{ getToolName(toolCall.toolName) }}
											</el-tag>
											<span v-if="toolCall.toolCallId" class="tool-card__id">{{
												toolCall.toolCallId
											}}</span>
										</div>
									</div>
									<pre class="tool-card__content">{{
										formatStructuredValue(toolCall.arguments || toolCall.content)
									}}</pre>
								</div>
							</div>
						</div>

						<div v-if="getToolResults(item).length" class="structured-block">
							<div class="structured-block__head">
								<span class="structured-block__title">{{ t('pages.chat.toolResultTitle') }}</span>
								<el-tag size="small" type="success" effect="plain">{{
									getToolResults(item).length
								}}</el-tag>
							</div>
							<div class="tool-list">
								<div
									v-for="(toolResult, index) in getToolResults(item)"
									:key="`${item.id || item.key}-tool-result-${index}`"
									class="tool-card"
								>
									<div class="tool-card__head">
										<div class="tool-card__meta">
											<el-tag
												:type="toolResult.success === false ? 'danger' : 'success'"
												size="small"
												effect="dark"
											>
												{{
													toolResult.success === false
														? t('pages.chat.failedTag')
														: t('pages.chat.successTag')
												}}
											</el-tag>
											<span class="tool-card__name">{{ getToolName(toolResult.toolName) }}</span>
											<span v-if="toolResult.toolCallId" class="tool-card__id">{{
												toolResult.toolCallId
											}}</span>
										</div>
										<div v-if="toolResult.durationMs !== undefined" class="tool-card__duration">
											{{ toolResult.durationMs }}ms
										</div>
									</div>
									<div v-if="hasToolResultThought(toolResult)" class="tool-card__section">
										<div class="tool-card__label">{{ t('pages.chat.thinkTitle') }}</div>
										<pre class="tool-card__content">
										<SMarkdown :markdown="getToolResultThought(toolResult)"/>
									</pre>
									</div>
									<div v-if="hasToolResultOutput(toolResult)" class="tool-card__section">
										<div class="tool-card__label">{{ t('pages.chat.toolResultTitle') }}</div>
										<pre class="tool-card__content">
												<SMarkdown :markdown="getToolResultOutput(toolResult)"/>
											</pre>
									</div>
									<div v-if="hasToolResultItems(toolResult)" class="tool-card__section">
										<div class="tool-card__label">content_items</div>
										<pre class="tool-card__content">
												<SMarkdown :markdown="formatStructuredValue(getToolResultItems(toolResult))"/>
											</pre>
									</div>
									<div v-if="toolResult.error" class="tool-card__section">
										<div class="tool-card__label">{{ t('pages.chat.failedTag') }}</div>
										<pre class="tool-card__content tool-card__content--error">{{
											toolResult.error
										}}</pre>
									</div>
								</div>
							</div>
						</div>
					</div>

					<div v-if="getInlineErrors(item).length" class="inline-errors">
						<div v-for="(err, idx) in getInlineErrors(item)" :key="idx" class="inline-error-item">
							<el-tag type="danger" size="small" effect="plain" class="inline-error-tag">
								{{ t('pages.chat.failedTag') }}
							</el-tag>
							<span class="inline-error-text">{{ err }}</span>
						</div>
					</div>

					<slot name="message-extra" :item="item" />
					<SMarkdown
						v-if="getDisplayText(item)"
						:class="['msg-content-text', { 'msg-content-text--error': isErrorMessage(item) }]"
						:markdown="getDisplayText(item)"
						allow-html
						enableLatex
						enableBreaks
						need-view-code-btn
						@card-submit="(val) => emit('card-submit', val)"
					/>

					<div v-if="isStreamingMessage(item)" class="ml-10px w-fit">
						<div class="elx-bubble__loading-wrap">
							<div
								v-for="(_, index) in 3"
								:key="index"
								class="elx-bubble__dot"
								:style="{ animationDelay: `${index * 0.2}s` }"
							/>
						</div>
					</div>

					<div v-if="item?.stopped" class="msg-stop-indicator">
						{{ t('pages.chat.stopped') }}
					</div>
				</div>
			</template>

			<template #footer="{ item }">
				<div v-if="getMessageImages(item).length" class="file-wrap">
					<el-image
						v-for="(src, index) in getMessageImages(item)"
						:key="`${item.id || item.key}-file-${index}`"
						:src="src"
						class="file-image"
						fit="cover"
						preview-teleported
						hide-on-click-modal
						:preview-src-list="getMessageImages(item)"
					/>
				</div>
				<div class="footer-wrapper" v-if="item.role === 'ai' && item.streamCompleted">
					<div class="footer-container">
						<!-- <el-button
								type="info"
								:icon="Refresh"
								size="small"
								circle
								@click="handleRetry(item)"
							/> -->
						<el-tooltip :content="t('pages.chat.copy')">
							<el-button
								v-if="props.copy"
								color="#626aef"
								:icon="DocumentCopy"
								size="small"
								circle
								@click="handleCopy(item)"
							/>
						</el-tooltip>
						<el-tooltip :content="t('pages.chat.addToKnowledgeBase')">
							<el-button
								v-if="props.canAddToKb"
								type="success"
								:icon="DocumentAdd"
								size="small"
								circle
								@click="handleAddToKb(item)"
							/>
						</el-tooltip>
					</div>
					<div class="footer-time">
						{{ item.updateTime }}
						<span v-if="item.total_duration_ms">{{
							t('pages.chat.totalDuration', { duration: item.total_duration_ms })
						}}</span>
					</div>
				</div>
			</template>
		</BubbleList>

		<div v-else class="empty-state">
			<div class="empty-icon">
				<SvgIcon name="sparkle" size="60" />
			</div>
			<div class="empty-text">{{ t('pages.chat.emptyTitle') }}</div>
			<div class="empty-subtext">{{ t('pages.chat.emptySubtitle') }}</div>
			<slot />
		</div>
	</div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { BubbleList, Thinking } from 'vue-element-plus-x'
import { useI18n } from '@/composables/useI18n'
import { ElMessage } from 'element-plus'
import { DocumentCopy, DocumentAdd } from '@element-plus/icons-vue'
import { Icon } from '@repo/ui'
import SMarkdown from './SMarkdown.vue'
import type { BubbleMessage, ChatToolResult } from '../../views/chat/types'

const { t } = useI18n()

const props = withDefaults(
	defineProps<{
		messages: BubbleMessage[]
		loading: boolean
		copy?: boolean
		canAddToKb?: boolean
	}>(),
	{
		canAddToKb: true,
		copy: true
	}
)

const emit = defineEmits<{
	retry: [message: BubbleMessage]
	addToKb: [text: string]
	'card-submit': [text: string]
}>()

const getMessageImages = (message: BubbleMessage) => {
	const files = message.message_files
	const ids = Array.isArray(files) ? files : files ? [files] : []
	return ids.filter(Boolean).map((fileId) => `/File/GetImage?fileId=${fileId}`)
}

const bubbleListRef = ref<{
	scrollToTop: () => void
	scrollToBottom: () => void
	scrollToBubble: (index: number) => void
} | null>(null)
const chatContentRef = ref<HTMLElement | null>(null)
const autoScrollEnabled = ref(true)
let scrollFrameId: number | null = null
let scrollResizeObserver: ResizeObserver | null = null
let scrollMutationObserver: MutationObserver | null = null
let scrollStabilityTimerId: number | null = null
let iframeLoadController: AbortController | null = null
let boundScrollContainer: HTMLElement | null = null
let isProgrammaticScroll = false
const scrollTimeoutIds = new Set<number>()
const AUTO_SCROLL_BOTTOM_THRESHOLD = 120
const SCROLL_STABILITY_MS = 10000

const bubbleListItems = computed(() =>
	props.messages.map((item) => ({
		...item,
		maxWidth: item.role === 'ai' ? 'min(920px, calc(100% - 56px))' : 'min(640px, calc(100% - 56px))'
	}))
)

const clearScheduledScroll = () => {
	if (scrollFrameId !== null) {
		window.cancelAnimationFrame(scrollFrameId)
		scrollFrameId = null
	}
	scrollResizeObserver?.disconnect()
	scrollResizeObserver = null
	scrollMutationObserver?.disconnect()
	scrollMutationObserver = null
	if (scrollStabilityTimerId !== null) {
		window.clearTimeout(scrollStabilityTimerId)
		scrollStabilityTimerId = null
	}
	scrollTimeoutIds.forEach((id) => window.clearTimeout(id))
	scrollTimeoutIds.clear()

	iframeLoadController?.abort()
	iframeLoadController = null
}

const unbindScrollContainer = () => {
	boundScrollContainer?.removeEventListener('scroll', handleScroll)
	boundScrollContainer = null
}

const getScrollContainer = () => {
	const root = chatContentRef.value
	if (!root) return null
	return root.querySelector<HTMLElement>('.el-bubble-list')
}

const getDistanceToBottom = (scrollContainer: HTMLElement) =>
	scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight

const updateAutoScrollState = () => {
	const scrollContainer = getScrollContainer()
	if (!scrollContainer) {
		autoScrollEnabled.value = true
		return
	}

	autoScrollEnabled.value = getDistanceToBottom(scrollContainer) <= AUTO_SCROLL_BOTTOM_THRESHOLD
}

function handleScroll() {
	// 程序化滚动触发的事件不更新自动滚动状态
	if (isProgrammaticScroll) return
	updateAutoScrollState()
	if (!autoScrollEnabled.value) {
		clearScheduledScroll()
	}
}

const bindScrollContainer = async () => {
	await nextTick()
	const scrollContainer = getScrollContainer()
	if (scrollContainer === boundScrollContainer) {
		updateAutoScrollState()
		return
	}

	unbindScrollContainer()
	boundScrollContainer = scrollContainer
	boundScrollContainer?.addEventListener('scroll', handleScroll, { passive: true })
	updateAutoScrollState()
}

const scrollToBottomOnce = () => {
	isProgrammaticScroll = true
	bubbleListRef.value?.scrollToBottom()

	const scrollContainer = getScrollContainer()
	if (scrollContainer) {
		scrollContainer.scrollTop = scrollContainer.scrollHeight
	}
	autoScrollEnabled.value = true
	// 延迟释放标志，确保 scroll 事件在当前帧内不会误判
	requestAnimationFrame(() => {
		isProgrammaticScroll = false
	})
}

const keepBottomDuringLayout = () => {
	const scrollContainer = getScrollContainer()
	if (!scrollContainer) return

	// ResizeObserver: 观察滚动容器本身和直接子元素的尺寸变化
	scrollResizeObserver?.disconnect()
	scrollResizeObserver = new ResizeObserver(() => {
		if (autoScrollEnabled.value) {
			scrollToBottomOnce()
			resetStabilityTimer()
		}
	})
	scrollResizeObserver.observe(scrollContainer)
	scrollContainer.querySelectorAll('.el-bubble').forEach((el) => {
		scrollResizeObserver?.observe(el)
	})

	// MutationObserver: 观察整个子树的 DOM 变化（Markdown 渲染、图片加载、虚拟滚动新增项等）
	scrollMutationObserver?.disconnect()
	scrollMutationObserver = new MutationObserver(() => {
		if (autoScrollEnabled.value) {
			scrollToBottomOnce()
			resetStabilityTimer()
		}
		// 动态观察新增的子元素（虚拟滚动可能动态添加）
		scrollContainer.querySelectorAll('.el-bubble').forEach((el) => {
			scrollResizeObserver?.observe(el)
		})
	})
	scrollMutationObserver.observe(scrollContainer, {
		attributes: true,
		attributeFilter: ['style'],
		characterData: true,
		childList: true,
		subtree: true
	})

	// 内容稳定后自动断开（无 DOM 变化 3s 后停止观察，避免长期性能开销）

	// iframe 加载完成后重置稳定性计时器，让观察器继续等待高度测量完成
	iframeLoadController?.abort()
	iframeLoadController = new AbortController()
	scrollContainer.addEventListener(
		'load',
		(e) => {
			if ((e.target as HTMLElement)?.tagName === 'IFRAME' && autoScrollEnabled.value) {
				// 不立刻滚动（iframe 高度还在测量中），但重置计时器保持观察器活跃
				resetStabilityTimer()
			}
		},
		{ capture: true, signal: iframeLoadController.signal }
	)

	// 启动稳定性计时器，内容无变化后自动断开观察器释放性能
	resetStabilityTimer()
}

const resetStabilityTimer = () => {
	if (scrollStabilityTimerId !== null) {
		window.clearTimeout(scrollStabilityTimerId)
	}
	scrollStabilityTimerId = window.setTimeout(() => {
		scrollStabilityTimerId = null
		scrollResizeObserver?.disconnect()
		scrollResizeObserver = null
		scrollMutationObserver?.disconnect()
		scrollMutationObserver = null
	}, SCROLL_STABILITY_MS)
}

const scrollToBottom = async () => {
	clearScheduledScroll()
	await nextTick()

	scrollToBottomOnce()
	keepBottomDuringLayout()

	scrollFrameId = window.requestAnimationFrame(() => {
		scrollFrameId = null
		if (autoScrollEnabled.value) {
			scrollToBottomOnce()
			keepBottomDuringLayout()
		}
	})

	for (const delay of [60, 180, 360, 1000, 3000, 6000]) {
		const timeoutId = window.setTimeout(() => {
			scrollTimeoutIds.delete(timeoutId)
			if (autoScrollEnabled.value) {
				scrollToBottomOnce()
			}
		}, delay)
		scrollTimeoutIds.add(timeoutId)
	}
}

watch(
	() => props.messages.length,
	(length, oldLength) => {
		bindScrollContainer()
		if (length > 0 && length !== oldLength) {
			if (autoScrollEnabled.value) {
				scrollToBottom()
			}
		}
	},
	{ flush: 'post' }
)

onMounted(() => {
	bindScrollContainer()
})

onBeforeUnmount(() => {
	clearScheduledScroll()
	unbindScrollContainer()
})

const THINK_TAG_RE = /<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/gi

const parseThinkContent = (content?: string) => {
	const text = content || ''
	const thinkingParts: string[] = []
	const displayContent = text
		.replace(THINK_TAG_RE, (_match, thinkingText) => {
			const trimmed = `${thinkingText || ''}`.trim()
			if (trimmed) thinkingParts.push(trimmed)
			return ''
		})
		.trim()

	return {
		displayContent,
		thinkingText: thinkingParts.join('\n\n')
	}
}

const parseMessageError = (message: BubbleMessage) => {
	if (message.error) {
		return `${message.error}`.trim()
	}

	const text = `${message.content || ''}`.trim()
	if (!text.startsWith('{') || !text.endsWith('}')) {
		return ''
	}

	try {
		const parsed = JSON.parse(text)
		if (parsed?.isSuccess === false) {
			return `${parsed.error || parsed.errors?.message || ''}`.trim()
		}
	} catch {
		return ''
	}

	return ''
}

const getEmbeddedThinkingText = (message: BubbleMessage) =>
	parseThinkContent(message.content).thinkingText

const getDisplayContent = (message: BubbleMessage) =>
	parseThinkContent(message.content).displayContent

const getDisplayText = (message: BubbleMessage) =>
	parseMessageError(message) || getDisplayContent(message)

const isErrorMessage = (message: BubbleMessage) => !!parseMessageError(message)

const isStreamingMessage = (message: BubbleMessage) =>
	message.role === 'ai' && !message.streamCompleted && !message.stopped

const getThinkingText = (message: BubbleMessage) => {
	const parts = [`${message.thinking || ''}`.trim(), getEmbeddedThinkingText(message)].filter(
		Boolean
	)
	return parts.join('\n\n')
}

const shouldShowThinking = (message: BubbleMessage) => !!getThinkingText(message)

const isThinkingOpen = (message: BubbleMessage) => message.thinkingOpen ?? !message.streamCompleted

const getThinkingStatus = (message: BubbleMessage) => {
	if (message.error) return 'error'
	if (getInlineErrors(message).length) return 'error'
	return message.streamCompleted ? 'end' : 'thinking'
}

const getToolCalls = (message: BubbleMessage) => message.toolCalls || []

const getToolResults = (message: BubbleMessage) => message.toolResults || []

const getToolName = (name?: string) => `${name || 'tool'}`

const getToolResultThought = (toolResult: ChatToolResult) => `${toolResult.thought || ''}`.trim()

const hasToolResultThought = (toolResult: ChatToolResult) => !!getToolResultThought(toolResult)

const getToolResultOutput = (toolResult: ChatToolResult) => formatStructuredValue(toolResult.output)

const hasToolResultOutput = (toolResult: ChatToolResult) => {
	const output = toolResult.output
	return output !== null && output !== undefined && `${output}`.trim() !== ''
}

const getToolResultItems = (toolResult: ChatToolResult) =>
	Array.isArray(toolResult.contentItems) ? toolResult.contentItems : []

const hasToolResultItems = (toolResult: ChatToolResult) => getToolResultItems(toolResult).length > 0

const hasStructuredBlocks = (message: BubbleMessage) =>
	!!(getThinkingText(message) || getToolCalls(message).length || getToolResults(message).length)

const formatStructuredValue = (value: any) => {
	if (value === null || value === undefined || value === '') return '-'
	if (typeof value === 'string') return value
	try {
		return JSON.stringify(value, null, 2)
	} catch {
		return `${value}`
	}
}

const getInlineErrors = (message: BubbleMessage) => message.inlineErrors || []

defineExpose({
	scrollToTop: () => bubbleListRef.value?.scrollToTop(),
	scrollToBottom,
	isNearBottom: () => autoScrollEnabled.value,
	scrollToBubble: (index: number) => bubbleListRef.value?.scrollToBubble(index)
})

const handleCopy = (message: BubbleMessage) => {
	const text = `${message.rawText || message.answerText || message.content || ''}`
	window.navigator.clipboard.writeText(text)
	ElMessage.success(t('pages.chat.copySuccess'))
}

const handleAddToKb = (message: BubbleMessage) => {
	const text = `${message.rawText || message.answerText || message.content || ''}`
	emit('addToKb', text)
}
</script>

<style lang="less" scoped>
:deep(.el-thinking) {
	margin: 0;
}
.chat-content {
	flex: 1;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	padding: 18px;

	.item-list {
		flex: 1;
		min-height: 0;
	}

	.item-list :deep(.el-bubble-list) {
		height: 100%;
		padding-right: 4px;
		scroll-behavior: auto !important;
	}

	.item-list :deep(.el-bubble + .el-bubble) {
		margin-top: 16px;
	}

	.empty-state {
		flex: 1;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		color: var(--text-tertiary);
	}
}

.avatar-wrapper {
	width: 40px;
	height: 40px;

	img {
		width: 100%;
		height: 100%;
		border-radius: 50%;
	}
}

.header-wrapper {
	.header-name {
		font-size: 14px;
		color: var(--text-tertiary);
	}
}

.msg-content-wrapper {
	display: flex;
	flex-direction: column;
	gap: 12px;
	width: 100%;
	min-width: 0;

	.msg-content-text {
		font-size: 14px;
		line-height: 1.7;
		color: var(--text-primary);
		word-break: break-word;
	}

	.msg-content-text--error {
		color: var(--el-color-danger);
	}

	.msg-stop-indicator {
		font-size: 12px;
		line-height: 1.4;
		color: var(--text-tertiary);
	}

	.msg-stream-indicator {
		display: inline-flex;
		align-items: center;
		align-self: flex-start;
		gap: 6px;
		font-size: 12px;
		line-height: 1.4;
		color: var(--text-tertiary);
	}

	.msg-stream-dots {
		display: inline-flex;
		gap: 3px;

		i {
			width: 4px;
			height: 4px;
			border-radius: 50%;
			background: currentColor;
			animation: msg-stream-dot 1s ease-in-out infinite;
		}

		i:nth-child(2) {
			animation-delay: 0.15s;
		}

		i:nth-child(3) {
			animation-delay: 0.3s;
		}
	}

	.msg-content-text :deep(h3) {
		margin: 0 0 10px;
		font-size: 12px;
		font-weight: 700;
		color: var(--text-secondary);
	}

	.msg-content-text :deep(p) {
		margin: 10px 0;
		white-space: pre-wrap;
	}

	.msg-content-text :deep(blockquote) {
		margin: 8px 0;
		padding: 8px 12px;
		border-left: 4px solid var(--el-color-primary);
		background: var(--bg-container);
		border-radius: 0 8px 8px 0;
		color: var(--text-secondary);
		white-space: pre-wrap;
	}

	.msg-content-text :deep(pre) {
		margin: 8px 0;
		padding: 10px 12px;
		border-radius: 8px;
		background: var(--bg-container);
		white-space: pre-wrap;
		overflow: auto;
	}

	.msg-content-text :deep(code) {
		font-size: 13px;
	}

	.msg-content-text :deep(ul),
	.msg-content-text :deep(ol) {
		margin: 0 0 10px 20px;
		padding: 0;
	}

	.msg-content-text :deep(li) {
		margin: 4px 0;
	}
}

@keyframes msg-stream-dot {
	0%,
	80%,
	100% {
		opacity: 0.35;
		transform: translateY(0);
	}

	40% {
		opacity: 1;
		transform: translateY(-2px);
	}
}

.structured-blocks {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.structured-block {
	padding: 12px;
	border: 1px solid var(--border-light);
	border-radius: 8px;
	background: var(--bg-container);
}

.structured-block__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 10px;
}

.structured-block__title {
	font-size: 13px;
	font-weight: 600;
	color: var(--text-secondary);
}

.structured-block__body {
	font-size: 13px;
	line-height: 1.7;
	color: var(--text-primary);
}

.structured-block__body--thinking {
	white-space: pre-wrap;
}

.structured-block__markdown :deep(p) {
	margin: 0 0 8px;
}

.tool-list {
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.tool-card {
	padding: 10px;
	border-radius: 8px;
	border: 1px solid var(--border-light);
	background: var(--bg-page);
}

.tool-card__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 8px;
}

.tool-card__meta {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	min-width: 0;
}

.tool-card__name,
.tool-card__id,
.tool-card__duration {
	font-size: 12px;
	color: var(--text-secondary);
	word-break: break-all;
}

.tool-card__section + .tool-card__section {
	margin-top: 8px;
}

.tool-card__label {
	margin-bottom: 4px;
	font-size: 12px;
	font-weight: 600;
	color: var(--text-secondary);
}

.tool-card__content {
	margin: 0;
	padding: 10px 12px;
	border-radius: 8px;
	background: var(--bg-container);
	color: var(--text-primary);
	font-size: 12px;
	line-height: 1.6;
	white-space: pre-wrap;
	word-break: break-word;
	overflow: auto;
}

.tool-card__content--error {
	color: var(--el-color-danger);
}

.footer-wrapper {
	display: flex;
	align-items: center;
	gap: 10px;

	.footer-time {
		font-size: 12px;
		margin-top: 3px;
	}
}

.file-wrap {
	display: flex;
	flex-wrap: wrap;
	margin-top: 8px;
}

.file-image {
	width: 96px;
	height: 96px;
	border-radius: 8px;
	object-fit: cover;
	margin-right: 8px;
	margin-bottom: 8px;
	border: 1px solid var(--border-light);
	cursor: zoom-in;
}

.footer-container {
	:deep(.el-button + .el-button) {
		margin-left: 8px;
	}
}

.loading-container {
	font-size: 14px;
	color: var(--text-primary);
	padding: 12px;
	background: var(--bg-container);
	border-radius: 15px;
	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.loading-container span {
	display: inline-block;
	margin-left: 8px;
}

@keyframes bounce {
	0%,
	100% {
		transform: translateY(5px);
	}

	50% {
		transform: translateY(-5px);
	}
}

.loading-container span:nth-child(4n) {
	animation: bounce 1.2s ease infinite;
}

.loading-container span:nth-child(4n + 1) {
	animation: bounce 1.2s ease infinite;
	animation-delay: 0.3s;
}

.loading-container span:nth-child(4n + 2) {
	animation: bounce 1.2s ease infinite;
	animation-delay: 0.6s;
}

.loading-container span:nth-child(4n + 3) {
	animation: bounce 1.2s ease infinite;
	animation-delay: 0.9s;
}
.inline-errors {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 8px 0;
}

.inline-error-item {
	display: flex;
	align-items: flex-start;
	gap: 8px;
	padding: 6px 10px;
	border-radius: 6px;
	background: rgba(245, 63, 63, 0.06);
	border: 1px solid rgba(245, 63, 63, 0.15);
}

.inline-error-text {
	font-size: 13px;
	color: var(--el-color-danger);
	line-height: 1.5;
	word-break: break-all;
}

.elx-bubble__loading-wrap {
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 5px;
}

.elx-bubble__dot {
	will-change: transform;
	width: 5px;
	height: 5px;
	background-color: var(--elx-bubble-dot-color, var(--el-color-primary));
	border-radius: 50%;
	animation: wave 1s infinite ease-in-out;
}

.dot-2 {
	animation-delay: 0.2s;
}

.dot-3 {
	animation-delay: 0.4s;
}

/* 波浪动画 */
@keyframes wave {
	0%,
	100% {
		transform: translateY(-2px);
	}

	50% {
		transform: translateY(2px);
	}
}
</style>
