feat: 移除自定义筛选的AI功能

This commit is contained in:
digua
2026-03-10 22:56:47 +08:00
parent bfe72d8e44
commit 2b354d269b
7 changed files with 5 additions and 488 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
/**
* 自定义筛选 Tab
* 用于精准提取聊天记录上下文,供 AI 分析使用
* 用于精准提取聊天记录上下文
*
* 支持两种互斥的筛选模式:
* 1. 条件筛选:按关键词、时间、发送者筛选,并自动扩展上下文
@@ -18,7 +18,6 @@ import ConditionPanel from './ConditionPanel.vue'
import SessionPanel from './SessionPanel.vue'
import PreviewPanel from './PreviewPanel.vue'
import FilterHistory from './FilterHistory.vue'
import LocalAnalysisModal from './LocalAnalysisModal.vue'
const { t } = useI18n()
const toast = useToast()
@@ -88,28 +87,10 @@ const filterResult = ref<{
const isFiltering = ref(false)
const isLoadingMore = ref(false)
const showHistory = ref(false)
const showAnalysisModal = ref(false)
// 每页块数
const PAGE_SIZE = 50
// 估算 Token 数
// 中文1 字符 ≈ 1.5 token因为中文分词后每个字符可能产生 1-2 个 token
// 考虑到消息格式(时间、发送人等),使用 1.5 作为估算系数
const estimatedTokens = computed(() => {
if (!filterResult.value) return 0
return Math.ceil(filterResult.value.stats.totalChars * 1.5)
})
// Token 状态green < 50000, yellow 50000-100000, red > 100000
// (基于大多数模型的上下文窗口大小)
const tokenStatus = computed(() => {
const tokens = estimatedTokens.value
if (tokens < 50000) return 'green'
if (tokens < 100000) return 'yellow'
return 'red'
})
// 是否可以执行筛选
const canExecuteFilter = computed(() => {
if (isFiltering.value) return false
@@ -324,12 +305,6 @@ async function exportFeedPack() {
}
}
// 打开本地 AI 分析
function openLocalAnalysis() {
if (!filterResult.value || filterResult.value.blocks.length === 0) return
showAnalysisModal.value = true
}
// 切换模式时清空结果
watch(filterMode, () => {
filterResult.value = null
@@ -423,8 +398,6 @@ function loadHistoryCondition(condition: {
:result="filterResult"
:is-loading="isFiltering"
:is-loading-more="isLoadingMore"
:estimated-tokens="estimatedTokens"
:token-status="tokenStatus"
@load-more="loadMoreBlocks"
/>
@@ -457,9 +430,6 @@ function loadHistoryCondition(condition: {
>
{{ t('analysis.filter.export') }}
</UButton>
<UButton color="primary" icon="i-heroicons-sparkles" @click="openLocalAnalysis">
{{ t('analysis.filter.localAnalysis') }}
</UButton>
</div>
</div>
</div>
@@ -467,8 +437,5 @@ function loadHistoryCondition(condition: {
<!-- 历史记录弹窗 -->
<FilterHistory v-model:open="showHistory" @load="loadHistoryCondition" />
<!-- 本地 AI 分析弹窗 -->
<LocalAnalysisModal v-model:open="showAnalysisModal" :filter-result="filterResult" :filter-mode="filterMode" />
</div>
</template>

View File

@@ -1,364 +0,0 @@
<script setup lang="ts">
/**
* 本地 AI 分析弹窗
* 支持预设分析和自定义提问
*/
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSessionStore } from '@/stores/session'
import MarkdownIt from 'markdown-it'
// 创建 markdown-it 实例
const md = new MarkdownIt({
html: false,
breaks: true,
linkify: true,
typographer: true,
})
const { t } = useI18n()
const sessionStore = useSessionStore()
// 筛选结果消息类型
interface FilterMessage {
id: number
senderName: string
senderPlatformId: string
senderAliases: string[]
senderAvatar: string | null
content: string
timestamp: number
type: number
replyToMessageId: string | null
replyToContent: string | null
replyToSenderName: string | null
isHit: boolean
}
// Props
const props = defineProps<{
filterResult: {
blocks: Array<{
startTs: number
endTs: number
messages: FilterMessage[]
hitCount: number
}>
stats: {
totalMessages: number
hitMessages: number
totalChars: number
}
} | null
filterMode: 'condition' | 'session'
}>()
const open = defineModel<boolean>('open', { default: false })
// 数据量阈值(超过此数量时提示数据量过大)
const DATA_TOO_LARGE_THRESHOLD = 5000
// 检查数据量是否过大
const isDataTooLarge = computed(() => {
if (!props.filterResult) return false
return props.filterResult.stats.totalMessages > DATA_TOO_LARGE_THRESHOLD
})
// 分析模式:'preset' | 'custom'
const analysisMode = ref<'preset' | 'custom'>('preset')
// 预设分析选项
const presetOptions = [
{ id: 'summary', label: '总结对话要点', prompt: '请总结这段对话的主要内容和关键要点。' },
{ id: 'sentiment', label: '情感分析', prompt: '请分析这段对话中参与者的情感变化和整体氛围。' },
{ id: 'topics', label: '话题提取', prompt: '请提取这段对话中讨论的主要话题,并简要说明每个话题的内容。' },
{ id: 'insights', label: '洞察分析', prompt: '请对这段对话进行深度分析,包括参与者的关系、互动模式、潜在问题等。' },
]
const selectedPreset = ref(presetOptions[0].id)
const customPrompt = ref('')
// 可编辑的预设提示词(允许用户临时修改)
const editablePresetPrompt = ref(presetOptions[0].prompt)
// 分析状态
const isAnalyzing = ref(false)
const analysisResult = ref('')
const analysisError = ref('')
// 当前请求 ID用于中止
let currentRequestId: string | null = null
// 构建上下文内容
const contextContent = computed(() => {
if (!props.filterResult) return ''
let content = ''
for (const block of props.filterResult.blocks) {
const startTime = new Date(block.startTs * 1000).toLocaleString()
content += `\n--- 对话段落 (${startTime}) ---\n`
for (const msg of block.messages) {
const time = new Date(msg.timestamp * 1000).toLocaleTimeString()
content += `[${time}] ${msg.senderName}: ${msg.content || '[非文本消息]'}\n`
}
}
return content
})
// 获取用户提问
const userQuestion = computed(() => {
if (analysisMode.value === 'preset') {
return editablePresetPrompt.value
}
return customPrompt.value
})
// 执行分析
async function executeAnalysis() {
if (!props.filterResult || !userQuestion.value) return
isAnalyzing.value = true
analysisResult.value = ''
analysisError.value = ''
try {
// 构建完整的消息
const fullMessage = `以下是一段聊天记录,请根据用户的问题进行分析:
${contextContent.value}
---
用户问题:${userQuestion.value}`
// 调用 Agent API
const context = {
sessionId: sessionStore.currentSessionId || '',
}
const { requestId, promise } = window.agentApi.runStream(
fullMessage,
context,
(chunk) => {
if (chunk.type === 'content' && chunk.content) {
analysisResult.value += chunk.content
} else if (chunk.type === 'error') {
analysisError.value = chunk.error || '分析出错'
}
},
[], // 不需要历史消息
sessionStore.currentSession?.type === 'group' ? 'group' : 'private'
)
currentRequestId = requestId
const result = await promise
if (!result.success && result.error) {
analysisError.value = result.error
}
} catch (error) {
console.error('分析失败:', error)
analysisError.value = String(error)
} finally {
isAnalyzing.value = false
currentRequestId = null
}
}
// 中止分析
async function abortAnalysis() {
if (currentRequestId) {
try {
await window.agentApi.abort(currentRequestId)
} catch (error) {
console.error('中止失败:', error)
}
isAnalyzing.value = false
currentRequestId = null
}
}
// 复制结果
async function copyResult() {
try {
await navigator.clipboard.writeText(analysisResult.value)
} catch (error) {
console.error('复制失败:', error)
}
}
// 监听预设选择变化,更新可编辑的提示词
watch(selectedPreset, (newPresetId) => {
const preset = presetOptions.find((p) => p.id === newPresetId)
if (preset) {
editablePresetPrompt.value = preset.prompt
}
})
// 关闭时重置状态
watch(open, (val) => {
if (!val) {
analysisResult.value = ''
analysisError.value = ''
if (isAnalyzing.value) {
abortAnalysis()
}
} else {
// 打开时重置为默认预设
editablePresetPrompt.value = presetOptions.find((p) => p.id === selectedPreset.value)?.prompt || ''
}
})
</script>
<template>
<UModal v-model:open="open" :ui="{ width: 'max-w-3xl' }">
<template #content>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">{{ t('analysis.filter.localAnalysisTitle') }}</h3>
<UButton variant="ghost" icon="i-heroicons-x-mark" size="sm" @click="open = false" />
</div>
</template>
<div class="space-y-4">
<!-- 上下文摘要 -->
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm">
<div class="flex items-center gap-4 text-gray-600 dark:text-gray-400">
<span>{{ filterResult?.blocks.length || 0 }} 个对话块</span>
<span>{{ filterResult?.stats.totalMessages || 0 }} 条消息</span>
<span>{{ filterResult?.stats.totalChars.toLocaleString() || 0 }} 字符</span>
</div>
</div>
<!-- 数据量过大警告 -->
<div
v-if="isDataTooLarge"
class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
>
<div class="flex items-start gap-2">
<UIcon name="i-heroicons-exclamation-triangle" class="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-red-700 dark:text-red-400">
{{ t('analysis.filter.dataTooLarge') }}
</p>
<p class="text-red-600 dark:text-red-500 mt-1">
{{ t('analysis.filter.dataTooLargeThreshold', { count: DATA_TOO_LARGE_THRESHOLD }) }}
</p>
</div>
</div>
</div>
<!-- 分析模式切换 -->
<div class="flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg w-fit">
<button
class="px-3 py-1.5 text-sm font-medium rounded-md transition-colors"
:class="
analysisMode === 'preset'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
"
@click="analysisMode = 'preset'"
>
{{ t('analysis.filter.presetAnalysis') }}
</button>
<button
class="px-3 py-1.5 text-sm font-medium rounded-md transition-colors"
:class="
analysisMode === 'custom'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
"
@click="analysisMode = 'custom'"
>
{{ t('analysis.filter.customAnalysis') }}
</button>
</div>
<!-- 预设分析选项 -->
<div v-if="analysisMode === 'preset'" class="space-y-3">
<div class="grid grid-cols-2 gap-2">
<label
v-for="option in presetOptions"
:key="option.id"
class="flex items-center gap-2 p-3 border rounded-lg cursor-pointer transition-colors"
:class="
selectedPreset === option.id
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
"
>
<input v-model="selectedPreset" type="radio" :value="option.id" class="text-primary-500" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{ option.label }}</span>
</label>
</div>
<!-- 可编辑的提示词 -->
<div class="space-y-1">
<label class="text-xs text-gray-500 dark:text-gray-400">
{{ t('analysis.filter.editablePromptLabel') }}
</label>
<UTextarea v-model="editablePresetPrompt" :rows="2" class="w-full text-sm" />
</div>
</div>
<!-- 自定义提问 -->
<div v-else>
<UTextarea
v-model="customPrompt"
:placeholder="t('analysis.filter.customPromptPlaceholder')"
:rows="3"
class="w-full"
/>
</div>
<!-- 分析按钮 -->
<div class="flex justify-end gap-2">
<UButton v-if="isAnalyzing" color="red" variant="outline" @click="abortAnalysis">
{{ t('common.cancel') }}
</UButton>
<UButton
color="primary"
:loading="isAnalyzing"
:disabled="isAnalyzing || isDataTooLarge || (analysisMode === 'custom' && !customPrompt.trim())"
@click="executeAnalysis"
>
<UIcon name="i-heroicons-sparkles" class="w-4 h-4 mr-1" />
{{ isAnalyzing ? t('analysis.filter.analyzing') : t('analysis.filter.startAnalysis') }}
</UButton>
</div>
<!-- 分析结果 -->
<div v-if="analysisResult || analysisError" class="border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('analysis.filter.analysisResult') }}
</h4>
<UButton v-if="analysisResult" size="xs" variant="ghost" @click="copyResult">
<UIcon name="i-heroicons-clipboard" class="w-4 h-4 mr-1" />
{{ t('common.copy') }}
</UButton>
</div>
<!-- 错误提示 -->
<div
v-if="analysisError"
class="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm"
>
{{ analysisError }}
</div>
<!-- 结果内容 -->
<div
v-else-if="analysisResult"
class="prose prose-sm dark:prose-invert max-w-none p-4 bg-gray-50 dark:bg-gray-800 rounded-lg max-h-80 overflow-y-auto"
v-html="md.render(analysisResult)"
/>
</div>
</div>
</UCard>
</template>
</UModal>
</template>

