Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions app/src/components/intelligence/DiagramViewerTab.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
160 changes: 160 additions & 0 deletions app/src/components/intelligence/DiagramViewerTab.tsx
Original file line number Diff line number Diff line change
@@ -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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Manual {seconds} substitution is fragile — a translator typo on the placeholder breaks this silently. If there's a standard interpolation helper in useT() or the i18n layer, use that instead. If not, at least assert the placeholder exists in the string in a test.

{t('intelligence.diagram.refreshesEvery').replace(
'{seconds}',
String(settings.refresh_interval_seconds)
)}
</span>
<span className="max-w-full truncate">{sourceUrl}</span>
</figcaption>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</figure>
)}
</section>
);
}
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/ar-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/chunks/ar-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': 'التطبيق',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/bn-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/chunks/bn-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': 'অ্যাপ',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/de-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/chunks/de-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/en-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,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',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/chunks/en-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/es-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/chunks/es-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/chunks/fr-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading