mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 15:03:57 +00:00
Improve settings, rewards, chat, and skills UI polish (#328)
* feat: add Rewards page and enhance Settings navigation - Introduced a new Rewards page, accessible via a protected route. - Updated the Settings component to include a new SettingsSectionPage for better organization of account and automation settings. - Refactored the SettingsHome component to group menu items and improve navigation. - Enhanced the useSettingsNavigation hook to support new routes for account and automation settings. * feat: implement app-dotted-canvas styling across multiple pages - Added a new CSS class `app-dotted-canvas` to create a dotted background effect for various components. - Updated the App, Agents, Conversations, Home, Intelligence, Invites, Mnemonic, Rewards, Settings, Skills, Webhooks, and Welcome pages to utilize the new styling, enhancing visual consistency across the application. - Refactored the BottomTabBar component's SVG path for improved icon representation. * refactor: update BottomTabBar styling for improved UI consistency - Modified the BottomTabBar component's navigation and button styles to enhance visual appeal and user experience. - Adjusted class names for better alignment with design standards, including changes to padding, border, and background colors. - Improved active and inactive button states for clearer user interaction feedback. * refactor: enhance layout and styling in App and Settings components - Adjusted padding in the App component to improve layout consistency. - Updated BottomTabBar component to enhance styling and user interaction. - Removed deprecated delete data functionality from SettingsHome and refined related menu items for clarity. - Modified Settings page layout for better responsiveness and visual appeal. * refactor: improve layout and styling in Settings and Skills components - Simplified the rendering of grouped and destructive settings in SettingsHome for better readability. - Adjusted padding and margin in SettingsMenuItem for a more consistent button appearance. - Introduced BuiltInSkillCard component in Skills page to enhance the display of built-in skills with improved navigation. - Updated button styles in SkillActionButton for better visual consistency and user experience. * style: align third-party skill card typography * style: refine built-in and connected skills layout * style: update Conversations component for improved UI consistency - Changed background color of message container to `bg-stone-50` for better visual contrast. - Adjusted input field layout to enhance user experience, including centering elements and refining focus styles. - Updated class names for input field to ensure consistent focus behavior and visual feedback. * feat: enhance settings navigation with fallback functionality - Introduced a new `goBackWithFallback` function to improve navigation behavior when the user attempts to go back in the settings. - Updated the `navigateBack` and `closeSettings` functions to utilize the new fallback mechanism, ensuring a smoother user experience when navigating through settings routes. - Removed redundant route handling logic to streamline the navigation process. * style: remove app-dotted-canvas class for consistent layout across pages - Eliminated the `app-dotted-canvas` class from multiple components to streamline styling and ensure a uniform layout. - Updated the Agents, Conversations, Home, Intelligence, Invites, Mnemonic, Rewards, Settings, Skills, Webhooks, and Welcome pages for improved visual consistency. * style: improve code formatting and readability in Rewards and Skills components - Reformatted code in the Rewards component for better readability by adjusting indentation and line breaks. - Enhanced the Skills component by updating the description formatting for clarity and consistency. - Ensured consistent styling practices across both components to maintain a clean codebase.
This commit is contained in:
+2
-2
@@ -29,8 +29,8 @@ function App() {
|
||||
<SocketProvider>
|
||||
<Router>
|
||||
<ServiceBlockingGate>
|
||||
<div className="relative h-screen flex flex-col overflow-hidden bg-[#F5F5F5]">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="app-dotted-canvas relative h-screen flex flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto pb-16">
|
||||
<AppRoutes />
|
||||
</div>
|
||||
<BottomTabBar />
|
||||
|
||||
@@ -9,6 +9,7 @@ import Conversations from './pages/Conversations';
|
||||
import Home from './pages/Home';
|
||||
import Intelligence from './pages/Intelligence';
|
||||
import Invites from './pages/Invites';
|
||||
import Rewards from './pages/Rewards';
|
||||
import Settings from './pages/Settings';
|
||||
import Skills from './pages/Skills';
|
||||
import Webhooks from './pages/Webhooks';
|
||||
@@ -90,6 +91,15 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/rewards"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<Rewards />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/agents"
|
||||
element={
|
||||
|
||||
@@ -74,7 +74,7 @@ const tabs = [
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a2 2 0 01-2-2v-4a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2z"
|
||||
d="M12 8v8m0-8l-3-3m3 3l3-3M8 14H6a2 2 0 01-2-2V7a2 2 0 012-2h2m8 9h2a2 2 0 002-2V7a2 2 0 00-2-2h-2M7 19h10"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
@@ -142,8 +142,8 @@ const BottomTabBar = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 flex justify-center pb-4 pt-2 z-50">
|
||||
<nav className="inline-flex items-center gap-6 bg-white rounded-full border border-stone-200 shadow-soft px-4 py-2.5">
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center px-4 pb-4 pt-2 z-50">
|
||||
<nav className="pointer-events-auto inline-flex items-center gap-2 rounded-sm border border-stone-300 bg-stone-200 shadow-soft px-1 py-1">
|
||||
{tabs.map(tab => {
|
||||
const active = isActive(tab.path);
|
||||
const showBadge = tab.id === 'chat' && conversationsUnreadCount > 0;
|
||||
@@ -151,10 +151,10 @@ const BottomTabBar = () => {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => navigate(tab.path)}
|
||||
className={`relative flex items-center gap-2 px-4 py-2 rounded-full text-sm transition-colors duration-150 cursor-pointer ${
|
||||
className={`relative flex items-center gap-2 px-4 py-2 rounded-sm text-sm transition-colors duration-150 cursor-pointer ${
|
||||
active
|
||||
? 'bg-stone-100 text-stone-900 font-semibold'
|
||||
: 'text-stone-400 hover:text-stone-600'
|
||||
? 'bg-white text-stone-900 font-semibold shadow-sm'
|
||||
: 'bg-transparent text-stone-500 hover:bg-stone-300/50 hover:text-stone-700'
|
||||
}`}
|
||||
aria-label={tab.label}>
|
||||
{tab.icon}
|
||||
|
||||
@@ -75,34 +75,23 @@ const SettingsHome = () => {
|
||||
// console.log('View encryption key');
|
||||
// };
|
||||
|
||||
const handleDeleteAllData = () => {
|
||||
// TODO: Show confirmation dialog and delete all data
|
||||
console.log('Delete all data');
|
||||
};
|
||||
|
||||
// Main settings menu items
|
||||
const mainMenuItems = [
|
||||
const groupedMenuItems = [
|
||||
{
|
||||
id: 'accessibility',
|
||||
title: 'Accessibility Automation',
|
||||
description: 'Desktop permissions, assisted controls, and safety-bound sessions',
|
||||
id: 'account',
|
||||
title: 'Account & Security',
|
||||
description: 'Billing, recovery phrase, team management, and linked account access',
|
||||
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 12h6m-7 9h8a2 2 0 002-2V7a2 2 0 00-2-2h-1l-.707-.707A1 1 0 0013.586 4h-3.172a1 1 0 00-.707.293L9 5H8a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5v14" />
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('accessibility'),
|
||||
onClick: () => navigateToSettings('account'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'screen-intelligence',
|
||||
title: 'Screen Intelligence',
|
||||
description: 'Window capture policy, vision summaries, and memory ingestion',
|
||||
id: 'automation',
|
||||
title: 'Automation & Channels',
|
||||
description: 'Accessibility, screen intelligence, messaging, autocomplete, and cron jobs',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -113,177 +102,13 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('screen-intelligence'),
|
||||
onClick: () => navigateToSettings('automation'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'autocomplete',
|
||||
title: 'Inline Autocomplete',
|
||||
description: 'Manage predictive text style, app filters, and live completion controls',
|
||||
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 7h16M4 12h10m-10 5h7m10 0l3 3m0 0l3-3m-3 3v-8"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('autocomplete'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'messaging',
|
||||
title: 'Messaging Channels',
|
||||
description: 'Configure Telegram/Discord auth modes and default channel routing',
|
||||
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 10h.01M12 10h.01M16 10h.01M21 11c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 19l1.395-3.72C3.512 14.042 3 12.574 3 11c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('messaging'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'cron-jobs',
|
||||
title: 'Cron Jobs',
|
||||
description: 'View and configure available scheduled jobs for runtime skills',
|
||||
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>
|
||||
),
|
||||
onClick: () => navigateToSettings('cron-jobs'),
|
||||
dangerous: false,
|
||||
},
|
||||
// {
|
||||
// id: "messaging",
|
||||
// title: "Messaging",
|
||||
// description: "Configure messaging preferences and templates",
|
||||
// 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 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings("messaging"),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'agent-chat',
|
||||
// title: 'Agent Chat',
|
||||
// description: 'Send messages directly to your agent',
|
||||
// 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 10h8m-8 4h5m-6 6l-4 4V6a2 2 0 012-2h12a2 2 0 012 2v9a2 2 0 01-2 2H7z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('agent-chat'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'privacy',
|
||||
// title: 'Privacy & Security',
|
||||
// description: 'Control your privacy and security settings',
|
||||
// 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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('privacy'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'profile',
|
||||
// title: 'Profile',
|
||||
// description: 'Update your profile information and preferences',
|
||||
// icon: (
|
||||
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('profile'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'advanced',
|
||||
// title: 'Advanced',
|
||||
// description: 'Advanced configuration and developer options',
|
||||
// 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.065 2.572c1.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.572 1.065c-.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.065-2.572c-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>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('advanced'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'encryption',
|
||||
// title: 'View Encryption Key',
|
||||
// description: 'Access your encryption key for backup purposes',
|
||||
// icon: (
|
||||
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M15 7a2 2 0 0 1 2 2m4 0a6 6 0 0 1-7.743 5.743L11 17H9v2H7v2H4a1 1 0 0 1-1-1v-2.586a1 1 0 0 1 .293-.707l5.964-5.964A6 6 0 1 1 21 9z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: handleViewEncryptionKey,
|
||||
// dangerous: false,
|
||||
// },
|
||||
{
|
||||
id: 'local-model',
|
||||
title: 'Local AI Model',
|
||||
description: 'Choose model tier by device capability and manage downloads',
|
||||
id: 'ai-tools',
|
||||
title: 'AI & Skills',
|
||||
description: 'Local model runtime, AI configuration, and skills behavior',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -294,64 +119,13 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('local-model'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
title: 'Team',
|
||||
description: 'Manage your team and invite members',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('team'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing & Usage',
|
||||
description: 'Manage your subscription and payment methods',
|
||||
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 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('billing'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'recovery-phrase',
|
||||
title: 'Recovery Phrase',
|
||||
description: 'Generate or import your BIP39 recovery phrase for encryption and wallet',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('recovery-phrase'),
|
||||
onClick: () => navigateToSettings('ai-tools'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'developer-options',
|
||||
title: 'Developer Options',
|
||||
description: 'Skills, AI config, Tauri console, and memory debug tools',
|
||||
description: 'Diagnostic tools, console access, webhooks, and memory inspection',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -369,27 +143,10 @@ const SettingsHome = () => {
|
||||
|
||||
// Destructive actions menu items
|
||||
const destructiveMenuItems = [
|
||||
{
|
||||
id: 'delete',
|
||||
title: 'Delete All Data',
|
||||
description: 'Permanently delete all your data and reset your account',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleDeleteAllData,
|
||||
dangerous: true,
|
||||
},
|
||||
{
|
||||
id: 'logout-and-clear',
|
||||
title: 'Log Out & Clear App Data',
|
||||
description: 'Sign out and permanently clear all local data',
|
||||
title: 'Clear App Data',
|
||||
description: 'Sign out and permanently clear all local app data',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -427,39 +184,33 @@ const SettingsHome = () => {
|
||||
<SettingsHeader />
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Main Settings */}
|
||||
<div>
|
||||
{mainMenuItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === mainMenuItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Grouped Settings */}
|
||||
{groupedMenuItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
// isLast={index === groupedMenuItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Destructive Actions */}
|
||||
<div>
|
||||
{destructiveMenuItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === destructiveMenuItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Destructive Actions */}
|
||||
{destructiveMenuItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === destructiveMenuItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Log Out & Clear Data Confirmation Modal */}
|
||||
@@ -482,15 +233,15 @@ const SettingsHome = () => {
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-stone-900">Log Out & Clear App Data</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900">Clear App Data</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-stone-700 text-sm leading-relaxed">
|
||||
This will sign you out and permanently delete ALL data including: • App settings and
|
||||
conversations • Email data from Gmail • Chat history from Telegram • Cached files
|
||||
from Notion • All other skills data
|
||||
This will sign you out and permanently delete local app data including: • App
|
||||
settings and conversations • Email data from Gmail • Chat history from Telegram •
|
||||
Cached files from Notion • All other skills data
|
||||
<br />
|
||||
<br />
|
||||
This action cannot be undone and may take a few moments to complete.
|
||||
@@ -516,7 +267,7 @@ const SettingsHome = () => {
|
||||
<button
|
||||
onClick={handleLogoutAndClearData}
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-500 text-white transition-colors disabled:opacity-50 flex items-center justify-center gap-2">
|
||||
className="flex-1 px-4 py-2 rounded-sm bg-amber-600 hover:bg-amber-500 text-white transition-colors disabled:opacity-50 flex items-center justify-center gap-2">
|
||||
{isLoading && (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
@@ -534,7 +285,7 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{isLoading ? 'Clearing All Data...' : 'Log Out & Clear Everything'}
|
||||
{isLoading ? 'Clearing App Data...' : 'Clear App Data'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
export interface SettingsSectionItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon: ReactNode;
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface SettingsSectionPageProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
items: SettingsSectionItem[];
|
||||
}
|
||||
|
||||
const SettingsSectionPage = ({ title, description, items }: SettingsSectionPageProps) => {
|
||||
const { navigateBack, navigateToSettings } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden h-full flex flex-col z-10 relative">
|
||||
<SettingsHeader title={title} showBackButton={true} onBack={navigateBack} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 space-y-4">
|
||||
{description && <p className="text-sm text-stone-500 px-1">{description}</p>}
|
||||
|
||||
<div>
|
||||
{items.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={() => navigateToSettings(item.route)}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsSectionPage;
|
||||
@@ -31,7 +31,7 @@ const SettingsMenuItem = ({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center justify-between p-3 bg-white ${borderClasses} hover:bg-stone-50 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-white ${borderClasses} hover:bg-stone-50 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
<div className={`w-5 h-5 opacity-60 flex-shrink-0 mr-3 ${iconColor}`}>{icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>{title}</div>
|
||||
|
||||
@@ -3,6 +3,9 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export type SettingsRoute =
|
||||
| 'home'
|
||||
| 'account'
|
||||
| 'automation'
|
||||
| 'ai-tools'
|
||||
| 'connections'
|
||||
| 'messaging'
|
||||
| 'cron-jobs'
|
||||
@@ -22,7 +25,9 @@ export type SettingsRoute =
|
||||
| 'local-model'
|
||||
| 'tauri-commands'
|
||||
| 'memory-debug'
|
||||
| 'recovery-phrase';
|
||||
| 'recovery-phrase'
|
||||
| 'webhooks-debug'
|
||||
| 'agent-chat';
|
||||
|
||||
interface SettingsNavigationHook {
|
||||
currentRoute: SettingsRoute;
|
||||
@@ -36,6 +41,18 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const goBackWithFallback = useCallback(
|
||||
(fallbackPath: string) => {
|
||||
const historyState = window.history.state as { idx?: number } | null;
|
||||
if (typeof historyState?.idx === 'number' && historyState.idx > 0) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
navigate(fallbackPath);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
// Determine current settings route from URL
|
||||
const getCurrentRoute = (): SettingsRoute => {
|
||||
const path = location.pathname;
|
||||
@@ -47,6 +64,9 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
if (path.includes('/settings/team/members')) return 'team-members';
|
||||
if (path.includes('/settings/team/invites')) return 'team-invites';
|
||||
if (path.includes('/settings/team')) return 'team';
|
||||
if (path.includes('/settings/account')) return 'account';
|
||||
if (path.includes('/settings/automation')) return 'automation';
|
||||
if (path.includes('/settings/ai-tools')) return 'ai-tools';
|
||||
if (path.includes('/settings/connections')) return 'connections';
|
||||
if (path.includes('/settings/messaging')) return 'messaging';
|
||||
if (path.includes('/settings/cron-jobs')) return 'cron-jobs';
|
||||
@@ -63,7 +83,9 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
if (path.includes('/settings/local-model')) return 'local-model';
|
||||
if (path.includes('/settings/tauri-commands')) return 'tauri-commands';
|
||||
if (path.includes('/settings/memory-debug')) return 'memory-debug';
|
||||
if (path.includes('/settings/webhooks-debug')) return 'webhooks-debug';
|
||||
if (path.includes('/settings/recovery-phrase')) return 'recovery-phrase';
|
||||
if (path.includes('/settings/agent-chat')) return 'agent-chat';
|
||||
return 'home';
|
||||
};
|
||||
|
||||
@@ -87,38 +109,17 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const developerSubRoutes: SettingsRoute[] = [
|
||||
'skills',
|
||||
'ai',
|
||||
'local-model',
|
||||
'tauri-commands',
|
||||
'memory-debug',
|
||||
];
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
if (currentRoute === 'home') {
|
||||
navigate('/home');
|
||||
} else if (currentRoute === 'team-members' || currentRoute === 'team-invites') {
|
||||
// Check if we're in team management context (has teamId in URL)
|
||||
const teamManageMatch = location.pathname.match(/\/team\/manage\/([^/]+)/);
|
||||
if (teamManageMatch) {
|
||||
const teamId = teamManageMatch[1];
|
||||
navigate(`/settings/team/manage/${teamId}`);
|
||||
} else {
|
||||
navigate('/settings/team');
|
||||
}
|
||||
} else if (location.pathname.includes('/team/manage/')) {
|
||||
navigate('/settings/team');
|
||||
} else if (developerSubRoutes.includes(currentRoute as SettingsRoute)) {
|
||||
navigate('/settings/developer-options');
|
||||
} else {
|
||||
navigate('/settings');
|
||||
goBackWithFallback('/home');
|
||||
return;
|
||||
}
|
||||
}, [navigate, currentRoute, location.pathname]);
|
||||
goBackWithFallback('/settings');
|
||||
}, [currentRoute, goBackWithFallback]);
|
||||
|
||||
const closeSettings = useCallback(() => {
|
||||
navigate('/home');
|
||||
}, [navigate]);
|
||||
goBackWithFallback('/home');
|
||||
}, [goBackWithFallback]);
|
||||
|
||||
return {
|
||||
currentRoute,
|
||||
|
||||
@@ -133,7 +133,7 @@ export function SkillActionButton({
|
||||
return (
|
||||
<button
|
||||
onClick={handleEnable}
|
||||
className="px-4 py-1.5 text-xs font-medium text-sage-300 bg-sage-500/10 border border-sage-500/30 rounded-lg hover:bg-sage-500/20 transition-colors flex-shrink-0 ml-3">
|
||||
className="ml-3 flex-shrink-0 rounded-lg border border-sage-200 bg-sage-50 px-4 py-1.5 text-xs font-medium text-sage-700 transition-colors hover:bg-sage-100">
|
||||
Enable
|
||||
</button>
|
||||
);
|
||||
@@ -146,7 +146,7 @@ export function SkillActionButton({
|
||||
e.stopPropagation();
|
||||
onOpenModal();
|
||||
}}
|
||||
className="px-4 py-1.5 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-lg hover:bg-primary-500/20 transition-colors flex-shrink-0 ml-3">
|
||||
className="ml-3 flex-shrink-0 rounded-lg border border-primary-200 bg-primary-50 px-4 py-1.5 text-xs font-medium text-primary-700 transition-colors hover:bg-primary-100">
|
||||
Setup
|
||||
</button>
|
||||
);
|
||||
@@ -156,7 +156,7 @@ export function SkillActionButton({
|
||||
return (
|
||||
<button
|
||||
onClick={handleEnable}
|
||||
className="px-4 py-1.5 text-xs font-medium text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded-lg hover:bg-amber-500/20 transition-colors flex-shrink-0 ml-3">
|
||||
className="ml-3 flex-shrink-0 rounded-lg border border-amber-200 bg-amber-50 px-4 py-1.5 text-xs font-medium text-amber-700 transition-colors hover:bg-amber-100">
|
||||
Retry
|
||||
</button>
|
||||
);
|
||||
@@ -168,7 +168,7 @@ export function SkillActionButton({
|
||||
e.stopPropagation();
|
||||
onOpenModal();
|
||||
}}
|
||||
className="px-4 py-1.5 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-lg hover:bg-primary-500/20 transition-colors flex-shrink-0 ml-3">
|
||||
className="ml-3 flex-shrink-0 rounded-lg border border-primary-200 bg-primary-50 px-4 py-1.5 text-xs font-medium text-primary-700 transition-colors hover:bg-primary-100">
|
||||
Configure
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -152,6 +152,13 @@
|
||||
@apply card hover:shadow-medium;
|
||||
}
|
||||
|
||||
.app-dotted-canvas {
|
||||
background-color: #f8f7f2;
|
||||
background-image: radial-gradient(circle at center, #cdcdcd 1px, transparent 1px);
|
||||
background-size: 18px 18px;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* Input components with consistent styling */
|
||||
.input-primary {
|
||||
@apply w-full px-4 py-3 rounded-lg;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Agents = () => {
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5] flex items-center justify-center p-4 pt-6">
|
||||
<div className="min-h-full flex items-center justify-center p-4 pt-6">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-8 animate-fade-up text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
|
||||
+30
-122
@@ -6,7 +6,6 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { type ChatSendError, chatSendError } from '../chat/chatSendError';
|
||||
import { useLocalModelStatus } from '../hooks/useLocalModelStatus';
|
||||
import { creditsApi, type TeamUsage } from '../services/api/creditsApi';
|
||||
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
|
||||
import {
|
||||
chatCancel,
|
||||
chatSend,
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
|
||||
const DEFAULT_THREAD_ID = 'default-thread';
|
||||
const DEFAULT_THREAD_TITLE = 'Conversation';
|
||||
const AGENTIC_MODEL_ID = 'agentic-v1';
|
||||
type ToolTimelineEntryStatus = 'running' | 'success' | 'error';
|
||||
type InputMode = 'text' | 'voice';
|
||||
type ReplyMode = 'text' | 'voice';
|
||||
@@ -134,9 +134,6 @@ const Conversations = () => {
|
||||
const [isPlayingReply, setIsPlayingReply] = useState(false);
|
||||
const [inlineSuggestionValue, setInlineSuggestionValue] = useState('');
|
||||
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState('agentic-v1');
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [sendError, setSendError] = useState<ChatSendError | null>(null);
|
||||
const socketStatus = useAppSelector(selectSocketStatus);
|
||||
@@ -210,23 +207,6 @@ const Conversations = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingModels(true);
|
||||
inferenceApi
|
||||
.listModels()
|
||||
.then(data => {
|
||||
if (data.data.length > 0) {
|
||||
setAvailableModels(data.data);
|
||||
const preferred = data.data.find(m => m.id === 'agentic-v1');
|
||||
setSelectedModel(preferred ? preferred.id : data.data[0].id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep default model on failure
|
||||
})
|
||||
.finally(() => setIsLoadingModels(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingBudget(true);
|
||||
creditsApi
|
||||
@@ -339,6 +319,14 @@ const Conversations = () => {
|
||||
}
|
||||
}, [inputMode, isRecording]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputMode === 'voice') {
|
||||
setReplyMode('voice');
|
||||
} else if (replyMode === 'voice') {
|
||||
setReplyMode('text');
|
||||
}
|
||||
}, [inputMode, replyMode]);
|
||||
|
||||
// Proactively check voice binary availability when switching to voice mode
|
||||
useEffect(() => {
|
||||
if (inputMode !== 'voice' || !rustChat) return;
|
||||
@@ -701,7 +689,7 @@ const Conversations = () => {
|
||||
|
||||
// ── Cloud socket path (unchanged) ────────────────────────────────────────
|
||||
try {
|
||||
await chatSend({ threadId: sendingThreadId, message: trimmed, model: selectedModel });
|
||||
await chatSend({ threadId: sendingThreadId, message: trimmed, model: AGENTIC_MODEL_ID });
|
||||
|
||||
// setIsSending(false) and setActiveThread(null) happen in the onDone/onError event handlers
|
||||
} catch (err) {
|
||||
@@ -922,36 +910,15 @@ const Conversations = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedThread = threads.find(t => t.id === selectedThreadId);
|
||||
const selectedThreadToolTimeline = selectedThreadId
|
||||
? (toolTimelineByThread[selectedThreadId] ?? [])
|
||||
: [];
|
||||
const inlineCompletionSuffix = getInlineCompletionSuffix(inputValue, inlineSuggestionValue);
|
||||
|
||||
return (
|
||||
<div className="h-full relative z-10 flex justify-center overflow-hidden bg-[#F5F5F5] p-4 pt-6">
|
||||
<div className="h-full relative z-10 flex justify-center overflow-hidden p-4 pt-6">
|
||||
<div className="flex-1 flex flex-col min-w-0 max-w-2xl bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-6 py-4 border-b border-stone-200">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-xl font-bold text-stone-900 truncate">
|
||||
{selectedThread?.title || DEFAULT_THREAD_TITLE}
|
||||
</h3>
|
||||
{selectedThread?.isActive && (
|
||||
<span className="text-[11px] px-2.5 py-0.5 rounded-full border border-sage-300 bg-sage-50 text-sage-600 font-medium flex-shrink-0">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedThread?.createdAt && (
|
||||
<p className="text-xs text-stone-400 mt-0.5">
|
||||
Created {formatRelativeTime(selectedThread.createdAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4 bg-stone-50">
|
||||
{isLoadingMessages ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
@@ -1219,81 +1186,7 @@ const Conversations = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{isLoadingModels ? (
|
||||
<span className="text-xs text-stone-600">Loading models…</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-sm font-medium text-stone-700">Model</span>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={e => setSelectedModel(e.target.value)}
|
||||
disabled={isSending}
|
||||
className="bg-stone-100 border-0 rounded-full px-3 py-1 text-xs text-stone-600 focus:outline-none focus:ring-1 focus:ring-primary-500/50 disabled:opacity-50 cursor-pointer">
|
||||
{availableModels.length > 0 ? (
|
||||
availableModels.map(m => (
|
||||
<option key={m.id} value={m.id} className="bg-white">
|
||||
{m.id}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value={selectedModel} className="bg-white">
|
||||
{selectedModel}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-1 rounded-lg border border-stone-200 bg-stone-50 p-1">
|
||||
<span className="text-[10px] text-stone-500 px-1">Input</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputMode('text')}
|
||||
disabled={isRecording || isTranscribing}
|
||||
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
|
||||
inputMode === 'text'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-stone-500 hover:bg-stone-100'
|
||||
}`}>
|
||||
Text
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputMode('voice')}
|
||||
disabled={isRecording || isTranscribing || !rustChat || !canUseMicrophoneApi}
|
||||
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
|
||||
inputMode === 'voice'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-stone-500 hover:bg-stone-100'
|
||||
}`}>
|
||||
Voice
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-lg border border-stone-200 bg-stone-50 p-1">
|
||||
<span className="text-[10px] text-stone-500 px-1">Reply</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReplyMode('text')}
|
||||
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
|
||||
replyMode === 'text'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-stone-500 hover:bg-stone-100'
|
||||
}`}>
|
||||
Text
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReplyMode('voice')}
|
||||
disabled={!rustChat}
|
||||
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
|
||||
replyMode === 'voice'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-stone-500 hover:bg-stone-100'
|
||||
}`}>
|
||||
Voice
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 mb-2">
|
||||
{(isLoadingBudget || teamUsage) && (
|
||||
<div className="relative group">
|
||||
{teamUsage ? (
|
||||
@@ -1371,7 +1264,7 @@ const Conversations = () => {
|
||||
|
||||
{inputMode === 'text' ? (
|
||||
<div className="flex items-end gap-3">
|
||||
<div className="relative flex-1 rounded-xl border border-stone-200 bg-white focus-within:ring-1 focus-within:ring-primary-500/50 focus-within:border-primary-500/50 transition-all">
|
||||
<div className="relative flex flex-1 items-center justify-center rounded-xl border border-stone-200 bg-white transition-all focus-within:border-primary-500/50 focus-within:ring-1 focus-within:ring-primary-500/50">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden whitespace-pre-wrap break-words px-4 py-2.5 text-sm leading-normal font-sans">
|
||||
@@ -1386,7 +1279,7 @@ const Conversations = () => {
|
||||
placeholder="Type a message..."
|
||||
rows={1}
|
||||
disabled={isSending || !rustChat}
|
||||
className="relative z-10 w-full resize-none border-0 bg-transparent pl-4 pr-10 py-2.5 text-sm leading-normal whitespace-pre-wrap break-words font-sans text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-0 max-h-32 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="relative z-10 w-full resize-none border-0 bg-transparent pl-4 pr-10 py-2.5 text-sm leading-normal whitespace-pre-wrap break-words font-sans text-stone-900 placeholder:text-stone-400 outline-none focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0 max-h-32 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
{/* Mic icon inside input */}
|
||||
<button
|
||||
@@ -1441,6 +1334,21 @@ const Conversations = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputMode('text')}
|
||||
disabled={isRecording || isTranscribing}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full border border-stone-200 bg-white text-stone-500 hover:text-stone-700 hover:border-stone-300 transition-colors disabled:opacity-40"
|
||||
title="Switch to text input">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M4 6h16M4 12h10m-10 6h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
||||
@@ -163,7 +163,7 @@ const Home = () => {
|
||||
const etaText = formatEta(localAiStatus?.eta_seconds);
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col items-center justify-center p-4 bg-[#F5F5F5]">
|
||||
<div className="min-h-full flex flex-col items-center justify-center p-4">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Main card */}
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6 animate-fade-up">
|
||||
|
||||
@@ -245,7 +245,7 @@ export default function Intelligence() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5] p-4 pt-6">
|
||||
<div className="min-h-full p-4 pt-6">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6">
|
||||
<div>
|
||||
|
||||
@@ -94,7 +94,7 @@ const Invites = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5] flex items-center justify-center p-4 pt-6">
|
||||
<div className="min-h-full flex items-center justify-center p-4 pt-6">
|
||||
<div className="max-w-md w-full space-y-4">
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -183,7 +183,7 @@ const Mnemonic = () => {
|
||||
const canContinue = mode === 'generate' ? confirmed : isImportComplete;
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5] flex items-center justify-center p-4 pt-6">
|
||||
<div className="min-h-full flex items-center justify-center p-4 pt-6">
|
||||
<div className="max-w-lg w-full">
|
||||
<div className="flex justify-center mb-6">
|
||||
<LottieAnimation src="/lottie/safe3.json" height={120} width={120} />
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useUser } from '../hooks/useUser';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
const DISCORD_INVITE_URL = 'https://discord.com/invite/k23Kn8nK';
|
||||
|
||||
interface RewardRole {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actionLabel: string;
|
||||
unlocked: boolean;
|
||||
progressLabel: string;
|
||||
}
|
||||
|
||||
const Rewards = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUser();
|
||||
const threads = useAppSelector(state => state.thread.threads);
|
||||
const channelConnections = useAppSelector(state => state.channelConnections.connections);
|
||||
|
||||
const totalMessages = useAppSelector(state =>
|
||||
Object.values(state.thread.messagesByThreadId).reduce(
|
||||
(sum, messages) => sum + messages.length,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const hasDiscordConnection = useMemo(
|
||||
() =>
|
||||
Object.values(channelConnections.discord).some(
|
||||
connection => connection?.status === 'connected'
|
||||
),
|
||||
[channelConnections.discord]
|
||||
);
|
||||
|
||||
const rewardRoles: RewardRole[] = [
|
||||
{
|
||||
id: 'first-contact',
|
||||
title: 'First Contact',
|
||||
description: 'Send your first message to OpenHuman.',
|
||||
actionLabel: 'Start one chat',
|
||||
unlocked: totalMessages > 0,
|
||||
progressLabel: totalMessages > 0 ? 'Unlocked' : '0 / 1 messages sent',
|
||||
},
|
||||
{
|
||||
id: 'discord-pilot',
|
||||
title: 'Discord Pilot',
|
||||
description: 'Link Discord messaging so OpenHuman can reach you there.',
|
||||
actionLabel: 'Connect Discord in Messaging',
|
||||
unlocked: hasDiscordConnection,
|
||||
progressLabel: hasDiscordConnection ? 'Unlocked' : 'Discord not connected yet',
|
||||
},
|
||||
{
|
||||
id: 'power-user',
|
||||
title: 'Power User',
|
||||
description: 'Build momentum by actively using OpenHuman in multiple sessions.',
|
||||
actionLabel: 'Reach 10 total chat messages',
|
||||
unlocked: totalMessages >= 10,
|
||||
progressLabel: `${Math.min(totalMessages, 10)} / 10 messages`,
|
||||
},
|
||||
{
|
||||
id: 'supporter',
|
||||
title: 'Supporter',
|
||||
description: 'Unlock the supporter role with an active paid plan.',
|
||||
actionLabel: 'Upgrade to Basic or Pro',
|
||||
unlocked: !!user?.subscription?.hasActiveSubscription,
|
||||
progressLabel: user?.subscription?.hasActiveSubscription
|
||||
? `${user.subscription.plan} plan active`
|
||||
: 'No active subscription',
|
||||
},
|
||||
{
|
||||
id: 'community-builder',
|
||||
title: 'Community Builder',
|
||||
description: 'Bring another human into the network through the invite system.',
|
||||
actionLabel: 'Redeem or participate in invites',
|
||||
unlocked: !!user?.referral?.invitedBy || threads.length > 1,
|
||||
progressLabel:
|
||||
!!user?.referral?.invitedBy || threads.length > 1
|
||||
? 'Unlocked'
|
||||
: 'No invite activity detected yet',
|
||||
},
|
||||
];
|
||||
|
||||
const unlockedCount = rewardRoles.filter(role => role.unlocked).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-full px-4 pt-6 pb-8">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6">
|
||||
<div className="flex flex-col gap-5 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700">
|
||||
Discord Rewards
|
||||
</div>
|
||||
<h1 className="text-3xl font-semibold text-stone-900">Earn community roles</h1>
|
||||
<p className="max-w-xl text-sm text-stone-600">
|
||||
Join the OpenHuman Discord, connect your account, and unlock roles as you use more
|
||||
of the app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<button
|
||||
onClick={() => window.open(DISCORD_INVITE_URL, '_blank', 'noopener,noreferrer')}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl bg-stone-900 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-stone-800">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M20.317 4.369A19.79 19.79 0 0 0 15.885 3c-.191.328-.403.775-.552 1.124a18.27 18.27 0 0 0-5.29 0A11.56 11.56 0 0 0 9.49 3a19.74 19.74 0 0 0-4.433 1.369C2.253 8.51 1.492 12.55 1.872 16.533a19.9 19.9 0 0 0 5.239 2.673c.423-.58.8-1.196 1.123-1.845a12.84 12.84 0 0 1-1.767-.85c.148-.106.292-.217.43-.332c3.408 1.6 7.104 1.6 10.472 0c.14.115.283.226.43.332c-.565.338-1.157.623-1.771.851c.322.648.698 1.264 1.123 1.844a19.84 19.84 0 0 0 5.241-2.673c.446-4.617-.761-8.621-3.787-12.164ZM9.46 14.088c-1.02 0-1.855-.936-1.855-2.084c0-1.148.82-2.084 1.855-2.084c1.044 0 1.87.944 1.855 2.084c0 1.148-.82 2.084-1.855 2.084Zm5.08 0c-1.02 0-1.855-.936-1.855-2.084c0-1.148.82-2.084 1.855-2.084c1.044 0 1.87.944 1.855 2.084c0 1.148-.812 2.084-1.855 2.084Z" />
|
||||
</svg>
|
||||
Join Discord
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/settings/messaging')}
|
||||
className="inline-flex items-center justify-center rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
||||
Connect Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-[1.1fr_1.9fr]">
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-5">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.18em] text-stone-400">
|
||||
Progress
|
||||
</div>
|
||||
<div className="mt-3 text-3xl font-semibold text-stone-900">
|
||||
{unlockedCount}/{rewardRoles.length}
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-stone-600">
|
||||
Roles unlocked from your current app activity.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 h-2 overflow-hidden rounded-full bg-stone-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-primary-500 to-amber-400 transition-all duration-300"
|
||||
style={{ width: `${(unlockedCount / rewardRoles.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3 text-sm text-stone-600">
|
||||
<div className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
|
||||
<span>Discord linked</span>
|
||||
<span className={hasDiscordConnection ? 'text-sage-600' : 'text-stone-500'}>
|
||||
{hasDiscordConnection ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
|
||||
<span>Total messages</span>
|
||||
<span className="text-stone-900">{totalMessages}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
|
||||
<span>Plan</span>
|
||||
<span className="text-stone-900">{user?.subscription?.plan ?? 'FREE'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{rewardRoles.map(role => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`rounded-2xl border p-5 shadow-soft ${
|
||||
role.unlocked ? 'border-sage-200 bg-white' : 'border-stone-200 bg-white/90'
|
||||
}`}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-stone-900">{role.title}</h2>
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-1 text-[11px] font-medium ${
|
||||
role.unlocked
|
||||
? 'bg-sage-100 text-sage-700'
|
||||
: 'bg-stone-100 text-stone-500'
|
||||
}`}>
|
||||
{role.unlocked ? 'Unlocked' : 'Locked'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-600">{role.description}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-xl ${
|
||||
role.unlocked ? 'bg-sage-100 text-sage-700' : 'bg-stone-100 text-stone-500'
|
||||
}`}>
|
||||
{role.unlocked ? (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2 rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-stone-400">
|
||||
Unlock Action
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-stone-800">{role.actionLabel}</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-stone-600">{role.progressLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rewards;
|
||||
+234
-2
@@ -24,15 +24,247 @@ import TeamMembersPanel from '../components/settings/panels/TeamMembersPanel';
|
||||
import TeamPanel from '../components/settings/panels/TeamPanel';
|
||||
import WebhooksDebugPanel from '../components/settings/panels/WebhooksDebugPanel';
|
||||
import SettingsHome from '../components/settings/SettingsHome';
|
||||
import SettingsSectionPage from '../components/settings/SettingsSectionPage';
|
||||
|
||||
const accountSettingsItems = [
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing & Usage',
|
||||
description: 'Manage your subscription, credits, and payment methods',
|
||||
route: 'billing',
|
||||
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 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'recovery-phrase',
|
||||
title: 'Recovery Phrase',
|
||||
description: 'Manage your BIP39 recovery phrase for encryption and wallet access',
|
||||
route: 'recovery-phrase',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
title: 'Team',
|
||||
description: 'Manage your team, members, and invites',
|
||||
route: 'team',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Connections',
|
||||
description: 'Review and manage linked account connections',
|
||||
route: 'connections',
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const automationSettingsItems = [
|
||||
{
|
||||
id: 'accessibility',
|
||||
title: 'Accessibility Automation',
|
||||
description: 'Desktop permissions, assisted controls, and safety-bound sessions',
|
||||
route: 'accessibility',
|
||||
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 12h6m-7 9h8a2 2 0 002-2V7a2 2 0 00-2-2h-1l-.707-.707A1 1 0 0013.586 4h-3.172a1 1 0 00-.707.293L9 5H8a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'screen-intelligence',
|
||||
title: 'Screen Intelligence',
|
||||
description: 'Window capture policy, vision summaries, and memory ingestion',
|
||||
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: 'autocomplete',
|
||||
title: 'Inline Autocomplete',
|
||||
description: 'Manage predictive text style, app filters, and live completion controls',
|
||||
route: 'autocomplete',
|
||||
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 7h16M4 12h10m-10 5h7m10 0l3 3m0 0l3-3m-3 3v-8"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'messaging',
|
||||
title: 'Messaging Channels',
|
||||
description: 'Configure Telegram/Discord auth modes and default channel routing',
|
||||
route: 'messaging',
|
||||
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 10h.01M12 10h.01M16 10h.01M21 11c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 19l1.395-3.72C3.512 14.042 3 12.574 3 11c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cron-jobs',
|
||||
title: 'Cron Jobs',
|
||||
description: 'View and configure scheduled jobs for runtime skills',
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const aiSettingsItems = [
|
||||
{
|
||||
id: 'local-model',
|
||||
title: 'Local AI Model',
|
||||
description: 'Choose model tier by device capability and manage downloads',
|
||||
route: 'local-model',
|
||||
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: 'ai',
|
||||
title: 'AI Configuration',
|
||||
description: 'Configure persona, prompting behavior, and AI runtime settings',
|
||||
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="M12 3l1.9 3.85 4.25.62-3.08 3 .73 4.23L12 12.77 8.2 14.7l.73-4.23-3.08-3 4.25-.62L12 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
title: 'Skills',
|
||||
description: 'Configure browser access, skill behavior, and installed skill capabilities',
|
||||
route: 'skills',
|
||||
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.75 3a.75.75 0 00-1.5 0v2.25H6a2.25 2.25 0 000 4.5h2.25V12H6a2.25 2.25 0 000 4.5h2.25V18a.75.75 0 001.5 0v-1.5H12V18a.75.75 0 001.5 0v-1.5H18a2.25 2.25 0 000-4.5h-4.5V9.75H18a2.25 2.25 0 000-4.5h-4.5V3a.75.75 0 00-1.5 0v2.25H9.75V3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<div className="h-full overflow-y-auto bg-[#F5F5F5] p-4 pt-6">
|
||||
<div className="h-full overflow-y-auto p-4 pt-6">
|
||||
<div
|
||||
className="max-w-2xl mx-auto bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden flex flex-col min-h-0"
|
||||
className="max-w-lg mx-auto bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden flex flex-col min-h-0"
|
||||
style={{ maxHeight: 'calc(100vh - 7rem)' }}>
|
||||
<Routes>
|
||||
<Route index element={<SettingsHome />} />
|
||||
<Route
|
||||
path="account"
|
||||
element={
|
||||
<SettingsSectionPage
|
||||
title="Account & Security"
|
||||
description="Billing, recovery, team access, and linked account settings."
|
||||
items={accountSettingsItems}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="automation"
|
||||
element={
|
||||
<SettingsSectionPage
|
||||
title="Automation & Channels"
|
||||
description="Desktop automation, capture, messaging, and scheduled jobs."
|
||||
items={automationSettingsItems}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="ai-tools"
|
||||
element={
|
||||
<SettingsSectionPage
|
||||
title="AI & Skills"
|
||||
description="Model management, AI behavior, and skill configuration."
|
||||
items={aiSettingsItems}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="connections" element={<ConnectionsPanel />} />
|
||||
<Route path="messaging" element={<MessagingPanel />} />
|
||||
<Route path="cron-jobs" element={<CronJobsPanel />} />
|
||||
|
||||
+137
-5
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
DefaultIcon,
|
||||
@@ -46,6 +47,46 @@ interface SkillCardProps {
|
||||
onSetup: () => void;
|
||||
}
|
||||
|
||||
interface BuiltInSkillCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
route: string;
|
||||
icon: React.ReactNode;
|
||||
ctaLabel?: string;
|
||||
}
|
||||
|
||||
function BuiltInSkillCard({
|
||||
title,
|
||||
description,
|
||||
route,
|
||||
icon,
|
||||
ctaLabel = 'Open settings',
|
||||
}: BuiltInSkillCardProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(route)}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 p-4 text-left transition-colors hover:bg-white hover:border-stone-300">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-white text-stone-700 shadow-sm border border-stone-200">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold text-stone-900">{title}</h2>
|
||||
<span className="rounded-lg border border-primary-200 bg-primary-50 px-2.5 py-1 text-[11px] font-medium text-primary-700">
|
||||
{ctaLabel}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-stone-600">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillCard({ skill, onSetup }: SkillCardProps) {
|
||||
const connectionStatus = useSkillConnectionStatus(skill.id);
|
||||
const statusDisplay = STATUS_DISPLAY[connectionStatus] || STATUS_DISPLAY.offline;
|
||||
@@ -106,7 +147,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-stone-900 truncate">{skill.name}</span>
|
||||
<span className="text-sm font-semibold text-stone-900 truncate">{skill.name}</span>
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${statusDotClass(connectionStatus)}`}
|
||||
/>
|
||||
@@ -114,8 +155,13 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
|
||||
{statusDisplay.text}
|
||||
</span>
|
||||
</div>
|
||||
{skill.description && (
|
||||
<p className="mt-1 text-xs leading-relaxed text-stone-600 line-clamp-2">
|
||||
{skill.description}
|
||||
</p>
|
||||
)}
|
||||
{syncSummaryText && (
|
||||
<p className="text-[11px] text-stone-500 truncate mt-0.5">{syncSummaryText}</p>
|
||||
<p className="text-[11px] text-stone-500 truncate mt-1">{syncSummaryText}</p>
|
||||
)}
|
||||
{isSyncing && (
|
||||
<div className="mt-1.5">
|
||||
@@ -225,6 +271,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
|
||||
// ─── Main Skills Page ───────────────────────────────────────────────────────
|
||||
|
||||
export default function Skills() {
|
||||
const navigate = useNavigate();
|
||||
// Skills from registry via RPC
|
||||
const { skills: availableSkills, loading: skillsLoading } = useAvailableSkills();
|
||||
|
||||
@@ -277,16 +324,101 @@ export default function Skills() {
|
||||
setSetupModalOpen(true);
|
||||
};
|
||||
|
||||
const builtInSkills = [
|
||||
{
|
||||
id: 'screen-intelligence',
|
||||
title: 'Screen Intelligence',
|
||||
description:
|
||||
'Capture windows, summarize what is on screen, and feed useful context into memory.',
|
||||
route: '/settings/screen-intelligence',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M3 5h18v12H3zM8 21h8m-4-4v4"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'text-autocomplete',
|
||||
title: 'Text Auto-Complete',
|
||||
description:
|
||||
'Suggest inline completions while you type and control where autocomplete is active.',
|
||||
route: '/settings/autocomplete',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M4 7h16M4 12h10m-10 5h7m10 0l3 3m0 0l3-3m-3 3v-8"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'voice-stt',
|
||||
title: 'Voice Speech To Text',
|
||||
description:
|
||||
'Use the microphone for dictation and voice-driven chat with local speech recognition.',
|
||||
route: '/settings/local-model',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5]">
|
||||
<div className="min-h-full">
|
||||
<div className="min-h-full flex flex-col">
|
||||
<div className="flex-1 flex items-start justify-center p-4 pt-6">
|
||||
<div className="max-w-lg w-full">
|
||||
<div className="mb-4 rounded-2xl border border-stone-200 bg-white p-3 shadow-soft animate-fade-up">
|
||||
<div className="px-1 pb-3 pt-1">
|
||||
<h2 className="text-sm font-semibold text-stone-900">Built-in Skills</h2>
|
||||
<p className="mt-1 text-xs text-stone-500">
|
||||
Core desktop capabilities configured from settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{builtInSkills.map(skill => (
|
||||
<BuiltInSkillCard
|
||||
key={skill.id}
|
||||
title={skill.title}
|
||||
description={skill.description}
|
||||
route={skill.route}
|
||||
icon={skill.icon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main card */}
|
||||
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6 animate-fade-up">
|
||||
{/* Header */}
|
||||
<div className="mb-5">
|
||||
<h1 className="text-xl font-bold text-stone-900">Skills</h1>
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-stone-900">Connected Skills</h2>
|
||||
<p className="mt-1 text-xs text-stone-500">
|
||||
Third-party integrations and external data sources.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/settings/skills')}
|
||||
className="text-xs font-medium text-stone-500 transition-colors hover:text-stone-800">
|
||||
Skill settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Skills list */}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Webhooks() {
|
||||
|
||||
if (loading && tunnels.length === 0) {
|
||||
return (
|
||||
<div className="h-full bg-[#F5F5F5] flex items-center justify-center p-4 pt-6">
|
||||
<div className="h-full flex items-center justify-center p-4 pt-6">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-stone-300 border-t-primary-500" />
|
||||
<span className="text-sm text-stone-500">Loading webhooks…</span>
|
||||
@@ -29,7 +29,7 @@ export default function Webhooks() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full bg-[#F5F5F5] overflow-y-auto p-4 pt-6">
|
||||
<div className="h-full overflow-y-auto p-4 pt-6">
|
||||
<div className="max-w-2xl mx-auto space-y-4">
|
||||
{/* Connection status */}
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -4,7 +4,7 @@ import TypewriterGreeting from '../components/TypewriterGreeting';
|
||||
|
||||
const Welcome = () => {
|
||||
return (
|
||||
<div className="min-h-full bg-[#F5F5F5] flex items-center justify-center p-4 pt-6">
|
||||
<div className="min-h-full flex items-center justify-center p-4 pt-6">
|
||||
<div className="flex w-full max-w-md flex-col items-center gap-7 text-center animate-fade-up">
|
||||
<div className="h-36 w-36 md:h-44 md:w-44">
|
||||
<RotatingTetrahedronCanvas />
|
||||
|
||||
Reference in New Issue
Block a user