feat: 集成Hermes智能体系统,增强聊天和差旅报销功能
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user