mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(intelligence): add architecture diagram viewer (#2687)
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com> Co-authored-by: sanil-23 <sanil@vezures.xyz>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../test/test-utils';
|
||||
import DiagramViewerTab, { buildDiagramImageUrl } from './DiagramViewerTab';
|
||||
|
||||
vi.mock('../../utils/tauriCommands/config', () => ({
|
||||
openhumanGetDashboardSettings: vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
result: {
|
||||
diagram_viewer: {
|
||||
enabled: true,
|
||||
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
|
||||
refresh_interval_seconds: 10,
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('buildDiagramImageUrl', () => {
|
||||
it('adds a cache-busting refresh parameter to absolute URLs', () => {
|
||||
expect(buildDiagramImageUrl('http://localhost:8787/latest.png?format=png', 4)).toBe(
|
||||
'http://localhost:8787/latest.png?format=png&openhuman_refresh=4'
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a cache-busting refresh parameter to relative URLs', () => {
|
||||
expect(buildDiagramImageUrl('/workspace/diagrams/latest.png', 2)).toBe(
|
||||
'/workspace/diagrams/latest.png?openhuman_refresh=2'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DiagramViewerTab', () => {
|
||||
it('refreshes the diagram image URL on demand', async () => {
|
||||
renderWithProviders(<DiagramViewerTab />);
|
||||
|
||||
const image = await screen.findByRole('img', {
|
||||
name: 'Latest generated OpenHuman architecture diagram',
|
||||
});
|
||||
expect(image).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=0'));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh diagram' }));
|
||||
|
||||
expect(
|
||||
screen.getByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
|
||||
).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=1'));
|
||||
});
|
||||
|
||||
it('shows an empty state instead of a broken image after load failure', async () => {
|
||||
renderWithProviders(<DiagramViewerTab />);
|
||||
|
||||
const image = await screen.findByRole('img', {
|
||||
name: 'Latest generated OpenHuman architecture diagram',
|
||||
});
|
||||
fireEvent.error(image);
|
||||
|
||||
expect(screen.getByText('No diagram available yet')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('npx skills add yizhiyanhua-ai/fireworks-tech-graph')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { LuImage, LuRefreshCw } from 'react-icons/lu';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import {
|
||||
type DiagramViewerSettings,
|
||||
openhumanGetDashboardSettings,
|
||||
} from '../../utils/tauriCommands/config';
|
||||
|
||||
const DEFAULT_SETTINGS: DiagramViewerSettings = {
|
||||
enabled: true,
|
||||
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
|
||||
refresh_interval_seconds: 10,
|
||||
};
|
||||
|
||||
type ImageState = 'idle' | 'loaded' | 'error';
|
||||
|
||||
function normalizeSettings(
|
||||
settings?: Partial<DiagramViewerSettings> | null
|
||||
): DiagramViewerSettings {
|
||||
const sourceUrl = settings?.source_url?.trim() || DEFAULT_SETTINGS.source_url;
|
||||
const refreshInterval = Number(settings?.refresh_interval_seconds);
|
||||
|
||||
return {
|
||||
enabled: settings?.enabled ?? DEFAULT_SETTINGS.enabled,
|
||||
source_url: sourceUrl,
|
||||
refresh_interval_seconds:
|
||||
Number.isFinite(refreshInterval) && refreshInterval > 0
|
||||
? Math.round(refreshInterval)
|
||||
: DEFAULT_SETTINGS.refresh_interval_seconds,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDiagramImageUrl(sourceUrl: string, refreshKey: number): string {
|
||||
try {
|
||||
const url = new URL(sourceUrl);
|
||||
url.searchParams.set('openhuman_refresh', String(refreshKey));
|
||||
return url.toString();
|
||||
} catch {
|
||||
const separator = sourceUrl.includes('?') ? '&' : '?';
|
||||
return `${sourceUrl}${separator}openhuman_refresh=${refreshKey}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DiagramViewerTab() {
|
||||
const { t } = useT();
|
||||
const [settings, setSettings] = useState<DiagramViewerSettings>(DEFAULT_SETTINGS);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [imageState, setImageState] = useState<ImageState>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
|
||||
openhumanGetDashboardSettings()
|
||||
.then(response => {
|
||||
if (!alive) return;
|
||||
setSettings(normalizeSettings(response.result.diagram_viewer));
|
||||
})
|
||||
.catch(() => {
|
||||
if (!alive) return;
|
||||
setSettings(DEFAULT_SETTINGS);
|
||||
});
|
||||
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshDiagram = useCallback(() => {
|
||||
setImageState('idle');
|
||||
setRefreshKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings.enabled || settings.refresh_interval_seconds <= 0) return undefined;
|
||||
|
||||
const interval = window.setInterval(refreshDiagram, settings.refresh_interval_seconds * 1000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [refreshDiagram, settings.enabled, settings.refresh_interval_seconds]);
|
||||
|
||||
const sourceUrl = settings.source_url.trim();
|
||||
const imageUrl = useMemo(
|
||||
() => (sourceUrl ? buildDiagramImageUrl(sourceUrl, refreshKey) : ''),
|
||||
[refreshKey, sourceUrl]
|
||||
);
|
||||
|
||||
const showImage = settings.enabled && sourceUrl.length > 0 && imageState !== 'error';
|
||||
const showEmptyState = !settings.enabled || sourceUrl.length === 0 || imageState === 'error';
|
||||
|
||||
return (
|
||||
<section className="space-y-5" aria-labelledby="diagram-viewer-title">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2
|
||||
id="diagram-viewer-title"
|
||||
className="text-lg font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('intelligence.diagram.title')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-stone-500 dark:text-neutral-400">
|
||||
{t('intelligence.diagram.description')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={refreshDiagram}
|
||||
className="inline-flex items-center gap-2 rounded-md border border-stone-200 bg-white px-3 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
||||
aria-label={t('intelligence.diagram.refreshAria')}>
|
||||
<LuRefreshCw aria-hidden="true" className="h-4 w-4" />
|
||||
{t('intelligence.diagram.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showEmptyState && (
|
||||
<div className="flex min-h-72 flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-stone-300 bg-stone-50 px-6 py-10 text-center dark:border-neutral-700 dark:bg-neutral-950/60">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary-50 text-primary-600 dark:bg-primary-500/10 dark:text-primary-300">
|
||||
<LuImage aria-hidden="true" className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('intelligence.diagram.emptyTitle')}
|
||||
</h3>
|
||||
<p className="mt-1 max-w-md text-sm text-stone-500 dark:text-neutral-400">
|
||||
{t('intelligence.diagram.emptyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex max-w-full flex-col gap-2">
|
||||
<code className="max-w-full overflow-x-auto rounded-md bg-white px-3 py-2 text-xs text-stone-600 dark:bg-neutral-900 dark:text-neutral-300">
|
||||
{t('intelligence.diagram.skillInstallCommand')}
|
||||
</code>
|
||||
<code className="max-w-full overflow-x-auto rounded-md bg-white px-3 py-2 text-xs text-stone-600 dark:bg-neutral-900 dark:text-neutral-300">
|
||||
{t('intelligence.diagram.promptExample')}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showImage && (
|
||||
<figure className="space-y-3">
|
||||
<img
|
||||
key={imageUrl}
|
||||
src={imageUrl}
|
||||
alt={t('intelligence.diagram.imageAlt')}
|
||||
className="block w-full rounded-lg border border-stone-200 bg-white object-contain dark:border-neutral-800 dark:bg-neutral-950"
|
||||
onLoad={() => setImageState('loaded')}
|
||||
onError={() => setImageState('error')}
|
||||
/>
|
||||
<figcaption className="flex flex-wrap items-center justify-between gap-2 text-xs text-stone-500 dark:text-neutral-400">
|
||||
<span>
|
||||
{t('intelligence.diagram.refreshesEvery').replace(
|
||||
'{seconds}',
|
||||
String(settings.refresh_interval_seconds)
|
||||
)}
|
||||
</span>
|
||||
<span className="max-w-full truncate">{sourceUrl}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -178,6 +178,7 @@ const ar1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'اللاوعي',
|
||||
'memory.tab.dreams': 'الأحلام',
|
||||
'memory.tab.calls': 'المكالمات',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'الإعدادات',
|
||||
'memory.analyzeNow': 'تحليل الآن',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -102,6 +102,19 @@ const ar4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'مُسقَط',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'س ب ب ا ل ح ف ظ',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'محفوظ',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'نوع الكيان',
|
||||
'intelligence.screenDebug.active': 'نشط',
|
||||
'intelligence.screenDebug.app': 'التطبيق',
|
||||
|
||||
@@ -180,6 +180,7 @@ const bn1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'সাবকনশাস',
|
||||
'memory.tab.dreams': 'স্বপ্ন',
|
||||
'memory.tab.calls': 'কল',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'সেটিংস',
|
||||
'memory.analyzeNow': 'এখনই বিশ্লেষণ করুন',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const bn4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'বাদ দেওয়া হয়েছে',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'কে ন রা খা',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'রাখা হয়েছে',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'এনটিটি ধরন',
|
||||
'intelligence.screenDebug.active': 'সক্রিয়',
|
||||
'intelligence.screenDebug.app': 'অ্যাপ',
|
||||
|
||||
@@ -219,6 +219,7 @@ const de1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Unterbewusstsein',
|
||||
'memory.tab.dreams': 'Träume',
|
||||
'memory.tab.calls': 'Anrufe',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Einstellungen',
|
||||
'memory.analyzeNow': 'Jetzt analysieren',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const de4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'fallen gelassen',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'Warum hast du es behalten?',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'gehalten',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Entitätstyp',
|
||||
'intelligence.screenDebug.active': 'Aktiv',
|
||||
'intelligence.screenDebug.app': 'App',
|
||||
|
||||
@@ -487,6 +487,7 @@ const en1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconscious',
|
||||
'memory.tab.dreams': 'Dreams',
|
||||
'memory.tab.calls': 'Calls',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Settings',
|
||||
'memory.analyzeNow': 'Analyze Now',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -111,6 +111,19 @@ const en4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'dropped',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'w h y k e p t',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'kept',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Entity type',
|
||||
'intelligence.screenDebug.active': 'Active',
|
||||
'intelligence.screenDebug.app': 'App',
|
||||
|
||||
@@ -186,6 +186,7 @@ const es1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconsciente',
|
||||
'memory.tab.dreams': 'Sueños',
|
||||
'memory.tab.calls': 'Llamadas',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Configuración',
|
||||
'memory.analyzeNow': 'Analizar ahora',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const es4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'descartado',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'p o r q u é s e c o n s e r v ó',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'conservado',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Tipo de entidad',
|
||||
'intelligence.screenDebug.active': 'Activo',
|
||||
'intelligence.screenDebug.app': 'Aplicación',
|
||||
|
||||
@@ -186,6 +186,7 @@ const fr1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconscient',
|
||||
'memory.tab.dreams': 'Rêves',
|
||||
'memory.tab.calls': 'Appels',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Paramètres',
|
||||
'memory.analyzeNow': 'Analyser maintenant',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const fr4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'abandonné',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'p o u r q u o i c o n s e r v é',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'conservé',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': "Type d'entité",
|
||||
'intelligence.screenDebug.active': 'Actif',
|
||||
'intelligence.screenDebug.app': 'Application',
|
||||
|
||||
@@ -178,6 +178,7 @@ const hi1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'सबकॉन्शस',
|
||||
'memory.tab.dreams': 'ड्रीम्स',
|
||||
'memory.tab.calls': 'कॉल्स',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'सेटिंग्स',
|
||||
'memory.analyzeNow': 'अभी एनालाइज़ करें',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const hi4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'हटाया गया',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'क्यों रखा',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'रखा गया',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'इकाई प्रकार',
|
||||
'intelligence.screenDebug.active': 'एक्टिव',
|
||||
'intelligence.screenDebug.app': 'ऐप',
|
||||
|
||||
@@ -179,6 +179,7 @@ const id1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Bawah sadar',
|
||||
'memory.tab.dreams': 'Mimpi',
|
||||
'memory.tab.calls': 'Panggilan',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Pengaturan',
|
||||
'memory.analyzeNow': 'Analisis Sekarang',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const id4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'dibuang',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'm e n g a p a d i s i m p a n',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'dipertahankan',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Tipe entitas',
|
||||
'intelligence.screenDebug.active': 'Aktif',
|
||||
'intelligence.screenDebug.app': 'Aplikasi',
|
||||
|
||||
@@ -184,6 +184,7 @@ const it1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconscio',
|
||||
'memory.tab.dreams': 'Sogni',
|
||||
'memory.tab.calls': 'Chiamate',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Impostazioni',
|
||||
'memory.analyzeNow': 'Analizza ora',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const it4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'scartati',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'p e r c h é t e n u t i',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'tenuti',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Tipo di entità',
|
||||
'intelligence.screenDebug.active': 'Attivo',
|
||||
'intelligence.screenDebug.app': 'App',
|
||||
|
||||
@@ -178,6 +178,7 @@ const ko1: TranslationMap = {
|
||||
'memory.tab.subconscious': '잠재의식',
|
||||
'memory.tab.dreams': '꿈',
|
||||
'memory.tab.calls': '통화',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': '설정',
|
||||
'memory.analyzeNow': '지금 분석',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -94,6 +94,19 @@ const ko4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': '제외됨',
|
||||
'intelligence.memoryChunk.scoreBars.heading': '유지된 이유',
|
||||
'intelligence.memoryChunk.scoreBars.kept': '유지됨',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': '엔터티 유형',
|
||||
'intelligence.screenDebug.active': '활성',
|
||||
'intelligence.screenDebug.app': '앱',
|
||||
|
||||
@@ -185,6 +185,7 @@ const pt1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconsciente',
|
||||
'memory.tab.dreams': 'Sonhos',
|
||||
'memory.tab.calls': 'Chamadas',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Configurações',
|
||||
'memory.analyzeNow': 'Analisar Agora',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -104,6 +104,19 @@ const pt4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'descartado',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'p o r q u ê m a n t i d o',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'mantido',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Tipo de entidade',
|
||||
'intelligence.screenDebug.active': 'Ativo',
|
||||
'intelligence.screenDebug.app': 'Aplicativo',
|
||||
|
||||
@@ -179,6 +179,7 @@ const ru1: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Подсознание',
|
||||
'memory.tab.dreams': 'Сны',
|
||||
'memory.tab.calls': 'Звонки',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Настройки',
|
||||
'memory.analyzeNow': 'Анализировать сейчас',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -103,6 +103,19 @@ const ru4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'отброшено',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'п о ч е м у с о х р а н е н о',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'сохранено',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Тип сущности',
|
||||
'intelligence.screenDebug.active': 'Активно',
|
||||
'intelligence.screenDebug.app': 'Приложение',
|
||||
|
||||
@@ -174,6 +174,7 @@ const zhCN1: TranslationMap = {
|
||||
'memory.tab.subconscious': '潜意识',
|
||||
'memory.tab.dreams': '梦境',
|
||||
'memory.tab.calls': '调用记录',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': '设置',
|
||||
'memory.analyzeNow': '立即分析',
|
||||
'memoryTree.status.title': 'Memory Tree',
|
||||
|
||||
@@ -100,6 +100,19 @@ const zhCN4: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': '已丢弃',
|
||||
'intelligence.memoryChunk.scoreBars.heading': '保留原因',
|
||||
'intelligence.memoryChunk.scoreBars.kept': '已保留',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': '实体类型',
|
||||
'intelligence.screenDebug.active': '活跃',
|
||||
'intelligence.screenDebug.app': '应用',
|
||||
|
||||
@@ -295,6 +295,7 @@ const en: TranslationMap = {
|
||||
'memory.tab.subconscious': 'Subconscious',
|
||||
'memory.tab.dreams': 'Dreams',
|
||||
'memory.tab.calls': 'Calls',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.settings': 'Settings',
|
||||
'memory.analyzeNow': 'Analyze Now',
|
||||
|
||||
@@ -2431,6 +2432,19 @@ const en: TranslationMap = {
|
||||
'intelligence.memoryChunk.scoreBars.dropped': 'dropped',
|
||||
'intelligence.memoryChunk.scoreBars.heading': 'w h y k e p t',
|
||||
'intelligence.memoryChunk.scoreBars.kept': 'kept',
|
||||
'intelligence.diagram.title': 'Architecture Diagram',
|
||||
'intelligence.diagram.description':
|
||||
'Latest local architecture output from the configured diagram endpoint.',
|
||||
'intelligence.diagram.refresh': 'Refresh',
|
||||
'intelligence.diagram.refreshAria': 'Refresh diagram',
|
||||
'intelligence.diagram.emptyTitle': 'No diagram available yet',
|
||||
'intelligence.diagram.emptyDescription':
|
||||
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
|
||||
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
|
||||
'intelligence.diagram.promptExample':
|
||||
'Generate an architecture diagram of the current swarm in dark terminal style',
|
||||
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
|
||||
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
|
||||
'intelligence.memoryText.entityTypePrefix': 'Entity type',
|
||||
'intelligence.screenDebug.active': 'Active',
|
||||
'intelligence.screenDebug.app': 'App',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
|
||||
import DiagramViewerTab from '../components/intelligence/DiagramViewerTab';
|
||||
import IntelligenceCallsTab from '../components/intelligence/IntelligenceCallsTab';
|
||||
import IntelligenceDreamsTab from '../components/intelligence/IntelligenceDreamsTab';
|
||||
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
|
||||
@@ -19,7 +20,7 @@ import type {
|
||||
ToastNotification,
|
||||
} from '../types/intelligence';
|
||||
|
||||
type IntelligenceTab = 'memory' | 'subconscious' | 'calls' | 'dreams' | 'tasks';
|
||||
type IntelligenceTab = 'memory' | 'subconscious' | 'calls' | 'dreams' | 'tasks' | 'diagram';
|
||||
|
||||
export default function Intelligence() {
|
||||
const { t } = useT();
|
||||
@@ -91,6 +92,7 @@ export default function Intelligence() {
|
||||
{ id: 'memory', label: t('memory.tab.memory') },
|
||||
{ id: 'subconscious', label: t('memory.tab.subconscious') },
|
||||
{ id: 'tasks', label: 'Tasks' },
|
||||
{ id: 'diagram', label: t('memory.tab.diagram') },
|
||||
{ id: 'calls', label: t('memory.tab.calls') },
|
||||
{ id: 'dreams', label: t('memory.tab.dreams') },
|
||||
];
|
||||
@@ -170,6 +172,8 @@ export default function Intelligence() {
|
||||
|
||||
{activeTab === 'tasks' && <IntelligenceTasksTab />}
|
||||
|
||||
{activeTab === 'diagram' && <DiagramViewerTab />}
|
||||
|
||||
{activeTab === 'calls' && <IntelligenceCallsTab onToast={addToast} />}
|
||||
|
||||
{activeTab === 'dreams' && <IntelligenceDreamsTab />}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../test/test-utils';
|
||||
import Intelligence from '../Intelligence';
|
||||
|
||||
vi.mock('../../components/intelligence/MemoryWorkspace', () => ({
|
||||
MemoryWorkspace: () => <div>Memory workspace</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/intelligence/IntelligenceSubconsciousTab', () => ({
|
||||
default: () => <div>Subconscious tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/intelligence/IntelligenceTasksTab', () => ({
|
||||
default: () => <div>Tasks tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/intelligence/IntelligenceCallsTab', () => ({
|
||||
default: () => <div>Calls tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/intelligence/IntelligenceDreamsTab', () => ({
|
||||
default: () => <div>Dreams tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useConsciousItems', () => ({
|
||||
useConsciousItems: () => ({ isRunning: false }),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useIntelligenceStats', () => ({
|
||||
useIntelligenceStats: () => ({ aiStatus: 'ready' }),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useMemoryIngestionStatus', () => ({
|
||||
useMemoryIngestionStatus: () => ({ status: { running: false, queueDepth: 0 } }),
|
||||
}));
|
||||
|
||||
const connectMock = vi.fn();
|
||||
|
||||
vi.mock('../../hooks/useIntelligenceSocket', () => ({
|
||||
useIntelligenceSocket: () => ({ isConnected: true }),
|
||||
useIntelligenceSocketManager: () => ({ connect: connectMock }),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useSubconscious', () => ({
|
||||
useSubconscious: () => ({
|
||||
tasks: [],
|
||||
escalations: [],
|
||||
logEntries: [],
|
||||
status: 'idle',
|
||||
loading: false,
|
||||
triggering: false,
|
||||
triggerTick: vi.fn(),
|
||||
addTask: vi.fn(),
|
||||
removeTask: vi.fn(),
|
||||
toggleTask: vi.fn(),
|
||||
approveEscalation: vi.fn(),
|
||||
dismissEscalation: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Intelligence diagram tab', () => {
|
||||
it('shows an architecture diagram viewer from the Intelligence tabs', () => {
|
||||
renderWithProviders(<Intelligence />);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Diagram' }));
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Architecture Diagram' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Refresh diagram' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ export const CORE_RPC_METHODS = {
|
||||
configGetAnalyticsSettings: 'openhuman.config_get_analytics_settings',
|
||||
configGetAutonomySettings: 'openhuman.config_get_autonomy_settings',
|
||||
configGetComposioTriggerSettings: 'openhuman.config_get_composio_trigger_settings',
|
||||
configGetDashboardSettings: 'openhuman.config_get_dashboard_settings',
|
||||
configGetRuntimeFlags: 'openhuman.config_get_runtime_flags',
|
||||
configGetSearchSettings: 'openhuman.config_get_search_settings',
|
||||
configUpdateSearchSettings: 'openhuman.config_update_search_settings',
|
||||
@@ -53,6 +54,7 @@ export const LEGACY_METHOD_ALIASES: Record<string, CoreRpcMethod> = {
|
||||
'openhuman.tool_registry_call': CORE_RPC_METHODS.mcpClientsToolCall,
|
||||
'openhuman.get_analytics_settings': CORE_RPC_METHODS.configGetAnalyticsSettings,
|
||||
'openhuman.get_composio_trigger_settings': CORE_RPC_METHODS.configGetComposioTriggerSettings,
|
||||
'openhuman.get_dashboard_settings': CORE_RPC_METHODS.configGetDashboardSettings,
|
||||
'openhuman.get_config': CORE_RPC_METHODS.configGet,
|
||||
'openhuman.get_runtime_flags': CORE_RPC_METHODS.configGetRuntimeFlags,
|
||||
'openhuman.ping': CORE_RPC_METHODS.corePing,
|
||||
|
||||
@@ -450,6 +450,25 @@ export interface SearchSettings {
|
||||
allow_all: boolean;
|
||||
}
|
||||
|
||||
export interface DiagramViewerSettings {
|
||||
enabled: boolean;
|
||||
source_url: string;
|
||||
refresh_interval_seconds: number;
|
||||
}
|
||||
|
||||
export interface DashboardSettings {
|
||||
diagram_viewer: DiagramViewerSettings;
|
||||
}
|
||||
|
||||
export async function openhumanGetDashboardSettings(): Promise<CommandResponse<DashboardSettings>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
}
|
||||
return await callCoreRpc<CommandResponse<DashboardSettings>>({
|
||||
method: CORE_RPC_METHODS.configGetDashboardSettings,
|
||||
});
|
||||
}
|
||||
|
||||
export async function openhumanGetSearchSettings(): Promise<CommandResponse<SearchSettings>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
|
||||
@@ -36,6 +36,10 @@ const LEGACY_ALIASES: &[(&str, &str)] = &[
|
||||
"openhuman.get_composio_trigger_settings",
|
||||
"openhuman.config_get_composio_trigger_settings",
|
||||
),
|
||||
(
|
||||
"openhuman.get_dashboard_settings",
|
||||
"openhuman.config_get_dashboard_settings",
|
||||
),
|
||||
("openhuman.get_config", "openhuman.config_get"),
|
||||
(
|
||||
"openhuman.get_runtime_flags",
|
||||
|
||||
+16
-16
@@ -27,22 +27,22 @@ pub use schema::{
|
||||
set_runtime_proxy_config, AgentConfig, AuditConfig, AutocompleteConfig, AutonomyConfig,
|
||||
BrowserComputerUseConfig, BrowserConfig, CapabilityProviderConfig,
|
||||
CapabilityProviderTrustState, ChannelsConfig, ComposioConfig, Config, ContextConfig,
|
||||
CostConfig, CronConfig, CurlConfig, DelegateAgentConfig, DictationActivationMode,
|
||||
DictationConfig, DiscordConfig, DockerRuntimeConfig, EmbeddingRouteConfig, GitbooksConfig,
|
||||
HeartbeatConfig, HttpRequestConfig, IMessageConfig, IntegrationToggle, IntegrationsConfig,
|
||||
LarkConfig, LearningConfig, LlmBackend, LocalAiConfig, MatrixConfig, McpAuthConfig,
|
||||
McpClientConfig, McpClientIdentityConfig, McpServerConfig, MeetConfig, MemoryConfig,
|
||||
MemoryTreeConfig, ModelRouteConfig, MultimodalConfig, ObservabilityConfig,
|
||||
OrchestratorModelConfig, PolymarketClobCredentials, PolymarketConfig, ProxyConfig, ProxyScope,
|
||||
ReflectionSource, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig, SandboxBackend,
|
||||
SandboxConfig, SchedulerConfig, SchedulerGateConfig, SchedulerGateMode,
|
||||
ScreenIntelligenceConfig, SearchConfig, SearchEngine, SearchEngineCredentials, SearxngConfig,
|
||||
SecretsConfig, SecurityConfig, SlackConfig, StorageConfig, StorageProviderConfig,
|
||||
StorageProviderSection, StreamMode, TeamModelConfig, TelegramConfig, UpdateConfig,
|
||||
UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig, WebSearchConfig, WebhookConfig,
|
||||
DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1,
|
||||
MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE,
|
||||
SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
|
||||
CostConfig, CronConfig, CurlConfig, DashboardConfig, DelegateAgentConfig, DiagramViewerConfig,
|
||||
DictationActivationMode, DictationConfig, DiscordConfig, DockerRuntimeConfig,
|
||||
EmbeddingRouteConfig, GitbooksConfig, HeartbeatConfig, HttpRequestConfig, IMessageConfig,
|
||||
IntegrationToggle, IntegrationsConfig, LarkConfig, LearningConfig, LlmBackend, LocalAiConfig,
|
||||
MatrixConfig, McpAuthConfig, McpClientConfig, McpClientIdentityConfig, McpServerConfig,
|
||||
MeetConfig, MemoryConfig, MemoryTreeConfig, ModelRouteConfig, MultimodalConfig,
|
||||
ObservabilityConfig, OrchestratorModelConfig, PolymarketClobCredentials, PolymarketConfig,
|
||||
ProxyConfig, ProxyScope, ReflectionSource, ReliabilityConfig, ResourceLimitsConfig,
|
||||
RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SchedulerGateConfig,
|
||||
SchedulerGateMode, ScreenIntelligenceConfig, SearchConfig, SearchEngine,
|
||||
SearchEngineCredentials, SearxngConfig, SecretsConfig, SecurityConfig, SlackConfig,
|
||||
StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode, TeamModelConfig,
|
||||
TelegramConfig, UpdateConfig, UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig,
|
||||
WebSearchConfig, WebhookConfig, DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1,
|
||||
MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1,
|
||||
MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
|
||||
};
|
||||
pub use schema::{
|
||||
clear_active_user, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir,
|
||||
|
||||
@@ -1129,6 +1129,63 @@ pub async fn get_search_settings() -> Result<RpcOutcome<serde_json::Value>, Stri
|
||||
))
|
||||
}
|
||||
|
||||
/// Reads dashboard settings exposed to the desktop UI.
|
||||
pub async fn get_dashboard_settings() -> Result<RpcOutcome<serde_json::Value>, String> {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
tracing::debug!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
"OPENHUMAN: get_dashboard_settings entry"
|
||||
);
|
||||
tracing::debug!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
"OPENHUMAN: get_dashboard_settings loading config"
|
||||
);
|
||||
|
||||
let config = load_config_with_timeout().await.map_err(|error| {
|
||||
tracing::warn!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
error = %error,
|
||||
"OPENHUMAN: get_dashboard_settings config load failed"
|
||||
);
|
||||
error
|
||||
})?;
|
||||
|
||||
tracing::debug!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
"OPENHUMAN: get_dashboard_settings serializing dashboard settings"
|
||||
);
|
||||
let result = serde_json::to_value(&config.dashboard).map_err(|error| {
|
||||
let message = error.to_string();
|
||||
tracing::warn!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
error = %message,
|
||||
"OPENHUMAN: get_dashboard_settings serialization failed"
|
||||
);
|
||||
message
|
||||
})?;
|
||||
|
||||
tracing::debug!(
|
||||
target: "openhuman_core::config",
|
||||
request_id = %request_id,
|
||||
method = "openhuman.config_get_dashboard_settings",
|
||||
"OPENHUMAN: get_dashboard_settings exit"
|
||||
);
|
||||
Ok(RpcOutcome::new(
|
||||
result,
|
||||
vec!["dashboard settings read".to_string()],
|
||||
))
|
||||
}
|
||||
|
||||
/// Loads the configuration, applies browser settings updates, and saves it.
|
||||
pub async fn load_and_apply_browser_settings(
|
||||
update: BrowserSettingsPatch,
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn default_diagram_viewer_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_diagram_viewer_source_url() -> String {
|
||||
"http://localhost:8787/workspace/diagrams/latest.png".to_string()
|
||||
}
|
||||
|
||||
fn default_diagram_viewer_refresh_interval_seconds() -> u64 {
|
||||
10
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct DashboardConfig {
|
||||
pub event_stream: EventStreamConfig,
|
||||
#[serde(default)]
|
||||
pub model_health: ModelHealthConfig,
|
||||
#[serde(default)]
|
||||
pub diagram_viewer: DiagramViewerConfig,
|
||||
}
|
||||
|
||||
impl Default for DashboardConfig {
|
||||
@@ -14,6 +28,7 @@ impl Default for DashboardConfig {
|
||||
Self {
|
||||
event_stream: EventStreamConfig::default(),
|
||||
model_health: ModelHealthConfig::default(),
|
||||
diagram_viewer: DiagramViewerConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,10 +87,57 @@ impl Default for ModelHealthConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct DiagramViewerConfig {
|
||||
#[serde(default = "default_diagram_viewer_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_diagram_viewer_source_url")]
|
||||
pub source_url: String,
|
||||
#[serde(default = "default_diagram_viewer_refresh_interval_seconds")]
|
||||
pub refresh_interval_seconds: u64,
|
||||
}
|
||||
|
||||
impl Default for DiagramViewerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_diagram_viewer_enabled(),
|
||||
source_url: default_diagram_viewer_source_url(),
|
||||
refresh_interval_seconds: default_diagram_viewer_refresh_interval_seconds(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dashboard_config_defaults_enable_local_diagram_viewer() {
|
||||
let config = DashboardConfig::default();
|
||||
|
||||
assert!(config.diagram_viewer.enabled);
|
||||
assert_eq!(
|
||||
config.diagram_viewer.source_url,
|
||||
"http://localhost:8787/workspace/diagrams/latest.png"
|
||||
);
|
||||
assert_eq!(config.diagram_viewer.refresh_interval_seconds, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_viewer_partial_toml_uses_missing_defaults() {
|
||||
let config: DashboardConfig =
|
||||
toml::from_str("[diagram_viewer]\nsource_url = \"http://localhost:9000/latest.svg\"")
|
||||
.expect("dashboard config should deserialize");
|
||||
|
||||
assert!(config.diagram_viewer.enabled);
|
||||
assert_eq!(
|
||||
config.diagram_viewer.source_url,
|
||||
"http://localhost:9000/latest.svg"
|
||||
);
|
||||
assert_eq!(config.diagram_viewer.refresh_interval_seconds, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_health_defaults_match_spec() {
|
||||
let mh = ModelHealthConfig::default();
|
||||
|
||||
@@ -14,6 +14,7 @@ mod autonomy;
|
||||
mod capability_providers;
|
||||
mod channels;
|
||||
mod context;
|
||||
mod dashboard;
|
||||
mod defaults;
|
||||
mod dictation;
|
||||
mod heartbeat_cron;
|
||||
@@ -54,6 +55,7 @@ pub use channels::{
|
||||
TelegramConfig, WebhookConfig, WhatsAppConfig,
|
||||
};
|
||||
pub use context::ContextConfig;
|
||||
pub use dashboard::{DashboardConfig, DiagramViewerConfig, EventStreamConfig, ModelHealthConfig};
|
||||
pub use dictation::{DictationActivationMode, DictationConfig};
|
||||
pub use heartbeat_cron::{CronConfig, HeartbeatConfig};
|
||||
pub use identity_cost::{CostConfig, ModelPricing};
|
||||
@@ -92,7 +94,5 @@ pub use voice_providers::{
|
||||
generate_voice_provider_id, is_voice_slug_reserved, BuiltinVoiceProvider, SttApiStyle,
|
||||
TtsApiStyle, VoiceCapability, VoiceProviderCreds, BUILTIN_VOICE_PROVIDERS,
|
||||
};
|
||||
mod dashboard;
|
||||
pub use dashboard::{DashboardConfig, EventStreamConfig, ModelHealthConfig};
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
@@ -89,6 +89,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub observability: ObservabilityConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub dashboard: DashboardConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub autonomy: AutonomyConfig,
|
||||
|
||||
@@ -366,9 +369,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub chat_onboarding_completed: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub dashboard: DashboardConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub model_registry: Vec<ModelRegistryEntry>,
|
||||
}
|
||||
@@ -618,6 +618,7 @@ impl Default for Config {
|
||||
output_language: None,
|
||||
temperature_unsupported_models: default_temperature_unsupported_models(),
|
||||
observability: ObservabilityConfig::default(),
|
||||
dashboard: DashboardConfig::default(),
|
||||
autonomy: AutonomyConfig::default(),
|
||||
runtime: RuntimeConfig::default(),
|
||||
screen_intelligence: ScreenIntelligenceConfig::default(),
|
||||
@@ -680,7 +681,6 @@ impl Default for Config {
|
||||
meet: MeetConfig::default(),
|
||||
onboarding_completed: false,
|
||||
chat_onboarding_completed: false,
|
||||
dashboard: DashboardConfig::default(),
|
||||
model_registry: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
schemas("workspace_onboarding_flag_set"),
|
||||
schemas("update_analytics_settings"),
|
||||
schemas("get_analytics_settings"),
|
||||
schemas("get_dashboard_settings"),
|
||||
schemas("update_meet_settings"),
|
||||
schemas("get_meet_settings"),
|
||||
schemas("agent_server_status"),
|
||||
@@ -318,6 +319,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
schema: schemas("get_analytics_settings"),
|
||||
handler: handle_get_analytics_settings,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("get_dashboard_settings"),
|
||||
handler: handle_get_dashboard_settings,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("update_meet_settings"),
|
||||
handler: handle_update_meet_settings,
|
||||
@@ -769,6 +774,18 @@ pub fn schemas(function: &str) -> ControllerSchema {
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
"get_dashboard_settings" => ControllerSchema {
|
||||
namespace: "config",
|
||||
function: "get_dashboard_settings",
|
||||
description: "Read dashboard settings, including the local architecture diagram viewer.",
|
||||
inputs: vec![],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "dashboard",
|
||||
ty: TypeSchema::Json,
|
||||
comment: "Current [dashboard] config block.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
"update_meet_settings" => ControllerSchema {
|
||||
namespace: "config",
|
||||
function: "update_meet_settings",
|
||||
@@ -1321,6 +1338,10 @@ fn handle_get_analytics_settings(_params: Map<String, Value>) -> ControllerFutur
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_dashboard_settings(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async { to_json(config_rpc::get_dashboard_settings().await?) })
|
||||
}
|
||||
|
||||
fn handle_update_meet_settings(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
log::debug!("[config][rpc] update_meet_settings enter");
|
||||
|
||||
@@ -217,9 +217,10 @@ mod tests {
|
||||
let (_provider, model) = build_chat_runtime(&cfg).unwrap();
|
||||
assert_eq!(model, DEFAULT_CLOUD_LLM_MODEL);
|
||||
// build_chat_runtime resolves the "summarization" workload role,
|
||||
// which routes to the dedicated `summarization-v1` tier (PR #2690)
|
||||
// rather than the generic `reasoning-v1` fallback.
|
||||
assert_eq!(model, "summarization-v1");
|
||||
// which routes to the dedicated DEFAULT_CLOUD_LLM_MODEL
|
||||
// (`summarization-v1`, PR #2690) rather than the generic
|
||||
// `reasoning-v1` fallback.
|
||||
assert_eq!(model, DEFAULT_CLOUD_LLM_MODEL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -243,13 +243,10 @@ fn pop_response(
|
||||
routes: &mut HashMap<String, VecDeque<MockResponse>>,
|
||||
target: &str,
|
||||
) -> MockResponse {
|
||||
if let Some(response) = pop_from_queue(routes.get_mut(target)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let path_only = target.split('?').next().unwrap_or(target);
|
||||
if let Some(response) = pop_from_queue(routes.get_mut(path_only)) {
|
||||
return response;
|
||||
for key in route_lookup_keys(target) {
|
||||
if let Some(response) = pop_from_queue(routes.get_mut(&key)) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
MockResponse {
|
||||
@@ -259,6 +256,36 @@ fn pop_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn route_lookup_keys(target: &str) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
push_lookup_key(&mut keys, target.to_string());
|
||||
push_path_only_key(&mut keys, target);
|
||||
|
||||
if let Ok(url) = reqwest::Url::parse(target) {
|
||||
let mut normalized = url.path().to_string();
|
||||
if let Some(query) = url.query() {
|
||||
normalized.push('?');
|
||||
normalized.push_str(query);
|
||||
}
|
||||
push_lookup_key(&mut keys, normalized.clone());
|
||||
push_path_only_key(&mut keys, &normalized);
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
fn push_path_only_key(keys: &mut Vec<String>, target: &str) {
|
||||
if let Some(path_only) = target.split('?').next() {
|
||||
push_lookup_key(keys, path_only.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn push_lookup_key(keys: &mut Vec<String>, key: String) {
|
||||
if !keys.iter().any(|existing| existing == &key) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_from_queue(queue: Option<&mut VecDeque<MockResponse>>) -> Option<MockResponse> {
|
||||
let queue = queue?;
|
||||
if queue.len() <= 1 {
|
||||
@@ -281,6 +308,16 @@ fn reason_phrase(status: u16) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_lookup_keys_normalizes_absolute_targets() {
|
||||
let keys = route_lookup_keys("http://127.0.0.1:1234/nonce?user=0xabc");
|
||||
|
||||
assert!(keys.contains(&"http://127.0.0.1:1234/nonce?user=0xabc".to_string()));
|
||||
assert!(keys.contains(&"http://127.0.0.1:1234/nonce".to_string()));
|
||||
assert!(keys.contains(&"/nonce?user=0xabc".to_string()));
|
||||
assert!(keys.contains(&"/nonce".to_string()));
|
||||
}
|
||||
|
||||
fn parse_tool_output(result: &ToolResult) -> Value {
|
||||
serde_json::from_str::<Value>(&result.output()).expect("tool output should be valid json")
|
||||
}
|
||||
@@ -770,10 +807,9 @@ async fn place_order_happy_path_posts_signed_order() {
|
||||
.expect("wallet setup");
|
||||
|
||||
let mut routes = HashMap::new();
|
||||
routes.insert(
|
||||
format!("/nonce?user={user}"),
|
||||
vec![MockResponse::body(200, r#"{"nonce": 42}"#)],
|
||||
);
|
||||
let nonce_response = vec![MockResponse::body(200, r#"{"nonce": 42}"#)];
|
||||
routes.insert(format!("/nonce?user={user}"), nonce_response.clone());
|
||||
routes.insert("/nonce".to_string(), nonce_response);
|
||||
routes.insert(
|
||||
"/order".to_string(),
|
||||
vec![MockResponse::body(
|
||||
@@ -812,10 +848,11 @@ async fn place_order_happy_path_posts_signed_order() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let requests = captured.lock().unwrap().clone();
|
||||
assert!(
|
||||
!result.is_error,
|
||||
"expected success, got: {}",
|
||||
result.output()
|
||||
"expected success, got: {}\nobserved requests: {requests:#?}",
|
||||
result.output(),
|
||||
);
|
||||
let output = parse_tool_output(&result);
|
||||
assert_eq!(output["action"], "place_order");
|
||||
@@ -823,7 +860,6 @@ async fn place_order_happy_path_posts_signed_order() {
|
||||
assert_eq!(output["data"]["success"], true);
|
||||
assert_eq!(output["data"]["orderID"], "ord-test-1");
|
||||
|
||||
let requests = captured.lock().unwrap().clone();
|
||||
assert_eq!(requests.len(), 2);
|
||||
|
||||
let nonce_request = &requests[0];
|
||||
|
||||
Reference in New Issue
Block a user