chore: 更新个人工作台和差旅报销相关功能

This commit is contained in:
caoxiaozhu
2026-05-19 17:24:13 +00:00
parent 54ffef66d3
commit 2574bc81d1
6 changed files with 14989 additions and 14022 deletions

View File

@@ -234,6 +234,255 @@
color: #0f172a; color: #0f172a;
} }
.flow-status-chip {
min-height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 10px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
font-size: 11px;
font-weight: 900;
white-space: nowrap;
}
.flow-status-chip.running {
background: #eff6ff;
color: #2563eb;
}
.flow-status-chip.completed {
background: #ecfdf5;
color: #059669;
}
.flow-status-chip.failed {
background: #fef2f2;
color: #dc2626;
}
.flow-icon-btn {
width: 34px;
height: 34px;
display: grid;
place-items: center;
padding: 0;
border: 1px solid rgba(203, 213, 225, 0.86);
border-radius: 12px;
background: rgba(255, 255, 255, 0.92);
color: #475569;
font-size: 16px;
box-shadow: 0 6px 14px rgba(148, 163, 184, 0.12);
}
.flow-icon-btn:hover:not(:disabled) {
border-color: rgba(37, 99, 235, 0.28);
color: #1d4ed8;
}
.flow-icon-btn:disabled {
opacity: 0.46;
cursor: not-allowed;
box-shadow: none;
}
.flow-step-item {
position: relative;
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 12px;
padding-bottom: 12px;
}
.flow-step-item:last-child {
padding-bottom: 0;
}
.flow-step-rail {
position: relative;
display: flex;
justify-content: center;
}
.flow-step-rail::after {
content: "";
position: absolute;
top: 34px;
bottom: -12px;
left: 50%;
width: 2px;
transform: translateX(-50%);
background: #e2e8f0;
}
.flow-step-item:last-child .flow-step-rail::after {
display: none;
}
.flow-step-rail span {
position: relative;
z-index: 1;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 999px;
background: #e2e8f0;
color: #64748b;
font-size: 12px;
font-weight: 900;
font-variant-numeric: tabular-nums;
}
.flow-step-item.completed .flow-step-rail span {
background: #10b981;
color: #fff;
}
.flow-step-item.running .flow-step-rail span {
background: #2563eb;
color: #fff;
box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.12);
}
.flow-step-item.failed .flow-step-rail span {
background: #ef4444;
color: #fff;
}
.flow-step-card {
min-width: 0;
display: grid;
gap: 7px;
padding: 13px 14px;
border: 1px solid #e5edf5;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 22px rgba(226, 232, 240, 0.34);
}
.flow-step-item.running .flow-step-card {
border-color: rgba(37, 99, 235, 0.42);
background: linear-gradient(180deg, #f8fbff 0%, #eef6ff 100%);
}
.flow-step-card header,
.flow-step-side {
display: flex;
align-items: center;
gap: 10px;
}
.flow-step-card header {
justify-content: space-between;
}
.flow-step-card strong {
min-width: 0;
color: #0f172a;
font-size: 13px;
font-weight: 900;
}
.flow-step-side {
flex: none;
gap: 8px;
}
.flow-step-status {
min-height: 24px;
display: inline-flex;
align-items: center;
padding: 0 9px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
font-size: 11px;
font-weight: 900;
white-space: nowrap;
}
.flow-step-status.completed {
background: #ecfdf5;
color: #059669;
}
.flow-step-status.running {
background: #eff6ff;
color: #2563eb;
}
.flow-step-status.failed {
background: #fef2f2;
color: #dc2626;
}
.flow-step-side time {
min-width: 36px;
color: #475569;
font-size: 12px;
font-weight: 850;
text-align: right;
font-variant-numeric: tabular-nums;
}
.flow-step-tool,
.flow-step-detail,
.flow-step-error {
margin: 0;
color: #64748b;
font-size: 12px;
line-height: 1.55;
}
.flow-step-detail {
color: #475569;
}
.flow-step-error {
color: #dc2626;
}
.flow-empty-state {
display: grid;
place-items: center;
align-content: center;
gap: 8px;
padding: 30px;
color: #64748b;
text-align: center;
}
.flow-empty-state i {
font-size: 34px;
color: #94a3b8;
}
.flow-empty-state strong {
color: #0f172a;
font-size: 14px;
font-weight: 900;
}
.flow-empty-state p {
max-width: 260px;
margin: 0;
font-size: 12px;
line-height: 1.6;
}
@keyframes flowPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.12);
}
}
.assistant-layout { .assistant-layout {
min-height: 0; min-height: 0;
flex: 1; flex: 1;
@@ -1562,6 +1811,23 @@
box-shadow: 0 6px 14px rgba(239, 68, 68, 0.16); box-shadow: 0 6px 14px rgba(239, 68, 68, 0.16);
} }
.review-insight-switch-icon-btn.flow.available {
border-color: rgba(37, 99, 235, 0.28);
background: rgba(239, 246, 255, 0.96);
color: #2563eb;
}
.review-insight-switch-icon-btn.flow.active {
border-color: rgba(37, 99, 235, 0.42);
background: rgba(219, 234, 254, 0.98);
color: #1d4ed8;
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.16);
}
.review-insight-switch-icon-btn.flow.running i {
animation: flowPulse 1.2s ease-in-out infinite;
}
.review-insight-switch-icon-btn:hover:not(:disabled) { .review-insight-switch-icon-btn:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -2542,6 +2808,49 @@
display: block; display: block;
} }
.review-flow-panel {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
}
.review-flow-summary {
display: flex;
align-items: center;
gap: 10px;
min-height: 34px;
color: #64748b;
font-size: 12px;
font-weight: 800;
}
.review-flow-summary .flow-icon-btn {
margin-left: auto;
}
.review-flow-list {
min-height: 0;
display: grid;
align-content: start;
gap: 0;
overflow-y: auto;
padding-right: 2px;
}
.review-flow-panel .flow-step-card {
border-radius: 14px;
box-shadow: none;
}
.review-flow-panel .flow-empty-state {
min-height: 260px;
}
.flow-empty-state.compact {
padding: 22px;
}
.review-message-block { .review-message-block {
margin-top: 12px; margin-top: 12px;
} }
@@ -2558,12 +2867,14 @@
display: grid; display: grid;
gap: 12px; gap: 12px;
padding: 15px; padding: 15px;
border-radius: 20px; border-radius: 18px;
border: 1px solid rgba(16, 185, 129, 0.14); border: 1px solid rgba(37, 99, 235, 0.14);
background: background:
radial-gradient(circle at top right, rgba(34, 197, 94, 0.08), transparent 28%), radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 30%),
linear-gradient(180deg, #fbfffd 0%, #f6fbf9 100%); linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
box-shadow: 0 8px 20px rgba(226, 232, 240, 0.28); box-shadow:
0 14px 30px rgba(15, 23, 42, 0.06),
0 1px 0 rgba(255, 255, 255, 0.9) inset;
} }
.review-flow-card { .review-flow-card {
@@ -2651,6 +2962,294 @@
padding: 12px 4px 0; padding: 12px 4px 0;
} }
.review-followup-panel {
display: grid;
gap: 0;
border: 1px solid #e2e8f0;
border-radius: 14px;
background: #fff;
overflow: hidden;
}
.review-followup-panel.pending {
border-color: #e2e8f0;
background: #fff;
}
.review-followup-panel.ready {
border-color: #d1fae5;
background: #fff;
}
.review-followup-panel summary {
list-style: none;
}
.review-followup-panel summary::-webkit-details-marker {
display: none;
}
.review-followup-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 12px;
cursor: pointer;
transition: background 0.18s ease;
}
.review-followup-head:hover {
background: #f8fafc;
}
.review-followup-head-main {
min-width: 0;
display: flex;
align-items: flex-start;
gap: 10px;
}
.review-followup-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
flex: none;
border-radius: 10px;
background: #f8fafc;
border: 1px solid #e2e8f0;
color: #64748b;
font-size: 17px;
}
.review-followup-panel.pending .review-followup-mark {
background: #fffbeb;
border-color: #fde68a;
color: #b45309;
}
.review-followup-panel.ready .review-followup-mark {
background: #ecfdf5;
border-color: #bbf7d0;
color: #059669;
}
.review-followup-title-copy {
min-width: 0;
display: grid;
gap: 4px;
}
.review-followup-title-copy strong {
color: #0f172a;
font-size: 13px;
font-weight: 900;
line-height: 1.35;
}
.review-followup-title-copy p {
margin: 0;
color: #64748b;
font-size: 11px;
line-height: 1.55;
}
.review-followup-preview {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
}
.review-followup-panel[open] .review-followup-preview {
display: none;
}
.review-followup-preview span {
min-height: 22px;
display: inline-flex;
align-items: center;
max-width: 160px;
padding: 0 8px;
border-radius: 999px;
background: #f8fafc;
border: 1px solid #e2e8f0;
color: #475569;
font-size: 10px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.review-followup-side {
display: inline-flex;
align-items: center;
gap: 8px;
flex: none;
}
.review-followup-count {
min-height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
padding: 0 10px;
border-radius: 999px;
border: 1px solid #e2e8f0;
background: #f8fafc;
color: #475569;
font-size: 11px;
font-weight: 850;
white-space: nowrap;
}
.review-followup-panel.pending .review-followup-count {
border-color: #fde68a;
background: #fffbeb;
color: #b45309;
}
.review-followup-panel.ready .review-followup-count {
border-color: rgba(16, 185, 129, 0.22);
background: #ecfdf5;
color: #047857;
}
.review-followup-chevron {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 999px;
color: #94a3b8;
font-size: 16px;
transition: transform 0.18s ease, color 0.18s ease;
}
.review-followup-panel[open] .review-followup-chevron {
transform: rotate(180deg);
color: #475569;
}
.review-followup-body {
display: grid;
gap: 10px;
padding: 0 12px 12px;
}
.review-followup-list {
display: grid;
gap: 0;
border-top: 1px solid #e2e8f0;
}
.review-followup-item {
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 52px;
padding: 10px 0;
border-bottom: 1px solid #f1f5f9;
}
.review-followup-item:last-child {
border-bottom: 0;
}
.review-followup-icon {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 9px;
background: #f8fafc;
color: #64748b;
font-size: 16px;
}
.review-followup-item.warning .review-followup-icon {
background: #fffbeb;
color: #b45309;
}
.review-followup-item.danger .review-followup-icon {
background: #fff1f2;
color: #e11d48;
}
.review-followup-item.ready .review-followup-icon {
background: #ecfdf5;
color: #059669;
}
.review-followup-copy {
min-width: 0;
display: grid;
gap: 4px;
}
.review-followup-copy strong {
color: #0f172a;
font-size: 13px;
font-weight: 850;
line-height: 1.35;
}
.review-followup-copy p {
margin: 0;
color: #64748b;
font-size: 11px;
line-height: 1.5;
}
.review-followup-status {
min-height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 999px;
background: transparent;
border: 1px solid #e2e8f0;
color: #64748b;
font-size: 10px;
font-weight: 850;
white-space: nowrap;
}
.review-followup-item.warning .review-followup-status {
border-color: #fde68a;
background: transparent;
color: #b45309;
}
.review-followup-item.danger .review-followup-status {
border-color: #fecdd3;
background: transparent;
color: #e11d48;
}
.review-followup-item.ready .review-followup-status {
border-color: #bbf7d0;
background: transparent;
color: #047857;
}
.review-followup-helper {
margin: 0;
padding: 9px 10px;
border-radius: 10px;
background: #f8fafc;
border: 1px solid #eef2f7;
color: #64748b;
font-size: 11px;
line-height: 1.6;
}
.review-card-head { .review-card-head {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -2671,10 +3270,10 @@
display: grid; display: grid;
place-items: center; place-items: center;
border-radius: 10px; border-radius: 10px;
background: linear-gradient(135deg, #22c55e, #10b981); background: linear-gradient(135deg, #2563eb, #0f766e);
color: #fff; color: #fff;
font-size: 16px; font-size: 16px;
box-shadow: 0 8px 16px rgba(16, 185, 129, 0.16); box-shadow: 0 8px 16px rgba(37, 99, 235, 0.16);
} }
.review-card-head-copy { .review-card-head-copy {
@@ -3674,6 +4273,57 @@
background: #fff; background: #fff;
} }
.welcome-quick-actions {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed rgba(203, 213, 225, 0.82);
}
.welcome-quick-actions-title {
margin: 0 0 10px;
color: #64748b;
font-size: 12px;
font-weight: 800;
}
.welcome-quick-action-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.welcome-quick-action-btn {
min-height: 36px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 14px;
border: 1px solid rgba(191, 219, 254, 0.92);
border-radius: 999px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 246, 255, 0.94) 100%);
color: #1d4ed8;
font-size: 12px;
font-weight: 750;
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.08);
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.welcome-quick-action-btn i {
font-size: 15px;
color: #2563eb;
}
.welcome-quick-action-btn:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(59, 130, 246, 0.34);
box-shadow: 0 10px 18px rgba(59, 130, 246, 0.14);
}
.welcome-quick-action-btn:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.welcome-grid { .welcome-grid {
display: grid; display: grid;
gap: 12px; gap: 12px;
@@ -3864,6 +4514,7 @@
.insight-panel-shell:not(.collapsed) { .insight-panel-shell:not(.collapsed) {
max-height: min(34dvh, 360px); max-height: min(34dvh, 360px);
} }
} }
@media (max-width: 760px) { @media (max-width: 760px) {
@@ -3900,6 +4551,10 @@
font-size: 16px; font-size: 16px;
} }
.flow-step-card header {
align-items: flex-start;
}
.assistant-layout { .assistant-layout {
padding: 14px; padding: 14px;
} }
@@ -3962,6 +4617,19 @@
flex-direction: column; flex-direction: column;
} }
.review-followup-head {
flex-direction: column;
}
.review-followup-side {
width: 100%;
justify-content: space-between;
}
.review-followup-count {
align-self: flex-start;
}
.metric-grid { .metric-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -3981,6 +4649,15 @@
justify-self: start; justify-self: start;
} }
.review-followup-item {
grid-template-columns: 34px minmax(0, 1fr);
}
.review-followup-status {
grid-column: 2;
justify-self: start;
}
.review-footer-btn-row { .review-footer-btn-row {
flex-direction: column; flex-direction: column;
} }

