Merge pull request #998 from Jasonzhu1207/main

feat: Add manually trigger AI insights in conversations
This commit is contained in:
cc authored and GitHub committed 2026-05-22 17:47:45 +08:00
commit 2e7d6ae62b
11 files changed
+211 -12

No files matched your search

+8
View File
@@ -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: {
+5
View File
@@ -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: {
+1 -1
View File
@@ -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 {
+70 -7
View File
@@ -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
+16
View File
@@ -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 &&
+23
View File
@@ -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
View File
@@ -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)}
+5
View File
@@ -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 {
+1
View File
@@ -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 '活跃分析'
} }
+6 -1
View File
@@ -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: {