dev #1
@ -40,28 +40,47 @@ export function sendConversationMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export function withdrawConversationMessage(
|
||||
conversationId: number,
|
||||
messageId: number,
|
||||
) {
|
||||
return http.delete<ApiResponse<Conversation>>(
|
||||
`/v1/conversations/${conversationId}/messages/${messageId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function streamConversationMessage(
|
||||
id: number,
|
||||
content: string,
|
||||
onEvent: (event: ConversationStreamEvent) => void,
|
||||
messageType: "text" | "image" | "file" | "audio" = "text",
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const token = localStorage.getItem("risk_prediction_token");
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort();
|
||||
if (signal?.aborted) controller.abort();
|
||||
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = window.setTimeout(() => controller.abort(), 180_000);
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL || "/api"}/v1/conversations/${id}/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL || "/api"}/v1/conversations/${id}/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ content, messageType }),
|
||||
signal: controller.signal,
|
||||
},
|
||||
body: JSON.stringify({ content, messageType }),
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
);
|
||||
} catch (reason) {
|
||||
if (signal?.aborted) throw new Error("已停止生成", { cause: reason });
|
||||
throw reason;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem("risk_prediction_token");
|
||||
window.dispatchEvent(new CustomEvent("risk-auth-expired"));
|
||||
@ -109,11 +128,15 @@ export async function streamConversationMessage(
|
||||
}
|
||||
if (!completed) throw new Error("AI 流式连接中断,请重试");
|
||||
} catch (reason) {
|
||||
if (signal?.aborted) {
|
||||
throw new Error("已停止生成", { cause: reason });
|
||||
}
|
||||
if (controller.signal.aborted) {
|
||||
throw new Error("AI 响应超时,请稍后重试", { cause: reason });
|
||||
}
|
||||
throw reason;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
signal?.removeEventListener("abort", abortFromCaller);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,9 @@ import type { AssistantResponseCard } from "@/types/conversation";
|
||||
|
||||
const props = defineProps<{ card: AssistantResponseCard }>();
|
||||
const router = useRouter();
|
||||
const feedback = ref("");
|
||||
const emit = defineEmits<{
|
||||
generateReport: [card: AssistantResponseCard];
|
||||
}>();
|
||||
interface CardData extends AssistantResponseCard {
|
||||
name?: string;
|
||||
partyType?: string;
|
||||
@ -48,20 +50,36 @@ interface CardData extends AssistantResponseCard {
|
||||
analysisBase?: string;
|
||||
risks?: Array<{ level: string; title: string; detail: string }>;
|
||||
similarCases?: Array<{
|
||||
caseId?: number | null;
|
||||
case_id?: number | null;
|
||||
date?: string | null;
|
||||
product?: string | null;
|
||||
category?: string | null;
|
||||
product_category?: string | null;
|
||||
material?: string | null;
|
||||
material_type?: string | null;
|
||||
anomalyType?: string | null;
|
||||
anomaly_type?: string | null;
|
||||
problem?: string | null;
|
||||
description?: string | null;
|
||||
rootCause?: string | null;
|
||||
root_cause?: string | null;
|
||||
solution?: string | null;
|
||||
result?: string | null;
|
||||
defectLevel?: string | null;
|
||||
defect_level?: string | null;
|
||||
similarity?: number | null;
|
||||
}>;
|
||||
warning?: string;
|
||||
}
|
||||
const data = computed(() => props.card as CardData);
|
||||
|
||||
function notify(label: string) {
|
||||
feedback.value = `${label}功能将在对应模块接入后执行`;
|
||||
window.setTimeout(() => (feedback.value = ""), 2400);
|
||||
}
|
||||
type SolutionCase = NonNullable<CardData["similarCases"]>[number];
|
||||
const selectedCase = ref<SolutionCase | null>(null);
|
||||
const standardCategories = computed(() =>
|
||||
[...new Set((data.value.standards ?? []).map((item) => item.category))].join(
|
||||
" / ",
|
||||
),
|
||||
);
|
||||
|
||||
function openSupplier() {
|
||||
const supplier = data.value.name;
|
||||
@ -71,9 +89,38 @@ function openSupplier() {
|
||||
});
|
||||
}
|
||||
|
||||
function openStandard(riskPoint: string) {
|
||||
void router.push({
|
||||
name: "standards",
|
||||
query: { keyword: riskPoint.slice(0, 50) },
|
||||
});
|
||||
}
|
||||
|
||||
function openSimilarCase(item: SolutionCase) {
|
||||
selectedCase.value = item;
|
||||
}
|
||||
|
||||
function closeSimilarCase() {
|
||||
selectedCase.value = null;
|
||||
}
|
||||
|
||||
function openStandardLibrary() {
|
||||
const firstStandard = data.value.standards?.[0];
|
||||
void router.push({
|
||||
name: "standards",
|
||||
query: firstStandard?.riskPoint
|
||||
? { keyword: firstStandard.riskPoint.slice(0, 50) }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function formatCount(value: unknown) {
|
||||
return typeof value === "number" ? value.toLocaleString("zh-CN") : "暂无数据";
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
emit("generateReport", props.card);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -230,50 +277,86 @@ function formatCount(value: unknown) {
|
||||
</div>
|
||||
<section
|
||||
v-if="data.standardFallbackEnabled || data.standards?.length"
|
||||
class="solution-section"
|
||||
class="solution-section solution-standards"
|
||||
>
|
||||
<h4>
|
||||
<Ruler :size="14" />
|
||||
<span v-if="data.standardFallbackEnabled"
|
||||
>标准兜底校验(无历史案例时启用)</span
|
||||
>
|
||||
<span v-else>关联标准校验</span>
|
||||
<span v-else>标准判定 · 双引擎</span>
|
||||
</h4>
|
||||
<div
|
||||
<div class="solution-standards__sub">
|
||||
命中判定标准库 {{ data.standards?.length ?? 0 }} 条<span
|
||||
v-if="standardCategories"
|
||||
>
|
||||
· 覆盖 {{ standardCategories }}</span
|
||||
>
|
||||
· 来源:《标准文件.xlsx》219 条
|
||||
</div>
|
||||
<button
|
||||
v-for="standard in data.standards"
|
||||
:key="`${standard.category}-${standard.riskPoint}`"
|
||||
class="standard-row"
|
||||
class="solution-standard-row"
|
||||
type="button"
|
||||
title="查看标准库详情"
|
||||
@click="openStandard(standard.riskPoint)"
|
||||
>
|
||||
<span>{{ standard.category }}</span>
|
||||
<span
|
||||
class="solution-standard-tag"
|
||||
:class="{
|
||||
law: standard.category === '法规' || standard.category === '包装',
|
||||
}"
|
||||
>{{ standard.category }}</span
|
||||
>
|
||||
<div>
|
||||
<b>{{ standard.riskPoint }}</b
|
||||
><small>{{ standard.rule }} · {{ standard.source }}</small>
|
||||
><small
|
||||
>{{ standard.rule }} ·
|
||||
{{ standard.source || "标准文件.xlsx" }}</small
|
||||
>
|
||||
</div>
|
||||
<strong>{{ standard.status }}</strong>
|
||||
</div>
|
||||
<strong
|
||||
:class="{
|
||||
ok: standard.status === '合规' || standard.status === '打样必测',
|
||||
}"
|
||||
>{{ standard.status }}</strong
|
||||
>
|
||||
</button>
|
||||
<p v-if="!data.standards?.length" class="empty">
|
||||
当前标准库暂无可匹配规则,请补充产品品类、包材或检测指标。
|
||||
</p>
|
||||
<div
|
||||
v-if="data.standardFallbackEnabled && data.standards?.length"
|
||||
class="solution-standards__foot"
|
||||
>
|
||||
🔄
|
||||
双引擎融合:历史数据(经验引擎)提供风险优先级与相似案例;标准库(规则引擎)给出量化阈值与检测依据。
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="data.similarCases?.length" class="solution-section">
|
||||
<h4>相似历史案例</h4>
|
||||
<div
|
||||
<h4>匹配到 {{ data.similarCases.length }} 条相似案例</h4>
|
||||
<button
|
||||
v-for="(item, index) in data.similarCases"
|
||||
:key="`${item.date}-${index}`"
|
||||
class="standard-row"
|
||||
class="solution-case-row"
|
||||
type="button"
|
||||
title="查看案例详情"
|
||||
@click="openSimilarCase(item)"
|
||||
>
|
||||
<span>TOP {{ index + 1 }}</span>
|
||||
<span class="solution-case-rank">TOP {{ index + 1 }}</span>
|
||||
<div>
|
||||
<b>{{ item.product || "未命名产品" }}</b>
|
||||
<small
|
||||
>{{ item.problem || "异常问题未填写" }} →
|
||||
>{{ item.date || "日期未知" }} ·
|
||||
{{ item.product || "未命名产品" }} ·
|
||||
{{ item.problem || "异常问题未填写" }} →
|
||||
{{ item.solution || "解决方案未填写" }}
|
||||
<em v-if="item.similarity != null"
|
||||
>相似度 {{ Math.round(item.similarity * 100) }}%</em
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
<section v-if="data.risks?.length" class="solution-section risk-list">
|
||||
<div v-for="risk in data.risks" :key="risk.title" class="solution-risk">
|
||||
@ -306,16 +389,25 @@ function formatCount(value: unknown) {
|
||||
<button
|
||||
type="button"
|
||||
class="response-actions__primary"
|
||||
@click="notify('生成报告')"
|
||||
:disabled="card.type !== 'solution'"
|
||||
:title="card.type === 'solution' ? '追加到当前风险报告' : '功能开发中'"
|
||||
@click="generateReport"
|
||||
>
|
||||
<CheckCircle2 :size="14" />生成报告
|
||||
</button>
|
||||
<button type="button" @click="notify('查看详情')">查看详情</button>
|
||||
<button type="button" @click="notify('追问详情')">追问详情</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="card.type !== 'solution'"
|
||||
:title="card.type === 'solution' ? '跳转判定标准库' : '功能开发中'"
|
||||
@click="openStandardLibrary"
|
||||
>
|
||||
查看标准详情
|
||||
</button>
|
||||
<button
|
||||
v-if="card.type === 'solution'"
|
||||
type="button"
|
||||
@click="notify('收藏方案')"
|
||||
disabled
|
||||
title="功能开发中"
|
||||
>
|
||||
📌 收藏方案
|
||||
</button>
|
||||
@ -327,7 +419,104 @@ function formatCount(value: unknown) {
|
||||
🏭 查供应商
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="feedback" class="response-feedback">{{ feedback }}</p>
|
||||
<div
|
||||
v-if="selectedCase"
|
||||
class="case-modal-backdrop"
|
||||
role="presentation"
|
||||
@click.self="closeSimilarCase"
|
||||
>
|
||||
<section
|
||||
class="case-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="solution-case-title"
|
||||
>
|
||||
<header class="case-modal__header">
|
||||
<div>
|
||||
<small>相似历史案例</small>
|
||||
<h3 id="solution-case-title">
|
||||
{{ selectedCase.product || "未命名产品" }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="case-modal__close"
|
||||
aria-label="关闭案例详情"
|
||||
@click="closeSimilarCase"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<div class="case-modal__summary">
|
||||
<strong>{{ selectedCase.date || "日期未知" }}</strong>
|
||||
<span v-if="selectedCase.similarity != null">
|
||||
相似度 {{ Math.round(selectedCase.similarity * 100) }}%
|
||||
</span>
|
||||
</div>
|
||||
<dl class="case-modal__grid">
|
||||
<div>
|
||||
<dt>品类</dt>
|
||||
<dd>
|
||||
{{
|
||||
selectedCase.category ||
|
||||
selectedCase.product_category ||
|
||||
"未填写"
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>物料</dt>
|
||||
<dd>
|
||||
{{
|
||||
selectedCase.material || selectedCase.material_type || "未填写"
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>异常类型</dt>
|
||||
<dd>
|
||||
{{
|
||||
selectedCase.anomalyType ||
|
||||
selectedCase.anomaly_type ||
|
||||
"未填写"
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>缺陷等级</dt>
|
||||
<dd>
|
||||
{{
|
||||
selectedCase.defectLevel ||
|
||||
selectedCase.defect_level ||
|
||||
"未定级"
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="case-modal__field--wide">
|
||||
<dt>问题描述</dt>
|
||||
<dd>
|
||||
{{ selectedCase.problem || selectedCase.description || "未填写" }}
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedCase.rootCause || selectedCase.root_cause"
|
||||
class="case-modal__field--wide"
|
||||
>
|
||||
<dt>原因分析</dt>
|
||||
<dd>{{ selectedCase.rootCause || selectedCase.root_cause }}</dd>
|
||||
</div>
|
||||
<div class="case-modal__field--wide">
|
||||
<dt>解决方案</dt>
|
||||
<dd>{{ selectedCase.solution || "未填写" }}</dd>
|
||||
</div>
|
||||
<div v-if="selectedCase.result" class="case-modal__field--wide">
|
||||
<dt>处理结果</dt>
|
||||
<dd>{{ selectedCase.result }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<footer class="case-modal__footer">点击遮罩或右上角关闭详情</footer>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@ -350,8 +539,19 @@ function formatCount(value: unknown) {
|
||||
border-left: 3px solid var(--color-accent);
|
||||
}
|
||||
.response-card--solution {
|
||||
border: 1px solid #bfdbfe;
|
||||
border-left: 3px solid var(--color-purple);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-accent-soft);
|
||||
border-radius: 10px;
|
||||
padding: 22px;
|
||||
}
|
||||
.response-card--solution::before {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 3px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
background: linear-gradient(180deg, var(--color-accent), var(--color-purple));
|
||||
content: "";
|
||||
}
|
||||
.response-card__header {
|
||||
display: flex;
|
||||
@ -376,7 +576,20 @@ function formatCount(value: unknown) {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.response-card--solution .response-card__icon {
|
||||
color: var(--color-purple);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-purple));
|
||||
color: #fff;
|
||||
}
|
||||
.response-card--solution .response-card__header {
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.response-card--solution .response-card__header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.response-card__header h3 {
|
||||
margin: 0;
|
||||
@ -496,6 +709,66 @@ function formatCount(value: unknown) {
|
||||
color: var(--color-warning);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-standard-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
border-top: 1px dashed var(--color-border-light);
|
||||
padding: 7px 0;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.solution-standard-row:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
.solution-standard-row:hover {
|
||||
border-radius: 5px;
|
||||
background: var(--color-bg-warm);
|
||||
}
|
||||
.solution-standard-tag {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--color-accent-soft);
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.solution-standard-tag.law {
|
||||
border-color: #fbcfe8;
|
||||
background: #fce7f3;
|
||||
color: #be185d;
|
||||
}
|
||||
.solution-standard-row > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.solution-standard-row > div b {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.solution-standard-row small {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
color: var(--color-text-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-standard-row > strong {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-warning);
|
||||
font-size: 11px;
|
||||
}
|
||||
.solution-standard-row > strong.ok {
|
||||
color: var(--color-success);
|
||||
}
|
||||
.suggestion-list {
|
||||
margin: 12px 0 0;
|
||||
padding-left: 18px;
|
||||
@ -540,13 +813,82 @@ function formatCount(value: unknown) {
|
||||
}
|
||||
.solution-base {
|
||||
margin-top: 14px;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background: #f3f7fb;
|
||||
border-radius: 5px;
|
||||
padding: 10px 12px;
|
||||
background: var(--color-bg-warm);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.solution-standards {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.solution-standards h4 {
|
||||
margin-bottom: 4px;
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.solution-standards__sub {
|
||||
margin-bottom: 10px;
|
||||
color: var(--color-text-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-standards__foot {
|
||||
margin-top: 8px;
|
||||
border-top: 1px dashed var(--color-border-light);
|
||||
padding-top: 8px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.solution-standards__foot b {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.solution-case-row {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: 58px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding: 7px 9px;
|
||||
background: var(--color-bg-warm);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.solution-case-row:hover {
|
||||
background: #f0f0ee;
|
||||
}
|
||||
.solution-case-rank {
|
||||
padding-top: 1px;
|
||||
color: var(--color-accent);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.solution-case-row > div {
|
||||
min-width: 0;
|
||||
}
|
||||
.solution-case-row small {
|
||||
display: block;
|
||||
color: var(--color-text-faint);
|
||||
font-size: 9px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.solution-case-row em {
|
||||
margin-left: 6px;
|
||||
color: var(--color-accent);
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.solution-confidence {
|
||||
margin-left: auto;
|
||||
border: 1px solid #fcd34d;
|
||||
@ -559,12 +901,13 @@ function formatCount(value: unknown) {
|
||||
}
|
||||
.solution-suggest {
|
||||
margin-top: 14px;
|
||||
border-radius: 6px;
|
||||
padding: 11px;
|
||||
background: #eff6ff;
|
||||
border-radius: 5px;
|
||||
padding: 12px;
|
||||
background: var(--color-accent-subtle);
|
||||
}
|
||||
.solution-suggest h4 {
|
||||
color: var(--color-accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
.solution-suggest ol {
|
||||
margin: 0;
|
||||
@ -572,16 +915,23 @@ function formatCount(value: unknown) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.solution-suggest li {
|
||||
margin: 4px 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
margin: 3px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.solution-risk {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.solution-risk:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
.solution-risk:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.solution-risk > span {
|
||||
flex: 0 0 auto;
|
||||
@ -602,24 +952,24 @@ function formatCount(value: unknown) {
|
||||
.solution-risk p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.solution-warning {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-warning-soft);
|
||||
color: #92400e;
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.response-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
padding-top: 12px;
|
||||
@ -628,27 +978,133 @@ function formatCount(value: unknown) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 30px;
|
||||
min-height: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 5px;
|
||||
padding: 0 10px;
|
||||
padding: 0 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.response-actions__primary {
|
||||
border-color: var(--color-text) !important;
|
||||
background: var(--color-text) !important;
|
||||
color: white !important;
|
||||
}
|
||||
.response-feedback {
|
||||
margin: 8px 0 0;
|
||||
.response-actions button:disabled {
|
||||
border-color: var(--color-border-light);
|
||||
background: var(--color-bg-warm);
|
||||
color: var(--color-text-faint);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.case-modal-backdrop {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgb(17 17 17 / 28%);
|
||||
}
|
||||
.case-modal {
|
||||
width: min(620px, 100%);
|
||||
max-height: min(720px, calc(100vh - 48px));
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 22px 54px rgb(15 23 42 / 22%);
|
||||
}
|
||||
.case-modal__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.case-modal__header small {
|
||||
color: var(--color-accent);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.case-modal__header h3 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
.case-modal__close {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.case-modal__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 10px;
|
||||
}
|
||||
.case-modal__summary span {
|
||||
border-radius: 4px;
|
||||
padding: 4px 7px;
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.case-modal__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 10px 20px 20px;
|
||||
}
|
||||
.case-modal__grid > div {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: 5px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
.case-modal__grid dt {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
.case-modal__grid dd {
|
||||
margin: 4px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.case-modal__field--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.case-modal__footer {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
padding: 10px 20px 14px;
|
||||
color: var(--color-text-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.case-modal-backdrop {
|
||||
align-items: end;
|
||||
padding: 0;
|
||||
}
|
||||
.case-modal {
|
||||
max-height: 90vh;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.case-modal__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.case-modal__field--wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -437,7 +437,12 @@ function openSupplier() {
|
||||
</button>
|
||||
<button type="button" @click="openCase()">案例详情</button>
|
||||
<button type="button" @click="openSupplier">供应商对比</button>
|
||||
<button v-if="canMonitor" type="button" @click="notify('加入监控')">
|
||||
<button
|
||||
v-if="canMonitor"
|
||||
type="button"
|
||||
disabled
|
||||
title="加入监控功能开发中"
|
||||
>
|
||||
加入监控
|
||||
</button>
|
||||
</div>
|
||||
@ -1117,6 +1122,13 @@ h3 {
|
||||
background: var(--color-text);
|
||||
color: white;
|
||||
}
|
||||
.risk-actions button:disabled {
|
||||
border-color: var(--color-border-light);
|
||||
background: var(--color-bg-warm);
|
||||
color: var(--color-text-faint);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.risk-feedback {
|
||||
margin: 8px 0 0;
|
||||
|
||||
@ -277,6 +277,47 @@ function dimensionConclusion(item: {
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<template v-if="report.aiSolutionCards?.length">
|
||||
<h3 class="report-subsection-title">AI 案例推理方案明细(已追加)</h3>
|
||||
<section
|
||||
v-for="(solution, index) in report.aiSolutionCards"
|
||||
:key="`${solution.title || 'solution'}-${index}`"
|
||||
class="ai-solution-report"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ solution.title || "AI 案例推理生成" }}</strong>
|
||||
<span v-if="solution.confidence"
|
||||
>置信度:{{ solution.confidence }}</span
|
||||
>
|
||||
</header>
|
||||
<p v-if="solution.analysisBase">
|
||||
<b>分析基础:</b>{{ solution.analysisBase }}
|
||||
</p>
|
||||
<template v-if="solution.risks?.length">
|
||||
<h3>风险项</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="risk in solution.risks"
|
||||
:key="`${risk.title}-${risk.detail}`"
|
||||
>
|
||||
<b>{{ risk.level }} · {{ risk.title }}:</b>{{ risk.detail }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-if="solution.suggestions?.length">
|
||||
<h3>建议方案</h3>
|
||||
<ol>
|
||||
<li v-for="suggestion in solution.suggestions" :key="suggestion">
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
<p v-if="solution.warning" class="ai-solution-report__warning">
|
||||
{{ solution.warning }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<h2>七、标准判定结果</h2>
|
||||
<template v-if="report.standardJudgment?.length">
|
||||
<table>
|
||||
@ -440,6 +481,61 @@ ol {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.ai-solution-report {
|
||||
margin: 0 0 16px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-left: 3px solid #7c3aed;
|
||||
border-radius: 7px;
|
||||
padding: 14px;
|
||||
background: #fafbff;
|
||||
}
|
||||
.report-subsection-title {
|
||||
margin: 14px 0 8px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
.ai-solution-report header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
color: #1d4ed8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.ai-solution-report header span {
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
font-size: 10px;
|
||||
}
|
||||
.ai-solution-report p,
|
||||
.ai-solution-report li {
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.ai-solution-report p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.ai-solution-report h3 {
|
||||
margin: 10px 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ai-solution-report ul,
|
||||
.ai-solution-report ol {
|
||||
margin: 4px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.ai-solution-report__warning {
|
||||
border-radius: 4px;
|
||||
padding: 7px 9px;
|
||||
background: #fffbeb;
|
||||
color: #92400e !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.report-page {
|
||||
padding: 12px;
|
||||
|
||||
@ -11,7 +11,7 @@ import OcrRecognitionDialog from "@/components/ocr/OcrRecognitionDialog.vue";
|
||||
import { useConversationStore } from "@/stores/conversation.store";
|
||||
import { useAuthStore } from "@/stores/auth.store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import type { RiskAssessmentResult } from "@/types/risk";
|
||||
import type { AiSolutionReport, RiskAssessmentResult } from "@/types/risk";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -20,8 +20,10 @@ const conversationStore = useConversationStore();
|
||||
const {
|
||||
current: conversation,
|
||||
sending: loading,
|
||||
generating,
|
||||
error,
|
||||
} = storeToRefs(conversationStore);
|
||||
const withdrawingId = ref<number | null>(null);
|
||||
const prompt = ref("");
|
||||
const ocrOpen = ref(false);
|
||||
const scrollArea = ref<HTMLElement | null>(null);
|
||||
@ -204,6 +206,25 @@ function handleScroll() {
|
||||
if (scrollArea.value && scrollArea.value.scrollTop <= 80) loadOlder();
|
||||
}
|
||||
|
||||
function hasAssistantPair(index: number) {
|
||||
return conversation.value?.messages[index + 1]?.role === "assistant";
|
||||
}
|
||||
|
||||
function stopGeneration() {
|
||||
conversationStore.stopGeneration();
|
||||
}
|
||||
|
||||
async function withdrawMessage(messageId: number) {
|
||||
if (!conversation.value || withdrawingId.value !== null) return;
|
||||
if (!window.confirm("撤回后将同时删除该问题和对应回答,确认撤回吗?")) return;
|
||||
withdrawingId.value = messageId;
|
||||
try {
|
||||
await conversationStore.withdraw(messageId);
|
||||
} finally {
|
||||
withdrawingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openFromRoute() {
|
||||
const rawId = route.query.conversation;
|
||||
if (typeof rawId !== "string") return;
|
||||
@ -227,6 +248,53 @@ function openReport(result: RiskAssessmentResult) {
|
||||
void router.push({ name: "report" });
|
||||
}
|
||||
|
||||
function openSolutionReport(card: Record<string, unknown>) {
|
||||
const solution = card as AiSolutionReport;
|
||||
const current = conversation.value?.messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((message) => message.riskResult)?.riskResult;
|
||||
let stored: RiskAssessmentResult | null = null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("dieya-risk-report");
|
||||
stored = raw ? (JSON.parse(raw) as RiskAssessmentResult) : null;
|
||||
} catch {
|
||||
stored = null;
|
||||
}
|
||||
const sameReport = Boolean(
|
||||
current && stored && current.subjectId === stored.subjectId,
|
||||
);
|
||||
const base: RiskAssessmentResult =
|
||||
sameReport && stored
|
||||
? structuredClone(stored)
|
||||
: current
|
||||
? structuredClone(current)
|
||||
: stored || {
|
||||
subjectId: "AI 案例推理方案",
|
||||
score: 50,
|
||||
level: "medium",
|
||||
modelVersion: "ai-solution-card",
|
||||
factors: [],
|
||||
};
|
||||
const existing = base.aiSolutionCards ?? [];
|
||||
const merged: RiskAssessmentResult = {
|
||||
...base,
|
||||
aiSolutionCards: [...existing, solution],
|
||||
aiSuggestions: [
|
||||
...(base.aiSuggestions ?? []),
|
||||
...(solution.suggestions ?? []),
|
||||
],
|
||||
similarCases: solution.similarCases?.length
|
||||
? [...(base.similarCases ?? []), ...solution.similarCases]
|
||||
: base.similarCases,
|
||||
standardJudgment: solution.standards?.length
|
||||
? [...(base.standardJudgment ?? []), ...solution.standards]
|
||||
: base.standardJudgment,
|
||||
};
|
||||
sessionStorage.setItem("dieya-risk-report", JSON.stringify(merged));
|
||||
void router.push({ name: "report" });
|
||||
}
|
||||
|
||||
async function submit(
|
||||
messageType: "text" | "image" | "file" | "audio" = "text",
|
||||
) {
|
||||
@ -293,6 +361,22 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
<div v-if="message.role === 'user'" class="user-bubble">
|
||||
{{ message.content }}
|
||||
<time>{{ formatMessageTime(message.createdAt) }}</time>
|
||||
<div class="user-message-actions">
|
||||
<button
|
||||
v-if="
|
||||
message.id > 0 &&
|
||||
!generating &&
|
||||
hasAssistantPair(
|
||||
conversation?.messages.indexOf(message) ?? -1,
|
||||
)
|
||||
"
|
||||
type="button"
|
||||
:disabled="withdrawingId !== null || loading"
|
||||
@click="withdrawMessage(message.id)"
|
||||
>
|
||||
撤回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="message-body">
|
||||
<div
|
||||
@ -330,6 +414,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
<AssistantResponseCard
|
||||
v-if="message.responseCard"
|
||||
:card="message.responseCard"
|
||||
@generate-report="openSolutionReport"
|
||||
/>
|
||||
<time class="message-time">{{
|
||||
formatMessageTime(message.createdAt)
|
||||
@ -342,6 +427,13 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
<div class="message-avatar">蝶</div>
|
||||
<div class="message-body">
|
||||
<p class="state-message">{{ loadingText }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="stop-generation"
|
||||
@click="stopGeneration"
|
||||
>
|
||||
停止生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="state-message state-message--error">
|
||||
@ -517,6 +609,45 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-message-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.user-message-actions button {
|
||||
border-color: rgb(255 255 255 / 35%);
|
||||
color: rgb(255 255 255 / 82%);
|
||||
}
|
||||
|
||||
.user-message-actions button,
|
||||
.stop-generation {
|
||||
border: 1px solid rgb(255 255 255 / 35%);
|
||||
border-radius: 999px;
|
||||
padding: 3px 9px;
|
||||
background: transparent;
|
||||
color: rgb(255 255 255 / 82%);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.user-message-actions button:hover,
|
||||
.stop-generation:hover {
|
||||
border-color: rgb(255 255 255 / 65%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-message-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.stop-generation {
|
||||
margin-top: 8px;
|
||||
border-color: var(--color-border-strong);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.assistant-bubble {
|
||||
width: fit-content;
|
||||
max-width: 82%;
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
createConversation,
|
||||
fetchConversation,
|
||||
fetchConversations,
|
||||
withdrawConversationMessage,
|
||||
streamConversationMessage,
|
||||
sendConversationMessage,
|
||||
} from "@/api/conversations";
|
||||
@ -19,6 +20,9 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
const sending = ref(false);
|
||||
const loadingOlder = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const generating = ref(false);
|
||||
let activeController: AbortController | null = null;
|
||||
let activeUserMessageId: number | null = null;
|
||||
let listRequest: Promise<void> | null = null;
|
||||
let listRequestSearch: string | undefined;
|
||||
let listLoadedAt = 0;
|
||||
@ -144,6 +148,8 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
if (!current.value) await create();
|
||||
if (!current.value) throw new Error("当前对话不存在");
|
||||
sending.value = true;
|
||||
generating.value = true;
|
||||
activeController = new AbortController();
|
||||
error.value = null;
|
||||
const startedAt = new Date().toISOString();
|
||||
const userMessage = {
|
||||
@ -156,6 +162,7 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
semantic: null,
|
||||
createdAt: startedAt,
|
||||
};
|
||||
activeUserMessageId = userMessage.id;
|
||||
const assistantMessage = {
|
||||
id: userMessage.id - 1,
|
||||
role: "assistant" as const,
|
||||
@ -178,6 +185,23 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
conversationId,
|
||||
content,
|
||||
(event) => {
|
||||
if (
|
||||
event.type === "start" &&
|
||||
event.data &&
|
||||
typeof event.data === "object" &&
|
||||
"messageId" in event.data &&
|
||||
typeof event.data.messageId === "number" &&
|
||||
current.value
|
||||
) {
|
||||
const serverMessageId = event.data.messageId;
|
||||
const messages = current.value.messages.map((message) =>
|
||||
message.id === userMessage.id
|
||||
? { ...message, id: serverMessageId }
|
||||
: message,
|
||||
);
|
||||
activeUserMessageId = serverMessageId;
|
||||
current.value = { ...current.value, messages };
|
||||
}
|
||||
if (event.type === "delta" && event.content) onDelta(event.content);
|
||||
if (event.type === "semantic" && event.data && current.value) {
|
||||
const messages = [...current.value.messages];
|
||||
@ -207,6 +231,7 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
current.value = event.data as Conversation;
|
||||
},
|
||||
messageType,
|
||||
activeController.signal,
|
||||
);
|
||||
if (
|
||||
!current.value ||
|
||||
@ -223,15 +248,49 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
return current.value;
|
||||
} catch (reason) {
|
||||
const conversationId = current.value?.id;
|
||||
if (activeController?.signal.aborted) {
|
||||
if (conversationId) {
|
||||
try {
|
||||
const response = await fetchConversation(conversationId);
|
||||
if (response.data.data) current.value = response.data.data;
|
||||
} catch {
|
||||
// If the request was cancelled before persistence, remove both
|
||||
// temporary messages from the local conversation.
|
||||
if (current.value) {
|
||||
const messages = current.value.messages.filter(
|
||||
(message) =>
|
||||
message.id !== assistantMessage.id &&
|
||||
message.id !== userMessage.id,
|
||||
);
|
||||
current.value = {
|
||||
...current.value,
|
||||
messages,
|
||||
messageCount: Math.max(0, current.value.messageCount - 2),
|
||||
messagesTotal: Math.max(0, current.value.messagesTotal - 2),
|
||||
};
|
||||
}
|
||||
}
|
||||
const summary = current.value ? toSummary(current.value) : null;
|
||||
if (summary) {
|
||||
const index = items.value.findIndex(
|
||||
(item) => item.id === summary.id,
|
||||
);
|
||||
if (index >= 0) items.value.splice(index, 1, summary);
|
||||
else items.value.unshift(summary);
|
||||
}
|
||||
}
|
||||
return current.value;
|
||||
}
|
||||
if (current.value) {
|
||||
const messages = current.value.messages.filter(
|
||||
(message) => message.id !== assistantMessage.id,
|
||||
(message) =>
|
||||
message.id !== assistantMessage.id && message.id !== userMessage.id,
|
||||
);
|
||||
current.value = {
|
||||
...current.value,
|
||||
messages,
|
||||
messageCount: Math.max(0, current.value.messageCount - 1),
|
||||
messagesTotal: Math.max(0, current.value.messagesTotal - 1),
|
||||
messageCount: Math.max(0, current.value.messageCount - 2),
|
||||
messagesTotal: Math.max(0, current.value.messagesTotal - 2),
|
||||
};
|
||||
}
|
||||
if (conversationId) {
|
||||
@ -246,6 +305,43 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
throw reason;
|
||||
} finally {
|
||||
sending.value = false;
|
||||
generating.value = false;
|
||||
activeController = null;
|
||||
activeUserMessageId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopGeneration() {
|
||||
activeController?.abort();
|
||||
}
|
||||
|
||||
function isGeneratingMessage(messageId: number) {
|
||||
return activeUserMessageId === messageId && generating.value;
|
||||
}
|
||||
|
||||
async function withdraw(messageId: number) {
|
||||
if (!current.value) return;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await withdrawConversationMessage(
|
||||
current.value.id,
|
||||
messageId,
|
||||
);
|
||||
if (!response.data.data) throw new Error("撤回响应为空");
|
||||
current.value = response.data.data;
|
||||
const index = items.value.findIndex(
|
||||
(item) => item.id === current.value?.id,
|
||||
);
|
||||
const summary = toSummary(current.value);
|
||||
if (index >= 0) items.value.splice(index, 1, summary);
|
||||
else items.value.unshift(summary);
|
||||
return current.value;
|
||||
} catch (reason) {
|
||||
error.value = reason instanceof Error ? reason.message : "撤回失败";
|
||||
throw reason;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,6 +365,7 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
current,
|
||||
loading,
|
||||
sending,
|
||||
generating,
|
||||
loadingOlder,
|
||||
error,
|
||||
loadList,
|
||||
@ -277,6 +374,9 @@ export const useConversationStore = defineStore("conversation", () => {
|
||||
create,
|
||||
send,
|
||||
streamSend,
|
||||
stopGeneration,
|
||||
isGeneratingMessage,
|
||||
withdraw,
|
||||
reset,
|
||||
};
|
||||
});
|
||||
|
||||
@ -53,6 +53,7 @@ export interface RiskAssessmentResult {
|
||||
similarity?: number | null;
|
||||
}>;
|
||||
aiSuggestions?: string[];
|
||||
aiSolutionCards?: AiSolutionReport[];
|
||||
riskBadge?: string | null;
|
||||
alertText?: string | null;
|
||||
categoryWarning?: {
|
||||
@ -84,6 +85,24 @@ export interface RiskAssessmentResult {
|
||||
dimensionScores?: RiskDimensionScore[];
|
||||
}
|
||||
|
||||
export interface AiSolutionReport {
|
||||
title?: string;
|
||||
confidence?: string | null;
|
||||
analysisBase?: string | null;
|
||||
standardFallbackEnabled?: boolean;
|
||||
standards?: Array<{
|
||||
category: string;
|
||||
riskPoint: string;
|
||||
rule: string;
|
||||
source: string | null;
|
||||
status: string;
|
||||
}>;
|
||||
similarCases?: NonNullable<RiskAssessmentResult["similarCases"]>;
|
||||
risks?: Array<{ level: string; title: string; detail: string }>;
|
||||
suggestions?: string[];
|
||||
warning?: string | null;
|
||||
}
|
||||
|
||||
export type RiskReportResult = RiskAssessmentResult;
|
||||
|
||||
export interface ModelInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user