Skip to content

Commit 3f5114d

Browse files
feat(intelligence): add architecture diagram viewer
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com>
1 parent 87f8ef4 commit 3f5114d

44 files changed

Lines changed: 724 additions & 22 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import DiagramViewerTab, { buildDiagramImageUrl } from './DiagramViewerTab';
5+
6+
vi.mock('../../utils/tauriCommands/config', () => ({
7+
openhumanGetDashboardSettings: vi
8+
.fn()
9+
.mockResolvedValue({
10+
result: {
11+
diagram_viewer: {
12+
enabled: true,
13+
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
14+
refresh_interval_seconds: 10,
15+
},
16+
},
17+
logs: [],
18+
}),
19+
}));
20+
21+
describe('buildDiagramImageUrl', () => {
22+
it('adds a cache-busting refresh parameter to absolute URLs', () => {
23+
expect(buildDiagramImageUrl('http://localhost:8787/latest.png?format=png', 4)).toBe(
24+
'http://localhost:8787/latest.png?format=png&openhuman_refresh=4'
25+
);
26+
});
27+
28+
it('adds a cache-busting refresh parameter to relative URLs', () => {
29+
expect(buildDiagramImageUrl('/workspace/diagrams/latest.png', 2)).toBe(
30+
'/workspace/diagrams/latest.png?openhuman_refresh=2'
31+
);
32+
});
33+
});
34+
35+
describe('DiagramViewerTab', () => {
36+
it('refreshes the diagram image URL on demand', async () => {
37+
render(<DiagramViewerTab />);
38+
39+
const image = await screen.findByRole('img', {
40+
name: 'Latest generated OpenHuman architecture diagram',
41+
});
42+
expect(image).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=0'));
43+
44+
fireEvent.click(screen.getByRole('button', { name: 'Refresh diagram' }));
45+
46+
expect(
47+
screen.getByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
48+
).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=1'));
49+
});
50+
51+
it('shows an empty state instead of a broken image after load failure', async () => {
52+
render(<DiagramViewerTab />);
53+
54+
const image = await screen.findByRole('img', {
55+
name: 'Latest generated OpenHuman architecture diagram',
56+
});
57+
fireEvent.error(image);
58+
59+
expect(screen.getByText('No diagram available yet')).toBeInTheDocument();
60+
expect(
61+
screen.getByText('npx skills add yizhiyanhua-ai/fireworks-tech-graph')
62+
).toBeInTheDocument();
63+
expect(
64+
screen.getByText(
65+
'Generate an architecture diagram of the current swarm in dark terminal style'
66+
)
67+
).toBeInTheDocument();
68+
expect(
69+
screen.queryByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
70+
).not.toBeInTheDocument();
71+
});
72+
});
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { LuImage, LuRefreshCw } from 'react-icons/lu';
3+
4+
import { useT } from '../../lib/i18n/I18nContext';
5+
import {
6+
type DiagramViewerSettings,
7+
openhumanGetDashboardSettings,
8+
} from '../../utils/tauriCommands/config';
9+
10+
const DEFAULT_SETTINGS: DiagramViewerSettings = {
11+
enabled: true,
12+
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
13+
refresh_interval_seconds: 10,
14+
};
15+
16+
type ImageState = 'idle' | 'loaded' | 'error';
17+
18+
function normalizeSettings(
19+
settings?: Partial<DiagramViewerSettings> | null
20+
): DiagramViewerSettings {
21+
const sourceUrl = settings?.source_url?.trim() || DEFAULT_SETTINGS.source_url;
22+
const refreshInterval = Number(settings?.refresh_interval_seconds);
23+
24+
return {
25+
enabled: settings?.enabled ?? DEFAULT_SETTINGS.enabled,
26+
source_url: sourceUrl,
27+
refresh_interval_seconds:
28+
Number.isFinite(refreshInterval) && refreshInterval > 0
29+
? Math.round(refreshInterval)
30+
: DEFAULT_SETTINGS.refresh_interval_seconds,
31+
};
32+
}
33+
34+
export function buildDiagramImageUrl(sourceUrl: string, refreshKey: number): string {
35+
try {
36+
const url = new URL(sourceUrl);
37+
url.searchParams.set('openhuman_refresh', String(refreshKey));
38+
return url.toString();
39+
} catch {
40+
const separator = sourceUrl.includes('?') ? '&' : '?';
41+
return `${sourceUrl}${separator}openhuman_refresh=${refreshKey}`;
42+
}
43+
}
44+
45+
export default function DiagramViewerTab() {
46+
const { t } = useT();
47+
const [settings, setSettings] = useState<DiagramViewerSettings>(DEFAULT_SETTINGS);
48+
const [refreshKey, setRefreshKey] = useState(0);
49+
const [imageState, setImageState] = useState<ImageState>('idle');
50+
51+
useEffect(() => {
52+
let alive = true;
53+
54+
openhumanGetDashboardSettings()
55+
.then(response => {
56+
if (!alive) return;
57+
setSettings(normalizeSettings(response.result.diagram_viewer));
58+
})
59+
.catch(() => {
60+
if (!alive) return;
61+
setSettings(DEFAULT_SETTINGS);
62+
});
63+
64+
return () => {
65+
alive = false;
66+
};
67+
}, []);
68+
69+
const refreshDiagram = useCallback(() => {
70+
setImageState('idle');
71+
setRefreshKey(prev => prev + 1);
72+
}, []);
73+
74+
useEffect(() => {
75+
if (!settings.enabled || settings.refresh_interval_seconds <= 0) return undefined;
76+
77+
const interval = window.setInterval(refreshDiagram, settings.refresh_interval_seconds * 1000);
78+
return () => window.clearInterval(interval);
79+
}, [refreshDiagram, settings.enabled, settings.refresh_interval_seconds]);
80+
81+
const sourceUrl = settings.source_url.trim();
82+
const imageUrl = useMemo(
83+
() => (sourceUrl ? buildDiagramImageUrl(sourceUrl, refreshKey) : ''),
84+
[refreshKey, sourceUrl]
85+
);
86+
87+
const showImage = settings.enabled && sourceUrl.length > 0 && imageState !== 'error';
88+
const showEmptyState = !settings.enabled || sourceUrl.length === 0 || imageState === 'error';
89+
90+
return (
91+
<section className="space-y-5" aria-labelledby="diagram-viewer-title">
92+
<div className="flex flex-wrap items-start justify-between gap-3">
93+
<div>
94+
<h2
95+
id="diagram-viewer-title"
96+
className="text-lg font-semibold text-stone-900 dark:text-neutral-100">
97+
{t('intelligence.diagram.title')}
98+
</h2>
99+
<p className="mt-1 text-sm text-stone-500 dark:text-neutral-400">
100+
{t('intelligence.diagram.description')}
101+
</p>
102+
</div>
103+
<button
104+
type="button"
105+
onClick={refreshDiagram}
106+
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"
107+
aria-label={t('intelligence.diagram.refreshAria')}>
108+
<LuRefreshCw aria-hidden="true" className="h-4 w-4" />
109+
{t('intelligence.diagram.refresh')}
110+
</button>
111+
</div>
112+
113+
{showEmptyState && (
114+
<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">
115+
<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">
116+
<LuImage aria-hidden="true" className="h-6 w-6" />
117+
</div>
118+
<div>
119+
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
120+
{t('intelligence.diagram.emptyTitle')}
121+
</h3>
122+
<p className="mt-1 max-w-md text-sm text-stone-500 dark:text-neutral-400">
123+
{t('intelligence.diagram.emptyDescription')}
124+
</p>
125+
</div>
126+
<div className="flex max-w-full flex-col gap-2">
127+
<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">
128+
{t('intelligence.diagram.skillInstallCommand')}
129+
</code>
130+
<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">
131+
{t('intelligence.diagram.promptExample')}
132+
</code>
133+
</div>
134+
</div>
135+
)}
136+
137+
{showImage && (
138+
<figure className="space-y-3">
139+
<img
140+
key={imageUrl}
141+
src={imageUrl}
142+
alt={t('intelligence.diagram.imageAlt')}
143+
className="block w-full rounded-lg border border-stone-200 bg-white object-contain dark:border-neutral-800 dark:bg-neutral-950"
144+
onLoad={() => setImageState('loaded')}
145+
onError={() => setImageState('error')}
146+
/>
147+
<figcaption className="flex flex-wrap items-center justify-between gap-2 text-xs text-stone-500 dark:text-neutral-400">
148+
<span>
149+
{t('intelligence.diagram.refreshesEvery').replace(
150+
'{seconds}',
151+
String(settings.refresh_interval_seconds)
152+
)}
153+
</span>
154+
<span className="max-w-full truncate">{sourceUrl}</span>
155+
</figcaption>
156+
</figure>
157+
)}
158+
</section>
159+
);
160+
}

