优化报告信息与对话思考状态展示

This commit is contained in:
kx_guo 2026-09-15 10:43:02 +08:00
parent b5484c8adb
commit 92a86efe73
2 changed files with 157 additions and 10 deletions

View File

@ -220,11 +220,7 @@ function dimensionConclusion(item: {
蝶雅产品部 · 风险预警系统 · 自动生成 · {{ generatedAt }} 蝶雅产品部 · 风险预警系统 · 自动生成 · {{ generatedAt }}
</p> </p>
<div class="report-meta"> <div class="report-meta">
<span><b>产品/评估对象:</b>{{ report.subjectId }}</span> <span
><span
><b>供应商:</b
>{{ report.supplierFreq?.supplierName || "未指定" }}</span
><span
><b>报告编号:</b>RP-{{ report.subjectId.slice(0, 8) }}-{{ ><b>报告编号:</b>RP-{{ report.subjectId.slice(0, 8) }}-{{
report.score.toFixed(0) report.score.toFixed(0)
}}</span }}</span

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import axios from "axios"; import axios from "axios";
import { computed, nextTick, onMounted, ref, watch } from "vue"; import { computed, nextTick, onActivated, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import AssistantComposer from "@/components/assistant/AssistantComposer.vue"; import AssistantComposer from "@/components/assistant/AssistantComposer.vue";
@ -45,6 +45,7 @@ const tool = ref<
const toolInput = ref(""); const toolInput = ref("");
const toolError = ref(""); const toolError = ref("");
const toolFile = ref<File | null>(null); const toolFile = ref<File | null>(null);
const expandedReplies = ref(new Set<number>());
const assessmentFields = ref({ const assessmentFields = ref({
category: "", category: "",
specification: "", specification: "",
@ -110,6 +111,12 @@ onMounted(async () => {
await openFromRoute(); await openFromRoute();
}); });
onActivated(async () => {
// MainLayout keeps this page alive. Returning from report/dashboard therefore
// does not remount it, so explicitly reveal the newest message on re-entry.
if (conversation.value?.messages.length) await scrollToBottom("auto");
});
watch( watch(
() => route.query.conversation, () => route.query.conversation,
async () => { async () => {
@ -124,6 +131,40 @@ async function usePrompt(value: string) {
composer.value?.focus(); composer.value?.focus();
} }
function toggleReply(messageId: number) {
const next = new Set(expandedReplies.value);
if (next.has(messageId)) next.delete(messageId);
else next.add(messageId);
expandedReplies.value = next;
}
function canCollapseReply(content: string) {
return content.trim().length > 90;
}
function isMessageThinking(messageId: number, index: number) {
const messages = conversation.value?.messages ?? [];
return (
generating.value &&
index === messages.length - 1 &&
messages[index]?.id === messageId &&
messages[index]?.role === "assistant"
);
}
function thinkingDuration(index: number) {
const messages = conversation.value?.messages ?? [];
const message = messages[index];
const previous = messages[index - 1];
if (!message || !previous || previous.role !== "user") return null;
const elapsed = Math.round(
(new Date(message.createdAt).getTime() -
new Date(previous.createdAt).getTime()) /
1000,
);
return elapsed > 0 ? elapsed : null;
}
function openTool( function openTool(
value: value:
| "assessment" | "assessment"
@ -275,7 +316,11 @@ async function openFromRoute() {
const rawId = route.query.conversation; const rawId = route.query.conversation;
if (typeof rawId !== "string") return; if (typeof rawId !== "string") return;
const id = Number(rawId); const id = Number(rawId);
if (Number.isSafeInteger(id) && id > 0 && conversation.value?.id !== id) { if (Number.isSafeInteger(id) && id > 0 && conversation.value?.id === id) {
await scrollToBottom("auto");
return;
}
if (Number.isSafeInteger(id) && id > 0) {
try { try {
await conversationStore.open(id); await conversationStore.open(id);
await scrollToBottom("auto"); await scrollToBottom("auto");
@ -436,7 +481,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
/> />
<template <template
v-for="message in conversation?.messages ?? []" v-for="(message, messageIndex) in conversation?.messages ?? []"
:key="message.id" :key="message.id"
> >
<div class="message" :class="`message--${message.role}`"> <div class="message" :class="`message--${message.role}`">
@ -487,13 +532,70 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
<div <div
v-if=" v-if="
message.content && message.content &&
!message.riskResult && (message.riskResult ||
!message.responseCard message.responseCard ||
isMessageThinking(message.id, messageIndex))
" "
class="assistant-thinking"
>
<button
type="button"
class="assistant-thinking__header"
:disabled="
isMessageThinking(message.id, messageIndex) ||
!canCollapseReply(message.content)
"
@click="toggleReply(message.id)"
>
<span v-if="isMessageThinking(message.id, messageIndex)"
>思考中…</span
>
<span v-else>
已完成<span v-if="thinkingDuration(messageIndex)">
{{ thinkingDuration(messageIndex) }}s</span
>
</span>
<span
v-if="canCollapseReply(message.content)"
class="assistant-thinking__arrow"
>{{ expandedReplies.has(message.id) ? "⌄" : "›" }}</span
>
</button>
<div
class="assistant-bubble" class="assistant-bubble"
:class="{
'assistant-bubble--collapsed':
canCollapseReply(message.content) &&
!expandedReplies.has(message.id),
}"
> >
{{ message.content }} {{ message.content }}
</div> </div>
</div>
<div
v-else-if="message.content"
class="assistant-bubble"
:class="{
'assistant-bubble--collapsed':
canCollapseReply(message.content) &&
!expandedReplies.has(message.id),
}"
>
{{ message.content }}
</div>
<button
v-if="
message.content &&
!message.riskResult &&
!message.responseCard &&
canCollapseReply(message.content)
"
type="button"
class="reply-toggle"
@click="toggleReply(message.id)"
>
{{ expandedReplies.has(message.id) ? "收起" : "展开全文" }}
</button>
<RiskAnalysisCard <RiskAnalysisCard
v-if="message.riskResult" v-if="message.riskResult"
:result="message.riskResult" :result="message.riskResult"
@ -740,6 +842,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
} }
.assistant-bubble { .assistant-bubble {
position: relative;
width: fit-content; width: fit-content;
max-width: 82%; max-width: 82%;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
@ -751,6 +854,54 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
line-height: 1.65; line-height: 1.65;
} }
.assistant-bubble--collapsed {
display: -webkit-box;
max-height: calc(2 * 1.65em + 22px);
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.assistant-thinking {
margin-bottom: 8px;
}
.assistant-thinking__header {
display: flex;
width: auto;
align-items: center;
gap: 5px;
border: 0;
margin: 0 0 5px 2px;
padding: 0;
background: transparent;
color: var(--color-text-muted);
font-size: 11px;
font-family: inherit;
cursor: pointer;
}
.assistant-thinking__header:disabled {
cursor: default;
}
.assistant-thinking__arrow {
color: var(--color-text-faint);
font-size: 15px;
line-height: 1;
}
.reply-toggle {
display: block;
margin-top: 4px;
border: 0;
padding: 0;
background: transparent;
color: var(--color-accent);
font-size: 11px;
cursor: pointer;
}
.semantic-strip { .semantic-strip {
margin-bottom: 8px; margin-bottom: 8px;
color: var(--color-text-muted); color: var(--color-text-muted);