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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions app/src/components/settings/panels/ToolsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useEffect, useState } from 'react';

import { useCoreState } from '../../../providers/CoreStateProvider';
import {
CATEGORY_DESCRIPTIONS,
getDefaultEnabledTools,
getToolsByCategory,
TOOL_CATEGORIES,
} from '../../../utils/toolDefinitions';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';

const ToolsPanel = () => {
const { navigateBack } = useSettingsNavigation();
const { snapshot, setOnboardingTasks } = useCoreState();
const toolsByCategory = getToolsByCategory();

const [enabled, setEnabled] = useState<Record<string, boolean>>({});
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);

const onboardingTasks = snapshot.localState.onboardingTasks;

// Initialise toggle state from core state (persisted) or defaults.
useEffect(() => {
const persisted = onboardingTasks?.enabledTools;
const enabledList = persisted && persisted.length > 0 ? persisted : getDefaultEnabledTools();
const map: Record<string, boolean> = {};
for (const cat of TOOL_CATEGORIES) {
for (const tool of toolsByCategory[cat]) {
map[tool.id] = enabledList.includes(tool.id);
}
}
setEnabled(map);
}, [onboardingTasks?.enabledTools]); // eslint-disable-line react-hooks/exhaustive-deps

const toggle = (toolId: string) => {
setEnabled(prev => ({ ...prev, [toolId]: !prev[toolId] }));
setDirty(true);
};

const handleSave = async () => {
setSaving(true);
try {
const enabledList = Object.entries(enabled)
.filter(([, v]) => v)
.map(([k]) => k);

await setOnboardingTasks({
accessibilityPermissionGranted: onboardingTasks?.accessibilityPermissionGranted ?? false,
localModelConsentGiven: onboardingTasks?.localModelConsentGiven ?? false,
localModelDownloadStarted: onboardingTasks?.localModelDownloadStarted ?? false,
enabledTools: enabledList,
connectedSources: onboardingTasks?.connectedSources ?? [],
updatedAtMs: Date.now(),
});
setDirty(false);
} catch (err) {
console.warn('[ToolsPanel] Failed to save tool preferences:', err);
} finally {
setSaving(false);
}
};

return (
<div>
<SettingsHeader title="Tools" showBackButton onBack={navigateBack} />

<div className="px-5 pb-5">
<p className="text-stone-500 text-sm mb-4">
Choose which capabilities OpenHuman can use on your behalf.
</p>

<div className="space-y-4 max-h-[420px] overflow-y-auto pr-1">
{TOOL_CATEGORIES.map(category => {
const tools = toolsByCategory[category];
if (tools.length === 0) return null;
return (
<div key={category}>
<div className="mb-2">
<h2 className="text-xs font-semibold uppercase tracking-wide text-stone-500">
{category}
</h2>
<p className="text-xs text-stone-400">{CATEGORY_DESCRIPTIONS[category]}</p>
</div>
<div className="space-y-1">
{tools.map(tool => (
<button
key={tool.id}
type="button"
onClick={() => toggle(tool.id)}
className="w-full flex items-center justify-between p-2.5 rounded-xl border border-stone-200 bg-white hover:border-stone-300 transition-colors text-left">
<div className="min-w-0 flex-1">
<span className="text-sm font-medium text-stone-900">
{tool.displayName}
</span>
<p className="text-xs text-stone-500 mt-0.5">{tool.description}</p>
</div>
<div
className={`ml-3 flex-shrink-0 w-9 h-5 rounded-full transition-colors relative ${
enabled[tool.id] ? 'bg-sage-500' : 'bg-stone-200'
}`}>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
enabled[tool.id] ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</div>
</button>
))}
</div>
</div>
);
})}
</div>

{dirty && (
<button
type="button"
onClick={handleSave}
disabled={saving}
className="mt-4 w-full py-2 rounded-xl bg-primary-600 text-white text-sm font-medium hover:bg-primary-500 transition-colors disabled:opacity-50">
{saving ? 'Saving...' : 'Save Changes'}
</button>
)}
</div>
</div>
);
};

