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:
Simon
2026-07-02 19:23:03 +08:00
parent fc22de7f4d
commit 6277db3951
5 changed files with 2207 additions and 18 deletions
+19
View File
@@ -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;