View File

@@ -280,8 +280,21 @@ async function handleExpenseConversationAction() {
return return
} }
pendingAction.value = 'expense'
const nextPayload = buildAssistantPayload() const nextPayload = buildAssistantPayload()
const shouldOpenImmediately = Boolean(nextPayload.prompt || nextPayload.files.length)
if (shouldOpenImmediately) {
emitAssistant({
...nextPayload,
conversation: null
})
void clearKnowledgeHistoryBeforeExpense().catch((error) => {
console.warn('Failed to clear knowledge history before expense:', error)
})
return
}
pendingAction.value = 'expense'
try { try {
await clearKnowledgeHistoryBeforeExpense() await clearKnowledgeHistoryBeforeExpense()
@@ -1131,4 +1144,3 @@ watch(
} }
} }
</style> </style>

View File

@@ -1,9 +1,17 @@
<template> <template>
<PersonalWorkbench :show-header="false" @open-assistant="emit('openAssistant', $event)" /> <PersonalWorkbench
:show-header="false"
:assistant-modal-open="assistantModalOpen"
@open-assistant="emit('openAssistant', $event)"
/>
</template> </template>
<script setup> <script setup>
import PersonalWorkbench from '../components/business/PersonalWorkbench.vue' import PersonalWorkbench from '../components/business/PersonalWorkbench.vue'
defineProps({
assistantModalOpen: { type: Boolean, default: false }
})
const emit = defineEmits(['openAssistant']) const emit = defineEmits(['openAssistant'])
</script> </script>

