Merge pull request #998 from Jasonzhu1207/main
feat: Add manually trigger AI insights in conversations
This commit is contained in:
11 files changed
+211
-12
No files matched your search
@@ -1813,6 +1813,14 @@ function registerIpcHandlers() {
|
|||||||
return insightService.triggerTest()
|
return insightService.triggerTest()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('insight:triggerSessionInsight', async (_, payload: {
|
||||||
|
sessionId: string
|
||||||
|
displayName?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}) => {
|
||||||
|
return insightService.triggerSessionInsight(payload)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('insight:generateFootprintInsight', async (_, payload: {
|
ipcMain.handle('insight:generateFootprintInsight', async (_, payload: {
|
||||||
rangeLabel: string
|
rangeLabel: string
|
||||||
summary: {
|
summary: {
|
||||||
|
|||||||
@@ -583,6 +583,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
markRecordRead: (id: string) => ipcRenderer.invoke('insight:markRecordRead', id),
|
markRecordRead: (id: string) => ipcRenderer.invoke('insight:markRecordRead', id),
|
||||||
clearRecords: (filters?: any) => ipcRenderer.invoke('insight:clearRecords', filters),
|
clearRecords: (filters?: any) => ipcRenderer.invoke('insight:clearRecords', filters),
|
||||||
triggerTest: () => ipcRenderer.invoke('insight:triggerTest'),
|
triggerTest: () => ipcRenderer.invoke('insight:triggerTest'),
|
||||||
|
triggerSessionInsight: (payload: {
|
||||||
|
sessionId: string
|
||||||
|
displayName?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}) => ipcRenderer.invoke('insight:triggerSessionInsight', payload),
|
||||||
generateFootprintInsight: (payload: {
|
generateFootprintInsight: (payload: {
|
||||||
rangeLabel: string
|
rangeLabel: string
|
||||||
summary: {
|
summary: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from 'path'
|
|||||||
import { createHash, randomUUID } from 'crypto'
|
import { createHash, randomUUID } from 'crypto'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
|
|
||||||
export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'message_analysis'
|
export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'manual' | 'message_analysis'
|
||||||
export type InsightRecordSourceType = 'insight' | 'message_analysis'
|
export type InsightRecordSourceType = 'insight' | 'message_analysis'
|
||||||
|
|
||||||
export interface MessageInsightAnalysis {
|
export interface MessageInsightAnalysis {
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ interface SharedAiModelConfig {
|
|||||||
maxTokens: number
|
maxTokens: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionInsightTriggerResult {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
recordId?: string
|
||||||
|
insight?: string
|
||||||
|
skipped?: boolean
|
||||||
|
notificationEnabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type InsightFilterMode = 'whitelist' | 'blacklist'
|
type InsightFilterMode = 'whitelist' | 'blacklist'
|
||||||
|
|
||||||
class ApiRequestError extends Error {
|
class ApiRequestError extends Error {
|
||||||
@@ -537,11 +546,14 @@ class InsightService {
|
|||||||
const sessionId = session.username?.trim() || ''
|
const sessionId = session.username?.trim() || ''
|
||||||
const displayName = session.displayName || sessionId
|
const displayName = session.displayName || sessionId
|
||||||
insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`)
|
insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`)
|
||||||
await this.generateInsightForSession({
|
const result = await this.generateInsightForSession({
|
||||||
sessionId,
|
sessionId,
|
||||||
displayName,
|
displayName,
|
||||||
triggerReason: 'test'
|
triggerReason: 'test'
|
||||||
})
|
})
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, message: result.message }
|
||||||
|
}
|
||||||
const notificationEnabled = this.config.get('aiInsightNotificationEnabled') !== false
|
const notificationEnabled = this.config.get('aiInsightNotificationEnabled') !== false
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -554,6 +566,47 @@ class InsightService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动对指定会话立即触发一次 AI 见解。
|
||||||
|
* 只新增触发入口;实际上下文、朋友圈/微博拼接、prompt 和入库仍走 generateInsightForSession。
|
||||||
|
*/
|
||||||
|
async triggerSessionInsight(params: {
|
||||||
|
sessionId: string
|
||||||
|
displayName?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}): Promise<SessionInsightTriggerResult> {
|
||||||
|
const sessionId = String(params?.sessionId || '').trim()
|
||||||
|
if (!sessionId) {
|
||||||
|
return { success: false, message: '当前会话无效,无法触发 AI 见解' }
|
||||||
|
}
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
return { success: false, message: '请先在设置中开启「AI 见解」' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiBaseUrl, apiKey } = this.getSharedAiModelConfig()
|
||||||
|
if (!apiBaseUrl || !apiKey) {
|
||||||
|
return { success: false, message: '请先填写通用 AI 模型配置(API 地址和 Key)' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const connectResult = await chatService.connect()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { success: false, message: '数据库连接失败,请先在"数据库连接"页完成配置' }
|
||||||
|
}
|
||||||
|
this.dbConnected = true
|
||||||
|
|
||||||
|
const displayName = String(params?.displayName || sessionId).trim() || sessionId
|
||||||
|
insightLog('INFO', `手动触发当前会话见解:${displayName} (${sessionId})`)
|
||||||
|
return await this.generateInsightForSession({
|
||||||
|
sessionId,
|
||||||
|
displayName,
|
||||||
|
triggerReason: 'manual'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: `触发失败:${(error as Error).message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取今日触发统计(供设置页展示) */
|
/** 获取今日触发统计(供设置页展示) */
|
||||||
getTodayStats(): { sessionId: string; count: number; times: string[] }[] {
|
getTodayStats(): { sessionId: string; count: number; times: string[] }[] {
|
||||||
this.resetIfNewDay()
|
this.resetIfNewDay()
|
||||||
@@ -1372,10 +1425,10 @@ ${afterText}
|
|||||||
displayName: string
|
displayName: string
|
||||||
triggerReason: InsightRecordTriggerReason
|
triggerReason: InsightRecordTriggerReason
|
||||||
silentDays?: number
|
silentDays?: number
|
||||||
}): Promise<void> {
|
}): Promise<SessionInsightTriggerResult> {
|
||||||
const { sessionId, displayName, triggerReason, silentDays } = params
|
const { sessionId, displayName, triggerReason, silentDays } = params
|
||||||
if (!sessionId) return
|
if (!sessionId) return { success: false, message: '会话无效,无法生成见解' }
|
||||||
if (!this.isEnabled()) return
|
if (!this.isEnabled()) return { success: false, message: '请先在设置中开启「AI 见解」' }
|
||||||
|
|
||||||
const { apiBaseUrl, apiKey, model, maxTokens } = this.getSharedAiModelConfig()
|
const { apiBaseUrl, apiKey, model, maxTokens } = this.getSharedAiModelConfig()
|
||||||
const allowContext = this.config.get('aiInsightAllowContext') as boolean
|
const allowContext = this.config.get('aiInsightAllowContext') as boolean
|
||||||
@@ -1393,7 +1446,7 @@ ${afterText}
|
|||||||
|
|
||||||
if (!apiBaseUrl || !apiKey) {
|
if (!apiBaseUrl || !apiKey) {
|
||||||
insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成')
|
insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成')
|
||||||
return
|
return { success: false, message: '请先填写通用 AI 模型配置(API 地址和 Key)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
||||||
@@ -1483,9 +1536,9 @@ ${afterText}
|
|||||||
// 模型主动选择跳过
|
// 模型主动选择跳过
|
||||||
if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) {
|
if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) {
|
||||||
insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`)
|
insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`)
|
||||||
return
|
return { success: true, message: `模型判断「${resolvedDisplayName}」暂无可生成的见解`, skipped: true }
|
||||||
}
|
}
|
||||||
if (!this.isEnabled()) return
|
if (!this.isEnabled()) return { success: false, message: 'AI 见解已关闭,生成结果未保存' }
|
||||||
|
|
||||||
const insight = result.trim()
|
const insight = result.trim()
|
||||||
const notifTitle = `见解 · ${resolvedDisplayName}`
|
const notifTitle = `见解 · ${resolvedDisplayName}`
|
||||||
@@ -1550,6 +1603,15 @@ ${afterText}
|
|||||||
|
|
||||||
insightLog('INFO', `已完成 ${resolvedDisplayName} 的见解处理`)
|
insightLog('INFO', `已完成 ${resolvedDisplayName} 的见解处理`)
|
||||||
this.recordTrigger(sessionId)
|
this.recordTrigger(sessionId)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: insightNotificationEnabled
|
||||||
|
? `已生成「${resolvedDisplayName}」的 AI 见解,请查看通知弹窗`
|
||||||
|
: `已生成「${resolvedDisplayName}」的 AI 见解,AI 见解消息通知当前已关闭`,
|
||||||
|
recordId: record.id,
|
||||||
|
insight,
|
||||||
|
notificationEnabled: insightNotificationEnabled
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightDebugSection(
|
insightDebugSection(
|
||||||
'ERROR',
|
'ERROR',
|
||||||
@@ -1557,6 +1619,7 @@ ${afterText}
|
|||||||
`错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}`
|
`错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}`
|
||||||
)
|
)
|
||||||
insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`)
|
insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`)
|
||||||
|
return { success: false, message: `生成失败:${(e as Error).message}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -557,8 +557,7 @@ export function ExportDateRangeDialog({
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
>
|
> <div className="export-date-range-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
|
||||||
<div className="export-date-range-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
|
|
||||||
<div className="export-date-range-dialog-header">
|
<div className="export-date-range-dialog-header">
|
||||||
<h4>{title}</h4>
|
<h4>{title}</h4>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Mic,
|
Mic,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
|
Sparkles,
|
||||||
Users
|
Users
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Avatar } from '../../components/Avatar'
|
import { Avatar } from '../../components/Avatar'
|
||||||
@@ -32,10 +33,12 @@ export interface ChatHeaderProps {
|
|||||||
isBatchTranscribing: boolean
|
isBatchTranscribing: boolean
|
||||||
runningBatchVoiceTaskType?: BatchVoiceTaskType
|
runningBatchVoiceTaskType?: BatchVoiceTaskType
|
||||||
isBatchDecrypting: boolean
|
isBatchDecrypting: boolean
|
||||||
|
isTriggeringSessionInsight: boolean
|
||||||
isRefreshingMessages: boolean
|
isRefreshingMessages: boolean
|
||||||
isLoadingMessages: boolean
|
isLoadingMessages: boolean
|
||||||
currentSessionId?: string | null
|
currentSessionId?: string | null
|
||||||
jumpCalendarWrapRef: React.RefObject<HTMLDivElement | null>
|
jumpCalendarWrapRef: React.RefObject<HTMLDivElement | null>
|
||||||
|
onTriggerSessionInsight: () => void
|
||||||
onGroupAnalytics: () => void
|
onGroupAnalytics: () => void
|
||||||
onToggleGroupMembersPanel: () => void
|
onToggleGroupMembersPanel: () => void
|
||||||
onExportCurrentSession: () => void
|
onExportCurrentSession: () => void
|
||||||
@@ -64,10 +67,12 @@ function ChatHeader({
|
|||||||
isBatchTranscribing,
|
isBatchTranscribing,
|
||||||
runningBatchVoiceTaskType,
|
runningBatchVoiceTaskType,
|
||||||
isBatchDecrypting,
|
isBatchDecrypting,
|
||||||
|
isTriggeringSessionInsight,
|
||||||
isRefreshingMessages,
|
isRefreshingMessages,
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
jumpCalendarWrapRef,
|
jumpCalendarWrapRef,
|
||||||
|
onTriggerSessionInsight,
|
||||||
onGroupAnalytics,
|
onGroupAnalytics,
|
||||||
onToggleGroupMembersPanel,
|
onToggleGroupMembersPanel,
|
||||||
onExportCurrentSession,
|
onExportCurrentSession,
|
||||||
@@ -102,6 +107,15 @@ function ChatHeader({
|
|||||||
{isGroupChat && <div className="header-subtitle">群聊</div>}
|
{isGroupChat && <div className="header-subtitle">群聊</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
<button
|
||||||
|
className={`icon-btn session-insight-btn${isTriggeringSessionInsight ? ' triggering' : ''}`}
|
||||||
|
onClick={onTriggerSessionInsight}
|
||||||
|
disabled={!currentSessionId || isTriggeringSessionInsight}
|
||||||
|
title={isTriggeringSessionInsight ? '正在生成 AI 见解' : '立即触发当前聊天 AI 见解'}
|
||||||
|
aria-label="立即触发当前聊天 AI 见解"
|
||||||
|
>
|
||||||
|
{isTriggeringSessionInsight ? <Loader2 size={18} className="spin" /> : <Sparkles size={18} />}
|
||||||
|
</button>
|
||||||
{!standaloneSessionWindow && isGroupChat && (
|
{!standaloneSessionWindow && isGroupChat && (
|
||||||
<button className="icon-btn group-analytics-btn" onClick={onGroupAnalytics} title="群聊分析">
|
<button className="icon-btn group-analytics-btn" onClick={onGroupAnalytics} title="群聊分析">
|
||||||
<BarChart3 size={18} />
|
<BarChart3 size={18} />
|
||||||
@@ -214,10 +228,12 @@ function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
|
|||||||
prev.isBatchTranscribing === next.isBatchTranscribing &&
|
prev.isBatchTranscribing === next.isBatchTranscribing &&
|
||||||
prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType &&
|
prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType &&
|
||||||
prev.isBatchDecrypting === next.isBatchDecrypting &&
|
prev.isBatchDecrypting === next.isBatchDecrypting &&
|
||||||
|
prev.isTriggeringSessionInsight === next.isTriggeringSessionInsight &&
|
||||||
prev.isRefreshingMessages === next.isRefreshingMessages &&
|
prev.isRefreshingMessages === next.isRefreshingMessages &&
|
||||||
prev.isLoadingMessages === next.isLoadingMessages &&
|
prev.isLoadingMessages === next.isLoadingMessages &&
|
||||||
prev.currentSessionId === next.currentSessionId &&
|
prev.currentSessionId === next.currentSessionId &&
|
||||||
prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef &&
|
prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef &&
|
||||||
|
prev.onTriggerSessionInsight === next.onTriggerSessionInsight &&
|
||||||
prev.onGroupAnalytics === next.onGroupAnalytics &&
|
prev.onGroupAnalytics === next.onGroupAnalytics &&
|
||||||
prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel &&
|
prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel &&
|
||||||
prev.onExportCurrentSession === next.onExportCurrentSession &&
|
prev.onExportCurrentSession === next.onExportCurrentSession &&
|
||||||
|
|||||||
@@ -1747,6 +1747,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-insight-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
+75
-1
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture, Newspaper, Star } from 'lucide-react'
|
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture, Newspaper, Star, Sparkles } from 'lucide-react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||||
@@ -1639,6 +1639,9 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
const highlightedMessageSet = useMemo(() => new Set(highlightedMessageKeys), [highlightedMessageKeys])
|
const highlightedMessageSet = useMemo(() => new Set(highlightedMessageKeys), [highlightedMessageKeys])
|
||||||
const [aiMessageInsightEnabled, setAiMessageInsightEnabled] = useState(false)
|
const [aiMessageInsightEnabled, setAiMessageInsightEnabled] = useState(false)
|
||||||
const [aiMessageInsightContextCount, setAiMessageInsightContextCount] = useState(50)
|
const [aiMessageInsightContextCount, setAiMessageInsightContextCount] = useState(50)
|
||||||
|
const [isTriggeringSessionInsight, setIsTriggeringSessionInsight] = useState(false)
|
||||||
|
const [sessionInsightHint, setSessionInsightHint] = useState<{ success: boolean; message: string } | null>(null)
|
||||||
|
const sessionInsightHintTimerRef = useRef<number | null>(null)
|
||||||
const messageKeySetRef = useRef<Set<string>>(new Set())
|
const messageKeySetRef = useRef<Set<string>>(new Set())
|
||||||
const lastMessageTimeRef = useRef(0)
|
const lastMessageTimeRef = useRef(0)
|
||||||
const isMessageListAtBottomRef = useRef(true)
|
const isMessageListAtBottomRef = useRef(true)
|
||||||
@@ -3144,6 +3147,12 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
currentSessionRef.current = currentSessionId
|
currentSessionRef.current = currentSessionId
|
||||||
messageInsightMemoryCache.clear()
|
messageInsightMemoryCache.clear()
|
||||||
|
setSessionInsightHint(null)
|
||||||
|
setIsTriggeringSessionInsight(false)
|
||||||
|
if (sessionInsightHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(sessionInsightHintTimerRef.current)
|
||||||
|
sessionInsightHintTimerRef.current = null
|
||||||
|
}
|
||||||
isMessageListAtBottomRef.current = true
|
isMessageListAtBottomRef.current = true
|
||||||
topRangeLoadLockRef.current = false
|
topRangeLoadLockRef.current = false
|
||||||
bottomRangeLoadLockRef.current = false
|
bottomRangeLoadLockRef.current = false
|
||||||
@@ -5840,6 +5849,27 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
})
|
})
|
||||||
}, [currentSession, isCurrentSessionPrivateSnsSupported])
|
}, [currentSession, isCurrentSessionPrivateSnsSupported])
|
||||||
|
|
||||||
|
const showSessionInsightHint = useCallback((hint: { success: boolean; message: string }) => {
|
||||||
|
if (sessionInsightHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(sessionInsightHintTimerRef.current)
|
||||||
|
sessionInsightHintTimerRef.current = null
|
||||||
|
}
|
||||||
|
setSessionInsightHint(hint)
|
||||||
|
sessionInsightHintTimerRef.current = window.setTimeout(() => {
|
||||||
|
setSessionInsightHint(null)
|
||||||
|
sessionInsightHintTimerRef.current = null
|
||||||
|
}, 5000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (sessionInsightHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(sessionInsightHintTimerRef.current)
|
||||||
|
sessionInsightHintTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!standaloneSessionWindow) return
|
if (!standaloneSessionWindow) return
|
||||||
setStandaloneInitialLoadRequested(false)
|
setStandaloneInitialLoadRequested(false)
|
||||||
@@ -6181,6 +6211,41 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
})
|
})
|
||||||
}, [currentSession, currentSessionId, inProgressExportSessionIds, isPreparingExportDialog])
|
}, [currentSession, currentSessionId, inProgressExportSessionIds, isPreparingExportDialog])
|
||||||
|
|
||||||
|
const handleTriggerSessionInsight = useCallback(async () => {
|
||||||
|
const session = currentSession
|
||||||
|
const sessionId = String(session?.username || currentSessionId || '').trim()
|
||||||
|
if (!sessionId || isTriggeringSessionInsight) return
|
||||||
|
|
||||||
|
setIsTriggeringSessionInsight(true)
|
||||||
|
if (sessionInsightHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(sessionInsightHintTimerRef.current)
|
||||||
|
sessionInsightHintTimerRef.current = null
|
||||||
|
}
|
||||||
|
setSessionInsightHint({ success: true, message: '正在生成当前聊天的 AI 见解...' })
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.insight.triggerSessionInsight({
|
||||||
|
sessionId,
|
||||||
|
displayName: session?.displayName || sessionId,
|
||||||
|
avatarUrl: session?.avatarUrl
|
||||||
|
})
|
||||||
|
if (currentSessionRef.current !== sessionId) return
|
||||||
|
showSessionInsightHint({
|
||||||
|
success: result.success,
|
||||||
|
message: result.message || (result.success ? 'AI 见解已生成' : 'AI 见解生成失败')
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (currentSessionRef.current !== sessionId) return
|
||||||
|
showSessionInsightHint({
|
||||||
|
success: false,
|
||||||
|
message: `触发失败:${(error as Error).message || String(error)}`
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (currentSessionRef.current === sessionId) {
|
||||||
|
setIsTriggeringSessionInsight(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentSession, currentSessionId, isTriggeringSessionInsight, showSessionInsightHint])
|
||||||
|
|
||||||
const handleGroupAnalytics = useCallback(() => {
|
const handleGroupAnalytics = useCallback(() => {
|
||||||
if (!currentSessionId || !isGroupChatSession(currentSessionId)) return
|
if (!currentSessionId || !isGroupChatSession(currentSessionId)) return
|
||||||
navigate('/analytics/group', {
|
navigate('/analytics/group', {
|
||||||
@@ -7321,10 +7386,12 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
isBatchTranscribing={isBatchTranscribing}
|
isBatchTranscribing={isBatchTranscribing}
|
||||||
runningBatchVoiceTaskType={runningBatchVoiceTaskType}
|
runningBatchVoiceTaskType={runningBatchVoiceTaskType}
|
||||||
isBatchDecrypting={isBatchDecrypting}
|
isBatchDecrypting={isBatchDecrypting}
|
||||||
|
isTriggeringSessionInsight={isTriggeringSessionInsight}
|
||||||
isRefreshingMessages={isRefreshingMessages}
|
isRefreshingMessages={isRefreshingMessages}
|
||||||
isLoadingMessages={isLoadingMessages}
|
isLoadingMessages={isLoadingMessages}
|
||||||
currentSessionId={currentSessionId}
|
currentSessionId={currentSessionId}
|
||||||
jumpCalendarWrapRef={jumpCalendarWrapRef}
|
jumpCalendarWrapRef={jumpCalendarWrapRef}
|
||||||
|
onTriggerSessionInsight={handleTriggerSessionInsight}
|
||||||
onGroupAnalytics={handleGroupAnalytics}
|
onGroupAnalytics={handleGroupAnalytics}
|
||||||
onToggleGroupMembersPanel={toggleGroupMembersPanel}
|
onToggleGroupMembersPanel={toggleGroupMembersPanel}
|
||||||
onExportCurrentSession={handleExportCurrentSession}
|
onExportCurrentSession={handleExportCurrentSession}
|
||||||
@@ -7369,6 +7436,13 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{sessionInsightHint && (
|
||||||
|
<div className={`session-insight-hint ${sessionInsightHint.success ? 'success' : 'error'}`} role="status" aria-live="polite">
|
||||||
|
{isTriggeringSessionInsight ? <Loader2 size={14} className="spin" /> : <Sparkles size={14} />}
|
||||||
|
<span>{sessionInsightHint.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ContactSnsTimelineDialog
|
<ContactSnsTimelineDialog
|
||||||
target={chatSnsTimelineTarget}
|
target={chatSnsTimelineTarget}
|
||||||
onClose={() => setChatSnsTimelineTarget(null)}
|
onClose={() => setChatSnsTimelineTarget(null)}
|
||||||
|
|||||||
@@ -265,6 +265,11 @@
|
|||||||
color: #5b55a0;
|
color: #5b55a0;
|
||||||
background: rgba(99, 102, 241, 0.12);
|
background: rgba(99, 102, 241, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.manual {
|
||||||
|
color: #0f766e;
|
||||||
|
background: rgba(20, 184, 166, 0.13);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-source-pill {
|
.insight-source-pill {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ function getTriggerLabel(reason: InsightRecordTriggerReason): string {
|
|||||||
if (reason === 'message_analysis') return '深度解析'
|
if (reason === 'message_analysis') return '深度解析'
|
||||||
if (reason === 'silence') return '沉默提醒'
|
if (reason === 'silence') return '沉默提醒'
|
||||||
if (reason === 'test') return '测试见解'
|
if (reason === 'test') return '测试见解'
|
||||||
|
if (reason === 'manual') return '手动触发'
|
||||||
return '活跃分析'
|
return '活跃分析'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
-1
@@ -21,7 +21,7 @@ export interface SocialSaveWeiboCookieResult {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'message_analysis'
|
export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'manual' | 'message_analysis'
|
||||||
export type InsightRecordSourceType = 'insight' | 'message_analysis'
|
export type InsightRecordSourceType = 'insight' | 'message_analysis'
|
||||||
|
|
||||||
export interface MessageInsightAnalysis {
|
export interface MessageInsightAnalysis {
|
||||||
@@ -1344,6 +1344,11 @@ export interface ElectronAPI {
|
|||||||
markRecordRead: (id: string) => Promise<{ success: boolean; error?: string }>
|
markRecordRead: (id: string) => Promise<{ success: boolean; error?: string }>
|
||||||
clearRecords: (filters?: InsightRecordFilters) => Promise<{ success: boolean; removed: number; error?: string }>
|
clearRecords: (filters?: InsightRecordFilters) => Promise<{ success: boolean; removed: number; error?: string }>
|
||||||
triggerTest: () => Promise<{ success: boolean; message: string }>
|
triggerTest: () => Promise<{ success: boolean; message: string }>
|
||||||
|
triggerSessionInsight: (payload: {
|
||||||
|
sessionId: string
|
||||||
|
displayName?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}) => Promise<{ success: boolean; message: string; recordId?: string; insight?: string; skipped?: boolean; notificationEnabled?: boolean }>
|
||||||
generateFootprintInsight: (payload: {
|
generateFootprintInsight: (payload: {
|
||||||
rangeLabel: string
|
rangeLabel: string
|
||||||
summary: {
|
summary: {
|
||||||
|
|||||||
Reference in new issue
Block a user