From 69a1433a86c8022c9a1beab72ae4987bacf2153b Mon Sep 17 00:00:00 2001 From: kx_guo Date: Wed, 9 Sep 2026 15:40:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20AI=20=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E4=B8=8E=E4=BC=9A=E8=AF=9D=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现 AI 案例推理方案生成报告并追加到当前会话报告;查看标准详情跳转标准库;优化标准兜底和相似案例展示;新增停止生成、撤回问题及对应回答功能,并将未接入按钮置灰隐藏。 --- src/api/conversations.ts | 47 +- .../assistant/AssistantResponseCard.vue | 558 ++++++++++++++++-- src/components/assistant/RiskAnalysisCard.vue | 14 +- src/pages/ReportPage.vue | 96 +++ src/pages/RiskAssistantPage.vue | 133 ++++- src/stores/conversation.store.ts | 106 +++- src/types/risk.ts | 19 + 7 files changed, 905 insertions(+), 68 deletions(-) diff --git a/src/api/conversations.ts b/src/api/conversations.ts index d0e403a..bae5a19 100644 --- a/src/api/conversations.ts +++ b/src/api/conversations.ts @@ -40,28 +40,47 @@ export function sendConversationMessage( ); } +export function withdrawConversationMessage( + conversationId: number, + messageId: number, +) { + return http.delete>( + `/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); } } diff --git a/src/components/assistant/AssistantResponseCard.vue b/src/components/assistant/AssistantResponseCard.vue index 1e2ffa7..fc4b033 100644 --- a/src/components/assistant/AssistantResponseCard.vue +++ b/src/components/assistant/AssistantResponseCard.vue @@ -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[number]; +const selectedCase = ref(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); +} @@ -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; + } +} diff --git a/src/components/assistant/RiskAnalysisCard.vue b/src/components/assistant/RiskAnalysisCard.vue index 9454001..2bf4500 100644 --- a/src/components/assistant/RiskAnalysisCard.vue +++ b/src/components/assistant/RiskAnalysisCard.vue @@ -437,7 +437,12 @@ function openSupplier() { - @@ -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; diff --git a/src/pages/ReportPage.vue b/src/pages/ReportPage.vue index 7a8bf7e..19376fc 100644 --- a/src/pages/ReportPage.vue +++ b/src/pages/ReportPage.vue @@ -277,6 +277,47 @@ function dimensionConclusion(item: {

+ +

七、标准判定结果