app/src/lib/i18n/chunks/ar-1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const ar1: TranslationMap = {
178178
'memory.tab.subconscious': 'اللاوعي',
179179
'memory.tab.dreams': 'الأحلام',
180180
'memory.tab.calls': 'المكالمات',
181+
'memory.tab.diagram': 'Diagram',
181182
'memory.tab.settings': 'الإعدادات',
182183
'memory.analyzeNow': 'تحليل الآن',
183184
'alerts.title': 'التنبيهات',

app/src/lib/i18n/chunks/ar-4.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ const ar4: TranslationMap = {
102102
'intelligence.memoryChunk.scoreBars.dropped': 'مُسقَط',
103103
'intelligence.memoryChunk.scoreBars.heading': 'س ب ب ا ل ح ف ظ',
104104
'intelligence.memoryChunk.scoreBars.kept': 'محفوظ',
105+
'intelligence.diagram.title': 'Architecture Diagram',
106+
'intelligence.diagram.description':
107+
'Latest local architecture output from the configured diagram endpoint.',
108+
'intelligence.diagram.refresh': 'Refresh',
109+
'intelligence.diagram.refreshAria': 'Refresh diagram',
110+
'intelligence.diagram.emptyTitle': 'No diagram available yet',
111+
'intelligence.diagram.emptyDescription':
112+
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
113+
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
114+
'intelligence.diagram.promptExample':
115+
'Generate an architecture diagram of the current swarm in dark terminal style',
116+
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
117+
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
105118
'intelligence.memoryText.entityTypePrefix': 'نوع الكيان',
106119
'intelligence.screenDebug.active': 'نشط',
107120
'intelligence.screenDebug.app': 'التطبيق',

app/src/lib/i18n/chunks/bn-1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ const bn1: TranslationMap = {
180180
'memory.tab.subconscious': 'সাবকনশাস',
181181
'memory.tab.dreams': 'স্বপ্ন',
182182
'memory.tab.calls': 'কল',
183+
'memory.tab.diagram': 'Diagram',
183184
'memory.tab.settings': 'সেটিংস',
184185
'memory.analyzeNow': 'এখনই বিশ্লেষণ করুন',
185186
'alerts.title': 'সতর্কতা',

app/src/lib/i18n/chunks/bn-4.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ const bn4: TranslationMap = {
103103
'intelligence.memoryChunk.scoreBars.dropped': 'বাদ দেওয়া হয়েছে',
104104
'intelligence.memoryChunk.scoreBars.heading': 'কে ন রা খা',
105105
'intelligence.memoryChunk.scoreBars.kept': 'রাখা হয়েছে',
106+
'intelligence.diagram.title': 'Architecture Diagram',
107+
'intelligence.diagram.description':
108+
'Latest local architecture output from the configured diagram endpoint.',
109+
'intelligence.diagram.refresh': 'Refresh',
110+
'intelligence.diagram.refreshAria': 'Refresh diagram',
111+
'intelligence.diagram.emptyTitle': 'No diagram available yet',
112+
'intelligence.diagram.emptyDescription':
113+
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
114+
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
115+
'intelligence.diagram.promptExample':
116+
'Generate an architecture diagram of the current swarm in dark terminal style',
117+
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
118+
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
106119
'intelligence.memoryText.entityTypePrefix': 'এনটিটি ধরন',
107120
'intelligence.screenDebug.active': 'সক্রিয়',
108121
'intelligence.screenDebug.app': 'অ্যাপ',

app/src/lib/i18n/chunks/de-1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ const de1: TranslationMap = {
222222
'memory.tab.subconscious': 'Unterbewusstsein',
223223
'memory.tab.dreams': 'Träume',
224224
'memory.tab.calls': 'Anrufe',
225+
'memory.tab.diagram': 'Diagram',
225226
'memory.tab.settings': 'Einstellungen',
226227
'memory.analyzeNow': 'Jetzt analysieren',
227228
'alerts.title': 'Warnungen',

app/src/lib/i18n/chunks/de-4.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ const de4: TranslationMap = {
103103
'intelligence.memoryChunk.scoreBars.dropped': 'fallen gelassen',
104104
'intelligence.memoryChunk.scoreBars.heading': 'Warum hast du es behalten?',
105105
'intelligence.memoryChunk.scoreBars.kept': 'gehalten',
106+
'intelligence.diagram.title': 'Architecture Diagram',
107+
'intelligence.diagram.description':
108+
'Latest local architecture output from the configured diagram endpoint.',
109+
'intelligence.diagram.refresh': 'Refresh',
110+
'intelligence.diagram.refreshAria': 'Refresh diagram',
111+
'intelligence.diagram.emptyTitle': 'No diagram available yet',
112+
'intelligence.diagram.emptyDescription':
113+
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
114+
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
115+
'intelligence.diagram.promptExample':
116+
'Generate an architecture diagram of the current swarm in dark terminal style',
117+
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
118+
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
106119
'intelligence.memoryText.entityTypePrefix': 'Entitätstyp',
107120
'intelligence.screenDebug.active': 'Aktiv',
108121
'intelligence.screenDebug.app': 'App',

app/src/lib/i18n/chunks/en-1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ const en1: TranslationMap = {
464464
'memory.tab.subconscious': 'Subconscious',
465465
'memory.tab.dreams': 'Dreams',
466466
'memory.tab.calls': 'Calls',
467+
'memory.tab.diagram': 'Diagram',
467468
'memory.tab.settings': 'Settings',
468469
'memory.analyzeNow': 'Analyze Now',
469470
'alerts.title': 'Alerts',

app/src/lib/i18n/chunks/en-4.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ const en4: TranslationMap = {
111111
'intelligence.memoryChunk.scoreBars.dropped': 'dropped',
112112
'intelligence.memoryChunk.scoreBars.heading': 'w h y k e p t',
113113
'intelligence.memoryChunk.scoreBars.kept': 'kept',
114+
'intelligence.diagram.title': 'Architecture Diagram',
115+
'intelligence.diagram.description':
116+
'Latest local architecture output from the configured diagram endpoint.',
117+
'intelligence.diagram.refresh': 'Refresh',
118+
'intelligence.diagram.refreshAria': 'Refresh diagram',
119+
'intelligence.diagram.emptyTitle': 'No diagram available yet',
120+
'intelligence.diagram.emptyDescription':
121+
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
122+
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
123+
'intelligence.diagram.promptExample':
124+
'Generate an architecture diagram of the current swarm in dark terminal style',
125+
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
126+
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
114127
'intelligence.memoryText.entityTypePrefix': 'Entity type',
115128
'intelligence.screenDebug.active': 'Active',
116129
'intelligence.screenDebug.app': 'App',

0 commit comments

Comments
 (0)