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
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
}
|
|
|
|
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>
|
|
|