export default ToolsPanel;
24 changes: 24 additions & 0 deletions app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import TeamInvitesPanel from '../components/settings/panels/TeamInvitesPanel';
import TeamManagementPanel from '../components/settings/panels/TeamManagementPanel';
import TeamMembersPanel from '../components/settings/panels/TeamMembersPanel';
import TeamPanel from '../components/settings/panels/TeamPanel';
import ToolsPanel from '../components/settings/panels/ToolsPanel';
import VoicePanel from '../components/settings/panels/VoicePanel';
import WebhooksDebugPanel from '../components/settings/panels/WebhooksDebugPanel';
import SettingsHome from '../components/settings/SettingsHome';
Expand Down Expand Up @@ -226,6 +227,28 @@ const aiSettingsItems = [
</svg>
),
},
{
id: 'tools',
title: 'Tools',
description: 'Enable or disable capabilities OpenHuman can use on your behalf',
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: 'skills',
title: 'Skills',
Expand Down Expand Up @@ -295,6 +318,7 @@ const Settings = () => {
<Route path="voice" element={<VoicePanel />} />
<Route path="billing" element={<BillingPanel />} />
<Route path="skills" element={<SkillsPanel />} />
<Route path="tools" element={<ToolsPanel />} />
<Route path="team" element={<TeamPanel />} />
<Route path="team/manage/:teamId" element={<TeamManagementPanel />} />
<Route path="team/manage/:teamId/members" element={<TeamMembersPanel />} />
Expand Down
15 changes: 3 additions & 12 deletions app/src/pages/onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import ProgressIndicator from '../../components/ProgressIndicator';
import { useCoreState } from '../../providers/CoreStateProvider';
import { userApi } from '../../services/api/userApi';
import { bootstrapLocalAiWithRecommendedPreset } from '../../utils/localAiBootstrap';
import { getDefaultEnabledTools } from '../../utils/toolDefinitions';
import LocalAIStep from './steps/LocalAIStep';
import ScreenPermissionsStep from './steps/ScreenPermissionsStep';
import SkillsStep from './steps/SkillsStep';
import ToolsStep from './steps/ToolsStep';
import WelcomeStep from './steps/WelcomeStep';

interface OnboardingProps {
Expand All @@ -20,7 +20,6 @@ interface OnboardingDraft {
localModelConsentGiven: boolean;
localModelDownloadStarted: boolean;
accessibilityPermissionGranted: boolean;
enabledTools: string[];
connectedSources: string[];
}

Expand All @@ -35,10 +34,9 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
localModelConsentGiven: false,
localModelDownloadStarted: false,
accessibilityPermissionGranted: false,
enabledTools: [],
connectedSources: [],
});
const totalSteps = 5;
const totalSteps = 4;

// Auto-dismiss the error banner after LOCAL_AI_ERROR_DISMISS_MS milliseconds.
useEffect(() => {
Expand Down Expand Up @@ -96,19 +94,14 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
handleNext();
};

const handleToolsNext = (enabledTools: string[]) => {
setDraft(prev => ({ ...prev, enabledTools }));
handleNext();
};

const handleSkillsNext = async (connectedSources: string[]) => {
setDraft(prev => ({ ...prev, connectedSources }));

await setOnboardingTasks({
accessibilityPermissionGranted: draft.accessibilityPermissionGranted,
localModelConsentGiven: draft.localModelConsentGiven,
localModelDownloadStarted: draft.localModelDownloadStarted,
enabledTools: draft.enabledTools,
enabledTools: getDefaultEnabledTools(),
connectedSources,
updatedAtMs: Date.now(),
});
Expand Down Expand Up @@ -145,8 +138,6 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
case 2:
return <ScreenPermissionsStep onNext={handleAccessibilityNext} onBack={handleBack} />;
case 3:
return <ToolsStep onNext={handleToolsNext} onBack={handleBack} />;
case 4:
return <SkillsStep onNext={handleSkillsNext} onBack={handleBack} />;
default:
return null;
Expand Down
9 changes: 9 additions & 0 deletions app/src/utils/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ export const TOOL_CATALOG: ToolDefinition[] = [
},
];

export const CATEGORY_DESCRIPTIONS: Record<ToolCategory, string> = {
System: 'Shell access and version control',
Files: 'Read and write files on disk',
Vision: 'Screen capture and image analysis',
Web: 'Browser, HTTP, and web search',
Memory: 'Persistent recall for the AI',
Automation: 'Cron jobs and scheduled tasks',
};

export function getToolsByCategory(): Record<ToolCategory, ToolDefinition[]> {
const grouped = {} as Record<ToolCategory, ToolDefinition[]>;
for (const cat of TOOL_CATEGORIES) grouped[cat] = [];
Expand Down
Loading