mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
feat(onboarding): move Tools step from onboarding to Settings (#408)
Remove the Tools toggle step from onboarding (5 → 4 steps) and add it as a dedicated Tools panel under Settings > AI & Skills. This reduces onboarding friction while keeping tool configuration accessible.
This commit is contained in:
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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 {
|
||||
@@ -20,7 +20,6 @@ interface OnboardingDraft {
|
||||
localModelConsentGiven: boolean;
|
||||
localModelDownloadStarted: boolean;
|
||||
accessibilityPermissionGranted: boolean;
|
||||
enabledTools: string[];
|
||||
connectedSources: string[];
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -96,11 +94,6 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
|
||||
handleNext();
|
||||
};
|
||||
|
||||
const handleToolsNext = (enabledTools: string[]) => {
|
||||
setDraft(prev => ({ ...prev, enabledTools }));
|
||||
handleNext();
|
||||
};
|
||||
|
||||
const handleSkillsNext = async (connectedSources: string[]) => {
|
||||
setDraft(prev => ({ ...prev, connectedSources }));
|
||||
|
||||
@@ -108,7 +101,7 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
|
||||
accessibilityPermissionGranted: draft.accessibilityPermissionGranted,
|
||||
localModelConsentGiven: draft.localModelConsentGiven,
|
||||
localModelDownloadStarted: draft.localModelDownloadStarted,
|
||||
enabledTools: draft.enabledTools,
|
||||
enabledTools: getDefaultEnabledTools(),
|
||||
connectedSources,
|
||||
updatedAtMs: Date.now(),
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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] = [];
|
||||
|
||||
Reference in New Issue
Block a user