mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(intelligence): add Namespace Overview (#2964)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Claude Opus 4.7
Steven Enamakel
parent
29f8806fdb
commit
6c3ad6388d
@@ -0,0 +1,57 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
|
||||
import type { GraphRelation } from '../../utils/tauriCommands/memory';
|
||||
import NamespaceOverviewPanel from './NamespaceOverviewPanel';
|
||||
|
||||
function rel(namespace: string | null, subject: string, object: string): GraphRelation {
|
||||
return {
|
||||
namespace,
|
||||
subject,
|
||||
predicate: 'p',
|
||||
object,
|
||||
attrs: {},
|
||||
updatedAt: 0,
|
||||
evidenceCount: 1,
|
||||
orderIndex: null,
|
||||
documentIds: [],
|
||||
chunkIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const report = computeNamespaceOverview([
|
||||
rel('work', 'A', 'B'),
|
||||
rel('work', 'B', 'C'),
|
||||
rel(null, 'P', 'Q'),
|
||||
]);
|
||||
|
||||
describe('<NamespaceOverviewPanel />', () => {
|
||||
it('renders the loading skeleton', () => {
|
||||
render(<NamespaceOverviewPanel report={null} loading />);
|
||||
expect(screen.getByTestId('namespace-overview-loading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the empty state when there are no namespaces', () => {
|
||||
render(<NamespaceOverviewPanel report={computeNamespaceOverview([])} />);
|
||||
expect(screen.getByText('No knowledge graph yet.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an error with a working retry button', () => {
|
||||
const onRetry = vi.fn();
|
||||
render(<NamespaceOverviewPanel report={null} error="graph unavailable" onRetry={onRetry} />);
|
||||
expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
||||
expect(onRetry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders summary tiles and the per-namespace list (un-namespaced labeled)', () => {
|
||||
render(<NamespaceOverviewPanel report={report} />);
|
||||
expect(screen.getByText('Namespaces')).toBeInTheDocument();
|
||||
expect(screen.getByText('Facts')).toBeInTheDocument();
|
||||
expect(screen.getByText('By namespace')).toBeInTheDocument();
|
||||
expect(screen.getByText('work')).toBeInTheDocument();
|
||||
// the null namespace renders with the "un-namespaced" label
|
||||
expect(screen.getByText('(un-namespaced)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Namespace Overview — presentational view. Pure: renders per-namespace fact /
|
||||
* entity counts as a ranked bar list + summary tiles. No data fetching, no
|
||||
* clock, no RNG.
|
||||
*/
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import type { NamespaceOverviewReport } from '../../lib/memory/namespaceOverview';
|
||||
|
||||
const MAX_ROWS = 50;
|
||||
|
||||
interface NamespaceOverviewPanelProps {
|
||||
report: NamespaceOverviewReport | null;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
const NamespaceOverviewPanel = ({
|
||||
report,
|
||||
loading,
|
||||
error,
|
||||
onRetry,
|
||||
}: NamespaceOverviewPanelProps) => {
|
||||
const { t } = useT();
|
||||
|
||||
const intro = (
|
||||
<div
|
||||
role="note"
|
||||
className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50 dark:bg-primary-500/10 px-3 py-2 text-xs text-stone-700 dark:text-neutral-200">
|
||||
<p className="font-medium mb-1">{t('namespaceOverview.title')}</p>
|
||||
<p>{t('namespaceOverview.intro')}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{intro}
|
||||
<div
|
||||
className="space-y-3"
|
||||
role="status"
|
||||
aria-label={t('namespaceOverview.loading')}
|
||||
data-testid="namespace-overview-loading">
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{[0, 1, 2].map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className="animate-pulse rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 h-16"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{[0, 1, 2].map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className="animate-pulse rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 h-6"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{intro}
|
||||
<div className="rounded-lg border border-coral-200 dark:border-coral-500/30 p-4 text-center">
|
||||
<p role="alert" className="text-xs text-coral-700 dark:text-coral-300">
|
||||
{t('namespaceOverview.errorPrefix')} {error}
|
||||
</p>
|
||||
{onRetry && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
className="mt-2 rounded-lg bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-primary-600">
|
||||
{t('namespaceOverview.retry')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!report || report.namespaceCount === 0) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{intro}
|
||||
<div className="py-8 text-center">
|
||||
<h3 className="text-sm font-semibold text-stone-700 dark:text-neutral-200">
|
||||
{t('namespaceOverview.empty')}
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-stone-500 dark:text-neutral-400">
|
||||
{t('namespaceOverview.emptyHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const maxFacts = report.namespaces[0]?.factCount || 1;
|
||||
const rows = report.namespaces.slice(0, MAX_ROWS);
|
||||
const truncated = report.namespaces.length > MAX_ROWS;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{intro}
|
||||
|
||||
{/* Summary tiles */}
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{[
|
||||
{ label: t('namespaceOverview.metricNamespaces'), value: report.namespaceCount },
|
||||
{ label: t('namespaceOverview.metricFacts'), value: report.totalFacts },
|
||||
{ label: t('namespaceOverview.metricEntities'), value: report.totalEntities },
|
||||
].map(tile => (
|
||||
<div
|
||||
key={tile.label}
|
||||
className="rounded-lg border border-stone-200 dark:border-neutral-800 p-3">
|
||||
<div className="text-[10px] uppercase tracking-wider text-stone-400 dark:text-neutral-500">
|
||||
{tile.label}
|
||||
</div>
|
||||
<div className="text-lg font-semibold tabular-nums text-stone-900 dark:text-neutral-100">
|
||||
{tile.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Ranked namespace list */}
|
||||
<section aria-labelledby="namespace-overview-heading" className="space-y-1">
|
||||
<h3
|
||||
id="namespace-overview-heading"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-stone-500 dark:text-neutral-400">
|
||||
{t('namespaceOverview.heading')}
|
||||
</h3>
|
||||
<ul className="space-y-1">
|
||||
{rows.map(stat => (
|
||||
<li
|
||||
key={JSON.stringify(stat.namespace)}
|
||||
className="flex items-center gap-2 text-[11px] tabular-nums">
|
||||
<span
|
||||
className={`w-28 shrink-0 truncate ${
|
||||
stat.namespace === null
|
||||
? 'italic text-stone-400 dark:text-neutral-500'
|
||||
: 'text-stone-700 dark:text-neutral-200'
|
||||
}`}
|
||||
title={stat.namespace ?? t('namespaceOverview.unnamespaced')}>
|
||||
{stat.namespace ?? t('namespaceOverview.unnamespaced')}
|
||||
</span>
|
||||
<div className="flex-1 h-3 rounded bg-stone-100 dark:bg-neutral-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary-400/70"
|
||||
style={{ width: `${(stat.factCount / maxFacts) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="w-16 shrink-0 text-right text-stone-500 dark:text-neutral-400"
|
||||
title={t('namespaceOverview.factsLabel').replace(
|
||||
'{count}',
|
||||
String(stat.factCount)
|
||||
)}>
|
||||
{stat.factCount}
|
||||
</span>
|
||||
<span
|
||||
className="w-16 shrink-0 text-right text-stone-400 dark:text-neutral-500"
|
||||
title={t('namespaceOverview.entitiesLabel').replace(
|
||||
'{count}',
|
||||
String(stat.entityCount)
|
||||
)}>
|
||||
{t('namespaceOverview.entitiesShort').replace('{count}', String(stat.entityCount))}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{truncated && (
|
||||
<p className="text-center text-xs text-stone-400 dark:text-neutral-500">
|
||||
{t('namespaceOverview.truncated')
|
||||
.replace('{shown}', String(rows.length))
|
||||
.replace('{total}', String(report.namespaces.length))}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NamespaceOverviewPanel;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
|
||||
import type { GraphRelation } from '../../utils/tauriCommands/memory';
|
||||
import NamespaceOverviewTab from './NamespaceOverviewTab';
|
||||
|
||||
const mockLoad = vi.fn();
|
||||
|
||||
vi.mock('../../services/api/namespaceOverviewApi', () => ({
|
||||
loadNamespaceOverview: (...args: unknown[]) => mockLoad(...args),
|
||||
}));
|
||||
|
||||
function rel(namespace: string | null, subject: string, object: string): GraphRelation {
|
||||
return {
|
||||
namespace,
|
||||
subject,
|
||||
predicate: 'p',
|
||||
object,
|
||||
attrs: {},
|
||||
updatedAt: 0,
|
||||
evidenceCount: 1,
|
||||
orderIndex: null,
|
||||
documentIds: [],
|
||||
chunkIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const report = computeNamespaceOverview([rel('work', 'A', 'B')]);
|
||||
|
||||
describe('<NamespaceOverviewTab />', () => {
|
||||
beforeEach(() => {
|
||||
mockLoad.mockReset();
|
||||
mockLoad.mockResolvedValue(report);
|
||||
});
|
||||
|
||||
it('loads on mount and renders the per-namespace list', async () => {
|
||||
render(<NamespaceOverviewTab />);
|
||||
await waitFor(() => expect(screen.getByText('By namespace')).toBeInTheDocument());
|
||||
expect(mockLoad).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('surfaces an error when the load fails', async () => {
|
||||
mockLoad.mockReset();
|
||||
mockLoad.mockRejectedValueOnce(new Error('graph unavailable'));
|
||||
render(<NamespaceOverviewTab />);
|
||||
await waitFor(() => expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Namespace Overview tab (container). Loads the whole graph on mount and
|
||||
* delegates rendering to the pure <NamespaceOverviewPanel>. Read-only. No
|
||||
* namespace selector — this view's axis IS the namespace, so it shows them all.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { NamespaceOverviewReport } from '../../lib/memory/namespaceOverview';
|
||||
import { loadNamespaceOverview } from '../../services/api/namespaceOverviewApi';
|
||||
import NamespaceOverviewPanel from './NamespaceOverviewPanel';
|
||||
|
||||
const NamespaceOverviewTab = () => {
|
||||
const [report, setReport] = useState<NamespaceOverviewReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Monotonic token: ignore a response if a newer load has since started.
|
||||
const latestRequestId = useRef(0);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const requestId = (latestRequestId.current += 1);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const next = await loadNamespaceOverview();
|
||||
if (requestId !== latestRequestId.current) return;
|
||||
setReport(next);
|
||||
} catch (err) {
|
||||
if (requestId !== latestRequestId.current) return;
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
if (requestId === latestRequestId.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<NamespaceOverviewPanel
|
||||
report={report}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={() => void load()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NamespaceOverviewTab;
|
||||
@@ -275,6 +275,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'المكالمات',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'مساحات الأسماء',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'الإعدادات',
|
||||
'memory.analyzeNow': 'تحليل الآن',
|
||||
@@ -296,6 +297,23 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'نظرة عامة على مساحات الأسماء',
|
||||
'namespaceOverview.intro':
|
||||
'كيفية توزيع معرفتك عبر السياقات — عدد الحقائق والكيانات المتميزة المسجَّلة في كل مساحة اسم.',
|
||||
'namespaceOverview.loading': 'تجميع مساحات الأسماء…',
|
||||
'namespaceOverview.errorPrefix': 'لا يمكن تحميل الرسم البياني:',
|
||||
'namespaceOverview.retry': 'إعادة المحاولة',
|
||||
'namespaceOverview.empty': 'لا يوجد رسم بياني للمعرفة بعد.',
|
||||
'namespaceOverview.emptyHint': 'عندما يسجّل المساعد حقائق عبر السياقات، ستظهر كل مساحة اسم هنا.',
|
||||
'namespaceOverview.metricNamespaces': 'مساحات الأسماء',
|
||||
'namespaceOverview.metricFacts': 'الحقائق',
|
||||
'namespaceOverview.metricEntities': 'الكيانات',
|
||||
'namespaceOverview.heading': 'حسب مساحة الاسم',
|
||||
'namespaceOverview.unnamespaced': '(بدون مساحة اسم)',
|
||||
'namespaceOverview.factsLabel': '{count} حقيقة',
|
||||
'namespaceOverview.entitiesLabel': '{count} كيان',
|
||||
'namespaceOverview.entitiesShort': '{count} كيان',
|
||||
'namespaceOverview.truncated': 'عرض أعلى {shown} من أصل {total} مساحة اسم.',
|
||||
'graphCentrality.title': 'مركز المعرفة',
|
||||
'graphCentrality.intro':
|
||||
'ويظهر الإرسال على صور ذاكرتك مراكز الحمل - والكيانات الموصلة التي تربط المجموعات المنفصلة عن بعضها البعض، والتي لا يمكن لإحصاء الترددات الخام أن يكشف عنها.',
|
||||
|
||||
@@ -278,6 +278,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'কল',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'নেমস্পেস',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'সেটিংস',
|
||||
'memory.analyzeNow': 'এখনই বিশ্লেষণ করুন',
|
||||
@@ -299,6 +300,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'নেমস্পেস ওভারভিউ',
|
||||
'namespaceOverview.intro':
|
||||
'প্রসঙ্গভেদে আপনার জ্ঞান কীভাবে বিতরণ করা হয়েছে — প্রতিটি নেমস্পেসে নথিভুক্ত তথ্য ও স্বতন্ত্র সত্তার সংখ্যা।',
|
||||
'namespaceOverview.loading': 'নেমস্পেস একত্রিত করা হচ্ছে…',
|
||||
'namespaceOverview.errorPrefix': 'রেখাচিত্র লোড করা যায়নি:',
|
||||
'namespaceOverview.retry': 'পুনরায় চেষ্টা করুন',
|
||||
'namespaceOverview.empty': 'এখনও কোনো জ্ঞান গ্রাফ নেই।',
|
||||
'namespaceOverview.emptyHint':
|
||||
'সহকারী প্রসঙ্গভেদে তথ্য রেকর্ড করার সাথে সাথে প্রতিটি নেমস্পেস এখানে দেখা যাবে।',
|
||||
'namespaceOverview.metricNamespaces': 'নেমস্পেস',
|
||||
'namespaceOverview.metricFacts': 'তথ্য',
|
||||
'namespaceOverview.metricEntities': 'সত্তা',
|
||||
'namespaceOverview.heading': 'নেমস্পেস অনুযায়ী',
|
||||
'namespaceOverview.unnamespaced': '(নেমস্পেসবিহীন)',
|
||||
'namespaceOverview.factsLabel': '{count}টি তথ্য',
|
||||
'namespaceOverview.entitiesLabel': '{count}টি সত্তা',
|
||||
'namespaceOverview.entitiesShort': '{count} সত্তা',
|
||||
'namespaceOverview.truncated': '{total}টির মধ্যে শীর্ষ {shown}টি নেমস্পেস দেখানো হচ্ছে।',
|
||||
'graphCentrality.title': 'জ্ঞান গ্রাফ',
|
||||
'graphCentrality.intro':
|
||||
'আপনার মেমরি গ্রাফের উপর প্রদর্শিত পেজের ছবি- এবং সংযোগকারী সত্তার সাথে সংযুক্ত একটি সংযুক্ত চক্রের সংযোগ রয়েছে, যা একটি raw ফ্রিকোয়েন্সি গণনা প্রকাশ করতে পারে না।',
|
||||
|
||||
@@ -287,6 +287,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Anrufe',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Namensräume',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Einstellungen',
|
||||
'memory.analyzeNow': 'Jetzt analysieren',
|
||||
@@ -308,6 +309,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Namensraum-Übersicht',
|
||||
'namespaceOverview.intro':
|
||||
'Wie sich dein Wissen über Kontexte verteilt — die Anzahl der Fakten und eindeutigen Entitäten, die in jedem Namensraum erfasst sind.',
|
||||
'namespaceOverview.loading': 'Namensräume werden aggregiert…',
|
||||
'namespaceOverview.errorPrefix': 'Das Diagramm konnte nicht geladen werden:',
|
||||
'namespaceOverview.retry': 'Wiederholen',
|
||||
'namespaceOverview.empty': 'Noch kein Wissensgraph.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'Sobald der Assistent Fakten über Kontexte hinweg erfasst, erscheint hier jeder Namensraum.',
|
||||
'namespaceOverview.metricNamespaces': 'Namensräume',
|
||||
'namespaceOverview.metricFacts': 'Fakten',
|
||||
'namespaceOverview.metricEntities': 'Entitäten',
|
||||
'namespaceOverview.heading': 'Nach Namensraum',
|
||||
'namespaceOverview.unnamespaced': '(ohne Namensraum)',
|
||||
'namespaceOverview.factsLabel': '{count} Fakten',
|
||||
'namespaceOverview.entitiesLabel': '{count} Entitäten',
|
||||
'namespaceOverview.entitiesShort': '{count} Ent.',
|
||||
'namespaceOverview.truncated': 'Die obersten {shown} von {total} Namensräumen werden angezeigt.',
|
||||
'graphCentrality.title': 'Zentralität des Wissensgraphen',
|
||||
'graphCentrality.intro':
|
||||
'Der PageRank über Ihr Speicherdiagramm zeigt die tragenden Knotenpunkte auf – und die Konnektorentitäten, die ansonsten getrennte Cluster verbinden, die eine reine Häufigkeitszählung nicht aufdecken kann.',
|
||||
|
||||
@@ -303,9 +303,28 @@ const en: TranslationMap = {
|
||||
'memory.tab.calls': 'Calls',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Namespaces',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Settings',
|
||||
'memory.analyzeNow': 'Analyze Now',
|
||||
'namespaceOverview.title': 'Namespace Overview',
|
||||
'namespaceOverview.intro':
|
||||
'How your knowledge is distributed across contexts — the number of facts and distinct entities recorded in each namespace.',
|
||||
'namespaceOverview.loading': 'Aggregating namespaces…',
|
||||
'namespaceOverview.errorPrefix': 'Could not load the graph:',
|
||||
'namespaceOverview.retry': 'Retry',
|
||||
'namespaceOverview.empty': 'No knowledge graph yet.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'As the assistant records facts across contexts, each namespace will appear here.',
|
||||
'namespaceOverview.metricNamespaces': 'Namespaces',
|
||||
'namespaceOverview.metricFacts': 'Facts',
|
||||
'namespaceOverview.metricEntities': 'Entities',
|
||||
'namespaceOverview.heading': 'By namespace',
|
||||
'namespaceOverview.unnamespaced': '(un-namespaced)',
|
||||
'namespaceOverview.factsLabel': '{count} facts',
|
||||
'namespaceOverview.entitiesLabel': '{count} entities',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated': 'Showing the top {shown} of {total} namespaces.',
|
||||
'memoryTimeline.title': 'Memory Timeline',
|
||||
'memoryTimeline.intro':
|
||||
'When the assistant learned about you — facts grouped by the month they were last reinforced. Surfaces growth, bursts of activity, and quiet stretches.',
|
||||
|
||||
@@ -287,6 +287,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Llamadas',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Espacios de nombres',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Configuración',
|
||||
'memory.analyzeNow': 'Analizar ahora',
|
||||
@@ -308,6 +309,25 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Resumen de espacios de nombres',
|
||||
'namespaceOverview.intro':
|
||||
'Cómo se distribuye tu conocimiento entre contextos: el número de hechos y entidades distintas registradas en cada espacio de nombres.',
|
||||
'namespaceOverview.loading': 'Agregando espacios de nombres…',
|
||||
'namespaceOverview.errorPrefix': 'No se pudo cargar el gráfico:',
|
||||
'namespaceOverview.retry': 'Reintentar',
|
||||
'namespaceOverview.empty': 'Aún no hay gráfico de conocimiento.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'A medida que el asistente registra hechos entre contextos, cada espacio de nombres aparecerá aquí.',
|
||||
'namespaceOverview.metricNamespaces': 'Espacios de nombres',
|
||||
'namespaceOverview.metricFacts': 'Hechos',
|
||||
'namespaceOverview.metricEntities': 'Entidades',
|
||||
'namespaceOverview.heading': 'Por espacio de nombres',
|
||||
'namespaceOverview.unnamespaced': '(sin espacio de nombres)',
|
||||
'namespaceOverview.factsLabel': '{count} hechos',
|
||||
'namespaceOverview.entitiesLabel': '{count} entidades',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated':
|
||||
'Mostrando los {shown} principales de {total} espacios de nombres.',
|
||||
'graphCentrality.title': 'Centralidad del gráfico de conocimiento',
|
||||
'graphCentrality.intro':
|
||||
'El PageRank sobre su gráfico de memoria muestra los centros de carga y las entidades conectoras que vinculan grupos que de otro modo estarían separados, que un recuento de frecuencia sin procesar no puede revelar.',
|
||||
|
||||
@@ -286,6 +286,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Appels',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Espaces de noms',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Paramètres',
|
||||
'memory.analyzeNow': 'Analyser maintenant',
|
||||
@@ -307,6 +308,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Aperçu des espaces de noms',
|
||||
'namespaceOverview.intro':
|
||||
'Comment vos connaissances sont réparties entre les contextes — le nombre de faits et d’entités distinctes enregistrés dans chaque espace de noms.',
|
||||
'namespaceOverview.loading': 'Agrégation des espaces de noms…',
|
||||
'namespaceOverview.errorPrefix': 'Impossible de charger le graphique:',
|
||||
'namespaceOverview.retry': 'Réessayer',
|
||||
'namespaceOverview.empty': 'Pas encore de graphe de connaissances.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'À mesure que l’assistant enregistre des faits entre les contextes, chaque espace de noms apparaîtra ici.',
|
||||
'namespaceOverview.metricNamespaces': 'Espaces de noms',
|
||||
'namespaceOverview.metricFacts': 'Faits',
|
||||
'namespaceOverview.metricEntities': 'Entités',
|
||||
'namespaceOverview.heading': 'Par espace de noms',
|
||||
'namespaceOverview.unnamespaced': '(sans espace de noms)',
|
||||
'namespaceOverview.factsLabel': '{count} faits',
|
||||
'namespaceOverview.entitiesLabel': '{count} entités',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated': 'Affichage des {shown} premiers espaces de noms sur {total}.',
|
||||
'graphCentrality.title': 'Centralité du graphe de connaissances',
|
||||
'graphCentrality.intro':
|
||||
"PageRank sur votre graphe de mémoire met en évidence les hubs porteurs de charge — et les entités connectrices qui relient des clusters autrement séparés, ce qu'un simple comptage de fréquence ne peut révéler.",
|
||||
|
||||
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'कॉल्स',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'नेमस्पेस',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'सेटिंग्स',
|
||||
'memory.analyzeNow': 'अभी एनालाइज़ करें',
|
||||
@@ -298,6 +299,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'नेमस्पेस अवलोकन',
|
||||
'namespaceOverview.intro':
|
||||
'आपका ज्ञान संदर्भों में कैसे वितरित है — प्रत्येक नेमस्पेस में दर्ज तथ्यों और विशिष्ट इकाइयों की संख्या।',
|
||||
'namespaceOverview.loading': 'नेमस्पेस एकत्र किए जा रहे हैं…',
|
||||
'namespaceOverview.errorPrefix': 'ग्राफ़ लोड नहीं किया जा सका:',
|
||||
'namespaceOverview.retry': 'पुनः प्रयास करें',
|
||||
'namespaceOverview.empty': 'अभी तक कोई ज्ञान ग्राफ़ नहीं है।',
|
||||
'namespaceOverview.emptyHint':
|
||||
'जैसे-जैसे सहायक संदर्भों में तथ्य दर्ज करता है, प्रत्येक नेमस्पेस यहाँ दिखाई देगा।',
|
||||
'namespaceOverview.metricNamespaces': 'नेमस्पेस',
|
||||
'namespaceOverview.metricFacts': 'तथ्य',
|
||||
'namespaceOverview.metricEntities': 'इकाइयाँ',
|
||||
'namespaceOverview.heading': 'नेमस्पेस के अनुसार',
|
||||
'namespaceOverview.unnamespaced': '(बिना नेमस्पेस)',
|
||||
'namespaceOverview.factsLabel': '{count} तथ्य',
|
||||
'namespaceOverview.entitiesLabel': '{count} इकाइयाँ',
|
||||
'namespaceOverview.entitiesShort': '{count} इका.',
|
||||
'namespaceOverview.truncated': '{total} में से शीर्ष {shown} नेमस्पेस दिखाए जा रहे हैं।',
|
||||
'graphCentrality.title': 'ज्ञान ग्राफ केंद्रीयता',
|
||||
'graphCentrality.intro':
|
||||
'अपने मेमोरी ग्राफ़ पर पेजरैंक लोड-असर हब सतहों - और कनेक्टर इकाइयां जो अन्यथा अलग-अलग समूहों को जोड़ती हैं, जो एक कच्चे आवृत्ति गिनती प्रकट नहीं हो सकती हैं।',
|
||||
|
||||
@@ -279,6 +279,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Panggilan',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Namespace',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Pengaturan',
|
||||
'memory.analyzeNow': 'Analisis Sekarang',
|
||||
@@ -300,6 +301,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Ikhtisar Namespace',
|
||||
'namespaceOverview.intro':
|
||||
'Bagaimana pengetahuan Anda terdistribusi di seluruh konteks — jumlah fakta dan entitas berbeda yang tercatat di setiap namespace.',
|
||||
'namespaceOverview.loading': 'Menggabungkan namespace…',
|
||||
'namespaceOverview.errorPrefix': 'Tidak dapat memuat grafiknya:',
|
||||
'namespaceOverview.retry': 'Coba lagi',
|
||||
'namespaceOverview.empty': 'Belum ada grafik pengetahuan.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'Saat asisten mencatat fakta di seluruh konteks, setiap namespace akan muncul di sini.',
|
||||
'namespaceOverview.metricNamespaces': 'Namespace',
|
||||
'namespaceOverview.metricFacts': 'Fakta',
|
||||
'namespaceOverview.metricEntities': 'Entitas',
|
||||
'namespaceOverview.heading': 'Berdasarkan namespace',
|
||||
'namespaceOverview.unnamespaced': '(tanpa namespace)',
|
||||
'namespaceOverview.factsLabel': '{count} fakta',
|
||||
'namespaceOverview.entitiesLabel': '{count} entitas',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated': 'Menampilkan {shown} teratas dari {total} namespace.',
|
||||
'graphCentrality.title': 'Centralitas Grafik Pengetahuan',
|
||||
'graphCentrality.intro':
|
||||
'PageRank melalui grafik memori Anda permukaan hub load- bantalan - dan entiti konektor yang menghubungkan lain-terpisah cluster, yang jumlah frekuensi mentah tidak dapat mengungkapkan.',
|
||||
|
||||
@@ -284,6 +284,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Chiamate',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Spazi dei nomi',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Impostazioni',
|
||||
'memory.analyzeNow': 'Analizza ora',
|
||||
@@ -305,6 +306,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Panoramica degli spazi dei nomi',
|
||||
'namespaceOverview.intro':
|
||||
'Come la tua conoscenza è distribuita tra i contesti — il numero di fatti ed entità distinte registrate in ogni spazio dei nomi.',
|
||||
'namespaceOverview.loading': 'Aggregazione degli spazi dei nomi…',
|
||||
'namespaceOverview.errorPrefix': 'Impossibile caricare il grafico:',
|
||||
'namespaceOverview.retry': 'Riprova',
|
||||
'namespaceOverview.empty': 'Ancora nessun knowledge graph.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'Man mano che l’assistente registra fatti tra i contesti, ogni spazio dei nomi apparirà qui.',
|
||||
'namespaceOverview.metricNamespaces': 'Spazi dei nomi',
|
||||
'namespaceOverview.metricFacts': 'Fatti',
|
||||
'namespaceOverview.metricEntities': 'Entità',
|
||||
'namespaceOverview.heading': 'Per spazio dei nomi',
|
||||
'namespaceOverview.unnamespaced': '(senza spazio dei nomi)',
|
||||
'namespaceOverview.factsLabel': '{count} fatti',
|
||||
'namespaceOverview.entitiesLabel': '{count} entità',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated': 'Mostrando i primi {shown} di {total} spazi dei nomi.',
|
||||
'graphCentrality.title': 'Centralità del Grafo della Conoscenza',
|
||||
'graphCentrality.intro':
|
||||
'PageRank sul tuo grafo della memoria mette in evidenza gli hub portanti — e le entità di collegamento che connettono cluster altrimenti separati, cosa che un semplice conteggio di frequenza non può rivelare.',
|
||||
|
||||
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': '통화',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': '네임스페이스',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': '설정',
|
||||
'memory.analyzeNow': '지금 분석',
|
||||
@@ -298,6 +299,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': '네임스페이스 개요',
|
||||
'namespaceOverview.intro':
|
||||
'컨텍스트 전반에 걸쳐 지식이 어떻게 분포되어 있는지 — 각 네임스페이스에 기록된 사실과 고유 엔터티의 수입니다.',
|
||||
'namespaceOverview.loading': '네임스페이스 집계 중…',
|
||||
'namespaceOverview.errorPrefix': '그래프를 로드할 수 없습니다:',
|
||||
'namespaceOverview.retry': '다시 시도',
|
||||
'namespaceOverview.empty': '아직 지식 그래프가 없습니다.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'어시스턴트가 컨텍스트 전반에 걸쳐 사실을 기록하면 각 네임스페이스가 여기에 표시됩니다.',
|
||||
'namespaceOverview.metricNamespaces': '네임스페이스',
|
||||
'namespaceOverview.metricFacts': '사실',
|
||||
'namespaceOverview.metricEntities': '엔터티',
|
||||
'namespaceOverview.heading': '네임스페이스별',
|
||||
'namespaceOverview.unnamespaced': '(네임스페이스 없음)',
|
||||
'namespaceOverview.factsLabel': '사실 {count}개',
|
||||
'namespaceOverview.entitiesLabel': '엔터티 {count}개',
|
||||
'namespaceOverview.entitiesShort': '{count}개 엔터티',
|
||||
'namespaceOverview.truncated': '전체 {total}개 네임스페이스 중 상위 {shown}개를 표시합니다.',
|
||||
'graphCentrality.title': '지식 그래프 중심성',
|
||||
'graphCentrality.intro':
|
||||
'메모리 그래프에 PageRank를 적용해 핵심 허브와, 단순 빈도 계산으로는 드러나지 않는 분리된 클러스터를 연결하는 엔터티를 드러냅니다.',
|
||||
|
||||
@@ -282,6 +282,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Połączenia',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Przestrzenie nazw',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Ustawienia',
|
||||
'memory.analyzeNow': 'Analizuj teraz',
|
||||
@@ -303,6 +304,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Przegląd przestrzeni nazw',
|
||||
'namespaceOverview.intro':
|
||||
'Jak Twoja wiedza jest rozłożona w kontekstach — liczba faktów i odrębnych encji zapisanych w każdej przestrzeni nazw.',
|
||||
'namespaceOverview.loading': 'Agregowanie przestrzeni nazw…',
|
||||
'namespaceOverview.errorPrefix': 'Nie udało się załadować grafu:',
|
||||
'namespaceOverview.retry': 'Ponów',
|
||||
'namespaceOverview.empty': 'Nie ma jeszcze grafu wiedzy.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'Gdy asystent zapisuje fakty w różnych kontekstach, każda przestrzeń nazw pojawi się tutaj.',
|
||||
'namespaceOverview.metricNamespaces': 'Przestrzenie nazw',
|
||||
'namespaceOverview.metricFacts': 'Fakty',
|
||||
'namespaceOverview.metricEntities': 'Encje',
|
||||
'namespaceOverview.heading': 'Według przestrzeni nazw',
|
||||
'namespaceOverview.unnamespaced': '(bez przestrzeni nazw)',
|
||||
'namespaceOverview.factsLabel': '{count} faktów',
|
||||
'namespaceOverview.entitiesLabel': '{count} encji',
|
||||
'namespaceOverview.entitiesShort': '{count} enc.',
|
||||
'namespaceOverview.truncated': 'Wyświetlanie {shown} najważniejszych z {total} przestrzeni nazw.',
|
||||
'graphCentrality.title': 'Centralność grafu wiedzy',
|
||||
'graphCentrality.intro':
|
||||
'PageRank nad grafem pamięci pokazuje kluczowe węzły oraz encje-łączniki, które spajają oddzielne klastry, czego nie ujawnia proste zliczanie częstotliwości.',
|
||||
|
||||
@@ -286,6 +286,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Chamadas',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Namespaces',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Configurações',
|
||||
'memory.analyzeNow': 'Analisar Agora',
|
||||
@@ -307,6 +308,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Visão geral dos namespaces',
|
||||
'namespaceOverview.intro':
|
||||
'Como o seu conhecimento está distribuído entre os contextos — o número de fatos e entidades distintas registradas em cada namespace.',
|
||||
'namespaceOverview.loading': 'Agregando namespaces…',
|
||||
'namespaceOverview.errorPrefix': 'Não foi possível carregar o gráfico:',
|
||||
'namespaceOverview.retry': 'Tentar novamente',
|
||||
'namespaceOverview.empty': 'Ainda não há gráfico de conhecimento.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'À medida que o assistente registra fatos entre contextos, cada namespace aparecerá aqui.',
|
||||
'namespaceOverview.metricNamespaces': 'Namespaces',
|
||||
'namespaceOverview.metricFacts': 'Fatos',
|
||||
'namespaceOverview.metricEntities': 'Entidades',
|
||||
'namespaceOverview.heading': 'Por namespace',
|
||||
'namespaceOverview.unnamespaced': '(sem namespace)',
|
||||
'namespaceOverview.factsLabel': '{count} fatos',
|
||||
'namespaceOverview.entitiesLabel': '{count} entidades',
|
||||
'namespaceOverview.entitiesShort': '{count} ent.',
|
||||
'namespaceOverview.truncated': 'Mostrando os {shown} principais de {total} namespaces.',
|
||||
'graphCentrality.title': 'Centralidade do Grafo de Conhecimento',
|
||||
'graphCentrality.intro':
|
||||
'O PageRank sobre seu grafo de memória revela os hubs que suportam carga — e as entidades conectoras que ligam clusters que, de outra forma, seriam separados, algo que uma contagem de frequência bruta não consegue revelar.',
|
||||
|
||||
@@ -279,6 +279,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': 'Звонки',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': 'Пространства имён',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': 'Настройки',
|
||||
'memory.analyzeNow': 'Анализировать сейчас',
|
||||
@@ -300,6 +301,24 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': 'Обзор пространств имён',
|
||||
'namespaceOverview.intro':
|
||||
'Как ваши знания распределены по контекстам — количество фактов и уникальных сущностей, записанных в каждом пространстве имён.',
|
||||
'namespaceOverview.loading': 'Агрегирование пространств имён…',
|
||||
'namespaceOverview.errorPrefix': 'Не удалось загрузить график:',
|
||||
'namespaceOverview.retry': 'Повторить попытку',
|
||||
'namespaceOverview.empty': 'Графа знаний пока нет.',
|
||||
'namespaceOverview.emptyHint':
|
||||
'По мере того как ассистент записывает факты в разных контекстах, каждое пространство имён будет появляться здесь.',
|
||||
'namespaceOverview.metricNamespaces': 'Пространства имён',
|
||||
'namespaceOverview.metricFacts': 'Факты',
|
||||
'namespaceOverview.metricEntities': 'Сущности',
|
||||
'namespaceOverview.heading': 'По пространству имён',
|
||||
'namespaceOverview.unnamespaced': '(без пространства имён)',
|
||||
'namespaceOverview.factsLabel': '{count} фактов',
|
||||
'namespaceOverview.entitiesLabel': '{count} сущностей',
|
||||
'namespaceOverview.entitiesShort': '{count} сущ.',
|
||||
'namespaceOverview.truncated': 'Показаны топ-{shown} из {total} пространств имён.',
|
||||
'graphCentrality.title': 'Централизованность графа знаний',
|
||||
'graphCentrality.intro':
|
||||
'PageRank по вашему графику памяти отображает несущие нагрузку концентраторы — и объекты-соединители, которые связывают отдельные кластеры, которые не может выявить необработанный подсчет частоты.',
|
||||
|
||||
@@ -267,6 +267,7 @@ const messages: TranslationMap = {
|
||||
'memory.tab.calls': '调用记录',
|
||||
'memory.tab.diagram': 'Diagram',
|
||||
'memory.tab.centrality': 'Centrality',
|
||||
'memory.tab.namespaces': '命名空间',
|
||||
'memory.tab.timeline': 'Timeline',
|
||||
'memory.tab.settings': '设置',
|
||||
'memory.analyzeNow': '立即分析',
|
||||
@@ -288,6 +289,23 @@ const messages: TranslationMap = {
|
||||
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
|
||||
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
|
||||
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
|
||||
'namespaceOverview.title': '命名空间概览',
|
||||
'namespaceOverview.intro':
|
||||
'您的知识如何在各个上下文中分布——每个命名空间中记录的事实和不同实体的数量。',
|
||||
'namespaceOverview.loading': '正在汇总命名空间…',
|
||||
'namespaceOverview.errorPrefix': '无法加载图谱:',
|
||||
'namespaceOverview.retry': '重试',
|
||||
'namespaceOverview.empty': '暂无知识图谱。',
|
||||
'namespaceOverview.emptyHint': '随着助手在各个上下文中记录事实,每个命名空间都会显示在这里。',
|
||||
'namespaceOverview.metricNamespaces': '命名空间',
|
||||
'namespaceOverview.metricFacts': '事实',
|
||||
'namespaceOverview.metricEntities': '实体',
|
||||
'namespaceOverview.heading': '按命名空间',
|
||||
'namespaceOverview.unnamespaced': '(无命名空间)',
|
||||
'namespaceOverview.factsLabel': '{count} 条事实',
|
||||
'namespaceOverview.entitiesLabel': '{count} 个实体',
|
||||
'namespaceOverview.entitiesShort': '{count} 实体',
|
||||
'namespaceOverview.truncated': '显示 {total} 个命名空间中的前 {shown} 个。',
|
||||
'graphCentrality.title': '知识图谱中心性',
|
||||
'graphCentrality.intro':
|
||||
'对你的记忆图谱运行 PageRank,可找出关键枢纽以及连接原本分离聚类的连接实体,这是单纯频次统计无法揭示的。',
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { GraphRelation } from '../../utils/tauriCommands/memory';
|
||||
import { computeNamespaceOverview } from './namespaceOverview';
|
||||
|
||||
function rel(namespace: string | null, subject: string, object: string): GraphRelation {
|
||||
return {
|
||||
namespace,
|
||||
subject,
|
||||
predicate: 'p',
|
||||
object,
|
||||
attrs: {},
|
||||
updatedAt: 0,
|
||||
evidenceCount: 1,
|
||||
orderIndex: null,
|
||||
documentIds: [],
|
||||
chunkIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('computeNamespaceOverview', () => {
|
||||
it('returns an empty report for no relations', () => {
|
||||
expect(computeNamespaceOverview([])).toEqual({
|
||||
namespaces: [],
|
||||
namespaceCount: 0,
|
||||
totalFacts: 0,
|
||||
totalEntities: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('aggregates distinct facts and entities per namespace', () => {
|
||||
const r = computeNamespaceOverview([
|
||||
rel('work', 'A', 'B'),
|
||||
rel('work', 'B', 'C'),
|
||||
rel('personal', 'X', 'Y'),
|
||||
rel(null, 'P', 'Q'),
|
||||
]);
|
||||
expect(r.namespaceCount).toBe(3);
|
||||
expect(r.totalFacts).toBe(4);
|
||||
expect(r.totalEntities).toBe(7); // A,B,C,X,Y,P,Q
|
||||
// Sorted by factCount desc; ties by namespace asc with null last.
|
||||
expect(r.namespaces).toEqual([
|
||||
{ namespace: 'work', factCount: 2, entityCount: 3 },
|
||||
{ namespace: 'personal', factCount: 1, entityCount: 2 },
|
||||
{ namespace: null, factCount: 1, entityCount: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('de-duplicates repeated triples within a namespace', () => {
|
||||
const r = computeNamespaceOverview([rel('work', 'A', 'B'), rel('work', 'A', 'B')]);
|
||||
expect(r.namespaces[0].factCount).toBe(1);
|
||||
expect(r.totalFacts).toBe(1);
|
||||
});
|
||||
|
||||
it('counts a shared entity per-namespace but once globally', () => {
|
||||
const r = computeNamespaceOverview([rel('work', 'A', 'B'), rel('personal', 'A', 'C')]);
|
||||
const byNs = Object.fromEntries(r.namespaces.map(s => [s.namespace, s]));
|
||||
expect(byNs.work.entityCount).toBe(2); // A, B
|
||||
expect(byNs.personal.entityCount).toBe(2); // A, C
|
||||
expect(r.totalEntities).toBe(3); // A counted once globally
|
||||
});
|
||||
|
||||
it('sorts the un-namespaced (null) bucket last on a tie', () => {
|
||||
const r = computeNamespaceOverview([rel(null, 'A', 'B'), rel('aaa', 'C', 'D')]);
|
||||
expect(r.namespaces.map(s => s.namespace)).toEqual(['aaa', null]);
|
||||
});
|
||||
|
||||
it('drops malformed relations with a non-string field', () => {
|
||||
const malformed = { ...rel('work', 'A', 'B'), object: null as unknown as string };
|
||||
const r = computeNamespaceOverview([rel('work', 'A', 'B'), malformed, rel('work', 'C', 'D')]);
|
||||
expect(r.totalFacts).toBe(2);
|
||||
});
|
||||
|
||||
it('is invariant to relation order', () => {
|
||||
const triples = [rel('work', 'A', 'B'), rel('personal', 'X', 'Y'), rel('work', 'B', 'C')];
|
||||
const forward = computeNamespaceOverview(triples);
|
||||
const reversed = computeNamespaceOverview([...triples].reverse());
|
||||
expect(reversed).toEqual(forward);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Namespace Overview — pure per-namespace aggregation engine.
|
||||
*
|
||||
* Every memory fact carries a `namespace` (e.g. "work", "personal", or null for
|
||||
* un-namespaced). This is the only lens that uses the NAMESPACE as its primary
|
||||
* axis: it shows how the user's knowledge is distributed across contexts — how
|
||||
* many facts and distinct entities live in each — so lopsided or empty contexts
|
||||
* are visible at a glance.
|
||||
*
|
||||
* Everything here is PURE and DETERMINISTIC: no React, no RPC, no clock, no
|
||||
* randomness. Output depends only on the relations, never on insertion order.
|
||||
* Facts are de-duplicated per namespace by their (subject, predicate, object)
|
||||
* triple; an entity counts once per namespace it appears in.
|
||||
*/
|
||||
import type { GraphRelation } from '../../utils/tauriCommands/memory';
|
||||
|
||||
export interface NamespaceStat {
|
||||
namespace: string | null; // raw namespace; null means un-namespaced
|
||||
factCount: number; // distinct (subject, predicate, object) triples in this namespace
|
||||
entityCount: number; // distinct entities (subject or object) in this namespace
|
||||
}
|
||||
|
||||
export interface NamespaceOverviewReport {
|
||||
namespaces: NamespaceStat[]; // sorted by factCount desc, then namespace asc (null last)
|
||||
namespaceCount: number;
|
||||
totalFacts: number; // distinct triples summed across namespaces
|
||||
totalEntities: number; // distinct entities across the WHOLE graph (deduped globally)
|
||||
}
|
||||
|
||||
const EMPTY_REPORT: NamespaceOverviewReport = {
|
||||
namespaces: [],
|
||||
namespaceCount: 0,
|
||||
totalFacts: 0,
|
||||
totalEntities: 0,
|
||||
};
|
||||
|
||||
/** Canonical, collision-free key for a directed triple. */
|
||||
function tripleKey(subject: string, predicate: string, object: string): string {
|
||||
return JSON.stringify([subject, predicate, object]);
|
||||
}
|
||||
|
||||
function compareIds(a: string, b: string): number {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
interface Bucket {
|
||||
facts: Set<string>; // distinct triple keys
|
||||
entities: Set<string>; // distinct entity ids
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the per-namespace overview. Pure function of `relations`.
|
||||
*/
|
||||
export function computeNamespaceOverview(relations: GraphRelation[]): NamespaceOverviewReport {
|
||||
const buckets = new Map<string | null, Bucket>();
|
||||
const allEntities = new Set<string>();
|
||||
const ensure = (ns: string | null): Bucket => {
|
||||
let bucket = buckets.get(ns);
|
||||
if (!bucket) {
|
||||
bucket = { facts: new Set<string>(), entities: new Set<string>() };
|
||||
buckets.set(ns, bucket);
|
||||
}
|
||||
return bucket;
|
||||
};
|
||||
for (const relation of relations) {
|
||||
const { subject, predicate, object } = relation;
|
||||
if (
|
||||
typeof subject !== 'string' ||
|
||||
typeof predicate !== 'string' ||
|
||||
typeof object !== 'string'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const ns = typeof relation.namespace === 'string' ? relation.namespace : null;
|
||||
const bucket = ensure(ns);
|
||||
bucket.facts.add(tripleKey(subject, predicate, object));
|
||||
bucket.entities.add(subject);
|
||||
bucket.entities.add(object);
|
||||
allEntities.add(subject);
|
||||
allEntities.add(object);
|
||||
}
|
||||
|
||||
if (buckets.size === 0) return EMPTY_REPORT;
|
||||
|
||||
const namespaces: NamespaceStat[] = [...buckets.entries()].map(([namespace, bucket]) => ({
|
||||
namespace,
|
||||
factCount: bucket.facts.size,
|
||||
entityCount: bucket.entities.size,
|
||||
}));
|
||||
|
||||
// Sort by factCount desc, then namespace asc with null (un-namespaced) last.
|
||||
namespaces.sort((a, b) => {
|
||||
if (b.factCount !== a.factCount) return b.factCount - a.factCount;
|
||||
if (a.namespace === null) return b.namespace === null ? 0 : 1;
|
||||
if (b.namespace === null) return -1;
|
||||
return compareIds(a.namespace, b.namespace);
|
||||
});
|
||||
|
||||
let totalFacts = 0;
|
||||
for (const stat of namespaces) totalFacts += stat.factCount;
|
||||
|
||||
return {
|
||||
namespaces,
|
||||
namespaceCount: namespaces.length,
|
||||
totalFacts,
|
||||
totalEntities: allEntities.size,
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTa
|
||||
import MemoryFreshnessTab from '../components/intelligence/MemoryFreshnessTab';
|
||||
import MemoryTimelineTab from '../components/intelligence/MemoryTimelineTab';
|
||||
import { MemoryWorkspace } from '../components/intelligence/MemoryWorkspace';
|
||||
import NamespaceOverviewTab from '../components/intelligence/NamespaceOverviewTab';
|
||||
import { ToastContainer } from '../components/intelligence/Toast';
|
||||
import PillTabBar from '../components/PillTabBar';
|
||||
import {
|
||||
@@ -34,7 +35,8 @@ type IntelligenceTab =
|
||||
| 'associations'
|
||||
| 'freshness'
|
||||
| 'timeline'
|
||||
| 'path';
|
||||
| 'path'
|
||||
| 'namespaces';
|
||||
|
||||
export default function Intelligence() {
|
||||
const { t } = useT();
|
||||
@@ -118,6 +120,7 @@ export default function Intelligence() {
|
||||
{ id: 'freshness', label: t('memory.tab.freshness') },
|
||||
{ id: 'timeline', label: t('memory.tab.timeline') },
|
||||
{ id: 'path', label: t('memory.tab.path') },
|
||||
{ id: 'namespaces', label: t('memory.tab.namespaces') },
|
||||
];
|
||||
const activeTabDef = tabs.find(tab => tab.id === activeTab);
|
||||
|
||||
@@ -216,6 +219,8 @@ export default function Intelligence() {
|
||||
{activeTab === 'timeline' && <MemoryTimelineTab />}
|
||||
|
||||
{activeTab === 'path' && <ConnectionPathTab />}
|
||||
|
||||
{activeTab === 'namespaces' && <NamespaceOverviewTab />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
|
||||
import type { GraphRelation } from '../../utils/tauriCommands/memory';
|
||||
import { loadNamespaceOverview, namespaceOverviewApi } from './namespaceOverviewApi';
|
||||
|
||||
const mockGraphQuery = vi.fn();
|
||||
|
||||
vi.mock('../../utils/tauriCommands/memory', () => ({
|
||||
memoryGraphQuery: (...args: unknown[]) => mockGraphQuery(...args),
|
||||
}));
|
||||
|
||||
function rel(namespace: string | null, subject: string, object: string): GraphRelation {
|
||||
return {
|
||||
namespace,
|
||||
subject,
|
||||
predicate: 'p',
|
||||
object,
|
||||
attrs: {},
|
||||
updatedAt: 0,
|
||||
evidenceCount: 1,
|
||||
orderIndex: null,
|
||||
documentIds: [],
|
||||
chunkIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('namespaceOverviewApi.loadNamespaceOverview', () => {
|
||||
beforeEach(() => {
|
||||
mockGraphQuery.mockReset();
|
||||
});
|
||||
|
||||
it('fetches the whole graph (no namespace arg) and returns the engine report', async () => {
|
||||
const triples = [rel('work', 'A', 'B'), rel('personal', 'X', 'Y')];
|
||||
mockGraphQuery.mockResolvedValueOnce(triples);
|
||||
const out = await loadNamespaceOverview();
|
||||
expect(mockGraphQuery).toHaveBeenCalledWith();
|
||||
expect(out).toEqual(computeNamespaceOverview(triples));
|
||||
});
|
||||
|
||||
it('returns an empty report when the graph is empty', async () => {
|
||||
mockGraphQuery.mockResolvedValueOnce([]);
|
||||
const out = await loadNamespaceOverview();
|
||||
expect(out.namespaceCount).toBe(0);
|
||||
});
|
||||
|
||||
it('propagates query errors', async () => {
|
||||
mockGraphQuery.mockRejectedValueOnce(new Error('graph unavailable'));
|
||||
await expect(loadNamespaceOverview()).rejects.toThrow('graph unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('namespaceOverviewApi object', () => {
|
||||
it('exposes the public surface', () => {
|
||||
expect(typeof namespaceOverviewApi.loadNamespaceOverview).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* RPC facade for Namespace Overview.
|
||||
*
|
||||
* Adds ZERO new core surface. Reuses ONE already-shipped JSON-RPC wrapper —
|
||||
* memoryGraphQuery (openhuman.memory_graph_query) — fetching ALL namespaces in
|
||||
* one call (no namespace arg) and grouping by each relation's `namespace`
|
||||
* field in the pure engine. Read-only — nothing is persisted.
|
||||
*/
|
||||
import debug from 'debug';
|
||||
|
||||
import {
|
||||
computeNamespaceOverview,
|
||||
type NamespaceOverviewReport,
|
||||
} from '../../lib/memory/namespaceOverview';
|
||||
import { memoryGraphQuery } from '../../utils/tauriCommands/memory';
|
||||
|
||||
const log = debug('namespace-overview:api');
|
||||
|
||||
/** Fetch the whole graph and aggregate per-namespace stats. */
|
||||
export async function loadNamespaceOverview(): Promise<NamespaceOverviewReport> {
|
||||
const relations = await memoryGraphQuery();
|
||||
log('loadNamespaceOverview relations=%d', relations.length);
|
||||
return computeNamespaceOverview(relations);
|
||||
}
|
||||
|
||||
export const namespaceOverviewApi = { loadNamespaceOverview };
|
||||
Reference in New Issue
Block a user