mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
455 lines
16 KiB
TypeScript
455 lines
16 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import { useT } from '../../../lib/i18n/I18nContext';
|
|
import { triggerSentryTestEvent } from '../../../services/analytics';
|
|
import { useAppSelector } from '../../../store/hooks';
|
|
import { APP_ENVIRONMENT } from '../../../utils/config';
|
|
// `safeInvoke` (aliased to `invoke`) converts the CEF
|
|
// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 /
|
|
// TAURI-REACT-6 — into a rejected Promise so the existing `.catch(...)` /
|
|
// try/catch handlers see it as a normal IPC failure.
|
|
import { safeInvoke as invoke, isTauri } from '../../../utils/tauriCommands/common';
|
|
import { resetWalkthrough } from '../../walkthrough/AppWalkthrough';
|
|
import SettingsHeader from '../components/SettingsHeader';
|
|
import SettingsMenuItem from '../components/SettingsMenuItem';
|
|
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
|
|
|
const developerItems = [
|
|
// Items pulled in from the (now-removed) Features tile and the AI tile
|
|
// in SettingsHome: AI Configuration, Screen Awareness, Messaging
|
|
// Channels, and Tools. They're power-user destinations and don't merit
|
|
// top-level menu space.
|
|
{
|
|
id: 'ai',
|
|
titleKey: 'settings.developerMenu.ai.title',
|
|
descriptionKey: 'settings.developerMenu.ai.desc',
|
|
route: 'ai',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'screen-intelligence',
|
|
titleKey: 'settings.developerMenu.screenAwareness.title',
|
|
descriptionKey: 'settings.developerMenu.screenAwareness.desc',
|
|
route: 'screen-intelligence',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M3 5h18v12H3zM8 21h8m-4-4v4"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'tools',
|
|
titleKey: 'settings.developerMenu.tools.title',
|
|
descriptionKey: 'settings.developerMenu.tools.desc',
|
|
route: 'tools',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'cron-jobs',
|
|
titleKey: 'settings.developerMenu.cronJobs.title',
|
|
descriptionKey: 'settings.developerMenu.cronJobs.desc',
|
|
route: 'cron-jobs',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'search',
|
|
titleKey: 'settings.search.title',
|
|
descriptionKey: 'settings.search.menuDesc',
|
|
route: 'search',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'webhooks-debug',
|
|
titleKey: 'settings.developerMenu.webhooks.title',
|
|
descriptionKey: 'settings.developerMenu.webhooks.desc',
|
|
route: 'webhooks-debug',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13.828 10.172a4 4 0 010 5.656l-2 2a4 4 0 01-5.656-5.656l1-1m5-5a4 4 0 015.656 5.656l-1 1m-5 5l5-5"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'intelligence',
|
|
titleKey: 'settings.developerMenu.intelligence.title',
|
|
descriptionKey: 'settings.developerMenu.intelligence.desc',
|
|
route: 'intelligence',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
// `notification-routing` moved into the main Settings → Notifications page
|
|
// as a tab. The old `/settings/notification-routing` path now redirects to
|
|
// `/settings/notifications#routing`, so deep links continue to work.
|
|
{
|
|
id: 'composio',
|
|
titleKey: 'settings.developerMenu.composio.title',
|
|
descriptionKey: 'settings.developerMenu.composio.desc',
|
|
route: 'composio',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'mcp-server',
|
|
titleKey: 'settings.developerMenu.mcpServer.title',
|
|
descriptionKey: 'settings.developerMenu.mcpServer.desc',
|
|
route: 'mcp-server',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
const CoreModeBadge = () => {
|
|
const { t } = useT();
|
|
const mode = useAppSelector(state => state.coreMode.mode);
|
|
|
|
if (mode.kind === 'unset') {
|
|
return (
|
|
<div className="px-4 py-3 rounded-lg border border-coral-300 dark:border-coral-500/40 bg-coral-50 dark:bg-coral-500/10 dark:border-coral-500/30">
|
|
<div className="text-sm font-semibold text-coral-900 dark:text-coral-300">
|
|
{t('devOptions.coreModeNotSet')}
|
|
</div>
|
|
<div className="text-xs text-coral-800 dark:text-coral-200 mt-0.5">
|
|
{t('devOptions.coreModeNotSetDesc')}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (mode.kind === 'local') {
|
|
return (
|
|
<div className="px-4 py-3 rounded-lg border border-primary-300 dark:border-primary-500/40 bg-primary-50 dark:bg-primary-500/10 dark:border-primary-500/30">
|
|
<div className="flex items-center gap-2">
|
|
<span className="px-2 py-0.5 rounded-full bg-primary-600 text-white text-[11px] font-medium">
|
|
{t('devOptions.local')}
|
|
</span>
|
|
<span className="text-sm font-semibold text-primary-900 dark:text-primary-200">
|
|
{t('devOptions.embeddedCoreSidecar')}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-primary-800 dark:text-primary-200 mt-1">
|
|
{t('devOptions.sidecarSpawned')}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-sage-50 dark:bg-sage-500/10 dark:border-sage-500/30">
|
|
<div className="flex items-center gap-2">
|
|
<span className="px-2 py-0.5 rounded-full bg-sage-600 text-white text-[11px] font-medium">
|
|
{t('devOptions.cloud')}
|
|
</span>
|
|
<span className="text-sm font-semibold text-sage-900 dark:text-sage-200">
|
|
{t('devOptions.remoteCoreRpc')}
|
|
</span>
|
|
</div>
|
|
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
|
|
<dt className="text-sage-700 dark:text-sage-300">URL:</dt>
|
|
<dd className="font-mono text-sage-900 dark:text-sage-200 truncate" title={mode.url}>
|
|
{mode.url}
|
|
</dd>
|
|
<dt className="text-sage-700 dark:text-sage-300">{t('devOptions.token')}:</dt>
|
|
<dd className="text-sage-900 dark:text-sage-200">
|
|
{mode.token ? (
|
|
<span className="font-mono">••••••{mode.token.slice(-4)}</span>
|
|
) : (
|
|
<span className="text-coral-600 dark:text-coral-300">
|
|
{t('devOptions.tokenNotSet')}
|
|
</span>
|
|
)}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type SentryTestStatus =
|
|
| { kind: 'idle' }
|
|
| { kind: 'sending' }
|
|
| { kind: 'sent'; eventId: string | undefined }
|
|
| { kind: 'error'; message: string };
|
|
|
|
const SentryTestRow = () => {
|
|
const { t } = useT();
|
|
const [status, setStatus] = useState<SentryTestStatus>({ kind: 'idle' });
|
|
|
|
const onClick = async () => {
|
|
setStatus({ kind: 'sending' });
|
|
try {
|
|
const eventId = await triggerSentryTestEvent();
|
|
setStatus({ kind: 'sent', eventId });
|
|
} catch (err) {
|
|
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="px-4 py-3 rounded-lg border border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10 dark:border-amber-500/30">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-amber-900 dark:text-amber-300">
|
|
{t('devOptions.triggerSentryTest')}
|
|
</div>
|
|
<div className="text-xs text-amber-800 dark:text-amber-200 mt-0.5">
|
|
{t('devOptions.triggerSentryTestDesc')}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClick}
|
|
disabled={status.kind === 'sending'}
|
|
className="shrink-0 px-3 py-1.5 rounded-md bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium transition-colors disabled:opacity-60">
|
|
{status.kind === 'sending' ? t('devOptions.sending') : t('devOptions.sendTestEvent')}
|
|
</button>
|
|
</div>
|
|
<div role="status" aria-live="polite" aria-atomic="true" className="mt-2 text-xs">
|
|
{status.kind === 'sent' && (
|
|
<span className="text-amber-900 dark:text-amber-300">
|
|
{t('devOptions.eventSent')}.{' '}
|
|
{status.eventId ? (
|
|
<span className="font-mono">id: {status.eventId}</span>
|
|
) : (
|
|
<span>{t('devOptions.sentryDisabled')}</span>
|
|
)}
|
|
</span>
|
|
)}
|
|
{status.kind === 'error' && (
|
|
<span className="text-coral-600 dark:text-coral-300">
|
|
{t('devOptions.failed')}: {status.message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LogsFolderRow = () => {
|
|
const { t } = useT();
|
|
const [path, setPath] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isTauri()) return;
|
|
invoke<string | null>('logs_folder_path')
|
|
.then(p => setPath(p ?? null))
|
|
.catch(err => {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
});
|
|
}, []);
|
|
|
|
const onClick = async () => {
|
|
setError(null);
|
|
try {
|
|
await invoke('reveal_logs_folder');
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
}
|
|
};
|
|
|
|
if (!isTauri()) return null;
|
|
|
|
return (
|
|
<div className="px-4 py-3 rounded-lg border border-slate-200 dark:border-neutral-800 bg-slate-50 dark:bg-neutral-800/60">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-slate-900 dark:text-neutral-100">
|
|
{t('devOptions.appLogs')}
|
|
</div>
|
|
<div className="text-xs text-slate-700 dark:text-neutral-300 mt-0.5">
|
|
{t('devOptions.appLogsDesc')}
|
|
</div>
|
|
{path && (
|
|
<div className="text-[11px] text-slate-500 dark:text-neutral-400 mt-1 font-mono truncate">
|
|
{path}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={onClick}
|
|
className="shrink-0 px-3 py-1.5 rounded-md bg-slate-700 hover:bg-slate-600 text-white text-xs font-medium transition-colors">
|
|
{t('devOptions.openLogsFolder')}
|
|
</button>
|
|
</div>
|
|
{error && (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
className="mt-2 text-xs text-coral-600 dark:text-coral-300">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const DeveloperOptionsPanel = () => {
|
|
const { t } = useT();
|
|
const navigate = useNavigate();
|
|
const { navigateToSettings, navigateBack, breadcrumbs } = useSettingsNavigation();
|
|
const showSentryTest = APP_ENVIRONMENT === 'staging';
|
|
|
|
// Trailing items moved here from SettingsHome: About + Restart Tour. They
|
|
// don't fit cleanly in the trimmed-down top-level menu but still need a
|
|
// home, so they live here under Advanced.
|
|
const trailingItems = [
|
|
{
|
|
id: 'restart-tour',
|
|
title: t('settings.restartTour'),
|
|
description: t('settings.restartTourDesc'),
|
|
onClick: () => {
|
|
resetWalkthrough();
|
|
navigate('/home');
|
|
},
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'about',
|
|
title: t('settings.about'),
|
|
description: t('settings.aboutDesc'),
|
|
onClick: () => navigateToSettings('about'),
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="z-10 relative">
|
|
<SettingsHeader
|
|
title={t('devOptions.title')}
|
|
showBackButton={true}
|
|
onBack={navigateBack}
|
|
breadcrumbs={breadcrumbs}
|
|
/>
|
|
|
|
<div>
|
|
{developerItems.map((item, index) => (
|
|
<SettingsMenuItem
|
|
key={item.id}
|
|
icon={item.icon}
|
|
title={t(item.titleKey)}
|
|
description={t(item.descriptionKey)}
|
|
onClick={() => navigateToSettings(item.route)}
|
|
testId={`settings-nav-${item.id}`}
|
|
isFirst={index === 0}
|
|
isLast={false}
|
|
/>
|
|
))}
|
|
{trailingItems.map((item, index) => (
|
|
<SettingsMenuItem
|
|
key={item.id}
|
|
icon={item.icon}
|
|
title={item.title}
|
|
description={item.description}
|
|
onClick={item.onClick}
|
|
testId={`settings-nav-${item.id}`}
|
|
isFirst={false}
|
|
isLast={index === trailingItems.length - 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
{/* Diagnostics callouts live outside the menu card so the spacing
|
|
and alignment don't clash with the SettingsMenuItem rows. */}
|
|
<div className="px-4 pt-6 flex flex-col gap-3">
|
|
<CoreModeBadge />
|
|
<LogsFolderRow />
|
|
{showSentryTest && <SentryTestRow />}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DeveloperOptionsPanel;
|