feat: 集成Hermes智能体系统,增强聊天和差旅报销功能

This commit is contained in:
caoxiaozhu
2026-05-16 06:14:08 +00:00
parent 763afa0ee2
commit 212c935308
46 changed files with 8802 additions and 5372 deletions

View File

@@ -37,10 +37,26 @@
.answer-card footer button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 6px; background: transparent; color: #64748b; }
.answer-card footer button:hover { background: #f1f5f9; color: #0f9f78; }
.agent-answer { margin: 0; padding: 12px 16px; font-size: 14px; line-height: 1.65; }
.agent-answer-content { max-width: 760px; display: grid; gap: 10px; }
.agent-answer-table-wrap { overflow-x: auto; border: 1px solid #dce5ef; border-radius: 10px; background: #fff; box-shadow: 0 8px 24px rgba(15, 23, 42, .04); }
.agent-answer-table { width: 100%; min-width: 360px; border-collapse: collapse; font-size: 13px; }
.agent-answer-table th, .agent-answer-table td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; text-align: left; }
.agent-answer-table th { background: #eef7f2; color: #0f172a; font-weight: 800; }
.agent-answer-table td { color: #334155; font-weight: 650; }
.agent-answer-table tbody tr:last-child td { border-bottom: 0; }
.agent-meta-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; max-width: 760px; }
.agent-meta-chip { min-height: 26px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 999px; background: #eef7f2; color: #0f766e; font-size: 12px; font-weight: 760; }
.agent-detail-block { max-width: 760px; margin-top: 10px; display: grid; gap: 8px; }
.agent-detail-block > strong { color: #0f172a; font-size: 12px; font-weight: 820; }
.agent-citation-disclosure { overflow: hidden; border: 1px solid #dce5ef; border-radius: 10px; background: #fff; }
.agent-citation-disclosure summary { min-height: 40px; display: flex; align-items: center; gap: 8px; padding: 0 12px; color: #0f172a; cursor: pointer; list-style: none; }
.agent-citation-disclosure summary::-webkit-details-marker { display: none; }
.agent-citation-disclosure summary strong { font-size: 12px; font-weight: 820; }
.agent-citation-disclosure summary span { color: #64748b; font-size: 12px; font-weight: 720; }
.agent-citation-disclosure summary i { margin-left: auto; color: #64748b; transition: transform .18s ease; }
.agent-citation-disclosure[open] summary { border-bottom: 1px solid #eef2f7; }
.agent-citation-disclosure[open] summary i { transform: rotate(180deg); }
.agent-citation-disclosure .agent-citation-list { padding: 10px; }
.agent-detail-chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
.agent-risk-chip, .agent-action-chip { min-height: 28px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 999px; font-size: 12px; font-weight: 760; }
.agent-risk-chip { background: #fff1f2; color: #be123c; }

View File

@@ -379,6 +379,52 @@
font-size: 14px;
}
.message-answer-content {
display: grid;
gap: 12px;
}
.message-answer-content p {
margin: 0;
}
.message-answer-table-wrap {
overflow-x: auto;
border: 1px solid #dbe4ee;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.message-answer-table {
width: 100%;
min-width: 360px;
border-collapse: collapse;
overflow: hidden;
font-size: 13px;
}
.message-answer-table th,
.message-answer-table td {
padding: 10px 12px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
}
.message-answer-table th {
background: #eff6ff;
color: #0f172a;
font-weight: 850;
}
.message-answer-table td {
color: #334155;
font-weight: 650;
}
.message-answer-table tbody tr:last-child td {
border-bottom: 0;
}
.message-meta-row {
display: flex;
flex-wrap: wrap;
@@ -429,6 +475,58 @@
font-weight: 850;
}
.message-citation-disclosure {
overflow: hidden;
border: 1px solid #dbe4ee;
border-radius: 16px;
background: #fbfdff;
}
.message-citation-disclosure summary {
min-height: 42px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 14px;
color: #0f172a;
cursor: pointer;
list-style: none;
}
.message-citation-disclosure summary::-webkit-details-marker {
display: none;
}
.message-citation-disclosure summary strong {
font-size: 12px;
font-weight: 850;
}
.message-citation-disclosure summary span {
color: #64748b;
font-size: 12px;
font-weight: 750;
}
.message-citation-disclosure summary i {
margin-left: auto;
color: #64748b;
font-size: 16px;
transition: transform 0.18s ease;
}
.message-citation-disclosure[open] summary {
border-bottom: 1px solid #e2e8f0;
}
.message-citation-disclosure[open] summary i {
transform: rotate(180deg);
}
.message-citation-disclosure .message-citation-list {
padding: 12px;
}
.expense-query-block {
gap: 10px;
}

View File

@@ -142,6 +142,8 @@ export function useChat(activeView) {
is_admin: Boolean(user.isAdmin),
name: user.name || '',
role: user.role || '',
position: user.position || '',
grade: user.grade || '',
active_case_id: activeCase.value?.id || ''
}
})

View File

@@ -86,11 +86,13 @@ function readStoredUsername() {
}
function buildAnonymousUser() {
return {
username: '',
name: '',
role: '',
roleCodes: [],
return {
username: '',
name: '',
role: '',
position: '',
grade: '',
roleCodes: [],
email: '',
avatar: '',
isAdmin: false
@@ -101,11 +103,13 @@ function buildLegacyAdminUser(username = '') {
const normalized = String(username || '').trim()
const name = normalized || DEFAULT_USER_NAME
return {
username: normalized,
name,
role: DEFAULT_USER_ROLE,
roleCodes: ['manager'],
return {
username: normalized,
name,
role: DEFAULT_USER_ROLE,
position: DEFAULT_USER_ROLE,
grade: '',
roleCodes: ['manager'],
email: '',
avatar: name.slice(0, 1).toUpperCase(),
isAdmin: true
@@ -127,11 +131,13 @@ function readStoredUser() {
const name = String(payload.name || username || DEFAULT_USER_NAME).trim()
const roleCodes = Array.isArray(payload.roleCodes) ? payload.roleCodes.filter(Boolean) : []
return {
username,
name,
role: String(payload.role || DEFAULT_USER_ROLE),
roleCodes,
return {
username,
name,
role: String(payload.role || DEFAULT_USER_ROLE),
position: String(payload.position || ''),
grade: String(payload.grade || ''),
roleCodes,
email: String(payload.email || ''),
avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()),
isAdmin: Boolean(payload.isAdmin)

View File

@@ -1,322 +1,333 @@
<template>
<section class="qa-view">
<div class="qa-layout">
<aside class="left-column">
<article class="panel side-panel conversation-list">
<header>
<h3>问答会话</h3>
<button class="outline-action" type="button" @click="emit('draft', '')">
<i class="mdi mdi-plus"></i>
<span>新建会话</span>
</button>
</header>
<div class="session-scroll">
<button
v-for="item in sessions"
:key="item.title"
class="session-row"
:class="{ active: item.active }"
type="button"
@click="applyPrompt(item.title)"
>
<span><i class="mdi mdi-message-processing-outline"></i></span>
<strong>{{ item.title }}</strong>
<time>{{ item.time }}</time>
</button>
</div>
</article>
</aside>
<article class="panel chat-panel">
<div ref="localMessageList" class="message-stream" aria-live="polite">
<div class="talk-row user">
<span class="avatar user-avatar"></span>
<div class="talk-content">
<header><strong>张明</strong><time>10:32</time></header>
<p class="user-question">北京出差酒店超标报销怎么处理</p>
</div>
</div>
<div class="talk-row assistant">
<span class="avatar assistant-avatar"><i class="mdi mdi-robot-outline"></i></span>
<div class="talk-content">
<header><strong>财务AI助手</strong><time>10:32</time></header>
<div class="answer-card">
<section>
<h4>结论</h4>
<p>酒店费用超过标准的部分原则上不予报销特殊情况可申请例外报销</p>
</section>
<section>
<h4>处理建议</h4>
<ul>
<li>超标部分由个人自理或按制度退回保留超标说明和相关凭证</li>
<li>符合公司相关政策的可提交佐证材料申请例外报销</li>
</ul>
</section>
<section>
<h4>适用规则</h4>
<ul>
<li>差旅报销管理办法2024第十二条住宿标准及超标处理</li>
<li>费用报销审批流程附件1国内差旅住宿标准</li>
</ul>
</section>
<footer>
<span>是否有帮助</span>
<button type="button" aria-label="有帮助"><i class="mdi mdi-thumb-up-outline"></i></button>
<button type="button" aria-label="无帮助"><i class="mdi mdi-thumb-down-outline"></i></button>
</footer>
</div>
</div>
</div>
<div class="talk-row user">
<span class="avatar user-avatar"></span>
<div class="talk-content">
<header><strong>张明</strong><time>10:35</time></header>
<p class="user-question">如果出差地公司名称不一致还能报销吗</p>
</div>
</div>
<div class="talk-row assistant">
<span class="avatar assistant-avatar"><i class="mdi mdi-robot-outline"></i></span>
<div class="talk-content">
<header><strong>财务AI助手</strong><time>10:35</time></header>
<div class="answer-card compact">
<section>
<h4>结论</h4>
<p>一般情况下差旅地与参会公司名称不一致需按异常处理建议提供情况说明并加盖公章或补充邀请材料</p>
</section>
<section>
<h4>适用规则</h4>
<ul>
<li>发票管理规定及失误销细则第二章发票基本要求</li>
<li>差旅报销管理办法附件1报销凭证要求</li>
</ul>
</section>
</div>
</div>
</div>
<div v-for="message in messages" :key="message.id" class="talk-row" :class="message.role === 'user' ? 'user' : 'assistant'">
<span class="avatar" :class="message.role === 'user' ? 'user-avatar' : 'assistant-avatar'">
<template v-if="message.role === 'user'"></template>
<i v-else class="mdi mdi-robot-outline"></i>
</span>
<div class="talk-content">
<header>
<strong>{{ message.role === 'user' ? '我' : '财务AI助手' }}</strong>
<time>刚刚</time>
</header>
<p :class="message.role === 'user' ? 'user-question' : 'agent-answer'">{{ message.text }}</p>
<div v-if="message.role !== 'user' && message.meta?.length" class="agent-meta-row">
<span v-for="item in message.meta" :key="item" class="agent-meta-chip">{{ item }}</span>
</div>
<div v-if="message.role !== 'user' && message.riskFlags?.length" class="agent-detail-block">
<strong>风险标签</strong>
<div class="agent-detail-chip-row">
<span v-for="item in message.riskFlags" :key="item" class="agent-risk-chip">{{ item }}</span>
</div>
</div>
<div v-if="message.role !== 'user' && message.citations?.length" class="agent-detail-block">
<strong>引用依据</strong>
<div class="agent-citation-list">
<article v-for="item in message.citations" :key="`${message.id}-${item.code}`" class="agent-citation-card">
<header>
<span>{{ item.title }}</span>
<small>{{ item.version || item.source_type }}</small>
</header>
<p>{{ item.excerpt || item.code }}</p>
</article>
</div>
</div>
<div v-if="message.role !== 'user' && message.suggestedActions?.length" class="agent-detail-block">
<strong>建议动作</strong>
<div class="agent-detail-chip-row">
<span
v-for="item in message.suggestedActions"
:key="`${message.id}-${item.action_type}-${item.label}`"
class="agent-action-chip"
>
{{ item.label }}
</span>
</div>
</div>
<div v-if="message.role !== 'user' && message.draftPayload" class="agent-draft-card">
<header>
<strong>{{ message.draftPayload.title }}</strong>
<span>待人工确认</span>
</header>
<pre>{{ message.draftPayload.body }}</pre>
</div>
</div>
</div>
</div>
<div class="composer-wrap">
<div class="prompt-toolbar">
<span>猜你想问</span>
<button v-for="prompt in visiblePrompts" :key="prompt.text" type="button" @click="applyPrompt(prompt.text)">
<i :class="prompt.icon"></i>
{{ prompt.short }}
</button>
<button class="icon-refresh" type="button" aria-label="换一批问题" @click="rotatePrompts">
<i class="mdi mdi-refresh"></i>
</button>
</div>
<div class="composer">
<textarea
:value="draft"
rows="2"
placeholder="请输入你的问题,例如:差旅报销特殊标准是什么?"
:disabled="sending"
@input="emit('draft', $event.target.value)"
@keydown.ctrl.enter.prevent="emit('send')"
></textarea>
<button
class="send-button"
type="button"
aria-label="发送问题"
:disabled="sending || !String(draft || '').trim()"
@click="emit('send')"
>
<i :class="sending ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
</button>
</div>
</div>
</article>
<aside class="right-column">
<article class="panel info-panel semantic-debug-panel">
<header>
<h3><i class="mdi mdi-shape-outline"></i> 语义解析调试</h3>
<button type="button" @click="useDraftAsSemanticInput">带入输入框</button>
</header>
<div class="semantic-debug-body">
<label class="semantic-debug-input">
<span>自然语言问题</span>
<textarea
v-model="semanticDraft"
rows="4"
placeholder="例如:查一下本周报销超标风险"
@keydown.ctrl.enter.prevent="parseSemanticQuery"
></textarea>
</label>
<div class="semantic-debug-actions">
<button
v-for="item in semanticExamples"
:key="item"
class="semantic-chip"
type="button"
@click="applySemanticExample(item)"
>
{{ item }}
</button>
</div>
<div class="semantic-debug-toolbar">
<button class="semantic-parse-btn" type="button" :disabled="semanticLoading" @click="parseSemanticQuery">
<i :class="semanticLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-play-circle-outline'"></i>
<span>{{ semanticLoading ? '解析中...' : '开始解析' }}</span>
</button>
<span class="semantic-inline-meta">
<template v-if="semanticResult">run_id{{ semanticResult.run_id }}</template>
<template v-else>支持 Ctrl + Enter</template>
</span>
</div>
<p v-if="semanticError" class="semantic-debug-error">{{ semanticError }}</p>
<div v-if="semanticResult" class="semantic-result-stack">
<div class="semantic-result-grid">
<article class="semantic-result-card">
<span>场景</span>
<strong>{{ semanticResult.scenario }}</strong>
</article>
<article class="semantic-result-card">
<span>意图</span>
<strong>{{ semanticResult.intent }}</strong>
</article>
<article class="semantic-result-card">
<span>权限</span>
<strong>{{ semanticResult.permission.level }}</strong>
</article>
<article class="semantic-result-card">
<span>置信度</span>
<strong>{{ semanticConfidenceLabel }}</strong>
</article>
</div>
<div class="semantic-field-list">
<section>
<h4>实体</h4>
<p>{{ semanticEntitiesText }}</p>
</section>
<section>
<h4>时间</h4>
<p>{{ semanticTimeRangeText }}</p>
</section>
<section>
<h4>指标</h4>
<p>{{ semanticMetricsText }}</p>
</section>
<section>
<h4>约束</h4>
<p>{{ semanticConstraintsText }}</p>
</section>
<section>
<h4>风险</h4>
<p>{{ semanticRiskFlagsText }}</p>
</section>
<section>
<h4>澄清</h4>
<p>{{ semanticClarificationText }}</p>
</section>
</div>
<div class="semantic-json-block">
<h4>原始 JSON</h4>
<pre>{{ semanticResultJson }}</pre>
</div>
</div>
</div>
</article>
<article class="panel info-panel hot-top-panel">
<header>
<h3><i class="mdi mdi-fire"></i> 热门问题 Top10</h3>
<button type="button" @click="rotatePrompts">换一批 <i class="mdi mdi-refresh"></i></button>
</header>
<div class="top-question-list">
<button v-for="(item, index) in hotQuestions" :key="item" type="button" @click="applyPrompt(item)">
<strong>{{ String(index + 1).padStart(2, '0') }}</strong>
<span>{{ item }}</span>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</article>
<article class="panel info-panel similar-panel">
<header>
<h3>相似历史问题</h3>
<button type="button">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</header>
<div class="similar-scroll">
<button v-for="item in similarQuestions" :key="item.text" class="similar-row" type="button" @click="applyPrompt(item.text)">
<span><i class="mdi mdi-file-question-outline"></i>{{ item.text }}</span>
<strong>{{ item.score }}</strong>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</article>
</aside>
</div>
</section>
</template>
<script src="./scripts/ChatView.js"></script>
<style scoped src="../assets/styles/views/chat-view.css"></style>
<template>
<section class="qa-view">
<div class="qa-layout">
<aside class="left-column">
<article class="panel side-panel conversation-list">
<header>
<h3>问答会话</h3>
<button class="outline-action" type="button" @click="emit('draft', '')">
<i class="mdi mdi-plus"></i>
<span>新建会话</span>
</button>
</header>
<div class="session-scroll">
<button
v-for="item in sessions"
:key="item.title"
class="session-row"
:class="{ active: item.active }"
type="button"
@click="applyPrompt(item.title)"
>
<span><i class="mdi mdi-message-processing-outline"></i></span>
<strong>{{ item.title }}</strong>
<time>{{ item.time }}</time>
</button>
</div>
</article>
</aside>
<article class="panel chat-panel">
<div ref="localMessageList" class="message-stream" aria-live="polite">
<div class="talk-row user">
<span class="avatar user-avatar"></span>
<div class="talk-content">
<header><strong>张明</strong><time>10:32</time></header>
<p class="user-question">北京出差酒店超标报销怎么处理</p>
</div>
</div>
<div class="talk-row assistant">
<span class="avatar assistant-avatar"><i class="mdi mdi-robot-outline"></i></span>
<div class="talk-content">
<header><strong>财务AI助手</strong><time>10:32</time></header>
<div class="answer-card">
<section>
<h4>结论</h4>
<p>酒店费用超过标准的部分原则上不予报销特殊情况可申请例外报销</p>
</section>
<section>
<h4>处理建议</h4>
<ul>
<li>超标部分由个人自理或按制度退回保留超标说明和相关凭证</li>
<li>符合公司相关政策的可提交佐证材料申请例外报销</li>
</ul>
</section>
<section>
<h4>适用规则</h4>
<ul>
<li>差旅报销管理办法2024第十二条住宿标准及超标处理</li>
<li>费用报销审批流程附件1国内差旅住宿标准</li>
</ul>
</section>
<footer>
<span>是否有帮助</span>
<button type="button" aria-label="有帮助"><i class="mdi mdi-thumb-up-outline"></i></button>
<button type="button" aria-label="无帮助"><i class="mdi mdi-thumb-down-outline"></i></button>
</footer>
</div>
</div>
</div>
<div class="talk-row user">
<span class="avatar user-avatar"></span>
<div class="talk-content">
<header><strong>张明</strong><time>10:35</time></header>
<p class="user-question">如果出差地公司名称不一致还能报销吗</p>
</div>
</div>
<div class="talk-row assistant">
<span class="avatar assistant-avatar"><i class="mdi mdi-robot-outline"></i></span>
<div class="talk-content">
<header><strong>财务AI助手</strong><time>10:35</time></header>
<div class="answer-card compact">
<section>
<h4>结论</h4>
<p>一般情况下差旅地与参会公司名称不一致需按异常处理建议提供情况说明并加盖公章或补充邀请材料</p>
</section>
<section>
<h4>适用规则</h4>
<ul>
<li>发票管理规定及失误销细则第二章发票基本要求</li>
<li>差旅报销管理办法附件1报销凭证要求</li>
</ul>
</section>
</div>
</div>
</div>
<div v-for="message in messages" :key="message.id" class="talk-row" :class="message.role === 'user' ? 'user' : 'assistant'">
<span class="avatar" :class="message.role === 'user' ? 'user-avatar' : 'assistant-avatar'">
<template v-if="message.role === 'user'"></template>
<i v-else class="mdi mdi-robot-outline"></i>
</span>
<div class="talk-content">
<header>
<strong>{{ message.role === 'user' ? '我' : '财务AI助手' }}</strong>
<time>刚刚</time>
</header>
<p v-if="message.role === 'user'" class="user-question">{{ message.text }}</p>
<div v-else class="agent-answer-content">
<template v-for="(block, blockIndex) in buildAnswerBlocks(message.text)" :key="`${message.id}-block-${blockIndex}`">
<p v-if="block.type === 'paragraph'" class="agent-answer">{{ block.text }}</p>
<div v-else-if="block.type === 'table'" class="agent-answer-table-wrap">
<table class="agent-answer-table">
<thead>
<tr>
<th v-for="header in block.headers" :key="`${message.id}-head-${header}`">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in block.rows" :key="`${message.id}-row-${rowIndex}`">
<td v-for="(cell, cellIndex) in row" :key="`${message.id}-cell-${rowIndex}-${cellIndex}`">{{ cell }}</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<div v-if="message.role !== 'user' && message.meta?.length" class="agent-meta-row">
<span v-for="item in message.meta" :key="item" class="agent-meta-chip">{{ item }}</span>
</div>
<div v-if="message.role !== 'user' && message.riskFlags?.length" class="agent-detail-block">
<strong>风险标签</strong>
<div class="agent-detail-chip-row">
<span v-for="item in message.riskFlags" :key="item" class="agent-risk-chip">{{ item }}</span>
</div>
</div>
<details v-if="message.role !== 'user' && message.citations?.length" class="agent-detail-block agent-citation-disclosure">
<summary>
<strong>引用依据</strong>
<span>{{ message.citations.length }} </span>
<i class="mdi mdi-chevron-down"></i>
</summary>
<div class="agent-citation-list">
<article v-for="item in message.citations" :key="`${message.id}-${item.code}`" class="agent-citation-card">
<header>
<span>{{ item.title }}</span>
<small>{{ item.version || item.source_type }}</small>
</header>
<p>{{ item.excerpt || item.code }}</p>
</article>
</div>
</details>
<div v-if="message.role !== 'user' && message.draftPayload" class="agent-draft-card">
<header>
<strong>{{ message.draftPayload.title }}</strong>
<span>待人工确认</span>
</header>
<pre>{{ message.draftPayload.body }}</pre>
</div>
</div>
</div>
</div>
<div class="composer-wrap">
<div class="prompt-toolbar">
<span>猜你想问</span>
<button v-for="prompt in visiblePrompts" :key="prompt.text" type="button" @click="applyPrompt(prompt.text)">
<i :class="prompt.icon"></i>
{{ prompt.short }}
</button>
<button class="icon-refresh" type="button" aria-label="换一批问题" @click="rotatePrompts">
<i class="mdi mdi-refresh"></i>
</button>
</div>
<div class="composer">
<textarea
:value="draft"
rows="2"
placeholder="请输入你的问题,例如:差旅报销特殊标准是什么?"
:disabled="sending"
@input="emit('draft', $event.target.value)"
@keydown.ctrl.enter.prevent="emit('send')"
></textarea>
<button
class="send-button"
type="button"
aria-label="发送问题"
:disabled="sending || !String(draft || '').trim()"
@click="emit('send')"
>
<i :class="sending ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
</button>
</div>
</div>
</article>
<aside class="right-column">
<article class="panel info-panel semantic-debug-panel">
<header>
<h3><i class="mdi mdi-shape-outline"></i> 语义解析调试</h3>
<button type="button" @click="useDraftAsSemanticInput">带入输入框</button>
</header>
<div class="semantic-debug-body">
<label class="semantic-debug-input">
<span>自然语言问题</span>
<textarea
v-model="semanticDraft"
rows="4"
placeholder="例如:查一下本周报销超标风险"
@keydown.ctrl.enter.prevent="parseSemanticQuery"
></textarea>
</label>
<div class="semantic-debug-actions">
<button
v-for="item in semanticExamples"
:key="item"
class="semantic-chip"
type="button"
@click="applySemanticExample(item)"
>
{{ item }}
</button>
</div>
<div class="semantic-debug-toolbar">
<button class="semantic-parse-btn" type="button" :disabled="semanticLoading" @click="parseSemanticQuery">
<i :class="semanticLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-play-circle-outline'"></i>
<span>{{ semanticLoading ? '解析中...' : '开始解析' }}</span>
</button>
<span class="semantic-inline-meta">
<template v-if="semanticResult">run_id{{ semanticResult.run_id }}</template>
<template v-else>支持 Ctrl + Enter</template>
</span>
</div>
<p v-if="semanticError" class="semantic-debug-error">{{ semanticError }}</p>
<div v-if="semanticResult" class="semantic-result-stack">
<div class="semantic-result-grid">
<article class="semantic-result-card">
<span>场景</span>
<strong>{{ semanticResult.scenario }}</strong>
</article>
<article class="semantic-result-card">
<span>意图</span>
<strong>{{ semanticResult.intent }}</strong>
</article>
<article class="semantic-result-card">
<span>权限</span>
<strong>{{ semanticResult.permission.level }}</strong>
</article>
<article class="semantic-result-card">
<span>置信度</span>
<strong>{{ semanticConfidenceLabel }}</strong>
</article>
</div>
<div class="semantic-field-list">
<section>
<h4>实体</h4>
<p>{{ semanticEntitiesText }}</p>
</section>
<section>
<h4>时间</h4>
<p>{{ semanticTimeRangeText }}</p>
</section>
<section>
<h4>指标</h4>
<p>{{ semanticMetricsText }}</p>
</section>
<section>
<h4>约束</h4>
<p>{{ semanticConstraintsText }}</p>
</section>
<section>
<h4>风险</h4>
<p>{{ semanticRiskFlagsText }}</p>
</section>
<section>
<h4>澄清</h4>
<p>{{ semanticClarificationText }}</p>
</section>
</div>
<div class="semantic-json-block">
<h4>原始 JSON</h4>
<pre>{{ semanticResultJson }}</pre>
</div>
</div>
</div>
</article>
<article class="panel info-panel hot-top-panel">
<header>
<h3><i class="mdi mdi-fire"></i> 热门问题 Top10</h3>
<button type="button" @click="rotatePrompts">换一批 <i class="mdi mdi-refresh"></i></button>
</header>
<div class="top-question-list">
<button v-for="(item, index) in hotQuestions" :key="item" type="button" @click="applyPrompt(item)">
<strong>{{ String(index + 1).padStart(2, '0') }}</strong>
<span>{{ item }}</span>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</article>
<article class="panel info-panel similar-panel">
<header>
<h3>相似历史问题</h3>
<button type="button">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</header>
<div class="similar-scroll">
<button v-for="item in similarQuestions" :key="item.text" class="similar-row" type="button" @click="applyPrompt(item.text)">
<span><i class="mdi mdi-file-question-outline"></i>{{ item.text }}</span>
<strong>{{ item.score }}</strong>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</article>
</aside>
</div>
</section>
</template>
<script src="./scripts/ChatView.js"></script>
<style scoped src="../assets/styles/views/chat-view.css"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,69 @@
import { useSystemState } from '../../composables/useSystemState.js'
import { fetchOntologyParse } from '../../services/ontology.js'
function isMarkdownTableDivider(line = '') {
const value = String(line || '').trim()
if (!value.includes('|')) return false
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(value)
}
function splitMarkdownTableRow(line = '') {
return String(line || '')
.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => cell.trim())
}
function buildAnswerBlocks(text = '') {
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n')
const blocks = []
let index = 0
while (index < lines.length) {
const line = lines[index].trim()
if (!line) {
index += 1
continue
}
if (
line.includes('|') &&
index + 1 < lines.length &&
isMarkdownTableDivider(lines[index + 1])
) {
const headers = splitMarkdownTableRow(line)
const rows = []
index += 2
while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
rows.push(splitMarkdownTableRow(lines[index]))
index += 1
}
blocks.push({ type: 'table', headers, rows })
continue
}
const paragraphLines = [line]
index += 1
while (
index < lines.length &&
lines[index].trim() &&
!(
lines[index].includes('|') &&
index + 1 < lines.length &&
isMarkdownTableDivider(lines[index + 1])
)
) {
paragraphLines.push(lines[index].trim())
index += 1
}
blocks.push({ type: 'paragraph', text: paragraphLines.join(' ') })
}
return blocks
}
export default {
name: 'ChatView',
props: {
@@ -170,7 +233,9 @@ export default {
role_codes: currentUser.value?.roleCodes || [],
is_admin: Boolean(currentUser.value?.isAdmin),
name: currentUser.value?.name || '',
role: currentUser.value?.role || ''
role: currentUser.value?.role || '',
position: currentUser.value?.position || '',
grade: currentUser.value?.grade || ''
}
})
} catch (error) {
@@ -212,6 +277,7 @@ export default {
semanticRiskFlagsText,
semanticClarificationText,
semanticResultJson,
buildAnswerBlocks,
applySemanticExample,
useDraftAsSemanticInput,
parseSemanticQuery

View File

@@ -228,6 +228,69 @@ function createMessage(role, text, attachments = [], extras = {}) {
}
}
function isMarkdownTableDivider(line = '') {
const value = String(line || '').trim()
if (!value.includes('|')) return false
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(value)
}
function splitMarkdownTableRow(line = '') {
return String(line || '')
.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => cell.trim())
}
function buildAnswerBlocks(text = '') {
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n')
const blocks = []
let index = 0
while (index < lines.length) {
const line = lines[index].trim()
if (!line) {
index += 1
continue
}
if (
line.includes('|') &&
index + 1 < lines.length &&
isMarkdownTableDivider(lines[index + 1])
) {
const headers = splitMarkdownTableRow(line)
const rows = []
index += 2
while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
rows.push(splitMarkdownTableRow(lines[index]))
index += 1
}
blocks.push({ type: 'table', headers, rows })
continue
}
const paragraphLines = [line]
index += 1
while (
index < lines.length &&
lines[index].trim() &&
!(
lines[index].includes('|') &&
index + 1 < lines.length &&
isMarkdownTableDivider(lines[index + 1])
)
) {
paragraphLines.push(lines[index].trim())
index += 1
}
blocks.push({ type: 'paragraph', text: paragraphLines.join(' ') })
}
return blocks
}
function formatMessageTime(value) {
if (!value) {
return nowTime()
@@ -3371,6 +3434,8 @@ export default {
is_admin: Boolean(user.isAdmin),
name: user.name || '',
role: user.role || '',
position: user.position || '',
grade: user.grade || '',
...buildClientTimeContext(),
session_type: activeSessionType.value,
entry_source: props.entrySource,
@@ -3787,6 +3852,7 @@ export default {
buildReviewRiskHint,
buildReviewActionHint,
buildReviewStatusTag,
buildAnswerBlocks,
buildExpenseQueryWindowLabel,
buildExpenseQueryHint,
getExpenseQueryActivePage,