Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/test-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
if: inputs.run_rust_core
name: Rust Core Tests (Windows — secrets ACL)
runs-on: windows-latest
timeout-minutes: 20
timeout-minutes: 35
env:
CARGO_INCREMENTAL: '0'
SCCACHE_GHA_ENABLED: 'true'
Expand All @@ -150,10 +150,10 @@ jobs:
- name: Install sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Run Windows-specific secrets tests
# Runs the full security::secrets suite including all #[cfg(windows)]
# Runs the encrypted secret-store suite including all #[cfg(windows)]
# tests: self-repair ACL path (OPENHUMAN-TAURI-GN), domain-qualified
# icacls username, is_permission_error, repair_windows_acl.
run: bash scripts/ci-cancel-aware.sh cargo test -p openhuman -- security::secrets --nocapture
run: bash scripts/ci-cancel-aware.sh cargo test -p openhuman -- keyring::encrypted_store::tests --nocapture

rust-tauri-tests:
if: inputs.run_rust_tauri
Expand Down
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>
{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': 'تحليل الآن',
'alerts.title': 'التنبيهات',
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': 'এখনই বিশ্লেষণ করুন',
'alerts.title': 'সতর্কতা',
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 @@ -222,6 +222,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',
'alerts.title': 'Warnungen',
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 @@ -464,6 +464,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',
'alerts.title': 'Alerts',
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',
'alerts.title': 'Alertas',
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
Loading
Loading