View File

@@ -55,8 +55,6 @@ const props = defineProps<{
} | null
isLoading: boolean
isLoadingMore?: boolean
estimatedTokens: number
tokenStatus: 'green' | 'yellow' | 'red'
}>()
// Emits
@@ -101,25 +99,6 @@ const blockVirtualizer = useVirtualizer(
const virtualBlocks = computed(() => blockVirtualizer.value.getVirtualItems())
// Token 进度条颜色
const tokenProgressColor = computed(() => {
switch (props.tokenStatus) {
case 'green':
return 'bg-green-500'
case 'yellow':
return 'bg-yellow-500'
case 'red':
return 'bg-red-500'
default:
return 'bg-gray-400'
}
})
// Token 进度百分比(最大 100000
const tokenProgressPercent = computed(() => {
return Math.min((props.estimatedTokens / 100000) * 100, 100)
})
// 当前块的消息列表(使用反转后的索引)
const currentBlockMessages = computed<ChatRecordMessage[]>(() => {
if (blockCount.value === 0) return []
@@ -299,39 +278,6 @@ function handleBlockListScroll(event: Event) {
</span>
</div>
</div>
<!-- Token 预估进度条 -->
<div class="flex items-center gap-3">
<span class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap">
{{ t('analysis.filter.stats.tokens') }}:
<span
class="font-medium"
:class="{
'text-green-600': tokenStatus === 'green',
'text-yellow-600': tokenStatus === 'yellow',
'text-red-600': tokenStatus === 'red',
}"
>
~{{ estimatedTokens.toLocaleString() }}
</span>
</span>
<div class="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-300"
:class="tokenProgressColor"
:style="{ width: `${tokenProgressPercent}%` }"
/>
</div>
<span class="text-xs text-gray-500 whitespace-nowrap">10K</span>
</div>
<!-- Token 状态提示 -->
<div v-if="tokenStatus === 'yellow'" class="mt-2 text-xs text-yellow-600 dark:text-yellow-400">
{{ t('analysis.filter.tokenWarning.yellow') }}
</div>
<div v-if="tokenStatus === 'red'" class="mt-2 text-xs text-red-600 dark:text-red-400">
{{ t('analysis.filter.tokenWarning.red') }}
</div>
</div>
<!-- 主内容区左右结构 -->

View File

@@ -3,7 +3,7 @@
"chatExplorer": "Chat Explorer",
"sqlLab": "SQL Lab",
"filterAnalysis": "Filter Analysis",
"filterAnalysisDesc": "Advanced filtering feature is planned. You can filter by person, time, or search content before AI analysis",
"filterAnalysisDesc": "Filter chat records by person, time, or search content, and export filter results",
"featureInDev": "{name} is in development",
"comingSoon": "Coming soon...",
"followNotice": "Follow me on X for updates"

View File

@@ -111,7 +111,6 @@
"history": "History",
"execute": "Execute Filter",
"export": "Export Feed Pack",
"localAnalysis": "AI Analysis",
"keywords": "Keywords",
"keywordsHint": "Multiple keywords supported, OR logic",
"keywordPlaceholder": "Enter keyword, press Enter to add",
@@ -141,27 +140,12 @@
"blocks": "Blocks",
"messages": "Messages",
"hits": "Hits",
"chars": "Characters",
"tokens": "Est. Tokens"
},
"tokenWarning": {
"yellow": "Token count is high, may affect AI analysis",
"red": "Token count too high, consider narrowing filter"
"chars": "Characters"
},
"historyTitle": "Filter History",
"noHistory": "No history records",
"localAnalysisTitle": "AI Analysis",
"presetAnalysis": "Preset Analysis",
"customAnalysis": "Custom Question",
"customPromptPlaceholder": "Enter your question...",
"editablePromptLabel": "Prompt (editable)",
"analyzing": "Analyzing...",
"startAnalysis": "Start Analysis",
"analysisResult": "Analysis Result",
"loadMore": "Load More",
"allLoaded": "All loaded",
"dataTooLarge": "Data too large for AI analysis. Please narrow your filter.",
"dataTooLargeThreshold": "Current data exceeds {count} messages",
"exportSuccess": "Export successful",
"exportFailed": "Export failed",
"exportPreparing": "Preparing export..."

View File

@@ -3,7 +3,7 @@
"chatExplorer": "对话式探索",
"sqlLab": "SQL实验室",
"filterAnalysis": "自定义筛选",
"filterAnalysisDesc": "计划实现高级筛选功能,可以先按人/按时间/按搜索内容手动筛选然后再进行AI分析",
"filterAnalysisDesc": "支持按人/按时间/按搜索内容手动筛选聊天记录,并导出筛选结果",
"featureInDev": "{name}功能开发中",
"comingSoon": "敬请期待...",
"followNotice": "功能上线通知,欢迎关注我的小红书"

View File

@@ -111,7 +111,6 @@
"history": "历史记录",
"execute": "开始筛选",
"export": "导出筛选结果",
"localAnalysis": "AI 分析",
"keywords": "关键词",
"keywordsHint": "支持多个关键词OR 逻辑匹配",
"keywordPlaceholder": "输入关键词,回车添加",
@@ -141,27 +140,12 @@
"blocks": "对话块",
"messages": "消息数",
"hits": "命中数",
"chars": "字符数",
"tokens": "预估 Token"
},
"tokenWarning": {
"yellow": "Token 数量较多,可能影响 AI 分析效果",
"red": "Token 数量过多,建议缩小筛选范围"
"chars": "字符数"
},
"historyTitle": "筛选历史",
"noHistory": "暂无历史记录",
"localAnalysisTitle": "AI 分析",
"presetAnalysis": "预设分析",
"customAnalysis": "自定义提问",
"customPromptPlaceholder": "输入你想问的问题...",
"editablePromptLabel": "提示词(可临时修改)",
"analyzing": "分析中...",
"startAnalysis": "开始分析",
"analysisResult": "分析结果",
"loadMore": "加载更多",
"allLoaded": "已加载全部",
"dataTooLarge": "数据量过大,无法进行 AI 分析。请缩小筛选范围。",
"dataTooLargeThreshold": "当前数据超过 {count} 条消息",
"exportSuccess": "导出成功",
"exportFailed": "导出失败",
"exportPreparing": "正在准备导出..."