You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1943 lines
47 KiB

<template>
<div class="wisdom-management">
<div class="wisdom-backdrop" aria-hidden="true">
<div class="wisdom-grid"></div>
<div class="wisdom-orbit wisdom-orbit-a"></div>
<div class="wisdom-orbit wisdom-orbit-b"></div>
<div class="wisdom-scan"></div>
</div>
<aside v-if="leftPanels.length && !artifactActive" class="wisdom-side wisdom-side-left">
<section v-for="panel in leftPanels" :key="panel.id" class="wisdom-side-panel" :class="`is-${panel.type}`">
<header v-if="!panel.chart" class="wisdom-side-header">
<div>
<span>{{ panel.kicker }}</span>
<strong>{{ panel.title }}</strong>
</div>
<em v-if="panel.badge">{{ panel.badge }}</em>
</header>
<div v-if="panel.items?.length" class="wisdom-side-metrics">
<div v-for="item in panel.items" :key="`${panel.id}-${item.label}`" class="wisdom-side-metric">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em v-if="item.note">{{ item.note }}</em>
</div>
</div>
<div v-if="panel.text" class="wisdom-side-text" v-html="renderMarkdown(panel.text)"></div>
<div v-if="panel.actions?.length" class="wisdom-side-actions">
<button
v-for="action in panel.actions"
:key="action.question"
type="button"
:disabled="loading"
@click="sendMessage(action.question)"
>
{{ action.label }}
</button>
</div>
</section>
</aside>
<aside v-if="rightPanels.length && !artifactActive" class="wisdom-side wisdom-side-right">
<section v-for="panel in rightPanels" :key="panel.id" class="wisdom-side-panel" :class="`is-${panel.type}`">
<header v-if="!panel.chart" class="wisdom-side-header">
<div>
<span>{{ panel.kicker }}</span>
<strong>{{ panel.title }}</strong>
</div>
<em v-if="panel.badge">{{ panel.badge }}</em>
</header>
<smart-chart-renderer
v-if="panel.chart"
class="wisdom-side-chart"
:chart="panel.chart"
/>
<div v-if="panel.items?.length" class="wisdom-side-metrics">
<div v-for="item in panel.items" :key="`${panel.id}-${item.label}`" class="wisdom-side-metric">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em v-if="item.note">{{ item.note }}</em>
</div>
</div>
<div v-if="panel.text" class="wisdom-side-text" v-html="renderMarkdown(panel.text)"></div>
<div v-if="panel.actions?.length" class="wisdom-side-actions">
<button
v-for="action in panel.actions"
:key="action.question"
type="button"
:disabled="loading"
@click="sendMessage(action.question)"
>
{{ action.label }}
</button>
</div>
</section>
</aside>
<section v-if="artifactActive" class="wisdom-artifact-workspace">
<header class="wisdom-artifact-header">
<div class="wisdom-artifact-heading">
<span class="wisdom-artifact-mark" aria-hidden="true"></span>
<div>
<small>专题分析</small>
<strong>{{ artifact.title }}</strong>
</div>
</div>
<div class="wisdom-artifact-tools">
<span class="wisdom-artifact-status" :class="`is-${artifact.status}`">
<i></i>{{ artifactStatusLabel }}
</span>
<button
v-if="artifact.status === 'ready' || artifact.status === 'error'"
type="button"
:disabled="artifactGenerating || !lastArtifactPayload"
title="重新生成当前界面"
@click="retryArtifactGeneration"
>
重新生成
</button>
</div>
</header>
<div class="wisdom-artifact-body">
<div v-if="artifactGenerating" class="wisdom-artifact-building">
<div class="wisdom-artifact-building__intro">
<span class="wisdom-artifact-building__spinner"></span>
<div>
<strong>{{ artifact.status === "collecting" ? "正在汇总业务数据" : "正在生成专题分析页面" }}</strong>
</div>
</div>
<div class="wisdom-artifact-skeleton is-wide"></div>
<div class="wisdom-artifact-skeleton-grid">
<div class="wisdom-artifact-skeleton"></div>
<div class="wisdom-artifact-skeleton"></div>
</div>
<div class="wisdom-artifact-skeleton is-chart"></div>
</div>
<iframe
v-else-if="artifact.html"
:key="artifact.renderKey"
ref="artifactFrameRef"
class="wisdom-artifact-frame"
:srcdoc="artifact.html"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
title="专题分析页面"
@load="handleArtifactFrameLoad"
></iframe>
<div v-else class="wisdom-artifact-error">
<strong>专题分析页面暂未生成</strong>
<p>{{ artifact.error || "当前数据不足以形成专题分析页面。" }}</p>
<button type="button" :disabled="!lastArtifactPayload" @click="retryArtifactGeneration">重新生成</button>
</div>
</div>
</section>
<main class="wisdom-chat" :class="{ 'has-artifact': artifactActive }">
<div ref="dialogueRef" class="wisdom-dialogue" :class="{ 'is-empty': !messages.length }">
<section v-if="!messages.length" class="wisdom-empty-state">
<div class="wisdom-empty-mark">
<span></span>
<strong>AI</strong>
</div>
<h2>智慧辅助分析</h2>
<p>{{ welcomeText }}</p>
</section>
<article
v-for="message in messages"
:key="message.id"
class="wisdom-message"
:class="`is-${message.role}`"
>
<div v-if="message.role === 'assistant'" class="wisdom-message-mark">
<span>智</span>
</div>
<div class="wisdom-bubble">
<template v-if="message.role === 'user'">
{{ message.text }}
</template>
<template v-else>
<div v-if="message.digesting" class="wisdom-digesting">
<span></span><span></span><span></span>
<em>{{ message.digestText || "正在汇总分析" }}</em>
</div>
<div v-if="message.visibleText" class="wisdom-answer" v-html="renderMarkdown(message.visibleText)"></div>
<span v-if="message.streaming" class="wisdom-stream-caret"></span>
<div v-if="message.suggestions?.length && (message.allowFollowUp || (!message.digesting && !message.streaming))" class="wisdom-suggestion-list">
<button
v-for="suggestion in message.suggestions"
:key="suggestion.question"
type="button"
:disabled="loading"
@click="sendMessage(suggestion.question)"
>
{{ suggestion.label }}
</button>
</div>
</template>
</div>
</article>
</div>
<form class="wisdom-input-bar" @submit.prevent="sendMessage()">
<div class="wisdom-input-shell">
<span class="wisdom-input-icon"></span>
<input
v-model="currentText"
:disabled="loading"
autocomplete="off"
placeholder="请输入需要了解的业务事项"
@keydown.enter.prevent="sendMessage()"
/>
<button
type="button"
class="wisdom-voice-button"
:class="{ 'is-listening': voiceListening }"
:disabled="loading || !voiceSupported"
:title="voiceSupported ? (voiceListening ? '停止语音输入' : '语音输入') : '当前浏览器不支持语音输入'"
:aria-label="voiceSupported ? (voiceListening ? '停止语音输入' : '语音输入') : '当前浏览器不支持语音输入'"
@click="toggleVoiceInput"
>
<span></span>
</button>
<button class="wisdom-submit-button" type="submit" :disabled="loading || !currentText.trim()">
<span v-if="loading" class="wisdom-button-loading"></span>
<span>{{ loading ? "分析中" : "发送" }}</span>
</button>
</div>
</form>
</main>
</div>
</template>
<script setup>
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import { buildSmartContextAnswer, buildSmartQuestionContext } from "@/services/smartContextBuilder"
import { buildLocalSmartArtifact, generateSmartArtifact } from "@/services/smartArtifactApi"
import { secureArtifactHtml } from "./artifactRuntime"
const SmartChartRenderer = defineAsyncComponent({
loader: () => import("./components/SmartChartRenderer.vue"),
loadingComponent: {
template: '<div class="smart-chart-loading">正在绘制统计图表</div>',
},
})
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const currentText = ref("")
const messages = ref([])
const loading = ref(false)
const dialogueRef = ref(null)
const artifactFrameRef = ref(null)
const leftPanels = ref([])
const rightPanels = ref([])
const artifact = ref(createEmptyArtifact())
const artifactActive = computed(() => artifact.value.status !== "idle")
const artifactGenerating = computed(() => ["collecting", "generating"].includes(artifact.value.status))
const artifactStatusLabel = computed(() => {
if (artifact.value.status === "collecting") return "汇总数据"
if (artifact.value.status === "generating") return "生成页面"
if (artifact.value.status === "ready") return "分析完成"
if (artifact.value.status === "error") return "生成失败"
if (artifact.value.status === "waiting") return "等待选择"
return "待生成"
})
const voiceSupported = ref(false)
const voiceListening = ref(false)
const voiceError = ref("")
const welcomeText = "请输入需要了解的业务事项,系统将依据平台汇总数据形成要点分析和专题页面。"
const ARTIFACT_SOFT_TIMEOUT = 28000
let streamTimer = null
let speechRecognition = null
let artifactRequestSerial = 0
const lastArtifactPayload = ref(null)
onMounted(() => {
initSpeechRecognition()
window.addEventListener("message", handleArtifactMessage)
})
onBeforeUnmount(() => {
stopStreaming()
stopVoiceInput()
artifactRequestSerial += 1
window.removeEventListener("message", handleArtifactMessage)
})
watch(
() => props.active,
(active) => {
if (active) return
stopStreaming()
stopVoiceInput()
artifactRequestSerial += 1
loading.value = false
messages.value = messages.value.map((message) =>
message.streaming ? { ...message, streaming: false, digesting: false } : message,
)
},
)
function createId() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}
function createEmptyArtifact() {
return {
status: "idle",
title: "动态分析",
question: "",
html: "",
error: "",
model: "",
fallback: false,
renderKey: 0,
}
}
function prepareArtifact(question) {
artifactRequestSerial += 1
lastArtifactPayload.value = null
artifact.value = {
...createEmptyArtifact(),
status: "collecting",
title: buildArtifactTitle(question),
question,
}
}
function buildArtifactTitle(question) {
const normalized = String(question || "动态分析").replace(/[??。!!]+$/g, "").trim()
return normalized.length > 28 ? `${normalized.slice(0, 28)}...` : normalized || "动态分析"
}
async function launchArtifactGeneration(payload) {
const normalizedPayload = {
question: payload?.question || artifact.value.question,
answer: payload?.answer || "",
charts: Array.isArray(payload?.charts) ? payload.charts : [],
datasets: Array.isArray(payload?.datasets) ? payload.datasets : [],
context: payload?.context || null,
pagePath: window.location.hash || window.location.pathname,
pageName: "智慧管理",
}
lastArtifactPayload.value = normalizedPayload
const requestId = ++artifactRequestSerial
artifact.value = {
...artifact.value,
status: "generating",
title: buildArtifactTitle(normalizedPayload.question),
question: normalizedPayload.question,
html: "",
error: "",
model: "",
fallback: false,
}
try {
const result = await withArtifactSoftTimeout(generateSmartArtifact(normalizedPayload))
if (requestId !== artifactRequestSerial) return { stale: true }
const securedHtml = secureArtifactHtml(result.html)
if (!securedHtml) throw new Error("生成页面未通过安全检查")
artifact.value = {
...artifact.value,
status: "ready",
html: securedHtml,
error: "",
model: result.model || "DeepSeek",
fallback: false,
renderKey: Date.now(),
}
return { ok: true, result }
} catch (error) {
if (requestId !== artifactRequestSerial) return { stale: true }
const errorMessage = getFriendlyErrorMessage(error, "专题分析页面生成暂时不可用,请稍后重试。")
console.error("[智慧管理] DeepSeek HTML生成失败", error)
const fallbackHtml = buildLocalArtifactHtml(normalizedPayload, errorMessage)
if (fallbackHtml) {
artifact.value = {
...artifact.value,
status: "ready",
html: fallbackHtml,
error: errorMessage,
model: "本地兜底",
fallback: true,
renderKey: Date.now(),
}
return { ok: false, fallback: true, message: errorMessage, error }
}
artifact.value = {
...artifact.value,
status: "error",
html: "",
error: errorMessage,
model: "",
fallback: false,
renderKey: Date.now(),
}
return { ok: false, message: errorMessage, error }
}
}
function withArtifactSoftTimeout(promise) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
const error = new Error("专题分析页面生成服务响应时间较长,已切换本地分析页面")
error.code = "ARTIFACT_SOFT_TIMEOUT"
reject(error)
}, ARTIFACT_SOFT_TIMEOUT)
promise.then(resolve, reject).finally(() => window.clearTimeout(timer))
})
}
function buildLocalArtifactHtml(payload, reason) {
try {
const html = buildLocalSmartArtifact({ ...payload, reason })
return secureArtifactHtml(html)
} catch (error) {
console.error("[智慧管理] 本地专题页面生成失败", error)
return ""
}
}
function retryArtifactGeneration() {
if (!lastArtifactPayload.value || artifactGenerating.value) return
launchArtifactGeneration(lastArtifactPayload.value)
}
function failArtifactPreparation(message) {
artifactRequestSerial += 1
artifact.value = {
...artifact.value,
status: "error",
html: "",
error: message,
fallback: false,
renderKey: Date.now(),
}
}
function handleArtifactFrameLoad() {
if (artifact.value.status === "ready") return
artifact.value = { ...artifact.value, status: "ready" }
}
function handleArtifactMessage(event) {
if (event.source !== artifactFrameRef.value?.contentWindow || event.data?.source !== "smart-artifact") return
if (event.data.type === "runtime-error") {
artifact.value = {
...artifact.value,
status: "error",
html: "",
error: event.data.message || "生成界面运行异常",
fallback: false,
renderKey: Date.now(),
}
}
}
function normalizeYakText(value) {
return String(value || "")
.replace(/耗牛/g, "牦牛")
.replace(/犛牛/g, "牦牛")
}
function normalizeQuestionText(value) {
return normalizeYakText(value).trim()
}
function initSpeechRecognition() {
if (typeof window === "undefined") return
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition
voiceSupported.value = Boolean(Recognition)
if (!Recognition) return
speechRecognition = new Recognition()
speechRecognition.lang = "zh-CN"
speechRecognition.continuous = false
speechRecognition.interimResults = true
speechRecognition.maxAlternatives = 1
speechRecognition.onstart = () => {
voiceListening.value = true
voiceError.value = ""
}
speechRecognition.onresult = (event) => {
const results = Array.from(event.results || [])
const transcript = results
.map((result) => result?.[0]?.transcript || "")
.join("")
.trim()
if (transcript) currentText.value = transcript
}
speechRecognition.onerror = (event) => {
voiceListening.value = false
voiceError.value = event?.error || "语音输入失败"
}
speechRecognition.onend = () => {
voiceListening.value = false
}
}
function toggleVoiceInput() {
if (!speechRecognition || loading.value) return
if (voiceListening.value) {
stopVoiceInput()
return
}
try {
voiceError.value = ""
speechRecognition.start()
} catch (error) {
voiceError.value = error?.message || "语音输入启动失败"
voiceListening.value = false
}
}
function stopVoiceInput() {
if (!speechRecognition || !voiceListening.value) return
try {
speechRecognition.stop()
} catch (error) {
voiceError.value = error?.message || "语音输入已结束"
} finally {
voiceListening.value = false
}
}
async function sendMessage(text = currentText.value) {
const question = normalizeQuestionText(text)
if (!question || loading.value) return
stopVoiceInput()
const userMessage = {
id: createId(),
role: "user",
text: question,
}
const assistantMessage = {
id: createId(),
role: "assistant",
steps: [],
digesting: true,
digestText: "生成中",
visibleText: "",
fullText: "",
streaming: false,
charts: [],
insights: [],
suggestions: [],
}
messages.value = [...messages.value, userMessage, assistantMessage]
currentText.value = ""
loading.value = true
prepareArtifact(question)
await scrollToBottom()
await handleUnifiedQuestion(question, assistantMessage.id)
}
async function handleUnifiedQuestion(question, assistantId) {
patchAssistant(assistantId, {
digestText: "生成中",
})
try {
const pagePath = window.location.hash || window.location.pathname
const context = await buildSmartQuestionContext(question, { pagePath })
const answer = buildSmartContextAnswer(context)
const insights = context.insights?.length ? context.insights : buildChartInsights(context.charts || [])
patchAssistant(assistantId, {
digesting: true,
digestText: "生成中",
insights,
suggestions: [],
allowFollowUp: true,
})
updateDynamicPanels({
question,
answer,
charts: context.charts || [],
insights,
suggestions: [],
})
finishAssistant(assistantId, buildArtifactPreparingMessage())
launchArtifactGeneration({
question,
answer,
charts: context.charts || [],
datasets: context.datasets || [],
context,
})
} catch (error) {
const errorText = getFriendlyErrorMessage(error, "上下文数据整理失败,请稍后重试。")
updateDynamicPanels({ question, answer: errorText, charts: [], insights: [], suggestions: [] })
failArtifactPreparation(errorText)
finishAssistant(assistantId, errorText)
}
}
function buildArtifactPreparingMessage() {
return [
"专题分析页面正在生成,完成后会自动显示在右侧。",
"你可以继续提出其他问题。",
].join("\n")
}
function formatNumber(value) {
const number = Number(String(value).replace(/,/g, ""))
if (!Number.isFinite(number)) return value || "--"
return number.toLocaleString("zh-CN", { maximumFractionDigits: 2 })
}
function updateDynamicPanels({ question, answer, charts = [], insights = [], suggestions = [], aiPanels } = {}) {
const aiLeft = aiPanels?.leftPanels || []
const aiRight = aiPanels?.rightPanels || []
if (aiLeft.length || aiRight.length) {
leftPanels.value = aiLeft
rightPanels.value = aiRight
return
}
const fallbackPanels = buildFallbackPanels({ question, answer, charts, insights, suggestions })
leftPanels.value = fallbackPanels.left
rightPanels.value = fallbackPanels.right
}
function buildFallbackPanels({ question, answer, charts = [], insights = [], suggestions = [] } = {}) {
const left = []
const right = []
const metricItems = (insights.length ? insights : buildChartInsights(charts)).map((item) => ({
label: item.label,
value: item.value,
note: item.note,
}))
if (!charts.length && !metricItems.length) {
return { left, right }
}
if (metricItems.length) {
left.push({
id: `summary-${createId()}`,
type: "metrics",
kicker: "数据要点",
title: "数据摘要",
badge: "实时",
items: metricItems.slice(0, 4),
actions: [],
})
}
charts.slice(0, 2).forEach((chart, index) => {
right.push({
id: `chart-${chart.id || index}-${createId()}`,
type: "chart",
kicker: index === 0 ? "可视分析" : "关联图表",
title: chart.title || `统计图表${index + 1}`,
badge: chart.unit || "",
chart,
items: [],
actions: [],
text: "",
})
})
return {
left: left.slice(0, 2),
right: right.slice(0, 2),
}
}
function buildChartInsights(charts = []) {
const insights = []
charts.slice(0, 2).forEach((chart) => {
const rows = (chart.rows || [])
.map((row) => ({
...row,
value: Number(row.value),
}))
.filter((row) => row.name && Number.isFinite(row.value))
if (!rows.length) return
const unit = chart.unit || rows.find((row) => row.unit)?.unit || ""
const sorted = rows.slice().sort((a, b) => b.value - a.value)
const total = rows.reduce((sum, row) => sum + row.value, 0)
const average = total / rows.length
const top = sorted[0]
const bottom = sorted[sorted.length - 1]
insights.push({
label: "最高",
value: `${top.name} ${formatNumber(top.value)}${unit}`,
note: chart.title,
})
if (rows.length > 2) {
insights.push({
label: chart.type === "pie" ? "占比项" : "最低",
value: `${bottom.name} ${formatNumber(bottom.value)}${unit}`,
note: chart.type === "pie" ? "最小结构项" : "低值区域",
})
}
insights.push({
label: "合计",
value: `${formatNumber(total)}${unit}`,
note: rows.length > 1 ? `覆盖 ${rows.length} 项` : chart.title,
})
if (chart.type !== "pie" && rows.length > 1) {
insights.push({
label: "均值",
value: `${formatNumber(average)}${unit}`,
note: "按当前图表项计算",
})
}
})
return insights.slice(0, 4)
}
function patchAssistant(id, patch, shouldScroll = true) {
messages.value = messages.value.map((message) => (message.id === id ? { ...message, ...patch } : message))
if (shouldScroll) scrollToBottom()
}
function finishAssistant(id, text) {
patchAssistant(id, {
digesting: false,
})
streamAssistantText(id, text)
}
function getFriendlyErrorMessage(error, fallback) {
const message = String(error?.message || "")
if (error?.code === 401 || error?.code === "401" || error?.code === "HTTP_401" || /未登录|授权|Unauthorized|HTTP 401/i.test(message)) {
return "当前登录状态已失效,请重新登录后查询。"
}
if (error?.code === "REQUEST_TIMEOUT" || /timeout|timed out|超时/i.test(message)) {
return "服务响应时间较长,请稍后再试,或换一个更具体的问题。"
}
if (error?.code === "REQUEST_ABORTED" || /aborted|abort|signal/i.test(message)) {
return "本次分析已中断,请重新提交。"
}
if (error?.code === "HTTP_404" || error?.status === 404 || /HTTP 404/i.test(message)) {
return "专题分析页面生成服务暂不可用,请稍后重试。"
}
if (/Failed to fetch|NetworkError|Load failed|HTTP 50|HTTP 40/i.test(message)) {
return fallback
}
return /[a-zA-Z]{4,}/.test(message) ? fallback : message || fallback
}
function streamAssistantText(id, text) {
stopStreaming()
const source = normalizeYakText(text)
const chunkSize = source.length > 1200 ? 36 : source.length > 700 ? 24 : 14
let index = 0
patchAssistant(id, {
fullText: source,
visibleText: "",
streaming: true,
charts: [],
})
const tick = () => {
index = Math.min(index + chunkSize, source.length)
patchAssistant(id, {
visibleText: source.slice(0, index),
streaming: index < source.length,
})
if (index >= source.length) {
streamTimer = null
loading.value = false
return
}
streamTimer = window.setTimeout(tick, 18)
}
streamTimer = window.setTimeout(tick, 0)
}
function stopStreaming() {
if (streamTimer) {
window.clearTimeout(streamTimer)
streamTimer = null
}
}
function renderMarkdown(value) {
const lines = escapeHtml(value).split(/\r?\n/)
const html = []
let listItems = []
let ordered = false
const flushList = () => {
if (!listItems.length) return
html.push(`<${ordered ? "ol" : "ul"}>${listItems.map((item) => `<li>${renderInline(item)}</li>`).join("")}</${ordered ? "ol" : "ul"}>`)
listItems = []
ordered = false
}
lines.forEach((line) => {
const trimmed = line.trim()
if (!trimmed) {
flushList()
return
}
const heading = trimmed.match(/^#{1,4}\s+(.+)$/) || trimmed.match(/^\*\*(.+)\*\*$/)
if (heading) {
flushList()
html.push(`<h3>${renderInline(heading[1])}</h3>`)
return
}
const orderedMatch = trimmed.match(/^\d+[.)、]\s+(.+)$/)
const unorderedMatch = trimmed.match(/^[-*]\s+(.+)$/)
if (orderedMatch || unorderedMatch) {
const nextOrdered = Boolean(orderedMatch)
if (listItems.length && ordered !== nextOrdered) flushList()
ordered = nextOrdered
listItems.push(orderedMatch ? orderedMatch[1] : unorderedMatch[1])
return
}
flushList()
html.push(`<p>${renderInline(trimmed)}</p>`)
})
flushList()
return html.join("")
}
function renderInline(value) {
return value.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
}
async function scrollToBottom() {
await nextTick()
window.requestAnimationFrame(() => {
if (dialogueRef.value) {
dialogueRef.value.scrollTop = dialogueRef.value.scrollHeight
}
})
}
</script>
<style lang="scss">
.wisdom-management {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
color: #dffaff;
background:
radial-gradient(circle at 50% 45%, rgba(37, 161, 133, 0.28), rgba(3, 16, 28, 0.18) 35%, rgba(2, 8, 18, 0.86) 72%),
linear-gradient(180deg, #041324, #030914);
pointer-events: auto;
}
.wisdom-backdrop {
position: absolute;
inset: 0;
pointer-events: none;
}
.wisdom-grid {
position: absolute;
left: 430px;
right: 430px;
top: 128px;
bottom: 8px;
opacity: 0.18;
background-image:
linear-gradient(rgba(48, 220, 255, 0.14) 1px, transparent 1px),
linear-gradient(90deg, rgba(48, 220, 255, 0.14) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: radial-gradient(circle at center, #000 42%, transparent 78%);
}
.wisdom-orbit {
position: absolute;
left: 50%;
top: 50%;
border: 1px solid rgba(48, 220, 255, 0.18);
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: inset 0 0 70px rgba(48, 220, 255, 0.06), 0 0 70px rgba(32, 232, 188, 0.08);
}
.wisdom-orbit-a {
width: 760px;
height: 760px;
animation: wisdomOrbitSpin 24s linear infinite;
}
.wisdom-orbit-b {
width: 980px;
height: 260px;
border-color: rgba(250, 211, 107, 0.16);
transform: translate(-50%, -48%) rotate(-9deg);
animation: wisdomPulse 6s ease-in-out infinite;
}
.wisdom-scan {
position: absolute;
left: 50%;
top: 50%;
width: 620px;
height: 620px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: conic-gradient(from 0deg, transparent, rgba(48, 220, 255, 0.2), transparent 24%);
opacity: 0.2;
animation: wisdomScanSpin 8s linear infinite;
}
.wisdom-artifact-workspace {
position: absolute;
left: 590px;
right: 36px;
top: 112px;
bottom: 18px;
z-index: 6;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(48, 220, 255, 0.3);
background: rgba(3, 17, 28, 0.96);
box-shadow: inset 0 0 40px rgba(48, 220, 255, 0.05), 0 18px 42px rgba(0, 0, 0, 0.28);
}
.wisdom-artifact-header {
flex: 0 0 62px;
min-width: 0;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid rgba(48, 220, 255, 0.18);
background: linear-gradient(90deg, rgba(8, 48, 61, 0.92), rgba(5, 25, 37, 0.82));
}
.wisdom-artifact-heading,
.wisdom-artifact-tools {
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.wisdom-artifact-mark {
position: relative;
flex: 0 0 28px;
width: 28px;
height: 28px;
border: 1px solid rgba(48, 220, 255, 0.68);
background: rgba(48, 220, 255, 0.08);
}
.wisdom-artifact-mark::before,
.wisdom-artifact-mark::after {
content: "";
position: absolute;
background: #30dcff;
box-shadow: 0 0 10px rgba(48, 220, 255, 0.6);
}
.wisdom-artifact-mark::before {
left: 6px;
right: 6px;
top: 8px;
height: 2px;
}
.wisdom-artifact-mark::after {
left: 6px;
right: 10px;
top: 16px;
height: 2px;
}
.wisdom-artifact-heading small,
.wisdom-artifact-heading strong {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wisdom-artifact-heading small {
margin-bottom: 3px;
color: rgba(48, 220, 255, 0.68);
font-size: 11px;
}
.wisdom-artifact-heading strong {
max-width: 720px;
color: #fff;
font-size: 17px;
}
.wisdom-artifact-status {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 7px;
color: rgba(223, 250, 255, 0.68);
font-size: 12px;
}
.wisdom-artifact-status i {
width: 7px;
height: 7px;
border-radius: 50%;
background: #ffd76c;
box-shadow: 0 0 10px rgba(255, 215, 108, 0.6);
}
.wisdom-artifact-status.is-ready i {
background: #27f0c0;
box-shadow: 0 0 10px rgba(39, 240, 192, 0.6);
}
.wisdom-artifact-status.is-error i {
background: #ff956e;
box-shadow: 0 0 10px rgba(255, 149, 110, 0.56);
}
.wisdom-artifact-tools button,
.wisdom-artifact-error button {
height: 30px;
padding: 0 12px;
border: 1px solid rgba(48, 220, 255, 0.32);
background: rgba(7, 43, 56, 0.78);
color: #dffaff;
font-size: 12px;
cursor: pointer;
}
.wisdom-artifact-tools button:hover:not(:disabled),
.wisdom-artifact-error button:hover:not(:disabled) {
border-color: rgba(255, 215, 108, 0.62);
color: #fff;
}
.wisdom-artifact-tools button:disabled,
.wisdom-artifact-error button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.wisdom-artifact-body {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
}
.wisdom-artifact-frame {
width: 100%;
height: 100%;
display: block;
border: 0;
background: #06131d;
}
.wisdom-artifact-building {
height: 100%;
overflow: hidden;
padding: 28px;
background:
linear-gradient(rgba(48, 220, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(48, 220, 255, 0.04) 1px, transparent 1px),
#06131d;
background-size: 36px 36px;
}
.wisdom-artifact-building__intro {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
}
.wisdom-artifact-building__intro strong,
.wisdom-artifact-building__intro p {
display: block;
margin: 0;
}
.wisdom-artifact-building__intro strong {
color: #fff;
font-size: 18px;
}
.wisdom-artifact-building__intro p {
margin-top: 4px;
color: rgba(223, 250, 255, 0.5);
font-size: 12px;
}
.wisdom-artifact-building__spinner {
width: 30px;
height: 30px;
border: 2px solid rgba(48, 220, 255, 0.18);
border-top-color: #30dcff;
border-radius: 50%;
animation: wisdomSpin 0.9s linear infinite;
}
.wisdom-artifact-skeleton {
position: relative;
height: 190px;
overflow: hidden;
border: 1px solid rgba(48, 220, 255, 0.12);
background: rgba(8, 35, 47, 0.66);
}
.wisdom-artifact-skeleton::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(99, 239, 226, 0.1), transparent);
animation: wisdomArtifactShimmer 1.6s ease-in-out infinite;
}
.wisdom-artifact-skeleton.is-wide {
height: 96px;
}
.wisdom-artifact-skeleton.is-chart {
height: 260px;
margin-top: 14px;
}
.wisdom-artifact-skeleton-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
}
.wisdom-artifact-error {
height: 100%;
display: grid;
place-content: center;
justify-items: center;
padding: 32px;
text-align: center;
background: #06131d;
}
.wisdom-artifact-error strong {
color: #fff;
font-size: 20px;
}
.wisdom-artifact-error p {
max-width: 520px;
margin: 10px 0 18px;
color: rgba(223, 250, 255, 0.62);
font-size: 13px;
line-height: 1.7;
}
.wisdom-side {
position: absolute;
top: 112px;
bottom: 18px;
z-index: 5;
width: clamp(390px, 21vw, 460px);
overflow-y: auto;
display: grid;
align-content: start;
gap: 16px;
padding: 6px 0 10px;
box-sizing: border-box;
scrollbar-width: none;
}
.wisdom-side::-webkit-scrollbar {
width: 0;
height: 0;
}
.wisdom-side-left {
left: clamp(24px, 2.4vw, 52px);
}
.wisdom-side-right {
right: clamp(24px, 2.4vw, 52px);
}
.wisdom-side-panel {
min-height: 0;
border: 1px solid rgba(48, 220, 255, 0.22);
background:
linear-gradient(135deg, rgba(6, 37, 50, 0.9), rgba(4, 19, 32, 0.86)),
radial-gradient(circle at 18% 0%, rgba(39, 240, 192, 0.12), transparent 34%);
box-shadow: inset 0 0 24px rgba(48, 220, 255, 0.07), 0 14px 28px rgba(0, 0, 0, 0.22);
padding: 15px;
box-sizing: border-box;
}
.wisdom-side-panel.is-chart {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.wisdom-side-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.wisdom-side-header span {
display: block;
margin-bottom: 5px;
color: rgba(48, 220, 255, 0.72);
font-size: 12px;
}
.wisdom-side-header strong {
display: block;
max-width: 220px;
color: #fff;
font-size: 17px;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wisdom-side-header em {
flex: 0 0 auto;
padding: 3px 8px;
border: 1px solid rgba(255, 215, 108, 0.32);
color: #ffd76c;
font-size: 12px;
font-style: normal;
background: rgba(255, 215, 108, 0.08);
}
.wisdom-side-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
.wisdom-side-metric {
min-width: 0;
padding: 10px;
border: 1px solid rgba(48, 220, 255, 0.16);
background: rgba(3, 22, 35, 0.58);
box-sizing: border-box;
}
.wisdom-side-metric span,
.wisdom-side-metric em {
display: block;
color: rgba(223, 250, 255, 0.55);
font-size: 12px;
font-style: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wisdom-side-metric strong {
display: block;
margin: 4px 0;
color: #ffd76c;
font-family: "D-DIN", Arial, sans-serif;
font-size: 17px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wisdom-side-text {
color: rgba(237, 253, 255, 0.88);
font-size: 13px;
line-height: 1.7;
}
.wisdom-side-text h3 {
margin: 0 0 8px;
color: #fff;
font-size: 15px;
}
.wisdom-side-text p {
margin: 7px 0;
}
.wisdom-side-text ul,
.wisdom-side-text ol {
margin: 8px 0 8px 18px;
padding: 0;
}
.wisdom-side-text strong {
color: #ffd76c;
}
.wisdom-side-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.wisdom-side-text + .wisdom-side-actions,
.wisdom-side-metrics + .wisdom-side-actions {
margin-top: 12px;
}
.wisdom-side-actions button {
height: 30px;
min-width: 84px;
padding: 0 10px;
border: 1px solid rgba(48, 220, 255, 0.28);
background: linear-gradient(135deg, rgba(8, 48, 62, 0.76), rgba(6, 28, 43, 0.76));
color: rgba(255, 215, 108, 0.9);
font-size: 12px;
cursor: pointer;
}
.wisdom-side-actions button:hover:not(:disabled) {
border-color: rgba(255, 215, 108, 0.62);
color: #fff;
}
.wisdom-side-chart.smart-chart-card {
width: 100%;
height: 348px;
margin-top: 0;
}
.wisdom-side-chart .smart-chart-header {
height: 58px;
padding: 10px 13px 0;
}
.wisdom-side-chart .smart-chart-header strong {
font-size: 15px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wisdom-side-chart .smart-chart-header span,
.wisdom-side-chart .smart-chart-header em {
font-size: 11px;
}
.wisdom-side-chart .smart-chart {
height: 264px;
}
.wisdom-avatar-ring,
.wisdom-empty-mark span {
position: absolute;
inset: 0;
border: 1px solid rgba(48, 220, 255, 0.62);
border-radius: 50%;
animation: wisdomSpin 7s linear infinite;
}
.wisdom-avatar-ring::before,
.wisdom-empty-mark span::before {
content: "";
position: absolute;
left: 50%;
top: -4px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ffd76c;
box-shadow: 0 0 14px rgba(255, 215, 108, 0.8);
}
.wisdom-chat {
position: absolute;
left: 50%;
top: 112px;
bottom: 18px;
z-index: 4;
width: clamp(720px, calc(100% - 1040px), 900px);
display: flex;
flex-direction: column;
min-width: 0;
transform: translateX(-50%);
}
.wisdom-chat.has-artifact {
left: 34px;
right: auto;
width: 520px;
transform: none;
}
.wisdom-chat.has-artifact .wisdom-bubble {
max-width: calc(100% - 46px);
}
.wisdom-dialogue {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px 8px 18px;
scrollbar-width: thin;
scrollbar-color: rgba(48, 220, 255, 0.5) rgba(3, 18, 30, 0.3);
}
.wisdom-dialogue.is-empty {
display: grid;
place-items: center;
}
.wisdom-empty-state {
position: relative;
width: 560px;
min-height: 280px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.wisdom-empty-mark {
position: relative;
width: 126px;
height: 126px;
display: grid;
place-items: center;
}
.wisdom-empty-mark strong {
width: 84px;
height: 84px;
border-radius: 50%;
display: grid;
place-items: center;
color: #071520;
font-family: "D-DIN", Arial, sans-serif;
font-size: 34px;
background: radial-gradient(circle at 32% 24%, #fff, #9cf8e9 48%, #2bd4cb 100%);
box-shadow: 0 0 42px rgba(78, 243, 222, 0.38);
}
.wisdom-empty-state h2 {
margin: 18px 0 10px;
color: #fff;
font-size: 28px;
letter-spacing: 0;
}
.wisdom-empty-state p {
margin: 0;
max-width: 420px;
color: rgba(223, 250, 255, 0.82);
font-size: 15px;
line-height: 1.8;
}
.wisdom-suggestion-list button {
height: 34px;
border: 1px solid rgba(48, 220, 255, 0.28);
background: linear-gradient(135deg, rgba(8, 48, 62, 0.78), rgba(6, 28, 43, 0.78));
color: rgba(223, 250, 255, 0.86);
font-size: 13px;
cursor: pointer;
box-shadow: inset 0 0 16px rgba(48, 220, 255, 0.06);
}
.wisdom-suggestion-list button:hover:not(:disabled) {
border-color: rgba(255, 215, 108, 0.62);
color: #fff;
background: linear-gradient(135deg, rgba(48, 220, 255, 0.18), rgba(255, 215, 108, 0.12));
}
.wisdom-message {
display: flex;
margin: 14px 0;
gap: 10px;
}
.wisdom-message.is-user {
justify-content: flex-end;
}
.wisdom-message-mark {
flex: 0 0 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
color: #06151f;
font-weight: 800;
background: linear-gradient(135deg, #dffaff, #4cf0de);
box-shadow: 0 0 22px rgba(78, 243, 222, 0.36);
}
.wisdom-bubble {
max-width: min(680px, calc(100% - 48px));
box-sizing: border-box;
border: 1px solid rgba(48, 220, 255, 0.18);
background: rgba(4, 22, 34, 0.92);
box-shadow: inset 0 0 24px rgba(48, 220, 255, 0.06), 0 14px 34px rgba(0, 0, 0, 0.34);
color: rgba(237, 253, 255, 0.92);
font-size: 14px;
line-height: 1.7;
padding: 13px 15px;
}
.smart-chart-loading {
width: min(620px, 100%);
height: 292px;
margin-top: 12px;
display: grid;
place-items: center;
box-sizing: border-box;
border: 1px solid rgba(48, 220, 255, 0.22);
background: rgba(4, 22, 34, 0.72);
color: rgba(255, 215, 108, 0.86);
font-size: 13px;
box-shadow: inset 0 0 24px rgba(48, 220, 255, 0.06);
}
.wisdom-suggestion-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.wisdom-suggestion-list button {
width: auto;
min-width: 86px;
padding: 0 12px;
color: rgba(255, 215, 108, 0.9);
}
.wisdom-message.is-user .wisdom-bubble {
max-width: min(560px, 76%);
border-color: rgba(255, 215, 108, 0.26);
background: linear-gradient(135deg, rgba(50, 115, 106, 0.9), rgba(13, 54, 65, 0.86));
}
.wisdom-steps {
display: grid;
gap: 8px;
margin-bottom: 10px;
}
.wisdom-step {
display: flex;
align-items: center;
gap: 8px;
color: rgba(223, 250, 255, 0.64);
font-size: 13px;
}
.wisdom-step span {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid rgba(48, 220, 255, 0.7);
position: relative;
}
.wisdom-step span::after {
content: "";
position: absolute;
left: 4px;
top: 2px;
width: 4px;
height: 7px;
border-right: 2px solid #30dcff;
border-bottom: 2px solid #30dcff;
transform: rotate(40deg);
}
.wisdom-digesting {
display: flex;
align-items: center;
gap: 8px;
color: rgba(255, 215, 108, 0.88);
}
.wisdom-digesting span,
.wisdom-button-loading {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ffd76c;
animation: wisdomBounce 1s ease-in-out infinite;
}
.wisdom-digesting span:nth-child(2) {
animation-delay: 0.14s;
}
.wisdom-digesting span:nth-child(3) {
animation-delay: 0.28s;
}
.wisdom-digesting em {
font-style: normal;
}
.wisdom-answer {
color: rgba(237, 253, 255, 0.94);
}
.wisdom-answer h3 {
margin: 12px 0 8px;
color: #fff;
font-size: 17px;
}
.wisdom-answer p {
margin: 8px 0;
}
.wisdom-answer ul,
.wisdom-answer ol {
margin: 8px 0 8px 20px;
padding: 0;
}
.wisdom-answer li {
margin: 4px 0;
}
.wisdom-answer strong {
color: #ffd76c;
}
.wisdom-stream-caret {
display: inline-block;
width: 8px;
height: 18px;
margin-left: 4px;
vertical-align: middle;
background: #30dcff;
animation: wisdomBlink 0.8s steps(2, start) infinite;
}
.wisdom-input-bar {
flex: 0 0 64px;
padding: 8px 8px 0;
box-sizing: border-box;
}
.wisdom-input-shell {
height: 50px;
border: 1px solid rgba(48, 220, 255, 0.34);
background: linear-gradient(135deg, rgba(8, 42, 55, 0.92), rgba(3, 18, 31, 0.92));
box-shadow: inset 0 0 24px rgba(48, 220, 255, 0.08), 0 0 30px rgba(48, 220, 255, 0.1);
display: flex;
align-items: center;
gap: 10px;
padding: 0 8px 0 16px;
box-sizing: border-box;
}
.wisdom-input-shell:focus-within {
border-color: rgba(126, 251, 246, 0.78);
box-shadow:
inset 0 0 24px rgba(48, 220, 255, 0.1),
0 0 0 2px rgba(126, 251, 246, 0.12),
0 0 30px rgba(48, 220, 255, 0.14);
}
.wisdom-input-icon {
width: 16px;
height: 16px;
border: 2px solid #30dcff;
border-radius: 50%;
position: relative;
flex: 0 0 auto;
}
.wisdom-input-icon::after {
content: "";
position: absolute;
right: -7px;
bottom: -5px;
width: 8px;
height: 2px;
border-radius: 2px;
background: #30dcff;
transform: rotate(45deg);
}
.wisdom-input-shell input {
min-width: 0;
flex: 1;
border: 0;
outline: 0;
background: transparent;
color: #fff;
font-size: 15px;
}
.wisdom-input-shell input::placeholder {
color: rgba(223, 250, 255, 0.62);
}
.wisdom-submit-button {
width: 92px;
height: 36px;
border: 1px solid rgba(255, 215, 108, 0.46);
background: linear-gradient(135deg, rgba(255, 215, 108, 0.26), rgba(48, 220, 255, 0.12));
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
}
.wisdom-voice-button {
position: relative;
flex: 0 0 auto;
width: 38px;
height: 36px;
border: 1px solid rgba(48, 220, 255, 0.32);
background: rgba(6, 35, 49, 0.76);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.wisdom-voice-button span {
position: relative;
width: 10px;
height: 17px;
border: 2px solid #30dcff;
border-radius: 8px;
box-sizing: border-box;
}
.wisdom-voice-button span::before {
content: "";
position: absolute;
left: 50%;
bottom: -8px;
width: 14px;
height: 8px;
border: 2px solid #30dcff;
border-top: 0;
border-radius: 0 0 10px 10px;
transform: translateX(-50%);
}
.wisdom-voice-button span::after {
content: "";
position: absolute;
left: 50%;
bottom: -12px;
width: 12px;
height: 2px;
border-radius: 2px;
background: #30dcff;
transform: translateX(-50%);
}
.wisdom-voice-button.is-listening {
border-color: rgba(255, 215, 108, 0.62);
background: rgba(255, 215, 108, 0.14);
box-shadow: 0 0 18px rgba(255, 215, 108, 0.22);
}
.wisdom-voice-button.is-listening span,
.wisdom-voice-button.is-listening span::before {
border-color: #ffd76c;
}
.wisdom-voice-button.is-listening span::after {
background: #ffd76c;
}
.wisdom-submit-button:disabled,
.wisdom-voice-button:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.wisdom-submit-button:focus-visible,
.wisdom-voice-button:focus-visible,
.wisdom-suggestion-list button:focus-visible {
outline: 2px solid #7efbf6;
outline-offset: 2px;
}
.wisdom-button-loading {
width: 12px;
height: 12px;
border: 2px solid rgba(255, 255, 255, 0.34);
border-top-color: #fff;
background: transparent;
animation: wisdomSpin 0.8s linear infinite;
}
@keyframes wisdomSpin {
to {
transform: rotate(360deg);
}
}
@keyframes wisdomOrbitSpin {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes wisdomScanSpin {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes wisdomPulse {
0%,
100% {
opacity: 0.36;
}
50% {
opacity: 0.72;
}
}
@keyframes wisdomBounce {
0%,
100% {
transform: translateY(0);
opacity: 0.42;
}
50% {
transform: translateY(-5px);
opacity: 1;
}
}
@keyframes wisdomBlink {
50% {
opacity: 0;
}
}
@keyframes wisdomArtifactShimmer {
100% {
transform: translateX(100%);
}
}
@media (max-width: 1480px) {
.wisdom-artifact-workspace {
left: 430px;
right: 20px;
}
.wisdom-chat.has-artifact {
left: 20px;
right: auto;
width: 390px;
}
.wisdom-artifact-heading strong {
max-width: 520px;
}
.wisdom-side {
top: 112px;
bottom: 16px;
width: 318px;
}
.wisdom-side-left {
left: 20px;
}
.wisdom-side-right {
right: 20px;
}
.wisdom-side-chart.smart-chart-card {
height: 320px;
}
.wisdom-side-chart .smart-chart {
height: 236px;
}
.wisdom-chat {
width: clamp(560px, calc(100% - 760px), 760px);
}
}
@media (max-width: 1280px) {
.wisdom-side {
display: none;
}
.wisdom-chat {
width: min(860px, calc(100% - 80px));
}
.wisdom-artifact-workspace {
left: 374px;
right: 16px;
}
.wisdom-chat.has-artifact {
left: 16px;
right: auto;
width: 342px;
}
.wisdom-artifact-heading strong {
max-width: 360px;
}
.wisdom-artifact-tools {
gap: 8px;
}
.wisdom-artifact-status {
display: none;
}
}
</style>