Merge pull request #12 from graycyrus/develop

feat: Intelligence Page
This commit is contained in:
Cyrus Gray
2026-03-26 16:52:34 +05:30
committed by GitHub
17 changed files with 3928 additions and 20 deletions
@@ -0,0 +1,375 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import type { ActionableItem, SnoozeOption } from '../../types/intelligence';
interface ActionableCardProps {
item: ActionableItem;
onComplete: (item: ActionableItem) => void;
onDismiss: (item: ActionableItem) => void;
onSnooze: (item: ActionableItem, duration: number) => void;
className?: string;
}
const SNOOZE_OPTIONS: SnoozeOption[] = [
{ label: '1 hour', duration: 60 * 60 * 1000 },
{ label: '6 hours', duration: 6 * 60 * 60 * 1000 },
{ label: '24 hours', duration: 24 * 60 * 60 * 1000 },
];
// Portal component for snooze dropdown to escape stacking contexts
interface SnoozeDropdownPortalProps {
isOpen: boolean;
buttonRef: React.RefObject<HTMLButtonElement | null>;
onClose: () => void;
onSnooze: (duration: number) => void;
}
function SnoozeDropdownPortal({ isOpen, buttonRef, onClose, onSnooze }: SnoozeDropdownPortalProps) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
// Calculate position based on button position
useEffect(() => {
if (isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const dropdownWidth = 120;
// Position dropdown below and aligned to right edge of button
const left = Math.max(8, rect.right - dropdownWidth);
const top = rect.bottom + 4;
setPosition({ top, left });
}
}, [isOpen, buttonRef]);
// Handle click outside to close dropdown
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
// Don't close if clicking the button or dropdown itself
if (
buttonRef.current?.contains(target) ||
dropdownRef.current?.contains(target) ||
target.closest('[data-snooze-dropdown]')
) {
return;
}
onClose();
};
// Use capture phase to ensure we handle this before other click handlers
document.addEventListener('click', handleClickOutside, true);
return () => document.removeEventListener('click', handleClickOutside, true);
}, [isOpen, onClose, buttonRef]);
// Handle escape key
useEffect(() => {
if (!isOpen) return;
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
ref={dropdownRef}
data-snooze-dropdown
className="fixed py-1 bg-stone-900 border border-stone-700 rounded-lg shadow-xl min-w-[120px] z-[9999] animate-fade-in"
style={{ top: position.top, left: position.left }}>
{SNOOZE_OPTIONS.map(option => (
<button
key={option.label}
onClick={() => onSnooze(option.duration)}
className="w-full text-left px-3 py-1.5 text-xs text-white hover:bg-stone-800 transition-colors cursor-pointer">
{option.label}
</button>
))}
</div>,
document.body
);
}
// Source icons for different actionable item types
const SOURCE_ICONS = {
email: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
),
calendar: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
),
telegram: (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
),
ai_insight: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
),
system: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
</svg>
),
trading: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
),
security: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
),
};
function formatTimeAgo(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const weeks = Math.floor(diff / (1000 * 60 * 60 * 24 * 7));
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes} min${minutes === 1 ? '' : 's'} ago`;
if (hours < 24) {
if (hours === 1) return '1 hour ago';
if (hours <= 6) return `${hours} hours ago`;
if (hours <= 12) return 'This morning';
return 'This afternoon';
}
if (days === 1) return 'Yesterday';
if (days < 7) return `${days} days ago`;
if (weeks === 1) return '1 week ago';
return `${weeks} weeks ago`;
}
function isNewItem(date: Date): boolean {
const now = new Date();
const diff = now.getTime() - date.getTime();
return diff < 5 * 60 * 1000; // Less than 5 minutes old
}
export function ActionableCard({
item,
onComplete,
onDismiss,
onSnooze,
className = '',
}: ActionableCardProps) {
const [showSnoozeMenu, setShowSnoozeMenu] = useState(false);
const [isAnimatingOut, setIsAnimatingOut] = useState(false);
const snoozeButtonRef = useRef<HTMLButtonElement>(null);
const handleComplete = () => {
// Always let the parent handle completion logic
// The parent (Intelligence.tsx) ALWAYS opens ChatModal for ALL tick actions
onComplete(item);
};
const handleDismiss = () => {
// Always let the parent handle dismiss logic and show confirmation modal
// The parent (Intelligence.tsx) always shows confirmation for ALL dismiss actions
onDismiss(item);
};
const handleSnooze = (duration: number) => {
setIsAnimatingOut(true);
setTimeout(() => {
onSnooze(item, duration);
setShowSnoozeMenu(false);
}, 200);
};
// Priority styling
const priorityClasses = {
critical: 'border-coral-500/30 bg-coral-500/5',
important: 'border-amber-500/30 bg-amber-500/5',
normal: 'border-white/10 bg-white/[0.02]',
};
const priorityDotClasses = {
critical: 'bg-coral-400',
important: 'bg-amber-400',
normal: 'bg-sage-400',
};
const sourceIcon = item.icon || SOURCE_ICONS[item.source];
const isNew = isNewItem(item.createdAt);
const timeAgo = formatTimeAgo(item.createdAt);
return (
<div
className={`
relative group transition-all duration-200 ease-in-out
${isAnimatingOut ? 'opacity-0 translate-x-4 scale-95' : 'animate-fade-up'}
${className}
`}>
<div
className={`
relative p-4 rounded-xl border backdrop-blur-sm transition-all duration-200
hover:bg-white/[0.04] hover:border-white/20
${priorityClasses[item.priority]}
`}>
{/* Main content row */}
<div className="flex items-start gap-3">
{/* Icon */}
<div className="w-8 h-8 flex items-center justify-center text-white/70 flex-shrink-0 mt-0.5">
{sourceIcon}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-white leading-snug">{item.title}</h3>
{item.description && (
<p className="text-xs text-stone-400 mt-1 leading-relaxed">{item.description}</p>
)}
</div>
{/* Action buttons */}
<div className="flex items-center gap-1 flex-shrink-0">
{/* Complete button */}
<button
onClick={handleComplete}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-sage-400 hover:bg-sage-400/10 transition-all duration-150"
title="Complete">
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</button>
{/* Dismiss button */}
<button
onClick={handleDismiss}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-coral-400 hover:bg-coral-400/10 transition-all duration-150"
title="Dismiss">
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* Snooze button */}
<div className="relative">
<button
ref={snoozeButtonRef}
onClick={() => setShowSnoozeMenu(!showSnoozeMenu)}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-150"
title="Snooze">
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
</div>
</div>
{/* Meta info */}
<div className="flex items-center gap-2 mt-2">
<div className="flex items-center gap-1.5">
<div className={`w-1.5 h-1.5 rounded-full ${priorityDotClasses[item.priority]}`} />
<span className="text-xs text-stone-500">{item.sourceLabel || item.source}</span>
</div>
<span className="text-xs text-stone-600"></span>
<span className="text-xs text-stone-500">{timeAgo}</span>
{isNew && (
<>
<span className="text-xs text-stone-600"></span>
<span className="text-xs bg-sage-500 text-white px-1.5 py-0.5 rounded-sm font-medium">
New
</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Snooze dropdown portal - renders outside of any stacking context */}
<SnoozeDropdownPortal
isOpen={showSnoozeMenu}
buttonRef={snoozeButtonRef}
onClose={() => setShowSnoozeMenu(false)}
onSnooze={handleSnooze}
/>
</div>
);
}
@@ -0,0 +1,100 @@
import { useState } from 'react';
import type { ConfirmationModal as ConfirmationModalType } from '../../types/intelligence';
interface ConfirmationModalProps {
modal: ConfirmationModalType;
onClose: () => void;
}
export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
const [dontShowAgain, setDontShowAgain] = useState(false);
if (!modal.isOpen) return null;
const handleConfirm = () => {
modal.onConfirm();
onClose();
// TODO: Handle dontShowAgain preference storage
if (dontShowAgain) {
console.log('User chose to not show similar confirmations again');
}
};
const handleCancel = () => {
modal.onCancel();
onClose();
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 animate-fade-in"
onClick={handleCancel}>
<div
className="bg-stone-900 rounded-2xl max-w-md w-full shadow-large border border-stone-700/50 animate-slide-up"
onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="p-6 pb-4">
<div className="flex items-center gap-3">
{modal.destructive && (
<div className="w-10 h-10 rounded-full bg-coral-500/10 flex items-center justify-center flex-shrink-0">
<svg
className="w-5 h-5 text-coral-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
)}
<div className="flex-1">
<h2 className="text-lg font-semibold text-white">{modal.title}</h2>
<p className="text-sm text-stone-400 mt-1">{modal.message}</p>
</div>
</div>
</div>
{/* Don't show again option */}
{modal.showDontShowAgain && (
<div className="px-6 pb-2">
<label className="flex items-center gap-2 text-sm text-stone-400 cursor-pointer">
<input
type="checkbox"
checked={dontShowAgain}
onChange={e => setDontShowAgain(e.target.checked)}
className="rounded border-stone-600 bg-stone-800 text-primary-500 focus:ring-primary-500 focus:ring-offset-0"
/>
Don't show similar suggestions
</label>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-end gap-3 p-6 pt-4 border-t border-stone-700/50">
<button
onClick={handleCancel}
className="px-4 py-2 text-sm font-medium text-stone-400 hover:text-white rounded-lg hover:bg-stone-800 transition-colors">
{modal.cancelText || 'Cancel'}
</button>
<button
onClick={handleConfirm}
className={`
px-4 py-2 text-sm font-medium rounded-lg transition-colors
${
modal.destructive
? 'bg-coral-500 hover:bg-coral-600 text-white'
: 'bg-primary-500 hover:bg-primary-600 text-white'
}
`}>
{modal.confirmText || 'Confirm'}
</button>
</div>
</div>
</div>
);
}
+148
View File
@@ -0,0 +1,148 @@
import { useCallback, useEffect, useState } from 'react';
import type { ToastNotification } from '../../types/intelligence';
interface ToastProps {
notification: ToastNotification;
onRemove: (id: string) => void;
}
const TOAST_ICONS = {
success: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
),
error: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
),
warning: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
),
info: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
};
const TOAST_STYLES = {
success: 'bg-sage-500 text-white',
error: 'bg-coral-500 text-white',
warning: 'bg-amber-500 text-white',
info: 'bg-primary-500 text-white',
};
export function Toast({ notification, onRemove }: ToastProps) {
const [isVisible, setIsVisible] = useState(false);
const [isExiting, setIsExiting] = useState(false);
const handleRemove = useCallback(() => {
setIsExiting(true);
setTimeout(() => {
onRemove(notification.id);
}, 200);
}, [onRemove, notification.id]);
useEffect(() => {
// Animate in
const showTimer = setTimeout(() => setIsVisible(true), 50);
// Auto remove after duration
const duration = notification.duration || 4000;
const removeTimer = setTimeout(() => {
handleRemove();
}, duration);
return () => {
clearTimeout(showTimer);
clearTimeout(removeTimer);
};
}, [notification, handleRemove]);
const icon = TOAST_ICONS[notification.type];
const styles = TOAST_STYLES[notification.type];
return (
<div
className={`
transform transition-all duration-200 ease-in-out
${isVisible && !isExiting ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
${isExiting ? 'scale-95' : ''}
`}>
<div
className={`
flex items-center gap-3 p-4 rounded-lg shadow-large border backdrop-blur-sm
max-w-sm w-full
${styles}
`}>
{/* Icon */}
<div className="flex-shrink-0">{icon}</div>
{/* Content */}
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium">{notification.title}</h4>
{notification.message && (
<p className="text-xs opacity-90 mt-1">{notification.message}</p>
)}
</div>
{/* Action button */}
{notification.action && (
<button
onClick={notification.action.handler}
className="text-xs font-medium underline hover:no-underline flex-shrink-0">
{notification.action.label}
</button>
)}
{/* Close button */}
<button
onClick={handleRemove}
className="flex-shrink-0 text-white/70 hover:text-white transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
);
}
interface ToastContainerProps {
notifications: ToastNotification[];
onRemove: (id: string) => void;
}
export function ToastContainer({ notifications, onRemove }: ToastContainerProps) {
if (notifications.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-50 space-y-2 pointer-events-none">
<div className="pointer-events-auto">
{notifications.map(notification => (
<Toast key={notification.id} notification={notification} onRemove={onRemove} />
))}
</div>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
// Intelligence Components
// Exports for the AI-powered actionable insights dashboard
export { ActionableCard } from './ActionableCard';
export { ConfirmationModal } from './ConfirmationModal';
export { Toast, ToastContainer } from './Toast';
export { MOCK_ACTIONABLE_ITEMS } from './mockData';
export { groupItemsByTime, filterItems, getItemStats } from './utils';
// Re-export types for convenience
export type {
ActionableItem,
ActionableItemSource,
ActionableItemPriority,
ActionableItemStatus,
TimeGroup,
ToastNotification,
ConfirmationModal as ConfirmationModalType,
} from '../../types/intelligence';
+311
View File
@@ -0,0 +1,311 @@
import type { ActionableItem } from '../../types/intelligence';
// Helper function to create dates relative to now
function createDate(minutesAgo: number): Date {
return new Date(Date.now() - minutesAgo * 60 * 1000);
}
function createDateDays(daysAgo: number): Date {
return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
}
export const MOCK_ACTIONABLE_ITEMS: ActionableItem[] = [
// Today - Fresh items
{
id: '1',
title: 'Reply to 2 critical emails expecting response within 24hrs',
description:
'Messages from john@coinbase.com and sarah@ethereum.org about partnership proposals',
source: 'email',
priority: 'critical',
status: 'active',
createdAt: createDate(2),
updatedAt: createDate(2),
expiresAt: createDate(-1440), // Expires in 24 hours
actionable: true,
hasComplexAction: true,
sourceLabel: 'Gmail',
},
{
id: '2',
title: 'Meeting with Sarah in 30 minutes - prepare documents?',
description:
'Q4 strategy meeting. Need to review the crypto market analysis and portfolio updates.',
source: 'calendar',
priority: 'important',
status: 'active',
createdAt: createDate(5),
updatedAt: createDate(5),
actionable: true,
hasComplexAction: false,
sourceLabel: 'Calendar',
},
{
id: '3',
title: 'Order lunch for your 2pm work session?',
description:
'You usually order from "Crypto Café" around this time. Your favorite: Bitcoin Bowl ($15.99)',
source: 'ai_insight',
priority: 'normal',
status: 'active',
createdAt: createDate(15),
updatedAt: createDate(15),
actionable: true,
hasComplexAction: false,
sourceLabel: 'AI Assistant',
},
{
id: '4',
title: '5 unread messages from Alpha Trading Group',
description:
'@cryptowhale mentioned you about the ETH analysis. 3 other traders are discussing market trends.',
source: 'telegram',
priority: 'normal',
status: 'active',
createdAt: createDate(25),
updatedAt: createDate(25),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Telegram',
},
{
id: '5',
title: 'Free 45min slot available for gym session',
description: 'Your calendar shows a gap between 3:15-4:00 PM. LA Fitness has availability.',
source: 'ai_insight',
priority: 'normal',
status: 'active',
createdAt: createDate(45),
updatedAt: createDate(45),
actionable: true,
hasComplexAction: false,
sourceLabel: 'AI Planner',
},
// Yesterday
{
id: '6',
title: 'Alex mentioned you in Alpha Human Development chat',
description: '"@john can you review the new trading algorithm?" - 3 hours ago',
source: 'telegram',
priority: 'important',
status: 'active',
createdAt: createDate(180),
updatedAt: createDate(180),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Telegram',
},
{
id: '7',
title: 'Update available for DeFi trading bot',
description:
'Version 2.1.4 includes security patches and 15% better performance. Requires restart.',
source: 'system',
priority: 'important',
status: 'active',
createdAt: createDate(360),
updatedAt: createDate(360),
actionable: true,
requiresConfirmation: true,
sourceLabel: 'System',
},
{
id: '8',
title: 'Backup your crypto wallet keys',
description: 'Last backup was 30 days ago. Your portfolio is up 34% since then.',
source: 'security',
priority: 'critical',
status: 'active',
createdAt: createDate(420),
updatedAt: createDate(420),
actionable: true,
requiresConfirmation: false,
hasComplexAction: true,
sourceLabel: 'Security',
},
// This week
{
id: '9',
title: 'Schedule meeting with VCs for Series A discussion',
description: 'Follow up on last weeks pitch. Andreessen Horowitz and Sequoia are interested.',
source: 'email',
priority: 'critical',
status: 'active',
createdAt: createDateDays(2),
updatedAt: createDateDays(2),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Gmail',
},
{
id: '10',
title: 'Review 12 pending GitHub pull requests',
description:
'Features for v0.21.0 release. 8 from team members, 4 from community contributors.',
source: 'system',
priority: 'important',
status: 'active',
createdAt: createDateDays(3),
updatedAt: createDateDays(3),
actionable: true,
hasComplexAction: true,
sourceLabel: 'GitHub',
},
{
id: '11',
title: 'BTC price alert: Approaching your target of $65,000',
description: 'Currently at $64,420. You set this alert 2 weeks ago. Consider taking profits?',
source: 'trading',
priority: 'important',
status: 'active',
createdAt: createDateDays(3),
updatedAt: createDateDays(3),
actionable: true,
hasComplexAction: false,
sourceLabel: 'Trading Bot',
},
{
id: '12',
title: 'Complete KYC verification for Binance account',
description: 'Required for withdrawal limits above $2,000. Upload ID and proof of address.',
source: 'trading',
priority: 'normal',
status: 'active',
createdAt: createDateDays(4),
updatedAt: createDateDays(4),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Binance',
},
// Older items
{
id: '13',
title: 'Respond to podcast interview request from Lex Fridman',
description: 'Topic: "The Future of Decentralized Communication". Suggested dates: next month.',
source: 'email',
priority: 'important',
status: 'active',
createdAt: createDateDays(8),
updatedAt: createDateDays(8),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Gmail',
},
{
id: '14',
title: 'Submit tax documents for Q3 crypto transactions',
description: '47 transactions across 5 exchanges. Deadline: October 15th.',
source: 'system',
priority: 'critical',
status: 'active',
createdAt: createDateDays(10),
updatedAt: createDateDays(10),
expiresAt: createDate(-14400), // Expires in 10 days
actionable: true,
hasComplexAction: true,
requiresConfirmation: false,
sourceLabel: 'TaxBot',
},
{
id: '15',
title: 'Renew SSL certificate for api.alphahuman.com',
description:
'Certificate expires in 5 days. Automatic renewal failed - manual intervention required.',
source: 'system',
priority: 'critical',
status: 'active',
createdAt: createDateDays(12),
updatedAt: createDateDays(12),
actionable: true,
hasComplexAction: false,
requiresConfirmation: false,
sourceLabel: 'DevOps',
},
{
id: '16',
title: 'Plan team building event for remote employees',
description:
'15 team members across 8 time zones. Budget approved: $5,000. Preference: virtual escape room.',
source: 'ai_insight',
priority: 'normal',
status: 'active',
createdAt: createDateDays(14),
updatedAt: createDateDays(14),
actionable: true,
hasComplexAction: true,
sourceLabel: 'HR Assistant',
},
// Expired/old items that should show as low priority
{
id: '17',
title: 'Update LinkedIn profile with latest achievements',
description: 'Add recent TechCrunch feature and Y Combinator alumni status.',
source: 'ai_insight',
priority: 'normal',
status: 'active',
createdAt: createDateDays(20),
updatedAt: createDateDays(20),
actionable: true,
hasComplexAction: false,
sourceLabel: 'Career AI',
},
{
id: '18',
title: 'Review and approve marketing budget for Q4',
description: '$50K allocated for influencer partnerships and conference sponsorships.',
source: 'email',
priority: 'important',
status: 'active',
createdAt: createDateDays(21),
updatedAt: createDateDays(21),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Team Email',
},
{
id: '19',
title: 'Check server capacity for expected user growth',
description: 'Daily active users grew 23% this month. Current infrastructure may need scaling.',
source: 'system',
priority: 'normal',
status: 'active',
createdAt: createDateDays(25),
updatedAt: createDateDays(25),
actionable: true,
hasComplexAction: false,
sourceLabel: 'Monitoring',
},
{
id: '20',
title: 'Organize crypto portfolio - consolidate across 7 wallets',
description:
'Assets spread across MetaMask, Ledger, Trust Wallet, and 4 others. Gas fees optimized for Sunday.',
source: 'security',
priority: 'normal',
status: 'active',
createdAt: createDateDays(30),
updatedAt: createDateDays(30),
actionable: true,
hasComplexAction: true,
sourceLabel: 'Portfolio AI',
},
];
+140
View File
@@ -0,0 +1,140 @@
import type { ActionableItem, TimeGroup } from '../../types/intelligence';
/**
* Groups actionable items by time periods (Today, Yesterday, This Week, Older)
*/
export function groupItemsByTime(items: ActionableItem[]): TimeGroup[] {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const groups: Record<string, ActionableItem[]> = {
today: [],
yesterday: [],
thisWeek: [],
older: [],
};
items.forEach(item => {
const itemDate = new Date(item.createdAt);
const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate());
if (itemDateOnly >= today) {
groups.today.push(item);
} else if (itemDateOnly >= yesterday) {
groups.yesterday.push(item);
} else if (itemDateOnly >= oneWeekAgo) {
groups.thisWeek.push(item);
} else {
groups.older.push(item);
}
});
// Sort items within each group by priority and then by date (newest first)
const sortItems = (items: ActionableItem[]) => {
const priorityOrder = { critical: 0, important: 1, normal: 2 };
return items.sort((a, b) => {
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0) return priorityDiff;
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
};
const timeGroups: TimeGroup[] = [];
if (groups.today.length > 0) {
timeGroups.push({ label: 'Today', items: sortItems(groups.today), count: groups.today.length });
}
if (groups.yesterday.length > 0) {
timeGroups.push({
label: 'Yesterday',
items: sortItems(groups.yesterday),
count: groups.yesterday.length,
});
}
if (groups.thisWeek.length > 0) {
timeGroups.push({
label: 'This Week',
items: sortItems(groups.thisWeek),
count: groups.thisWeek.length,
});
}
if (groups.older.length > 0) {
timeGroups.push({ label: 'Older', items: sortItems(groups.older), count: groups.older.length });
}
return timeGroups;
}
/**
* Filters items based on various criteria
*/
export function filterItems(
items: ActionableItem[],
options: { source?: string; priority?: string; status?: string; searchTerm?: string }
): ActionableItem[] {
let filtered = [...items];
if (options.source && options.source !== 'all') {
filtered = filtered.filter(item => item.source === options.source);
}
if (options.priority && options.priority !== 'all') {
filtered = filtered.filter(item => item.priority === options.priority);
}
if (options.status && options.status !== 'all') {
filtered = filtered.filter(item => item.status === options.status);
}
if (options.searchTerm) {
const term = options.searchTerm.toLowerCase();
filtered = filtered.filter(
item =>
item.title.toLowerCase().includes(term) ||
item.description?.toLowerCase().includes(term) ||
item.sourceLabel?.toLowerCase().includes(term)
);
}
return filtered;
}
/**
* Gets summary statistics for actionable items
*/
export function getItemStats(items: ActionableItem[]) {
const total = items.length;
const byPriority = items.reduce(
(acc, item) => {
acc[item.priority]++;
return acc;
},
{ critical: 0, important: 0, normal: 0 }
);
const bySource = items.reduce(
(acc, item) => {
acc[item.source] = (acc[item.source] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const newItems = items.filter(item => {
const diff = Date.now() - item.createdAt.getTime();
return diff < 5 * 60 * 1000; // Less than 5 minutes
}).length;
const expiringSoon = items.filter(item => {
if (!item.expiresAt) return false;
const diff = item.expiresAt.getTime() - Date.now();
return diff < 24 * 60 * 60 * 1000 && diff > 0; // Expires within 24 hours
}).length;
return { total, byPriority, bySource, newItems, expiringSoon };
}
+256
View File
@@ -0,0 +1,256 @@
import { useCallback, useEffect, useState } from 'react';
import { MOCK_ACTIONABLE_ITEMS } from '../components/intelligence/mockData';
import { intelligenceApi, type ConnectedTool } from '../services/intelligenceApi';
import type { ActionableItem, ActionableItemStatus } from '../types/intelligence';
import { transformBackendItemsToFrontend, transformBackendMessagesToFrontend } from '../utils/intelligenceTransforms';
/**
* Fallback implementation of Intelligence API hooks without React Query
* Used when React Query is not available
*/
interface UseActionableItemsResult {
data: ActionableItem[] | undefined;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
/**
* Hook for fetching actionable items (fallback version)
* TODO: Remove MOCK_ACTIONABLE_ITEMS once backend APIs are ready
*/
export const useActionableItems = (options?: {
refetchInterval?: number;
enabled?: boolean;
}): UseActionableItemsResult => {
const [data, setData] = useState<ActionableItem[] | undefined>(MOCK_ACTIONABLE_ITEMS);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchItems = useCallback(async () => {
if (options?.enabled === false) return;
try {
setLoading(true);
setError(null);
const backendItems = await intelligenceApi.getActionableItems();
const items = transformBackendItemsToFrontend(backendItems);
setData(items.length > 0 ? items : MOCK_ACTIONABLE_ITEMS);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch items';
setError(errorMessage);
console.error('Failed to fetch actionable items:', err);
// TODO: Replace with actual data
setData(MOCK_ACTIONABLE_ITEMS);
} finally {
setLoading(false);
}
}, [options?.enabled]);
// Initial fetch
useEffect(() => {
fetchItems();
}, [fetchItems]);
// Set up refetch interval
useEffect(() => {
if (options?.refetchInterval) {
const interval = setInterval(fetchItems, options.refetchInterval);
return () => clearInterval(interval);
}
}, [options?.refetchInterval, fetchItems]);
return {
data,
loading,
error,
refetch: fetchItems,
};
};
interface UseUpdateActionableItemResult {
mutateAsync: (variables: {
itemId: string;
status: ActionableItemStatus;
}) => Promise<{ itemId: string; status: ActionableItemStatus; updatedAt: Date }>;
loading: boolean;
error: string | null;
}
/**
* Hook for updating actionable item status (fallback version)
*/
export const useUpdateActionableItem = (): UseUpdateActionableItemResult => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const mutateAsync = useCallback(async (variables: {
itemId: string;
status: ActionableItemStatus;
}) => {
try {
setLoading(true);
setError(null);
await intelligenceApi.updateItemStatus(variables.itemId, variables.status);
return { ...variables, updatedAt: new Date() };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
return {
mutateAsync,
loading,
error,
};
};
interface UseSnoozeActionableItemResult {
mutateAsync: (variables: {
itemId: string;
snoozeUntil: Date;
}) => Promise<{ itemId: string; snoozeUntil: Date; updatedAt: Date }>;
loading: boolean;
error: string | null;
}
/**
* Hook for snoozing actionable item (fallback version)
*/
export const useSnoozeActionableItem = (): UseSnoozeActionableItemResult => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const mutateAsync = useCallback(async (variables: {
itemId: string;
snoozeUntil: Date;
}) => {
try {
setLoading(true);
setError(null);
await intelligenceApi.snoozeItem(variables.itemId, variables.snoozeUntil);
return { ...variables, updatedAt: new Date() };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to snooze item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
return {
mutateAsync,
loading,
error,
};
};
interface UseChatSessionResult {
data: {
threadId: string;
messages: any[];
} | null;
loading: boolean;
error: string | null;
}
/**
* Hook for creating or getting chat session (fallback version)
*/
export const useChatSession = (itemId: string | null): UseChatSessionResult => {
const [data, setData] = useState<{ threadId: string; messages: any[] } | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!itemId) return;
const fetchSession = async () => {
try {
setLoading(true);
setError(null);
const response = await intelligenceApi.getOrCreateThread(itemId);
setData({
threadId: response.threadId,
messages: transformBackendMessagesToFrontend(response.messages),
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create session';
setError(errorMessage);
console.error('Failed to create chat session:', err);
} finally {
setLoading(false);
}
};
fetchSession();
}, [itemId]);
return {
data,
loading,
error,
};
};
interface UseExecuteTaskResult {
mutateAsync: (variables: {
itemId: string;
connectedTools: ConnectedTool[];
}) => Promise<{
executionId: string;
sessionId: string;
status: string;
}>;
loading: boolean;
error: string | null;
}
/**
* Hook for executing tasks (fallback version)
*/
export const useExecuteTask = (): UseExecuteTaskResult => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const mutateAsync = useCallback(async (variables: {
itemId: string;
connectedTools: ConnectedTool[];
}) => {
try {
setLoading(true);
setError(null);
const result = await intelligenceApi.executeTask(variables.itemId, variables.connectedTools);
return result;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to execute task';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
return {
mutateAsync,
loading,
error,
};
};
// Export query key utilities for consistency
export const intelligenceKeys = {
all: ['intelligence'] as const,
items: () => [...intelligenceKeys.all, 'items'] as const,
item: (id: string) => [...intelligenceKeys.all, 'item', id] as const,
thread: (itemId: string) => [...intelligenceKeys.all, 'thread', itemId] as const,
messages: (threadId: string) => [...intelligenceKeys.all, 'messages', threadId] as const,
execution: (executionId: string) => [...intelligenceKeys.all, 'execution', executionId] as const,
};
+388
View File
@@ -0,0 +1,388 @@
import { useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { socketService } from '../services/socketService';
import type { RootState } from '../store';
import { addMessage, setExecutionResult, setTyping, updateExecutionProgress } from '../store/intelligenceSlice';
import { createChatMessage } from '../utils/intelligenceTransforms';
import { emitViaRustSocket } from '../utils/tauriSocket';
/**
* WebSocket event payloads for Intelligence system
*/
interface ProcessMessagePayload {
message: string;
threadId: string;
sessionId?: string;
context?: Record<string, any>;
}
interface AgentResponsePayload {
message: string;
threadId: string;
sessionId?: string;
shouldExecute?: boolean;
executionPlan?: any;
metadata?: Record<string, any>;
}
interface ExecutionProgressPayload {
executionId: string;
sessionId: string;
step: {
id: string;
label: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
timestamp: string;
};
progress: Array<{
id: string;
label: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
timestamp?: Date;
}>;
}
interface ExecutionCompletePayload {
executionId: string;
sessionId: string;
status: 'completed' | 'failed';
result?: any;
error?: string;
artifacts?: Array<{
type: string;
url: string;
title: string;
description?: string;
}>;
}
interface ChatInitPayload {
tools: any[];
threadId: string;
sessionId?: string;
context?: Record<string, any>;
}
/**
* Check if running in Tauri environment for socket routing
*/
function isTauri(): boolean {
try {
return (window as any)?.__TAURI__ !== undefined;
} catch {
return false;
}
}
/**
* Hook for Intelligence WebSocket integration
* Handles real-time communication for chat and task execution
*/
export const useIntelligenceSocket = () => {
const dispatch = useDispatch();
const socket = socketService.getSocket();
const eventHandlersRegistered = useRef(false);
// Get current socket connection status
const isSocketConnected = useSelector((state: RootState) => {
const userId = state.user.user?._id || '__pending__';
return state.socket.byUser[userId]?.status === 'connected';
});
/**
* Send message to AI agent via WebSocket
*/
const sendMessage = useCallback(async (payload: ProcessMessagePayload) => {
if (isTauri()) {
// Use Rust socket for Tauri environment
emitViaRustSocket('processMessageForUser', payload);
} else if (socket?.connected) {
socket.emit('processMessageForUser', payload);
} else {
console.warn('Cannot send message - socket not connected');
throw new Error('Socket not connected');
}
}, [socket]);
/**
* Initialize chat session with tools
*/
const sendChatInit = useCallback(async (payload: ChatInitPayload) => {
if (isTauri()) {
emitViaRustSocket('chat:init', payload);
} else if (socket?.connected) {
socket.emit('chat:init', payload);
} else {
console.warn('Cannot initialize chat - socket not connected');
throw new Error('Socket not connected');
}
}, [socket]);
/**
* Send typing indicator
*/
const sendTyping = useCallback((threadId: string, isTyping: boolean) => {
const payload = { threadId, isTyping };
if (isTauri()) {
emitViaRustSocket('chat:typing', payload);
} else if (socket?.connected) {
socket.emit('chat:typing', payload);
}
}, [socket]);
/**
* Register WebSocket event handlers
*/
const registerEventHandlers = useCallback(() => {
if (!socket || eventHandlersRegistered.current) return;
// Agent response handler
const handleAgentResponse = (data: AgentResponsePayload) => {
console.log('Intelligence: Received agent response', {
threadId: data.threadId,
hasMessage: !!data.message,
shouldExecute: data.shouldExecute,
});
if (data.message && data.threadId) {
const aiMessage = createChatMessage(data.message, 'ai');
dispatch(addMessage({
threadId: data.threadId,
message: aiMessage,
}));
}
// Stop typing indicator
dispatch(setTyping({
threadId: data.threadId,
isTyping: false,
}));
// Handle execution trigger
if (data.shouldExecute && data.executionPlan) {
console.log('Intelligence: Execution requested', {
threadId: data.threadId,
plan: data.executionPlan,
});
// Execution will be handled by the component
}
};
// Execution progress handler
const handleExecutionProgress = (data: ExecutionProgressPayload) => {
console.log('Intelligence: Execution progress', {
executionId: data.executionId,
step: data.step?.label,
status: data.step?.status,
});
if (data.progress) {
dispatch(updateExecutionProgress({
executionId: data.executionId,
progress: data.progress,
}));
}
};
// Execution complete handler
const handleExecutionComplete = (data: ExecutionCompletePayload) => {
console.log('Intelligence: Execution complete', {
executionId: data.executionId,
status: data.status,
hasResult: !!data.result,
hasArtifacts: !!data.artifacts?.length,
});
dispatch(setExecutionResult({
executionId: data.executionId,
result: data.result,
status: data.status,
error: data.error,
}));
// Send completion message if we have artifacts
if (data.artifacts?.length && data.sessionId) {
// This would need the threadId - we'd need to track execution to thread mapping
// For now, we'll let the component handle the completion message
console.log('Intelligence: Task completed with artifacts', {
artifactCount: data.artifacts.length,
sessionId: data.sessionId,
});
}
};
// Typing indicator handler
const handleTyping = (data: { threadId: string; isTyping: boolean }) => {
dispatch(setTyping({
threadId: data.threadId,
isTyping: data.isTyping,
}));
};
// Register all handlers
socket.on('agentResponse', handleAgentResponse);
socket.on('execution:step_progress', handleExecutionProgress);
socket.on('execution:complete', handleExecutionComplete);
socket.on('chat:typing', handleTyping);
eventHandlersRegistered.current = true;
// Return cleanup function
return () => {
socket.off('agentResponse', handleAgentResponse);
socket.off('execution:step_progress', handleExecutionProgress);
socket.off('execution:complete', handleExecutionComplete);
socket.off('chat:typing', handleTyping);
eventHandlersRegistered.current = false;
};
}, [socket, dispatch]);
/**
* Register event handlers when socket is available
*/
useEffect(() => {
if (socket && isSocketConnected) {
const cleanup = registerEventHandlers();
return cleanup;
}
}, [socket, isSocketConnected, registerEventHandlers]);
/**
* Cleanup on unmount
*/
useEffect(() => {
return () => {
eventHandlersRegistered.current = false;
};
}, []);
return {
// Connection status
isConnected: isSocketConnected,
// Message sending
sendMessage,
sendChatInit,
sendTyping,
// Utility functions
isReady: isSocketConnected && !!socket,
};
};
/**
* Hook for managing Intelligence WebSocket connection lifecycle
*/
export const useIntelligenceSocketManager = () => {
const token = useSelector((state: RootState) => state.auth.token);
const isConnected = useSelector((state: RootState) => {
const userId = state.user.user?._id || '__pending__';
return state.socket.byUser[userId]?.status === 'connected';
});
/**
* Initialize Intelligence socket connection
*/
const connect = useCallback(() => {
if (token && !isConnected) {
console.log('Intelligence: Initializing socket connection');
socketService.connect(token);
}
}, [token, isConnected]);
/**
* Disconnect Intelligence socket
*/
const disconnect = useCallback(() => {
console.log('Intelligence: Disconnecting socket');
socketService.disconnect();
}, []);
/**
* Auto-connect when token is available
*/
useEffect(() => {
if (token && !isConnected) {
connect();
}
}, [token, isConnected, connect]);
return {
connect,
disconnect,
isConnected,
isReady: isConnected && !!token,
};
};
/**
* Hook for Intelligence-specific event subscriptions
*/
export const useIntelligenceEvents = () => {
const socket = socketService.getSocket();
/**
* Subscribe to agent responses for a specific thread
*/
const onAgentResponse = useCallback((
threadId: string,
callback: (data: AgentResponsePayload) => void
) => {
if (!socket) return () => {};
const handler = (data: AgentResponsePayload) => {
if (data.threadId === threadId) {
callback(data);
}
};
socket.on('agentResponse', handler);
return () => socket.off('agentResponse', handler);
}, [socket]);
/**
* Subscribe to execution progress for a specific execution
*/
const onExecutionProgress = useCallback((
executionId: string,
callback: (data: ExecutionProgressPayload) => void
) => {
if (!socket) return () => {};
const handler = (data: ExecutionProgressPayload) => {
if (data.executionId === executionId) {
callback(data);
}
};
socket.on('execution:step_progress', handler);
return () => socket.off('execution:step_progress', handler);
}, [socket]);
/**
* Subscribe to execution completion for a specific execution
*/
const onExecutionComplete = useCallback((
executionId: string,
callback: (data: ExecutionCompletePayload) => void
) => {
if (!socket) return () => {};
const handler = (data: ExecutionCompletePayload) => {
if (data.executionId === executionId) {
callback(data);
}
};
socket.on('execution:complete', handler);
return () => socket.off('execution:complete', handler);
}, [socket]);
return {
onAgentResponse,
onExecutionProgress,
onExecutionComplete,
};
};
+316
View File
@@ -0,0 +1,316 @@
/**
* Chat tools initialization for Intelligence system.
*
* Provides functionality to gather all active skills' tools and send them
* to the backend when a chat session is initialized, enabling AI to understand
* available capabilities for task execution.
*/
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
import debug from 'debug';
import type { ConnectedTool } from '../../services/intelligenceApi';
import { socketService } from '../../services/socketService';
import { store } from '../../store';
import { emitViaRustSocket } from '../../utils/tauriSocket';
import { transformMCPToConnectedTools } from '../../utils/intelligenceTransforms';
import type { MCPTool } from '../mcp';
import { deriveConnectionStatus } from '../skills/hooks';
// Chat tools logger using debug package
const chatToolsLog = debug('chat:tools');
const chatToolsWarn = debug('chat:tools:warn');
// Enable logging in development
if (import.meta.env.DEV || import.meta.env.MODE === 'development') {
debug.enable('chat:tools*');
}
/**
* Check if running in Tauri environment
*/
function isTauri(): boolean {
try {
return coreIsTauri();
} catch {
return false;
}
}
/**
* Interface for chat initialization payload
*/
export interface ChatInitPayload {
tools: MCPTool[];
connectedTools: ConnectedTool[];
sessionId?: string;
threadId?: string;
timestamp: number;
}
/**
* Gather all tools from active skills and send them to the backend
* when initializing a chat session.
*
* This function:
* 1. Retrieves all skills from Redux store
* 2. Filters for active/ready skills with completed setup
* 3. Extracts and formats tool definitions in both MCP and Connected formats
* 4. Emits 'chat:init' event via appropriate socket method
*
* @param sessionId - Optional chat session identifier
* @param threadId - Optional thread identifier for chat session
* @param useConnectedFormat - Whether to send tools in Connected format (default: true)
*/
export function initializeChatWithTools(
sessionId?: string,
threadId?: string,
useConnectedFormat: boolean = true
): void {
try {
const state = store.getState();
const skills = state.skills.skills;
const skillStates = state.skills.skillStates;
chatToolsLog('Initializing chat with tools', {
sessionId,
threadId,
useConnectedFormat,
totalSkills: Object.keys(skills).length,
});
const activeTools: MCPTool[] = [];
// Process each skill to extract active tools
for (const [skillId, skill] of Object.entries(skills)) {
// Derive connection status using existing logic
const connectionStatus = deriveConnectionStatus(
skill.status,
skill.setupComplete,
skillStates[skillId]
);
// Only include tools from skills that are ready and properly connected
const isSkillActive =
(skill.status === 'ready' || skill.status === 'running') &&
skill.setupComplete &&
(connectionStatus === 'connected' || connectionStatus === 'connecting'); // Include connecting state for gradual initialization
if (isSkillActive && skill.tools?.length) {
chatToolsLog('Processing tools for active skill', {
skillId,
skillName: skill.manifest.name,
toolCount: skill.tools.length,
status: skill.status,
connectionStatus,
});
// Transform skill tools to MCP format with skill prefix
for (const tool of skill.tools) {
activeTools.push({
name: `${skillId}__${tool.name}`,
description: `${skill.manifest.name}: ${tool.description}`,
inputSchema: tool.inputSchema,
});
}
} else {
chatToolsLog('Skipping inactive skill', {
skillId,
skillName: skill.manifest.name,
status: skill.status,
setupComplete: skill.setupComplete,
connectionStatus,
toolCount: skill.tools?.length || 0,
});
}
}
// Create tools breakdown by skill for detailed logging
const toolsBySkill: Record<string, { skillName: string; tools: string[] }> = {};
activeTools.forEach(tool => {
const [skillId] = tool.name.split('__');
const skill = skills[skillId];
if (skill) {
if (!toolsBySkill[skillId]) {
toolsBySkill[skillId] = { skillName: skill.manifest.name, tools: [] };
}
toolsBySkill[skillId].tools.push(tool.name.split('__')[1]);
}
});
// Transform to connected tools format if requested
const connectedTools = useConnectedFormat ? transformMCPToConnectedTools(activeTools) : [];
// Prepare chat initialization payload
const payload: ChatInitPayload = {
tools: activeTools,
connectedTools,
sessionId,
threadId,
timestamp: Date.now()
};
// Detailed logging of available tools
chatToolsLog('🛠️ AVAILABLE TOOLS FOR CHAT INITIALIZATION', {
sessionId,
threadId,
totalTools: activeTools.length,
totalConnectedTools: connectedTools.length,
totalSkills: Object.keys(toolsBySkill).length,
useConnectedFormat,
});
// Log tools breakdown by skill
Object.entries(toolsBySkill).forEach(([skillId, { skillName, tools }]) => {
chatToolsLog(`📦 ${skillName} (${skillId}):`, { toolCount: tools.length, tools });
});
// Log complete tools list with descriptions
if (activeTools.length > 0) {
chatToolsLog(
'📋 Complete Tools List:',
activeTools.map(tool => ({
name: tool.name,
description: tool.description.slice(0, 80) + (tool.description.length > 80 ? '...' : ''),
}))
);
} else {
chatToolsLog('⚠️ No active tools available - chat will have no tool capabilities');
}
// Emit via appropriate socket method based on environment
if (isTauri()) {
chatToolsLog('Emitting chat:init via Rust socket');
emitViaRustSocket('chat:init', payload);
} else {
chatToolsLog('Emitting chat:init via web socket');
if (socketService.isConnected()) {
socketService.emit('chat:init', payload);
} else {
chatToolsWarn('Socket not connected - chat initialization may be delayed');
// Could potentially queue the initialization for when socket connects
}
}
chatToolsLog('Chat tools initialization completed', {
sessionId,
threadId,
toolCount: activeTools.length,
connectedToolCount: connectedTools.length,
environment: isTauri() ? 'tauri' : 'web',
});
} catch (error) {
chatToolsWarn('Failed to initialize chat with tools', {
sessionId,
error: error instanceof Error ? error.message : String(error),
});
// Don't throw - allow chat to continue without tools if needed
// The backend should handle empty tools gracefully
}
}
/**
* Initialize Intelligence chat session with both MCP and Connected tools formats
* @param sessionId - Chat session identifier
* @param threadId - Thread identifier for chat session
* @param connectedTools - Pre-transformed connected tools (optional)
*/
export function initializeIntelligenceChatSession(
sessionId: string,
threadId: string,
connectedTools?: ConnectedTool[]
): void {
try {
const mcpTools = getCurrentActiveTools();
const finalConnectedTools = connectedTools || transformMCPToConnectedTools(mcpTools);
const payload = {
tools: finalConnectedTools, // Intelligence system expects Connected format
sessionId,
threadId,
timestamp: Date.now(),
};
chatToolsLog('Intelligence: Initializing chat session', {
sessionId,
threadId,
toolCount: finalConnectedTools.length,
});
// Emit via appropriate socket method based on environment
if (isTauri()) {
chatToolsLog('Intelligence: Emitting chat:init via Rust socket');
emitViaRustSocket('chat:init', payload);
} else {
chatToolsLog('Intelligence: Emitting chat:init via web socket');
if (socketService.isConnected()) {
socketService.emit('chat:init', payload);
} else {
chatToolsWarn('Intelligence: Socket not connected - chat initialization may be delayed');
}
}
} catch (error) {
chatToolsWarn('Intelligence: Failed to initialize chat session', {
sessionId,
threadId,
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* Get current active tools without emitting (for debugging/display purposes)
*/
export function getCurrentActiveTools(): MCPTool[] {
try {
const state = store.getState();
const skills = state.skills.skills;
const skillStates = state.skills.skillStates;
const activeTools: MCPTool[] = [];
for (const [skillId, skill] of Object.entries(skills)) {
const connectionStatus = deriveConnectionStatus(
skill.status,
skill.setupComplete,
skillStates[skillId]
);
const isSkillActive =
(skill.status === 'ready' || skill.status === 'running') &&
skill.setupComplete &&
(connectionStatus === 'connected' || connectionStatus === 'connecting');
if (isSkillActive && skill.tools?.length) {
for (const tool of skill.tools) {
activeTools.push({
name: `${skillId}__${tool.name}`,
description: `${skill.manifest.name}: ${tool.description}`,
inputSchema: tool.inputSchema,
});
}
}
}
return activeTools;
} catch (error) {
chatToolsWarn('Failed to get current active tools', {
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
/**
* Get current active tools in Connected format for Intelligence system
*/
export function getCurrentConnectedTools(): ConnectedTool[] {
try {
const mcpTools = getCurrentActiveTools();
return transformMCPToConnectedTools(mcpTools);
} catch (error) {
chatToolsWarn('Failed to get current connected tools', {
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
+375 -20
View File
@@ -1,31 +1,386 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ActionableCard } from '../components/intelligence/ActionableCard';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import { ToastContainer } from '../components/intelligence/Toast';
import { filterItems, getItemStats, groupItemsByTime } from '../components/intelligence/utils';
import {
useActionableItems,
useSnoozeActionableItem,
useUpdateActionableItem,
} from '../hooks/useIntelligenceApiFallback';
import { useIntelligenceSocket, useIntelligenceSocketManager } from '../hooks/useIntelligenceSocket';
import { useIntelligenceStats } from '../hooks/useIntelligenceStats';
import type { RootState } from '../store';
import {
setSourceFilter,
setSearchFilter,
} from '../store/intelligenceSlice';
import type {
ActionableItem,
ActionableItemStatus,
ConfirmationModal as ConfirmationModalType,
ToastNotification,
} from '../types/intelligence';
export default function Intelligence() {
const dispatch = useDispatch();
const { aiStatus } = useIntelligenceStats();
// Redux state
const intelligenceState = useSelector((state: RootState) => state.intelligence);
const { filters } = intelligenceState;
// API hooks
const {
data: apiItems,
loading: itemsLoading,
error: itemsError,
refetch: refetchItems,
} = useActionableItems();
const { mutateAsync: updateItemStatus } = useUpdateActionableItem();
const { mutateAsync: snoozeItem } = useSnoozeActionableItem();
// Socket integration
const socketManager = useIntelligenceSocketManager();
const { isConnected: socketConnected } = useIntelligenceSocket();
// Local state for UI
const [toasts, setToasts] = useState<ToastNotification[]>([]);
const [confirmationModal, setConfirmationModal] = useState<ConfirmationModalType>({
isOpen: false,
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {},
});
// Use API data or fallback to empty array
const items = apiItems || [];
// Initialize socket connection
useEffect(() => {
if (!socketConnected) {
socketManager.connect();
}
}, [socketConnected, socketManager]);
// Handle API errors with toast notifications
useEffect(() => {
if (itemsError) {
addToast({
type: 'error',
title: 'Failed to load items',
message: typeof itemsError === 'string' ? itemsError : 'Unable to fetch actionable items',
});
}
}, [itemsError]);
// Filter and group items
const filteredItems = useMemo(() => {
const activeItems = items.filter(item => item.status === 'active');
return filterItems(activeItems, {
source: filters.source,
priority: filters.priority,
searchTerm: filters.search,
});
}, [items, filters.source, filters.priority, filters.search]);
const timeGroups = useMemo(() => {
return groupItemsByTime(filteredItems);
}, [filteredItems]);
const stats = useMemo(() => {
return getItemStats(filteredItems);
}, [filteredItems]);
// Toast utilities
const addToast = useCallback((toast: Omit<ToastNotification, 'id'>) => {
const newToast: ToastNotification = { ...toast, id: `toast-${Date.now()}-${Math.random()}` };
setToasts(prev => [...prev, newToast]);
}, []);
const removeToast = useCallback((id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
}, []);
// Item action handlers with real backend integration
const handleUpdateItemStatus = useCallback(async (itemId: string, status: ActionableItemStatus) => {
try {
await updateItemStatus({ itemId, status });
// Success toast
let message = '';
switch (status) {
case 'completed':
message = 'Task marked as completed';
break;
case 'dismissed':
message = 'Task dismissed';
break;
case 'active':
message = 'Task reactivated';
break;
default:
message = 'Status updated';
}
addToast({
type: 'success',
title: 'Status Updated',
message,
});
} catch (error) {
console.error('Failed to update item status:', error);
addToast({
type: 'error',
title: 'Update Failed',
message: error instanceof Error ? error.message : 'Failed to update item status',
});
}
}, [updateItemStatus]);
const handleComplete = useCallback(async (item: ActionableItem) => {
await handleUpdateItemStatus(item.id, 'completed');
}, [handleUpdateItemStatus]);
const handleDismiss = useCallback(
(item: ActionableItem) => {
// Always show confirmation modal for ALL dismiss actions
setConfirmationModal({
isOpen: true,
title: 'Dismiss item?',
message: `Are you sure you want to dismiss "${item.title}"?`,
confirmText: 'Dismiss',
cancelText: 'Cancel',
destructive: item.priority === 'critical',
showDontShowAgain: !item.requiresConfirmation,
onConfirm: async () => {
try {
await handleUpdateItemStatus(item.id, 'dismissed');
// Add undo action to toast
addToast({
type: 'info',
title: 'Dismissed',
message: item.title.length > 40 ? `${item.title.substring(0, 40)}...` : item.title,
action: {
label: 'Undo',
handler: () => handleUpdateItemStatus(item.id, 'active'),
},
});
} catch (error) {
console.error('Failed to dismiss item:', error);
}
},
onCancel: () => {},
});
},
[handleUpdateItemStatus, addToast]
);
const handleSnooze = useCallback(
async (item: ActionableItem, duration: number) => {
try {
const snoozeUntil = new Date(Date.now() + duration);
await snoozeItem({ itemId: item.id, snoozeUntil });
const hours = Math.round(duration / (1000 * 60 * 60));
addToast({
type: 'info',
title: 'Snoozed',
message: `Reminded in ${hours === 1 ? '1 hour' : `${hours} hours`}`,
});
} catch (error) {
console.error('Failed to snooze item:', error);
addToast({
type: 'error',
title: 'Snooze Failed',
message: 'Failed to snooze item. Please try again.',
});
}
},
[snoozeItem, addToast]
);
// Combined AI and socket status indicator
const systemStatus = socketConnected && aiStatus === 'ready' ? 'ready' :
itemsLoading ? 'loading' :
!socketConnected ? 'disconnected' : aiStatus;
const systemStatusLabel =
systemStatus === 'ready' ? 'System Ready' :
systemStatus === 'loading' ? 'Loading...' :
systemStatus === 'disconnected' ? 'Connecting...' :
systemStatus === 'initializing' ? 'Initializing...' :
systemStatus === 'error' ? 'System Error' : 'System Idle';
const systemStatusDot =
systemStatus === 'ready' ? 'bg-sage-400' :
systemStatus === 'loading' ? 'bg-amber-400 animate-pulse' :
systemStatus === 'disconnected' ? 'bg-amber-400 animate-pulse' :
systemStatus === 'initializing' ? 'bg-amber-400 animate-pulse' :
systemStatus === 'error' ? 'bg-coral-400' : 'bg-stone-600';
return (
<div className="min-h-full relative">
<div className="relative z-10 min-h-full flex flex-col items-center justify-center">
<div className="max-w-md mx-auto text-center px-6">
{/* Icon */}
<div className="w-16 h-16 mx-auto mb-6 rounded-2xl bg-white/[0.05] border border-white/[0.08] flex items-center justify-center">
<svg className="w-8 h-8 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<div className="relative z-10 min-h-full flex flex-col">
<div className="flex-1 p-6">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold text-white">Intelligence</h1>
{stats.total > 0 && (
<div className="text-xs bg-white/10 text-white px-2 py-1 rounded-full">
{stats.total}
</div>
)}
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${systemStatusDot}`} />
<span className="text-xs text-stone-400">{systemStatusLabel}</span>
</div>
</div>
<h1 className="text-2xl font-bold text-white mb-2">Intelligence</h1>
<p className="text-stone-400 text-sm mb-6">
AI-powered insights, memory, and analytics are coming soon.
</p>
{/* Filters */}
<div className="flex items-center gap-3 mb-6 animate-fade-up">
<div className="flex-1">
<input
type="text"
placeholder="Search actionable items..."
value={filters.search}
onChange={e => dispatch(setSearchFilter(e.target.value))}
className="w-full px-3 py-2 text-sm bg-white/5 border border-white/10 rounded-lg text-white placeholder-stone-500 focus:outline-none focus:border-primary-500/50 transition-colors"
/>
</div>
<select
value={filters.source}
onChange={e => dispatch(setSourceFilter(e.target.value as any))}
className="px-3 py-2 text-sm bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500/50 transition-colors">
<option value="all">All Sources</option>
<option value="email">Email</option>
<option value="calendar">Calendar</option>
<option value="telegram">Telegram</option>
<option value="ai_insight">AI Insights</option>
<option value="system">System</option>
<option value="trading">Trading</option>
<option value="security">Security</option>
</select>
</div>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary-500/10 border border-primary-500/20">
<div className="w-2 h-2 rounded-full bg-primary-400 animate-pulse" />
<span className="text-xs font-medium text-primary-400">Coming Soon</span>
{/* Content */}
{itemsLoading ? (
/* Loading State */
<div className="glass rounded-2xl p-8 text-center animate-fade-up">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-primary-500/10">
<div className="w-8 h-8 border-2 border-primary-400 border-t-transparent rounded-full animate-spin"></div>
</div>
<h2 className="text-lg font-semibold text-white mb-2">Loading Intelligence...</h2>
<p className="text-stone-400 text-sm">Fetching your actionable items</p>
</div>
) : itemsError ? (
/* Error State */
<div className="glass rounded-2xl p-8 text-center animate-fade-up">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-coral-500/10">
<svg
className="w-8 h-8 text-coral-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-white mb-2">Unable to load items</h2>
<p className="text-stone-400 text-sm mb-4">
{typeof itemsError === 'string' ? itemsError : 'Something went wrong'}
</p>
<button
onClick={() => refetchItems()}
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white text-sm rounded-lg transition-colors"
>
Try Again
</button>
</div>
) : timeGroups.length === 0 ? (
/* Empty State */
<div className="glass rounded-2xl p-8 text-center animate-fade-up">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-primary-500/10">
<svg
className="w-8 h-8 text-primary-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-white mb-2">All caught up!</h2>
<p className="text-stone-400 text-sm">
{filters.search || filters.source !== 'all'
? 'No items match your current filters.'
: 'No actionable items at the moment. Great work!'}
</p>
</div>
) : (
/* Time Groups */
<div className="space-y-6">
{timeGroups.map((group, groupIndex) => (
<div
key={group.label}
className="animate-fade-up"
style={{ animationDelay: `${groupIndex * 50}ms` }}>
{/* Group Header */}
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-white opacity-80">{group.label}</h2>
<div className="text-xs bg-white/10 text-white px-2 py-1 rounded-full">
{group.count}
</div>
</div>
{/* Items */}
<div className="space-y-3">
{group.items.map((item, itemIndex) => (
<div
key={item.id}
className="animate-fade-up"
style={{ animationDelay: `${groupIndex * 50 + itemIndex * 25}ms` }}>
<ActionableCard
item={item}
onComplete={handleComplete}
onDismiss={handleDismiss}
onSnooze={handleSnooze}
/>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Toast notifications */}
<ToastContainer notifications={toasts} onRemove={removeToast} />
{/* Confirmation modal */}
<ConfirmationModal
modal={confirmationModal}
onClose={() => setConfirmationModal(prev => ({ ...prev, isOpen: false }))}
/>
</div>
);
}
+67
View File
@@ -0,0 +1,67 @@
import type { ReactNode } from 'react';
import { createContext, useContext, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useIntelligenceSocketManager } from '../hooks/useIntelligenceSocket';
import { setInitialized, setConnectionStatus } from '../store/intelligenceSlice';
/**
* Intelligence context for managing system-wide Intelligence state
*/
interface IntelligenceContextValue {
isInitialized: boolean;
isConnected: boolean;
initialize: () => void;
}
const IntelligenceContext = createContext<IntelligenceContextValue | null>(null);
interface IntelligenceProviderProps {
children: ReactNode;
}
/**
* Intelligence Provider - manages Intelligence system initialization and state
*/
export function IntelligenceProvider({ children }: IntelligenceProviderProps) {
const dispatch = useDispatch();
const socketManager = useIntelligenceSocketManager();
// Initialize Intelligence system
useEffect(() => {
dispatch(setInitialized(true));
dispatch(setConnectionStatus(socketManager.isConnected ? 'connected' : 'connecting'));
}, [dispatch, socketManager.isConnected]);
// Monitor connection status
useEffect(() => {
if (socketManager.isConnected) {
dispatch(setConnectionStatus('connected'));
} else {
dispatch(setConnectionStatus('connecting'));
}
}, [dispatch, socketManager.isConnected]);
const contextValue: IntelligenceContextValue = {
isInitialized: true,
isConnected: socketManager.isConnected,
initialize: socketManager.connect,
};
return (
<IntelligenceContext.Provider value={contextValue}>
{children}
</IntelligenceContext.Provider>
);
}
/**
* Hook to access Intelligence context
*/
export function useIntelligenceContext() {
const context = useContext(IntelligenceContext);
if (!context) {
throw new Error('useIntelligenceContext must be used within IntelligenceProvider');
}
return context;
}
+177
View File
@@ -0,0 +1,177 @@
import type { ActionableItemStatus } from '../types/intelligence';
import { apiClient } from './apiClient';
/**
* Backend API response types for Intelligence system
*/
export interface BackendActionableItem {
id: string;
title: string;
description?: string;
source: string;
priority: string;
status: string;
createdAt: string;
updatedAt: string;
expiresAt?: string;
snoozeUntil?: string;
actionable: boolean;
requiresConfirmation?: boolean;
hasComplexAction?: boolean;
dismissedAt?: string;
completedAt?: string;
reminderCount?: number;
// Backend-specific fields
threadId?: string;
executionStatus?: 'idle' | 'running' | 'completed' | 'failed';
currentSessionId?: string;
}
export interface BackendChatMessage {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: string;
threadId: string;
}
export interface BackendThreadResponse {
threadId: string;
messages: BackendChatMessage[];
}
export interface BackendExecutionResponse {
executionId: string;
sessionId: string;
status: 'started' | 'running' | 'completed' | 'failed';
}
export interface ConnectedTool {
name: string;
description: string;
parameters: Record<string, any>;
skillId: string;
enabled: boolean;
}
/**
* Intelligence API Service - handles all REST API calls for the Intelligence system
*/
export class IntelligenceApiService {
/**
* Get all actionable items from the backend
*/
async getActionableItems(): Promise<BackendActionableItem[]> {
try {
const response = await apiClient.get<{ items: BackendActionableItem[] }>('/telegram/actionable-items');
return response.items || [];
} catch (error) {
console.error('Failed to fetch actionable items:', error);
throw error;
}
}
/**
* Update the status of an actionable item
*/
async updateItemStatus(itemId: string, status: ActionableItemStatus): Promise<void> {
try {
await apiClient.patch(`/actionable-items/${itemId}`, { status });
} catch (error) {
console.error('Failed to update item status:', error);
throw error;
}
}
/**
* Snooze an actionable item until a specific time
*/
async snoozeItem(itemId: string, snoozeUntil: Date): Promise<void> {
try {
await apiClient.patch(`/actionable-items/${itemId}`, {
status: 'snoozed',
snoozeUntil: snoozeUntil.toISOString()
});
} catch (error) {
console.error('Failed to snooze item:', error);
throw error;
}
}
/**
* Get or create a conversation thread for an actionable item
*/
async getOrCreateThread(itemId: string): Promise<BackendThreadResponse> {
try {
const response = await apiClient.get<BackendThreadResponse>(`/${itemId}/thread`);
return response;
} catch (error) {
console.error('Failed to get or create thread:', error);
throw error;
}
}
/**
* Get chat history for a specific thread
*/
async getChatHistory(threadId: string): Promise<BackendChatMessage[]> {
try {
const response = await apiClient.get<{ messages: BackendChatMessage[] }>(`/threads/${threadId}/messages`);
return response.messages || [];
} catch (error) {
console.error('Failed to get chat history:', error);
throw error;
}
}
/**
* Start task execution for an actionable item with connected tools
*/
async executeTask(itemId: string, connectedTools: ConnectedTool[]): Promise<BackendExecutionResponse> {
try {
const response = await apiClient.post<BackendExecutionResponse>(`/${itemId}/execute`, {
connectedTools
});
return response;
} catch (error) {
console.error('Failed to execute task:', error);
throw error;
}
}
/**
* Get execution status for a specific execution ID
*/
async getExecutionStatus(executionId: string): Promise<{
status: 'running' | 'completed' | 'failed';
progress?: any[];
result?: any;
}> {
try {
const response = await apiClient.get(`/executions/${executionId}/status`);
return response as {
status: 'running' | 'completed' | 'failed';
progress?: any[];
result?: any;
};
} catch (error) {
console.error('Failed to get execution status:', error);
throw error;
}
}
/**
* Cancel a running execution
*/
async cancelExecution(executionId: string): Promise<void> {
try {
await apiClient.post(`/executions/${executionId}/cancel`);
} catch (error) {
console.error('Failed to cancel execution:', error);
throw error;
}
}
}
// Export singleton instance
export const intelligenceApi = new IntelligenceApiService();
+2
View File
@@ -19,6 +19,7 @@ import aiReducer from './aiSlice';
import authReducer, { setOnboardedForUser, setToken } from './authSlice';
import daemonReducer from './daemonSlice';
import gmailReducer from './gmailSlice';
import intelligenceReducer from './intelligenceSlice';
import inviteReducer from './inviteSlice';
import notionReducer from './notionSlice';
import skillsReducer from './skillsSlice';
@@ -104,6 +105,7 @@ export const store = configureStore({
gmail: gmailReducer,
team: teamReducer,
thread: persistedThreadReducer,
intelligence: intelligenceReducer,
invite: inviteReducer,
notion: notionReducer,
},
+505
View File
@@ -0,0 +1,505 @@
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { intelligenceApi } from '../services/intelligenceApi';
import type {
ActionableItem,
ActionableItemSource,
ActionableItemStatus,
ChatMessage
} from '../types/intelligence';
import { transformBackendItemsToFrontend, transformBackendMessagesToFrontend } from '../utils/intelligenceTransforms';
/**
* Chat session state for managing individual conversations
*/
export interface ChatSessionState {
threadId: string;
itemId: string;
messages: ChatMessage[];
isTyping: boolean;
lastMessageId?: string;
isConnected: boolean;
}
/**
* Execution state for tracking task progress
*/
export interface ExecutionState {
executionId: string;
sessionId: string;
itemId: string;
status: 'idle' | 'starting' | 'running' | 'completed' | 'failed';
progress: Array<{
id: string;
label: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
timestamp?: Date;
}>;
result?: any;
error?: string;
}
/**
* Intelligence Redux state
*/
export interface IntelligenceState {
// Actionable Items
items: ActionableItem[];
loading: boolean;
error: string | null;
lastUpdate: Date | null;
// Chat Sessions
activeSessions: Record<string, ChatSessionState>;
currentChatSession: string | null;
// Execution Management
activeExecutions: Record<string, ExecutionState>;
// UI State
filters: {
source: ActionableItemSource | 'all';
priority: 'critical' | 'important' | 'normal' | 'all';
search: string;
};
// System state
initialized: boolean;
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
}
const initialState: IntelligenceState = {
// Actionable Items
items: [],
loading: false,
error: null,
lastUpdate: null,
// Chat Sessions
activeSessions: {},
currentChatSession: null,
// Execution Management
activeExecutions: {},
// UI State
filters: {
source: 'all',
priority: 'all',
search: '',
},
// System state
initialized: false,
connectionStatus: 'disconnected',
};
// Async thunks for API operations
/**
* Fetch actionable items from backend
*/
export const fetchActionableItems = createAsyncThunk(
'intelligence/fetchActionableItems',
async (_, { rejectWithValue }) => {
try {
const backendItems = await intelligenceApi.getActionableItems();
return transformBackendItemsToFrontend(backendItems);
} catch (error) {
return rejectWithValue(
error && typeof error === 'object' && 'error' in error
? error.error
: 'Failed to fetch actionable items'
);
}
}
);
/**
* Update item status
*/
export const updateItemStatus = createAsyncThunk(
'intelligence/updateItemStatus',
async (
{ itemId, status }: { itemId: string; status: ActionableItemStatus },
{ rejectWithValue }
) => {
try {
await intelligenceApi.updateItemStatus(itemId, status);
return { itemId, status, updatedAt: new Date() };
} catch (error) {
return rejectWithValue(
error && typeof error === 'object' && 'error' in error
? error.error
: 'Failed to update item status'
);
}
}
);
/**
* Snooze item until specific time
*/
export const snoozeItem = createAsyncThunk(
'intelligence/snoozeItem',
async (
{ itemId, snoozeUntil }: { itemId: string; snoozeUntil: Date },
{ rejectWithValue }
) => {
try {
await intelligenceApi.snoozeItem(itemId, snoozeUntil);
return { itemId, snoozeUntil, updatedAt: new Date() };
} catch (error) {
return rejectWithValue(
error && typeof error === 'object' && 'error' in error
? error.error
: 'Failed to snooze item'
);
}
}
);
/**
* Get or create chat session
*/
export const createChatSession = createAsyncThunk(
'intelligence/createChatSession',
async ({ itemId }: { itemId: string }, { rejectWithValue }) => {
try {
const threadResponse = await intelligenceApi.getOrCreateThread(itemId);
const messages = transformBackendMessagesToFrontend(threadResponse.messages);
return {
threadId: threadResponse.threadId,
itemId,
messages,
};
} catch (error) {
return rejectWithValue(
error && typeof error === 'object' && 'error' in error
? error.error
: 'Failed to create chat session'
);
}
}
);
/**
* Execute task with connected tools
*/
export const executeTask = createAsyncThunk(
'intelligence/executeTask',
async (
{ itemId, connectedTools }: { itemId: string; connectedTools: any[] },
{ rejectWithValue }
) => {
try {
const response = await intelligenceApi.executeTask(itemId, connectedTools);
return {
executionId: response.executionId,
sessionId: response.sessionId,
itemId,
status: response.status,
};
} catch (error) {
return rejectWithValue(
error && typeof error === 'object' && 'error' in error
? error.error
: 'Failed to execute task'
);
}
}
);
/**
* Intelligence slice
*/
export const intelligenceSlice = createSlice({
name: 'intelligence',
initialState,
reducers: {
// System actions
setInitialized: (state, action: PayloadAction<boolean>) => {
state.initialized = action.payload;
},
setConnectionStatus: (
state,
action: PayloadAction<'disconnected' | 'connecting' | 'connected' | 'error'>
) => {
state.connectionStatus = action.payload;
},
// Items actions
setItems: (state, action: PayloadAction<ActionableItem[]>) => {
state.items = action.payload;
state.lastUpdate = new Date();
},
addItem: (state, action: PayloadAction<ActionableItem>) => {
state.items.unshift(action.payload);
state.lastUpdate = new Date();
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(item => item.id !== action.payload);
state.lastUpdate = new Date();
},
// Filters actions
setSourceFilter: (state, action: PayloadAction<ActionableItemSource | 'all'>) => {
state.filters.source = action.payload;
},
setPriorityFilter: (state, action: PayloadAction<'critical' | 'important' | 'normal' | 'all'>) => {
state.filters.priority = action.payload;
},
setSearchFilter: (state, action: PayloadAction<string>) => {
state.filters.search = action.payload;
},
// Chat session actions
setChatSession: (
state,
action: PayloadAction<{
threadId: string;
itemId: string;
messages?: ChatMessage[];
}>
) => {
const { threadId, itemId, messages = [] } = action.payload;
state.activeSessions[threadId] = {
threadId,
itemId,
messages,
isTyping: false,
isConnected: true,
};
state.currentChatSession = threadId;
},
addMessage: (
state,
action: PayloadAction<{
threadId: string;
message: ChatMessage;
}>
) => {
const { threadId, message } = action.payload;
const session = state.activeSessions[threadId];
if (session) {
session.messages.push(message);
session.lastMessageId = message.id;
}
},
setTyping: (
state,
action: PayloadAction<{
threadId: string;
isTyping: boolean;
}>
) => {
const { threadId, isTyping } = action.payload;
const session = state.activeSessions[threadId];
if (session) {
session.isTyping = isTyping;
}
},
closeChatSession: (state, action: PayloadAction<string>) => {
const threadId = action.payload;
delete state.activeSessions[threadId];
if (state.currentChatSession === threadId) {
state.currentChatSession = null;
}
},
setCurrentChatSession: (state, action: PayloadAction<string | null>) => {
state.currentChatSession = action.payload;
},
// Execution actions
setExecution: (
state,
action: PayloadAction<{
executionId: string;
execution: ExecutionState;
}>
) => {
const { executionId, execution } = action.payload;
state.activeExecutions[executionId] = execution;
},
updateExecutionProgress: (
state,
action: PayloadAction<{
executionId: string;
progress: ExecutionState['progress'];
}>
) => {
const { executionId, progress } = action.payload;
const execution = state.activeExecutions[executionId];
if (execution) {
execution.progress = progress;
execution.status = 'running';
}
},
setExecutionResult: (
state,
action: PayloadAction<{
executionId: string;
result: any;
status: 'completed' | 'failed';
error?: string;
}>
) => {
const { executionId, result, status, error } = action.payload;
const execution = state.activeExecutions[executionId];
if (execution) {
execution.result = result;
execution.status = status;
execution.error = error;
}
},
clearExecution: (state, action: PayloadAction<string>) => {
const executionId = action.payload;
delete state.activeExecutions[executionId];
},
// Error handling
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
// Fetch actionable items
builder
.addCase(fetchActionableItems.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchActionableItems.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
state.lastUpdate = new Date();
state.error = null;
})
.addCase(fetchActionableItems.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
// Update item status
builder
.addCase(updateItemStatus.fulfilled, (state, action) => {
const { itemId, status, updatedAt } = action.payload;
const item = state.items.find(item => item.id === itemId);
if (item) {
item.status = status;
item.updatedAt = updatedAt;
// Set completion/dismissal timestamps
if (status === 'completed') {
item.completedAt = updatedAt;
} else if (status === 'dismissed') {
item.dismissedAt = updatedAt;
}
}
state.lastUpdate = new Date();
})
.addCase(updateItemStatus.rejected, (state, action) => {
state.error = action.payload as string;
});
// Snooze item
builder
.addCase(snoozeItem.fulfilled, (state, action) => {
const { itemId, snoozeUntil, updatedAt } = action.payload;
const item = state.items.find(item => item.id === itemId);
if (item) {
item.status = 'snoozed';
item.snoozeUntil = snoozeUntil;
item.updatedAt = updatedAt;
item.reminderCount = (item.reminderCount || 0) + 1;
}
state.lastUpdate = new Date();
})
.addCase(snoozeItem.rejected, (state, action) => {
state.error = action.payload as string;
});
// Create chat session
builder
.addCase(createChatSession.fulfilled, (state, action) => {
const { threadId, itemId, messages } = action.payload;
state.activeSessions[threadId] = {
threadId,
itemId,
messages,
isTyping: false,
isConnected: true,
};
state.currentChatSession = threadId;
// Update item with thread information
const item = state.items.find(item => item.id === itemId);
if (item) {
item.threadId = threadId;
}
})
.addCase(createChatSession.rejected, (state, action) => {
state.error = action.payload as string;
});
// Execute task
builder
.addCase(executeTask.fulfilled, (state, action) => {
const { executionId, sessionId, itemId, status } = action.payload;
state.activeExecutions[executionId] = {
executionId,
sessionId,
itemId,
status: status === 'started' ? 'running' : 'idle',
progress: [],
};
// Update item execution status
const item = state.items.find(item => item.id === itemId);
if (item) {
item.executionStatus = 'running';
item.currentSessionId = sessionId;
}
})
.addCase(executeTask.rejected, (state, action) => {
state.error = action.payload as string;
});
},
});
export const {
setInitialized,
setConnectionStatus,
setItems,
addItem,
removeItem,
setSourceFilter,
setPriorityFilter,
setSearchFilter,
setChatSession,
addMessage,
setTyping,
closeChatSession,
setCurrentChatSession,
setExecution,
updateExecutionProgress,
setExecutionResult,
clearExecution,
clearError,
} = intelligenceSlice.actions;
export default intelligenceSlice.reducer;
+390
View File
@@ -0,0 +1,390 @@
/**
* Intelligence Chat API Types
*
* TypeScript definitions for backend integration
* These types ensure consistency between frontend and backend implementations
*/
// ===== Core Types =====
export type ConversationFlow =
| 'discovery'
| 'planning'
| 'confirmation'
| 'execution'
| 'completion'
| 'auto_close';
export type MessageType =
| 'message'
| 'plan'
| 'progress'
| 'completion'
| 'discovery_question'
| 'confirmation_request';
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
export type SessionStatus = 'active' | 'executing' | 'completed' | 'failed';
export type WorkflowType =
| 'meeting_prep'
| 'email_response'
| 'system_task'
| 'document_analysis'
| 'calendar_management';
// ===== Request/Response Types =====
export interface CreateChatSessionRequest {
actionable_item_id: string;
user_id: string;
context: { item_type: WorkflowType; metadata: Record<string, any> };
}
export interface CreateChatSessionResponse {
success: true;
data: {
session_id: string;
initial_message: {
content: string;
type: MessageType;
options: string[];
context: Record<string, any>;
};
expected_flow: ConversationFlow[];
};
}
export interface ChatMessage {
id: string;
content: string;
sender: 'ai' | 'user';
timestamp: string; // ISO 8601
type: MessageType;
metadata?: Record<string, any>;
}
export interface ChatSessionDetails {
session_id: string;
status: SessionStatus;
current_flow: ConversationFlow;
messages: ChatMessage[];
context: Record<string, any>;
execution_id?: string;
}
export interface SendMessageRequest {
content: string;
sender: 'user';
current_flow: ConversationFlow;
context: Record<string, any>;
}
export interface SendMessageResponse {
success: true;
data: {
ai_response: {
content: string;
type: MessageType;
next_flow: ConversationFlow;
options: string[];
metadata: Record<string, any>;
};
should_execute: boolean;
execution_plan?: ExecutionPlan;
};
}
// ===== Execution Types =====
export interface ExecutionStep {
id: string;
label: string;
status: TaskStatus;
estimated_duration: number;
dependencies: string[];
started_at?: string; // ISO 8601
completed_at?: string; // ISO 8601
progress_percentage?: number;
result?: Record<string, any>;
}
export interface ExecutionPlan {
id: string;
steps: ExecutionStep[];
estimated_total_duration: number;
requirements: { gmail_access?: boolean; notion_access?: boolean; calendar_access?: boolean };
}
export interface StartExecutionRequest {
execution_plan_id: string;
confirmed: true;
modifications?: Record<string, any>;
}
export interface StartExecutionResponse {
success: true;
data: {
execution_id: string;
status: 'started';
estimated_duration: number;
steps: ExecutionStep[];
};
}
export interface ExecutionStatusResponse {
success: true;
data: {
execution_id: string;
status: 'running' | 'completed' | 'failed';
current_step?: { id: string; label: string; status: TaskStatus; progress_percentage: number };
completed_steps: string[];
results?: ExecutionResults;
};
}
export interface ExecutionResults {
summary: string;
artifacts: Artifact[];
metrics: {
total_duration: number;
emails_processed?: number;
documents_created?: number;
apis_called?: number;
};
}
export interface Artifact {
type: 'notion_doc' | 'email_draft' | 'calendar_event' | 'document' | 'link';
title: string;
url: string;
created_at: string; // ISO 8601
metadata: Record<string, any>;
}
// ===== WebSocket Event Types =====
export interface WebSocketMessage<T = any> {
type: string;
data: T;
timestamp?: string; // ISO 8601
}
// Tool definition for chat initialization
export interface ChatTool {
name: string;
description: string;
inputSchema: { type: 'object'; properties: Record<string, any>; required?: string[] };
}
// Client → Server Events
export interface ChatInitEvent {
type: 'chat:init';
data: { tools: ChatTool[]; sessionId?: string; timestamp: number };
}
export interface AuthenticateEvent {
type: 'authenticate';
data: { token: string; session_id: string };
}
export interface JoinSessionEvent {
type: 'join_session';
data: { session_id: string };
}
// Server → Client Events
export interface AuthenticatedEvent {
type: 'authenticated';
data: { user_id: string; session_id: string };
}
export interface ExecutionStepProgressEvent {
type: 'execution_step_progress';
data: {
session_id: string;
execution_id: string;
step_id: string;
status: TaskStatus;
progress_percentage: number;
message: string;
timestamp: string; // ISO 8601
};
}
export interface ExecutionCompleteEvent {
type: 'execution_complete';
data: {
session_id: string;
execution_id: string;
status: 'completed' | 'failed';
results: ExecutionResults;
};
}
export interface AIThinkingEvent {
type: 'ai_thinking';
data: { session_id: string; message: string; estimated_time: number };
}
export interface ErrorEvent {
type: 'error';
data: {
session_id: string;
error_code: ErrorCode;
message: string;
details: Record<string, any>;
retry_after?: number;
};
}
// ===== Error Types =====
export type ErrorCode =
| 'SESSION_NOT_FOUND'
| 'EXECUTION_FAILED'
| 'RATE_LIMITED'
| 'INVALID_INPUT'
| 'SOURCE_UNAVAILABLE'
| 'AI_UNAVAILABLE'
| 'TIMEOUT'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'INTERNAL_ERROR';
export interface APIError {
code: ErrorCode;
message: string;
details: Record<string, any>;
}
// ===== Standard API Response Envelope =====
export interface APIResponse<T = any> {
success: boolean;
data: T | null;
error: APIError | null;
meta: {
timestamp: string; // ISO 8601
request_id: string;
rate_limit: {
remaining: number;
reset_at: string; // ISO 8601
};
};
}
// ===== Meeting Preparation Specific Types =====
export interface MeetingPrepWorkflow {
workflow_type: 'meeting_preparation';
inputs: {
meeting_title: string;
participant: string;
time_context: string;
document_sources: ('gmail' | 'notion' | 'calendar')[];
output_format: 'notion_doc' | 'email_summary' | 'both';
};
processing_steps: MeetingPrepStep[];
}
export interface MeetingPrepStep {
step: 'fetch_gmail' | 'access_notion' | 'analyze_context' | 'consolidate_docs' | 'generate_link';
action: string;
params: Record<string, any>;
}
// ===== Service Integration Types =====
export interface ExternalServiceConfig {
gmail?: { enabled: boolean; scopes: string[]; rate_limit: number };
notion?: { enabled: boolean; workspace_id: string; rate_limit: number };
calendar?: { enabled: boolean; calendar_ids: string[] };
}
export interface ServiceAuthStatus {
gmail: 'connected' | 'disconnected' | 'error';
notion: 'connected' | 'disconnected' | 'error';
calendar: 'connected' | 'disconnected' | 'error';
}
// ===== Performance & Monitoring Types =====
export interface PerformanceMetrics {
response_time: number;
execution_time: number;
external_api_calls: number;
tokens_used: number;
cost_estimate: number;
}
export interface SessionMetrics {
session_id: string;
user_id: string;
started_at: string; // ISO 8601
ended_at?: string; // ISO 8601
total_messages: number;
execution_count: number;
performance: PerformanceMetrics;
satisfaction_score?: number;
}
// ===== Type Guards =====
export function isWebSocketMessage<T>(obj: any): obj is WebSocketMessage<T> {
return typeof obj === 'object' && obj !== null && typeof obj.type === 'string' && 'data' in obj;
}
export function isAPIResponse<T>(obj: any): obj is APIResponse<T> {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.success === 'boolean' &&
('data' in obj || 'error' in obj) &&
'meta' in obj
);
}
export function isExecutionStepProgressEvent(obj: any): obj is ExecutionStepProgressEvent {
return (
isWebSocketMessage(obj) &&
obj.type === 'execution_step_progress' &&
typeof obj.data === 'object' &&
obj.data !== null &&
'step_id' in obj.data &&
'progress_percentage' in obj.data
);
}
// ===== Utility Types =====
export type ChatEventHandler<T = any> = (event: WebSocketMessage<T>) => void;
export interface ChatClientConfig {
websocket_url: string;
api_base_url: string;
auth_token: string;
retry_attempts: number;
timeout_ms: number;
}
// ===== Frontend State Types =====
export interface ChatState {
session: ChatSessionDetails | null;
messages: ChatMessage[];
currentFlow: ConversationFlow;
isExecuting: boolean;
executionProgress: ExecutionStep[];
isConnected: boolean;
lastError: APIError | null;
}
export interface ChatActions {
sendMessage: (content: string) => Promise<void>;
startExecution: (planId: string) => Promise<void>;
connect: () => Promise<void>;
disconnect: () => void;
clearError: () => void;
}
+110
View File
@@ -0,0 +1,110 @@
// Intelligence System Types
// Actionable items and AI insights for the Intelligence page
import type React from 'react';
export type ActionableItemSource =
| 'email'
| 'calendar'
| 'telegram'
| 'ai_insight'
| 'system'
| 'trading'
| 'security';
export type ActionableItemPriority = 'critical' | 'important' | 'normal';
export type ActionableItemStatus = 'active' | 'dismissed' | 'completed' | 'snoozed';
export interface ActionableItem {
id: string;
title: string;
description?: string;
source: ActionableItemSource;
priority: ActionableItemPriority;
status: ActionableItemStatus;
createdAt: Date;
updatedAt: Date;
expiresAt?: Date;
snoozeUntil?: Date;
// Action metadata
actionable: boolean;
requiresConfirmation?: boolean;
hasComplexAction?: boolean;
// Visual presentation
icon?: React.ReactElement;
sourceLabel?: string;
// Interaction tracking
dismissedAt?: Date;
completedAt?: Date;
reminderCount?: number;
// Backend integration fields
threadId?: string;
executionStatus?: 'idle' | 'running' | 'completed' | 'failed';
currentSessionId?: string;
}
export interface ActionableItemAction {
type: 'complete' | 'dismiss' | 'snooze';
timestamp: Date;
itemId: string;
metadata?: Record<string, unknown>;
}
export interface TimeGroup {
label: string;
items: ActionableItem[];
count: number;
}
export interface IntelligencePageState {
items: ActionableItem[];
loading: boolean;
error: string | null;
lastUpdate: Date | null;
// UI state
showCompleted: boolean;
filter: ActionableItemSource | 'all';
}
// Snooze time options
export type SnoozeOption = {
label: string;
duration: number; // milliseconds
customTime?: Date;
};
// Toast notification types
export interface ToastNotification {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title: string;
message?: string;
duration?: number;
action?: { label: string; handler: () => void };
}
// Confirmation modal data
export interface ConfirmationModal {
isOpen: boolean;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
onCancel: () => void;
destructive?: boolean;
showDontShowAgain?: boolean;
}
// Chat message type
export interface ChatMessage {
id: string;
content: string;
sender: 'user' | 'ai';
timestamp: Date;
}
+249
View File
@@ -0,0 +1,249 @@
import type { MCPTool } from '../lib/mcp';
import type {
BackendActionableItem,
BackendChatMessage,
ConnectedTool
} from '../services/intelligenceApi';
import type {
ActionableItem,
ActionableItemPriority,
ActionableItemSource,
ActionableItemStatus,
ChatMessage
} from '../types/intelligence';
/**
* Transform backend actionable item to frontend format
*/
export function transformBackendItemToFrontend(backendItem: BackendActionableItem): ActionableItem {
return {
id: backendItem.id,
title: backendItem.title,
description: backendItem.description,
source: backendItem.source as ActionableItemSource,
priority: backendItem.priority as ActionableItemPriority,
status: backendItem.status as ActionableItemStatus,
createdAt: new Date(backendItem.createdAt),
updatedAt: new Date(backendItem.updatedAt),
expiresAt: backendItem.expiresAt ? new Date(backendItem.expiresAt) : undefined,
snoozeUntil: backendItem.snoozeUntil ? new Date(backendItem.snoozeUntil) : undefined,
actionable: backendItem.actionable,
requiresConfirmation: backendItem.requiresConfirmation,
hasComplexAction: backendItem.hasComplexAction,
dismissedAt: backendItem.dismissedAt ? new Date(backendItem.dismissedAt) : undefined,
completedAt: backendItem.completedAt ? new Date(backendItem.completedAt) : undefined,
reminderCount: backendItem.reminderCount,
// Backend integration fields
threadId: backendItem.threadId,
executionStatus: backendItem.executionStatus,
currentSessionId: backendItem.currentSessionId,
};
}
/**
* Transform multiple backend items to frontend format
*/
export function transformBackendItemsToFrontend(backendItems: BackendActionableItem[]): ActionableItem[] {
return backendItems.map(transformBackendItemToFrontend);
}
/**
* Transform backend chat message to frontend format
*/
export function transformBackendMessageToFrontend(backendMessage: BackendChatMessage): ChatMessage {
return {
id: backendMessage.id,
content: backendMessage.content,
sender: backendMessage.role === 'user' ? 'user' : 'ai',
timestamp: new Date(backendMessage.timestamp),
};
}
/**
* Transform multiple backend messages to frontend format
*/
export function transformBackendMessagesToFrontend(backendMessages: BackendChatMessage[]): ChatMessage[] {
return backendMessages.map(transformBackendMessageToFrontend);
}
/**
* Transform frontend chat message to backend format
*/
export function transformFrontendMessageToBackend(
message: ChatMessage,
threadId: string
): Omit<BackendChatMessage, 'id'> {
return {
content: message.content,
role: message.sender === 'user' ? 'user' : 'assistant',
timestamp: message.timestamp.toISOString(),
threadId,
};
}
/**
* Transform MCP tools to connected tools format for backend
*/
export function transformMCPToConnectedTools(mcpTools: MCPTool[]): ConnectedTool[] {
return mcpTools.map(tool => {
const [skillId, toolName] = tool.name.split('__');
return {
name: toolName || tool.name,
description: tool.description,
parameters: tool.inputSchema || {},
skillId: skillId || 'unknown',
enabled: true,
};
});
}
/**
* Transform connected tools back to MCP format
*/
export function transformConnectedToolsToMCP(connectedTools: ConnectedTool[]): MCPTool[] {
return connectedTools.map(tool => ({
name: `${tool.skillId}__${tool.name}`,
description: tool.description,
inputSchema: {
type: 'object',
properties: tool.parameters || {},
},
}));
}
/**
* Validate actionable item data from backend
*/
export function validateBackendItem(item: any): item is BackendActionableItem {
return (
typeof item === 'object' &&
item !== null &&
typeof item.id === 'string' &&
typeof item.title === 'string' &&
typeof item.source === 'string' &&
typeof item.priority === 'string' &&
typeof item.status === 'string' &&
typeof item.createdAt === 'string' &&
typeof item.updatedAt === 'string' &&
typeof item.actionable === 'boolean'
);
}
/**
* Create a new chat message in frontend format
*/
export function createChatMessage(
content: string,
sender: 'user' | 'ai',
id?: string
): ChatMessage {
return {
id: id || `${sender}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
content,
sender,
timestamp: new Date(),
};
}
/**
* Map execution status to user-friendly labels
*/
export function getExecutionStatusLabel(status?: string): string {
switch (status) {
case 'idle':
return 'Ready';
case 'running':
return 'In Progress';
case 'completed':
return 'Completed';
case 'failed':
return 'Failed';
default:
return 'Unknown';
}
}
/**
* Get priority display information
*/
export function getPriorityInfo(priority: ActionableItemPriority): {
label: string;
className: string;
color: string;
} {
switch (priority) {
case 'critical':
return {
label: 'Critical',
className: 'text-coral-400 bg-coral-500/10',
color: 'coral',
};
case 'important':
return {
label: 'Important',
className: 'text-amber-400 bg-amber-500/10',
color: 'amber',
};
case 'normal':
return {
label: 'Normal',
className: 'text-sage-400 bg-sage-500/10',
color: 'sage',
};
}
}
/**
* Get source display information
*/
export function getSourceInfo(source: ActionableItemSource): {
label: string;
icon: string;
className: string;
} {
switch (source) {
case 'email':
return {
label: 'Email',
icon: '📧',
className: 'text-blue-400 bg-blue-500/10',
};
case 'calendar':
return {
label: 'Calendar',
icon: '📅',
className: 'text-green-400 bg-green-500/10',
};
case 'telegram':
return {
label: 'Telegram',
icon: '💬',
className: 'text-blue-400 bg-blue-500/10',
};
case 'ai_insight':
return {
label: 'AI Insight',
icon: '🤖',
className: 'text-purple-400 bg-purple-500/10',
};
case 'system':
return {
label: 'System',
icon: '⚙️',
className: 'text-stone-400 bg-stone-500/10',
};
case 'trading':
return {
label: 'Trading',
icon: '📈',
className: 'text-yellow-400 bg-yellow-500/10',
};
case 'security':
return {
label: 'Security',
icon: '🔒',
className: 'text-red-400 bg-red-500/10',
};
}
}