优化报告信息与对话思考状态展示
This commit is contained in:
parent
b5484c8adb
commit
92a86efe73
@ -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
|
||||||
|
|||||||
@ -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--collapsed':
|
||||||
|
canCollapseReply(message.content) &&
|
||||||
|
!expandedReplies.has(message.id),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="message.content"
|
||||||
class="assistant-bubble"
|
class="assistant-bubble"
|
||||||
|
:class="{
|
||||||
|
'assistant-bubble--collapsed':
|
||||||
|
canCollapseReply(message.content) &&
|
||||||
|
!expandedReplies.has(message.id),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user