View File

@@ -46,7 +46,13 @@
</div> </div>
</header> </header>
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }"> <div
class="assistant-layout"
:class="{
'can-show-insight': hasInsightPanelContent,
'has-insight': showInsightPanel
}"
>
<section class="dialog-panel"> <section class="dialog-panel">
<div v-if="shortcuts.length" class="dialog-toolbar"> <div v-if="shortcuts.length" class="dialog-toolbar">
<button <button
@@ -72,7 +78,7 @@
<span class="message-avatar"> <span class="message-avatar">
<img <img
:src="message.role === 'assistant' ? aiAvatar : userAvatar" :src="message.role === 'assistant' ? aiAvatar : userAvatar"
:alt="message.role === 'assistant' ? 'AI 助手头像' : '用户头像'" :alt="message.role === 'assistant' ? '财务助手头像' : '用户头像'"
/> />
</span> </span>
@@ -92,9 +98,9 @@
v-else-if="message.text && message.role === 'assistant'" v-else-if="message.text && message.role === 'assistant'"
class="message-answer-content message-answer-markdown" class="message-answer-content message-answer-markdown"
v-html="renderMarkdown(message.text)" v-html="renderMarkdown(message.text)"
></motion> ></div>
<motion <div
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length" v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
class="welcome-quick-actions" class="welcome-quick-actions"
> >
@@ -111,12 +117,12 @@
<i :class="action.icon"></i> <i :class="action.icon"></i>
<span>{{ action.label }}</span> <span>{{ action.label }}</span>
</button> </button>
</motion> </div>
</motion> </div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row"> <div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row">
<span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span> <span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span>
</motion> </div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block"> <div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
<strong>风险标签</strong> <strong>风险标签</strong>
@@ -256,35 +262,62 @@
</span> </span>
</div> </div>
<details class="review-disclosure-card" :open="shouldOpenReviewDisclosure(message.reviewPayload)"> <details
<summary class="review-disclosure-summary"> class="review-followup-panel"
<div class="review-disclosure-copy"> :class="buildReviewStateTone(message.reviewPayload, message.draftPayload)"
<strong>{{ buildReviewDisclosureTitle(message.reviewPayload) }}</strong> :open="shouldOpenReviewDisclosure(message.reviewPayload)"
<p>{{ buildReviewDisclosureHint(message.reviewPayload) }}</p> >
<summary class="review-followup-head">
<div class="review-followup-head-main">
<span class="review-followup-mark">
<i :class="buildReviewStateTone(message.reviewPayload, message.draftPayload) === 'ready' ? 'mdi mdi-clipboard-check-outline' : 'mdi mdi-clipboard-text-clock-outline'"></i>
</span>
<div class="review-followup-title-copy">
<strong>{{ buildReviewTodoSectionTitle(message.reviewPayload) }}</strong>
<p>{{ buildReviewDisclosureHint(message.reviewPayload) }}</p>
<div class="review-followup-preview">
<span
v-for="item in buildReviewTodoItems(message.reviewPayload).slice(0, 2)"
:key="`${message.id}-preview-${item.key}`"
>
{{ item.title }}
</span>
<span v-if="buildReviewTodoItems(message.reviewPayload).length > 2">
+{{ buildReviewTodoItems(message.reviewPayload).length - 2 }}
</span>
</div>
</div>
</div>
<div class="review-followup-side">
<span class="review-followup-count">{{ buildReviewTodoSectionMeta(message.reviewPayload) }}</span>
<span class="review-followup-chevron">
<i class="mdi mdi-chevron-down"></i>
</span>
</div> </div>
<span class="review-disclosure-toggle">
<i class="mdi mdi-chevron-down"></i>
</span>
</summary> </summary>
<div class="review-disclosure-body"> <div class="review-followup-body">
<div class="review-pending-list plain"> <div class="review-followup-list">
<!-- 待补充信息项 -->
<article <article
v-for="item in buildReviewTodoItems(message.reviewPayload)" v-for="item in buildReviewTodoItems(message.reviewPayload)"
:key="`${message.id}-${item.key}`" :key="`${message.id}-${item.key}`"
class="review-pending-item" class="review-followup-item"
:class="item.tone"
> >
<span class="review-pending-icon"> <span class="review-followup-icon">
<i :class="item.icon"></i> <i :class="item.icon"></i>
</span> </span>
<div class="review-pending-copy"> <div class="review-followup-copy">
<strong>{{ item.title }}</strong> <strong>{{ item.title }}</strong>
<p>{{ item.hint }}</p> <p>{{ item.hint }}</p>
</div> </div>
<span class="review-pending-status" :class="item.tone">{{ item.status }}</span> <span class="review-followup-status">{{ item.status }}</span>
</article> </article>
</div> </div>
<p v-if="buildReviewDecisionHint(message.reviewPayload)" class="review-followup-helper">
{{ buildReviewDecisionHint(message.reviewPayload) }}
</p>
</div> </div>
</details> </details>
@@ -538,7 +571,7 @@
:placeholder="composerPlaceholder" :placeholder="composerPlaceholder"
:disabled="submitting || reviewActionBusy || sessionSwitchBusy" :disabled="submitting || reviewActionBusy || sessionSwitchBusy"
@input="handleComposerInput" @input="handleComposerInput"
@keydown.enter.exact.stop @keydown.enter.exact.prevent="handleComposerEnter"
@keydown.ctrl.enter.prevent="submitComposer" @keydown.ctrl.enter.prevent="submitComposer"
/> />
</div> </div>
@@ -558,9 +591,13 @@
:aria-hidden="(!showInsightPanel).toString()" :aria-hidden="(!showInsightPanel).toString()"
> >
<aside class="insight-panel"> <aside class="insight-panel">
<div v-if="!isKnowledgeSession" class="insight-head" :class="{ 'review-mode': activeReviewPayload }"> <div
v-if="!isKnowledgeSession"
class="insight-head"
:class="{ 'review-mode': activeReviewPayload || isReviewFlowDrawer }"
>
<div> <div>
<div v-if="!activeReviewPayload" class="insight-head-eyebrow"> <div v-if="!activeReviewPayload && !isReviewFlowDrawer" class="insight-head-eyebrow">
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span> <span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
</div> </div>
<div v-else class="review-insight-title-row"> <div v-else class="review-insight-title-row">
@@ -568,12 +605,13 @@
<h3>{{ reviewDrawerTitle }}</h3> <h3>{{ reviewDrawerTitle }}</h3>
</div> </div>
</div> </div>
<h3 v-if="!activeReviewPayload">{{ currentInsight.title }}</h3> <h3 v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.title }}</h3>
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p> <p v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.summary }}</p>
</div> </div>
<div v-if="activeReviewPayload" class="review-insight-tools"> <div v-if="activeReviewPayload || isReviewFlowDrawer" class="review-insight-tools">
<button <button
v-if="activeReviewPayload"
type="button" type="button"
class="review-insight-switch-icon-btn" class="review-insight-switch-icon-btn"
:class="{ :class="{
@@ -589,6 +627,7 @@
</button> </button>
<button <button
v-if="activeReviewPayload"
type="button" type="button"
class="review-insight-switch-icon-btn risk" class="review-insight-switch-icon-btn risk"
:class="{ :class="{
@@ -602,9 +641,25 @@
> >
<i :class="reviewRiskDrawerIcon"></i> <i :class="reviewRiskDrawerIcon"></i>
</button> </button>
<button
type="button"
class="review-insight-switch-icon-btn flow"
:class="{
available: reviewFlowDrawerAvailable,
active: reviewFlowDrawerAvailable && isReviewFlowDrawer,
running: flowOverallStatusTone === 'running'
}"
:disabled="!reviewFlowDrawerAvailable || submitting || reviewActionBusy"
:title="reviewFlowDrawerLabel"
:aria-label="reviewFlowDrawerLabel"
@click="toggleReviewFlowDrawer"
>
<i :class="reviewFlowDrawerIcon"></i>
</button>
</div> </div>
<div class="confidence-card" v-if="!activeReviewPayload"> <div class="confidence-card" v-if="!activeReviewPayload && !isReviewFlowDrawer">
<span>{{ currentInsight.metricLabel }}</span> <span>{{ currentInsight.metricLabel }}</span>
<strong>{{ currentInsight.metricValue }}</strong> <strong>{{ currentInsight.metricValue }}</strong>
</div> </div>
@@ -639,9 +694,60 @@
</section> </section>
</template> </template>
<template v-else-if="isReviewFlowDrawer">
<section class="review-flow-panel">
<div class="review-flow-summary">
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
<span>总耗时 {{ flowTotalDurationText }}</span>
<button
type="button"
class="flow-icon-btn"
:disabled="!flowRunId || flowRefreshBusy"
title="刷新流程"
aria-label="刷新流程"
@click="refreshFlowRunDetail"
>
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
</button>
</div>
<div v-if="flowSteps.length" class="review-flow-list">
<article
v-for="(step, index) in flowSteps"
:key="step.key"
class="flow-step-item"
:class="step.status"
>
<div class="flow-step-rail">
<span>{{ index + 1 }}</span>
</div>
<div class="flow-step-card">
<header>
<strong>{{ step.title }}</strong>
<div class="flow-step-side">
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
<time>{{ formatFlowStepDuration(step) }}</time>
</div>
</header>
<p class="flow-step-tool">工具{{ step.tool }}</p>
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
</div>
</article>
</div>
<div v-else class="flow-empty-state compact">
<i class="mdi mdi-timeline-question-outline"></i>
<strong>暂无识别流程</strong>
<p>发起识别后这里会显示调用步骤和耗时</p>
</div>
</section>
</template>
<template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent"> <template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent">
<template v-if="activeReviewPayload"> <template v-if="activeReviewPayload">
<template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer"> <template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer && !isReviewFlowDrawer">
<section class="review-side-card review-side-overview-card"> <section class="review-side-card review-side-overview-card">
<div class="review-side-intent-row"> <div class="review-side-intent-row">
<i class="mdi mdi-account-outline"></i> <i class="mdi mdi-account-outline"></i>
@@ -778,6 +884,57 @@
</template> </template>
<template v-else-if="isReviewFlowDrawer">
<section class="review-flow-panel">
<div class="review-flow-summary">
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
<span>总耗时 {{ flowTotalDurationText }}</span>
<button
type="button"
class="flow-icon-btn"
:disabled="!flowRunId || flowRefreshBusy"
title="刷新流程"
aria-label="刷新流程"
@click="refreshFlowRunDetail"
>
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
</button>
</div>
<div v-if="flowSteps.length" class="review-flow-list">
<article
v-for="(step, index) in flowSteps"
:key="step.key"
class="flow-step-item"
:class="step.status"
>
<div class="flow-step-rail">
<span>{{ index + 1 }}</span>
</div>
<div class="flow-step-card">
<header>
<strong>{{ step.title }}</strong>
<div class="flow-step-side">
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
<time>{{ formatFlowStepDuration(step) }}</time>
</div>
</header>
<p class="flow-step-tool">工具{{ step.tool }}</p>
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
</div>
</article>
</div>
<div v-else class="flow-empty-state compact">
<i class="mdi mdi-timeline-question-outline"></i>
<strong>暂无识别流程</strong>
<p>发起识别后这里会显示调用步骤和耗时</p>
</div>
</section>
</template>
<template v-else-if="isReviewDocumentDrawer"> <template v-else-if="isReviewDocumentDrawer">
<section class="review-side-card review-document-switch-card review-ticket-drawer"> <section class="review-side-card review-document-switch-card review-ticket-drawer">
<div class="review-side-head review-document-switch-head"> <div class="review-side-head review-document-switch-head">
@@ -996,6 +1153,7 @@
</Transition> </Transition>
</aside> </aside>
</div> </div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -185,6 +185,7 @@ const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const REVIEW_DRAWER_MODE_REVIEW = 'review' const REVIEW_DRAWER_MODE_REVIEW = 'review'
const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents' const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents'
const REVIEW_DRAWER_MODE_RISK = 'risk' const REVIEW_DRAWER_MODE_RISK = 'risk'
const REVIEW_DRAWER_MODE_FLOW = 'flow'
const FLOW_STEP_STATUS_PENDING = 'pending' const FLOW_STEP_STATUS_PENDING = 'pending'
const FLOW_STEP_STATUS_RUNNING = 'running' const FLOW_STEP_STATUS_RUNNING = 'running'
const FLOW_STEP_STATUS_COMPLETED = 'completed' const FLOW_STEP_STATUS_COMPLETED = 'completed'
@@ -208,6 +209,12 @@ const FLOW_STEP_FALLBACKS = {
runningText: '正在识别票据附件...', runningText: '正在识别票据附件...',
completedText: '票据识别完成' completedText: '票据识别完成'
}, },
agent: {
title: '智能体编排',
tool: 'UserAgent',
runningText: '正在调用财务智能体...',
completedText: '智能体处理完成'
},
result: { result: {
title: '生成结果', title: '生成结果',
tool: 'ResultGenerator', tool: 'ResultGenerator',
@@ -230,7 +237,7 @@ const EXPENSE_WELCOME_QUICK_ACTIONS = [
}, },
{ {
label: '交通费报销', label: '交通费报销',
prompt: '我要报销交通出行费用,请帮我识别常见票据类型和报销注意事项。', prompt: '我要报销交通出行费用,请帮我识别场景并列出待补充信息。',
icon: 'mdi mdi-car-outline' icon: 'mdi mdi-car-outline'
}, },
{ {
@@ -250,41 +257,6 @@ const EXPENSE_WELCOME_QUICK_ACTIONS = [
} }
] ]
const ASSISTANT_DISPLAY_NAME = '财务助手'
const EXPENSE_WELCOME_QUICK_ACTIONS = [
{
label: '发起差旅报销',
prompt: '我要报销一笔出差费用,请帮我说明需要准备的材料,并引导我上传票据。',
icon: 'mdi mdi-bag-suitcase-outline'
},
{
label: '招待费报销',
prompt: '我要报销客户招待餐费,请告诉我需要补充的客户、参与人员和票据要求。',
icon: 'mdi mdi-food-fork-drink'
},
{
label: '交通费报销',
prompt: '我要报销交通出行费用,请帮我识别场景并列出待补充信息。',
icon: 'mdi mdi-car-outline'
},
{
label: '上传票据识别',
prompt: '我已准备好票据,请帮我识别票据内容并生成报销草稿。',
icon: 'mdi mdi-file-upload-outline'
},
{
label: '查询近期报销',
prompt: '帮我查询近10天的报销记录和金额汇总。',
icon: 'mdi mdi-chart-timeline-variant'
},
{
label: '解释报销风险',
prompt: '请结合公司制度,说明酒店超标、发票抬头不一致等常见报销风险与处理方式。',
icon: 'mdi mdi-shield-alert-outline'
}
]
const HOT_KNOWLEDGE_QUESTIONS = [ const HOT_KNOWLEDGE_QUESTIONS = [
'差旅住宿标准按什么规则执行?', '差旅住宿标准按什么规则执行?',
'酒店超标后如何申请例外报销?', '酒店超标后如何申请例外报销?',
@@ -373,8 +345,39 @@ function formatMessageTime(value) {
}) })
} }
function createFlowSteps() { function createFlowSteps(options = {}) {
return [] const keys = []
if (options.includeIntent) {
keys.push('intent')
}
if (options.includeOcr) {
keys.push('ocr')
}
if (options.includeExtraction) {
keys.push('extraction')
}
if (options.includeAgent) {
keys.push('agent')
}
if (options.includeResult) {
keys.push('result')
}
return keys.map((key, index) => {
const definition = FLOW_STEP_FALLBACKS[key] || {}
return {
key,
index: index + 1,
title: definition.title || '智能体工具调用',
tool: definition.tool || 'AgentTool',
status: FLOW_STEP_STATUS_PENDING,
detail: '',
durationMs: null,
startedAt: 0,
finishedAt: 0,
error: ''
}
})
} }
function formatSemanticEntityValue(entity) { function formatSemanticEntityValue(entity) {
@@ -2888,7 +2891,6 @@ export default {
url: '' url: ''
}) })
const sessionSwitchBusy = ref(false) const sessionSwitchBusy = ref(false)
const flowPanelOpen = ref(false)
const flowRunId = ref('') const flowRunId = ref('')
const flowStartedAt = ref(0) const flowStartedAt = ref(0)
const flowFinishedAt = ref(0) const flowFinishedAt = ref(0)
@@ -2950,8 +2952,24 @@ export default {
} }
return total ? `待执行 0/${total}` : '暂无流程' return total ? `待执行 0/${total}` : '暂无流程'
}) })
const flowTotalDurationText = computed(() => {
if (!flowStartedAt.value) {
return '--'
}
const finishedAt = flowFinishedAt.value || (runningFlowStep.value ? flowTick.value : 0)
if (finishedAt > flowStartedAt.value) {
return formatFlowDuration(finishedAt - flowStartedAt.value)
}
const measuredDuration = flowSteps.value.reduce((total, step) => {
const duration = Number(step.durationMs)
return total + (Number.isFinite(duration) && duration > 0 ? duration : 0)
}, 0)
return measuredDuration ? formatFlowDuration(measuredDuration) : '--'
})
const hasInsightPanelContent = computed( const hasInsightPanelContent = computed(
() => isKnowledgeSession.value || currentInsight.value.intent !== 'welcome' () => isKnowledgeSession.value || currentInsight.value.intent !== 'welcome' || flowSteps.value.length > 0
) )
const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value) const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value)
const insightPanelToggleLabel = computed(() => const insightPanelToggleLabel = computed(() =>
@@ -3027,18 +3045,22 @@ export default {
const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0) const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0)
const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value) const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value)
const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0) const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0)
const reviewFlowDrawerAvailable = computed(() => flowSteps.value.length > 0)
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value)) const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value)) const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value)) const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value))
const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length) const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length)
const isReviewDocumentDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) const isReviewDocumentDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS)
const isReviewRiskDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) const isReviewRiskDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK)
const isReviewFlowDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW)
const reviewDrawerTitle = computed(() => ( const reviewDrawerTitle = computed(() => (
isReviewDocumentDrawer.value isReviewDocumentDrawer.value
? '票据识别结果' ? '票据识别结果'
: isReviewRiskDrawer.value : isReviewRiskDrawer.value
? '风险提示' ? '风险提示'
: '报销识别核对' : isReviewFlowDrawer.value
? '调用流程'
: '报销识别核对'
)) ))
const reviewDocumentDrawerLabel = computed(() => ( const reviewDocumentDrawerLabel = computed(() => (
isReviewDocumentDrawer.value ? '显示核对' : '显示票据' isReviewDocumentDrawer.value ? '显示核对' : '显示票据'
@@ -3056,6 +3078,14 @@ export default {
? 'mdi mdi-shield-alert' ? 'mdi mdi-shield-alert'
: 'mdi mdi-shield-alert-outline' : 'mdi mdi-shield-alert-outline'
)) ))
const reviewFlowDrawerLabel = computed(() => (
isReviewFlowDrawer.value ? '显示核对' : '显示流程'
))
const reviewFlowDrawerIcon = computed(() => (
isReviewFlowDrawer.value
? 'mdi mdi-timeline-clock'
: 'mdi mdi-timeline-clock-outline'
))
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null) const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
const activeReviewDocumentPreview = computed(() => const activeReviewDocumentPreview = computed(() =>
activeReviewDocument.value activeReviewDocument.value
@@ -3305,6 +3335,15 @@ export default {
} }
) )
watch(
() => reviewFlowDrawerAvailable.value,
(available) => {
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
}
}
)
watch( watch(
() => composerDraft.value, () => composerDraft.value,
() => { () => {
@@ -3377,7 +3416,6 @@ export default {
flowStartedAt.value = 0 flowStartedAt.value = 0
flowFinishedAt.value = 0 flowFinishedAt.value = 0
flowSteps.value = createFlowSteps() flowSteps.value = createFlowSteps()
flowPanelOpen.value = false
} }
function adjustComposerTextareaHeight() { function adjustComposerTextareaHeight() {
@@ -3408,14 +3446,6 @@ export default {
submitComposer() submitComposer()
} }
function toggleFlowPanel() {
flowPanelOpen.value = !flowPanelOpen.value
}
function openFlowPanel() {
flowPanelOpen.value = true
}
function clearFlowSimulationTimers() { function clearFlowSimulationTimers() {
while (flowSimulationTimers.length) { while (flowSimulationTimers.length) {
const timerId = flowSimulationTimers.pop() const timerId = flowSimulationTimers.pop()
@@ -3424,25 +3454,22 @@ export default {
} }
} }
function scheduleFlowPanelAutoCollapse(delayMs = 1200) { function resetFlowRun(options = {}) {
const collapseTimer = window.setTimeout(() => {
if (runningFlowStep.value || flowRefreshBusy.value || submitting.value) {
return
}
if (flowSteps.value.length && !flowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) {
flowPanelOpen.value = false
}
}, delayMs)
flowSimulationTimers.push(collapseTimer)
}
function resetFlowRun() {
clearFlowSimulationTimers() clearFlowSimulationTimers()
flowPanelOpen.value = true
flowRunId.value = '' flowRunId.value = ''
flowStartedAt.value = Date.now() flowStartedAt.value = Date.now()
flowFinishedAt.value = 0 flowFinishedAt.value = 0
flowSteps.value = createFlowSteps() reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
insightPanelCollapsed.value = false
const hasText = Boolean(String(options.rawText || '').trim())
const attachmentCount = Number(options.attachmentCount || 0)
flowSteps.value = createFlowSteps({
includeIntent: hasText,
includeOcr: attachmentCount > 0,
includeExtraction: hasText || attachmentCount > 0,
includeAgent: true,
includeResult: true
})
} }
function findFlowDefinition(key) { function findFlowDefinition(key) {
@@ -3476,10 +3503,22 @@ export default {
} }
} }
function normalizeFlowStepIndexes(steps) {
return steps.map((step, index) => ({ ...step, index: index + 1 }))
}
function upsertFlowStep(key, patch) { function upsertFlowStep(key, patch) {
const existingStep = flowSteps.value.find((step) => step.key === key) const existingStep = flowSteps.value.find((step) => step.key === key)
if (!existingStep) { if (!existingStep) {
flowSteps.value = [...flowSteps.value, createFlowStep(key, patch)] const nextStep = createFlowStep(key, patch)
const resultIndex = flowSteps.value.findIndex((step) => step.key === 'result')
if (resultIndex !== -1 && key !== 'result') {
const nextSteps = [...flowSteps.value]
nextSteps.splice(resultIndex, 0, nextStep)
flowSteps.value = normalizeFlowStepIndexes(nextSteps)
return
}
flowSteps.value = normalizeFlowStepIndexes([...flowSteps.value, nextStep])
return return
} }
const normalizedPatch = normalizeFlowStepPatch(key, patch) const normalizedPatch = normalizeFlowStepPatch(key, patch)
@@ -3660,6 +3699,13 @@ export default {
) )
} }
if (flowSteps.value.some((step) => step.key === 'agent')) {
completePendingFlowStep(
'agent',
toolCalls.length ? `已完成 ${toolCalls.length} 个工具调用` : FLOW_STEP_FALLBACKS.agent.completedText
)
}
toolCalls.forEach((toolCall, index) => { toolCalls.forEach((toolCall, index) => {
const meta = resolveToolCallFlowMeta(toolCall, index) const meta = resolveToolCallFlowMeta(toolCall, index)
const failed = String(toolCall?.status || '').toLowerCase() === 'failed' const failed = String(toolCall?.status || '').toLowerCase() === 'failed'
@@ -3687,10 +3733,17 @@ export default {
if (!answer && !payload?.result) { if (!answer && !payload?.result) {
return return
} }
flowSteps.value
.filter((step) => step.key !== 'result' && ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
.forEach((step) => {
completeFlowStep(step.key, resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED }))
})
startFlowStep('result', '正在返回处理结果...') startFlowStep('result', '正在返回处理结果...')
completeFlowStep('result', '结果已返回到对话区', resolveResultStepDurationMs(run)) completeFlowStep('result', '结果已返回到对话区', resolveResultStepDurationMs(run))
flowFinishedAt.value = Date.now() flowFinishedAt.value = Date.now()
scheduleFlowPanelAutoCollapse() if (reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
}
} }
async function refreshFlowRunDetail() { async function refreshFlowRunDetail() {
@@ -3717,6 +3770,38 @@ export default {
return formatFlowDuration(step?.durationMs) return formatFlowDuration(step?.durationMs)
} }
function resolveFlowStepStatusLabel(step) {
const status = String(step?.status || '').trim()
if (status === FLOW_STEP_STATUS_COMPLETED) {
return '完成'
}
if (status === FLOW_STEP_STATUS_RUNNING) {
return '执行中'
}
if (status === FLOW_STEP_STATUS_FAILED) {
return '异常'
}
return '待执行'
}
function resolveFlowStepDetail(step) {
const detail = String(step?.detail || '').trim()
if (detail) {
return detail
}
const definition = findFlowDefinition(step?.key)
if (step?.status === FLOW_STEP_STATUS_COMPLETED) {
return definition?.completedText || '步骤已完成'
}
if (step?.status === FLOW_STEP_STATUS_RUNNING) {
return definition?.runningText || '正在执行当前步骤...'
}
if (step?.status === FLOW_STEP_STATUS_FAILED) {
return step?.error || '步骤执行异常'
}
return definition?.runningText ? `等待${definition.title || '当前步骤'}...` : '等待智能体调度...'
}
function buildComposerBusinessTimeLabel() { function buildComposerBusinessTimeLabel() {
if (composerDateMode.value === 'single') { if (composerDateMode.value === 'single') {
return `业务发生时间:${composerSingleDate.value}` return `业务发生时间:${composerSingleDate.value}`
@@ -4064,6 +4149,16 @@ export default {
: REVIEW_DRAWER_MODE_RISK : REVIEW_DRAWER_MODE_RISK
} }
function toggleReviewFlowDrawer() {
if (!reviewFlowDrawerAvailable.value) {
return
}
reviewDrawerMode.value =
reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW
? REVIEW_DRAWER_MODE_REVIEW
: REVIEW_DRAWER_MODE_FLOW
}
function setInlineReviewFieldError(key, message) { function setInlineReviewFieldError(key, message) {
reviewInlineErrors.value = { reviewInlineErrors.value = {
...reviewInlineErrors.value, ...reviewInlineErrors.value,
@@ -4438,7 +4533,7 @@ export default {
return null return null
} }
resetFlowRun() resetFlowRun({ rawText, attachmentCount: files.length })
if (rawText) { if (rawText) {
startFlowStep('intent', '正在识别业务意图...') startFlowStep('intent', '正在识别业务意图...')
startSemanticFlowPreview(rawText, { attachmentCount: files.length }) startSemanticFlowPreview(rawText, { attachmentCount: files.length })
@@ -4524,6 +4619,17 @@ export default {
extraContext.review_action = 'create_new_claim_from_documents' extraContext.review_action = 'create_new_claim_from_documents'
} }
const runningExtractionStep = flowSteps.value.find(
(step) => step.key === 'extraction' && step.status === FLOW_STEP_STATUS_RUNNING
)
if (runningExtractionStep) {
completeFlowStep(
'extraction',
runningExtractionStep.detail || FLOW_STEP_FALLBACKS.extraction.completedText
)
}
startFlowStep('agent', FLOW_STEP_FALLBACKS.agent.runningText)
const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary) const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary)
const payload = await runOrchestrator( const payload = await runOrchestrator(
{ {
@@ -4887,12 +4993,13 @@ export default {
closeComposerDatePicker, closeComposerDatePicker,
setComposerDateMode, setComposerDateMode,
removeComposerBusinessTimeTag, removeComposerBusinessTimeTag,
flowPanelOpen,
flowSteps, flowSteps,
flowRunId, flowRunId,
flowRefreshBusy, flowRefreshBusy,
completedFlowStepCount,
flowOverallStatusTone, flowOverallStatusTone,
flowOverallStatusText, flowOverallStatusText,
flowTotalDurationText,
attachedFiles, attachedFiles,
composerFilesExpanded, composerFilesExpanded,
visibleAttachedFiles, visibleAttachedFiles,
@@ -4918,13 +5025,17 @@ export default {
reviewDrawerMode, reviewDrawerMode,
isReviewDocumentDrawer, isReviewDocumentDrawer,
isReviewRiskDrawer, isReviewRiskDrawer,
isReviewFlowDrawer,
reviewDrawerTitle, reviewDrawerTitle,
reviewDocumentDrawerAvailable, reviewDocumentDrawerAvailable,
reviewRiskDrawerAvailable, reviewRiskDrawerAvailable,
reviewFlowDrawerAvailable,
reviewDocumentDrawerLabel, reviewDocumentDrawerLabel,
reviewDocumentDrawerIcon, reviewDocumentDrawerIcon,
reviewRiskDrawerLabel, reviewRiskDrawerLabel,
reviewRiskDrawerIcon, reviewRiskDrawerIcon,
reviewFlowDrawerLabel,
reviewFlowDrawerIcon,
activeReviewDocument, activeReviewDocument,
activeReviewDocumentIndex, activeReviewDocumentIndex,
activeReviewDocumentPreview, activeReviewDocumentPreview,
@@ -5005,13 +5116,14 @@ export default {
askHotKnowledgeQuestion, askHotKnowledgeQuestion,
resolveKnowledgeRankLabel, resolveKnowledgeRankLabel,
resolveKnowledgeRankTone, resolveKnowledgeRankTone,
toggleFlowPanel,
openFlowPanel,
refreshFlowRunDetail, refreshFlowRunDetail,
formatFlowStepDuration, formatFlowStepDuration,
resolveFlowStepStatusLabel,
resolveFlowStepDetail,
toggleInsightPanel, toggleInsightPanel,
toggleReviewDocumentDrawer, toggleReviewDocumentDrawer,
toggleReviewRiskDrawer, toggleReviewRiskDrawer,
toggleReviewFlowDrawer,
toggleAttachedFilesExpanded, toggleAttachedFilesExpanded,
removeAttachedFile, removeAttachedFile,
clearAttachedFiles, clearAttachedFiles,