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:
Steven Enamakel
2026-04-05 14:45:07 -07:00
committed by GitHub
parent e13980d8ca
commit c27d796c8e
20 changed files with 803 additions and 476 deletions
+2 -2
View File
@@ -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 />
+10
View File
@@ -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={
+6 -6
View File
@@ -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}
+49 -298
View File
@@ -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,
+4 -4
View File
@@ -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>
);
+7
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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={() => {
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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} />
+235
View File
@@ -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
View File
@@ -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
View File
@@ -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 */}
+2 -2
View File
@@ -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">
+1 -1
View File
@@ -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 />