优化案例卡片和思考过程展示
This commit is contained in:
parent
7f74f5a041
commit
d04e8888cb
@ -1,6 +1,6 @@
|
|||||||
import { http } from "@/api/http";
|
import { http } from "@/api/http";
|
||||||
import type { ApiResponse } from "@/types/api";
|
|
||||||
import type { Conversation, ConversationSummary } from "@/types/conversation";
|
import type { Conversation, ConversationSummary } from "@/types/conversation";
|
||||||
|
import type { ApiResponse } from "@/types/api";
|
||||||
|
|
||||||
export interface ConversationStreamEvent {
|
export interface ConversationStreamEvent {
|
||||||
type: "start" | "semantic" | "delta" | "done" | "error";
|
type: "start" | "semantic" | "delta" | "done" | "error";
|
||||||
@ -22,6 +22,10 @@ export function createConversation(title?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function directConversationQuery(kind: "supplier" | "material" | "cases" | "solution", query: string) {
|
||||||
|
return http.post<ApiResponse<Record<string, unknown>>>("/v1/conversations/direct-query", { kind, query });
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchConversation(id: number, offset = 0) {
|
export function fetchConversation(id: number, offset = 0) {
|
||||||
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
|
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
|
||||||
params: { limit: 20, offset },
|
params: { limit: 20, offset },
|
||||||
|
|||||||
@ -47,6 +47,7 @@ const functions: Array<{
|
|||||||
icon: typeof Search;
|
icon: typeof Search;
|
||||||
tone: string;
|
tone: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
isNew?: boolean;
|
||||||
tool:
|
tool:
|
||||||
"assessment" | "supplier" | "material" | "cases" | "solution" | "review";
|
"assessment" | "supplier" | "material" | "cases" | "solution" | "review";
|
||||||
}> = [
|
}> = [
|
||||||
@ -89,6 +90,7 @@ const functions: Array<{
|
|||||||
tone: "purple",
|
tone: "purple",
|
||||||
prompt: "帮我生成一个新品风险预防方案",
|
prompt: "帮我生成一个新品风险预防方案",
|
||||||
tool: "solution",
|
tool: "solution",
|
||||||
|
isNew: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "审核队列",
|
title: "审核队列",
|
||||||
@ -96,6 +98,7 @@ const functions: Array<{
|
|||||||
icon: ClipboardCheck,
|
icon: ClipboardCheck,
|
||||||
tone: "red",
|
tone: "red",
|
||||||
tool: "review",
|
tool: "review",
|
||||||
|
isNew: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
@ -155,14 +158,11 @@ const functions: Array<{
|
|||||||
:key="item.title"
|
:key="item.title"
|
||||||
class="function-card"
|
class="function-card"
|
||||||
type="button"
|
type="button"
|
||||||
@click="
|
@click="emit('tool', item.tool)"
|
||||||
item.tool === 'assessment' || !item.prompt
|
|
||||||
? emit('tool', item.tool)
|
|
||||||
: emit('select', item.prompt)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<span class="function-dot" :class="`function-dot--${item.tone}`"></span>
|
<span class="function-dot" :class="`function-dot--${item.tone}`"></span>
|
||||||
<component :is="item.icon" :size="13" />
|
<component :is="item.icon" :size="13" />
|
||||||
|
<span v-if="item.isNew" class="function-new">新</span>
|
||||||
<span
|
<span
|
||||||
><strong>{{ item.title }}</strong
|
><strong>{{ item.title }}</strong
|
||||||
><small>{{ item.description }}</small></span
|
><small>{{ item.description }}</small></span
|
||||||
@ -253,8 +253,8 @@ const functions: Array<{
|
|||||||
.function-grid {
|
.function-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
max-width: var(--content-width);
|
max-width: var(--content-width);
|
||||||
margin: 8px auto 0;
|
margin: 10px auto 0;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,18 +264,22 @@ const functions: Array<{
|
|||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 9px;
|
border-radius: 8px;
|
||||||
padding: 7px 8px;
|
padding: 8px 10px;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-card:hover {
|
.function-card:hover {
|
||||||
border-color: var(--color-border-strong);
|
border-color: var(--color-border-strong);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-card > span:last-child {
|
.function-card > span:last-child {
|
||||||
@ -292,12 +296,26 @@ const functions: Array<{
|
|||||||
|
|
||||||
.function-card strong {
|
.function-card strong {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-card small {
|
.function-card small {
|
||||||
color: var(--color-text-faint);
|
color: var(--color-text-faint);
|
||||||
|
font-size: 9px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-new {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent);
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-dot {
|
.function-dot {
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
|
ChevronDown,
|
||||||
PackageSearch,
|
PackageSearch,
|
||||||
Ruler,
|
Ruler,
|
||||||
|
Search,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from "@lucide/vue";
|
} from "@lucide/vue";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
@ -24,6 +26,7 @@ const props = defineProps<{
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
generateReport: [card: ReportCard];
|
generateReport: [card: ReportCard];
|
||||||
|
openAssessment: [];
|
||||||
}>();
|
}>();
|
||||||
interface CardData extends AssistantResponseCard {
|
interface CardData extends AssistantResponseCard {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -42,7 +45,6 @@ interface CardData extends AssistantResponseCard {
|
|||||||
suggestions?: string[];
|
suggestions?: string[];
|
||||||
note?: string;
|
note?: string;
|
||||||
detectedValues?: Array<{ name: string; value: string }>;
|
detectedValues?: Array<{ name: string; value: string }>;
|
||||||
matchedCount?: number;
|
|
||||||
standards?: Array<{
|
standards?: Array<{
|
||||||
category: string;
|
category: string;
|
||||||
riskPoint: string;
|
riskPoint: string;
|
||||||
@ -50,7 +52,17 @@ interface CardData extends AssistantResponseCard {
|
|||||||
source?: string | null;
|
source?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
}>;
|
}>;
|
||||||
|
cases?: Array<{
|
||||||
|
caseId?: number | null;
|
||||||
|
date?: string | null;
|
||||||
|
product?: string | null;
|
||||||
|
problem?: string | null;
|
||||||
|
solution?: string | null;
|
||||||
|
similarity?: number | null;
|
||||||
|
}>;
|
||||||
|
matchedCount?: number;
|
||||||
confidence?: string;
|
confidence?: string;
|
||||||
|
thinkingSummary?: string[];
|
||||||
analysisBase?: string;
|
analysisBase?: string;
|
||||||
risks?: Array<{ level: string; title: string; detail: string }>;
|
risks?: Array<{ level: string; title: string; detail: string }>;
|
||||||
similarCases?: Array<{
|
similarCases?: Array<{
|
||||||
@ -79,6 +91,7 @@ interface CardData extends AssistantResponseCard {
|
|||||||
const data = computed(() => props.card as CardData);
|
const data = computed(() => props.card as CardData);
|
||||||
type SolutionCase = NonNullable<CardData["similarCases"]>[number];
|
type SolutionCase = NonNullable<CardData["similarCases"]>[number];
|
||||||
const selectedCase = ref<SolutionCase | null>(null);
|
const selectedCase = ref<SolutionCase | null>(null);
|
||||||
|
const thinkingExpanded = ref(false);
|
||||||
const standardCategories = computed(() =>
|
const standardCategories = computed(() =>
|
||||||
[...new Set((data.value.standards ?? []).map((item) => item.category))].join(
|
[...new Set((data.value.standards ?? []).map((item) => item.category))].join(
|
||||||
" / ",
|
" / ",
|
||||||
@ -135,6 +148,7 @@ function generateReport() {
|
|||||||
<PackageSearch v-else-if="card.type === 'material'" :size="16" />
|
<PackageSearch v-else-if="card.type === 'material'" :size="16" />
|
||||||
<BookOpenCheck v-else-if="card.type === 'standard'" :size="16" />
|
<BookOpenCheck v-else-if="card.type === 'standard'" :size="16" />
|
||||||
<ClipboardCheck v-else-if="card.type === 'compliance'" :size="16" />
|
<ClipboardCheck v-else-if="card.type === 'compliance'" :size="16" />
|
||||||
|
<Search v-else-if="card.type === 'cases'" :size="22" />
|
||||||
<Bot v-else :size="16" />
|
<Bot v-else :size="16" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@ -151,11 +165,17 @@ function generateReport() {
|
|||||||
<small v-else-if="card.type === 'solution'"
|
<small v-else-if="card.type === 'solution'"
|
||||||
>AI 推理生成 · 置信度:{{ data.confidence }}</small
|
>AI 推理生成 · 置信度:{{ data.confidence }}</small
|
||||||
>
|
>
|
||||||
|
<small v-else-if="card.type === 'cases'"
|
||||||
|
>匹配 {{ data.matchedCount ?? data.cases?.length ?? 0 }} 条相似历史案例</small
|
||||||
|
>
|
||||||
<small v-else>标准库规则匹配 · 待品控确认</small>
|
<small v-else>标准库规则匹配 · 待品控确认</small>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="card.type === 'solution'" class="solution-confidence"
|
<span v-if="card.type === 'solution'" class="solution-confidence"
|
||||||
>置信度:{{ data.confidence || "中" }}</span
|
>置信度:{{ data.confidence || "中" }}</span
|
||||||
>
|
>
|
||||||
|
<span v-else-if="card.type === 'cases'" class="case-semantic-badge"
|
||||||
|
>语义检索</span
|
||||||
|
>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<template v-if="card.type === 'supplier'">
|
<template v-if="card.type === 'supplier'">
|
||||||
@ -273,7 +293,61 @@ function generateReport() {
|
|||||||
<p v-if="data.note" class="card-note">{{ data.note }}</p>
|
<p v-if="data.note" class="card-note">{{ data.note }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="card.type === 'cases'">
|
||||||
|
<section class="case-results">
|
||||||
|
<h4>相似案例 TOP {{ Math.min(3, data.cases?.length ?? 0) }}</h4>
|
||||||
|
<template v-if="data.cases?.length">
|
||||||
|
<button
|
||||||
|
v-for="(item, index) in data.cases.slice(0, 3)"
|
||||||
|
:key="`${item.caseId}-${index}`"
|
||||||
|
class="case-result-row"
|
||||||
|
type="button"
|
||||||
|
title="查看案例详情"
|
||||||
|
@click="openSimilarCase(item)"
|
||||||
|
>
|
||||||
|
<span class="case-result-rank">TOP {{ index + 1 }}</span>
|
||||||
|
<span class="case-result-text"
|
||||||
|
>{{ item.date || "日期未知" }}|{{ item.product || "未命名产品" }}|{{
|
||||||
|
item.problem || "异常问题未填写"
|
||||||
|
}} → {{ item.solution || "处理方案未填写" }}</span
|
||||||
|
>
|
||||||
|
<em v-if="item.similarity != null"
|
||||||
|
>相似度 {{ Math.round(item.similarity * 100) }}%</em
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<p v-else class="empty">暂无匹配案例</p>
|
||||||
|
</section>
|
||||||
|
<section v-if="data.suggestions?.length" class="case-suggestions">
|
||||||
|
<h4>💡 处理建议</h4>
|
||||||
|
<ol>
|
||||||
|
<li v-for="suggestion in data.suggestions" :key="suggestion">{{ suggestion }}</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<section
|
||||||
|
v-if="data.thinkingSummary?.length"
|
||||||
|
class="solution-thinking"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="solution-thinking__header"
|
||||||
|
@click="thinkingExpanded = !thinkingExpanded"
|
||||||
|
>
|
||||||
|
<span class="solution-thinking__title">思考过程</span>
|
||||||
|
<span class="solution-thinking__status">已完成</span>
|
||||||
|
<ChevronDown
|
||||||
|
:size="13"
|
||||||
|
class="solution-thinking__arrow"
|
||||||
|
:class="{ expanded: thinkingExpanded }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<ol v-if="thinkingExpanded" class="solution-thinking__steps">
|
||||||
|
<li v-for="step in data.thinkingSummary" :key="step">{{ step }}</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
<div class="solution-base">
|
<div class="solution-base">
|
||||||
<b>分析基础:</b
|
<b>分析基础:</b
|
||||||
>{{ data.analysisBase || "基于相似品类、相似物料与标准库规则生成。" }}
|
>{{ data.analysisBase || "基于相似品类、相似物料与标准库规则生成。" }}
|
||||||
@ -393,9 +467,20 @@ function generateReport() {
|
|||||||
<CheckCircle2 :size="14" />追加到报告
|
<CheckCircle2 :size="14" />追加到报告
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="card.type === 'cases'"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="card.type !== 'solution'"
|
class="response-actions__primary"
|
||||||
:title="card.type === 'solution' ? '跳转判定标准库' : '功能开发中'"
|
@click="emit('openAssessment')"
|
||||||
|
>
|
||||||
|
评估关联风险
|
||||||
|
</button>
|
||||||
|
<button v-if="card.type === 'cases'" type="button" @click="openStandardLibrary">
|
||||||
|
查看标准
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="card.type === 'solution'"
|
||||||
|
type="button"
|
||||||
|
title="跳转判定标准库"
|
||||||
@click="openStandardLibrary"
|
@click="openStandardLibrary"
|
||||||
>
|
>
|
||||||
查看标准详情
|
查看标准详情
|
||||||
@ -523,6 +608,9 @@ function generateReport() {
|
|||||||
.response-card--material {
|
.response-card--material {
|
||||||
border-left: 3px solid var(--color-warning);
|
border-left: 3px solid var(--color-warning);
|
||||||
}
|
}
|
||||||
|
.response-card--cases {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
.response-card--standard,
|
.response-card--standard,
|
||||||
.response-card--compliance {
|
.response-card--compliance {
|
||||||
border-left: 3px solid var(--color-accent);
|
border-left: 3px solid var(--color-accent);
|
||||||
@ -571,6 +659,35 @@ function generateReport() {
|
|||||||
background: linear-gradient(135deg, var(--color-accent), var(--color-purple));
|
background: linear-gradient(135deg, var(--color-accent), var(--color-purple));
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.response-card--cases .response-card__icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
color: #111827;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.response-card--cases .response-card__header {
|
||||||
|
position: relative;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
.response-card--cases .response-card__header h3 {
|
||||||
|
color: var(--color-warning);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.response-card--cases .response-card__header small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.case-semantic-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.response-card--solution .response-card__header {
|
.response-card--solution .response-card__header {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
@ -735,6 +852,77 @@ function generateReport() {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-result-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
background: #f5f5f4;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.case-result-row:hover {
|
||||||
|
background: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-result-rank {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 9px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-result-row > div {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-result-text {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.case-result-row em {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.case-suggestions {
|
||||||
|
margin-top: 15px;
|
||||||
|
border: 1px solid #86efac;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.case-suggestions h4 {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.case-suggestions ol {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 17px;
|
||||||
|
}
|
||||||
|
.case-suggestions li {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
.solution-standard-row > div b {
|
.solution-standard-row > div b {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -796,6 +984,52 @@ function generateReport() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
.solution-thinking {
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid var(--color-border-light);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--color-bg-warm);
|
||||||
|
}
|
||||||
|
.solution-thinking__header {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
border: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.solution-thinking__title {
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.solution-thinking__status {
|
||||||
|
color: var(--color-success);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.solution-thinking__arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
.solution-thinking__arrow.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.solution-thinking__steps {
|
||||||
|
margin: 0;
|
||||||
|
border-top: 1px solid var(--color-border-light);
|
||||||
|
padding: 7px 12px 8px 27px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.solution-thinking__steps li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
.solution-standards {
|
.solution-standards {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border: 1px solid #bfdbfe;
|
border: 1px solid #bfdbfe;
|
||||||
|
|||||||
@ -9,10 +9,12 @@ import AssistantWelcome from "@/components/assistant/AssistantWelcome.vue";
|
|||||||
import RiskAnalysisCard from "@/components/assistant/RiskAnalysisCard.vue";
|
import RiskAnalysisCard from "@/components/assistant/RiskAnalysisCard.vue";
|
||||||
import AssistantResponseCard from "@/components/assistant/AssistantResponseCard.vue";
|
import AssistantResponseCard from "@/components/assistant/AssistantResponseCard.vue";
|
||||||
import OcrRecognitionDialog from "@/components/ocr/OcrRecognitionDialog.vue";
|
import OcrRecognitionDialog from "@/components/ocr/OcrRecognitionDialog.vue";
|
||||||
|
import { directConversationQuery } from "@/api/conversations";
|
||||||
import { useConversationStore } from "@/stores/conversation.store";
|
import { useConversationStore } from "@/stores/conversation.store";
|
||||||
import { useAuthStore } from "@/stores/auth.store";
|
import { useAuthStore } from "@/stores/auth.store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
|
import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
|
||||||
|
import type { AssistantResponseCard as AssistantResponseCardData } from "@/types/conversation";
|
||||||
import {
|
import {
|
||||||
readConversationReport,
|
readConversationReport,
|
||||||
writeConversationReport,
|
writeConversationReport,
|
||||||
@ -46,6 +48,10 @@ 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 directResult = ref<Record<string, unknown> | null>(null);
|
||||||
|
const directLoading = ref(false);
|
||||||
|
const directUserPrompt = ref("");
|
||||||
|
const directResponseCard = ref<AssistantResponseCardData | null>(null);
|
||||||
const expandedReplies = ref(new Set<number>());
|
const expandedReplies = ref(new Set<number>());
|
||||||
const primaryReportReady = ref(false);
|
const primaryReportReady = ref(false);
|
||||||
const assessmentFields = ref({
|
const assessmentFields = ref({
|
||||||
@ -96,9 +102,15 @@ const toolPlaceholder = computed(() => {
|
|||||||
return "请输入内容";
|
return "请输入内容";
|
||||||
});
|
});
|
||||||
const toolSubmitLabel = computed(() =>
|
const toolSubmitLabel = computed(() =>
|
||||||
["assessment", "material", "cases", "solution"].includes(tool.value ?? "")
|
["supplier", "material", "cases"].includes(tool.value ?? "")
|
||||||
? "开始分析"
|
? directLoading.value
|
||||||
: "带入对话",
|
? "查询中…"
|
||||||
|
: "查询"
|
||||||
|
: tool.value === "solution"
|
||||||
|
? directLoading.value
|
||||||
|
? "生成中…"
|
||||||
|
: "生成方案"
|
||||||
|
: "提交",
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadingText = computed(() =>
|
const loadingText = computed(() =>
|
||||||
@ -186,7 +198,11 @@ function openTool(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value === "supplier") {
|
if (value === "supplier") {
|
||||||
router.push({ name: "supplier" });
|
tool.value = "supplier";
|
||||||
|
toolInput.value = "";
|
||||||
|
toolFile.value = null;
|
||||||
|
toolError.value = "";
|
||||||
|
directResult.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value === "ocr") {
|
if (value === "ocr") {
|
||||||
@ -197,6 +213,7 @@ function openTool(
|
|||||||
toolInput.value = "";
|
toolInput.value = "";
|
||||||
toolFile.value = null;
|
toolFile.value = null;
|
||||||
toolError.value = "";
|
toolError.value = "";
|
||||||
|
directResult.value = null;
|
||||||
assessmentFields.value = {
|
assessmentFields.value = {
|
||||||
category: "",
|
category: "",
|
||||||
specification: "",
|
specification: "",
|
||||||
@ -210,9 +227,32 @@ function openTool(
|
|||||||
function closeTool() {
|
function closeTool() {
|
||||||
tool.value = null;
|
tool.value = null;
|
||||||
toolError.value = "";
|
toolError.value = "";
|
||||||
|
directResult.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitTool() {
|
async function submitDirectTool(kind: "supplier" | "material" | "cases" | "solution", query: string) {
|
||||||
|
directLoading.value = true;
|
||||||
|
toolError.value = "";
|
||||||
|
try {
|
||||||
|
const response = await directConversationQuery(kind, query);
|
||||||
|
directResult.value = response.data.data ?? null;
|
||||||
|
if (response.data.data) {
|
||||||
|
directUserPrompt.value = query;
|
||||||
|
directResponseCard.value = response.data.data as AssistantResponseCardData;
|
||||||
|
closeTool();
|
||||||
|
await nextTick();
|
||||||
|
await scrollToBottom();
|
||||||
|
}
|
||||||
|
} catch (reason) {
|
||||||
|
toolError.value = axios.isAxiosError(reason)
|
||||||
|
? String(reason.response?.data?.message || "查询失败")
|
||||||
|
: reason instanceof Error ? reason.message : "查询失败";
|
||||||
|
} finally {
|
||||||
|
directLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTool() {
|
||||||
const selectedTool = tool.value;
|
const selectedTool = tool.value;
|
||||||
const selectedTitle = toolTitle.value;
|
const selectedTitle = toolTitle.value;
|
||||||
if (selectedTool === "assessment") {
|
if (selectedTool === "assessment") {
|
||||||
@ -237,31 +277,33 @@ function submitTool() {
|
|||||||
toolError.value = "请输入要查询的物料名称";
|
toolError.value = "请输入要查询的物料名称";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toolInput.value = `查物料风险:${value}`;
|
toolInput.value = value;
|
||||||
} else if (selectedTool === "cases") {
|
} else if (selectedTool === "cases") {
|
||||||
const value = toolInput.value.trim();
|
const value = toolInput.value.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
toolError.value = "请输入要检索的异常现象或关键词";
|
toolError.value = "请输入要检索的异常现象或关键词";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toolInput.value = `搜索相似案例:${value}`;
|
toolInput.value = value;
|
||||||
} else if (selectedTool === "solution") {
|
} else if (selectedTool === "solution") {
|
||||||
const value = toolInput.value.trim();
|
const value = toolInput.value.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
toolError.value = "请输入需要生成方案的产品或风险问题";
|
toolError.value = "请输入需要生成方案的产品或风险问题";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toolInput.value = `生成 AI 案例推理方案:${value}`;
|
toolInput.value = value;
|
||||||
}
|
}
|
||||||
const finalValue = toolInput.value.trim();
|
const finalValue = toolInput.value.trim();
|
||||||
if (!finalValue && !toolFile.value) {
|
if (!finalValue && !toolFile.value) {
|
||||||
toolError.value = "请先填写必填内容";
|
toolError.value = "请先填写必填内容";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (["supplier", "material", "cases", "solution"].includes(selectedTool ?? "")) {
|
||||||
|
await submitDirectTool(selectedTool as "supplier" | "material" | "cases" | "solution", finalValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
closeTool();
|
closeTool();
|
||||||
void usePrompt(
|
void usePrompt(finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`);
|
||||||
finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectFile(event: Event) {
|
function selectFile(event: Event) {
|
||||||
@ -588,6 +630,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
|||||||
:card="message.responseCard"
|
:card="message.responseCard"
|
||||||
:can-append-report="primaryReportReady"
|
:can-append-report="primaryReportReady"
|
||||||
@generate-report="openSolutionReport"
|
@generate-report="openSolutionReport"
|
||||||
|
@open-assessment="openTool('assessment')"
|
||||||
/>
|
/>
|
||||||
<time class="message-time">{{
|
<time class="message-time">{{
|
||||||
formatMessageTime(message.createdAt)
|
formatMessageTime(message.createdAt)
|
||||||
@ -596,6 +639,24 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="directResponseCard">
|
||||||
|
<div class="message message--user">
|
||||||
|
<div class="message-avatar">{{ auth.user?.name?.slice(0, 1) ?? "我" }}</div>
|
||||||
|
<div class="user-bubble">{{ directUserPrompt }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="message message--assistant">
|
||||||
|
<div class="message-avatar">蝶</div>
|
||||||
|
<div class="message-body">
|
||||||
|
<AssistantResponseCard
|
||||||
|
:card="directResponseCard"
|
||||||
|
:can-append-report="primaryReportReady"
|
||||||
|
@generate-report="openSolutionReport"
|
||||||
|
@open-assessment="openTool('assessment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-if="loading" class="message message--assistant">
|
<div v-if="loading" class="message message--assistant">
|
||||||
<div class="message-avatar">蝶</div>
|
<div class="message-avatar">蝶</div>
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
@ -686,11 +747,49 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p v-if="toolError" class="dialog-error">{{ toolError }}</p>
|
<p v-if="toolError" class="dialog-error">{{ toolError }}</p>
|
||||||
|
<section v-if="directResult" class="direct-result-card">
|
||||||
|
<template v-if="tool === 'supplier'">
|
||||||
|
<span class="direct-tier">{{ directResult.tier || "新供应商" }}</span>
|
||||||
|
<div class="direct-lines">
|
||||||
|
<p>近 3 年异常 <b>N={{ directResult.anomalyCount ?? 0 }}</b>(代理合作频次口径)</p>
|
||||||
|
<p>基础分 <b>{{ directResult.supplierBaseScore ?? 70 }}</b> · α <b>×{{ Number(directResult.frequencyCoefficient ?? 1).toFixed(2) }}</b></p>
|
||||||
|
<p>供应商得分 S供 = <b>{{ directResult.supplierScore ?? 70 }}</b> 分</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="Array.isArray(directResult.topProblems) && directResult.topProblems.length">
|
||||||
|
高频异常:{{ (directResult.topProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
|
||||||
|
</p>
|
||||||
|
<p v-if="Array.isArray(directResult.suggestions)">建议:{{ (directResult.suggestions as string[]).join(" + ") }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="tool === 'material'">
|
||||||
|
<strong>{{ directResult.title || "物料风险分析" }}</strong>
|
||||||
|
<p>物料:<b>{{ directResult.material || directResult.query }}</b> · 历史异常 {{ directResult.anomalyCount ?? 0 }} 条</p>
|
||||||
|
<p v-if="Array.isArray(directResult.frequentProblems) && directResult.frequentProblems.length">
|
||||||
|
高频问题:{{ (directResult.frequentProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
|
||||||
|
</p>
|
||||||
|
<p v-if="Array.isArray(directResult.suggestions)">建议:{{ (directResult.suggestions as string[]).join(";") }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="tool === 'cases'">
|
||||||
|
<strong>相似案例检索</strong>
|
||||||
|
<p>关键词:{{ directResult.query }} · 匹配 {{ directResult.matchedCount ?? 0 }} 条</p>
|
||||||
|
<ul v-if="Array.isArray(directResult.cases)" class="direct-case-list">
|
||||||
|
<li v-for="(item, index) in (directResult.cases as Array<Record<string, unknown>>).slice(0, 5)" :key="index">
|
||||||
|
<b>{{ item.product || "未命名产品" }}</b>:{{ item.problem || item.description || "异常描述未填写" }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<strong>{{ directResult.title || "AI 方案生成" }}</strong>
|
||||||
|
<p>{{ directResult.analysisBase || "已结合历史案例与标准库生成分析方案。" }}</p>
|
||||||
|
<ol v-if="Array.isArray(directResult.suggestions)" class="direct-suggestion-list">
|
||||||
|
<li v-for="item in directResult.suggestions as string[]" :key="item">{{ item }}</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button type="button" class="dialog-secondary" @click="closeTool">
|
<button type="button" class="dialog-secondary" @click="closeTool">
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="dialog-primary">
|
<button type="submit" class="dialog-primary" :disabled="directLoading">
|
||||||
{{ toolSubmitLabel }}
|
{{ toolSubmitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -1070,6 +1169,52 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.direct-result-card {
|
||||||
|
margin-top: 14px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-bg-warm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-result-card p {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-result-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-tier {
|
||||||
|
display: inline-block !important;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: var(--color-warning-soft);
|
||||||
|
color: #92400e !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-lines {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-result-card ul,
|
||||||
|
.direct-result-card ol {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direct-case-list li,
|
||||||
|
.direct-suggestion-list li {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.conversation-column {
|
.conversation-column {
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { RiskAssessmentResult, RiskLevel } from "@/types/risk";
|
import type { RiskAssessmentResult, RiskLevel } from "@/types/risk";
|
||||||
|
|
||||||
export interface AssistantResponseCard {
|
export interface AssistantResponseCard {
|
||||||
type: "supplier" | "material" | "standard" | "compliance" | "solution";
|
type: "supplier" | "material" | "standard" | "compliance" | "solution" | "cases";
|
||||||
title: string;
|
title: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user