Files
openhuman/app/src/pages/Intelligence.tsx
T

258 lines
9.7 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import IntelligenceAgentsTab from '../components/intelligence/IntelligenceAgentsTab';
import IntelligenceAgentWorkTab from '../components/intelligence/IntelligenceAgentWorkTab';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab';
import MemorySection from '../components/intelligence/MemorySection';
import ModelCouncilTab from '../components/intelligence/ModelCouncilTab';
import { ToastContainer } from '../components/intelligence/Toast';
import WorkflowsTab from '../components/intelligence/WorkflowsTab';
import PillTabBar from '../components/PillTabBar';
import { useDeveloperMode } from '../hooks/useDeveloperMode';
import {
useIntelligenceSocket,
useIntelligenceSocketManager,
} from '../hooks/useIntelligenceSocket';
import { useSubconscious } from '../hooks/useSubconscious';
import { useT } from '../lib/i18n/I18nContext';
import type {
ConfirmationModal as ConfirmationModalType,
ToastNotification,
} from '../types/intelligence';
type IntelligenceTab =
| 'memory'
| 'subconscious'
| 'tasks'
| 'agent-work'
| 'agents'
| 'workflows'
| 'council';
const INTELLIGENCE_TABS: IntelligenceTab[] = [
'memory',
'subconscious',
'tasks',
'agent-work',
'agents',
'workflows',
'council',
];
// Tabs gated to dev builds or runtime developer mode (mirrors the `devOnly`
// flags on `allTabs` below). A `?tab=` deep link must be validated against the
// *visible* set, not the raw enum, so a user cannot force-open a hidden tab.
const DEV_ONLY_TABS: IntelligenceTab[] = ['council'];
const makeIsVisibleTab =
(developerModeEnabled: boolean) =>
(tab: string | null | undefined): tab is IntelligenceTab =>
(INTELLIGENCE_TABS as string[]).includes(tab ?? '') &&
(developerModeEnabled || !(DEV_ONLY_TABS as string[]).includes(tab ?? ''));
export default function Intelligence() {
const { t } = useT();
const developerMode = useDeveloperMode();
const isVisibleTab = makeIsVisibleTab(developerMode);
// Tab is URL-backed (`/intelligence?tab=…`) so navigating away — e.g. to
// Settings → Task Sources from the Agent Tasks tab — and coming back via
// browser-back restores the same tab instead of resetting to Memory.
// `replace` so switching tabs doesn't stack history entries.
const [searchParams, setSearchParams] = useSearchParams();
const tabParam = searchParams.get('tab');
const activeTab: IntelligenceTab = isVisibleTab(tabParam) ? tabParam : 'tasks';
const setActiveTab = useCallback(
(tab: IntelligenceTab) => {
setSearchParams(
prev => {
prev.set('tab', tab);
return prev;
},
{ replace: true }
);
},
[setSearchParams]
);
// The legacy header pills (system-status + Ingesting/Queued chips) were
// sourced from `useConsciousItems` + `useMemoryIngestionStatus`. They are
// replaced by the Memory Tree status panel (#1856 Part 1), rendered inside
// `MemoryWorkspace`, which polls `memory_tree_pipeline_status` for a much
// richer dashboard. The hooks themselves still exist for any future
// consumers / tests; we just no longer feed them into a half-baked pill
// up here.
// useUpdateActionableItem / useSnoozeActionableItem hooks were the
// mutations behind handleComplete / Dismiss / Snooze. Removed along
// with those handlers since the Memory tab no longer renders the
// actionable-card surface.
// Subconscious engine data
const {
status: subconsciousEngineStatus,
mode: subconsciousMode,
intervalMinutes: subconsciousInterval,
triggering: subconsciousTriggering,
settingMode: subconsciousSettingMode,
triggerTick,
setMode: setSubconsciousMode,
setIntervalMinutes: setSubconsciousInterval,
} = useSubconscious();
// 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: () => {},
});
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));
}, []);
// Initialize socket connection
useEffect(() => {
if (!socketConnected) {
socketManager.connect();
}
}, [socketConnected, socketManager]);
const allTabs: {
id: IntelligenceTab;
label: string;
description?: string;
comingSoon?: boolean;
devOnly?: boolean;
}[] = [
{ id: 'tasks', label: t('memory.tab.tasks'), description: t('memory.tab.tasksDescription') },
{
id: 'agent-work',
label: t('memory.tab.agentWork'),
description: t('memory.tab.agentWorkDescription'),
},
{ id: 'memory', label: t('memory.tab.memory') },
{ id: 'subconscious', label: t('memory.tab.subconscious') },
{
id: 'workflows',
label: t('memory.tab.workflows'),
description: t('memory.tab.workflowsDescription'),
},
{ id: 'council', label: t('memory.tab.council'), devOnly: true },
{ id: 'agents', label: t('memory.tab.agents'), description: t('memory.tab.agentsDescription') },
];
const tabs = allTabs.filter(tab => !tab.devOnly || developerMode);
const activeTabDef = tabs.find(tab => tab.id === activeTab);
return (
<div className="min-h-full p-4 pt-6">
<div className="max-w-4xl mx-auto space-y-4">
<PillTabBar
items={tabs.map(tab => ({ label: tab.label, value: tab.id }))}
selected={activeTab}
onChange={setActiveTab}
activeClassName="border-primary-600 bg-primary-600 text-white"
renderItem={(item, active) => {
const tab = tabs.find(entry => entry.id === item.value);
return (
<span className="inline-flex items-center gap-1.5">
<span>{item.label}</span>
{tab?.comingSoon && (
<span
className={`rounded-full border px-1.5 py-0.5 text-[10px] ${
active
? 'border-white/30 bg-white/15 text-white'
: 'border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 text-stone-500 dark:text-neutral-400'
}`}>
{t('misc.beta')}
</span>
)}
</span>
);
}}
/>
<div className="bg-white dark:bg-neutral-900 rounded-2xl shadow-soft border border-stone-200 dark:border-neutral-800 p-6">
<div>
{/* Header — reflects the active tab so the panel title matches
what's shown below it (e.g. "Agent Tasks" on the Tasks tab),
rather than a static "Memory". */}
<div className="flex items-center justify-between mb-6">
<div className="min-w-0">
<h1
className="text-xl font-bold text-stone-900 dark:text-neutral-100"
data-walkthrough="intelligence-header">
{activeTabDef?.label ?? t('memory.title')}
</h1>
{activeTabDef?.description && (
<p className="mt-1 text-sm text-stone-500 dark:text-neutral-400">
{activeTabDef.description}
</p>
)}
{/* Header count badge was sourced from `stats.total` which
in turn came from the legacy actionable-items pipeline
(`filterItems(items, ...)`). The Memory tab now mounts
`MemoryWorkspace`, which renders chunks from
`memory_tree` and has nothing to do with that pipeline,
so the badge would have shown a count that no longer
matches anything visible. Hidden until a memory_tree
-native count signal is exposed. */}
</div>
</div>
{/* Tab content */}
{activeTab === 'memory' && <MemorySection onToast={addToast} />}
{activeTab === 'subconscious' && (
<IntelligenceSubconsciousTab
status={subconsciousEngineStatus}
mode={subconsciousMode}
intervalMinutes={subconsciousInterval}
triggerTick={triggerTick}
triggering={subconsciousTriggering}
settingMode={subconsciousSettingMode}
setMode={setSubconsciousMode}
setIntervalMinutes={setSubconsciousInterval}
/>
)}
{activeTab === 'tasks' && <IntelligenceTasksTab />}
{activeTab === 'agent-work' && <IntelligenceAgentWorkTab />}
{activeTab === 'agents' && <IntelligenceAgentsTab />}
{activeTab === 'workflows' && <WorkflowsTab />}
{activeTab === 'council' && <ModelCouncilTab />}
</div>
</div>
</div>
{/* Toast notifications */}
<ToastContainer notifications={toasts} onRemove={removeToast} />
{/* Confirmation modal */}
<ConfirmationModal
modal={confirmationModal}
onClose={() => setConfirmationModal(prev => ({ ...prev, isOpen: false }))}
/>
</div>
);
}