mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-26 14:27:22 +08:00
fix: 修复windows用户自定义筛选时消息量过大导致软件崩溃的BUG
This commit is contained in:
@@ -149,7 +149,7 @@ export function registerMessagesHandlers({ win }: IpcContext): void {
|
|||||||
// ==================== 自定义筛选 ====================
|
// ==================== 自定义筛选 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按条件筛选消息并扩充上下文
|
* 按条件筛选消息并扩充上下文(支持分页)
|
||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'ai:filterMessagesWithContext',
|
'ai:filterMessagesWithContext',
|
||||||
@@ -159,26 +159,78 @@ export function registerMessagesHandlers({ win }: IpcContext): void {
|
|||||||
keywords?: string[],
|
keywords?: string[],
|
||||||
timeFilter?: { startTs: number; endTs: number },
|
timeFilter?: { startTs: number; endTs: number },
|
||||||
senderIds?: number[],
|
senderIds?: number[],
|
||||||
contextSize?: number
|
contextSize?: number,
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await worker.filterMessagesWithContext(sessionId, keywords, timeFilter, senderIds, contextSize)
|
return await worker.filterMessagesWithContext(
|
||||||
|
sessionId,
|
||||||
|
keywords,
|
||||||
|
timeFilter,
|
||||||
|
senderIds,
|
||||||
|
contextSize,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('筛选消息失败:', error)
|
console.error('筛选消息失败:', error)
|
||||||
return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 } }
|
return {
|
||||||
|
blocks: [],
|
||||||
|
stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 },
|
||||||
|
pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多个会话的完整消息
|
* 获取多个会话的完整消息(支持分页)
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('ai:getMultipleSessionsMessages', async (_, sessionId: string, chatSessionIds: number[]) => {
|
ipcMain.handle(
|
||||||
try {
|
'ai:getMultipleSessionsMessages',
|
||||||
return await worker.getMultipleSessionsMessages(sessionId, chatSessionIds)
|
async (_, sessionId: string, chatSessionIds: number[], page?: number, pageSize?: number) => {
|
||||||
} catch (error) {
|
try {
|
||||||
console.error('获取多个会话消息失败:', error)
|
return await worker.getMultipleSessionsMessages(sessionId, chatSessionIds, page, pageSize)
|
||||||
return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 } }
|
} catch (error) {
|
||||||
|
console.error('获取多个会话消息失败:', error)
|
||||||
|
return {
|
||||||
|
blocks: [],
|
||||||
|
stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 },
|
||||||
|
pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false },
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出筛选结果到文件(后端生成,支持进度)
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'ai:exportFilterResultToFile',
|
||||||
|
async (
|
||||||
|
_,
|
||||||
|
params: {
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
outputDir: string
|
||||||
|
filterMode: 'condition' | 'session'
|
||||||
|
keywords?: string[]
|
||||||
|
timeFilter?: { startTs: number; endTs: number }
|
||||||
|
senderIds?: number[]
|
||||||
|
contextSize?: number
|
||||||
|
chatSessionIds?: number[]
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await worker.exportFilterResultToFile(params, (progress) => {
|
||||||
|
// 发送进度到渲染进程
|
||||||
|
win.webContents.send('ai:exportProgress', progress)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出筛选结果失败:', error)
|
||||||
|
return { success: false, error: String(error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
// 自定义筛选
|
// 自定义筛选
|
||||||
filterMessagesWithContext,
|
filterMessagesWithContext,
|
||||||
getMultipleSessionsMessages,
|
getMultipleSessionsMessages,
|
||||||
|
exportFilterResultToFile,
|
||||||
// NLP 查询
|
// NLP 查询
|
||||||
getWordFrequency,
|
getWordFrequency,
|
||||||
segmentText,
|
segmentText,
|
||||||
@@ -148,10 +149,11 @@ const syncHandlers: Record<string, (payload: any) => any> = {
|
|||||||
searchSessions: (p) => searchSessions(p.sessionId, p.keywords, p.timeFilter, p.limit, p.previewCount),
|
searchSessions: (p) => searchSessions(p.sessionId, p.keywords, p.timeFilter, p.limit, p.previewCount),
|
||||||
getSessionMessages: (p) => getSessionMessages(p.sessionId, p.chatSessionId, p.limit),
|
getSessionMessages: (p) => getSessionMessages(p.sessionId, p.chatSessionId, p.limit),
|
||||||
|
|
||||||
// 自定义筛选
|
// 自定义筛选(支持分页)
|
||||||
filterMessagesWithContext: (p) =>
|
filterMessagesWithContext: (p) =>
|
||||||
filterMessagesWithContext(p.sessionId, p.keywords, p.timeFilter, p.senderIds, p.contextSize),
|
filterMessagesWithContext(p.sessionId, p.keywords, p.timeFilter, p.senderIds, p.contextSize, p.page, p.pageSize),
|
||||||
getMultipleSessionsMessages: (p) => getMultipleSessionsMessages(p.sessionId, p.chatSessionIds),
|
getMultipleSessionsMessages: (p) =>
|
||||||
|
getMultipleSessionsMessages(p.sessionId, p.chatSessionIds, p.page, p.pageSize),
|
||||||
|
|
||||||
// NLP 查询
|
// NLP 查询
|
||||||
getWordFrequency: (p) => getWordFrequency(p),
|
getWordFrequency: (p) => getWordFrequency(p),
|
||||||
@@ -168,6 +170,8 @@ const asyncHandlers: Record<string, (payload: any, requestId: string) => Promise
|
|||||||
// 增量导入
|
// 增量导入
|
||||||
analyzeIncrementalImport: (p, id) => analyzeIncrementalImport(p.sessionId, p.filePath, id),
|
analyzeIncrementalImport: (p, id) => analyzeIncrementalImport(p.sessionId, p.filePath, id),
|
||||||
incrementalImport: (p, id) => incrementalImport(p.sessionId, p.filePath, id),
|
incrementalImport: (p, id) => incrementalImport(p.sessionId, p.filePath, id),
|
||||||
|
// 导出筛选结果到文件(支持进度报告)
|
||||||
|
exportFilterResultToFile: async (p, id) => exportFilterResultToFile(p, id),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function initWorker(): void {
|
|||||||
/**
|
/**
|
||||||
* 发送消息到 Worker 并等待响应
|
* 发送消息到 Worker 并等待响应
|
||||||
*/
|
*/
|
||||||
function sendToWorker<T>(type: string, payload: any): Promise<T> {
|
function sendToWorker<T>(type: string, payload: any, timeoutMs: number = 30000): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!worker) {
|
if (!worker) {
|
||||||
try {
|
try {
|
||||||
@@ -139,13 +139,13 @@ function sendToWorker<T>(type: string, payload: any): Promise<T> {
|
|||||||
|
|
||||||
worker!.postMessage({ id, type, payload })
|
worker!.postMessage({ id, type, payload })
|
||||||
|
|
||||||
// 设置超时(30秒)
|
// 设置超时
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (pendingRequests.has(id)) {
|
if (pendingRequests.has(id)) {
|
||||||
pendingRequests.delete(id)
|
pendingRequests.delete(id)
|
||||||
reject(new Error(`Worker request timeout: ${type}`))
|
reject(new Error(`Worker request timeout: ${type}`))
|
||||||
}
|
}
|
||||||
}, 30000)
|
}, timeoutMs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,23 +810,93 @@ export interface FilterResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按条件筛选消息并扩充上下文
|
* 分页信息类型
|
||||||
|
*/
|
||||||
|
export interface PaginationInfo {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带分页的筛选结果类型
|
||||||
|
*/
|
||||||
|
export interface FilterResultWithPagination extends FilterResult {
|
||||||
|
pagination: PaginationInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按条件筛选消息并扩充上下文(支持分页)
|
||||||
*/
|
*/
|
||||||
export async function filterMessagesWithContext(
|
export async function filterMessagesWithContext(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
keywords?: string[],
|
keywords?: string[],
|
||||||
timeFilter?: { startTs: number; endTs: number },
|
timeFilter?: { startTs: number; endTs: number },
|
||||||
senderIds?: number[],
|
senderIds?: number[],
|
||||||
contextSize?: number
|
contextSize?: number,
|
||||||
): Promise<FilterResult> {
|
page?: number,
|
||||||
return sendToWorker('filterMessagesWithContext', { sessionId, keywords, timeFilter, senderIds, contextSize })
|
pageSize?: number
|
||||||
|
): Promise<FilterResultWithPagination> {
|
||||||
|
return sendToWorker('filterMessagesWithContext', {
|
||||||
|
sessionId,
|
||||||
|
keywords,
|
||||||
|
timeFilter,
|
||||||
|
senderIds,
|
||||||
|
contextSize,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多个会话的完整消息
|
* 获取多个会话的完整消息(支持分页)
|
||||||
*/
|
*/
|
||||||
export async function getMultipleSessionsMessages(sessionId: string, chatSessionIds: number[]): Promise<FilterResult> {
|
export async function getMultipleSessionsMessages(
|
||||||
return sendToWorker('getMultipleSessionsMessages', { sessionId, chatSessionIds })
|
sessionId: string,
|
||||||
|
chatSessionIds: number[],
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
|
): Promise<FilterResultWithPagination> {
|
||||||
|
return sendToWorker('getMultipleSessionsMessages', { sessionId, chatSessionIds, page, pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出筛选结果参数
|
||||||
|
*/
|
||||||
|
export interface ExportFilterParams {
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
outputDir: string
|
||||||
|
filterMode: 'condition' | 'session'
|
||||||
|
keywords?: string[]
|
||||||
|
timeFilter?: { startTs: number; endTs: number }
|
||||||
|
senderIds?: number[]
|
||||||
|
contextSize?: number
|
||||||
|
chatSessionIds?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出进度回调类型
|
||||||
|
*/
|
||||||
|
export interface ExportProgress {
|
||||||
|
stage: 'preparing' | 'exporting' | 'done' | 'error'
|
||||||
|
currentBlock: number
|
||||||
|
totalBlocks: number
|
||||||
|
percentage: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出筛选结果到文件(后端生成)
|
||||||
|
* 使用 10 分钟超时,支持大数据量导出和进度回调
|
||||||
|
*/
|
||||||
|
export async function exportFilterResultToFile(
|
||||||
|
params: ExportFilterParams,
|
||||||
|
onProgress?: (progress: ExportProgress) => void
|
||||||
|
): Promise<{ success: boolean; filePath?: string; error?: string }> {
|
||||||
|
return sendToWorkerWithProgress('exportFilterResultToFile', params, onProgress as any, 600000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 增量导入 ====================
|
// ==================== 增量导入 ====================
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* AI 相关 API - AI 对话、LLM 服务、Agent、Embedding
|
* AI 相关 API - AI 对话、LLM 服务、Agent、Embedding
|
||||||
*/
|
*/
|
||||||
import { ipcRenderer } from 'electron'
|
import { ipcRenderer } from 'electron'
|
||||||
|
import type { ExportProgress } from '../../../src/types/base'
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
@@ -237,17 +238,26 @@ export const aiApi = {
|
|||||||
return ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords)
|
return ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords)
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== 自定义筛选 ====================
|
// ==================== 自定义筛选(支持分页) ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按条件筛选消息并扩充上下文
|
* 筛选结果消息类型
|
||||||
|
*/
|
||||||
|
// FilterMessage 和 FilterResult 类型定义在下方
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按条件筛选消息并扩充上下文(支持分页)
|
||||||
|
* @param page 页码(从 1 开始,默认 1)
|
||||||
|
* @param pageSize 每页块数(默认 50)
|
||||||
*/
|
*/
|
||||||
filterMessagesWithContext: (
|
filterMessagesWithContext: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
keywords?: string[],
|
keywords?: string[],
|
||||||
timeFilter?: { startTs: number; endTs: number },
|
timeFilter?: { startTs: number; endTs: number },
|
||||||
senderIds?: number[],
|
senderIds?: number[],
|
||||||
contextSize?: number
|
contextSize?: number,
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
): Promise<{
|
): Promise<{
|
||||||
blocks: Array<{
|
blocks: Array<{
|
||||||
startTs: number
|
startTs: number
|
||||||
@@ -273,16 +283,36 @@ export const aiApi = {
|
|||||||
hitMessages: number
|
hitMessages: number
|
||||||
totalChars: number
|
totalChars: number
|
||||||
}
|
}
|
||||||
|
pagination: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
}> => {
|
}> => {
|
||||||
return ipcRenderer.invoke('ai:filterMessagesWithContext', sessionId, keywords, timeFilter, senderIds, contextSize)
|
return ipcRenderer.invoke(
|
||||||
|
'ai:filterMessagesWithContext',
|
||||||
|
sessionId,
|
||||||
|
keywords,
|
||||||
|
timeFilter,
|
||||||
|
senderIds,
|
||||||
|
contextSize,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多个会话的完整消息
|
* 获取多个会话的完整消息(支持分页)
|
||||||
|
* @param page 页码(从 1 开始,默认 1)
|
||||||
|
* @param pageSize 每页块数(默认 50)
|
||||||
*/
|
*/
|
||||||
getMultipleSessionsMessages: (
|
getMultipleSessionsMessages: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
chatSessionIds: number[]
|
chatSessionIds: number[],
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
): Promise<{
|
): Promise<{
|
||||||
blocks: Array<{
|
blocks: Array<{
|
||||||
startTs: number
|
startTs: number
|
||||||
@@ -308,8 +338,45 @@ export const aiApi = {
|
|||||||
hitMessages: number
|
hitMessages: number
|
||||||
totalChars: number
|
totalChars: number
|
||||||
}
|
}
|
||||||
|
pagination: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
}> => {
|
}> => {
|
||||||
return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds)
|
return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds, page, pageSize)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出筛选结果到文件(后端生成,支持大数据量)
|
||||||
|
*/
|
||||||
|
exportFilterResultToFile: (params: {
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
outputDir: string
|
||||||
|
filterMode: 'condition' | 'session'
|
||||||
|
keywords?: string[]
|
||||||
|
timeFilter?: { startTs: number; endTs: number }
|
||||||
|
senderIds?: number[]
|
||||||
|
contextSize?: number
|
||||||
|
chatSessionIds?: number[]
|
||||||
|
}): Promise<{ success: boolean; filePath?: string; error?: string }> => {
|
||||||
|
return ipcRenderer.invoke('ai:exportFilterResultToFile', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听导出进度
|
||||||
|
*/
|
||||||
|
onExportProgress: (callback: (progress: ExportProgress) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, progress: ExportProgress) => {
|
||||||
|
callback(progress)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('ai:exportProgress', handler)
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('ai:exportProgress', handler)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
43
electron/preload/index.d.ts
vendored
43
electron/preload/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
import type { AnalysisSession, MessageType, ImportProgress } from '../../src/types/base'
|
import type { AnalysisSession, MessageType, ImportProgress, ExportProgress } from '../../src/types/base'
|
||||||
import type {
|
import type {
|
||||||
MemberActivity,
|
MemberActivity,
|
||||||
MemberNameHistory,
|
MemberNameHistory,
|
||||||
@@ -252,6 +252,20 @@ interface FilterResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分页信息类型
|
||||||
|
interface PaginationInfo {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带分页的筛选结果类型
|
||||||
|
interface FilterResultWithPagination extends FilterResult {
|
||||||
|
pagination: PaginationInfo
|
||||||
|
}
|
||||||
|
|
||||||
interface AIConversation {
|
interface AIConversation {
|
||||||
id: string
|
id: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -348,15 +362,36 @@ interface AiApi {
|
|||||||
getMessages: (conversationId: string) => Promise<AIMessage[]>
|
getMessages: (conversationId: string) => Promise<AIMessage[]>
|
||||||
deleteMessage: (messageId: string) => Promise<boolean>
|
deleteMessage: (messageId: string) => Promise<boolean>
|
||||||
showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }>
|
showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||||
// 自定义筛选
|
// 自定义筛选(支持分页)
|
||||||
filterMessagesWithContext: (
|
filterMessagesWithContext: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
keywords?: string[],
|
keywords?: string[],
|
||||||
timeFilter?: TimeFilter,
|
timeFilter?: TimeFilter,
|
||||||
senderIds?: number[],
|
senderIds?: number[],
|
||||||
|
contextSize?: number,
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
|
) => Promise<FilterResultWithPagination>
|
||||||
|
getMultipleSessionsMessages: (
|
||||||
|
sessionId: string,
|
||||||
|
chatSessionIds: number[],
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
|
) => Promise<FilterResultWithPagination>
|
||||||
|
// 导出筛选结果到文件
|
||||||
|
exportFilterResultToFile: (params: {
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
outputDir: string
|
||||||
|
filterMode: 'condition' | 'session'
|
||||||
|
keywords?: string[]
|
||||||
|
timeFilter?: TimeFilter
|
||||||
|
senderIds?: number[]
|
||||||
contextSize?: number
|
contextSize?: number
|
||||||
) => Promise<FilterResult>
|
chatSessionIds?: number[]
|
||||||
getMultipleSessionsMessages: (sessionId: string, chatSessionIds: number[]) => Promise<FilterResult>
|
}) => Promise<{ success: boolean; filePath?: string; error?: string }>
|
||||||
|
// 监听导出进度
|
||||||
|
onExportProgress: (callback: (progress: ExportProgress) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM 相关类型
|
// LLM 相关类型
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
* 支持两种互斥的筛选模式:
|
* 支持两种互斥的筛选模式:
|
||||||
* 1. 条件筛选:按关键词、时间、发送者筛选,并自动扩展上下文
|
* 1. 条件筛选:按关键词、时间、发送者筛选,并自动扩展上下文
|
||||||
* 2. 会话筛选:直接选择已有的会话(对话段落)
|
* 2. 会话筛选:直接选择已有的会话(对话段落)
|
||||||
|
*
|
||||||
|
* 支持分页加载,避免大数据量时内存溢出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref, computed, watch, toRaw } from 'vue'
|
import { ref, computed, watch, toRaw } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import ConditionPanel from './ConditionPanel.vue'
|
import ConditionPanel from './ConditionPanel.vue'
|
||||||
import SessionPanel from './SessionPanel.vue'
|
import SessionPanel from './SessionPanel.vue'
|
||||||
@@ -18,6 +21,7 @@ import FilterHistory from './FilterHistory.vue'
|
|||||||
import LocalAnalysisModal from './LocalAnalysisModal.vue'
|
import LocalAnalysisModal from './LocalAnalysisModal.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
const sessionStore = useSessionStore()
|
const sessionStore = useSessionStore()
|
||||||
|
|
||||||
// 筛选模式:'condition' | 'session'
|
// 筛选模式:'condition' | 'session'
|
||||||
@@ -55,7 +59,16 @@ interface FilterMessage {
|
|||||||
isHit: boolean
|
isHit: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 筛选结果
|
// 分页信息类型
|
||||||
|
interface PaginationInfo {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选结果(带分页)
|
||||||
const filterResult = ref<{
|
const filterResult = ref<{
|
||||||
blocks: Array<{
|
blocks: Array<{
|
||||||
startTs: number
|
startTs: number
|
||||||
@@ -68,13 +81,18 @@ const filterResult = ref<{
|
|||||||
hitMessages: number
|
hitMessages: number
|
||||||
totalChars: number
|
totalChars: number
|
||||||
}
|
}
|
||||||
|
pagination: PaginationInfo
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const isFiltering = ref(false)
|
const isFiltering = ref(false)
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
const showHistory = ref(false)
|
const showHistory = ref(false)
|
||||||
const showAnalysisModal = ref(false)
|
const showAnalysisModal = ref(false)
|
||||||
|
|
||||||
|
// 每页块数
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
// 估算 Token 数
|
// 估算 Token 数
|
||||||
// 中文:1 字符 ≈ 1.5 token(因为中文分词后每个字符可能产生 1-2 个 token)
|
// 中文:1 字符 ≈ 1.5 token(因为中文分词后每个字符可能产生 1-2 个 token)
|
||||||
// 考虑到消息格式(时间、发送人等),使用 1.5 作为估算系数
|
// 考虑到消息格式(时间、发送人等),使用 1.5 作为估算系数
|
||||||
@@ -109,7 +127,7 @@ const canExecuteFilter = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 执行筛选
|
// 执行筛选(首次加载)
|
||||||
async function executeFilter() {
|
async function executeFilter() {
|
||||||
const sessionId = sessionStore.currentSessionId
|
const sessionId = sessionStore.currentSessionId
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
@@ -133,14 +151,16 @@ async function executeFilter() {
|
|||||||
keywords,
|
keywords,
|
||||||
timeFilter,
|
timeFilter,
|
||||||
senderIds,
|
senderIds,
|
||||||
contextSize
|
contextSize,
|
||||||
|
1, // 第一页
|
||||||
|
PAGE_SIZE
|
||||||
)
|
)
|
||||||
filterResult.value = result
|
filterResult.value = result
|
||||||
} else {
|
} else {
|
||||||
// 会话筛选模式
|
// 会话筛选模式
|
||||||
if (selectedSessionIds.value.length === 0) return
|
if (selectedSessionIds.value.length === 0) return
|
||||||
const sessionIds = [...toRaw(selectedSessionIds.value)]
|
const sessionIds = [...toRaw(selectedSessionIds.value)]
|
||||||
const result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds)
|
const result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds, 1, PAGE_SIZE)
|
||||||
filterResult.value = result
|
filterResult.value = result
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -150,69 +170,159 @@ async function executeFilter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出投喂包(Markdown 格式)
|
// 加载更多块
|
||||||
|
async function loadMoreBlocks() {
|
||||||
|
const sessionId = sessionStore.currentSessionId
|
||||||
|
if (!sessionId || !filterResult.value || !filterResult.value.pagination.hasMore || isLoadingMore.value) return
|
||||||
|
|
||||||
|
isLoadingMore.value = true
|
||||||
|
const nextPage = filterResult.value.pagination.page + 1
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
if (filterMode.value === 'condition') {
|
||||||
|
const rawFilter = toRaw(conditionFilter.value)
|
||||||
|
const keywords = rawFilter.keywords.length > 0 ? [...rawFilter.keywords] : undefined
|
||||||
|
const timeFilter = rawFilter.timeRange
|
||||||
|
? { startTs: rawFilter.timeRange.start, endTs: rawFilter.timeRange.end }
|
||||||
|
: undefined
|
||||||
|
const senderIds = rawFilter.senderIds.length > 0 ? [...rawFilter.senderIds] : undefined
|
||||||
|
const contextSize = rawFilter.contextSize
|
||||||
|
|
||||||
|
result = await window.aiApi.filterMessagesWithContext(
|
||||||
|
sessionId,
|
||||||
|
keywords,
|
||||||
|
timeFilter,
|
||||||
|
senderIds,
|
||||||
|
contextSize,
|
||||||
|
nextPage,
|
||||||
|
PAGE_SIZE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const sessionIds = [...toRaw(selectedSessionIds.value)]
|
||||||
|
result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds, nextPage, PAGE_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并新加载的块到现有结果
|
||||||
|
if (result && result.blocks.length > 0) {
|
||||||
|
filterResult.value = {
|
||||||
|
blocks: [...filterResult.value.blocks, ...result.blocks],
|
||||||
|
stats: filterResult.value.stats, // 统计信息保持不变(第一页已获取)
|
||||||
|
pagination: result.pagination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载更多失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出状态
|
||||||
|
const isExporting = ref(false)
|
||||||
|
const exportProgress = ref<{
|
||||||
|
percentage: number
|
||||||
|
message: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
// 监听导出进度
|
||||||
|
let unsubscribeExportProgress: (() => void) | null = null
|
||||||
|
|
||||||
|
function startExportProgressListener() {
|
||||||
|
unsubscribeExportProgress = window.aiApi.onExportProgress((progress) => {
|
||||||
|
exportProgress.value = {
|
||||||
|
percentage: progress.percentage,
|
||||||
|
message: progress.message,
|
||||||
|
}
|
||||||
|
// 如果完成或出错,不再需要监听
|
||||||
|
if (progress.stage === 'done' || progress.stage === 'error') {
|
||||||
|
exportProgress.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopExportProgressListener() {
|
||||||
|
if (unsubscribeExportProgress) {
|
||||||
|
unsubscribeExportProgress()
|
||||||
|
unsubscribeExportProgress = null
|
||||||
|
}
|
||||||
|
exportProgress.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出投喂包(后端生成 Markdown 文件,支持大数据量)
|
||||||
async function exportFeedPack() {
|
async function exportFeedPack() {
|
||||||
if (!filterResult.value || filterResult.value.blocks.length === 0) return
|
if (!filterResult.value || filterResult.value.blocks.length === 0) return
|
||||||
|
|
||||||
|
const sessionId = sessionStore.currentSessionId
|
||||||
|
if (!sessionId) return
|
||||||
|
|
||||||
const sessionInfo = sessionStore.currentSession
|
const sessionInfo = sessionStore.currentSession
|
||||||
const sessionName = sessionInfo?.name || '未知会话'
|
const sessionName = sessionInfo?.name || '未知会话'
|
||||||
|
|
||||||
// 构建 Markdown 内容
|
// 让用户选择保存目录
|
||||||
let markdown = `# ${sessionName} - 聊天记录筛选结果\n\n`
|
const dialogResult = await window.api.dialog.showOpenDialog({
|
||||||
markdown += `> 导出时间: ${new Date().toLocaleString()}\n\n`
|
title: '选择保存目录',
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
})
|
||||||
|
if (dialogResult.canceled || !dialogResult.filePaths[0]) return
|
||||||
|
const outputDir = dialogResult.filePaths[0]
|
||||||
|
|
||||||
// 筛选条件摘要
|
isExporting.value = true
|
||||||
markdown += `## 筛选条件\n\n`
|
exportProgress.value = { percentage: 0, message: t('analysis.filter.exportPreparing') }
|
||||||
if (filterMode.value === 'condition') {
|
|
||||||
if (conditionFilter.value.keywords.length > 0) {
|
// 开始监听进度
|
||||||
markdown += `- 关键词: ${conditionFilter.value.keywords.join(', ')}\n`
|
startExportProgressListener()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建导出参数
|
||||||
|
const rawFilter = toRaw(conditionFilter.value)
|
||||||
|
const exportParams = {
|
||||||
|
sessionId,
|
||||||
|
sessionName,
|
||||||
|
outputDir,
|
||||||
|
filterMode: filterMode.value,
|
||||||
|
keywords: rawFilter.keywords.length > 0 ? [...rawFilter.keywords] : undefined,
|
||||||
|
timeFilter: rawFilter.timeRange
|
||||||
|
? { startTs: rawFilter.timeRange.start, endTs: rawFilter.timeRange.end }
|
||||||
|
: undefined,
|
||||||
|
senderIds: rawFilter.senderIds.length > 0 ? [...rawFilter.senderIds] : undefined,
|
||||||
|
contextSize: rawFilter.contextSize,
|
||||||
|
chatSessionIds:
|
||||||
|
filterMode.value === 'session' ? [...toRaw(selectedSessionIds.value)] : undefined,
|
||||||
}
|
}
|
||||||
if (conditionFilter.value.timeRange) {
|
|
||||||
const start = new Date(conditionFilter.value.timeRange.start * 1000).toLocaleString()
|
// 调用后端导出
|
||||||
const end = new Date(conditionFilter.value.timeRange.end * 1000).toLocaleString()
|
const exportResult = await window.aiApi.exportFilterResultToFile(exportParams)
|
||||||
markdown += `- 时间范围: ${start} ~ ${end}\n`
|
|
||||||
|
if (exportResult.success && exportResult.filePath) {
|
||||||
|
// 导出成功
|
||||||
|
toast.add({
|
||||||
|
title: t('analysis.filter.exportSuccess'),
|
||||||
|
description: exportResult.filePath,
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-heroicons-check-circle',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 导出失败
|
||||||
|
toast.add({
|
||||||
|
title: t('analysis.filter.exportFailed'),
|
||||||
|
description: exportResult.error || t('common.error.unknown'),
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-heroicons-x-circle',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
markdown += `- 上下文扩展: ±${conditionFilter.value.contextSize} 条消息\n`
|
} catch (error) {
|
||||||
} else {
|
console.error('导出失败:', error)
|
||||||
markdown += `- 模式: 会话筛选\n`
|
toast.add({
|
||||||
markdown += `- 选中会话数: ${selectedSessionIds.value.length}\n`
|
title: t('analysis.filter.exportFailed'),
|
||||||
|
description: String(error),
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-heroicons-x-circle',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
stopExportProgressListener()
|
||||||
|
isExporting.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计信息
|
|
||||||
markdown += `\n## 统计信息\n\n`
|
|
||||||
markdown += `- 消息总数: ${filterResult.value.stats.totalMessages}\n`
|
|
||||||
markdown += `- 命中消息: ${filterResult.value.stats.hitMessages}\n`
|
|
||||||
markdown += `- 字符数: ${filterResult.value.stats.totalChars}\n`
|
|
||||||
markdown += `- 预估 Token: ${estimatedTokens.value}\n`
|
|
||||||
|
|
||||||
// 对话内容
|
|
||||||
markdown += `\n## 对话内容\n\n`
|
|
||||||
|
|
||||||
for (let i = 0; i < filterResult.value.blocks.length; i++) {
|
|
||||||
const block = filterResult.value.blocks[i]
|
|
||||||
const startTime = new Date(block.startTs * 1000).toLocaleString()
|
|
||||||
const endTime = new Date(block.endTs * 1000).toLocaleString()
|
|
||||||
|
|
||||||
markdown += `### 对话块 ${i + 1} (${startTime} ~ ${endTime})\n\n`
|
|
||||||
|
|
||||||
for (const msg of block.messages) {
|
|
||||||
const time = new Date(msg.timestamp * 1000).toLocaleTimeString()
|
|
||||||
const hitMark = msg.isHit ? ' ⭐' : ''
|
|
||||||
const content = msg.content || '[非文本消息]'
|
|
||||||
markdown += `${time} ${msg.senderName}${hitMark}: ${content}\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建并下载文件
|
|
||||||
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = `${sessionName}_筛选结果_${Date.now()}.md`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开本地 AI 分析
|
// 打开本地 AI 分析
|
||||||
@@ -313,21 +423,45 @@ function loadHistoryCondition(condition: {
|
|||||||
<PreviewPanel
|
<PreviewPanel
|
||||||
:result="filterResult"
|
:result="filterResult"
|
||||||
:is-loading="isFiltering"
|
:is-loading="isFiltering"
|
||||||
|
:is-loading-more="isLoadingMore"
|
||||||
:estimated-tokens="estimatedTokens"
|
:estimated-tokens="estimatedTokens"
|
||||||
:token-status="tokenStatus"
|
:token-status="tokenStatus"
|
||||||
|
@load-more="loadMoreBlocks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 底部操作按钮 -->
|
<!-- 底部操作按钮 -->
|
||||||
<div
|
<div
|
||||||
v-if="filterResult && filterResult.blocks.length > 0"
|
v-if="filterResult && filterResult.blocks.length > 0"
|
||||||
class="flex-none flex items-center justify-end gap-3 px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
class="flex-none flex flex-col gap-2 px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<UButton variant="outline" icon="i-heroicons-document-arrow-down" @click="exportFeedPack">
|
<!-- 导出进度条 -->
|
||||||
{{ t('analysis.filter.export') }}
|
<div v-if="isExporting && exportProgress" class="w-full">
|
||||||
</UButton>
|
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
<UButton color="primary" icon="i-heroicons-sparkles" @click="openLocalAnalysis">
|
<span>{{ exportProgress.message }}</span>
|
||||||
{{ t('analysis.filter.localAnalysis') }}
|
<span>{{ exportProgress.percentage }}%</span>
|
||||||
</UButton>
|
</div>
|
||||||
|
<div class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary-500 transition-all duration-300"
|
||||||
|
:style="{ width: `${exportProgress.percentage}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center justify-end gap-3">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
icon="i-heroicons-document-arrow-down"
|
||||||
|
:loading="isExporting"
|
||||||
|
:disabled="isExporting"
|
||||||
|
@click="exportFeedPack"
|
||||||
|
>
|
||||||
|
{{ t('analysis.filter.export') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton color="primary" icon="i-heroicons-sparkles" @click="openLocalAnalysis">
|
||||||
|
{{ t('analysis.filter.localAnalysis') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false })
|
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'
|
// 分析模式:'preset' | 'custom'
|
||||||
const analysisMode = ref<'preset' | 'custom'>('preset')
|
const analysisMode = ref<'preset' | 'custom'>('preset')
|
||||||
|
|
||||||
@@ -224,6 +233,24 @@ watch(open, (val) => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg w-fit">
|
||||||
<button
|
<button
|
||||||
@@ -295,7 +322,7 @@ watch(open, (val) => {
|
|||||||
<UButton
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="isAnalyzing"
|
:loading="isAnalyzing"
|
||||||
:disabled="isAnalyzing || (analysisMode === 'custom' && !customPrompt.trim())"
|
:disabled="isAnalyzing || isDataTooLarge || (analysisMode === 'custom' && !customPrompt.trim())"
|
||||||
@click="executeAnalysis"
|
@click="executeAnalysis"
|
||||||
>
|
>
|
||||||
<UIcon name="i-heroicons-sparkles" class="w-4 h-4 mr-1" />
|
<UIcon name="i-heroicons-sparkles" class="w-4 h-4 mr-1" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* 预览面板
|
* 预览面板
|
||||||
* 左右结构:左侧对话块列表(虚拟滚动),右侧消息内容(复用 MessageList)
|
* 左右结构:左侧对话块列表(虚拟滚动),右侧消息内容(复用 MessageList)
|
||||||
* 支持连续滚动:滚动到底部时自动加载下一个对话块
|
* 支持连续滚动:滚动到底部时自动加载下一个对话块
|
||||||
|
* 支持分页加载:滚动到左侧列表底部时触发加载更多
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, ref, watch, nextTick } from 'vue'
|
import { computed, ref, watch, nextTick } from 'vue'
|
||||||
@@ -14,6 +15,15 @@ import type { ChatRecordMessage } from '@/types/format'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 分页信息类型
|
||||||
|
interface PaginationInfo {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalBlocks: number
|
||||||
|
totalHits: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
result: {
|
result: {
|
||||||
@@ -41,12 +51,19 @@ const props = defineProps<{
|
|||||||
hitMessages: number
|
hitMessages: number
|
||||||
totalChars: number
|
totalChars: number
|
||||||
}
|
}
|
||||||
|
pagination?: PaginationInfo
|
||||||
} | null
|
} | null
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
isLoadingMore?: boolean
|
||||||
estimatedTokens: number
|
estimatedTokens: number
|
||||||
tokenStatus: 'green' | 'yellow' | 'red'
|
tokenStatus: 'green' | 'yellow' | 'red'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'load-more'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
// 当前选中的对话块索引(用于左侧高亮和右侧显示)
|
// 当前选中的对话块索引(用于左侧高亮和右侧显示)
|
||||||
const selectedBlockIndex = ref(0)
|
const selectedBlockIndex = ref(0)
|
||||||
|
|
||||||
@@ -231,6 +248,19 @@ watch(pendingScrollToMessageId, async (messageId) => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 处理左侧块列表滚动,接近底部时自动加载更多
|
||||||
|
function handleBlockListScroll(event: Event) {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (!target || !props.result?.pagination?.hasMore || props.isLoadingMore) return
|
||||||
|
|
||||||
|
// 距离底部 100px 时触发加载
|
||||||
|
const threshold = 100
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
||||||
|
emit('load-more')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -244,7 +274,12 @@ watch(pendingScrollToMessageId, async (messageId) => {
|
|||||||
<div class="flex items-center gap-6 text-sm">
|
<div class="flex items-center gap-6 text-sm">
|
||||||
<span class="text-gray-600 dark:text-gray-400">
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
{{ t('analysis.filter.stats.blocks') }}:
|
{{ t('analysis.filter.stats.blocks') }}:
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{ result.blocks.length }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ result.blocks.length }}
|
||||||
|
<template v-if="result.pagination && result.pagination.totalBlocks > result.blocks.length">
|
||||||
|
/ {{ result.pagination.totalBlocks }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-600 dark:text-gray-400">
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
{{ t('analysis.filter.stats.messages') }}:
|
{{ t('analysis.filter.stats.messages') }}:
|
||||||
@@ -252,7 +287,9 @@ watch(pendingScrollToMessageId, async (messageId) => {
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-gray-600 dark:text-gray-400">
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
{{ t('analysis.filter.stats.hits') }}:
|
{{ t('analysis.filter.stats.hits') }}:
|
||||||
<span class="font-medium text-primary-500">{{ result.stats.hitMessages }}</span>
|
<span class="font-medium text-primary-500">
|
||||||
|
{{ result.pagination?.totalHits ?? result.stats.hitMessages }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-600 dark:text-gray-400">
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
{{ t('analysis.filter.stats.chars') }}:
|
{{ t('analysis.filter.stats.chars') }}:
|
||||||
@@ -326,11 +363,15 @@ watch(pendingScrollToMessageId, async (messageId) => {
|
|||||||
>
|
>
|
||||||
<div class="flex-none px-3 py-2 border-b border-gray-200 dark:border-gray-700">
|
<div class="flex-none px-3 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{{ t('analysis.filter.stats.blocks') }} ({{ result.blocks.length }})
|
{{ t('analysis.filter.stats.blocks') }}
|
||||||
|
({{ result.blocks.length
|
||||||
|
}}<template v-if="result.pagination && result.pagination.totalBlocks > result.blocks.length"
|
||||||
|
>/{{ result.pagination.totalBlocks }}</template
|
||||||
|
>)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="blockListRef" class="flex-1 overflow-y-auto">
|
<div ref="blockListRef" class="flex-1 overflow-y-auto" @scroll="handleBlockListScroll">
|
||||||
<div :style="{ height: `${blockVirtualizer.getTotalSize()}px`, position: 'relative' }">
|
<div :style="{ height: `${blockVirtualizer.getTotalSize()}px`, position: 'relative' }">
|
||||||
<div
|
<div
|
||||||
v-for="virtualItem in virtualBlocks"
|
v-for="virtualItem in virtualBlocks"
|
||||||
@@ -381,6 +422,28 @@ watch(pendingScrollToMessageId, async (messageId) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<div
|
||||||
|
v-if="result.pagination?.hasMore"
|
||||||
|
class="py-3 text-center text-sm text-gray-500 dark:text-gray-400 border-t border-gray-100 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<template v-if="isLoadingMore">
|
||||||
|
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 animate-spin inline mr-1" />
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="text-primary-500 hover:text-primary-600" @click="emit('load-more')">
|
||||||
|
{{ t('analysis.filter.loadMore') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="result.pagination && result.blocks.length >= result.pagination.totalBlocks"
|
||||||
|
class="py-3 text-center text-xs text-gray-400 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
{{ t('analysis.filter.allLoaded') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,13 @@
|
|||||||
"editablePromptLabel": "Prompt (editable)",
|
"editablePromptLabel": "Prompt (editable)",
|
||||||
"analyzing": "Analyzing...",
|
"analyzing": "Analyzing...",
|
||||||
"startAnalysis": "Start Analysis",
|
"startAnalysis": "Start Analysis",
|
||||||
"analysisResult": "Analysis Result"
|
"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..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"noData": "No data",
|
"noData": "No data",
|
||||||
"error": {
|
"error": {
|
||||||
"general": "Operation failed, please try again",
|
"general": "Operation failed, please try again",
|
||||||
"network": "Network error"
|
"network": "Network error",
|
||||||
|
"unknown": "Unknown error"
|
||||||
},
|
},
|
||||||
"attachment": {
|
"attachment": {
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
|
|||||||
@@ -99,6 +99,13 @@
|
|||||||
"editablePromptLabel": "提示词(可临时修改)",
|
"editablePromptLabel": "提示词(可临时修改)",
|
||||||
"analyzing": "分析中...",
|
"analyzing": "分析中...",
|
||||||
"startAnalysis": "开始分析",
|
"startAnalysis": "开始分析",
|
||||||
"analysisResult": "分析结果"
|
"analysisResult": "分析结果",
|
||||||
|
"loadMore": "加载更多",
|
||||||
|
"allLoaded": "已加载全部",
|
||||||
|
"dataTooLarge": "数据量过大,无法进行 AI 分析。请缩小筛选范围。",
|
||||||
|
"dataTooLargeThreshold": "当前数据超过 {count} 条消息",
|
||||||
|
"exportSuccess": "导出成功",
|
||||||
|
"exportFailed": "导出失败",
|
||||||
|
"exportPreparing": "正在准备导出..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"noData": "暂无数据",
|
"noData": "暂无数据",
|
||||||
"error": {
|
"error": {
|
||||||
"general": "操作失败,请重试",
|
"general": "操作失败,请重试",
|
||||||
"network": "网络错误"
|
"network": "网络错误",
|
||||||
|
"unknown": "未知错误"
|
||||||
},
|
},
|
||||||
"attachment": {
|
"attachment": {
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
|
|||||||
@@ -250,6 +250,17 @@ export interface ImportProgress {
|
|||||||
messagesProcessed?: number
|
messagesProcessed?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出进度
|
||||||
|
*/
|
||||||
|
export interface ExportProgress {
|
||||||
|
stage: 'preparing' | 'exporting' | 'done' | 'error'
|
||||||
|
currentBlock: number
|
||||||
|
totalBlocks: number
|
||||||
|
percentage: number // 0-100
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入结果
|
* 导入结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user