/**
* CognitiveToolsPanel Component
*
* HUD panel providing AI-powered tools for analyzing and transforming memories.
* Includes Summarize, Transform, Find Similar, and Analyze actions with loading states.
*
* Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6
*/
import { useCallback, useState } from 'react';
import { getDefaultClient } from '../../api/client';
import { selectIsProcessing, useCognitiveStore } from '../../stores/cognitiveStore';
import { useGraphStore } from '../../stores/graphStore';
import type {
AssessConfidenceResponse,
DetectBiasResponse,
Memory,
SearchResultItem,
ThinkResponse,
} from '../../types/api';
// ============================================================================
// Types
// ============================================================================
export type CognitiveToolAction = 'summarize' | 'transform' | 'find-similar' | 'analyze';
export interface CognitiveToolsPanelProps {
/** The memory to perform actions on */
memory: Memory;
/** Callback when similar nodes are found (for highlighting in 3D view) */
onSimilarNodesFound?: (nodeIds: string[]) => void;
/** Callback when a result modal should be shown */
onShowResult?: (result: CognitiveToolResult) => void;
/** Additional CSS classes */
className?: string;
/** Layout mode - 'grid' for 2x2 grid, 'horizontal' for inline row */
layout?: 'grid' | 'horizontal';
}
export interface SummarizeResult {
type: 'summarize';
summary: string;
insights: string[];
confidence: number;
}
export interface TransformResult {
type: 'transform';
transformed: string;
mode: string;
}
export interface FindSimilarResult {
type: 'find-similar';
similarMemories: SearchResultItem[];
highlightedNodeIds: string[];
}
export interface AnalyzeResult {
type: 'analyze';
confidence: AssessConfidenceResponse;
bias: DetectBiasResponse;
}
export type CognitiveToolResult =
| SummarizeResult
| TransformResult
| FindSimilarResult
| AnalyzeResult;
// ============================================================================
// Constants
// ============================================================================
const TOOL_DESCRIPTIONS: Record<CognitiveToolAction, string> = {
summarize: 'Generate an AI summary of this memory',
transform: 'Transform the memory content',
'find-similar': 'Find semantically similar memories',
analyze: 'Analyze confidence and detect biases',
};
// ============================================================================
// Sub-Components
// ============================================================================
interface GlassPanelProps {
children: React.ReactNode;
className?: string;
}
/**
* Glassmorphism panel wrapper with cyan glow
* Requirements: 23.5
*/
function GlassPanel({ children, className = '' }: GlassPanelProps): React.ReactElement {
return (
<div
className={`rounded-xl ${className}`}
style={{
background: 'var(--theme-surface)',
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
border: '1px solid var(--theme-primary-glow)',
boxShadow: `
0 0 20px var(--theme-primary-glow),
0 0 40px var(--theme-primary-bg),
inset 0 0 30px var(--theme-primary-bg)
`,
}}
>
{children}
</div>
);
}
interface ToolButtonProps {
action: CognitiveToolAction;
label: string;
icon: React.ReactNode;
isLoading: boolean;
isDisabled: boolean;
onClick: () => void;
layout?: 'grid' | 'horizontal';
}
/**
* Individual tool button with loading state
* Requirements: 9.1, 9.6
*/
function ToolButton({
action,
label,
icon,
isLoading,
isDisabled,
onClick,
layout = 'grid',
}: ToolButtonProps): React.ReactElement {
const isHorizontal = layout === 'horizontal';
return (
<button
onClick={onClick}
disabled={isDisabled || isLoading}
title={TOOL_DESCRIPTIONS[action]}
className={`
flex items-center justify-center
rounded-lg
transition-all duration-200
${isHorizontal ? 'flex-row gap-1.5 px-3 py-1.5' : 'flex-col p-3'}
${
isDisabled || isLoading
? 'bg-ui-border/50 text-ui-text-muted cursor-not-allowed'
: 'bg-ui-border hover:bg-ui-accent-primary/20 text-ui-text-primary hover:text-ui-accent-primary'
}
`}
aria-label={`${label} - ${TOOL_DESCRIPTIONS[action]}`}
aria-busy={isLoading}
>
<div className="relative flex items-center justify-center">
{isLoading ? (
<LoadingSpinner size={isHorizontal ? 14 : 24} />
) : (
<span className={isHorizontal ? 'text-sm' : 'text-2xl'}>{icon}</span>
)}
</div>
<span className={`font-medium ${isHorizontal ? 'text-xs' : 'text-xs mt-1'}`}>{label}</span>
</button>
);
}
interface LoadingSpinnerProps {
size?: number;
}
/**
* Loading spinner component
* Requirements: 9.6
*/
function LoadingSpinner({ size = 24 }: LoadingSpinnerProps): React.ReactElement {
return (
<svg
className="animate-spin text-ui-accent-primary"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
}
// ============================================================================
// Icons
// ============================================================================
const SummarizeIcon = (): React.ReactElement => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h10M4 18h14" />
</svg>
);
const TransformIcon = (): React.ReactElement => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</svg>
);
const FindSimilarIcon = (): React.ReactElement => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
<path d="M11 8v6M8 11h6" />
</svg>
);
const AnalyzeIcon = (): React.ReactElement => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" />
</svg>
);
// ============================================================================
// Main Component
// ============================================================================
/**
* CognitiveToolsPanel - AI tools for memory analysis and transformation
*
* Features:
* - Summarize: Generate AI summary of memory content
* - Transform: Transform memory content (placeholder for future modes)
* - Find Similar: Find semantically similar memories and highlight in 3D view
* - Analyze: Assess confidence and detect biases
*
* Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6
*/
export function CognitiveToolsPanel({
memory,
onSimilarNodesFound,
onShowResult,
className = '',
layout = 'grid',
}: CognitiveToolsPanelProps): React.ReactElement {
// Local loading states for each action
const [loadingAction, setLoadingAction] = useState<CognitiveToolAction | null>(null);
const [error, setError] = useState<string | null>(null);
const [lastFailedAction, setLastFailedAction] = useState<CognitiveToolAction | null>(null);
// Global cognitive store state
const isGlobalProcessing = useCognitiveStore(selectIsProcessing);
const startOperation = useCognitiveStore((state) => state.startOperation);
const completeOperation = useCognitiveStore((state) => state.completeOperation);
const failOperation = useCognitiveStore((state) => state.failOperation);
// Graph store for highlighting similar nodes
const visibleNodes = useGraphStore((state) => state.visibleNodes);
/**
* Handle Summarize action
* Requirements: 9.2
*/
const handleSummarize = useCallback(async () => {
setLoadingAction('summarize');
setError(null);
setLastFailedAction(null);
const operationId = startOperation('think', memory.content, 'analytical');
try {
const client = getDefaultClient();
const response: ThinkResponse = await client.think({
input: `Summarize the following memory content and extract key insights:\n\n${memory.content}`,
mode: 'analytical',
userId: memory.userId,
context: `This is a ${memory.primarySector} memory with salience ${String(memory.salience)} and strength ${String(memory.strength)}.`,
});
const result: SummarizeResult = {
type: 'summarize',
summary: response.conclusion,
insights: response.thoughts.map((t) => t.content),
confidence: response.confidence,
};
completeOperation(operationId, { type: 'think', data: response });
onShowResult?.(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to summarize memory';
setError(message);
setLastFailedAction('summarize');
failOperation(operationId, message);
} finally {
setLoadingAction(null);
}
}, [memory, startOperation, completeOperation, failOperation, onShowResult]);
/**
* Handle Transform action (placeholder for future implementation)
*/
const handleTransform = useCallback(async () => {
setLoadingAction('transform');
setError(null);
setLastFailedAction(null);
const operationId = startOperation('think', memory.content, 'creative');
try {
const client = getDefaultClient();
const response: ThinkResponse = await client.think({
input: `Transform and enhance the following memory content with creative insights:\n\n${memory.content}`,
mode: 'creative',
userId: memory.userId,
});
const result: TransformResult = {
type: 'transform',
transformed: response.conclusion,
mode: 'creative',
};
completeOperation(operationId, { type: 'think', data: response });
onShowResult?.(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to transform memory';
setError(message);
setLastFailedAction('transform');
failOperation(operationId, message);
} finally {
setLoadingAction(null);
}
}, [memory, startOperation, completeOperation, failOperation, onShowResult]);
/**
* Handle Find Similar action
* Requirements: 9.3
*/
const handleFindSimilar = useCallback(async () => {
setLoadingAction('find-similar');
setError(null);
setLastFailedAction(null);
try {
const client = getDefaultClient();
const response = await client.searchMemories({
userId: memory.userId,
text: memory.content,
minSimilarity: 0.5,
limit: 10,
});
// Filter out the current memory and get IDs
const similarMemories = response.results.filter((r) => r.id !== memory.id);
const highlightedNodeIds = similarMemories.map((r) => r.id);
// Filter to only include nodes that are visible in the 3D view
const visibleHighlightedIds = highlightedNodeIds.filter((id) => visibleNodes.has(id));
const result: FindSimilarResult = {
type: 'find-similar',
similarMemories,
highlightedNodeIds: visibleHighlightedIds,
};
// Notify parent to highlight nodes in 3D view
onSimilarNodesFound?.(visibleHighlightedIds);
onShowResult?.(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to find similar memories';
setError(message);
setLastFailedAction('find-similar');
} finally {
setLoadingAction(null);
}
}, [memory, visibleNodes, onSimilarNodesFound, onShowResult]);
/**
* Handle Analyze action
* Requirements: 9.4, 9.5, 30.4
* Uses the unified /api/v1/metacognition/analyze endpoint for both confidence and bias
*/
const handleAnalyze = useCallback(async () => {
setLoadingAction('analyze');
setError(null);
setLastFailedAction(null);
const operationId = startOperation('analyze', memory.content);
try {
const client = getDefaultClient();
// Use the unified metacognition analyze endpoint (Requirement 30.4)
// This returns both confidence and bias data in a single call
const response = await client.analyzeMetacognition({
reasoningChain: memory.content,
context: `${memory.primarySector} memory with salience ${String(memory.salience)} and strength ${String(memory.strength)}`,
});
const result: AnalyzeResult = {
type: 'analyze',
confidence: response.confidence,
bias: response.biases,
};
completeOperation(operationId, { type: 'analyze_metacognition', data: response });
onShowResult?.(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to analyze memory';
setError(message);
setLastFailedAction('analyze');
failOperation(operationId, message);
} finally {
setLoadingAction(null);
}
}, [memory, startOperation, completeOperation, failOperation, onShowResult]);
/**
* Handle retry of the last failed action
* Requirements: 30.5
*/
const handleRetry = useCallback((): void => {
if (lastFailedAction === null) return;
switch (lastFailedAction) {
case 'summarize':
void handleSummarize();
break;
case 'transform':
void handleTransform();
break;
case 'find-similar':
void handleFindSimilar();
break;
case 'analyze':
void handleAnalyze();
break;
}
}, [lastFailedAction, handleSummarize, handleTransform, handleFindSimilar, handleAnalyze]);
// Determine if any action is loading
const isAnyLoading = loadingAction !== null || isGlobalProcessing;
const isHorizontal = layout === 'horizontal';
const toolButtons = (
<div className={isHorizontal ? 'flex items-center gap-2' : 'grid grid-cols-2 gap-2'}>
<ToolButton
action="summarize"
label="Summarize"
icon={<SummarizeIcon />}
isLoading={loadingAction === 'summarize'}
isDisabled={isAnyLoading && loadingAction !== 'summarize'}
onClick={() => void handleSummarize()}
layout={layout}
/>
<ToolButton
action="transform"
label="Transform"
icon={<TransformIcon />}
isLoading={loadingAction === 'transform'}
isDisabled={isAnyLoading && loadingAction !== 'transform'}
onClick={() => void handleTransform()}
layout={layout}
/>
<ToolButton
action="find-similar"
label={isHorizontal ? 'Similar' : 'Find Similar'}
icon={<FindSimilarIcon />}
isLoading={loadingAction === 'find-similar'}
isDisabled={isAnyLoading && loadingAction !== 'find-similar'}
onClick={() => void handleFindSimilar()}
layout={layout}
/>
<ToolButton
action="analyze"
label="Analyze"
icon={<AnalyzeIcon />}
isLoading={loadingAction === 'analyze'}
isDisabled={isAnyLoading && loadingAction !== 'analyze'}
onClick={() => void handleAnalyze()}
layout={layout}
/>
</div>
);
// Horizontal layout - simple div wrapper
if (isHorizontal) {
return (
<div className={className}>
{error !== null && (
<div className="mb-0 mr-2 p-2 bg-red-500/20 border border-red-500/50 rounded">
<p className="text-red-400 text-xs">Server unavailable</p>
</div>
)}
{toolButtons}
</div>
);
}
// Grid layout - GlassPanel wrapper
return (
<GlassPanel className={`p-4 ${className}`}>
<h3 className="text-sm font-semibold neon-text-subtle mb-3">AI Tools</h3>
{error !== null && (
<div className="mb-3 p-2 bg-red-500/20 border border-red-500/50 rounded">
<p className="text-red-400 text-xs mb-2">
{error.includes('EAGAIN') || error.includes('ECONNREFUSED') || error.includes('fetch')
? 'Backend server not available. Start ThoughtMCP server to use AI tools.'
: error}
</p>
{lastFailedAction !== null && (
<button
onClick={handleRetry}
disabled={isAnyLoading}
className="text-xs px-2 py-1 bg-red-500/30 hover:bg-red-500/50 text-red-300 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Retry {lastFailedAction.replace('-', ' ')}
</button>
)}
</div>
)}
{toolButtons}
{isGlobalProcessing && loadingAction === null && (
<div className="mt-3 flex items-center justify-center text-ui-text-secondary text-xs">
<LoadingSpinner size={16} />
<span className="ml-2">Processing...</span>
</div>
)}
</GlassPanel>
);
}
export default CognitiveToolsPanel;