feat(C组): C-T1~T13 AI与数据模块完成
- C-T1 Wingman 辅助面板 - C-T2 Wingman 后端接口 - C-T3 排查流程图编辑器 - C-T4 流程图后端接口 - C-T5 流程图H5端展示 - C-T6 运营数据看板 - C-T7 看板数据接口 - C-T8 全局一致性审查(周2) - C-T9 坐席绩效报表 - C-T10 绩效数据接口+Excel导出 - C-T11 会话智能标注 - C-T12 全局一致性审查(周3) - C-T13 代码评审+PR创建
This commit is contained in:
@@ -89,3 +89,22 @@ export async function suggestTags(conversationId: string): Promise<TagsResult> {
|
||||
)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存会话标签
|
||||
* 将标签保存到会话记录中
|
||||
*
|
||||
* @param conversationId - 会话ID
|
||||
* @param tags - 标签字典
|
||||
* @returns 更新后的会话详情
|
||||
*/
|
||||
export async function saveConversationTags(
|
||||
conversationId: string,
|
||||
tags: Record<string, any>
|
||||
): Promise<any> {
|
||||
const response: AxiosResponse = await apiClient.post(
|
||||
`/conversations/${conversationId}/tags`,
|
||||
{ tags }
|
||||
)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
@@ -1,14 +1,149 @@
|
||||
<!-- =============================================================================
|
||||
// 企微IT智能服务台 — AI助手面板容器组件(v5.3 重构)
|
||||
// 企微IT智能服务台 — AI助手面板容器组件(v5.4 重构 + Wingman 集成)
|
||||
// =============================================================================
|
||||
// 说明:坐席工作台右侧的AI助手面板
|
||||
// 功能:上下两区域布局
|
||||
// 功能:
|
||||
// 顶部:Wingman 触发按钮组(生成回复建议 / 获取知识库引用 / 推荐排查流程)
|
||||
// Wingman 结果展示区:根据 API 结果展示不同类型的 AI 建议
|
||||
// 上方 ~1/3:🤖 AI 智能推荐区(1-3张推荐卡片)
|
||||
// 智能标注区:标签显示 + AI建议 + 手动编辑
|
||||
// 下方 ~2/3:快速回复区(复用 QuickReplyPanel)
|
||||
// ============================================================================= -->
|
||||
|
||||
<template>
|
||||
<div class="ai-assistant-panel">
|
||||
<!-- ================================================================== -->
|
||||
<!-- Wingman 触发按钮组 -->
|
||||
<!-- ================================================================== -->
|
||||
<div class="wingman-triggers">
|
||||
<el-button
|
||||
class="wingman-btn"
|
||||
size="small"
|
||||
:loading="loadingDraft"
|
||||
:disabled="!hasActiveConversation"
|
||||
@click="handleGenerateDraft"
|
||||
title="基于当前对话生成AI回复建议"
|
||||
>
|
||||
🤖 生成回复建议
|
||||
</el-button>
|
||||
<el-button
|
||||
class="wingman-btn"
|
||||
size="small"
|
||||
:loading="loadingKnowledge"
|
||||
:disabled="!hasActiveConversation"
|
||||
@click="handleGetKnowledge"
|
||||
title="从知识库中检索相关引用"
|
||||
>
|
||||
📚 获取知识库引用
|
||||
</el-button>
|
||||
<el-button
|
||||
class="wingman-btn"
|
||||
size="small"
|
||||
:loading="loadingFlow"
|
||||
:disabled="!hasActiveConversation"
|
||||
@click="handleRecommendFlow"
|
||||
title="基于问题类型推荐排查流程"
|
||||
>
|
||||
🔍 推荐排查流程
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Wingman 结果展示区 -->
|
||||
<!-- ================================================================== -->
|
||||
<div v-if="wingmanResult" class="wingman-result-section">
|
||||
<!-- 结果标题栏 -->
|
||||
<div class="wingman-result-header">
|
||||
<span class="wingman-result-bar"></span>
|
||||
<span class="wingman-result-title">{{ wingmanResult.title }}</span>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="clearWingmanResult"
|
||||
title="关闭"
|
||||
>
|
||||
✕
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 回复草稿结果 -->
|
||||
<div v-if="wingmanResult.type === 'draft'" class="wingman-draft-card">
|
||||
<div class="draft-content">{{ wingmanResult.data.content }}</div>
|
||||
<div class="draft-meta">
|
||||
<el-tag size="small" type="success">
|
||||
置信度: {{ (wingmanResult.data.confidence * 100).toFixed(0) }}%
|
||||
</el-tag>
|
||||
<span v-if="wingmanResult.data.reasoning" class="draft-reasoning">
|
||||
💡 {{ wingmanResult.data.reasoning }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="draft-actions">
|
||||
<el-button type="primary" size="small" @click="adoptDraft(wingmanResult.data.content)">
|
||||
✅ 采纳到输入框
|
||||
</el-button>
|
||||
<el-button size="small" @click="editDraft(wingmanResult.data.content)">
|
||||
✏️ 编辑后发送
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识库引用结果 -->
|
||||
<div v-else-if="wingmanResult.type === 'knowledge'" class="wingman-knowledge-card">
|
||||
<div class="knowledge-section">
|
||||
<div class="knowledge-label">📋 问题描述</div>
|
||||
<div class="knowledge-text">{{ wingmanResult.data.problem }}</div>
|
||||
</div>
|
||||
<div class="knowledge-section">
|
||||
<div class="knowledge-label">🔍 原因分析</div>
|
||||
<div class="knowledge-text">{{ wingmanResult.data.cause }}</div>
|
||||
</div>
|
||||
<div class="knowledge-section">
|
||||
<div class="knowledge-label">✅ 解决方案</div>
|
||||
<div class="knowledge-text">{{ wingmanResult.data.solution }}</div>
|
||||
</div>
|
||||
<div class="knowledge-actions">
|
||||
<el-button type="primary" size="small" @click="adoptDraft(wingmanResult.data.solution)">
|
||||
📝 将方案填入输入框
|
||||
</el-button>
|
||||
<el-button size="small" @click="copyKnowledgeToClipboard">
|
||||
📋 复制全部
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排查流程推荐结果 -->
|
||||
<div v-else-if="wingmanResult.type === 'flow'" class="wingman-flow-card">
|
||||
<div class="flow-header">
|
||||
<span class="flow-icon">🔧</span>
|
||||
<span class="flow-title">推荐排查流程: {{ wingmanResult.data.category }}</span>
|
||||
</div>
|
||||
<div v-if="wingmanResult.data.priority" class="flow-priority">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="wingmanResult.data.priority === 'high' ? 'danger' : wingmanResult.data.priority === 'medium' ? 'warning' : 'info'"
|
||||
>
|
||||
优先级: {{ priorityLabel(wingmanResult.data.priority) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flow-steps">
|
||||
<div
|
||||
v-for="(tag, idx) in wingmanResult.data.suggested_tags"
|
||||
:key="idx"
|
||||
class="flow-step-item"
|
||||
>
|
||||
<span class="flow-step-num">{{ idx + 1 }}</span>
|
||||
<span class="flow-step-label">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-empty" v-if="!wingmanResult.data.suggested_tags || wingmanResult.data.suggested_tags.length === 0">
|
||||
暂无排查步骤建议,请结合知识库信息手动排查
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线(仅在有 Wingman 结果时显示) -->
|
||||
<div v-if="wingmanResult" class="ai-section-divider"></div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 上方 ~1/3 区域 — 🤖 AI 智能推荐区 -->
|
||||
<!-- ================================================================== -->
|
||||
@@ -39,6 +174,83 @@
|
||||
<!-- 分隔线 -->
|
||||
<div class="ai-section-divider"></div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 智能标注区 — C-T11 新增 -->
|
||||
<!-- ================================================================== -->
|
||||
<div class="ai-tags-section">
|
||||
<!-- 标题栏 -->
|
||||
<div class="ai-tags-header">
|
||||
<span class="ai-tags-bar"></span>
|
||||
<span class="ai-tags-title">🏷️ 智能标注</span>
|
||||
<el-button
|
||||
size="small"
|
||||
:loading="loadingTagSuggestions"
|
||||
@click="handleSuggestTags"
|
||||
title="获取AI标签建议"
|
||||
>
|
||||
AI建议
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<div class="ai-tags-list">
|
||||
<!-- 已保存的标签 -->
|
||||
<div v-if="Object.keys(currentTags).length > 0" class="tags-saved">
|
||||
<el-tag
|
||||
v-for="(value, key) in currentTags"
|
||||
:key="key"
|
||||
class="tag-item"
|
||||
:type="getTagType(key as string)"
|
||||
closable
|
||||
@close="handleRemoveTag(key as string)"
|
||||
>
|
||||
{{ formatTagLabel(key as string, value) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- AI 建议的标签 -->
|
||||
<div v-if="suggestedTagList.length > 0" class="tags-suggested">
|
||||
<div class="tags-suggested-title">AI 建议:</div>
|
||||
<el-tag
|
||||
v-for="tag in suggestedTagList"
|
||||
:key="tag"
|
||||
class="tag-item tag-suggested"
|
||||
type="info"
|
||||
@click="handleAddSuggestedTag(tag)"
|
||||
>
|
||||
+ {{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 无标签时提示 -->
|
||||
<div v-if="Object.keys(currentTags).length === 0 && suggestedTagList.length === 0" class="ai-tags-empty">
|
||||
暂无标签,点击"AI建议"获取智能标注
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动添加标签 -->
|
||||
<div class="ai-tags-add">
|
||||
<el-input
|
||||
v-model="newTagKey"
|
||||
placeholder="标签名"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
@keyup.enter="handleAddManualTag"
|
||||
/>
|
||||
<el-input
|
||||
v-model="newTagValue"
|
||||
placeholder="值"
|
||||
size="small"
|
||||
style="width: 80px"
|
||||
@keyup.enter="handleAddManualTag"
|
||||
/>
|
||||
<el-button size="small" @click="handleAddManualTag">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="ai-section-divider"></div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 下方 ~2/3 区域 — 快速回复区 -->
|
||||
<!-- ================================================================== -->
|
||||
@@ -52,11 +264,13 @@
|
||||
// ============================================================================
|
||||
// 导入
|
||||
// ============================================================================
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import AiSuggestReply from './AiSuggestReply.vue'
|
||||
import QuickReplyPanel from './QuickReplyPanel.vue'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import type { DraftResult } from '@/api/wingman'
|
||||
import type { DraftResult, SummaryResult, TagsResult } from '@/api/wingman'
|
||||
import { generateDraft, generateSummary, suggestTags, saveConversationTags } from '@/api/wingman'
|
||||
|
||||
// ============================================================================
|
||||
// 类型
|
||||
@@ -72,6 +286,22 @@ interface AiRecommendation {
|
||||
confidence: number
|
||||
}
|
||||
|
||||
/** 当前会话的标签类型 */
|
||||
type ConversationTags = Record<string, any>
|
||||
|
||||
/** Wingman 结果类型枚举 */
|
||||
type WingmanResultType = 'draft' | 'knowledge' | 'flow'
|
||||
|
||||
/** Wingman 结果展示数据 */
|
||||
interface WingmanResultData {
|
||||
/** 结果类型 */
|
||||
type: WingmanResultType
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 原始 API 数据 */
|
||||
data: DraftResult | SummaryResult | TagsResult
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 状态
|
||||
// ============================================================================
|
||||
@@ -79,22 +309,52 @@ interface AiRecommendation {
|
||||
/** 会话 Store */
|
||||
const conversationStore = useConversationStore()
|
||||
|
||||
/** AI 推荐列表(Mock 数据,后续由 AI 引擎填充) */
|
||||
/** AI 推荐列表 */
|
||||
const recommendations = ref<AiRecommendation[]>([])
|
||||
|
||||
/** 当前会话的标签 */
|
||||
const currentTags = ref<ConversationTags>({})
|
||||
|
||||
/** AI 建议的标签列表 */
|
||||
const suggestedTagList = ref<string[]>([])
|
||||
|
||||
/** 加载标签建议中 */
|
||||
const loadingTagSuggestions = ref(false)
|
||||
|
||||
/** 新增标签 - 键 */
|
||||
const newTagKey = ref('')
|
||||
|
||||
/** 新增标签 - 值 */
|
||||
const newTagValue = ref('')
|
||||
|
||||
/** Wingman 当前结果 */
|
||||
const wingmanResult = ref<WingmanResultData | null>(null)
|
||||
|
||||
/** Wingman 加载状态 - 生成回复 */
|
||||
const loadingDraft = ref(false)
|
||||
|
||||
/** Wingman 加载状态 - 知识库 */
|
||||
const loadingKnowledge = ref(false)
|
||||
|
||||
/** Wingman 加载状态 - 排查流程 */
|
||||
const loadingFlow = ref(false)
|
||||
|
||||
// ============================================================================
|
||||
// 计算属性
|
||||
// ============================================================================
|
||||
|
||||
/** 是否有活跃的会话 */
|
||||
const hasActiveConversation = computed(() => {
|
||||
return !!conversationStore.currentConversationId
|
||||
})
|
||||
|
||||
/**
|
||||
* 从当前会话的 AI 草稿生成推荐列表
|
||||
* 将 draft 数据映射为推荐卡片格式
|
||||
*/
|
||||
const loadRecommendationsFromDraft = computed(() => {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) return []
|
||||
|
||||
// 尝试从 AI 草稿缓存中获取推荐
|
||||
const convDrafts = conversationStore.aiDrafts.get(convId)
|
||||
if (!convDrafts || convDrafts.size === 0) return []
|
||||
|
||||
@@ -113,11 +373,177 @@ const loadRecommendationsFromDraft = computed(() => {
|
||||
// 方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 优先级中文标签
|
||||
*/
|
||||
function priorityLabel(priority: string): string {
|
||||
const map: Record<string, string> = { high: '高', medium: '中', low: '低' }
|
||||
return map[priority] || priority
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签类型
|
||||
*/
|
||||
function getTagType(key: string): '' | 'success' | 'info' | 'warning' | 'danger' {
|
||||
const tagTypeMap: Record<string, '' | 'success' | 'info' | 'warning' | 'danger'> = {
|
||||
priority: 'danger',
|
||||
category: 'warning',
|
||||
emotion: 'info',
|
||||
blocking: 'danger',
|
||||
impact_scope: 'warning',
|
||||
}
|
||||
return tagTypeMap[key] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化标签显示文本
|
||||
*/
|
||||
function formatTagLabel(key: string, value: any): string {
|
||||
const labelMap: Record<string, string> = {
|
||||
priority: '优先级',
|
||||
category: '分类',
|
||||
emotion: '情绪',
|
||||
blocking: '阻断',
|
||||
impact_scope: '影响范围',
|
||||
}
|
||||
const label = labelMap[key] || key
|
||||
return `${label}: ${value}`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wingman 触发方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Wingman 触发 - 生成回复建议
|
||||
* 调用 wingman/draft API,展示 AI 生成的回复草稿
|
||||
*/
|
||||
async function handleGenerateDraft(): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) {
|
||||
ElMessage.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
loadingDraft.value = true
|
||||
try {
|
||||
const result: DraftResult = await generateDraft(convId)
|
||||
wingmanResult.value = {
|
||||
type: 'draft',
|
||||
title: '🤖 AI 回复建议',
|
||||
data: result,
|
||||
}
|
||||
ElMessage.success('回复建议已生成')
|
||||
} catch (error: any) {
|
||||
console.error('生成回复建议失败:', error)
|
||||
ElMessage.error(error?.message || '生成回复建议失败')
|
||||
} finally {
|
||||
loadingDraft.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wingman 触发 - 获取知识库引用
|
||||
* 调用 wingman/summary API,展示结构化摘要作为知识库引用
|
||||
*/
|
||||
async function handleGetKnowledge(): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) {
|
||||
ElMessage.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
loadingKnowledge.value = true
|
||||
try {
|
||||
const result: SummaryResult = await generateSummary(convId)
|
||||
wingmanResult.value = {
|
||||
type: 'knowledge',
|
||||
title: '📚 知识库引用',
|
||||
data: result,
|
||||
}
|
||||
ElMessage.success('知识库引用已获取')
|
||||
} catch (error: any) {
|
||||
console.error('获取知识库引用失败:', error)
|
||||
ElMessage.error(error?.message || '获取知识库引用失败')
|
||||
} finally {
|
||||
loadingKnowledge.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wingman 触发 - 推荐排查流程
|
||||
* 调用 wingman/tags API,按分类推荐排查步骤
|
||||
*/
|
||||
async function handleRecommendFlow(): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) {
|
||||
ElMessage.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
loadingFlow.value = true
|
||||
try {
|
||||
const result: TagsResult = await suggestTags(convId)
|
||||
wingmanResult.value = {
|
||||
type: 'flow',
|
||||
title: '🔍 排查流程推荐',
|
||||
data: result,
|
||||
}
|
||||
ElMessage.success('排查流程已推荐')
|
||||
} catch (error: any) {
|
||||
console.error('推荐排查流程失败:', error)
|
||||
ElMessage.error(error?.message || '推荐排查流程失败')
|
||||
} finally {
|
||||
loadingFlow.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 Wingman 结果展示
|
||||
*/
|
||||
function clearWingmanResult(): void {
|
||||
wingmanResult.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 采纳草稿到输入框
|
||||
*/
|
||||
function adoptDraft(content: string): void {
|
||||
conversationStore.pendingReplyText = content
|
||||
ElMessage.success('已填入输入框')
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑草稿后发送(与 adopt 相同行为,填入输入框让坐席编辑)
|
||||
*/
|
||||
function editDraft(content: string): void {
|
||||
conversationStore.pendingReplyText = content
|
||||
ElMessage.success('已填入输入框,可编辑后发送')
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制知识库内容到剪贴板
|
||||
*/
|
||||
async function copyKnowledgeToClipboard(): Promise<void> {
|
||||
if (!wingmanResult.value || wingmanResult.value.type !== 'knowledge') return
|
||||
|
||||
const data = wingmanResult.value.data as SummaryResult
|
||||
const text = `问题描述:${data.problem}\n原因分析:${data.cause}\n解决方案:${data.solution}`
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.warning('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI 推荐区方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 处理 AI 推荐卡片的选择
|
||||
* 将推荐内容填充到对话输入框
|
||||
*
|
||||
* @param content - 推荐内容
|
||||
*/
|
||||
function handleSelectRecommendation(content: string): void {
|
||||
conversationStore.pendingReplyText = content
|
||||
@@ -125,9 +551,6 @@ function handleSelectRecommendation(content: string): void {
|
||||
|
||||
/**
|
||||
* 处理快速回复模板的"使用"事件
|
||||
* 将模板内容填充到对话输入框
|
||||
*
|
||||
* @param content - 模板内容(已替换变量)
|
||||
*/
|
||||
function handleUseTemplate(content: string): void {
|
||||
conversationStore.pendingReplyText = content
|
||||
@@ -135,7 +558,6 @@ function handleUseTemplate(content: string): void {
|
||||
|
||||
/**
|
||||
* 加载 AI 推荐数据
|
||||
* 优先从草稿缓存获取,否则使用 Mock 数据
|
||||
*/
|
||||
function loadRecommendations(): void {
|
||||
const fromDraft = loadRecommendationsFromDraft.value
|
||||
@@ -144,7 +566,6 @@ function loadRecommendations(): void {
|
||||
return
|
||||
}
|
||||
|
||||
// Mock 数据:当无真实 AI 推荐时展示示例
|
||||
if (conversationStore.currentConversationId) {
|
||||
recommendations.value = [
|
||||
{
|
||||
@@ -163,12 +584,145 @@ function loadRecommendations(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 标签区方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 加载当前会话的标签
|
||||
*/
|
||||
function loadCurrentTags(): void {
|
||||
const conversation = conversationStore.currentConversation
|
||||
if (conversation && conversation.tags) {
|
||||
currentTags.value = conversation.tags as ConversationTags
|
||||
} else {
|
||||
currentTags.value = {}
|
||||
}
|
||||
suggestedTagList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 标签建议
|
||||
*/
|
||||
async function handleSuggestTags(): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) {
|
||||
ElMessage.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
loadingTagSuggestions.value = true
|
||||
try {
|
||||
const result = await suggestTags(convId)
|
||||
if (result && result.suggested_tags) {
|
||||
suggestedTagList.value = result.suggested_tags.filter(
|
||||
(tag: string) => !Object.keys(currentTags.value).includes(tag)
|
||||
)
|
||||
if (suggestedTagList.value.length === 0) {
|
||||
ElMessage.info('暂无新的标签建议')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签建议失败:', error)
|
||||
ElMessage.error('获取标签建议失败')
|
||||
} finally {
|
||||
loadingTagSuggestions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 AI 建议的标签
|
||||
*/
|
||||
async function handleAddSuggestedTag(tag: string): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) return
|
||||
|
||||
currentTags.value[tag] = true
|
||||
suggestedTagList.value = suggestedTagList.value.filter((t) => t !== tag)
|
||||
|
||||
try {
|
||||
await saveConversationTags(convId, currentTags.value)
|
||||
ElMessage.success(`已添加标签: ${tag}`)
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error)
|
||||
ElMessage.error('保存标签失败')
|
||||
delete currentTags.value[tag]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除已保存的标签
|
||||
*/
|
||||
async function handleRemoveTag(key: string): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) return
|
||||
|
||||
const oldValue = currentTags.value[key]
|
||||
delete currentTags.value[key]
|
||||
|
||||
try {
|
||||
await saveConversationTags(convId, currentTags.value)
|
||||
ElMessage.success(`已移除标签: ${key}`)
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error)
|
||||
ElMessage.error('保存标签失败')
|
||||
currentTags.value[key] = oldValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动添加标签
|
||||
*/
|
||||
async function handleAddManualTag(): Promise<void> {
|
||||
const convId = conversationStore.currentConversationId
|
||||
if (!convId) {
|
||||
ElMessage.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
const key = newTagKey.value.trim()
|
||||
const value = newTagValue.value.trim()
|
||||
|
||||
if (!key) {
|
||||
ElMessage.warning('请输入标签名')
|
||||
return
|
||||
}
|
||||
|
||||
currentTags.value[key] = value || true
|
||||
newTagKey.value = ''
|
||||
newTagValue.value = ''
|
||||
|
||||
try {
|
||||
await saveConversationTags(convId, currentTags.value)
|
||||
ElMessage.success(`已添加标签: ${key}`)
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error)
|
||||
ElMessage.error('保存标签失败')
|
||||
delete currentTags.value[key]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 监听
|
||||
// ============================================================================
|
||||
|
||||
/** 监听当前会话变化,重新加载数据和清除 Wingman 结果 */
|
||||
watch(
|
||||
() => conversationStore.currentConversationId,
|
||||
() => {
|
||||
loadRecommendations()
|
||||
loadCurrentTags()
|
||||
clearWingmanResult()
|
||||
}
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
onMounted(() => {
|
||||
loadRecommendations()
|
||||
loadCurrentTags()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -180,6 +734,199 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---- Wingman 触发按钮组 ---- */
|
||||
.wingman-triggers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wingman-btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---- Wingman 结果展示区 ---- */
|
||||
.wingman-result-section {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wingman-result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wingman-result-bar {
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
background-color: #07C160;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wingman-result-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ---- 回复草稿卡片 ---- */
|
||||
.wingman-draft-card {
|
||||
background: rgba(7, 193, 96, 0.04);
|
||||
border: 1px solid rgba(7, 193, 96, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.draft-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.draft-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.draft-reasoning {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.draft-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ---- 知识库引用卡片 ---- */
|
||||
.wingman-knowledge-card {
|
||||
background: rgba(64, 158, 255, 0.04);
|
||||
border: 1px solid rgba(64, 158, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.knowledge-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.knowledge-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.knowledge-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.knowledge-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- 排查流程推荐卡片 ---- */
|
||||
.wingman-flow-card {
|
||||
background: rgba(245, 158, 11, 0.04);
|
||||
border: 1px solid rgba(245, 158, 11, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.flow-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flow-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.flow-priority {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.flow-step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.flow-step-num {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
color: #07C160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-step-label {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.flow-empty {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* ---- 分隔线 ---- */
|
||||
.ai-section-divider {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- 上方 AI 推荐区 ---- */
|
||||
.ai-recommend-section {
|
||||
flex: 0 0 auto;
|
||||
@@ -225,12 +972,84 @@ onMounted(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- 分隔线 ---- */
|
||||
.ai-section-divider {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
/* ---- 智能标注区 ---- */
|
||||
.ai-tags-section {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-tags-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-tags-bar {
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
background-color: var(--accent);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-tags-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-tags-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tags-saved {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tags-suggested {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags-suggested-title {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-suggested {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.ai-tags-empty {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ai-tags-add {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ---- 下方快速回复区 ---- */
|
||||
.quick-reply-section {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user