feat(ui): full light theme redesign based on Figma design tokens (#307)

* feat(ui): full light theme redesign based on Figma design tokens

Redesign the entire app from dark glass-morphism to a clean light theme
per the Figma reference (OpenHuman-Prod / Ref for mithil). This is a
purely visual change — zero logic, handlers, state, or API changes.

- Update Tailwind color tokens: neutral grayscale, primary #2F6EF4,
  success #34C759, alert #E8A728, error #EF4444
- Switch global CSS from dark (black bg, white text) to light (#F5F5F5
  bg, #171717 text)
- Replace left MiniSidebar with bottom BottomTabBar (Home, Chat, Skills,
  Intelligence, Automation, Notification)
- Add Settings gear icon on Home page header for persistent access
- Restyle Home: white card, Welcome Onboard title, connection badge pill
- Restyle Chat: gray agent bubbles, blue user bubbles, white input with
  mic icon, circular send button, model pill selector
- Restyle Skills: white card, filter tabs, + Add button, skill rows
- Restyle Intelligence: light containers, filters, cards
- Convert all 25+ settings panels from dark to light theme
- Convert all shared components (modals, cards, inputs, status badges)

* fix(test): update ConnectionIndicator tests for pill badge redesign

The component no longer renders a description prop — it's now a compact
pill badge. Remove description-related test cases, add pill badge test.

* fix(ui): address CodeRabbit review — contrast, dead controls, regressions

- Remove unused description prop from ConnectionIndicator interface
- Fix Save Note button hover affordance (MemoryWorkspace)
- Fix low-contrast text in AccessibilityPanel vision summary
- Fix AIPanel metadata text contrast (gray-400 → stone-500)
- Fix ActionPanel spinner visibility on light bg variants
- Fix LocalModelPanel ollama link hover color
- Fix MessagingPanel error chip to light theme tokens
- Fix RecoveryPhrasePanel import inputs from dark to light tokens
- Fix TeamMembersPanel avatar initial contrast + modal name text
- Fix Intelligence refresh button text contrast
- Remove non-functional + Add button and filter tabs from Skills page

* feat(ui): center conversation page in a white card container

Match the Figma design where the chat is inside a centered white card
with rounded corners and shadow, consistent with the Skills page layout.

* feat(ui): wrap all remaining pages in centered white card containers

Apply the centered card layout (max-w-2xl, white bg, rounded-2xl,
shadow-soft) to every page for visual consistency with Figma designs:

- Settings: wrap Routes container in centered card with max-height
- Intelligence: centered card with padding
- Agents: centered card with empty state
- Invites: centered card, replace glass with white cards
- Webhooks: centered cards for tunnel list and activity
- Mnemonic: centered card, replace glass with white card
- Welcome: light bg, updated text colors

* fix(ui): stretch settings menu items to fill card container

Remove max-w-md constraint from SettingsHome scrollable area so menu
items fill the full width of the centered card wrapper.

* fix(ui): convert onboarding screens from dark to light theme

All onboarding steps (Welcome, LocalAI, ScreenPermissions, Analytics,
Tools, Skills, Connect, InviteCode, Mnemonic) and the overlay container
now use white cards with light backgrounds, matching the app redesign.

- Card containers: bg-black → bg-white with border-stone-200
- Info boxes: dark tinted → bg-stone-50 with border-stone-200
- Text: text-white → text-stone-900, secondary to text-stone-600
- Progress dots: inactive bg-stone-700 → bg-stone-300
- Toggle rows, permission badges, inputs all converted to light tokens

* fix(ui): fix onboarding Continue button hover turning green

Replace sage hover styles with proper primary blue hover (bg-primary-600)
on the shared OnboardingNextButton component.

* fix(ui): reapply light theme to Home.tsx after upstream merge overwrite

The merge with upstream (Local AI preset bootstrap PR #304) reverted
Home.tsx to the dark theme. Reapply the Figma-based light redesign while
preserving the new first-run bootstrap logic from upstream.

* fix(ui): light theme cleanup for MemoryWorkspace component

Remaining dark-theme classes from upstream merge — update borders,
text colors, and backgrounds to match light theme.
This commit is contained in:
Cyrus Gray
2026-04-03 21:05:59 +05:30
committed by GitHub
parent 735152cb3f
commit b3e3e9bb96
73 changed files with 1607 additions and 1376 deletions
+9
View File
@@ -62,6 +62,15 @@ Quick reference for anyone starting with Claude on this project. Updated by the
- **Patch is fragile** — Resets on `cargo clean`, crate version bump, or registry re-download. Deleting build cache alone (`target/debug/build/whisper-rs-sys-*`) is NOT enough — cmake regenerates with the same bad flags.
- **Correct fix** — Needs an upstream patch in `whisper-rs-sys` or a Cargo feature to opt out of `GGML_NATIVE` on Apple Silicon cross-builds.
## UI Redesign (Light Theme — April 2026)
- **Full dark-to-light redesign shipped** — All pages, components, and settings panels converted from dark glass-morphism to clean light theme based on Figma designs by Mithil (`OpenHuman-Prod` file, node `2094-250136` for tokens).
- **Design tokens saved** in `my_docs/figma-design-tokens.md` — neutral grayscale, primary blue `#2F6EF4`, success `#34C759`, alert `#E8A728`, error `#EF4444`, SF Pro typography scale.
- **Navigation changed**: Left `MiniSidebar` → bottom `BottomTabBar` (Home, Chat, Skills, Intelligence, Automation, Notification). Settings accessible via gear icon on Home page header.
- **MiniSidebar.tsx retained** (not deleted) as backup. `BottomTabBar.tsx` is the active nav component.
- **Agent message bubbles** need `bg-stone-200/80` (not `bg-stone-100`) on `#F5F5F5` background — `bg-stone-100` is nearly invisible.
- **~55 files touched** — purely CSS class changes, zero logic/handler/state changes.
## Environment
- **Core sidecar port** — `7788` (default). Check with `lsof -i :7788`.
+5 -14
View File
@@ -4,11 +4,11 @@ import { HashRouter as Router } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import AppRoutes from './AppRoutes';
import BottomTabBar from './components/BottomTabBar';
import ServiceBlockingGate from './components/daemon/ServiceBlockingGate';
import DictationOverlay from './components/dictation/DictationOverlay';
import ErrorFallbackScreen from './components/ErrorFallbackScreen';
import LocalAIDownloadSnackbar from './components/LocalAIDownloadSnackbar';
import MiniSidebar from './components/MiniSidebar';
import OnboardingOverlay from './components/OnboardingOverlay';
import SocketProvider from './providers/SocketProvider';
import UserProvider from './providers/UserProvider';
@@ -41,20 +41,11 @@ function App() {
<SocketProvider>
<Router>
<ServiceBlockingGate>
<div className="relative h-screen flex flex-col overflow-hidden">
<div className="flex-1 flex overflow-hidden">
<MiniSidebar />
<div className="flex flex-col flex-1 relative overflow-hidden">
<div className="flex-1 overflow-y-auto">
<AppRoutes />
</div>
<div className="pointer-events-none flex-shrink-0 flex justify-center z-50">
<div className="w-full px-3 py-1.5 text-[9px] uppercase tracking-[0.18em] text-white/40 text-center bg-[#000]">
OpenHuman is in early beta
</div>
</div>
</div>
<div className="relative h-screen flex flex-col overflow-hidden bg-[#F5F5F5]">
<div className="flex-1 overflow-y-auto">
<AppRoutes />
</div>
<BottomTabBar />
</div>
<OnboardingOverlay />
<DictationOverlay />
+170
View File
@@ -0,0 +1,170 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useAppSelector } from '../store/hooks';
const tabs = [
{
id: 'home',
label: 'Home',
path: '/home',
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 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"
/>
</svg>
),
},
{
id: 'chat',
label: 'Chat',
path: '/conversations',
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="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>
),
},
{
id: 'skills',
label: 'Skills',
path: '/skills',
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="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5"
/>
</svg>
),
},
{
id: 'intelligence',
label: 'Intelligence',
path: '/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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
),
},
{
id: 'automation',
label: 'Automation',
path: '/settings/cron-jobs',
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="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={1.8}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
),
},
{
id: 'notification',
label: 'Notification',
path: '/settings/messaging',
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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
),
},
];
const BottomTabBar = () => {
const location = useLocation();
const navigate = useNavigate();
const token = useAppSelector(state => state.auth.token);
const conversationsUnreadCount = useAppSelector(state => {
const { threads, lastViewedAt } = state.thread;
if (threads.length === 0) return 0;
return threads.filter(t => {
const viewed = lastViewedAt[t.id];
const lastMsg = new Date(t.lastMessageAt || t.createdAt).getTime();
return viewed == null || lastMsg > viewed;
}).length;
});
const hiddenPaths = ['/', '/login'];
if (
!token ||
hiddenPaths.some(path => location.pathname === path || location.pathname.startsWith(`${path}/`))
) {
return null;
}
const isActive = (path: string) => {
if (path === '/conversations') return location.pathname.startsWith('/conversations');
if (path === '/settings/cron-jobs') return location.pathname.startsWith('/settings/cron-jobs');
if (path === '/settings/messaging') return location.pathname.startsWith('/settings/messaging');
return location.pathname === path;
};
return (
<div className="flex-shrink-0 bg-white border-t border-stone-200 px-2 pb-[env(safe-area-inset-bottom)] z-50">
<div className="flex items-center justify-around h-14">
{tabs.map(tab => {
const active = isActive(tab.path);
const showBadge = tab.id === 'chat' && conversationsUnreadCount > 0;
return (
<button
key={tab.id}
onClick={() => navigate(tab.path)}
className={`relative flex flex-col items-center justify-center gap-0.5 px-3 py-1.5 rounded-lg transition-colors duration-150 cursor-pointer ${
active ? 'text-stone-900' : 'text-stone-400 hover:text-stone-600'
}`}
aria-label={tab.label}>
<div className="relative">
{tab.icon}
{showBadge && (
<span
className="absolute -top-1 -right-1.5 min-w-[16px] h-[16px] px-1 flex items-center justify-center rounded-full bg-coral-500 text-white text-[9px] font-medium"
aria-label={`${conversationsUnreadCount} unread`}>
{conversationsUnreadCount > 99 ? '99+' : conversationsUnreadCount}
</span>
)}
</div>
<span
className={`text-[10px] leading-tight ${active ? 'font-medium' : 'font-normal'}`}>
{tab.label}
</span>
</button>
);
})}
</div>
</div>
);
};
export default BottomTabBar;
+5 -9
View File
@@ -3,13 +3,11 @@ import { selectSocketStatus } from '../store/socketSelectors';
interface ConnectionIndicatorProps {
status?: 'connected' | 'disconnected' | 'connecting';
description?: string;
className?: string;
}
const ConnectionIndicator = ({
status: overrideStatus,
description = 'Your device is now connected to the OpenHuman AI. Keep the app running to keep the connection alive. You can message your assistant with the button below.',
className = '',
}: ConnectionIndicatorProps) => {
// Use socket store status, but allow override via props
@@ -28,15 +26,13 @@ const ConnectionIndicator = ({
const config = statusConfig[status];
return (
<div className={`mb-6 ${className}`}>
<div className="flex items-center justify-center space-x-2 mb-3">
<div className={`${className}`}>
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-stone-50 border border-stone-200">
<div
className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`}></div>
<span className={`text-sm ${config.textColor}`}>{config.text}</span>
className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`}
/>
<span className={`text-xs font-medium ${config.textColor}`}>{config.text}</span>
</div>
{description && (
<p className="text-xs opacity-60 text-center leading-relaxed">{description}</p>
)}
</div>
);
};
+1 -1
View File
@@ -85,7 +85,7 @@ const OnboardingOverlay = () => {
if (!shouldShow) return null;
return createPortal(
<div className="fixed inset-0 z-[9999] bg-canvas-900/95 backdrop-blur-md flex items-center justify-center">
<div className="fixed inset-0 z-[9999] bg-white/95 backdrop-blur-md flex items-center justify-center">
<Onboarding onComplete={handleDone} onDefer={handleDone} />
</div>,
document.body
+1 -1
View File
@@ -10,7 +10,7 @@ const ProgressIndicator = ({ currentStep, totalSteps }: ProgressIndicatorProps)
<div
key={index}
className={`w-8 h-1 rounded-full ${
index <= currentStep ? 'bg-sage-500' : 'bg-stone-700'
index <= currentStep ? 'bg-sage-500' : 'bg-stone-300'
}`}
/>
))}
+1 -1
View File
@@ -5,7 +5,7 @@ interface RouteLoadingScreenProps {
const RouteLoadingScreen = ({ label = 'Initializing OpenHuman...' }: RouteLoadingScreenProps) => {
return (
<div className="h-full min-h-[280px] w-full flex items-center justify-center">
<div className="rounded-xl border border-white/10 bg-black/30 px-4 py-3 text-sm text-white/80">
<div className="rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
{label}
</div>
</div>
+7 -7
View File
@@ -36,13 +36,13 @@ function SkillRow({ skillId, name, icon, skillType, syncSummaryText, onConnect }
return (
<tr
onClick={onConnect}
className="skill-row group hover:bg-stone-800/20 transition-all duration-300 cursor-pointer border-b border-stone-800/30 last:border-0">
className="skill-row group hover:bg-stone-50 transition-all duration-300 cursor-pointer border-b border-stone-200 last:border-0">
<td className="py-2.5 px-3">
<div className="flex items-center gap-3">
<div className="w-5 h-5 flex items-center justify-center text-white opacity-70 group-hover:opacity-100 transition-opacity flex-shrink-0">
<div className="w-5 h-5 flex items-center justify-center text-stone-900 opacity-70 group-hover:opacity-100 transition-opacity flex-shrink-0">
{icon || <DefaultIcon />}
</div>
<span className="text-sm text-white font-medium">{name}</span>
<span className="text-sm text-stone-900 font-medium">{name}</span>
<SkillTypeBadge type={skillType} />
</div>
</td>
@@ -67,7 +67,7 @@ function SkillRow({ skillId, name, icon, skillType, syncSummaryText, onConnect }
</td>
<td className="py-2.5 px-3 w-8">
<svg
className="w-4 h-4 text-stone-500 group-hover:text-stone-300 transition-colors"
className="w-4 h-4 text-stone-500 group-hover:text-stone-700 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -144,7 +144,7 @@ export default function SkillsGrid() {
<>
<div className="animate-fade-up mt-4 mb-8 relative">
<div className="flex items-center justify-between mb-3 px-1">
<h3 className="text-sm font-semibold text-white opacity-80">Available Skills</h3>
<h3 className="text-sm font-semibold text-stone-900 opacity-80">Available Skills</h3>
</div>
<div
className="glass rounded-xl overflow-hidden skills-table-container relative cursor-pointer"
@@ -152,7 +152,7 @@ export default function SkillsGrid() {
<div className="skills-table-scroll">
<table className="w-full">
<thead className="skills-table-header">
<tr className="border-b border-stone-800/30">
<tr className="border-b border-stone-200">
<th className="py-2 px-3 text-left">
<span className="text-xs font-medium text-stone-400 uppercase tracking-wider">
Skill
@@ -193,7 +193,7 @@ export default function SkillsGrid() {
</tbody>
</table>
</div>
<div className="skills-table-overlay absolute inset-0 bg-black/80 flex items-center justify-center rounded-xl opacity-0 transition-opacity duration-200 pointer-events-none">
<div className="skills-table-overlay absolute inset-0 bg-black/30 flex items-center justify-center rounded-xl opacity-0 transition-opacity duration-200 pointer-events-none">
<span className="text-sm font-medium text-white">View all skills</span>
</div>
</div>
@@ -20,17 +20,10 @@ describe('ConnectionIndicator', () => {
expect(screen.getByText('Connecting')).toBeInTheDocument();
});
it('renders description text when provided', () => {
renderWithProviders(
<ConnectionIndicator status="connected" description="Custom description" />
);
expect(screen.getByText('Custom description')).toBeInTheDocument();
});
it('does not render description when empty string', () => {
renderWithProviders(<ConnectionIndicator status="connected" description="" />);
// Default description should not appear
expect(screen.queryByText(/Keep the app running/)).not.toBeInTheDocument();
it('renders as a pill badge', () => {
renderWithProviders(<ConnectionIndicator status="connected" />);
// The indicator renders as an inline pill — status text is visible
expect(screen.getByText(/Connected to OpenHuman AI/)).toBeInTheDocument();
});
it('falls back to store socket status when no override', () => {
@@ -173,21 +173,21 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => {
const canUninstall = !isOperating && installed;
return (
<div className="h-screen w-screen flex items-center justify-center bg-[#0a0d12] text-white px-6">
<div className="w-full max-w-xl rounded-2xl border border-white/15 bg-black/30 p-6 space-y-4">
<div className="h-screen w-screen flex items-center justify-center bg-stone-50 text-stone-900 px-6">
<div className="w-full max-w-xl rounded-2xl border border-stone-200 bg-white p-6 space-y-4">
<h1 className="text-xl font-semibold">OpenHuman Service Required</h1>
<p className="text-sm text-white/70">
<p className="text-sm text-stone-600">
The desktop service must be installed and running before the app can continue. Use the
buttons below to set up or restart the service.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div className="rounded-lg border border-white/10 bg-white/5 p-3">
<div className="text-white/60">Service</div>
<div className="rounded-lg border border-stone-200 bg-stone-50 p-3">
<div className="text-stone-600">Service</div>
<div className="font-medium">{serviceStateText}</div>
</div>
<div className="rounded-lg border border-white/10 bg-white/5 p-3">
<div className="text-white/60">Agent Server</div>
<div className="rounded-lg border border-stone-200 bg-stone-50 p-3">
<div className="text-stone-600">Agent Server</div>
<div className="font-medium">{agentRunning ? 'Running' : 'Not Running'}</div>
</div>
</div>
@@ -251,7 +251,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => {
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
canStop
? 'bg-red-600 hover:bg-red-500 text-white cursor-pointer'
: 'bg-white/5 text-white/30 cursor-not-allowed'
: 'bg-stone-50 text-stone-300 cursor-not-allowed'
}`}>
Stop Service
</button>
@@ -265,7 +265,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => {
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
canRestart
? 'bg-cyan-700 hover:bg-cyan-600 text-white cursor-pointer'
: 'bg-white/5 text-white/30 cursor-not-allowed'
: 'bg-stone-50 text-stone-300 cursor-not-allowed'
}`}>
Restart Service
</button>
@@ -279,7 +279,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => {
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
canUninstall
? 'bg-amber-700 hover:bg-amber-600 text-white cursor-pointer'
: 'bg-white/5 text-white/30 cursor-not-allowed'
: 'bg-stone-50 text-stone-300 cursor-not-allowed'
}`}>
Uninstall Service
</button>
@@ -289,7 +289,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => {
console.warn('[ServiceGate] REFRESH clicked');
void refreshStatus({ showChecking: true, clearError: true });
}}
className="px-3 py-2 rounded-lg text-sm font-medium transition-colors bg-white/10 hover:bg-white/20 text-white cursor-pointer">
className="px-3 py-2 rounded-lg text-sm font-medium transition-colors bg-stone-100 hover:bg-stone-200 text-stone-900 cursor-pointer">
Refresh
</button>
</div>
@@ -87,13 +87,13 @@ function SnoozeDropdownPortal({ isOpen, buttonRef, onClose, onSnooze }: SnoozeDr
<div
ref={dropdownRef}
data-snooze-dropdown
className="fixed py-1 bg-stone-900 border border-stone-700 rounded-lg shadow-xl min-w-[120px] z-[9999] animate-fade-in"
className="fixed py-1 bg-white border border-stone-200 rounded-lg shadow-xl min-w-[120px] z-[9999] animate-fade-in"
style={{ top: position.top, left: position.left }}>
{SNOOZE_OPTIONS.map(option => (
<button
key={option.label}
onClick={() => onSnooze(option.duration)}
className="w-full text-left px-3 py-1.5 text-xs text-white hover:bg-stone-800 transition-colors cursor-pointer">
className="w-full text-left px-3 py-1.5 text-xs text-stone-900 hover:bg-stone-100 transition-colors cursor-pointer">
{option.label}
</button>
))}
@@ -235,7 +235,7 @@ export function ActionableCard({
const priorityClasses = {
critical: 'border-coral-500/30 bg-coral-500/5',
important: 'border-amber-500/30 bg-amber-500/5',
normal: 'border-white/10 bg-white/[0.02]',
normal: 'border-stone-200 bg-stone-50',
};
const priorityDotClasses = {
@@ -258,13 +258,13 @@ export function ActionableCard({
<div
className={`
relative p-4 rounded-xl border backdrop-blur-sm transition-all duration-200
hover:bg-white/[0.04] hover:border-white/20
hover:bg-stone-50 hover:border-stone-200
${priorityClasses[item.priority]}
`}>
{/* Main content row */}
<div className="flex items-start gap-3">
{/* Icon */}
<div className="w-8 h-8 flex items-center justify-center text-white/70 flex-shrink-0 mt-0.5">
<div className="w-8 h-8 flex items-center justify-center text-stone-600 flex-shrink-0 mt-0.5">
{sourceIcon}
</div>
@@ -272,7 +272,7 @@ export function ActionableCard({
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-white leading-snug">{item.title}</h3>
<h3 className="text-sm font-medium text-stone-900 leading-snug">{item.title}</h3>
{item.description && (
<p className="text-xs text-stone-400 mt-1 leading-relaxed">{item.description}</p>
)}
@@ -28,10 +28,10 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 animate-fade-in"
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 animate-fade-in"
onClick={handleCancel}>
<div
className="bg-stone-900 rounded-2xl max-w-md w-full shadow-large border border-stone-700/50 animate-slide-up"
className="bg-white rounded-2xl max-w-md w-full shadow-large border border-stone-200 animate-slide-up"
onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="p-6 pb-4">
@@ -53,8 +53,8 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
</div>
)}
<div className="flex-1">
<h2 className="text-lg font-semibold text-white">{modal.title}</h2>
<p className="text-sm text-stone-400 mt-1">{modal.message}</p>
<h2 className="text-lg font-semibold text-stone-900">{modal.title}</h2>
<p className="text-sm text-stone-600 mt-1">{modal.message}</p>
</div>
</div>
</div>
@@ -62,12 +62,12 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
{/* Don't show again option */}
{modal.showDontShowAgain && (
<div className="px-6 pb-2">
<label className="flex items-center gap-2 text-sm text-stone-400 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-stone-600 cursor-pointer">
<input
type="checkbox"
checked={dontShowAgain}
onChange={e => setDontShowAgain(e.target.checked)}
className="rounded border-stone-600 bg-stone-800 text-primary-500 focus:ring-primary-500 focus:ring-offset-0"
className="rounded border-stone-300 bg-stone-100 text-primary-500 focus:ring-primary-500 focus:ring-offset-0"
/>
Don't show similar suggestions
</label>
@@ -75,10 +75,10 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
)}
{/* Actions */}
<div className="flex items-center justify-end gap-3 p-6 pt-4 border-t border-stone-700/50">
<div className="flex items-center justify-end gap-3 p-6 pt-4 border-t border-stone-200">
<button
onClick={handleCancel}
className="px-4 py-2 text-sm font-medium text-stone-400 hover:text-white rounded-lg hover:bg-stone-800 transition-colors">
className="px-4 py-2 text-sm font-medium text-stone-600 hover:text-stone-900 rounded-lg hover:bg-stone-100 transition-colors">
{modal.cancelText || 'Cancel'}
</button>
<button
@@ -219,10 +219,10 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
if (loading) {
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-4">
<p className="text-sm font-semibold text-white mb-3">Memory Graph</p>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
<p className="text-sm font-semibold text-stone-900 mb-3">Memory Graph</p>
<div className="flex items-center justify-center" style={{ minHeight: 320 }}>
<div className="flex gap-2 items-center text-stone-400 text-sm">
<div className="flex gap-2 items-center text-stone-600 text-sm">
<div className="w-4 h-4 rounded-full border-2 border-primary-500 border-t-transparent animate-spin" />
Loading graph
</div>
@@ -233,10 +233,10 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
if (nodes.length === 0) {
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-4">
<p className="text-sm font-semibold text-white mb-3">Memory Graph</p>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
<p className="text-sm font-semibold text-stone-900 mb-3">Memory Graph</p>
<div className="flex items-center justify-center" style={{ minHeight: 320 }}>
<p className="text-stone-400 text-sm">No memory graph data yet</p>
<p className="text-stone-600 text-sm">No memory graph data yet</p>
</div>
</div>
);
@@ -245,20 +245,20 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
const maxConn = Math.max(...nodes.map(n => n.connectionCount), 1);
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-4">
<p className="text-sm font-semibold text-white mb-3">Memory Graph</p>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
<p className="text-sm font-semibold text-stone-900 mb-3">Memory Graph</p>
<div
className="w-full overflow-hidden rounded-lg border border-white/5"
className="w-full overflow-hidden rounded-lg border border-stone-200"
style={{ minHeight: 320 }}>
<svg
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
width="100%"
style={{ display: 'block', background: 'rgba(0,0,0,0.25)' }}
style={{ display: 'block', background: 'rgba(248,248,247,1)' }}
onClick={() => setSelectedNode(null)}>
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="rgba(255,255,255,0.18)" />
<polygon points="0 0, 8 3, 0 6" fill="rgba(0,0,0,0.25)" />
</marker>
</defs>
@@ -281,7 +281,7 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
y1={src.y}
x2={tgt.x}
y2={tgt.y}
stroke={isHighlighted ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.05)'}
stroke={isHighlighted ? 'rgba(0,0,0,0.22)' : 'rgba(0,0,0,0.08)'}
strokeWidth={isHighlighted ? 1.5 : 1}
markerEnd="url(#arrowhead)"
style={{ cursor: 'pointer', transition: 'stroke 0.15s' }}
@@ -296,10 +296,10 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
fontSize={9}
fill={
hoveredEdge === i
? 'rgba(255,255,255,0.85)'
? 'rgba(0,0,0,0.7)'
: isHighlighted
? 'rgba(255,255,255,0.3)'
: 'rgba(255,255,255,0.08)'
? 'rgba(0,0,0,0.35)'
: 'rgba(0,0,0,0.1)'
}
style={{ pointerEvents: 'none', userSelect: 'none', transition: 'fill 0.15s' }}>
{truncate(edge.predicate, 18)}
@@ -332,7 +332,7 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
r={r}
fill={color}
opacity={isDimmed ? 0.15 : isSelected ? 1 : 0.82}
stroke={isSelected ? 'white' : 'rgba(255,255,255,0.18)'}
stroke={isSelected ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.5)'}
strokeWidth={isSelected ? 2 : 1}
style={{ transition: 'opacity 0.15s' }}
/>
@@ -341,7 +341,7 @@ export function MemoryGraphMap({ relations, loading }: MemoryGraphMapProps) {
textAnchor="middle"
fontSize={isCenter ? 11 : 9}
fontWeight={isCenter ? 600 : 400}
fill={isDimmed ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.85)'}
fill={isDimmed ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.75)'}
style={{ pointerEvents: 'none', userSelect: 'none', transition: 'fill 0.15s' }}>
{isCenter && node.id !== 'you' ? 'You' : truncate(node.label)}
</text>
@@ -123,18 +123,18 @@ export function MemoryHeatmap({ timestamps, loading }: MemoryHeatmapProps) {
if (loading) {
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-5">
<h3 className="text-sm font-semibold text-white mb-3">Ingestion Activity</h3>
<div className="h-28 rounded-lg bg-white/5 animate-pulse" />
<div className="rounded-xl border border-stone-200 bg-stone-50 p-5">
<h3 className="text-sm font-semibold text-stone-900 mb-3">Ingestion Activity</h3>
<div className="h-28 rounded-lg bg-stone-200 animate-pulse" />
</div>
);
}
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-5">
<div className="rounded-xl border border-stone-200 bg-stone-50 p-5">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-semibold text-white">Ingestion Activity</h3>
<h3 className="text-sm font-semibold text-stone-900">Ingestion Activity</h3>
<p className="text-xs text-stone-500 mt-0.5">
{totalEvents} event{totalEvents !== 1 ? 's' : ''} over the last {MONTHS} months
{maxDailyCount > 0 && <> · peak: {maxDailyCount}/day</>}
@@ -166,7 +166,7 @@ export function MemoryHeatmap({ timestamps, loading }: MemoryHeatmapProps) {
x={0}
y={22 + i * (cellSize + CELL_GAP) + cellSize * 0.75}
fontSize={9}
fill="rgba(255,255,255,0.3)"
fill="rgba(0,0,0,0.4)"
style={{ userSelect: 'none' }}>
{label}
</text>
@@ -180,7 +180,7 @@ export function MemoryHeatmap({ timestamps, loading }: MemoryHeatmapProps) {
x={DAY_LABEL_WIDTH + m.weekIdx * (cellSize + CELL_GAP)}
y={12}
fontSize={9}
fill="rgba(255,255,255,0.3)"
fill="rgba(0,0,0,0.4)"
style={{ userSelect: 'none' }}>
{m.label}
</text>
@@ -226,7 +226,7 @@ export function MemoryHeatmap({ timestamps, loading }: MemoryHeatmapProps) {
{/* Tooltip */}
{hoveredCell && (
<div
className="fixed z-50 px-2 py-1 rounded-md bg-stone-800 border border-white/10 text-[11px] text-white shadow-lg pointer-events-none"
className="fixed z-50 px-2 py-1 rounded-md bg-white border border-stone-200 text-[11px] text-stone-900 shadow-lg pointer-events-none"
style={{ left: hoveredCell.x, top: hoveredCell.y - 32, transform: 'translateX(-50%)' }}>
<span className="font-medium">
{hoveredCell.count} event{hoveredCell.count !== 1 ? 's' : ''}
@@ -95,44 +95,44 @@ const CATEGORY_CONFIG: Record<
facts: {
label: 'Known Facts',
icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
color: 'text-emerald-300',
bgColor: 'bg-emerald-500/10',
borderColor: 'border-emerald-500/20',
color: 'text-emerald-600',
bgColor: 'bg-emerald-50',
borderColor: 'border-emerald-200',
},
preferences: {
label: 'Preferences',
icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
color: 'text-rose-300',
bgColor: 'bg-rose-500/10',
borderColor: 'border-rose-500/20',
color: 'text-rose-600',
bgColor: 'bg-rose-50',
borderColor: 'border-rose-200',
},
relationships: {
label: 'Relationships',
icon: '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',
color: 'text-primary-300',
bgColor: 'bg-primary-500/10',
borderColor: 'border-primary-500/20',
color: 'text-primary-600',
bgColor: 'bg-primary-50',
borderColor: 'border-primary-200',
},
skills: {
label: 'Skills & Expertise',
icon: 'M13 10V3L4 14h7v7l9-11h-7z',
color: 'text-amber-300',
bgColor: 'bg-amber-500/10',
borderColor: 'border-amber-500/20',
color: 'text-amber-600',
bgColor: 'bg-amber-100',
borderColor: 'border-amber-200',
},
opinions: {
label: 'Opinions & Beliefs',
icon: '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',
color: 'text-lavender-300',
bgColor: 'bg-lavender-500/10',
borderColor: 'border-lavender-500/20',
color: 'text-lavender-600',
bgColor: 'bg-lavender-50',
borderColor: 'border-lavender-200',
},
other: {
label: 'Other Insights',
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
color: 'text-stone-300',
bgColor: 'bg-stone-500/10',
borderColor: 'border-stone-500/20',
color: 'text-stone-600',
bgColor: 'bg-stone-100',
borderColor: 'border-stone-200',
},
};
@@ -189,11 +189,11 @@ export function MemoryInsights({ relations, loading }: MemoryInsightsProps) {
if (loading) {
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-5">
<h3 className="text-sm font-semibold text-white mb-4">Intelligent Insights</h3>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-5">
<h3 className="text-sm font-semibold text-stone-900 mb-4">Intelligent Insights</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
{[1, 2, 3].map(i => (
<div key={i} className="h-28 rounded-lg bg-white/5 animate-pulse" />
<div key={i} className="h-28 rounded-lg bg-stone-200 animate-pulse" />
))}
</div>
</div>
@@ -202,9 +202,9 @@ export function MemoryInsights({ relations, loading }: MemoryInsightsProps) {
if (groups.length === 0) {
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-5">
<h3 className="text-sm font-semibold text-white mb-2">Intelligent Insights</h3>
<p className="text-sm text-stone-400">
<div className="rounded-xl border border-stone-200 bg-stone-50 p-5">
<h3 className="text-sm font-semibold text-stone-900 mb-2">Intelligent Insights</h3>
<p className="text-sm text-stone-600">
No insights yet. Ingest documents to extract facts, preferences, and relationships.
</p>
</div>
@@ -212,10 +212,10 @@ export function MemoryInsights({ relations, loading }: MemoryInsightsProps) {
}
return (
<div className="rounded-xl border border-white/10 bg-black/20 p-5">
<div className="rounded-xl border border-stone-200 bg-stone-50 p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-white">Intelligent Insights</h3>
<h3 className="text-sm font-semibold text-stone-900">Intelligent Insights</h3>
<p className="text-xs text-stone-500 mt-0.5">
Extracted knowledge organized by type {relations.length} total relations
</p>
@@ -275,13 +275,13 @@ export function MemoryInsights({ relations, loading }: MemoryInsightsProps) {
key={`${item.subject}-${item.predicate}-${item.object}-${idx}`}
className="flex items-start gap-1.5 text-[11px] leading-relaxed">
<span
className="text-white/80 font-medium shrink-0 max-w-[30%] truncate"
className="text-stone-900 font-medium shrink-0 max-w-[30%] truncate"
title={item.subject}>
{item.subject}
{item.subjectType && <EntityTypeBadge type={item.subjectType} />}
</span>
<span className="text-stone-500 shrink-0 italic">{item.predicate}</span>
<span className="text-white/60 truncate" title={item.object}>
<span className="text-stone-600 truncate" title={item.object}>
{item.object}
{item.objectType && <EntityTypeBadge type={item.objectType} />}
</span>
@@ -58,37 +58,37 @@ export function MemoryStatsBar(props: MemoryStatsBarProps) {
label: 'Storage',
value: estimatedStorageBytes > 0 ? formatBytes(estimatedStorageBytes) : '--',
sub: totalFiles > 0 ? `${formatNumber(totalFiles)} files` : undefined,
color: 'text-primary-300',
color: 'text-primary-500',
},
{
label: 'Documents',
value: formatNumber(totalDocs),
sub: docsToday > 0 ? `+${docsToday} today` : undefined,
color: 'text-emerald-300',
color: 'text-emerald-600',
},
{
label: 'Namespaces',
value: formatNumber(totalNamespaces),
sub: undefined,
color: 'text-amber-300',
color: 'text-amber-600',
},
{
label: 'Relations',
value: formatNumber(totalRelations),
sub: undefined,
color: 'text-lavender-300',
color: 'text-lavender-600',
},
{
label: 'First Memory',
value: oldestDocTimestamp ? formatTimeAgo(oldestDocTimestamp) : '--',
sub: newestDocTimestamp ? `Latest: ${formatTimeAgo(newestDocTimestamp)}` : undefined,
color: 'text-sky-300',
color: 'text-sky-600',
},
{
label: 'Sessions',
value: totalSessions !== null ? formatNumber(totalSessions) : '--',
sub: totalTokens !== null ? `${formatNumber(totalTokens)} tokens` : undefined,
color: 'text-rose-300',
color: 'text-rose-600',
},
];
@@ -97,12 +97,12 @@ export function MemoryStatsBar(props: MemoryStatsBarProps) {
{stats.map(stat => (
<div
key={stat.label}
className="rounded-xl border border-white/10 bg-black/20 p-3 transition-colors hover:bg-black/30">
className="rounded-xl border border-stone-200 bg-stone-50 p-3 transition-colors hover:bg-stone-100">
<div className="text-[11px] uppercase tracking-wide text-stone-500 mb-1">
{stat.label}
</div>
<div className={`text-xl font-semibold ${stat.color}`}>
{loading ? <div className="h-7 w-16 rounded bg-white/5 animate-pulse" /> : stat.value}
{loading ? <div className="h-7 w-16 rounded bg-stone-200 animate-pulse" /> : stat.value}
</div>
{stat.sub && <div className="text-[11px] text-stone-500 mt-0.5">{stat.sub}</div>}
</div>
@@ -347,11 +347,11 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
return (
<section className="space-y-4 animate-fade-up">
{/* Header */}
<div className="glass rounded-2xl p-5 border border-white/10">
<div className="glass rounded-2xl p-5 border border-stone-200">
<div className="flex items-start justify-between gap-4 mb-5">
<div>
<h2 className="text-lg font-semibold text-white">Memory</h2>
<p className="text-sm text-stone-400">
<h2 className="text-lg font-semibold text-stone-900">Memory</h2>
<p className="text-sm text-stone-500">
Your AI's knowledge graph, extracted insights, and ingestion activity.
</p>
</div>
@@ -360,7 +360,7 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
void Promise.all([loadWorkspace(), refetchStats()]);
}}
disabled={memoryWorkspaceLoading || statsLoading}
className="px-3 py-1.5 text-xs bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-stone-300 disabled:opacity-40 transition-colors">
className="px-3 py-1.5 text-xs bg-stone-50 hover:bg-stone-100 border border-stone-200 rounded-lg text-stone-600 disabled:opacity-40 transition-colors">
{memoryWorkspaceLoading ? 'Loading...' : 'Refresh'}
</button>
</div>
@@ -391,19 +391,19 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
<MemoryHeatmap timestamps={heatmapTimestamps} loading={memoryWorkspaceLoading} />
{/* Collapsible: Files & Management */}
<div className="rounded-xl border border-white/10 bg-black/20">
<div className="rounded-xl border border-stone-200 bg-stone-50">
<button
onClick={() => setManageOpen(!manageOpen)}
className="w-full flex items-center justify-between p-4 text-left hover:bg-white/5 transition-colors rounded-xl">
className="w-full flex items-center justify-between p-4 text-left hover:bg-stone-100 transition-colors rounded-xl">
<div className="flex items-center gap-2">
<svg
className={`w-4 h-4 text-stone-400 transition-transform ${manageOpen ? 'rotate-90' : ''}`}
className={`w-4 h-4 text-stone-500 transition-transform ${manageOpen ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<h3 className="text-sm font-semibold text-white">Files & Management</h3>
<h3 className="text-sm font-semibold text-stone-900">Files & Management</h3>
<span className="text-xs text-stone-500">
{memoryFilesList.length} files · {memoryNamespaces.length} namespaces ·{' '}
{memoryDocs.length} docs
@@ -415,9 +415,9 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
<div className="px-4 pb-4 space-y-4 animate-fade-up">
{/* File browser */}
<div>
<h4 className="text-xs font-medium text-stone-400 mb-2">Memory Files</h4>
<h4 className="text-xs font-medium text-stone-500 mb-2">Memory Files</h4>
<div className="grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-3">
<div className="rounded-lg border border-white/10 bg-stone-950/50 p-2 h-52 overflow-y-auto">
<div className="rounded-lg border border-stone-200 bg-white p-2 h-52 overflow-y-auto">
{memoryFilesList.length === 0 ? (
<div className="text-xs text-stone-500 p-2">No files found.</div>
) : (
@@ -427,21 +427,21 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
onClick={() => setSelectedFile(filePath)}
className={`w-full text-left px-2 py-1.5 rounded text-xs mb-1 border transition-colors ${
selectedFile === filePath
? 'border-primary-400/40 bg-primary-500/20 text-primary-200'
: 'border-transparent hover:border-white/10 hover:bg-white/5 text-stone-300'
? 'border-primary-400/40 bg-primary-500/10 text-primary-600'
: 'border-transparent hover:border-stone-200 hover:bg-stone-50 text-stone-600'
}`}>
{filePath}
</button>
))
)}
</div>
<div className="rounded-lg border border-white/10 bg-stone-950/50 p-3 h-52 overflow-auto">
<div className="rounded-lg border border-stone-200 bg-white p-3 h-52 overflow-auto">
{selectedFileLoading ? (
<div className="text-xs text-stone-400">Loading file...</div>
<div className="text-xs text-stone-500">Loading file...</div>
) : selectedFileError ? (
<div className="text-xs text-coral-300">{selectedFileError}</div>
<div className="text-xs text-coral-500">{selectedFileError}</div>
) : (
<pre className="text-[11px] leading-5 text-stone-200 whitespace-pre-wrap">
<pre className="text-[11px] leading-5 text-stone-700 whitespace-pre-wrap">
{selectedFileContent || 'Empty file'}
</pre>
)}
+9 -9
View File
@@ -444,7 +444,7 @@ const SettingsHome = () => {
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<SettingsHeader />
<div className="flex-1 overflow-y-auto max-w-md mx-auto">
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-6">
{/* Main Settings */}
<div>
@@ -482,10 +482,10 @@ const SettingsHome = () => {
{/* Log Out & Clear Data Confirmation Modal */}
{showLogoutAndClearModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60">
<div className="bg-stone-900 rounded-2xl max-w-md w-full p-6 border border-stone-700/50">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30">
<div className="bg-white rounded-2xl max-w-md w-full p-6 border border-stone-200">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-amber-500/20 flex items-center justify-center">
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
<svg
className="w-5 h-5 text-amber-400"
fill="none"
@@ -500,12 +500,12 @@ const SettingsHome = () => {
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-white">Log Out & Clear App Data</h3>
<h3 className="text-lg font-semibold text-stone-900">Log Out & Clear App Data</h3>
</div>
</div>
<div className="mb-6">
<p className="text-stone-300 text-sm leading-relaxed">
<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
@@ -515,8 +515,8 @@ const SettingsHome = () => {
</p>
{error && (
<div className="mt-3 p-3 rounded-lg bg-coral-500/10 border border-coral-500/20">
<p className="text-coral-400 text-sm">{error}</p>
<div className="mt-3 p-3 rounded-lg bg-coral-100 border border-coral-500/20">
<p className="text-coral-600 text-sm">{error}</p>
</div>
)}
</div>
@@ -528,7 +528,7 @@ const SettingsHome = () => {
setError(null);
}}
disabled={isLoading}
className="flex-1 px-4 py-2 rounded-lg border border-stone-600 text-stone-300 hover:bg-stone-800 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 rounded-lg border border-stone-200 text-stone-700 hover:bg-stone-100 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -10,10 +10,10 @@ const SettingsBackButton = ({
className = '',
}: SettingsBackButtonProps) => {
return (
<div className={`bg-black/30 border-b border-stone-700 p-6 ${className}`}>
<div className={`bg-stone-50 border-b border-stone-200 p-6 ${className}`}>
<button
onClick={onClick}
className="flex items-center space-x-3 text-white hover:text-stone-300 transition-colors duration-150"
className="flex items-center space-x-3 text-stone-900 hover:text-stone-700 transition-colors duration-150"
aria-label="Go back">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
@@ -12,13 +12,13 @@ const SettingsHeader = ({
onBack,
}: SettingsHeaderProps) => {
return (
<div className={`bg-black/30 border-b border-stone-700 p-3 relative ${className}`}>
<div className={`bg-stone-50 border-b border-stone-200 p-3 relative ${className}`}>
<div className="flex items-center">
{/* Back button */}
{showBackButton && onBack && (
<button
onClick={onBack}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-200 transition-colors mr-3"
aria-label="Go back">
<svg
className="w-5 h-5 opacity-70"
@@ -36,7 +36,7 @@ const SettingsHeader = ({
)}
{/* Title */}
<h2 className="text-lg font-semibold text-white">{title}</h2>
<h2 className="text-lg font-semibold text-stone-900">{title}</h2>
</div>
</div>
);
@@ -20,9 +20,9 @@ const SettingsMenuItem = ({
isLast = false,
}: SettingsMenuItemProps) => {
// Color variations for dangerous items (like logout/delete)
const titleColor = dangerous ? 'text-amber-400' : 'text-white';
const iconColor = dangerous ? 'text-amber-400' : 'text-white';
const borderColor = 'border-stone-700'; // Use consistent border color for all items
const titleColor = dangerous ? 'text-amber-600' : 'text-stone-900';
const iconColor = dangerous ? 'text-amber-600' : 'text-stone-900';
const borderColor = 'border-stone-200'; // Use consistent border color for all items
// Border classes for first/last items
const borderClasses = isLast ? '' : `border-b ${borderColor}`;
@@ -31,7 +31,7 @@ const SettingsMenuItem = ({
return (
<button
onClick={onClick}
className={`w-full flex items-center justify-between p-3 bg-black/50 ${borderClasses} hover:bg-stone-800/30 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
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`}>
<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>
+47 -45
View File
@@ -79,27 +79,27 @@ const AIPanel = () => {
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="space-y-4">
<h3 className="text-lg font-semibold text-white">AI System Overview</h3>
<p className="text-sm text-gray-400">
<h3 className="text-lg font-semibold text-stone-900">AI System Overview</h3>
<p className="text-sm text-stone-500">
Prompt and markdown orchestration is handled in Rust runtime.
</p>
{aiConfig && (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700">
<div className="bg-stone-50 rounded-lg p-4 border border-stone-200">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Configuration Status
</label>
<div className="text-sm text-green-400 font-medium mt-1">
<div className="text-sm text-green-600 font-medium mt-1">
{aiConfig.metadata.hasFallbacks ? 'Fallback Mode' : 'Loaded from Runtime'}
</div>
</div>
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Loading Duration
</label>
<div className="text-sm text-blue-400 font-medium mt-1">
<div className="text-sm text-primary-600 font-medium mt-1">
{aiConfig.metadata.loadingDuration}ms
</div>
</div>
@@ -110,11 +110,11 @@ const AIPanel = () => {
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Local Model Runtime</h3>
<h3 className="text-lg font-semibold text-stone-900">Local Model Runtime</h3>
<div className="flex items-center gap-4">
<button
onClick={() => navigateToSettings('local-model')}
className="text-sm text-cyan-400 hover:text-cyan-300 transition-colors">
className="text-sm text-primary-500 hover:text-primary-600 transition-colors">
Open Manager
</button>
<button
@@ -122,70 +122,72 @@ const AIPanel = () => {
await openhumanLocalAiDownload(true);
await loadLocalAiStatus();
}}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
className="text-sm text-primary-500 hover:text-primary-600 transition-colors">
Retry Download
</button>
</div>
</div>
{localAiStatus ? (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-2">
<div className="bg-stone-50 rounded-lg p-4 border border-stone-200 space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">State</span>
<span className="text-blue-300 font-medium">{localAiStatus.state}</span>
<span className="text-primary-600 font-medium">{localAiStatus.state}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Target Model</span>
<span className="text-green-300 font-medium">{localAiStatus.model_id}</span>
<span className="text-stone-500">Target Model</span>
<span className="text-green-600 font-medium">{localAiStatus.model_id}</span>
</div>
{localAiStatus.download_progress != null && (
<div className="text-xs text-gray-300">
<div className="text-xs text-stone-500">
Download: {(localAiStatus.download_progress * 100).toFixed(0)}%
</div>
)}
{localAiStatus.warning && (
<div className="text-xs text-amber-300">{localAiStatus.warning}</div>
<div className="text-xs text-amber-700">{localAiStatus.warning}</div>
)}
</div>
) : (
<div className="text-sm text-gray-500">Local model status unavailable.</div>
<div className="text-sm text-stone-400">Local model status unavailable.</div>
)}
</section>
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">SOUL Persona Configuration</h3>
<h3 className="text-lg font-semibold text-stone-900">SOUL Persona Configuration</h3>
<button
onClick={() => refreshConfig('soul')}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
className="text-sm text-primary-500 hover:text-primary-600 transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'soul'}>
{refreshingComponent === 'soul' ? 'Refreshing...' : 'Refresh SOUL'}
</button>
</div>
{loading && (
<div className="text-sm text-gray-400 animate-pulse">Loading SOUL configuration...</div>
<div className="text-sm text-stone-500 animate-pulse">
Loading SOUL configuration...
</div>
)}
{error && (
<div className="bg-red-500/10 border border-red-500/40 rounded-lg p-3">
<div className="text-sm text-red-200">{error}</div>
<div className="bg-red-50 border border-red-300 rounded-lg p-3">
<div className="text-sm text-red-600">{error}</div>
</div>
)}
{aiConfig && (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
<div className="bg-stone-50 rounded-lg p-4 border border-stone-200 space-y-3">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Identity</label>
<div className="text-sm text-green-400 font-medium mt-1">{aiConfig.soul.name}</div>
<label className="text-xs text-stone-500 uppercase tracking-wide">Identity</label>
<div className="text-sm text-green-600 font-medium mt-1">{aiConfig.soul.name}</div>
<div className="text-xs text-gray-300 mt-1">{aiConfig.soul.description}</div>
</div>
{aiConfig.soul.personalityPreview.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Personality
</label>
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
<div className="text-xs text-stone-600 mt-1 leading-relaxed">
{aiConfig.soul.personalityPreview.join(' • ')}
</div>
</div>
@@ -193,20 +195,20 @@ const AIPanel = () => {
{aiConfig.soul.safetyRulesPreview.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Safety Rules
</label>
<div className="text-xs text-yellow-300 mt-1 leading-relaxed">
<div className="text-xs text-yellow-700 mt-1 leading-relaxed">
{aiConfig.soul.safetyRulesPreview.join(' • ')}
</div>
</div>
)}
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
<div className="text-xs text-gray-400">
<div className="flex items-center justify-between pt-2 border-t border-stone-200">
<div className="text-xs text-stone-500">
Source: {aiConfig.metadata.sources.soul}
</div>
<div className="text-xs text-gray-400">
<div className="text-xs text-stone-500">
Loaded: {new Date(aiConfig.soul.loadedAt).toLocaleTimeString()}
</div>
</div>
@@ -216,31 +218,31 @@ const AIPanel = () => {
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">TOOLS Configuration</h3>
<h3 className="text-lg font-semibold text-stone-900">TOOLS Configuration</h3>
<button
onClick={() => refreshConfig('tools')}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
className="text-sm text-primary-500 hover:text-primary-600 transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'tools'}>
{refreshingComponent === 'tools' ? 'Refreshing...' : 'Refresh TOOLS'}
</button>
</div>
{aiConfig && (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
<div className="bg-stone-50 rounded-lg p-4 border border-stone-200 space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Tools Available
</label>
<div className="text-sm text-green-400 font-medium mt-1">
<div className="text-sm text-green-600 font-medium mt-1">
{aiConfig.tools.totalTools} tools
</div>
</div>
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Active Skills
</label>
<div className="text-sm text-green-400 font-medium mt-1">
<div className="text-sm text-green-600 font-medium mt-1">
{aiConfig.tools.activeSkills} skills
</div>
</div>
@@ -248,20 +250,20 @@ const AIPanel = () => {
{aiConfig.tools.skillsPreview.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">
<label className="text-xs text-stone-500 uppercase tracking-wide">
Skills Overview
</label>
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
<div className="text-xs text-stone-600 mt-1 leading-relaxed">
{aiConfig.tools.skillsPreview.join(' • ')}
</div>
</div>
)}
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
<div className="text-xs text-gray-400">
<div className="flex items-center justify-between pt-2 border-t border-stone-200">
<div className="text-xs text-stone-500">
Source: {aiConfig.metadata.sources.tools}
</div>
<div className="text-xs text-gray-400">
<div className="text-xs text-stone-500">
Loaded: {new Date(aiConfig.tools.loadedAt).toLocaleTimeString()}
</div>
</div>
@@ -273,7 +275,7 @@ const AIPanel = () => {
<div className="flex items-center justify-center">
<button
onClick={() => refreshConfig('all')}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
className="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'all'}>
{refreshingComponent === 'all' ? 'Refreshing All...' : 'Refresh All AI Configuration'}
</button>
@@ -29,14 +29,14 @@ const formatRemaining = (remainingMs: number | null): string => {
const PermissionBadge = ({ label, value }: { label: string; value: string }) => {
const colorClass =
value === 'granted'
? 'bg-green-900/40 text-green-300 border-green-700/40'
? 'bg-green-50 text-green-700 border-green-200'
: value === 'denied'
? 'bg-red-900/40 text-red-300 border-red-700/40'
: 'bg-stone-800/60 text-stone-300 border-stone-700';
? 'bg-red-50 text-red-600 border-red-200'
: 'bg-stone-100 text-stone-600 border-stone-200';
return (
<div className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 p-3">
<span className="text-sm text-stone-200">{label}</span>
<div className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 p-3">
<span className="text-sm text-stone-700">{label}</span>
<span className={`rounded-md border px-2 py-1 text-xs uppercase tracking-wide ${colorClass}`}>
{value}
</span>
@@ -117,8 +117,8 @@ const AccessibilityPanel = () => {
/>
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Permissions</h3>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
<PermissionBadge
label="Screen Recording"
value={status?.permissions.screen_recording ?? 'unknown'}
@@ -133,14 +133,14 @@ const AccessibilityPanel = () => {
/>
{anyPermissionDenied && (
<div className="rounded-xl border border-amber-700/40 bg-amber-900/20 p-3 text-sm text-amber-200 space-y-1">
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-700 space-y-1">
<p>
After granting in System Settings, click &ldquo;Restart &amp; Refresh&rdquo; below.
</p>
{status?.permission_check_process_path ? (
<p className="opacity-75 text-xs">
Enable the same app macOS lists for this path (TCC is per executable).{' '}
<span className="font-mono break-all text-stone-300">
<span className="font-mono break-all text-stone-600">
{status.permission_check_process_path}
</span>
</p>
@@ -158,21 +158,21 @@ const AccessibilityPanel = () => {
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('screen_recording'))}
disabled={isRequestingPermissions || isRestartingCore}
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Screen Recording'}
</button>
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('accessibility'))}
disabled={isRequestingPermissions || isRestartingCore}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Accessibility'}
</button>
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('input_monitoring'))}
disabled={isRequestingPermissions || isRestartingCore}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Open Input Monitoring'}
</button>
@@ -181,7 +181,7 @@ const AccessibilityPanel = () => {
type="button"
onClick={() => void dispatch(refreshPermissionsWithRestart())}
disabled={isRestartingCore || isLoading}
className="rounded-lg border border-amber-500/60 bg-amber-500/20 px-3 py-2 text-sm text-amber-200 disabled:opacity-50">
className="rounded-lg border border-amber-500/60 bg-amber-50 px-3 py-2 text-sm text-amber-700 disabled:opacity-50">
{isRestartingCore ? 'Restarting core…' : 'Restart & Refresh Permissions'}
</button>
) : (
@@ -189,17 +189,17 @@ const AccessibilityPanel = () => {
type="button"
onClick={() => void dispatch(fetchAccessibilityStatus())}
disabled={isLoading || isRestartingCore}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-2 text-sm text-stone-200 disabled:opacity-50">
className="rounded-lg border border-stone-300 bg-stone-100 px-3 py-2 text-sm text-stone-700 disabled:opacity-50">
{isLoading ? 'Refreshing…' : 'Refresh Status'}
</button>
)}
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Features</h3>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Features</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Screen Monitoring</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Screen Monitoring</span>
<input
type="checkbox"
checked={screenMonitoring}
@@ -212,8 +212,8 @@ const AccessibilityPanel = () => {
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Device Control</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Device Control</span>
<input
type="checkbox"
checked={deviceControl}
@@ -226,8 +226,8 @@ const AccessibilityPanel = () => {
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Predictive Input</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Predictive Input</span>
<input
type="checkbox"
checked={predictiveInput}
@@ -241,9 +241,9 @@ const AccessibilityPanel = () => {
</label>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Session</h3>
<div className="text-sm text-stone-300 space-y-1">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Session</h3>
<div className="text-sm text-stone-700 space-y-1">
<div>Status: {status?.session.active ? 'Active' : 'Stopped'}</div>
<div>Remaining: {remaining}</div>
<div>Frames (ephemeral): {status?.session.frames_in_memory ?? 0}</div>
@@ -273,52 +273,52 @@ const AccessibilityPanel = () => {
)
}
disabled={startDisabled}
className="rounded-lg border border-green-500/60 bg-green-500/20 px-3 py-2 text-sm text-green-200 disabled:opacity-50">
className="rounded-lg border border-green-500/60 bg-green-50 px-3 py-2 text-sm text-green-700 disabled:opacity-50">
{isStartingSession ? 'Starting…' : 'Start Session'}
</button>
<button
type="button"
onClick={() => void dispatch(stopAccessibilitySession('manual_stop'))}
disabled={stopDisabled}
className="rounded-lg border border-red-500/60 bg-red-500/20 px-3 py-2 text-sm text-red-200 disabled:opacity-50">
className="rounded-lg border border-red-500/60 bg-red-50 px-3 py-2 text-sm text-red-600 disabled:opacity-50">
{isStoppingSession ? 'Stopping…' : 'Stop Session'}
</button>
<button
type="button"
onClick={() => void dispatch(flushAccessibilityVision())}
disabled={isFlushingVision || !status?.session.active}
className="rounded-lg border border-cyan-500/60 bg-cyan-500/20 px-3 py-2 text-sm text-cyan-200 disabled:opacity-50">
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isFlushingVision ? 'Analyzing…' : 'Analyze Now'}
</button>
</div>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">Vision Summaries</h3>
<h3 className="text-sm font-semibold text-stone-900">Vision Summaries</h3>
<button
type="button"
onClick={() => void dispatch(fetchAccessibilityVisionRecent(10))}
disabled={isLoadingVision}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-1.5 text-xs text-stone-200 disabled:opacity-50">
className="rounded-lg border border-stone-300 bg-stone-100 px-3 py-1.5 text-xs text-stone-700 disabled:opacity-50">
{isLoadingVision ? 'Refreshing…' : 'Refresh'}
</button>
</div>
{recentVisionSummaries.length === 0 ? (
<div className="text-xs text-stone-400">No summaries yet.</div>
<div className="text-xs text-stone-500">No summaries yet.</div>
) : (
<div className="space-y-2">
{recentVisionSummaries.map(summary => (
<div
key={summary.id}
className="rounded-xl border border-stone-700 bg-stone-900/50 p-3 text-xs text-stone-200">
<div className="text-stone-400">
className="rounded-xl border border-stone-200 bg-stone-50 p-3 text-xs text-stone-600">
<div className="text-stone-500">
{new Date(summary.captured_at_ms).toLocaleTimeString()} ·{' '}
{summary.app_name ?? 'Unknown App'}
{summary.window_title ? ` · ${summary.window_title}` : ''}
</div>
<div className="mt-1 text-stone-100">{summary.actionable_notes}</div>
<div className="mt-1 text-stone-800">{summary.actionable_notes}</div>
</div>
))}
</div>
@@ -326,13 +326,13 @@ const AccessibilityPanel = () => {
</section>
{!status?.platform_supported && (
<div className="rounded-xl border border-amber-700/40 bg-amber-900/20 p-3 text-sm text-amber-200">
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-700">
Accessibility Automation V1 is currently supported on macOS only.
</div>
)}
{lastError && (
<div className="rounded-xl border border-red-700/40 bg-red-900/20 p-3 text-sm text-red-200">
<div className="rounded-xl border border-red-300 bg-red-50 p-3 text-sm text-red-600">
{lastError}
</div>
)}
@@ -11,9 +11,9 @@ const AdvancedPanel = () => {
<div className="flex-1 overflow-y-auto">
<div className="p-4 h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-stone-400"
className="w-8 h-8 text-stone-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -31,12 +31,12 @@ const AdvancedPanel = () => {
/>
</svg>
</div>
<h3 className="text-lg font-medium text-white mb-2">Advanced Settings</h3>
<p className="text-stone-400 text-sm max-w-sm mx-auto">
<h3 className="text-lg font-medium text-stone-900 mb-2">Advanced Settings</h3>
<p className="text-stone-500 text-sm max-w-sm mx-auto">
Configure advanced features, developer options, and system-level settings.
</p>
<div className="mt-6">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-100 text-stone-600 border-stone-200">
Coming Soon
</span>
</div>
@@ -77,13 +77,13 @@ const AgentChatPanel = () => {
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Overrides</h3>
<h3 className="text-lg font-semibold text-stone-900">Overrides</h3>
<p className="text-sm text-stone-400">
Inference uses your OpenHuman backend (config API URL and session). Optional model and
temperature override the defaults for this panel only.
</p>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-2 text-sm text-gray-300">
<label className="space-y-2 text-sm text-stone-600">
Model
<input
className="input input-bordered w-full text-slate-900 bg-white"
@@ -92,7 +92,7 @@ const AgentChatPanel = () => {
onChange={event => setModelOverride(event.target.value)}
/>
</label>
<label className="space-y-2 text-sm text-gray-300">
<label className="space-y-2 text-sm text-stone-600">
Temperature
<input
className="input input-bordered w-full text-slate-900 bg-white"
@@ -105,24 +105,24 @@ const AgentChatPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Conversation</h3>
<h3 className="text-lg font-semibold text-stone-900">Conversation</h3>
{error && (
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
{error}
</div>
)}
<div className="rounded-xl border border-stone-800/60 bg-black/40 p-4 space-y-3">
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4 space-y-3">
{messages.length === 0 && (
<div className="text-sm text-stone-400">Start a conversation with the agent.</div>
)}
{messages.map((message, index) => (
<div key={`${message.role}-${index}`} className="space-y-1">
<div className="text-[11px] uppercase tracking-wide text-stone-400">
<div className="text-[11px] uppercase tracking-wide text-stone-500">
{message.role === 'user' ? 'You' : 'Agent'}
</div>
<div
className={`text-sm whitespace-pre-wrap ${
message.role === 'user' ? 'text-white' : 'text-emerald-200'
message.role === 'user' ? 'text-stone-900' : 'text-emerald-700'
}`}>
{message.text}
</div>
@@ -327,9 +327,9 @@ const AutocompletePanel = () => {
<SettingsHeader title="Inline Autocomplete" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Runtime</h3>
<div className="text-sm text-stone-300 space-y-1">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Runtime</h3>
<div className="text-sm text-stone-700 space-y-1">
<div>Platform supported: {status?.platform_supported ? 'yes' : 'no'}</div>
<div>Enabled: {status?.enabled ? 'yes' : 'no'}</div>
<div>Running: {status?.running ? 'yes' : 'no'}</div>
@@ -345,44 +345,44 @@ const AutocompletePanel = () => {
type="button"
onClick={() => void refreshStatus()}
disabled={isLoading}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-2 text-sm text-stone-200 disabled:opacity-50">
className="rounded-lg border border-stone-300 bg-stone-100 px-3 py-2 text-sm text-stone-700 disabled:opacity-50">
{isLoading ? 'Refreshing…' : 'Refresh Status'}
</button>
<button
type="button"
onClick={() => void start()}
className="rounded-lg border border-green-500/60 bg-green-500/20 px-3 py-2 text-sm text-green-200 disabled:opacity-50">
className="rounded-lg border border-green-500/60 bg-green-50 px-3 py-2 text-sm text-green-700 disabled:opacity-50">
Start
</button>
<button
type="button"
onClick={() => void stop()}
className="rounded-lg border border-red-500/60 bg-red-500/20 px-3 py-2 text-sm text-red-200 disabled:opacity-50">
className="rounded-lg border border-red-500/60 bg-red-50 px-3 py-2 text-sm text-red-600 disabled:opacity-50">
Stop
</button>
</div>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Settings</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Enabled</span>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Settings</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Enabled</span>
<input
type="checkbox"
checked={enabled}
onChange={event => setEnabled(event.target.checked)}
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Accept With Tab</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Accept With Tab</span>
<input
type="checkbox"
checked={acceptWithTab}
onChange={event => setAcceptWithTab(event.target.checked)}
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Debounce (ms)</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Debounce (ms)</span>
<input
type="number"
min={50}
@@ -390,11 +390,11 @@ const AutocompletePanel = () => {
step={10}
value={debounceMs}
onChange={event => setDebounceMs(event.target.value)}
className="w-28 rounded border border-stone-600 bg-stone-800 px-2 py-1 text-xs text-stone-200"
className="w-28 rounded border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700"
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Max Chars</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Max Chars</span>
<input
type="number"
min={32}
@@ -402,15 +402,15 @@ const AutocompletePanel = () => {
step={8}
value={maxChars}
onChange={event => setMaxChars(event.target.value)}
className="w-28 rounded border border-stone-600 bg-stone-800 px-2 py-1 text-xs text-stone-200"
className="w-28 rounded border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700"
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Style Preset</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Style Preset</span>
<select
value={stylePreset}
onChange={event => setStylePreset(event.target.value)}
className="rounded border border-stone-600 bg-stone-800 px-2 py-1 text-xs text-stone-200">
className="rounded border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700">
<option value="balanced">Balanced</option>
<option value="concise">Concise</option>
<option value="formal">Formal</option>
@@ -419,55 +419,55 @@ const AutocompletePanel = () => {
</select>
</label>
<div className="space-y-1">
<div className="text-xs text-stone-300">Style Instructions</div>
<div className="text-xs text-stone-600">Style Instructions</div>
<textarea
value={styleInstructions}
onChange={event => setStyleInstructions(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
<div className="space-y-1">
<div className="text-xs text-stone-300">Style Examples (one per line)</div>
<div className="text-xs text-stone-600">Style Examples (one per line)</div>
<textarea
value={styleExamplesText}
onChange={event => setStyleExamplesText(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
<div className="space-y-1">
<div className="text-xs text-stone-300">
<div className="text-xs text-stone-600">
Disabled Apps (one bundle/app token per line)
</div>
<textarea
value={disabledAppsText}
onChange={event => setDisabledAppsText(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
<button
type="button"
onClick={() => void saveConfig()}
disabled={isSaving}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isSaving ? 'Saving…' : 'Save Autocomplete Settings'}
</button>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">Personalization History</h3>
<h3 className="text-sm font-semibold text-stone-900">Personalization History</h3>
<button
type="button"
onClick={() => void clearHistory()}
disabled={isClearingHistory || historyEntries.length === 0}
className="rounded-lg border border-red-500/60 bg-red-500/20 px-3 py-1.5 text-xs text-red-200 disabled:opacity-40">
className="rounded-lg border border-red-500/60 bg-red-50 px-3 py-1.5 text-xs text-red-600 disabled:opacity-40">
{isClearingHistory ? 'Clearing…' : 'Clear History'}
</button>
</div>
<p className="text-xs text-stone-400">
<p className="text-xs text-stone-500">
{isHistoryLoading
? 'Loading…'
: historyEntries.length === 0
@@ -475,26 +475,28 @@ const AutocompletePanel = () => {
: `${String(historyEntries.length)} accepted completion${historyEntries.length === 1 ? '' : 's'} stored — used to personalise future suggestions.`}
</p>
{historyEntries.length > 0 && (
<div className="max-h-48 overflow-y-auto rounded-xl border border-stone-700 bg-stone-900/50 p-2 space-y-1">
<div className="max-h-48 overflow-y-auto rounded-xl border border-stone-200 bg-stone-50 p-2 space-y-1">
{historyEntries.map((entry, idx) => (
<div
key={`${String(entry.timestamp_ms)}-${String(idx)}`}
className="flex flex-col gap-0.5 rounded-lg bg-stone-800/40 px-2 py-1.5 text-xs">
<div className="flex items-center gap-2 text-stone-400">
className="flex flex-col gap-0.5 rounded-lg bg-white px-2 py-1.5 text-xs border border-stone-100 text-xs">
<div className="flex items-center gap-2 text-stone-500">
<span className="shrink-0">
{new Date(entry.timestamp_ms).toLocaleString()}
</span>
{entry.app_name && (
<span className="rounded bg-stone-700/60 px-1 text-stone-300">
<span className="rounded bg-stone-100 px-1 text-stone-600">
{entry.app_name}
</span>
)}
</div>
<div className="flex items-baseline gap-1 text-stone-200 truncate">
<span className="shrink-0 text-stone-500"></span>
<span className="truncate text-stone-400">{entry.context.slice(-40)}</span>
<span className="shrink-0 text-stone-500"></span>
<span className="font-medium text-cyan-300 truncate">{entry.suggestion}</span>
<div className="flex items-baseline gap-1 text-stone-700 truncate">
<span className="shrink-0 text-stone-400"></span>
<span className="truncate text-stone-500">{entry.context.slice(-40)}</span>
<span className="shrink-0 text-stone-400"></span>
<span className="font-medium text-primary-500 truncate">
{entry.suggestion}
</span>
</div>
</div>
))}
@@ -502,61 +504,61 @@ const AutocompletePanel = () => {
)}
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Test</h3>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Test</h3>
<div className="space-y-1">
<div className="text-xs text-stone-300">Context Override (optional)</div>
<div className="text-xs text-stone-600">Context Override (optional)</div>
<textarea
value={contextOverride}
onChange={event => setContextOverride(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => void testCurrent()}
className="rounded-lg border border-cyan-500/60 bg-cyan-500/20 px-3 py-2 text-sm text-cyan-200">
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600">
Get Suggestion
</button>
<button
type="button"
onClick={() => void acceptSuggestion()}
className="rounded-lg border border-emerald-500/60 bg-emerald-500/20 px-3 py-2 text-sm text-emerald-200">
className="rounded-lg border border-emerald-500/60 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">
Accept Suggestion
</button>
<button
type="button"
onClick={() => void debugFocus()}
className="rounded-lg border border-amber-500/60 bg-amber-500/20 px-3 py-2 text-sm text-amber-200">
className="rounded-lg border border-amber-500/60 bg-amber-50 px-3 py-2 text-sm text-amber-700">
Debug Focus
</button>
</div>
{focusDebug && (
<pre className="max-h-48 overflow-auto rounded-xl border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200">
<pre className="max-h-48 overflow-auto rounded-xl border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700">
{focusDebug}
</pre>
)}
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">Live Logs</h3>
<h3 className="text-sm font-semibold text-stone-900">Live Logs</h3>
<button
type="button"
onClick={() => setLogs([])}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-1.5 text-xs text-stone-200">
className="rounded-lg border border-stone-300 bg-stone-100 px-3 py-1.5 text-xs text-stone-700">
Clear
</button>
</div>
<pre className="max-h-56 overflow-auto rounded-xl border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200">
<pre className="max-h-56 overflow-auto rounded-xl border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700">
{logs.length > 0 ? logs.join('\n') : 'No logs yet.'}
</pre>
</section>
{message && <div className="text-xs text-green-300">{message}</div>}
{error && <div className="text-xs text-red-300">{error}</div>}
{message && <div className="text-xs text-green-700">{message}</div>}
{error && <div className="text-xs text-red-600">{error}</div>}
</div>
</div>
);
@@ -378,9 +378,11 @@ const BillingPanel = () => {
<div className="space-y-2">
<div className="max-w-md mt-4 mx-auto px-4 space-y-3">
{/* ── Current Plan Header ───────────────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<div className="rounded-2xl border border-stone-200 bg-white p-3">
<div className="flex items-center justify-between mb-1.5">
<h3 className="text-sm font-semibold text-white">Current Plan {currentTier}</h3>
<h3 className="text-sm font-semibold text-stone-900">
Current Plan {currentTier}
</h3>
{hasActive && (
<button
onClick={handleManageSubscription}
@@ -402,9 +404,9 @@ const BillingPanel = () => {
</div>
{/* ── Inference Budget (Team Usage) ─────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<div className="rounded-2xl border border-stone-200 bg-white p-3">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-white">Inference Budget</h3>
<h3 className="text-sm font-semibold text-stone-900">Inference Budget</h3>
{isLoadingCredits && <span className="text-[10px] text-stone-500">Loading</span>}
{teamUsage && !isLoadingCredits && (
<span className="text-xs text-stone-400">
@@ -456,20 +458,20 @@ const BillingPanel = () => {
</div>
{/* ── Credits Balance & Top-up ──────────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<h3 className="text-sm font-semibold text-white mb-2">Credits Balance</h3>
<div className="rounded-2xl border border-stone-200 bg-white p-3">
<h3 className="text-sm font-semibold text-stone-900 mb-2">Credits Balance</h3>
{creditBalance ? (
<div className="space-y-1.5 mb-3">
<div className="flex items-center justify-between">
<span className="text-xs text-stone-400">General credits</span>
<span className="text-xs font-medium text-white">
<span className="text-xs font-medium text-stone-900">
${creditBalance.balanceUsd.toFixed(2)}
</span>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-stone-400">Top-up credits</span>
<span className="text-xs font-medium text-white">
<span className="text-xs font-medium text-stone-900">
${creditBalance.topUpBalanceUsd.toFixed(2)}
{creditBalance.topUpBaselineUsd != null &&
creditBalance.topUpBaselineUsd > 0 && (
@@ -536,7 +538,7 @@ const BillingPanel = () => {
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
billingInterval === 'monthly'
? 'bg-primary-500/20 text-primary-400 border border-primary-500/30'
: 'text-stone-400 hover:text-stone-300'
: 'text-stone-500 hover:text-stone-700'
} ${paymentMethod === 'crypto' ? 'opacity-40 cursor-not-allowed' : ''}`}>
Monthly
</button>
@@ -545,7 +547,7 @@ const BillingPanel = () => {
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
billingInterval === 'annual'
? 'bg-primary-500/20 text-primary-400 border border-primary-500/30'
: 'text-stone-400 hover:text-stone-300'
: 'text-stone-500 hover:text-stone-700'
}`}>
Annual
</button>
@@ -566,15 +568,15 @@ const BillingPanel = () => {
className={`rounded-2xl border p-3 transition-all ${
isCurrent
? 'border-primary-500/40 bg-primary-500/5'
: 'border-stone-700/50 bg-stone-800/40'
: 'border-stone-200 bg-white'
}`}>
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h4 className="text-sm font-semibold text-white">{plan.name}</h4>
<h4 className="text-sm font-semibold text-stone-900">{plan.name}</h4>
{/* Features inline with title */}
{plan.features.map(f => (
<span key={f.text} className="text-xs text-stone-300">
<span key={f.text} className="text-xs text-stone-600">
<span className="text-stone-500 mx-1"></span>
{f.text}
</span>
@@ -591,7 +593,7 @@ const BillingPanel = () => {
)}
</div>
<div className="mt-0.5 flex items-baseline gap-1">
<span className="text-xl font-bold text-white">
<span className="text-xl font-bold text-stone-900">
{displayPrice(plan, billingInterval)}
</span>
{plan.tier !== 'FREE' && (
@@ -669,7 +671,7 @@ const BillingPanel = () => {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<p className="text-xs text-amber-300">
<p className="text-xs text-amber-700">
Waiting for payment confirmation... Complete checkout in the browser window that
opened.
</p>
@@ -678,9 +680,9 @@ const BillingPanel = () => {
)}
{/* ── Pay with crypto toggle ────────────────────────────── */}
<div className="flex items-center justify-between rounded-xl bg-stone-800/40 border border-stone-700/40 p-3 mx-4">
<div className="flex items-center justify-between rounded-xl bg-stone-50 border border-stone-200 p-3 mx-4">
<div>
<p className="text-xs font-medium text-white">Pay with Crypto</p>
<p className="text-xs font-medium text-stone-900">Pay with Crypto</p>
<p className="text-[11px] text-stone-400 mt-0.5">
You can choose to pay annually using crypto
</p>
@@ -702,11 +704,11 @@ const BillingPanel = () => {
{/* ── Auto-Recharge Credits ─────────────────────────────── */}
<div className="px-4 pt-2">
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 overflow-hidden">
<div className="rounded-2xl border border-stone-200 bg-white overflow-hidden">
{/* Header row */}
<div className="flex items-center justify-between p-3">
<div>
<p className="text-xs font-semibold text-white">Auto-Recharge Credits</p>
<p className="text-xs font-semibold text-stone-900">Auto-Recharge Credits</p>
<p className="text-[11px] text-stone-400 mt-0.5">
Automatically top up when your balance runs low
</p>
@@ -770,11 +772,11 @@ const BillingPanel = () => {
{/* Settings — only shown when enabled */}
{!arLoading && arSettings?.enabled && (
<div className="border-t border-stone-700/50 px-3 pt-3 pb-2 space-y-3">
<div className="border-t border-stone-200 px-3 pt-3 pb-2 space-y-3">
{/* Status row */}
<div className="flex items-center gap-3 flex-wrap">
{arSettings.inFlight && (
<span className="flex items-center gap-1 text-[10px] text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-full px-2 py-0.5">
<span className="flex items-center gap-1 text-[10px] text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5">
<svg className="w-2.5 h-2.5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
@@ -844,7 +846,7 @@ const BillingPanel = () => {
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
arThreshold === v
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
: 'bg-stone-100 text-stone-500 border-stone-200 hover:text-stone-700'
}`}>
${v}
</button>
@@ -863,7 +865,7 @@ const BillingPanel = () => {
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
arAmount === v
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
: 'bg-stone-100 text-stone-500 border-stone-200 hover:text-stone-700'
}`}>
${v}
</button>
@@ -882,7 +884,7 @@ const BillingPanel = () => {
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
arWeeklyLimit === v
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
: 'bg-stone-100 text-stone-500 border-stone-200 hover:text-stone-700'
}`}>
${v}
</button>
@@ -914,9 +916,9 @@ const BillingPanel = () => {
)}
{/* Payment methods */}
<div className="border-t border-stone-700/50 px-3 py-2.5">
<div className="border-t border-stone-200 px-3 py-2.5">
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-medium text-stone-300">Payment Methods</p>
<p className="text-[11px] font-medium text-stone-600">Payment Methods</p>
<button
onClick={handleAddCard}
className="text-[11px] text-primary-400 hover:text-primary-300 font-medium transition-colors">
@@ -931,7 +933,7 @@ const BillingPanel = () => {
))}
</div>
) : cards.length === 0 ? (
<div className="flex items-center gap-2 rounded-lg bg-stone-700/20 border border-stone-700/30 p-2.5">
<div className="flex items-center gap-2 rounded-lg bg-stone-50 border border-stone-200 p-2.5">
<svg
className="w-4 h-4 text-stone-500 flex-shrink-0"
fill="none"
@@ -958,7 +960,7 @@ const BillingPanel = () => {
return (
<div
key={card.id}
className="flex items-center gap-2 rounded-lg bg-stone-700/20 border border-stone-700/30 px-2.5 py-2">
className="flex items-center gap-2 rounded-lg bg-stone-50 border border-stone-200 px-2.5 py-2">
{/* Card icon */}
<svg
className="w-4 h-4 text-stone-400 flex-shrink-0"
@@ -976,7 +978,7 @@ const BillingPanel = () => {
{/* Card info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-xs text-white font-medium">
<span className="text-xs text-stone-900 font-medium">
{cardBrandLabel(card.brand)} {card.last4}
</span>
{card.isDefault && (
@@ -997,7 +999,7 @@ const BillingPanel = () => {
<button
onClick={() => handleSetDefault(card.id)}
disabled={!!settingDefaultId || !!deletingCardId}
className="text-[10px] text-stone-400 hover:text-stone-300 transition-colors disabled:opacity-40 disabled:cursor-not-allowed px-1.5 py-1">
className="text-[10px] text-stone-500 hover:text-stone-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed px-1.5 py-1">
{isSettingDefault ? '…' : 'Set default'}
</button>
)}
@@ -1037,8 +1039,8 @@ const BillingPanel = () => {
{/* ── Upgrade benefits ───────────────────────────────────── */}
<div className="px-4 pb-4 pt-2">
<div className="rounded-xl bg-gradient-to-br from-primary-500/10 to-sage-500/10 border border-primary-500/20 p-4">
<h3 className="text-sm font-semibold text-white mb-2">Why upgrade?</h3>
<ul className="space-y-1.5 text-xs text-stone-300">
<h3 className="text-sm font-semibold text-stone-900 mb-2">Why upgrade?</h3>
<ul className="space-y-1.5 text-xs text-stone-600">
<li className="flex items-start gap-2">
<svg
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
@@ -115,18 +115,18 @@ function ConnectionOptionRow({
<button
onClick={() => onConnect(option)}
disabled={isDisabled}
className={`w-full flex items-center justify-between p-3 bg-black/50 ${
isLast ? '' : 'border-b border-stone-700'
} hover:bg-stone-800/30 transition-all duration-200 text-left ${
className={`w-full flex items-center justify-between p-3 bg-white ${
isLast ? '' : 'border-b border-stone-200'
} hover:bg-stone-50 transition-all duration-200 text-left ${
isFirst ? 'first:rounded-t-3xl' : ''
} ${
isLast ? 'last:rounded-b-3xl' : ''
} focus:outline-none focus:ring-0 focus:border-inherit relative ${
isDisabled ? 'opacity-60 cursor-not-allowed' : ''
}`}>
<div className="w-5 h-5 opacity-60 flex-shrink-0 mr-3 text-white">{option.icon}</div>
<div className="w-5 h-5 opacity-60 flex-shrink-0 mr-3 text-stone-700">{option.icon}</div>
<div className="flex-1">
<div className="font-medium text-sm mb-1 text-white">{option.name}</div>
<div className="font-medium text-sm mb-1 text-stone-900">{option.name}</div>
<p className="opacity-70 text-xs">{option.description}</p>
</div>
<div className="flex items-center space-x-3">{badge}</div>
@@ -207,10 +207,10 @@ const ConnectionsPanel = () => {
</div>
{/* Security notice */}
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-xl">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-xl">
<div className="flex items-start space-x-2">
<svg
className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -222,8 +222,8 @@ const ConnectionsPanel = () => {
/>
</svg>
<div>
<p className="font-medium text-blue-300 text-sm">Privacy & Security</p>
<p className="text-blue-200 text-xs mt-1">
<p className="font-medium text-blue-700 text-sm">Privacy & Security</p>
<p className="text-blue-600 text-xs mt-1">
All data and credentials are stored locally with zero-data retention policy. Your
information is encrypted and never shared with third parties.
</p>
@@ -308,29 +308,29 @@ const CronJobsPanel = () => {
<SettingsHeader title="Cron Jobs" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="rounded-xl border border-stone-800/60 bg-black/40 p-4 space-y-2">
<h3 className="text-lg font-semibold text-white">Scheduled Jobs</h3>
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-2">
<h3 className="text-lg font-semibold text-stone-900">Scheduled Jobs</h3>
<p className="text-xs text-stone-400">
Manage cron jobs from both the core scheduler and runtime skills.
</p>
</section>
{error && (
<div className="rounded-lg border border-coral-500/40 bg-coral-500/10 px-4 py-3 text-sm text-coral-200">
<div className="rounded-lg border border-coral-300 bg-coral-50 px-4 py-3 text-sm text-coral-700">
{error}
</div>
)}
{coreError && (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
<div className="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-700">
{coreError}
</div>
)}
<section className="rounded-xl border border-stone-800/60 bg-black/40">
<div className="p-4 border-b border-stone-800/60">
<h3 className="text-sm font-semibold text-white">Core Cron Jobs</h3>
<p className="text-xs text-stone-400 mt-1">
<section className="rounded-xl border border-stone-200 bg-white">
<div className="p-4 border-b border-stone-200">
<h3 className="text-sm font-semibold text-stone-900">Core Cron Jobs</h3>
<p className="text-xs text-stone-500 mt-1">
Jobs persisted in the OpenHuman core scheduler database.
</p>
</div>
@@ -347,26 +347,28 @@ const CronJobsPanel = () => {
return (
<div
key={job.id}
className={`p-4 ${index === 0 ? '' : 'border-t border-stone-800/60'} space-y-3`}>
className={`p-4 ${index === 0 ? '' : 'border-t border-stone-200'} space-y-3`}>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold text-white">{job.name || job.id}</div>
<div className="text-[11px] text-stone-500">{job.id}</div>
<div className="text-sm font-semibold text-stone-900">
{job.name || job.id}
</div>
<div className="text-[11px] text-stone-400">{job.id}</div>
</div>
<span
className={`px-2 py-1 text-[11px] font-semibold uppercase border rounded-full ${
job.enabled
? 'bg-sage-500/20 text-sage-300 border-sage-500/30'
: 'bg-stone-500/20 text-stone-300 border-stone-500/30'
? 'bg-sage-50 text-sage-700 border-sage-200'
: 'bg-stone-100 text-stone-600 border-stone-200'
}`}>
{job.enabled ? 'Enabled' : 'Paused'}
</span>
</div>
<div className="text-xs text-stone-300 space-y-1">
<div className="text-xs text-stone-600 space-y-1">
<div>
Schedule:{' '}
<span className="font-medium text-stone-200">
<span className="font-medium text-stone-700">
{job.schedule.kind === 'cron'
? job.schedule.expr
: job.schedule.kind === 'every'
@@ -376,14 +378,14 @@ const CronJobsPanel = () => {
</div>
<div>
Next run:{' '}
<span className="font-medium text-stone-200">
<span className="font-medium text-stone-700">
{new Date(job.next_run).toLocaleString()}
</span>
</div>
{job.last_status && (
<div>
Last status:{' '}
<span className="font-medium text-stone-200">{job.last_status}</span>
<span className="font-medium text-stone-700">{job.last_status}</span>
</div>
)}
</div>
@@ -432,13 +434,13 @@ const CronJobsPanel = () => {
</div>
{runs.length > 0 && (
<div className="rounded-lg border border-stone-700/60 bg-stone-900/30 p-3 space-y-1">
<div className="text-[11px] uppercase tracking-wide text-stone-500">
<div className="rounded-lg border border-stone-200 bg-stone-50 p-3 space-y-1">
<div className="text-[11px] uppercase tracking-wide text-stone-400">
Recent Runs
</div>
{runs.map(run => (
<div key={run.id} className="text-xs text-stone-300">
<span className="font-medium text-stone-200">{run.status}</span> at{' '}
<div key={run.id} className="text-xs text-stone-600">
<span className="font-medium text-stone-700">{run.status}</span> at{' '}
{new Date(run.finished_at).toLocaleString()}
</div>
))}
@@ -449,10 +451,10 @@ const CronJobsPanel = () => {
})}
</section>
<section className="rounded-xl border border-stone-800/60 bg-black/40">
<div className="p-4 border-b border-stone-800/60">
<h3 className="text-sm font-semibold text-white">Runtime Skill Cron Settings</h3>
<p className="text-xs text-stone-400 mt-1">
<section className="rounded-xl border border-stone-200 bg-white">
<div className="p-4 border-b border-stone-200">
<h3 className="text-sm font-semibold text-stone-900">Runtime Skill Cron Settings</h3>
<p className="text-xs text-stone-500 mt-1">
Skill-level cron and interval options from the runtime.
</p>
</div>
@@ -471,31 +473,31 @@ const CronJobsPanel = () => {
skills.map((skill, skillIndex) => (
<div
key={skill.skillId}
className={`p-4 ${skillIndex === 0 ? '' : 'border-t border-stone-800/60'}`}>
className={`p-4 ${skillIndex === 0 ? '' : 'border-t border-stone-200'}`}>
<div className="flex items-center justify-between gap-3 mb-3">
<div>
<div className="text-sm font-semibold text-white">{skill.name}</div>
<div className="text-[11px] text-stone-500">{skill.skillId}</div>
<div className="text-sm font-semibold text-stone-900">{skill.name}</div>
<div className="text-[11px] text-stone-400">{skill.skillId}</div>
</div>
<span
className={`px-2 py-1 text-[11px] font-semibold uppercase border rounded-full ${
skill.enabled
? 'bg-sage-500/20 text-sage-300 border-sage-500/30'
: 'bg-stone-500/20 text-stone-300 border-stone-500/30'
? 'bg-sage-50 text-sage-700 border-sage-200'
: 'bg-stone-100 text-stone-600 border-stone-200'
}`}>
{skill.enabled ? 'Enabled' : 'Disabled'}
</span>
</div>
{skill.manifestTickInterval !== null && (
<div className="rounded-lg border border-stone-700/60 bg-stone-900/30 px-3 py-2 text-xs text-stone-300 mb-3">
<div className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-2 text-xs text-stone-600 mb-3">
Manifest tick interval:{' '}
<span className="font-semibold">{skill.manifestTickInterval}s</span>
</div>
)}
{skill.optionsError && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200 mb-3">
<div className="rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-700 mb-3">
Could not load runtime options: {skill.optionsError}
</div>
)}
@@ -510,9 +512,9 @@ const CronJobsPanel = () => {
return (
<div
key={optionKey}
className="rounded-lg border border-stone-700/60 bg-stone-900/20 p-3 space-y-2">
className="rounded-lg border border-stone-200 bg-stone-50 p-3 space-y-2">
<div>
<div className="text-xs font-medium text-stone-200">{option.label}</div>
<div className="text-xs font-medium text-stone-700">{option.label}</div>
{option.description && (
<div className="text-[11px] text-stone-500 mt-0.5">
{option.description}
@@ -521,7 +523,7 @@ const CronJobsPanel = () => {
</div>
{option.type === 'boolean' && (
<label className="flex items-center gap-2 text-xs text-stone-300">
<label className="flex items-center gap-2 text-xs text-stone-600">
<input
type="checkbox"
className="checkbox checkbox-primary"
@@ -131,24 +131,24 @@ const DictationPanel = () => {
<div className="flex-1 overflow-y-auto p-4 space-y-5 max-w-md mx-auto w-full">
<div>
<h2 className="text-lg font-semibold text-white mb-1">Voice Dictation</h2>
<p className="text-sm text-stone-400">
<h2 className="text-lg font-semibold text-stone-900 mb-1">Voice Dictation</h2>
<p className="text-sm text-stone-500">
Transcribe speech to text using your microphone and local AI.
</p>
</div>
{/* STT Engine Status */}
<div className="bg-stone-800/50 rounded-xl border border-stone-700/40 p-4 space-y-3">
<div className="bg-white rounded-xl border border-stone-200 p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-white">Speech-to-Text Engine</p>
<p className="text-sm font-medium text-stone-900">Speech-to-Text Engine</p>
<p className="text-xs text-stone-400 mt-0.5">{statusLabel()}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => void dispatch(checkDictationAvailability())}
disabled={isCheckingStatus}
className="text-xs text-stone-400 hover:text-white transition-colors disabled:opacity-40 px-2 py-1 rounded border border-stone-700 hover:border-stone-500">
className="text-xs text-stone-400 hover:text-stone-900 transition-colors disabled:opacity-40 px-2 py-1 rounded border border-stone-200 hover:border-stone-300">
{isCheckingStatus ? '...' : 'Refresh'}
</button>
<div className={`w-2.5 h-2.5 rounded-full ${statusColor()}`} />
@@ -157,7 +157,7 @@ const DictationPanel = () => {
{/* Detailed status rows */}
{voiceStatus && (
<div className="space-y-1.5 pt-1 border-t border-stone-700/40">
<div className="space-y-1.5 pt-1 border-t border-stone-200">
<StatusRow
label="Whisper binary"
value={voiceStatus.whisper_binary ?? 'not found'}
@@ -180,12 +180,12 @@ const DictationPanel = () => {
{/* Model not found guidance */}
{voiceStatus && !voiceStatus.stt_model_path && !isCheckingStatus && (
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4">
<p className="text-xs text-amber-400 leading-relaxed">
Model file <code className="text-amber-300">{voiceStatus.stt_model_id}</code> was not
found. Go to <strong className="text-amber-300">Settings Local AI Model</strong> to
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<p className="text-xs text-amber-700 leading-relaxed">
Model file <code className="text-amber-700">{voiceStatus.stt_model_id}</code> was not
found. Go to <strong className="text-amber-700">Settings Local AI Model</strong> to
download it, or place the file at{' '}
<code className="text-amber-300 break-all">
<code className="text-amber-700 break-all">
{(sttModelDirectory ?? '<workspace>/models/local-ai/stt') +
(sttModelDirectory?.includes('\\') ? '\\' : '/') +
voiceStatus.stt_model_id}
@@ -195,9 +195,9 @@ const DictationPanel = () => {
)}
{/* Global Hotkey */}
<div className="bg-stone-800/50 rounded-xl border border-stone-700/40 p-4 space-y-3">
<div className="bg-white rounded-xl border border-stone-200 p-4 space-y-3">
<div>
<p className="text-sm font-medium text-white">Global Hotkey</p>
<p className="text-sm font-medium text-stone-900">Global Hotkey</p>
<p className="text-xs text-stone-400 mt-0.5">
Press anywhere to start / stop dictation
</p>
@@ -208,7 +208,7 @@ const DictationPanel = () => {
value={hotkeyInput}
onChange={e => setHotkeyInput(e.target.value)}
placeholder="e.g. CmdOrCtrl+Shift+D"
className="flex-1 bg-stone-700/60 border border-stone-600/50 rounded-lg px-3 py-2 text-sm text-white placeholder-stone-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
className="flex-1 bg-white border border-stone-200 rounded-lg px-3 py-2 text-sm text-stone-900 placeholder-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<button
onClick={() => void handleSaveHotkey()}
@@ -217,18 +217,20 @@ const DictationPanel = () => {
{isSavingHotkey ? 'Saving...' : hotkeySuccess ? 'Saved!' : 'Save'}
</button>
</div>
{hotkeyError && <p className="text-xs text-red-400">{hotkeyError}</p>}
<p className="text-xs text-stone-500">
{hotkeyError && <p className="text-xs text-red-600">{hotkeyError}</p>}
<p className="text-xs text-stone-400">
Modifiers: <code>CmdOrCtrl</code>, <code>Alt</code>, <code>Shift</code>,{' '}
<code>Super</code> (also accepts CommandOrControl)
</p>
</div>
{/* Floating launcher preference */}
<div className="bg-stone-800/50 rounded-xl border border-stone-700/40 p-4 space-y-3">
<div className="bg-white rounded-xl border border-stone-200 p-4 space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-white">Always show floating Start button</p>
<p className="text-sm font-medium text-stone-900">
Always show floating Start button
</p>
<p className="text-xs text-stone-400 mt-0.5">
If disabled, dictation starts via hotkey only while idle.
</p>
@@ -251,9 +253,9 @@ const DictationPanel = () => {
</div>
{/* How to use */}
<div className="bg-stone-800/50 rounded-xl border border-stone-700/40 p-4 space-y-2">
<p className="text-sm font-medium text-white">How to use</p>
<ol className="text-xs text-stone-400 space-y-1.5 list-decimal list-inside">
<div className="bg-white rounded-xl border border-stone-200 p-4 space-y-2">
<p className="text-sm font-medium text-stone-900">How to use</p>
<ol className="text-xs text-stone-500 space-y-1.5 list-decimal list-inside">
<li>Press the global hotkey (or click Record in the overlay)</li>
<li>Speak clearly into your microphone</li>
<li>Press the hotkey again (or click Stop) to finish</li>
@@ -280,8 +282,8 @@ const StatusRow = ({ label, value, ok, muted }: StatusRowProps) => (
muted ? 'bg-stone-600' : ok ? 'bg-green-400' : 'bg-red-400'
}`}
/>
<span className="text-stone-500 flex-shrink-0 w-28">{label}</span>
<span className="text-stone-300 break-all leading-relaxed">{value}</span>
<span className="text-stone-400 flex-shrink-0 w-28">{label}</span>
<span className="text-stone-600 break-all leading-relaxed">{value}</span>
</div>
);
@@ -42,17 +42,17 @@ import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
const statusTone = (state: string): string => {
switch (state) {
case 'ready':
return 'text-green-300';
return 'text-green-600';
case 'downloading':
case 'installing':
case 'loading':
return 'text-blue-300';
return 'text-primary-600';
case 'degraded':
return 'text-amber-300';
return 'text-amber-700';
case 'disabled':
return 'text-stone-400';
return 'text-stone-500';
default:
return 'text-stone-200';
return 'text-stone-700';
}
};
@@ -373,42 +373,42 @@ const LocalModelPanel = () => {
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
{/* --- Model Tier Selection --- */}
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Model Tier</h3>
<h3 className="text-lg font-semibold text-stone-900">Model Tier</h3>
{/* Loading / error states */}
{presetsLoading && !presetsData && (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 text-sm text-stone-400 animate-pulse">
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 text-sm text-stone-500 animate-pulse">
Loading device info and presets
</div>
)}
{!presetsLoading && !presetsData && presetError && (
<div className="bg-gray-900 rounded-lg border border-red-700/40 p-4 text-sm text-red-300">
<div className="bg-red-50 rounded-lg border border-red-300 p-4 text-sm text-red-600">
Could not load presets: {presetError}
</div>
)}
{/* Device info */}
{presetsData?.device && (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-3">
<div className="bg-stone-50 rounded-lg border border-stone-200 p-3">
<div className="grid grid-cols-3 gap-3 text-xs">
<div>
<div className="text-stone-400 uppercase tracking-wide">RAM</div>
<div className="text-stone-100 mt-0.5 font-medium">
<div className="text-stone-500 uppercase tracking-wide">RAM</div>
<div className="text-stone-800 mt-0.5 font-medium">
{formatRamGb(presetsData.device.total_ram_bytes)}
</div>
</div>
<div>
<div className="text-stone-400 uppercase tracking-wide">CPU</div>
<div className="text-stone-500 uppercase tracking-wide">CPU</div>
<div
className="text-stone-100 mt-0.5 font-medium truncate"
className="text-stone-800 mt-0.5 font-medium truncate"
title={presetsData.device.cpu_brand}>
{presetsData.device.cpu_count} cores
</div>
</div>
<div>
<div className="text-stone-400 uppercase tracking-wide">GPU</div>
<div className="text-stone-500 uppercase tracking-wide">GPU</div>
<div
className="text-stone-100 mt-0.5 font-medium truncate"
className="text-stone-800 mt-0.5 font-medium truncate"
title={presetsData.device.gpu_description ?? undefined}>
{presetsData.device.has_gpu
? (presetsData.device.gpu_description ?? 'Detected')
@@ -433,24 +433,24 @@ const LocalModelPanel = () => {
disabled={isApplyingPreset || isCurrent}
className={`w-full text-left rounded-lg border p-3 transition-colors ${
isCurrent
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 bg-gray-900 hover:border-gray-500'
? 'border-primary-400 bg-primary-50'
: 'border-stone-200 bg-white hover:border-stone-300'
} ${isApplyingPreset ? 'opacity-60' : ''}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-white">{preset.label}</span>
<span className="text-sm font-semibold text-stone-900">{preset.label}</span>
{isRecommended && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-emerald-600/30 text-emerald-300 uppercase tracking-wide">
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-emerald-50 text-emerald-700 uppercase tracking-wide">
Recommended
</span>
)}
{isCurrent && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-blue-600/30 text-blue-300 uppercase tracking-wide">
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-primary-50 text-primary-600 uppercase tracking-wide">
Active
</span>
)}
</div>
<span className="text-xs text-stone-400">
<span className="text-xs text-stone-500">
~{preset.approx_download_gb} GB
</span>
</div>
@@ -463,16 +463,16 @@ const LocalModelPanel = () => {
})}
{presetsData.current_tier === 'custom' && (
<div className="rounded-lg border border-amber-600/30 bg-amber-600/5 p-3 text-xs text-amber-300">
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-700">
You are using custom model IDs that do not match any built-in preset.
</div>
)}
</div>
)}
{presetError && <div className="text-xs text-red-300">{presetError}</div>}
{presetError && <div className="text-xs text-red-600">{presetError}</div>}
{presetSuccess && (
<div className="text-xs text-green-300">
<div className="text-xs text-green-700">
Applied {presetSuccess.applied_tier} tier: {presetSuccess.chat_model_id}
</div>
)}
@@ -482,7 +482,7 @@ const LocalModelPanel = () => {
<button
type="button"
onClick={() => setShowAdvanced(prev => !prev)}
className="flex items-center gap-2 text-sm text-stone-400 hover:text-stone-200 transition-colors">
className="flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700 transition-colors">
<svg
className={`w-4 h-4 transition-transform ${showAdvanced ? 'rotate-90' : ''}`}
fill="none"
@@ -497,23 +497,23 @@ const LocalModelPanel = () => {
<>
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Runtime Status</h3>
<h3 className="text-lg font-semibold text-stone-900">Runtime Status</h3>
<button
onClick={() => void loadStatus()}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
className="text-sm text-primary-500 hover:text-primary-600 transition-colors">
Refresh
</button>
</div>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">State</span>
<span className="text-stone-500">State</span>
<span className={`font-medium ${statusTone(status?.state ?? 'idle')}`}>
{status ? statusLabel(downloads?.state ?? status.state) : 'Unavailable'}
</span>
</div>
<div className="h-2 rounded-full bg-stone-800 overflow-hidden">
<div className="h-2 rounded-full bg-stone-200 overflow-hidden">
<div
className={`h-full bg-gradient-to-r from-blue-500 to-cyan-400 transition-all duration-500 ${
isIndeterminateDownload ? 'animate-pulse' : ''
@@ -524,7 +524,7 @@ const LocalModelPanel = () => {
/>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-400">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500">
<span>
Progress:{' '}
{isInstalling
@@ -533,42 +533,42 @@ const LocalModelPanel = () => {
? 'Downloading (size unknown)'
: `${Math.round(progress * 100)}%`}
</span>
{downloadedText && <span className="text-stone-300">{downloadedText}</span>}
{speedText && <span className="text-blue-300">{speedText}</span>}
{etaText && <span className="text-cyan-300">ETA {etaText}</span>}
{downloadedText && <span className="text-stone-600">{downloadedText}</span>}
{speedText && <span className="text-primary-600">{speedText}</span>}
{etaText && <span className="text-primary-500">ETA {etaText}</span>}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">Provider</div>
<div className="text-stone-100 mt-1">{status?.provider ?? 'n/a'}</div>
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">Provider</div>
<div className="text-stone-800 mt-1">{status?.provider ?? 'n/a'}</div>
</div>
<div className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">Model</div>
<div className="text-stone-100 mt-1">{status?.model_id ?? 'n/a'}</div>
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">Model</div>
<div className="text-stone-800 mt-1">{status?.model_id ?? 'n/a'}</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
<div className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">Backend</div>
<div className="text-stone-100 mt-1">{status?.active_backend ?? 'cpu'}</div>
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">Backend</div>
<div className="text-stone-800 mt-1">{status?.active_backend ?? 'cpu'}</div>
</div>
<div className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">
Last Latency
</div>
<div className="text-stone-100 mt-1">
<div className="text-stone-800 mt-1">
{typeof status?.last_latency_ms === 'number'
? `${status.last_latency_ms} ms`
: 'n/a'}
</div>
</div>
<div className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">
Generation TPS
</div>
<div className="text-stone-100 mt-1">
<div className="text-stone-800 mt-1">
{typeof status?.gen_toks_per_sec === 'number'
? `${status.gen_toks_per_sec.toFixed(1)} tok/s`
: 'n/a'}
@@ -577,36 +577,36 @@ const LocalModelPanel = () => {
</div>
{status?.model_path && (
<div className="text-xs text-stone-400 break-all">
<div className="text-xs text-stone-500 break-all">
Artifact: {status.model_path}
</div>
)}
{status?.backend_reason && (
<div className="text-xs text-blue-300">{status.backend_reason}</div>
<div className="text-xs text-primary-600">{status.backend_reason}</div>
)}
{status?.warning && <div className="text-xs text-amber-300">{status.warning}</div>}
{statusError && <div className="text-xs text-red-300">{statusError}</div>}
{status?.warning && <div className="text-xs text-amber-700">{status.warning}</div>}
{statusError && <div className="text-xs text-red-600">{statusError}</div>}
{isInstallError && status?.error_detail && (
<div className="space-y-1">
<button
onClick={() => setShowErrorDetail(v => !v)}
className="text-xs text-red-400 hover:text-red-300 underline">
className="text-xs text-red-600 hover:text-red-500 underline">
{showErrorDetail ? 'Hide error details' : 'Show error details'}
</button>
{showErrorDetail && (
<pre className="max-h-40 overflow-auto rounded bg-black/60 p-2 text-[10px] text-red-300 leading-tight whitespace-pre-wrap break-words">
<pre className="max-h-40 overflow-auto rounded bg-red-50 border border-red-200 p-2 text-[10px] text-red-600 leading-tight whitespace-pre-wrap break-words">
{status.error_detail}
</pre>
)}
<p className="text-xs text-stone-400">
<p className="text-xs text-stone-500">
Install Ollama manually from{' '}
<a
href="https://ollama.com"
target="_blank"
rel="noopener noreferrer"
className="text-cyan-300 hover:text-cyan-200 underline">
className="text-primary-500 hover:text-primary-600 underline">
ollama.com
</a>{' '}
then set its path below.
@@ -615,7 +615,7 @@ const LocalModelPanel = () => {
)}
<div className="space-y-1">
<div className="text-stone-400 text-xs uppercase tracking-wide">
<div className="text-stone-500 text-xs uppercase tracking-wide">
Ollama Binary Path (optional)
</div>
<div className="flex items-center gap-2">
@@ -624,7 +624,7 @@ const LocalModelPanel = () => {
value={ollamaPathInput}
onChange={e => setOllamaPathInput(e.target.value)}
placeholder="/usr/local/bin/ollama"
className="flex-1 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs text-stone-100 placeholder:text-stone-500 focus:border-blue-500 focus:outline-none"
className="flex-1 rounded-md border border-stone-200 bg-white px-2 py-1.5 text-xs text-stone-900 placeholder:text-stone-400 focus:border-primary-500 focus:outline-none"
/>
<button
onClick={async () => {
@@ -642,7 +642,7 @@ const LocalModelPanel = () => {
}
}}
disabled={isSettingPath}
className="px-2 py-1.5 text-xs rounded-md bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white whitespace-nowrap">
className="px-2 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white whitespace-nowrap">
{isSettingPath ? 'Setting...' : 'Set Path'}
</button>
{ollamaPathInput && (
@@ -662,7 +662,7 @@ const LocalModelPanel = () => {
}
}}
disabled={isSettingPath}
className="px-2 py-1.5 text-xs rounded-md border border-gray-600 hover:border-gray-500 disabled:opacity-60 text-stone-300 whitespace-nowrap">
className="px-2 py-1.5 text-xs rounded-md border border-stone-200 hover:border-stone-300 disabled:opacity-60 text-stone-600 whitespace-nowrap">
Clear
</button>
)}
@@ -673,13 +673,13 @@ const LocalModelPanel = () => {
<button
onClick={() => void triggerDownload(false)}
disabled={isTriggeringDownload}
className="px-3 py-1.5 text-xs rounded-md bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white">
className="px-3 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white">
{isTriggeringDownload ? 'Triggering...' : 'Bootstrap / Resume'}
</button>
<button
onClick={() => void triggerDownload(true)}
disabled={isTriggeringDownload}
className="px-3 py-1.5 text-xs rounded-md border border-gray-600 hover:border-gray-500 disabled:opacity-60 text-stone-200">
className="px-3 py-1.5 text-xs rounded-md border border-stone-200 hover:border-stone-300 disabled:opacity-60 text-stone-600">
Force Re-bootstrap
</button>
</div>
@@ -688,7 +688,7 @@ const LocalModelPanel = () => {
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Ollama Diagnostics</h3>
<h3 className="text-lg font-semibold text-stone-900">Ollama Diagnostics</h3>
<button
onClick={async () => {
setIsDiagnosticsLoading(true);
@@ -705,25 +705,25 @@ const LocalModelPanel = () => {
}
}}
disabled={isDiagnosticsLoading}
className="px-3 py-1.5 text-xs rounded-md bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white">
className="px-3 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white">
{isDiagnosticsLoading ? 'Checking...' : 'Run Diagnostics'}
</button>
</div>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
{!diagnostics && !diagnosticsError && (
<p className="text-xs text-stone-400">
<p className="text-xs text-stone-500">
Click &ldquo;Run Diagnostics&rdquo; to verify Ollama is running and models are
installed.
</p>
)}
{isDiagnosticsLoading && (
<div className="flex items-center gap-2 text-xs text-blue-300">
<div className="flex items-center gap-2 text-xs text-primary-600">
<div className="h-3 w-3 rounded-full border-2 border-blue-400 border-t-transparent animate-spin" />
Checking Ollama server and models...
</div>
)}
{diagnosticsError && (
<div className="rounded-md bg-red-950/50 border border-red-800/50 p-3 text-xs text-red-300">
<div className="rounded-md bg-red-50 border border-red-300 p-3 text-xs text-red-600">
{diagnosticsError}
</div>
)}
@@ -733,7 +733,7 @@ const LocalModelPanel = () => {
<span
className={`inline-block h-2.5 w-2.5 rounded-full ${diagnostics.ok ? 'bg-green-400' : 'bg-red-400'}`}
/>
<span className={diagnostics.ok ? 'text-green-300' : 'text-red-300'}>
<span className={diagnostics.ok ? 'text-green-600' : 'text-red-600'}>
{diagnostics.ok
? 'All checks passed'
: `${diagnostics.issues.length} issue(s) found`}
@@ -741,16 +741,16 @@ const LocalModelPanel = () => {
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="rounded-md border border-gray-700 p-2">
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-400 uppercase tracking-wide text-[10px]">
Server
</div>
<div
className={`mt-1 font-medium ${diagnostics.ollama_running ? 'text-green-300' : 'text-red-300'}`}>
className={`mt-1 font-medium ${diagnostics.ollama_running ? 'text-green-600' : 'text-red-600'}`}>
{diagnostics.ollama_running ? 'Running' : 'Not running'}
</div>
</div>
<div className="rounded-md border border-gray-700 p-2">
<div className="rounded-md border border-stone-200 p-2">
<div className="text-stone-400 uppercase tracking-wide text-[10px]">
Binary
</div>
@@ -771,8 +771,8 @@ const LocalModelPanel = () => {
{diagnostics.installed_models.map(m => (
<div
key={m.name}
className="flex items-center justify-between rounded border border-gray-700 px-2 py-1.5 text-xs">
<span className="text-stone-100 font-medium">{m.name}</span>
className="flex items-center justify-between rounded border border-stone-200 px-2 py-1.5 text-xs">
<span className="text-stone-800 font-medium">{m.name}</span>
<span className="text-stone-400">
{typeof m.size === 'number' ? formatBytes(m.size) : ''}
</span>
@@ -790,11 +790,11 @@ const LocalModelPanel = () => {
<div className="flex items-center gap-2">
<span
className={
diagnostics.expected.chat_found ? 'text-green-300' : 'text-red-300'
diagnostics.expected.chat_found ? 'text-green-600' : 'text-red-600'
}>
{diagnostics.expected.chat_found ? '\u2713' : '\u2717'}
</span>
<span className="text-stone-200">
<span className="text-stone-700">
Chat: {diagnostics.expected.chat_model}
</span>
</div>
@@ -802,12 +802,12 @@ const LocalModelPanel = () => {
<span
className={
diagnostics.expected.embedding_found
? 'text-green-300'
: 'text-red-300'
? 'text-green-600'
: 'text-red-600'
}>
{diagnostics.expected.embedding_found ? '\u2713' : '\u2717'}
</span>
<span className="text-stone-200">
<span className="text-stone-700">
Embedding: {diagnostics.expected.embedding_model}
</span>
</div>
@@ -815,12 +815,12 @@ const LocalModelPanel = () => {
<span
className={
diagnostics.expected.vision_found
? 'text-green-300'
: 'text-amber-300'
? 'text-green-600'
: 'text-amber-700'
}>
{diagnostics.expected.vision_found ? '\u2713' : '\u2013'}
</span>
<span className="text-stone-200">
<span className="text-stone-700">
Vision: {diagnostics.expected.vision_model}
</span>
</div>
@@ -829,10 +829,10 @@ const LocalModelPanel = () => {
{diagnostics.issues.length > 0 && (
<div>
<div className="text-red-400 uppercase tracking-wide text-[10px] mb-1">
<div className="text-red-600 uppercase tracking-wide text-[10px] mb-1">
Issues
</div>
<ul className="space-y-1 text-xs text-red-300">
<ul className="space-y-1 text-xs text-red-600">
{diagnostics.issues.map((issue, i) => (
<li key={i} className="flex gap-1.5">
<span className="shrink-0">&bull;</span>
@@ -848,9 +848,9 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Capability Assets</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<div className="text-xs text-stone-400">
<h3 className="text-lg font-semibold text-stone-900">Capability Assets</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<div className="text-xs text-stone-500">
Quantization preference: {assets?.quantization ?? 'q4'}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
@@ -861,9 +861,9 @@ const LocalModelPanel = () => {
{ label: 'STT', key: 'stt' as const, item: assets?.stt },
{ label: 'TTS', key: 'tts' as const, item: assets?.tts },
].map(({ label, key, item }) => (
<div key={String(label)} className="rounded-md border border-gray-700 p-2">
<div className="text-stone-400 text-xs uppercase tracking-wide">{label}</div>
<div className="text-stone-100 mt-1 break-all">{item?.id ?? 'n/a'}</div>
<div key={String(label)} className="rounded-md border border-stone-200 p-2">
<div className="text-stone-500 text-xs uppercase tracking-wide">{label}</div>
<div className="text-stone-800 mt-1 break-all">{item?.id ?? 'n/a'}</div>
<div className={`text-xs mt-1 ${statusTone(item?.state ?? 'idle')}`}>
{statusLabel(item?.state ?? 'idle')}
</div>
@@ -873,7 +873,7 @@ const LocalModelPanel = () => {
<button
onClick={() => void triggerAssetDownload(key)}
disabled={assetDownloadBusy[key]}
className="mt-2 px-2 py-1 text-[10px] rounded border border-gray-600 hover:border-gray-500 disabled:opacity-60 text-stone-200">
className="mt-2 px-2 py-1 text-[10px] rounded border border-stone-200 hover:border-stone-300 disabled:opacity-60 text-stone-600">
{assetDownloadBusy[key] ? 'Downloading...' : 'Download'}
</button>
</div>
@@ -883,16 +883,16 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Summarization</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Summarization</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={summaryInput}
onChange={e => setSummaryInput(e.target.value)}
placeholder="Paste text to summarize with the local model..."
className="w-full min-h-28 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-28 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<div className="flex items-center justify-between">
<div className="text-xs text-stone-400">
<div className="text-xs text-stone-500">
Calls `openhuman.local_ai_summarize` via Rust core
</div>
<button
@@ -903,7 +903,7 @@ const LocalModelPanel = () => {
</button>
</div>
{summaryOutput && (
<pre className="whitespace-pre-wrap rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200">
<pre className="whitespace-pre-wrap rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700">
{summaryOutput}
</pre>
)}
@@ -911,22 +911,22 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Suggested Prompts</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Suggested Prompts</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={suggestInput}
onChange={e => setSuggestInput(e.target.value)}
placeholder="Paste conversation context to generate suggestions..."
className="w-full min-h-28 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-28 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<div className="flex items-center justify-between">
<div className="text-xs text-stone-400">
<div className="text-xs text-stone-500">
Calls `openhuman.local_ai_suggest_questions` via Rust core
</div>
<button
onClick={() => void runSuggestTest()}
disabled={isSuggestLoading || !suggestInput.trim()}
className="px-3 py-1.5 text-xs rounded-md bg-cyan-600 hover:bg-cyan-700 disabled:opacity-60 text-white">
className="px-3 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white">
{isSuggestLoading ? 'Running...' : 'Run Suggestion Test'}
</button>
</div>
@@ -936,9 +936,9 @@ const LocalModelPanel = () => {
{suggestions.map(suggestion => (
<div
key={`${suggestion.text}-${suggestion.confidence}`}
className="rounded-md border border-gray-700 bg-stone-950 p-3">
<div className="text-sm text-stone-100">{suggestion.text}</div>
<div className="text-xs text-stone-500 mt-1">
className="rounded-md border border-stone-200 bg-stone-50 p-3">
<div className="text-sm text-stone-800">{suggestion.text}</div>
<div className="text-xs text-stone-400 mt-1">
Confidence: {(suggestion.confidence * 100).toFixed(0)}%
</div>
</div>
@@ -949,13 +949,13 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Custom Prompt</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Custom Prompt</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={promptInput}
onChange={e => setPromptInput(e.target.value)}
placeholder="Type any prompt and run it against the local model..."
className="w-full min-h-28 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-28 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<div className="flex flex-wrap items-center justify-between gap-2">
<label className="flex items-center gap-2 text-xs text-stone-300">
@@ -963,30 +963,30 @@ const LocalModelPanel = () => {
type="checkbox"
checked={promptNoThink}
onChange={e => setPromptNoThink(e.target.checked)}
className="h-3.5 w-3.5 rounded border-gray-600 bg-stone-900 text-blue-500 focus:ring-blue-500"
className="h-3.5 w-3.5 rounded border-stone-300 bg-white text-primary-500 focus:ring-primary-500"
/>
No-think mode
</label>
<button
onClick={() => void runPromptTest()}
disabled={isPromptLoading || !promptInput.trim()}
className="px-3 py-1.5 text-xs rounded-md bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white">
className="px-3 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white">
{isPromptLoading ? 'Running...' : 'Run Prompt Test'}
</button>
</div>
{isPromptLoading && (
<div className="flex items-center gap-2 text-xs text-blue-300">
<div className="flex items-center gap-2 text-xs text-primary-600">
<div className="h-3 w-3 rounded-full border-2 border-blue-400 border-t-transparent animate-spin" />
Running prompt against local model...
</div>
)}
{promptError && (
<div className="rounded-md bg-red-950/50 border border-red-800/50 p-3 text-xs text-red-300">
<div className="rounded-md bg-red-50 border border-red-300 p-3 text-xs text-red-600">
{promptError}
</div>
)}
{promptOutput && (
<pre className="whitespace-pre-wrap rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200 max-h-64 overflow-auto">
<pre className="whitespace-pre-wrap rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700 max-h-64 overflow-auto">
{promptOutput}
</pre>
)}
@@ -994,19 +994,19 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Vision Prompt</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Vision Prompt</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={visionPromptInput}
onChange={e => setVisionPromptInput(e.target.value)}
placeholder="Enter a prompt for the vision model..."
className="w-full min-h-20 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-20 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<textarea
value={visionImageInput}
onChange={e => setVisionImageInput(e.target.value)}
placeholder="One image reference per line (data URI, URL, or local path marker)"
className="w-full min-h-20 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-20 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<button
onClick={() => void runVisionTest()}
@@ -1017,7 +1017,7 @@ const LocalModelPanel = () => {
{isVisionLoading ? 'Running...' : 'Run Vision Test'}
</button>
{visionOutput && (
<pre className="whitespace-pre-wrap rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200">
<pre className="whitespace-pre-wrap rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700">
{visionOutput}
</pre>
)}
@@ -1025,13 +1025,13 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Embeddings</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Embeddings</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={embeddingInput}
onChange={e => setEmbeddingInput(e.target.value)}
placeholder="One input string per line..."
className="w-full min-h-20 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-20 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<button
onClick={() => void runEmbeddingTest()}
@@ -1040,7 +1040,7 @@ const LocalModelPanel = () => {
{isEmbeddingLoading ? 'Running...' : 'Run Embedding Test'}
</button>
{embeddingOutput && (
<div className="rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200 space-y-1">
<div className="rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700 space-y-1">
<div>Model: {embeddingOutput.model_id}</div>
<div>Dimensions: {embeddingOutput.dimensions}</div>
<div>Vectors: {embeddingOutput.vectors.length}</div>
@@ -1050,13 +1050,13 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Voice Input (STT)</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Voice Input (STT)</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<input
value={audioPathInput}
onChange={e => setAudioPathInput(e.target.value)}
placeholder="Absolute path to audio file"
className="w-full rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<button
onClick={() => void runTranscribeTest()}
@@ -1065,7 +1065,7 @@ const LocalModelPanel = () => {
{isTranscribeLoading ? 'Running...' : 'Run Transcription Test'}
</button>
{transcribeOutput && (
<div className="rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200 space-y-2">
<div className="rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700 space-y-2">
<div>Model: {transcribeOutput.model_id}</div>
<div>
<span className="text-stone-400">Transcript:</span>
@@ -1077,19 +1077,19 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-white">Test Voice Output (TTS)</h3>
<div className="bg-gray-900 rounded-lg border border-gray-700 p-4 space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Voice Output (TTS)</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={ttsInput}
onChange={e => setTtsInput(e.target.value)}
placeholder="Enter text to synthesize..."
className="w-full min-h-20 rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full min-h-20 rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<input
value={ttsOutputPath}
onChange={e => setTtsOutputPath(e.target.value)}
placeholder="Optional output WAV path"
className="w-full rounded-md bg-stone-950 border border-gray-700 px-3 py-2 text-sm text-stone-100 placeholder:text-stone-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full rounded-md bg-white border border-stone-200 px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary-400"
/>
<button
onClick={() => void runTtsTest()}
@@ -1098,7 +1098,7 @@ const LocalModelPanel = () => {
{isTtsLoading ? 'Running...' : 'Run TTS Test'}
</button>
{ttsOutput && (
<div className="rounded-md bg-stone-950 border border-gray-700 p-3 text-xs text-stone-200 space-y-1">
<div className="rounded-md bg-stone-50 border border-stone-200 p-3 text-xs text-stone-700 space-y-1">
<div>Voice: {ttsOutput.voice_id}</div>
<div className="break-all">Output: {ttsOutput.output_path}</div>
</div>
@@ -23,16 +23,13 @@ import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
const STATUS_STYLES: Record<ChannelConnectionStatus, { label: string; className: string }> = {
connected: { label: 'Connected', className: 'bg-sage-500/20 text-sage-300 border-sage-500/30' },
connecting: {
label: 'Connecting',
className: 'bg-amber-500/20 text-amber-300 border-amber-500/30',
},
connected: { label: 'Connected', className: 'bg-sage-50 text-sage-700 border-sage-200' },
connecting: { label: 'Connecting', className: 'bg-amber-50 text-amber-700 border-amber-200' },
disconnected: {
label: 'Disconnected',
className: 'bg-stone-500/20 text-stone-300 border-stone-500/30',
className: 'bg-stone-100 text-stone-600 border-stone-200',
},
error: { label: 'Error', className: 'bg-coral-500/20 text-coral-300 border-coral-500/30' },
error: { label: 'Error', className: 'bg-coral-50 text-coral-600 border-coral-200' },
};
const AUTH_MODE_LABELS: Record<string, string> = {
@@ -334,8 +331,8 @@ const MessagingPanel = () => {
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Default channel selector */}
<section className="rounded-xl border border-stone-800/60 bg-black/40 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Default Messaging Channel</h3>
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Default Messaging Channel</h3>
<div className="grid grid-cols-2 gap-2">
{definitions.map(def => {
const channelId = def.id as ChannelType;
@@ -349,8 +346,8 @@ const MessagingPanel = () => {
disabled={busyKeys[busyKey]}
className={`rounded-lg border px-3 py-2 text-sm transition-colors ${
selected
? 'border-primary-500/60 bg-primary-500/20 text-primary-200'
: 'border-stone-700 bg-stone-900/30 text-stone-300 hover:border-stone-500'
? 'border-primary-500/60 bg-primary-50 text-primary-600'
: 'border-stone-200 bg-stone-50 text-stone-600 hover:border-stone-300'
}`}>
{def.display_name}
</button>
@@ -358,7 +355,7 @@ const MessagingPanel = () => {
})}
</div>
<p className="text-xs text-stone-400">
Active route: <span className="text-primary-300">{recommendedRoute}</span>
Active route: <span className="text-primary-600">{recommendedRoute}</span>
</p>
</section>
@@ -369,7 +366,7 @@ const MessagingPanel = () => {
)}
{loading && (
<div className="rounded-xl border border-stone-800/60 bg-black/40 p-4 text-sm text-stone-400">
<div className="rounded-xl border border-stone-200 bg-white p-4 text-sm text-stone-400">
Loading channel definitions...
</div>
)}
@@ -379,18 +376,16 @@ const MessagingPanel = () => {
definitions.map(def => {
const channelId = def.id as ChannelType;
return (
<section
key={channelId}
className="rounded-xl border border-stone-800/60 bg-black/40 p-4">
<section key={channelId} className="rounded-xl border border-stone-200 bg-white p-4">
<div className="mb-4">
<h3 className="text-base font-semibold text-white">{def.display_name}</h3>
<h3 className="text-base font-semibold text-stone-900">{def.display_name}</h3>
<p className="text-xs text-stone-400">{def.description}</p>
{def.capabilities.length > 0 && (
<div className="flex gap-1.5 mt-2">
{def.capabilities.map(cap => (
<span
key={cap}
className="px-1.5 py-0.5 text-[10px] rounded bg-stone-800 text-stone-400 border border-stone-700">
className="px-1.5 py-0.5 text-[10px] rounded bg-stone-100 text-stone-500 border border-stone-200">
{cap.replace(/_/g, ' ')}
</span>
))}
@@ -408,13 +403,13 @@ const MessagingPanel = () => {
return (
<div
key={spec.mode}
className="rounded-lg border border-stone-800 bg-stone-900/20 p-3">
className="rounded-lg border border-stone-200 bg-stone-50 p-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-white">
<p className="text-sm font-medium text-stone-900">
{AUTH_MODE_LABELS[spec.mode] ?? spec.mode}
</p>
<p className="text-xs text-stone-400 mt-1">{spec.description}</p>
<p className="text-xs text-stone-500 mt-1">{spec.description}</p>
{connection?.lastError && (
<p className="text-xs text-coral-300 mt-1">{connection.lastError}</p>
)}
@@ -435,7 +430,7 @@ const MessagingPanel = () => {
value={fieldValues[compositeKey]?.[field.key] ?? ''}
onChange={e => updateField(compositeKey, field.key, e.target.value)}
placeholder={field.placeholder || field.label}
className="w-full rounded-lg border border-stone-700 bg-stone-900 px-3 py-2 text-sm text-white placeholder:text-stone-500 focus:outline-none focus:border-primary-500/60"
className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-primary-500/60"
/>
))}
</div>
@@ -453,7 +448,7 @@ const MessagingPanel = () => {
type="button"
disabled={busyKeys[compositeKey] || status === 'disconnected'}
onClick={() => handleDisconnect(channelId, spec.mode)}
className="rounded-lg border border-stone-700 px-3 py-1.5 text-xs font-medium text-stone-300 hover:border-stone-500 disabled:opacity-50">
className="rounded-lg border border-stone-200 px-3 py-1.5 text-xs font-medium text-stone-600 hover:border-stone-300 disabled:opacity-50">
Disconnect
</button>
</div>
@@ -40,11 +40,11 @@ const PrivacyPanel = () => {
<h3 className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-3 px-1">
Anonymized Analytics
</h3>
<div className="bg-stone-800/50 rounded-xl border border-stone-700/50 overflow-hidden">
<div className="bg-white rounded-xl border border-stone-200 overflow-hidden">
<div className="flex items-center justify-between p-4">
<div className="flex-1 mr-4">
<p className="text-sm font-medium text-white">Share Anonymized Usage Data</p>
<p className="text-xs text-stone-400 mt-1 leading-relaxed">
<p className="text-sm font-medium text-stone-900">Share Anonymized Usage Data</p>
<p className="text-xs text-stone-500 mt-1 leading-relaxed">
Help improve OpenHuman by sharing anonymous crash reports and usage analytics.
All data is fully anonymized &mdash; no personal data, messages, wallet keys, or
session information is ever collected.
@@ -68,7 +68,7 @@ const PrivacyPanel = () => {
</div>
{/* Info Box */}
<div className="p-4 bg-stone-800/30 rounded-xl border border-stone-700/30">
<div className="p-4 bg-stone-50 rounded-xl border border-stone-200">
<div className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-stone-400 mt-0.5 flex-shrink-0"
@@ -81,7 +81,7 @@ const PrivacyPanel = () => {
/>
</svg>
<div>
<p className="text-xs text-stone-400 leading-relaxed">
<p className="text-xs text-stone-500 leading-relaxed">
All analytics and bug reports are fully anonymized. When enabled, we collect only
crash information, device type, and the file location of errors. We never access
your messages, session data, wallet keys, API keys, or any personally identifiable
@@ -11,7 +11,7 @@ const ProfilePanel = () => {
<div className="flex-1 overflow-y-auto">
<div className="p-4 h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-stone-400"
fill="none"
@@ -25,12 +25,12 @@ const ProfilePanel = () => {
/>
</svg>
</div>
<h3 className="text-lg font-medium text-white mb-2">Profile Settings</h3>
<p className="text-stone-400 text-sm max-w-sm mx-auto">
<h3 className="text-lg font-medium text-stone-900 mb-2">Profile Settings</h3>
<p className="text-stone-500 text-sm max-w-sm mx-auto">
Update your profile information, avatar, and personal preferences.
</p>
<div className="mt-6">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-100 text-stone-600 border-stone-200">
Coming Soon
</span>
</div>
@@ -233,12 +233,12 @@ const RecoveryPhrasePanel = () => {
</p>
</div>
<div className="bg-black/20 rounded-2xl p-4 mb-4 border border-stone-700">
<div className="bg-stone-50 rounded-2xl p-4 mb-4 border border-stone-200">
<div className="grid grid-cols-3 gap-2">
{words.map((word, index) => (
<div
key={index}
className="flex items-center gap-2 bg-white/10 rounded-lg px-3 py-2 text-sm">
className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 text-sm border border-stone-200">
<span className="text-stone-500 font-mono text-xs w-5 text-right">
{index + 1}.
</span>
@@ -250,7 +250,7 @@ const RecoveryPhrasePanel = () => {
<button
onClick={handleCopy}
className="w-full flex items-center justify-center gap-2 border border-stone-600 hover:border-stone-500 font-medium py-2.5 text-sm rounded-xl transition-all duration-200 mb-3">
className="w-full flex items-center justify-center gap-2 border border-stone-200 hover:border-stone-300 font-medium py-2.5 text-sm rounded-xl text-stone-700 transition-all duration-200 mb-3">
{copied ? (
<>
<svg
@@ -284,7 +284,7 @@ const RecoveryPhrasePanel = () => {
<button
onClick={() => setMode('import')}
className="w-full text-center text-sm text-primary-400 hover:text-primary-300 transition-colors mb-3">
className="w-full text-center text-sm text-primary-400 hover:text-primary-600 transition-colors mb-3">
I already have a recovery phrase
</button>
@@ -310,7 +310,7 @@ const RecoveryPhrasePanel = () => {
</div>
<div className="flex items-center gap-2 mb-3">
<span className="text-xs text-stone-500">Words:</span>
<span className="text-xs text-stone-400">Words:</span>
{BIP39_IMPORT_LENGTHS.map(len => (
<button
key={len}
@@ -318,15 +318,15 @@ const RecoveryPhrasePanel = () => {
onClick={() => handleWordCountChange(len)}
className={`px-2.5 py-1 text-xs font-medium rounded-lg transition-colors ${
selectedWordCount === len
? 'bg-primary-500/20 border-primary-500/40 text-primary-300 border'
: 'border border-stone-700 text-stone-400 hover:border-stone-500'
? 'bg-primary-500/20 border-primary-500/40 text-primary-600 border'
: 'border border-stone-200 text-stone-500 hover:border-stone-300'
}`}>
{len}
</button>
))}
</div>
<div className="bg-black/20 rounded-2xl p-4 mb-4 border border-stone-700">
<div className="bg-stone-50 rounded-2xl p-4 mb-4 border border-stone-200">
<div className="grid grid-cols-3 gap-2">
{importWords.map((word, index) => (
<div key={index} className="flex items-center gap-1.5">
@@ -344,12 +344,12 @@ const RecoveryPhrasePanel = () => {
onKeyDown={e => handleImportKeyDown(index, e)}
autoComplete="off"
spellCheck={false}
className={`w-full font-mono text-sm font-medium px-2 py-1.5 rounded-lg border bg-white/10 outline-none transition-colors ${
className={`w-full font-mono text-sm font-medium px-2 py-1.5 rounded-lg border bg-white text-stone-900 outline-none transition-colors ${
importValid === false && word.trim()
? 'border-coral-400 focus:border-coral-300'
: importValid === true
? 'border-sage-400 focus:border-sage-300'
: 'border-stone-600 focus:border-primary-400'
: 'border-stone-200 focus:border-primary-400'
}`}
/>
</div>
@@ -373,7 +373,7 @@ const RecoveryPhrasePanel = () => {
<button
onClick={() => setMode('generate')}
className="w-full text-center text-sm text-primary-400 hover:text-primary-300 transition-colors mb-3">
className="w-full text-center text-sm text-primary-400 hover:text-primary-600 transition-colors mb-3">
Generate a new recovery phrase instead
</button>
</>
@@ -31,14 +31,14 @@ const formatRemaining = (remainingMs: number | null): string => {
const PermissionBadge = ({ label, value }: { label: string; value: string }) => {
const colorClass =
value === 'granted'
? 'bg-green-900/40 text-green-300 border-green-700/40'
? 'bg-green-50 text-green-700 border-green-200'
: value === 'denied'
? 'bg-red-900/40 text-red-300 border-red-700/40'
: 'bg-stone-800/60 text-stone-300 border-stone-700';
? 'bg-red-50 text-red-700 border-red-200'
: 'bg-stone-100 text-stone-600 border-stone-200';
return (
<div className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 p-3">
<span className="text-sm text-stone-200">{label}</span>
<div className="flex items-center justify-between rounded-xl border border-stone-200 bg-white p-3">
<span className="text-sm text-stone-700">{label}</span>
<span className={`rounded-md border px-2 py-1 text-xs uppercase tracking-wide ${colorClass}`}>
{value}
</span>
@@ -165,8 +165,8 @@ const ScreenIntelligencePanel = () => {
<SettingsHeader title="Screen Intelligence" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Permissions</h3>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
<PermissionBadge
label="Screen Recording"
value={status?.permissions.screen_recording ?? 'unknown'}
@@ -181,7 +181,7 @@ const ScreenIntelligencePanel = () => {
/>
{anyPermissionDenied && (
<div className="rounded-xl border border-amber-700/40 bg-amber-900/20 p-3 text-sm text-amber-200 space-y-1">
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-700 space-y-1">
<p>
After granting in System Settings, click &ldquo;Restart &amp; Refresh
Permissions&rdquo; so a new core process picks up the grants.
@@ -189,7 +189,7 @@ const ScreenIntelligencePanel = () => {
{status?.permission_check_process_path ? (
<p className="opacity-75 text-xs">
macOS applies privacy to this executable:{' '}
<span className="font-mono break-all text-stone-300">
<span className="font-mono break-all text-stone-600">
{status.permission_check_process_path}
</span>
</p>
@@ -201,21 +201,21 @@ const ScreenIntelligencePanel = () => {
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('screen_recording'))}
disabled={isRequestingPermissions || isRestartingCore}
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="mt-1 rounded-lg border border-primary-400 bg-primary-50 px-3 py-2 text-sm text-primary-700 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Screen Recording'}
</button>
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('accessibility'))}
disabled={isRequestingPermissions || isRestartingCore}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-400 bg-primary-50 px-3 py-2 text-sm text-primary-700 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Accessibility'}
</button>
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('input_monitoring'))}
disabled={isRequestingPermissions || isRestartingCore}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-400 bg-primary-50 px-3 py-2 text-sm text-primary-700 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Open Input Monitoring'}
</button>
{anyPermissionDenied ? (
@@ -223,7 +223,7 @@ const ScreenIntelligencePanel = () => {
type="button"
onClick={() => void dispatch(refreshPermissionsWithRestart())}
disabled={isRestartingCore || isLoading}
className="rounded-lg border border-amber-500/60 bg-amber-500/20 px-3 py-2 text-sm text-amber-200 disabled:opacity-50">
className="rounded-lg border border-amber-400 bg-amber-50 px-3 py-2 text-sm text-amber-700 disabled:opacity-50">
{isRestartingCore ? 'Restarting core…' : 'Restart & Refresh Permissions'}
</button>
) : (
@@ -231,17 +231,17 @@ const ScreenIntelligencePanel = () => {
type="button"
onClick={() => void dispatch(fetchAccessibilityStatus())}
disabled={isLoading || isRestartingCore}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-2 text-sm text-stone-200 disabled:opacity-50">
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-2 text-sm text-stone-700 disabled:opacity-50">
{isLoading ? 'Refreshing…' : 'Refresh Status'}
</button>
)}
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Screen Intelligence Policy</h3>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Screen Intelligence Policy</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Enabled</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Enabled</span>
<input
type="checkbox"
checked={enabled}
@@ -249,8 +249,8 @@ const ScreenIntelligencePanel = () => {
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Mode</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Mode</span>
<select
value={policyMode}
onChange={event =>
@@ -260,14 +260,14 @@ const ScreenIntelligencePanel = () => {
: 'all_except_blacklist'
)
}
className="rounded border border-stone-600 bg-stone-800 px-2 py-1 text-xs text-stone-200">
className="rounded border border-stone-200 bg-white px-2 py-1 text-xs text-stone-700">
<option value="all_except_blacklist">All Except Blacklist</option>
<option value="whitelist_only">Whitelist Only</option>
</select>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Baseline FPS</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Baseline FPS</span>
<input
type="number"
min={0.2}
@@ -275,27 +275,27 @@ const ScreenIntelligencePanel = () => {
step={0.1}
value={baselineFps}
onChange={event => setBaselineFps(event.target.value)}
className="w-24 rounded border border-stone-600 bg-stone-800 px-2 py-1 text-xs text-stone-200"
className="w-24 rounded border border-stone-200 bg-white px-2 py-1 text-xs text-stone-700"
/>
</label>
<div className="space-y-1">
<div className="text-xs text-stone-300">Allowlist (one rule per line)</div>
<div className="text-xs text-stone-600">Allowlist (one rule per line)</div>
<textarea
value={allowlistText}
onChange={event => setAllowlistText(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
<div className="space-y-1">
<div className="text-xs text-stone-300">Denylist (one rule per line)</div>
<div className="text-xs text-stone-600">Denylist (one rule per line)</div>
<textarea
value={denylistText}
onChange={event => setDenylistText(event.target.value)}
rows={3}
className="w-full rounded border border-stone-700 bg-stone-900/50 p-2 text-xs text-stone-200"
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
</div>
@@ -303,13 +303,13 @@ const ScreenIntelligencePanel = () => {
type="button"
onClick={() => void saveConfig()}
disabled={isSavingConfig}
className="rounded-lg border border-primary-500/60 bg-primary-500/20 px-3 py-2 text-sm text-primary-200 disabled:opacity-50">
className="rounded-lg border border-primary-400 bg-primary-50 px-3 py-2 text-sm text-primary-700 disabled:opacity-50">
{isSavingConfig ? 'Saving…' : 'Save Screen Intelligence Settings'}
</button>
{configError && <div className="text-xs text-red-300">{configError}</div>}
{configError && <div className="text-xs text-red-600">{configError}</div>}
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Screen Monitoring</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Screen Monitoring</span>
<input
type="checkbox"
checked={screenMonitoring}
@@ -322,8 +322,8 @@ const ScreenIntelligencePanel = () => {
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Device Control</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Device Control</span>
<input
type="checkbox"
checked={deviceControl}
@@ -336,8 +336,8 @@ const ScreenIntelligencePanel = () => {
/>
</label>
<label className="flex items-center justify-between rounded-xl border border-stone-700 bg-stone-900/50 px-3 py-2">
<span className="text-sm text-stone-200">Predictive Input</span>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<span className="text-sm text-stone-700">Predictive Input</span>
<input
type="checkbox"
checked={predictiveInput}
@@ -351,9 +351,9 @@ const ScreenIntelligencePanel = () => {
</label>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<h3 className="text-sm font-semibold text-white">Session</h3>
<div className="text-sm text-stone-300 space-y-1">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Session</h3>
<div className="text-sm text-stone-600 space-y-1">
<div>Status: {status?.session.active ? 'Active' : 'Stopped'}</div>
<div>Remaining: {remaining}</div>
<div>Frames (ephemeral): {status?.session.frames_in_memory ?? 0}</div>
@@ -383,52 +383,52 @@ const ScreenIntelligencePanel = () => {
)
}
disabled={startDisabled}
className="rounded-lg border border-green-500/60 bg-green-500/20 px-3 py-2 text-sm text-green-200 disabled:opacity-50">
className="rounded-lg border border-green-400 bg-green-50 px-3 py-2 text-sm text-green-700 disabled:opacity-50">
{isStartingSession ? 'Starting…' : 'Start Session'}
</button>
<button
type="button"
onClick={() => void dispatch(stopAccessibilitySession('manual_stop'))}
disabled={stopDisabled}
className="rounded-lg border border-red-500/60 bg-red-500/20 px-3 py-2 text-sm text-red-200 disabled:opacity-50">
className="rounded-lg border border-red-400 bg-red-50 px-3 py-2 text-sm text-red-700 disabled:opacity-50">
{isStoppingSession ? 'Stopping…' : 'Stop Session'}
</button>
<button
type="button"
onClick={() => void dispatch(flushAccessibilityVision())}
disabled={isFlushingVision || !status?.session.active}
className="rounded-lg border border-cyan-500/60 bg-cyan-500/20 px-3 py-2 text-sm text-cyan-200 disabled:opacity-50">
className="rounded-lg border border-primary-400 bg-primary-50 px-3 py-2 text-sm text-primary-700 disabled:opacity-50">
{isFlushingVision ? 'Analyzing…' : 'Analyze Now'}
</button>
</div>
</section>
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">Vision Summaries</h3>
<h3 className="text-sm font-semibold text-stone-900">Vision Summaries</h3>
<button
type="button"
onClick={() => void dispatch(fetchAccessibilityVisionRecent(10))}
disabled={isLoadingVision}
className="rounded-lg border border-stone-600 bg-stone-800/60 px-3 py-1.5 text-xs text-stone-200 disabled:opacity-50">
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-600 disabled:opacity-50">
{isLoadingVision ? 'Refreshing…' : 'Refresh'}
</button>
</div>
{recentVisionSummaries.length === 0 ? (
<div className="text-xs text-stone-400">No summaries yet.</div>
<div className="text-xs text-stone-500">No summaries yet.</div>
) : (
<div className="space-y-2">
{recentVisionSummaries.map(summary => (
<div
key={summary.id}
className="rounded-xl border border-stone-700 bg-stone-900/50 p-3 text-xs text-stone-200">
<div className="text-stone-400">
className="rounded-xl border border-stone-200 bg-white p-3 text-xs text-stone-200">
<div className="text-stone-500">
{new Date(summary.captured_at_ms).toLocaleTimeString()} ·{' '}
{summary.app_name ?? 'Unknown App'}
{summary.window_title ? ` · ${summary.window_title}` : ''}
</div>
<div className="mt-1 text-stone-100">{summary.actionable_notes}</div>
<div className="mt-1 text-stone-800">{summary.actionable_notes}</div>
</div>
))}
</div>
@@ -438,13 +438,13 @@ const ScreenIntelligencePanel = () => {
<DebugSection />
{status !== null && !status.platform_supported && (
<div className="rounded-xl border border-amber-700/40 bg-amber-900/20 p-3 text-sm text-amber-200">
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-700">
Screen Intelligence V1 is currently supported on macOS only.
</div>
)}
{lastError && (
<div className="rounded-xl border border-red-700/40 bg-red-900/20 p-3 text-sm text-red-200">
<div className="rounded-xl border border-red-300 bg-red-50 p-3 text-sm text-red-600">
{lastError}
</div>
)}
@@ -457,11 +457,11 @@ const DebugSection = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<section className="rounded-2xl border border-stone-700 bg-black/30 p-4 space-y-3">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<button
type="button"
onClick={() => setIsOpen(prev => !prev)}
className="flex w-full items-center justify-between text-sm font-semibold text-white">
className="flex w-full items-center justify-between text-sm font-semibold text-stone-900">
<span>Debug & Diagnostics</span>
<span className="text-xs text-stone-400">{isOpen ? 'Collapse' : 'Expand'}</span>
</button>
@@ -72,15 +72,15 @@ const SkillsPanel = () => {
<SettingsHeader title="Skills" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="rounded-xl border border-stone-800/60 bg-black/40 p-4 space-y-3">
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-3">
<div>
<h3 className="text-lg font-semibold text-white">Browser Access</h3>
<h3 className="text-lg font-semibold text-stone-900">Browser Access</h3>
<p className="text-xs text-stone-400">
Allow the browser tool to visit any public domain (private and file URLs are still
blocked).
</p>
</div>
<label className="flex items-center gap-3 text-sm text-stone-300">
<label className="flex items-center gap-3 text-sm text-stone-600">
<input
type="checkbox"
className="checkbox checkbox-primary"
@@ -109,11 +109,11 @@ const SkillsPanel = () => {
</section>
{error && (
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
<div className="rounded-xl border border-stone-800/60 bg-black/40">
<div className="rounded-xl border border-stone-200 bg-white">
{loading && <div className="p-4 text-sm text-stone-400">Loading integrations...</div>}
{!loading && integrations.length === 0 && (
<div className="p-4 text-sm text-stone-400">
@@ -122,8 +122,8 @@ const SkillsPanel = () => {
)}
{!loading &&
Object.entries(groupedIntegrations).map(([category, items]) => (
<div key={category} className="border-b border-stone-800/60 last:border-0">
<div className="px-4 pt-4 pb-2 text-xs uppercase tracking-wide text-stone-500">
<div key={category} className="border-b border-stone-200 last:border-0">
<div className="px-4 pt-4 pb-2 text-xs uppercase tracking-wide text-stone-400">
{CATEGORY_LABELS[category as IntegrationCategory] ?? category}
</div>
<div>
@@ -185,25 +185,25 @@ function IntegrationRow({
}) {
const statusStyle =
integration.status === 'Active'
? 'bg-sage-500/20 text-sage-300 border-sage-500/30'
? 'bg-sage-50 text-sage-700 border-sage-200'
: integration.status === 'Available'
? 'bg-amber-500/15 text-amber-300 border-amber-500/30'
: 'bg-stone-500/20 text-stone-300 border-stone-500/30';
: 'bg-stone-100 text-stone-600 border-stone-200';
return (
<div
className={`flex items-center justify-between gap-4 p-4 ${
isLast ? '' : 'border-b border-stone-800/60'
isLast ? '' : 'border-b border-stone-200'
}`}>
<div className="flex items-center gap-3 text-left flex-1 min-w-0">
<div className="w-6 h-6 flex items-center justify-center text-white/70">
<div className="w-6 h-6 flex items-center justify-center text-stone-500">
<span className="text-xs font-semibold uppercase">{integration.name.slice(0, 2)}</span>
</div>
<div className="min-w-0">
<div className="text-sm font-semibold text-white truncate">{integration.name}</div>
<div className="text-xs text-stone-400 line-clamp-2">{integration.description}</div>
<div className="text-sm font-semibold text-stone-900 truncate">{integration.name}</div>
<div className="text-xs text-stone-500 line-clamp-2">{integration.description}</div>
{integration.setup_hints.length > 0 && (
<div className="mt-1 text-[11px] text-stone-500">{integration.setup_hints[0]}</div>
<div className="mt-1 text-[11px] text-stone-400">{integration.setup_hints[0]}</div>
)}
</div>
</div>
@@ -213,7 +213,7 @@ function IntegrationRow({
className={`px-2 py-1 text-[11px] font-semibold uppercase border rounded-full ${statusStyle}`}>
{integration.status}
</span>
<label className="flex items-center gap-2 text-xs text-stone-300">
<label className="flex items-center gap-2 text-xs text-stone-600">
<input
type="checkbox"
className="checkbox checkbox-primary"
@@ -707,7 +707,7 @@ const TauriCommandsPanel = () => {
label="Runtime Kind"
helpText="JavaScript execution environment for skills. 'native' uses V8 engine for maximum performance and compatibility. 'docker' provides isolation but requires Docker. 'wasm' offers security with some limitations.">
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="native"
value={runtimeKind}
onChange={event => setRuntimeKind(event.target.value)}
@@ -734,14 +734,16 @@ const TauriCommandsPanel = () => {
{skills.length > 0 && (
<div className="space-y-4">
<h5 className="text-sm font-medium text-gray-300">Skills</h5>
<h5 className="text-sm font-medium text-stone-600">Skills</h5>
<div className="grid gap-3 max-h-52 overflow-y-auto">
{skills.map(item => (
<div
key={item.snapshot.skill_id}
className="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 backdrop-blur-sm px-4 py-3">
<div className="flex-1 min-w-0">
<div className="text-sm text-white font-medium">{item.snapshot.name}</div>
<div className="text-sm text-stone-900 font-medium">
{item.snapshot.name}
</div>
<div className="text-xs text-gray-400 truncate">
{item.snapshot.skill_id}
</div>
@@ -762,11 +764,11 @@ const TauriCommandsPanel = () => {
<div className="md:col-span-2">
<div className="space-y-4">
{/* Live Status Display */}
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-900/40 border border-stone-800/60">
<div className="flex items-center justify-between p-3 rounded-lg bg-white border border-stone-200">
<div className="flex items-center gap-3">
<DaemonHealthIndicator size="md" />
<div>
<div className="text-white font-medium">
<div className="text-stone-900 font-medium">
Agent Status: {daemonHealth.status}
</div>
<div className="text-xs text-gray-400">
@@ -799,7 +801,7 @@ const TauriCommandsPanel = () => {
{Object.entries(daemonHealth.components).map(([name, health]) => (
<div
key={name}
className="flex items-center gap-2 p-2 rounded bg-stone-800/40">
className="flex items-center gap-2 p-2 rounded bg-stone-50">
<div
className={`w-2 h-2 rounded-full ${
health.status === 'ok'
@@ -809,7 +811,7 @@ const TauriCommandsPanel = () => {
: 'bg-red-500'
}`}
/>
<span className="capitalize text-gray-300">{name}</span>
<span className="capitalize text-stone-600">{name}</span>
{health.restart_count > 0 && (
<span className="text-xs text-yellow-400">
({health.restart_count})
@@ -856,9 +858,9 @@ const TauriCommandsPanel = () => {
</ActionPanel>
{/* Auto-start Toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-200">
<div>
<div className="text-sm font-medium text-gray-300">Auto-start Agent</div>
<div className="text-sm font-medium text-stone-700">Auto-start Agent</div>
<div className="text-xs text-gray-500">
Automatically start agent on app launch
</div>
@@ -875,9 +877,9 @@ const TauriCommandsPanel = () => {
</div>
{/* Tray Toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-200">
<div>
<div className="text-sm font-medium text-gray-300">Show Daemon Tray</div>
<div className="text-sm font-medium text-stone-700">Show Daemon Tray</div>
<div className="text-xs text-gray-500">
Keep OpenHuman Core tray icon visible in daemon host mode
</div>
@@ -939,7 +941,7 @@ const TauriCommandsPanel = () => {
helpText="Convert sensitive data to encrypted format using the system's secure encryption. Useful for safely storing API keys, tokens, or other confidential information in configuration files."
fullWidth>
<textarea
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[90px] resize-y"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[90px] resize-y"
placeholder="Plaintext"
value={encryptInput}
onChange={event => setEncryptInput(event.target.value)}
@@ -950,7 +952,7 @@ const TauriCommandsPanel = () => {
helpText="Convert encrypted data back to readable format. Only works with data encrypted by this system. Use this to verify encrypted values or retrieve original content when needed."
fullWidth>
<textarea
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[90px] resize-y"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[90px] resize-y"
placeholder="Ciphertext"
value={decryptInput}
onChange={event => setDecryptInput(event.target.value)}
@@ -994,7 +996,7 @@ const TauriCommandsPanel = () => {
label="Backend"
helpText="Memory storage system for conversations and agent memory. 'sqlite' for local file storage (default), 'postgres' for scalable database, 'redis' for high-performance caching, 'neo4j' for graph relationships.">
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="sqlite"
value={memoryBackend}
onChange={event => setMemoryBackend(event.target.value)}
@@ -1010,7 +1012,7 @@ const TauriCommandsPanel = () => {
label="Embedding Provider"
helpText="AI service for generating vector embeddings for semantic search and memory retrieval. 'openai' for high quality, 'cohere' for multilingual, 'huggingface' for local models, 'none' to disable.">
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="openai"
value={embeddingProvider}
onChange={event => setEmbeddingProvider(event.target.value)}
@@ -1020,7 +1022,7 @@ const TauriCommandsPanel = () => {
label="Embedding Model"
helpText="Specific model for generating vector embeddings. OpenAI: 'text-embedding-3-small' (fast, cost-effective) or 'text-embedding-3-large' (higher accuracy). Must match your provider.">
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="text-embedding-3-small"
value={embeddingModel}
onChange={event => setEmbeddingModel(event.target.value)}
@@ -1030,7 +1032,7 @@ const TauriCommandsPanel = () => {
label="Embedding Dimensions"
helpText="Vector size for embeddings. Must match your model: text-embedding-3-small supports 512-1536 (default 1536), text-embedding-3-large supports up to 3072. Higher dimensions = better accuracy, more storage.">
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="1536"
value={embeddingDims}
onChange={event => setEmbeddingDims(event.target.value)}
@@ -1087,7 +1089,7 @@ const TauriCommandsPanel = () => {
helpText="Full path to hardware device for introspection. Common paths: /dev/tty.usbmodem* (macOS USB), /dev/ttyUSB* (Linux), COM* (Windows). Use 'Discover Devices' to find available hardware."
fullWidth>
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="Device path (e.g. /dev/tty.usbmodem)"
value={hardwarePath}
onChange={event => setHardwarePath(event.target.value)}
@@ -1102,7 +1104,7 @@ const TauriCommandsPanel = () => {
helpText="Path to existing agent workspace for data migration. Leave empty to migrate from default locations. Supports importing from OpenClaw, AutoGen, and other agent frameworks. Run dry-run first to preview changes."
fullWidth>
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="Source workspace (optional)"
value={migrationSource}
onChange={event => setMigrationSource(event.target.value)}
@@ -1162,12 +1164,12 @@ const TauriCommandsPanel = () => {
loading={operationLoading === 'sendChat'}>
{/* Agent Chat - Preserve original styling */}
<div className="space-y-6">
<h4 className="text-lg font-medium text-white">Agent Chat</h4>
<h4 className="text-lg font-medium text-stone-900">Agent Chat</h4>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2 text-sm text-gray-300">
<label className="space-y-2 text-sm text-stone-600">
Model Override
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="gpt-4.1-mini"
value={chatModel}
onChange={event => setChatModel(event.target.value)}
@@ -1177,10 +1179,10 @@ const TauriCommandsPanel = () => {
claude-3-sonnet. Leave empty for system default.
</p>
</label>
<label className="space-y-2 text-sm text-gray-300">
<label className="space-y-2 text-sm text-stone-600">
Temperature
<input
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
placeholder="0.7"
value={chatTemperature}
onChange={event => setChatTemperature(event.target.value)}
@@ -1193,7 +1195,7 @@ const TauriCommandsPanel = () => {
</div>
<div className="space-y-3">
<textarea
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[120px] resize-y"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[120px] resize-y"
placeholder="Send a message to the agent..."
value={chatInput}
onChange={event => setChatInput(event.target.value)}
@@ -1204,7 +1206,7 @@ const TauriCommandsPanel = () => {
</p>
</div>
<button
className="bg-primary-600 hover:bg-primary-500 active:bg-primary-700 text-white font-medium px-6 py-3 rounded-lg transition-all duration-200 ease-in-out shadow-soft hover:shadow-medium focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-black disabled:opacity-50 disabled:cursor-not-allowed"
className="bg-primary-600 hover:bg-primary-500 active:bg-primary-700 text-white font-medium px-6 py-3 rounded-lg transition-all duration-200 ease-in-out shadow-soft hover:shadow-medium focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed"
onClick={sendChat}>
Send Message
</button>
@@ -1214,7 +1216,7 @@ const TauriCommandsPanel = () => {
<div
key={`${entry.role}-${index}`}
className={`text-sm ${
entry.role === 'user' ? 'text-white' : 'text-emerald-200'
entry.role === 'user' ? 'text-stone-900' : 'text-emerald-700'
}`}>
<span className="font-semibold uppercase text-[10px] tracking-wide">
{entry.role}
@@ -1229,7 +1231,7 @@ const TauriCommandsPanel = () => {
{/* Output Console */}
<div className="space-y-6">
<div>
<h4 className="text-lg font-medium text-white flex items-center gap-2">
<h4 className="text-lg font-medium text-stone-900 flex items-center gap-2">
<DocumentTextIcon className="h-5 w-5" />
Output Console
</h4>
@@ -1245,7 +1247,7 @@ const TauriCommandsPanel = () => {
)}
<div className="space-y-3">
<textarea
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[240px] font-mono text-xs resize-y"
className="w-full px-4 py-3 rounded-lg bg-white border border-stone-200 text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200 min-h-[240px] font-mono text-xs resize-y"
value={output}
readOnly
placeholder="Command output will appear here..."
@@ -204,8 +204,8 @@ const TeamInvitesPanel = () => {
key={invite._id}
className={`rounded-xl border p-3 ${
isInactive
? 'border-stone-700/30 bg-stone-800/20 opacity-60'
: 'border-stone-700/50 bg-stone-800/40'
? 'border-stone-200 bg-stone-50 opacity-60'
: 'border-stone-200 bg-white'
}`}>
<div className="flex items-center justify-between mb-2">
{/* Code with status label */}
@@ -213,8 +213,8 @@ const TeamInvitesPanel = () => {
<code
className={`text-sm font-mono px-2 py-1 rounded-lg ${
isInactive
? 'text-stone-500 bg-stone-900/30'
: 'text-white bg-stone-900/60'
? 'text-stone-500 bg-stone-100'
: 'text-stone-900 bg-stone-200'
}`}>
{invite.code}
</code>
@@ -236,7 +236,7 @@ const TeamInvitesPanel = () => {
disabled={status !== 'active'}
className={`p-1.5 rounded-lg transition-colors ${
status === 'active'
? 'text-stone-400 hover:text-white hover:bg-stone-700/50'
? 'text-stone-500 hover:text-stone-900 hover:bg-stone-100'
: 'text-stone-600 cursor-not-allowed'
}`}
aria-label="Copy invite code">
@@ -329,9 +329,9 @@ const TeamInvitesPanel = () => {
{/* Revoke Invite Confirmation Modal */}
{inviteToRevoke && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Revoke Invite Code</h3>
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Revoke Invite Code</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -343,7 +343,7 @@ const TeamInvitesPanel = () => {
<div className="text-sm text-stone-400">
<p>
Are you sure you want to revoke the invite code{' '}
<code className="text-white bg-stone-800/60 px-1.5 py-0.5 rounded font-mono text-xs">
<code className="text-stone-900 bg-stone-100 px-1.5 py-0.5 rounded font-mono text-xs">
{inviteToRevoke.code}
</code>
?
@@ -357,7 +357,7 @@ const TeamInvitesPanel = () => {
<button
onClick={() => setInviteToRevoke(null)}
disabled={revokingId === inviteToRevoke.id}
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-700 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -111,15 +111,15 @@ const TeamManagementPanel = () => {
<div className="flex-1 overflow-y-auto">
<div className="max-w-md mx-auto p-4 space-y-4">
{/* Team Info */}
<div className="rounded-xl border border-stone-700/50 bg-stone-800/40 p-4">
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-stone-700/60 flex items-center justify-center">
<span className="text-sm font-semibold text-stone-300">
<div className="w-10 h-10 rounded-lg bg-stone-200 flex items-center justify-center">
<span className="text-sm font-semibold text-stone-700">
{team.name.charAt(0).toUpperCase()}
</span>
</div>
<div>
<h3 className="text-lg font-semibold text-white">{team.name}</h3>
<h3 className="text-lg font-semibold text-stone-900">{team.name}</h3>
<p className="text-xs text-stone-500">
{team.subscription.plan} Plan Created{' '}
{new Date(team.createdAt).toLocaleDateString()}
@@ -137,10 +137,10 @@ const TeamManagementPanel = () => {
{/* Members */}
<button
onClick={() => navigateToSettings(`team/manage/${teamId}/members`)}
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-700/50 bg-stone-800/40 hover:bg-stone-800/60 transition-all text-left">
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-200 bg-stone-50 hover:bg-stone-100 transition-all text-left">
<div className="flex items-center gap-3">
<svg
className="w-5 h-5 text-primary-400"
className="w-5 h-5 text-primary-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -152,7 +152,7 @@ const TeamManagementPanel = () => {
/>
</svg>
<div>
<div className="font-medium text-sm text-white">Members</div>
<div className="font-medium text-sm text-stone-900">Members</div>
<p className="text-xs text-stone-500">Manage team members and roles</p>
</div>
</div>
@@ -173,10 +173,10 @@ const TeamManagementPanel = () => {
{/* Invites */}
<button
onClick={() => navigateToSettings(`team/manage/${teamId}/invites`)}
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-700/50 bg-stone-800/40 hover:bg-stone-800/60 transition-all text-left">
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-200 bg-stone-50 hover:bg-stone-100 transition-all text-left">
<div className="flex items-center gap-3">
<svg
className="w-5 h-5 text-primary-400"
className="w-5 h-5 text-primary-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -188,7 +188,7 @@ const TeamManagementPanel = () => {
/>
</svg>
<div>
<div className="font-medium text-sm text-white">Invites</div>
<div className="font-medium text-sm text-stone-900">Invites</div>
<p className="text-xs text-stone-500">Generate and manage invite codes</p>
</div>
</div>
@@ -209,10 +209,10 @@ const TeamManagementPanel = () => {
{/* Edit Team Settings */}
<button
onClick={handleEditTeam}
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-700/50 bg-stone-800/40 hover:bg-stone-800/60 transition-all text-left">
className="w-full flex items-center justify-between p-3 rounded-xl border border-stone-200 bg-stone-50 hover:bg-stone-100 transition-all text-left">
<div className="flex items-center gap-3">
<svg
className="w-5 h-5 text-primary-400"
className="w-5 h-5 text-primary-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -224,7 +224,7 @@ const TeamManagementPanel = () => {
/>
</svg>
<div>
<div className="font-medium text-sm text-white">Team Settings</div>
<div className="font-medium text-sm text-stone-900">Team Settings</div>
<p className="text-xs text-stone-500">Edit team name and settings</p>
</div>
</div>
@@ -283,19 +283,19 @@ const TeamManagementPanel = () => {
{/* Edit Team Modal */}
{isEditModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Edit Team Settings</h3>
<div className="fixed inset-0 bg-stone-900/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Edit Team Settings</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
<p className="text-xs text-coral-400">{error}</p>
<p className="text-xs text-coral-600">{error}</p>
</div>
)}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-stone-300 mb-2">
<label className="block text-sm font-medium text-stone-700 mb-2">
Team Name
</label>
<input
@@ -303,7 +303,7 @@ const TeamManagementPanel = () => {
value={editTeamName}
onChange={e => setEditTeamName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleUpdateTeam()}
className="w-full px-3 py-2 text-sm bg-stone-800/60 border border-stone-700/50 rounded-xl text-white placeholder-stone-500 focus:outline-none focus:border-primary-500/50"
className="w-full px-3 py-2 text-sm bg-stone-50 border border-stone-200 rounded-xl text-stone-900 placeholder-stone-400 focus:outline-none focus:border-primary-500/50"
placeholder="Enter team name"
/>
</div>
@@ -312,7 +312,7 @@ const TeamManagementPanel = () => {
<button
onClick={() => setIsEditModalOpen(false)}
disabled={isUpdating}
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-700 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -329,13 +329,13 @@ const TeamManagementPanel = () => {
{/* Delete Team Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Delete Team</h3>
<div className="fixed inset-0 bg-stone-900/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Delete Team</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
<p className="text-xs text-coral-400">{error}</p>
<p className="text-xs text-coral-600">{error}</p>
</div>
)}
@@ -343,7 +343,7 @@ const TeamManagementPanel = () => {
<div className="text-sm text-stone-400">
<p>
Are you sure you want to delete{' '}
<strong className="text-white">{teamEntry?.team.name}</strong>?
<strong className="text-stone-900">{teamEntry?.team.name}</strong>?
</p>
<p className="mt-2 text-coral-400">
This action cannot be undone. All team data will be permanently removed.
@@ -354,7 +354,7 @@ const TeamManagementPanel = () => {
<button
onClick={() => setIsDeleteModalOpen(false)}
disabled={isDeleting}
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-700 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -174,17 +174,17 @@ const TeamMembersPanel = () => {
{members.map(member => (
<div
key={member._id}
className="flex items-center justify-between p-3 rounded-xl border border-stone-700/50 bg-stone-800/40">
className="flex items-center justify-between p-3 rounded-xl border border-stone-200 bg-white">
<div className="flex items-center gap-3 min-w-0">
{/* Avatar */}
<div className="w-8 h-8 rounded-full bg-stone-700/60 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-semibold text-stone-300">
<span className="text-xs font-semibold text-white">
{displayName(member).charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white truncate">
<span className="text-sm font-medium text-stone-900 truncate">
{displayName(member)}
</span>
{isCurrentUser(member) && (
@@ -204,7 +204,7 @@ const TeamMembersPanel = () => {
value={member.role.toUpperCase()}
onChange={e => handleChangeRole(member, e.target.value as TeamRole)}
disabled={changingRoleId === member._id}
className="px-2 py-1 text-[10px] font-medium rounded-full border bg-stone-800 text-stone-300 border-stone-600 focus:outline-none focus:border-primary-500/50 disabled:opacity-50">
className="px-2 py-1 text-[10px] font-medium rounded-full border bg-white text-stone-700 border-stone-300 focus:outline-none focus:border-primary-500/50 disabled:opacity-50">
{ROLES.map(r => (
<option key={r} value={r}>
{r}
@@ -253,9 +253,9 @@ const TeamMembersPanel = () => {
{/* Remove Member Confirmation Modal */}
{memberToRemove && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Remove Team Member</h3>
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Remove Team Member</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -267,8 +267,8 @@ const TeamMembersPanel = () => {
<div className="text-sm text-stone-400">
<p>
Are you sure you want to remove{' '}
<strong className="text-white">{displayName(memberToRemove)}</strong> from the
team?
<strong className="text-stone-900">{displayName(memberToRemove)}</strong> from
the team?
</p>
<p className="mt-2 text-coral-400">
They will lose access to the team and all team resources.
@@ -279,7 +279,7 @@ const TeamMembersPanel = () => {
<button
onClick={() => setMemberToRemove(null)}
disabled={removingId === memberToRemove._id}
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-700 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -296,9 +296,9 @@ const TeamMembersPanel = () => {
{/* Change Role Confirmation Modal */}
{roleChangeConfirmation && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Change Member Role</h3>
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Change Member Role</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -310,7 +310,7 @@ const TeamMembersPanel = () => {
<div className="text-sm text-stone-400">
<p>
Change{' '}
<strong className="text-white">
<strong className="text-stone-900 font-semibold">
{displayName(roleChangeConfirmation.member)}
</strong>
's role from{' '}
@@ -172,19 +172,19 @@ const TeamPanel = () => {
<div
className={`flex items-center justify-between p-3 rounded-xl border transition-all ${
isActive
? 'border-primary-500/40 bg-primary-500/5'
: 'border-stone-700/50 bg-stone-800/40 hover:bg-stone-800/60'
? 'border-primary-200 bg-primary-50'
: 'border-stone-200 bg-white hover:bg-stone-50'
}`}>
<div className="flex items-center gap-3 min-w-0 flex-1">
{/* Team avatar */}
<div className="w-9 h-9 rounded-lg bg-stone-700/60 flex items-center justify-center flex-shrink-0">
<span className="text-sm font-semibold text-stone-300">
<div className="w-9 h-9 rounded-lg bg-stone-100 flex items-center justify-center flex-shrink-0">
<span className="text-sm font-semibold text-stone-600">
{team.name.charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-white truncate">{team.name}</span>
<span className="text-sm font-medium text-stone-900 truncate">{team.name}</span>
{roleBadge(role, team.createdBy)}
{planBadge(team.subscription.plan)}
{isActive && (
@@ -193,7 +193,7 @@ const TeamPanel = () => {
</span>
)}
</div>
{team.isPersonal && <p className="text-xs text-stone-500 mt-0.5">Personal team</p>}
{team.isPersonal && <p className="text-xs text-stone-400 mt-0.5">Personal team</p>}
</div>
</div>
@@ -201,7 +201,7 @@ const TeamPanel = () => {
{canManage && (
<button
onClick={() => navigateToTeamManagement(team._id)}
className="px-2.5 py-1 text-xs font-medium rounded-lg bg-primary-500/20 hover:bg-primary-500/30 text-primary-400 transition-colors">
className="px-2.5 py-1 text-xs font-medium rounded-lg bg-primary-50 hover:bg-primary-100 text-primary-600 transition-colors">
Manage Team
</button>
)}
@@ -209,7 +209,7 @@ const TeamPanel = () => {
<button
onClick={() => handleSwitchTeam(team._id)}
disabled={isSwitching === team._id}
className="px-2.5 py-1 text-xs font-medium rounded-lg bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="px-2.5 py-1 text-xs font-medium rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-600 transition-colors disabled:opacity-50">
{isSwitching === team._id ? 'Switching...' : 'Switch'}
</button>
)}
@@ -217,7 +217,7 @@ const TeamPanel = () => {
<button
onClick={() => handleLeaveTeam(entry)}
disabled={isLeaving === team._id}
className="px-2.5 py-1 text-xs font-medium rounded-lg text-amber-400 hover:bg-amber-500/10 transition-colors disabled:opacity-50">
className="px-2.5 py-1 text-xs font-medium rounded-lg text-amber-700 hover:bg-amber-50 transition-colors disabled:opacity-50">
{isLeaving === team._id ? 'Leaving...' : 'Leave'}
</button>
)}
@@ -275,7 +275,7 @@ const TeamPanel = () => {
)}
{/* Team Actions - Secondary Content */}
<div className="space-y-4 border-t border-stone-700/50 pt-4">
<div className="space-y-4 border-t border-stone-200 pt-4">
{/* Create team */}
<div className="space-y-2">
<h3 className="text-xs font-medium text-stone-500 uppercase tracking-wider px-1">
@@ -288,7 +288,7 @@ const TeamPanel = () => {
onChange={e => setNewTeamName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreateTeam()}
placeholder="Team name"
className="flex-1 px-3 py-2 text-sm bg-stone-800/60 border border-stone-700/50 rounded-xl text-white placeholder-stone-500 focus:outline-none focus:border-primary-500/50"
className="flex-1 px-3 py-2 text-sm bg-white border border-stone-200 rounded-xl text-stone-900 placeholder-stone-400 focus:outline-none focus:border-primary-500/50"
/>
<button
onClick={handleCreateTeam}
@@ -311,12 +311,12 @@ const TeamPanel = () => {
onChange={e => setJoinCode(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleJoinTeam()}
placeholder="Invite code"
className="flex-1 px-3 py-2 text-sm bg-stone-800/60 border border-stone-700/50 rounded-xl text-white placeholder-stone-500 focus:outline-none focus:border-primary-500/50 font-mono"
className="flex-1 px-3 py-2 text-sm bg-white border border-stone-200 rounded-xl text-stone-900 placeholder-stone-400 focus:outline-none focus:border-primary-500/50 font-mono"
/>
<button
onClick={handleJoinTeam}
disabled={isJoining || !joinCode.trim()}
className="px-4 py-2 text-xs font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
className="px-4 py-2 text-xs font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{isJoining ? 'Joining...' : 'Join'}
</button>
</div>
@@ -325,9 +325,9 @@ const TeamPanel = () => {
{/* Leave Team Confirmation Modal */}
{teamToLeave && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-stone-900 rounded-2xl p-6 w-full max-w-md border border-stone-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Leave Team</h3>
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Leave Team</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -336,10 +336,10 @@ const TeamPanel = () => {
)}
<div className="space-y-4">
<div className="text-sm text-stone-400">
<div className="text-sm text-stone-500">
<p>
Are you sure you want to leave{' '}
<strong className="text-white">{teamToLeave.team.name}</strong>?
<strong className="text-stone-900">{teamToLeave.team.name}</strong>?
</p>
<p className="mt-2 text-amber-400">
You will lose access to the team and all team resources. You'll need a new
@@ -351,7 +351,7 @@ const TeamPanel = () => {
<button
onClick={() => setTeamToLeave(null)}
disabled={isLeaving === teamToLeave.team._id}
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-700/50 hover:bg-stone-700 text-stone-300 transition-colors disabled:opacity-50">
className="flex-1 px-4 py-2 text-sm font-medium rounded-xl bg-stone-100 hover:bg-stone-200 text-stone-600 transition-colors disabled:opacity-50">
Cancel
</button>
<button
@@ -21,7 +21,7 @@ const ActionPanel: React.FC<ActionPanelProps> = ({
<div className="flex flex-wrap items-center gap-4">
{children}
{hasChanges && (
<div className="flex items-center gap-2 text-xs text-amber-300">
<div className="flex items-center gap-2 text-xs text-amber-700">
<div className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />
Unsaved changes
</div>
@@ -29,14 +29,14 @@ const ActionPanel: React.FC<ActionPanelProps> = ({
</div>
{success && (
<div className="flex items-center gap-2 rounded-lg border border-sage-500/40 bg-sage-500/10 px-3 py-2 text-sm text-sage-200">
<div className="flex items-center gap-2 rounded-lg border border-sage-500/40 bg-sage-50 px-3 py-2 text-sm text-sage-700">
<CheckIcon className="h-4 w-4" />
{typeof success === 'string' ? success : 'Operation completed successfully'}
</div>
)}
{error && (
<div className="flex items-center gap-2 rounded-lg border border-coral-500/40 bg-coral-500/10 px-3 py-2 text-sm text-coral-200">
<div className="flex items-center gap-2 rounded-lg border border-coral-500/40 bg-coral-50 px-3 py-2 text-sm text-coral-700">
<ExclamationTriangleIcon className="h-4 w-4" />
{error}
</div>
@@ -66,11 +66,11 @@ const PrimaryButton: React.FC<PrimaryButtonProps> = ({
'px-6 py-3 rounded-lg font-medium transition-all duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses = {
primary:
'bg-primary-600 hover:bg-primary-500 active:bg-primary-700 text-white shadow-soft hover:shadow-lg hover:shadow-primary-500/25 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-black',
'bg-primary-600 hover:bg-primary-500 active:bg-primary-700 text-white shadow-soft hover:shadow-lg hover:shadow-primary-500/25 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white',
secondary:
'bg-stone-800 hover:bg-stone-700 active:bg-stone-600 text-white border border-stone-600 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-black',
'bg-stone-100 hover:bg-stone-200 active:bg-stone-300 text-stone-900 border border-stone-200 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white',
outline:
'border border-stone-600 text-white hover:bg-white/10 active:bg-white/20 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-black',
'border border-stone-200 text-stone-900 hover:bg-stone-100 active:bg-stone-200 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white',
};
return (
@@ -80,7 +80,13 @@ const PrimaryButton: React.FC<PrimaryButtonProps> = ({
disabled={disabled || loading}>
<div className="flex items-center gap-2">
{loading && (
<div className="h-4 w-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
<div
className={`h-4 w-4 border-2 rounded-full animate-spin ${
variant === 'primary'
? 'border-white/40 border-t-white'
: 'border-stone-600/40 border-t-stone-600'
}`}
/>
)}
<span>{children}</span>
</div>
@@ -17,12 +17,12 @@ const InputGroup: React.FC<InputGroupProps> = ({
<div className={`space-y-6 ${className}`}>
{title && (
<div className="space-y-2">
<h4 className="text-lg font-medium text-white">{title}</h4>
{description && <p className="text-sm text-gray-400">{description}</p>}
<h4 className="text-lg font-medium text-stone-900">{title}</h4>
{description && <p className="text-sm text-stone-500">{description}</p>}
</div>
)}
<div className="rounded-lg border border-white/10 bg-white/5 backdrop-blur-sm p-6">
<div className="rounded-lg border border-stone-200 bg-stone-50 backdrop-blur-sm p-6">
<div className="grid gap-6 md:grid-cols-2">{children}</div>
</div>
</div>
@@ -46,10 +46,10 @@ const Field: React.FC<FieldProps> = ({
}) => {
return (
<label
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
className={`space-y-3 text-sm text-stone-600 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<div>
<span className="font-medium">{label}</span>
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
{helpText && <p className="text-xs text-stone-500 leading-relaxed mt-1">{helpText}</p>}
</div>
{children}
</label>
@@ -73,16 +73,16 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({
}) => {
return (
<div className={`flex flex-col gap-3 ${className}`}>
<label className="flex items-center gap-3 text-sm text-gray-300 cursor-pointer">
<label className="flex items-center gap-3 text-sm text-stone-600 cursor-pointer">
<input
type="checkbox"
className="w-5 h-5 rounded border-2 border-stone-600 bg-stone-900/40 text-primary-500 focus:ring-2 focus:ring-primary-500/30 focus:border-primary-500/50 transition-all duration-200"
className="w-5 h-5 rounded border-2 border-stone-300 bg-white text-primary-500 focus:ring-2 focus:ring-primary-500/30 focus:border-primary-500/50 transition-all duration-200"
checked={checked}
onChange={e => onChange(e.target.checked)}
/>
<span className="font-medium">{label}</span>
</label>
{helpText && <p className="text-xs text-gray-400 ml-7 leading-relaxed">{helpText}</p>}
{helpText && <p className="text-xs text-stone-500 ml-7 leading-relaxed">{helpText}</p>}
</div>
);
};
@@ -13,16 +13,16 @@ interface SectionCardProps {
}
const priorityStyles = {
critical: 'bg-gradient-to-br from-primary-500/10 to-primary-600/5 border-primary-500/20',
infrastructure: 'bg-gradient-to-br from-slate-500/8 to-slate-600/4 border-slate-500/15',
development: 'bg-gradient-to-br from-amber-500/8 to-amber-600/4 border-amber-500/15',
tools: 'bg-black/30 border-stone-600/30',
critical: 'bg-gradient-to-br from-primary-50 to-white border-primary-200',
infrastructure: 'bg-gradient-to-br from-stone-50 to-white border-stone-200',
development: 'bg-gradient-to-br from-amber-50 to-white border-amber-200',
tools: 'bg-stone-50 border-stone-200',
} as const;
const priorityIconColors = {
critical: 'text-primary-400',
infrastructure: 'text-slate-400',
development: 'text-amber-400',
critical: 'text-primary-500',
infrastructure: 'text-stone-500',
development: 'text-amber-500',
tools: 'text-stone-400',
} as const;
@@ -48,13 +48,13 @@ const SectionCard: React.FC<SectionCardProps> = ({
<div
className={`rounded-xl border backdrop-blur-sm transition-all duration-200 ${priorityStyles[priority]}`}>
<div
className={`flex items-center justify-between p-6 ${collapsible ? 'cursor-pointer hover:bg-white/5' : ''}`}
className={`flex items-center justify-between p-6 ${collapsible ? 'cursor-pointer hover:bg-stone-100' : ''}`}
onClick={handleToggle}>
<div className="flex items-center gap-3">
<div
className={`flex-shrink-0 ${priorityIconColors[priority]} ${loading ? 'relative' : ''}`}>
{loading ? (
<div className="h-5 w-5 border-2 border-white/20 border-t-white rounded-full animate-spin" />
<div className="h-5 w-5 border-2 border-stone-200 border-t-stone-600 rounded-full animate-spin" />
) : (
React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
className: 'h-5 w-5',
@@ -62,13 +62,13 @@ const SectionCard: React.FC<SectionCardProps> = ({
)}
</div>
<div className="flex items-center gap-2">
<h3 className="text-xl font-semibold text-white font-display">{title}</h3>
<h3 className="text-xl font-semibold text-stone-900 font-display">{title}</h3>
{hasChanges && <div className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />}
{loading && <span className="text-sm text-gray-400 ml-2">Loading...</span>}
{loading && <span className="text-sm text-stone-500 ml-2">Loading...</span>}
</div>
</div>
{collapsible && (
<div className="text-gray-400 transition-transform duration-200">
<div className="text-stone-500 transition-transform duration-200">
{isExpanded ? (
<ChevronDownIcon className="h-5 w-5" />
) : (
@@ -35,26 +35,26 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
const inputClasses = `
w-full px-4 py-3 rounded-lg border transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white
disabled:opacity-50 disabled:cursor-not-allowed
${
hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
? 'border-coral-500/60 bg-coral-50 text-coral-700 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
? 'border-sage-500/60 bg-sage-50 text-stone-900 placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-200 bg-white text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
}
`;
return (
<label
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
className={`space-y-3 text-sm text-stone-600 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<div>
<span className="font-medium">
{label}
{required && <span className="text-coral-400 ml-1">*</span>}
</span>
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
{helpText && <p className="text-xs text-stone-500 leading-relaxed mt-1">{helpText}</p>}
</div>
<div className="relative">
@@ -81,7 +81,7 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
{/* Error message */}
{hasError && (
<div className="flex items-center gap-2 text-xs text-coral-300">
<div className="flex items-center gap-2 text-xs text-coral-600">
<ExclamationTriangleIcon className="h-3 w-3 flex-shrink-0" />
<span>{error}</span>
</div>
@@ -124,26 +124,26 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
const selectClasses = `
w-full px-4 py-3 rounded-lg border transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white
disabled:opacity-50 disabled:cursor-not-allowed
${
hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 focus:border-coral-500 focus:ring-coral-500/30'
? 'border-coral-500/60 bg-coral-50 text-coral-700 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white focus:border-primary-500/50 focus:ring-primary-500/30'
? 'border-sage-500/60 bg-sage-50 text-stone-900 focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-200 bg-white text-stone-900 focus:border-primary-500/50 focus:ring-primary-500/30'
}
`;
return (
<label
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
className={`space-y-3 text-sm text-stone-600 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<div>
<span className="font-medium">
{label}
{required && <span className="text-coral-400 ml-1">*</span>}
</span>
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
{helpText && <p className="text-xs text-stone-500 leading-relaxed mt-1">{helpText}</p>}
</div>
<div className="relative">
@@ -174,7 +174,7 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
{/* Error message */}
{hasError && (
<div className="flex items-center gap-2 text-xs text-coral-300">
<div className="flex items-center gap-2 text-xs text-coral-600">
<ExclamationTriangleIcon className="h-3 w-3 flex-shrink-0" />
<span>{error}</span>
</div>
+11 -11
View File
@@ -60,7 +60,7 @@ export default function SetupFormRenderer({
<form onSubmit={handleSubmit} className="space-y-4">
{/* Step header */}
<div className="mb-4">
<h3 className="text-lg font-semibold text-white">{step.title}</h3>
<h3 className="text-lg font-semibold text-stone-900">{step.title}</h3>
{step.description && (
<p className="text-sm text-stone-400 mt-1">{step.description}</p>
)}
@@ -80,7 +80,7 @@ export default function SetupFormRenderer({
return (
<div key={field.name} className="space-y-1.5">
<label className="block text-sm font-medium text-stone-300">
<label className="block text-sm font-medium text-stone-700">
{field.label}
{field.required && (
<span className="text-coral-400 ml-0.5">*</span>
@@ -106,10 +106,10 @@ export default function SetupFormRenderer({
}
placeholder={field.placeholder ?? undefined}
disabled={loading}
className={`w-full px-3 py-2.5 bg-black/50 border rounded-xl text-sm text-white placeholder-stone-600 focus:outline-none focus:ring-1 transition-colors ${
className={`w-full px-3 py-2.5 bg-stone-50 border rounded-xl text-sm text-stone-900 placeholder-stone-400 focus:outline-none focus:ring-1 transition-colors ${
fieldError
? "border-coral-500/50 focus:ring-coral-500/30"
: "border-stone-700 focus:ring-primary-500/30 focus:border-primary-500/50"
: "border-stone-200 focus:ring-primary-500/30 focus:border-primary-500/50"
}`}
/>
)}
@@ -123,10 +123,10 @@ export default function SetupFormRenderer({
onChange={(e) => updateValue(field.name, e.target.value)}
placeholder={field.placeholder ?? undefined}
disabled={loading}
className={`w-full px-3 py-2.5 pr-10 bg-black/50 border rounded-xl text-sm text-white placeholder-stone-600 focus:outline-none focus:ring-1 transition-colors ${
className={`w-full px-3 py-2.5 pr-10 bg-stone-50 border rounded-xl text-sm text-stone-900 placeholder-stone-400 focus:outline-none focus:ring-1 transition-colors ${
fieldError
? "border-coral-500/50 focus:ring-coral-500/30"
: "border-stone-700 focus:ring-primary-500/30 focus:border-primary-500/50"
: "border-stone-200 focus:ring-primary-500/30 focus:border-primary-500/50"
}`}
/>
<button
@@ -137,7 +137,7 @@ export default function SetupFormRenderer({
[field.name]: !prev[field.name],
}))
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-stone-500 hover:text-stone-300"
className="absolute right-3 top-1/2 -translate-y-1/2 text-stone-500 hover:text-stone-700"
>
{showPasswords[field.name] ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -159,10 +159,10 @@ export default function SetupFormRenderer({
value={String(value ?? "")}
onChange={(e) => updateValue(field.name, e.target.value)}
disabled={loading}
className={`w-full px-3 py-2.5 bg-black/50 border rounded-xl text-sm text-white focus:outline-none focus:ring-1 transition-colors ${
className={`w-full px-3 py-2.5 bg-stone-50 border rounded-xl text-sm text-stone-900 focus:outline-none focus:ring-1 transition-colors ${
fieldError
? "border-coral-500/50 focus:ring-coral-500/30"
: "border-stone-700 focus:ring-primary-500/30 focus:border-primary-500/50"
: "border-stone-200 focus:ring-primary-500/30 focus:border-primary-500/50"
}`}
>
<option value="">Select...</option>
@@ -193,7 +193,7 @@ export default function SetupFormRenderer({
}`}
/>
</div>
<span className="text-sm text-stone-300">
<span className="text-sm text-stone-700">
{value ? "Enabled" : "Disabled"}
</span>
</label>
@@ -213,7 +213,7 @@ export default function SetupFormRenderer({
type="button"
onClick={onCancel}
disabled={loading}
className="flex-1 py-2.5 text-sm font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors disabled:opacity-50"
className="flex-1 py-2.5 text-sm font-medium text-stone-600 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-colors disabled:opacity-50"
>
Cancel
</button>
+22 -22
View File
@@ -106,20 +106,20 @@ export default function SkillDebugModal({ skillId, skillName, onClose }: SkillDe
const content = (
<div
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
className="fixed inset-0 z-[9999] bg-black/30 backdrop-blur-sm flex items-center justify-center p-4"
onClick={handleBackdropClick}
role="dialog"
aria-modal="true">
<div
ref={modalRef}
className="bg-stone-900 border border-stone-600 rounded-3xl shadow-large w-full max-w-[600px] max-h-[80vh] overflow-hidden animate-fade-up focus:outline-none"
className="bg-white border border-stone-200 rounded-3xl shadow-large w-full max-w-[600px] max-h-[80vh] overflow-hidden animate-fade-up focus:outline-none"
style={{ animationDuration: '200ms' }}
tabIndex={-1}
onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="p-4 border-b border-stone-700/50 flex items-center justify-between">
<div className="p-4 border-b border-stone-200 flex items-center justify-between">
<div>
<h2 className="text-base font-semibold text-white">
<h2 className="text-base font-semibold text-stone-900">
Debug: {skillName}
</h2>
<div className="flex items-center gap-2 mt-1">
@@ -130,7 +130,7 @@ export default function SkillDebugModal({ skillId, skillName, onClose }: SkillDe
</div>
<button
onClick={onClose}
className="p-1 text-stone-400 hover:text-white transition-colors rounded-lg hover:bg-stone-700/50">
className="p-1 text-stone-400 hover:text-stone-900 transition-colors rounded-lg hover:bg-stone-100">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -138,15 +138,15 @@ export default function SkillDebugModal({ skillId, skillName, onClose }: SkillDe
</div>
{/* Tabs */}
<div className="flex border-b border-stone-700/50">
<div className="flex border-b border-stone-200">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 py-2 text-xs font-medium transition-colors ${
activeTab === tab.id
? 'text-primary-400 border-b-2 border-primary-400'
: 'text-stone-400 hover:text-stone-300'
? 'text-primary-500 border-b-2 border-primary-500'
: 'text-stone-600 hover:text-stone-700'
}`}>
{tab.label}
</button>
@@ -181,9 +181,9 @@ export default function SkillDebugModal({ skillId, skillName, onClose }: SkillDe
function StatusBadge({ label, variant = 'default' }: { label: string; variant?: 'default' | 'green' | 'blue' }) {
const colors = {
default: 'bg-stone-700 text-stone-300',
green: 'bg-sage-500/20 text-sage-400',
blue: 'bg-primary-500/20 text-primary-400',
default: 'bg-stone-100 text-stone-600',
green: 'bg-sage-100 text-sage-600',
blue: 'bg-primary-100 text-primary-500',
};
return (
<span className={`px-2 py-0.5 text-[10px] font-mono rounded ${colors[variant]}`}>
@@ -221,7 +221,7 @@ function StateTab({
{stateEntries.length === 0 ? (
<p className="text-xs text-stone-500 italic">No published state</p>
) : (
<pre className="text-xs text-stone-300 font-mono bg-black/30 rounded-lg p-3 overflow-x-auto whitespace-pre-wrap break-all max-h-64 overflow-y-auto">
<pre className="text-xs text-stone-700 font-mono bg-stone-50 rounded-lg p-3 overflow-x-auto whitespace-pre-wrap break-all max-h-64 overflow-y-auto">
{formatJson(snap.state)}
</pre>
)}
@@ -229,7 +229,7 @@ function StateTab({
{/* Raw snapshot */}
<Section title="Raw Snapshot">
<pre className="text-xs text-stone-400 font-mono bg-black/30 rounded-lg p-3 overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
<pre className="text-xs text-stone-600 font-mono bg-stone-50 rounded-lg p-3 overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
{formatJson(snap)}
</pre>
</Section>
@@ -270,13 +270,13 @@ function ToolsTab({
const hasResult = toolResult?.toolName === tool.name;
return (
<div key={tool.name} className="border border-stone-700/50 rounded-xl overflow-hidden">
<div key={tool.name} className="border border-stone-200 rounded-xl overflow-hidden">
{/* Tool header */}
<button
onClick={() => setExpandedTool(isExpanded ? null : tool.name)}
className="w-full flex items-center justify-between p-3 text-left hover:bg-white/5 transition-colors">
className="w-full flex items-center justify-between p-3 text-left hover:bg-stone-50 transition-colors">
<div className="min-w-0">
<span className="text-sm font-mono text-primary-400">{tool.name}</span>
<span className="text-sm font-mono text-primary-500">{tool.name}</span>
<p className="text-xs text-stone-400 mt-0.5 line-clamp-1">{tool.description}</p>
</div>
<svg
@@ -290,14 +290,14 @@ function ToolsTab({
{/* Expanded content */}
{isExpanded && (
<div className="px-3 pb-3 space-y-2 border-t border-stone-700/30">
<div className="px-3 pb-3 space-y-2 border-t border-stone-200">
{/* Input schema */}
{tool.inputSchema != null && (
<div className="mt-2">
<label className="text-[10px] font-medium text-stone-500 uppercase tracking-wider">
Input Schema
</label>
<pre className="text-xs text-stone-400 font-mono bg-black/30 rounded-lg p-2 mt-1 overflow-x-auto max-h-32 overflow-y-auto">
<pre className="text-xs text-stone-600 font-mono bg-stone-50 rounded-lg p-2 mt-1 overflow-x-auto max-h-32 overflow-y-auto">
{formatJson(tool.inputSchema)}
</pre>
</div>
@@ -313,7 +313,7 @@ function ToolsTab({
onChange={e => setToolArgs({ ...toolArgs, [tool.name]: e.target.value })}
placeholder="{}"
rows={3}
className="w-full mt-1 px-3 py-2 text-xs font-mono bg-black/30 border border-stone-700 rounded-lg text-stone-300 placeholder-stone-600 focus:outline-none focus:border-primary-500/50 resize-y"
className="w-full mt-1 px-3 py-2 text-xs font-mono bg-stone-50 border border-stone-200 rounded-lg text-stone-700 placeholder-stone-400 focus:outline-none focus:border-primary-500/50 resize-y"
/>
</div>
@@ -321,7 +321,7 @@ function ToolsTab({
<button
onClick={() => onCallTool(tool.name)}
disabled={isLoading}
className="w-full py-2 text-xs font-medium bg-primary-500/20 text-primary-400 border border-primary-500/30 rounded-lg hover:bg-primary-500/30 disabled:opacity-50 transition-colors flex items-center justify-center gap-2">
className="w-full py-2 text-xs font-medium bg-primary-50 text-primary-500 border border-primary-500/30 rounded-lg hover:bg-primary-100 disabled:opacity-50 transition-colors flex items-center justify-center gap-2">
{isLoading ? (
<>
<svg className="animate-spin w-3 h-3" fill="none" viewBox="0 0 24 24">
@@ -344,7 +344,7 @@ function ToolsTab({
</span>
<span className="text-[10px] text-stone-500">{toolResult.durationMs}ms</span>
</div>
<pre className="text-xs font-mono text-stone-300 whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
<pre className="text-xs font-mono text-stone-700 whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
{formatJson(toolResult.result)}
</pre>
</div>
@@ -371,7 +371,7 @@ function KV({ label, value, isError }: { label: string; value: string; isError?:
return (
<div className="flex items-center gap-2 py-0.5">
<span className="text-xs text-stone-500 font-mono min-w-[120px]">{label}</span>
<span className={`text-xs font-mono ${isError ? 'text-red-400' : 'text-stone-300'}`}>{value}</span>
<span className={`text-xs font-mono ${isError ? 'text-red-500' : 'text-stone-700'}`}>{value}</span>
</div>
);
}
@@ -75,7 +75,7 @@ export default function SkillSetupModal({
const modalContent = (
<div
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
className="fixed inset-0 z-[9999] bg-black/30 backdrop-blur-sm flex items-center justify-center p-4"
onClick={handleBackdropClick}
role="dialog"
aria-modal="true"
@@ -83,7 +83,7 @@ export default function SkillSetupModal({
>
<div
ref={modalRef}
className="bg-stone-900 border border-stone-600 rounded-3xl shadow-large w-full max-w-[460px] overflow-hidden animate-fade-up focus:outline-none focus:ring-0"
className="bg-white border border-stone-200 rounded-3xl shadow-large w-full max-w-[460px] overflow-hidden animate-fade-up focus:outline-none focus:ring-0"
style={{
animationDuration: "200ms",
animationTimingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
@@ -93,13 +93,13 @@ export default function SkillSetupModal({
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-4 border-b border-stone-700/50">
<div className="p-4 border-b border-stone-200">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-2">
<div className="flex items-center gap-2">
<h2
id="skill-setup-title"
className="text-base font-semibold text-white"
className="text-base font-semibold text-stone-900"
>
{headerTitle}
</h2>
@@ -121,7 +121,7 @@ export default function SkillSetupModal({
</div>
<button
onClick={onClose}
className="p-1 text-stone-400 hover:text-white transition-colors rounded-lg hover:bg-stone-700/50 flex-shrink-0"
className="p-1 text-stone-400 hover:text-stone-900 transition-colors rounded-lg hover:bg-stone-100 flex-shrink-0"
>
<svg
className="w-5 h-5"
+39 -54
View File
@@ -13,50 +13,36 @@
html {
font-family:
'Inter',
system-ui,
-apple-system,
BlinkMacSystemFont,
'SF Pro Display',
'SF Pro Text',
system-ui,
'Segoe UI',
Roboto,
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
/* background-color: #000000; */
color: #ffffff;
color: #171717;
}
body {
@apply text-white;
@apply text-stone-900;
@apply font-sans;
line-height: 1.6;
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #0f0f0f 100%);
color: #ffffff;
z-index: -1;
background: #f5f5f5;
color: #171717;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&::after {
content: '';
background-image: url('/bg-dark.png');
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-repeat: repeat;
background-position: top right;
background-size: 510px auto;
pointer-events: none;
}
}
* {
color: #ffffff;
color: inherit;
}
/* Heading hierarchy for clear information architecture */
@@ -66,8 +52,8 @@
h4,
h5,
h6 {
@apply font-display font-semibold;
@apply text-white;
@apply font-sans font-semibold;
@apply text-stone-900;
line-height: 1.2;
}
@@ -120,21 +106,21 @@
@layer components {
/* Button variants for consistent interaction design */
.btn-primary {
@apply bg-primary-600 hover:bg-primary-500 active:bg-primary-700;
@apply bg-primary-500 hover:bg-primary-600 active:bg-primary-700;
@apply text-white font-medium;
@apply px-4 py-2 rounded-lg;
@apply transition-all duration-200 ease-in-out;
@apply shadow-soft hover:shadow-medium;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-black;
@apply focus:outline-none;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply bg-stone-800 hover:bg-stone-700 active:bg-stone-600;
@apply bg-stone-900 hover:bg-stone-800 active:bg-stone-700;
@apply text-white font-medium;
@apply px-4 py-2 rounded-lg border border-stone-600;
@apply px-4 py-2 rounded-lg border border-stone-300;
@apply transition-all duration-200 ease-in-out;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-black;
@apply focus:outline-none;
}
.btn-success {
@@ -143,7 +129,7 @@
@apply px-4 py-2 rounded-lg;
@apply transition-all duration-200 ease-in-out;
@apply shadow-soft hover:shadow-medium;
@apply focus:outline-none focus:ring-2 focus:ring-sage-500 focus:ring-offset-2 focus:ring-offset-black;
@apply focus:outline-none;
}
.btn-danger {
@@ -152,13 +138,13 @@
@apply px-4 py-2 rounded-lg;
@apply transition-all duration-200 ease-in-out;
@apply shadow-soft hover:shadow-medium;
@apply focus:outline-none focus:ring-2 focus:ring-coral-500 focus:ring-offset-2 focus:ring-offset-black;
@apply focus:outline-none;
}
/* Card components for content organization */
.card {
@apply bg-black rounded-xl shadow-soft border border-stone-700;
@apply p-6 text-white;
@apply bg-white rounded-xl shadow-soft border border-stone-200;
@apply p-6 text-stone-900;
@apply transition-shadow duration-200;
}
@@ -169,37 +155,37 @@
/* Input components with consistent styling */
.input-primary {
@apply w-full px-4 py-3 rounded-lg;
@apply border border-stone-600 focus:border-primary-500;
@apply bg-black text-white placeholder-stone-400;
@apply border border-stone-200 focus:border-primary-500;
@apply bg-white text-stone-900 placeholder-stone-400;
@apply transition-colors duration-200;
@apply focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:outline-none;
@apply focus:outline-none;
}
/* Status indicators */
.status-online {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
@apply bg-sage-800 text-white;
@apply bg-sage-100 text-sage-700;
}
.status-offline {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
@apply bg-stone-800 text-white;
@apply bg-stone-100 text-stone-600;
}
.status-warning {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
@apply bg-amber-800 text-white;
@apply bg-amber-100 text-amber-700;
}
/* Navigation styles */
.nav-item {
@apply flex items-center px-4 py-2 text-sm font-medium rounded-lg;
@apply text-white hover:text-white hover:bg-canvas-100;
@apply text-stone-600 hover:text-stone-900 hover:bg-stone-100;
@apply transition-colors duration-150;
}
.nav-item-active {
@apply nav-item bg-primary-600 text-white;
@apply nav-item bg-stone-100 text-stone-900;
}
/* Message/chat specific styles */
@@ -213,12 +199,12 @@
}
.message-received {
@apply message-bubble bg-stone-800 text-white;
@apply message-bubble bg-stone-200/80 text-stone-900;
}
/* Loading states */
.loading-pulse {
@apply animate-pulse bg-stone-800 rounded;
@apply animate-pulse bg-stone-100 rounded;
}
/* Crypto price styling */
@@ -274,13 +260,12 @@
@apply bg-primary-600 text-white;
}
/* Enhanced glass effect for better visibility */
/* Glass effect for light theme */
.glass {
backdrop-filter: blur(1px);
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); */
color: #ffffff;
backdrop-filter: blur(12px);
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
/* Animation utilities */
@@ -355,8 +340,8 @@
position: sticky;
top: 0;
z-index: 10;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(1px);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
}
/* Gradient fade overlay at bottom */
@@ -367,7 +352,7 @@
left: 0;
right: 0;
height: 50px;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.15));
background: linear-gradient(to bottom, transparent, rgba(245, 245, 245, 0.9));
pointer-events: none;
z-index: 10;
}
@@ -383,7 +368,7 @@
}
}
/* Dark mode support (always enabled) */
/* Light mode (default) */
:root {
color-scheme: dark;
color-scheme: light;
}
+18 -22
View File
@@ -1,28 +1,24 @@
const Agents = () => {
return (
<div className="min-h-full relative">
<div className="relative z-10 min-h-full flex flex-col">
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-md w-full">
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up text-center">
<div className="flex justify-center mb-4">
<svg
className="w-12 h-12 text-primary-400 opacity-60"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<h1 className="text-xl font-bold mb-2">Agents</h1>
<p className="text-sm opacity-60">Your AI agents will appear here</p>
</div>
<div className="min-h-full bg-[#F5F5F5] 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">
<svg
className="w-12 h-12 text-primary-500 opacity-60"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<h1 className="text-xl font-bold text-stone-900 mb-2">Agents</h1>
<p className="text-sm text-stone-500">Your AI agents will appear here</p>
</div>
</div>
</div>
+59 -43
View File
@@ -899,22 +899,22 @@ const Conversations = () => {
const inlineCompletionSuffix = getInlineCompletionSuffix(inputValue, inlineSuggestionValue);
return (
<div className="h-full relative z-10 flex overflow-hidden">
<div className="flex-1 flex flex-col min-w-0">
<div className="flex items-center gap-3 px-5 py-3 border-b border-white/10">
<div className="h-full relative z-10 flex justify-center overflow-hidden bg-[#F5F5F5] 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-2">
<h3 className="text-sm font-semibold truncate">
<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-[10px] px-1.5 py-0.5 rounded-full bg-sage-500/20 text-sage-500 flex-shrink-0">
<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-500 mt-0.5">
<p className="text-xs text-stone-400 mt-0.5">
Created {formatRelativeTime(selectedThread.createdAt)}
</p>
)}
@@ -927,7 +927,7 @@ const Conversations = () => {
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
<div
className={`h-12 rounded-2xl animate-pulse bg-white/5 ${
className={`h-12 rounded-2xl animate-pulse bg-stone-100 ${
i % 2 === 0 ? 'w-2/3' : 'w-1/2'
}`}
/>
@@ -966,11 +966,11 @@ const Conversations = () => {
<div
className={`rounded-2xl px-4 py-2.5 ${
msg.sender === 'user'
? 'bg-primary-600/20 rounded-br-md'
: 'bg-white/5 rounded-bl-md'
? 'bg-primary-500 text-white rounded-br-md'
: 'bg-stone-200/80 text-stone-900 rounded-bl-md'
}`}>
{msg.sender === 'agent' ? (
<div className="text-sm prose prose-invert prose-sm max-w-none prose-p:my-1 prose-pre:my-2 prose-pre:bg-black/30 prose-pre:rounded-lg prose-code:text-primary-300 prose-code:text-xs prose-a:text-primary-400 prose-headings:text-sm prose-headings:font-semibold prose-ul:my-1 prose-ol:my-1 prose-li:my-0">
<div className="text-sm prose prose-sm max-w-none prose-p:my-1 prose-pre:my-2 prose-pre:bg-stone-300/50 prose-pre:rounded-lg prose-code:text-primary-700 prose-code:text-xs prose-a:text-primary-500 prose-headings:text-sm prose-headings:font-semibold prose-ul:my-1 prose-ol:my-1 prose-li:my-0">
<Markdown>{msg.content}</Markdown>
</div>
) : (
@@ -978,14 +978,14 @@ const Conversations = () => {
)}
<p
className={`text-[10px] mt-1 ${
msg.sender === 'user' ? 'text-primary-400/50' : 'text-stone-600'
msg.sender === 'user' ? 'text-white/60' : 'text-stone-400'
}`}>
{formatRelativeTime(msg.createdAt)}
</p>
</div>
<button
onClick={() => handleCopyMessage(msg.id, msg.content)}
className={`absolute -top-1 ${msg.sender === 'user' ? '-left-8' : '-right-8'} p-1 rounded-md opacity-0 group-hover/msg:opacity-100 hover:bg-white/10 text-stone-600 hover:text-stone-300 transition-all`}
className={`absolute -top-1 ${msg.sender === 'user' ? '-left-8' : '-right-8'} p-1 rounded-md opacity-0 group-hover/msg:opacity-100 hover:bg-stone-100 text-stone-400 hover:text-stone-600 transition-all`}
title="Copy message">
{copiedMessageId === msg.id ? (
<svg
@@ -1037,14 +1037,14 @@ const Conversations = () => {
})
)
}
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-primary-600/20 border border-primary-500/30 text-xs transition-colors hover:bg-primary-600/30"
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-primary-100 border border-primary-200 text-xs transition-colors hover:bg-primary-200"
title={`Remove ${emoji}`}>
{emoji}
</button>
))}
{msg.sender === 'agent' &&
(reactionPickerMsgId === msg.id ? (
<div className="flex items-center gap-0.5 px-1 py-0.5 rounded-full bg-white/10">
<div className="flex items-center gap-0.5 px-1 py-0.5 rounded-full bg-stone-100">
{['👍', '❤️', '😂', '🔥', '👀', '🎯'].map(emoji => (
<button
key={emoji}
@@ -1074,7 +1074,7 @@ const Conversations = () => {
) : (
<button
onClick={() => setReactionPickerMsgId(msg.id)}
className="opacity-0 group-hover/msg:opacity-100 flex items-center px-1.5 py-0.5 rounded-full bg-white/5 hover:bg-white/15 text-stone-500 hover:text-stone-300 text-xs transition-all"
className="opacity-0 group-hover/msg:opacity-100 flex items-center px-1.5 py-0.5 rounded-full bg-stone-50 hover:bg-stone-200 text-stone-500 hover:text-stone-300 text-xs transition-all"
title="Add reaction">
+
</button>
@@ -1087,7 +1087,7 @@ const Conversations = () => {
))}
{((activeThreadId === selectedThreadId && isSending) || isDelivering) && (
<div className="flex justify-start">
<div className="bg-white/5 rounded-2xl rounded-bl-md px-4 py-3">
<div className="bg-stone-200/80 rounded-2xl rounded-bl-md px-4 py-3">
<div className="flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-stone-500 animate-bounce [animation-delay:0ms]" />
<span className="w-1.5 h-1.5 rounded-full bg-stone-500 animate-bounce [animation-delay:150ms]" />
@@ -1104,10 +1104,10 @@ const Conversations = () => {
<span
className={`rounded-full px-2 py-0.5 text-[10px] ${
entry.status === 'running'
? 'bg-amber-500/20 text-amber-300'
? 'bg-amber-100 text-amber-600'
: entry.status === 'success'
? 'bg-sage-500/20 text-sage-300'
: 'bg-coral-500/20 text-coral-300'
? 'bg-sage-100 text-sage-600'
: 'bg-coral-100 text-coral-600'
}`}>
{entry.status}
</span>
@@ -1121,7 +1121,7 @@ const Conversations = () => {
onClick={() => {
if (selectedThreadId) void chatCancel(selectedThreadId);
}}
className="text-xs text-stone-400 hover:text-stone-200 transition-colors">
className="text-xs text-stone-500 hover:text-stone-700 transition-colors">
Cancel
</button>
</div>
@@ -1146,7 +1146,7 @@ const Conversations = () => {
void handleSendMessage(s.text);
}}
disabled={isSending || !rustChat}
className="flex-shrink-0 px-3 py-1.5 rounded-lg text-[12px] whitespace-nowrap bg-white/5 text-stone-400 hover:bg-white/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
className="flex-shrink-0 px-3 py-1.5 rounded-lg text-[12px] whitespace-nowrap bg-white text-stone-500 border border-stone-200 hover:bg-stone-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{s.text}
</button>
))}
@@ -1154,9 +1154,9 @@ const Conversations = () => {
</div>
)}
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
<div className="flex-shrink-0 border-t border-stone-200 px-4 py-3">
{teamUsage && teamUsage.remainingUsd <= 0 && (
<div className="mb-3 p-3 rounded-xl bg-coral-500/10 border border-coral-500/20 flex items-center justify-between gap-3">
<div className="mb-3 p-3 rounded-xl bg-coral-50 border border-coral-200 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
<svg
className="w-4 h-4 text-coral-400 flex-shrink-0"
@@ -1170,7 +1170,7 @@ const Conversations = () => {
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p className="text-xs text-coral-300 truncate">
<p className="text-xs text-coral-600 truncate">
Daily inference budget exhausted. Top up to continue.
</p>
</div>
@@ -1187,20 +1187,20 @@ const Conversations = () => {
<span className="text-xs text-stone-600">Loading models</span>
) : (
<>
<span className="text-xs text-stone-500">Model</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-white/5 border border-white/10 rounded-lg px-2 py-1 text-xs text-stone-300 focus:outline-none focus:ring-1 focus:ring-primary-500/50 disabled:opacity-50 cursor-pointer">
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-stone-900">
<option key={m.id} value={m.id} className="bg-white">
{m.id}
</option>
))
) : (
<option value={selectedModel} className="bg-stone-900">
<option value={selectedModel} className="bg-white">
{selectedModel}
</option>
)}
@@ -1208,7 +1208,7 @@ const Conversations = () => {
</>
)}
<div className="flex-1" />
<div className="flex items-center gap-1 rounded-lg border border-white/10 bg-white/5 p-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"
@@ -1217,7 +1217,7 @@ const Conversations = () => {
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
inputMode === 'text'
? 'bg-primary-600 text-white'
: 'text-stone-300 hover:bg-white/10'
: 'text-stone-500 hover:bg-stone-100'
}`}>
Text
</button>
@@ -1228,12 +1228,12 @@ const Conversations = () => {
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
inputMode === 'voice'
? 'bg-primary-600 text-white'
: 'text-stone-300 hover:bg-white/10'
: 'text-stone-500 hover:bg-stone-100'
}`}>
Voice
</button>
</div>
<div className="flex items-center gap-1 rounded-lg border border-white/10 bg-white/5 p-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">Reply</span>
<button
type="button"
@@ -1241,7 +1241,7 @@ const Conversations = () => {
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
replyMode === 'text'
? 'bg-primary-600 text-white'
: 'text-stone-300 hover:bg-white/10'
: 'text-stone-500 hover:bg-stone-100'
}`}>
Text
</button>
@@ -1252,7 +1252,7 @@ const Conversations = () => {
className={`px-2 py-1 rounded-md text-[11px] transition-colors ${
replyMode === 'voice'
? 'bg-primary-600 text-white'
: 'text-stone-300 hover:bg-white/10'
: 'text-stone-500 hover:bg-stone-100'
}`}>
Voice
</button>
@@ -1286,7 +1286,7 @@ const Conversations = () => {
fill="none"
stroke="currentColor"
strokeWidth="2.5"
className="text-white/10"
className="text-stone-200"
/>
{teamUsage ? (
<circle
@@ -1333,15 +1333,15 @@ const Conversations = () => {
</p>
<button
onClick={() => setSendError(null)}
className="text-xs text-stone-500 hover:text-stone-300 transition-colors ml-2 flex-shrink-0">
className="text-xs text-stone-500 hover:text-stone-700 transition-colors ml-2 flex-shrink-0">
Dismiss
</button>
</div>
)}
{inputMode === 'text' ? (
<div className="flex items-end gap-2">
<div className="relative flex-1 rounded-xl border border-white/10 bg-white/5 focus-within:ring-1 focus-within:ring-primary-500/50 focus-within:border-primary-500/50 transition-all">
<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
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">
@@ -1356,15 +1356,31 @@ const Conversations = () => {
placeholder="Type a message..."
rows={1}
disabled={isSending || !rustChat}
className="relative z-10 w-full resize-none border-0 bg-transparent px-4 py-2.5 text-sm leading-normal whitespace-pre-wrap break-words font-sans placeholder:text-stone-500 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 focus:outline-none focus:ring-0 max-h-32 disabled:opacity-50 disabled:cursor-not-allowed"
/>
{/* Mic icon inside input */}
<button
type="button"
onClick={() => setInputMode('voice')}
disabled={isRecording || isTranscribing || !rustChat}
className="absolute right-3 top-1/2 -translate-y-1/2 z-20 text-stone-400 hover:text-stone-600 transition-colors disabled:opacity-40"
title="Switch to voice 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="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>
</button>
</div>
<button
onClick={() => {
void handleSendMessage();
}}
disabled={!inputValue.trim() || isSending || !rustChat}
className="p-2.5 rounded-xl bg-primary-600 hover:bg-primary-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors flex-shrink-0">
className="w-10 h-10 flex items-center justify-center rounded-full bg-primary-500 hover:bg-primary-600 text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors flex-shrink-0">
{isSending ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
@@ -1386,8 +1402,8 @@ const Conversations = () => {
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 12h14M12 5l7 7-7 7"
strokeWidth={2.5}
d="M9 5l7 7-7 7"
/>
</svg>
)}
+158 -124
View File
@@ -163,144 +163,178 @@ const Home = () => {
const etaText = formatEta(localAiStatus?.eta_seconds);
return (
<div className="min-h-full relative">
{/* Content overlay */}
<div className="relative z-10 min-h-full flex flex-col">
{/* Main content */}
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-md w-full">
{/* Weather card */}
<div className="glass rounded-3xl p-4 shadow-large animate-fade-up text-center">
{/* Greeting */}
<h1 className="text-2xl font-bold mb-4">
{getGreeting()}, {userName}
</h1>
<div className="min-h-full flex flex-col items-center justify-center p-4 bg-[#F5F5F5]">
<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">
{/* Header row: logo + version + settings */}
<div className="flex items-center justify-between mb-8">
<div className="w-9 h-9 bg-stone-900 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<span className="text-xs text-stone-400">web, 01</span>
<button
onClick={() => navigate('/settings')}
className="w-9 h-9 rounded-full bg-stone-100 flex items-center justify-center hover:bg-stone-200 transition-colors"
aria-label="Settings">
<svg
className="w-4 h-4 text-stone-500"
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>
</button>
</div>
{/* Connection indicators */}
<ConnectionIndicator />
{/* Get Access button */}
{/* Welcome title */}
<h1 className="text-3xl font-bold text-stone-900 text-center mb-6">Welcome Onboard</h1>
{/* Greeting */}
<div className="text-center mb-3">
<p className="text-lg font-medium text-stone-700">
{getGreeting()}, {userName}
</p>
</div>
{/* Connection status */}
<div className="flex justify-center mb-3">
<ConnectionIndicator />
</div>
{/* Description */}
<p className="text-sm text-stone-500 text-center mb-6 leading-relaxed">
Your device is now connected to the OpenHuman AI. Keep the app running to keep the
connection alive. You can message your assistant with the button below.
</p>
{/* CTA button */}
<button
onClick={handleStartCooking}
className="w-full py-3 bg-primary-500 hover:bg-primary-600 text-white font-medium rounded-xl transition-colors duration-200">
Message OpenHuman
</button>
</div>
{/* Local AI card (desktop only) */}
{isTauri() && (
<div className="mt-3 bg-white rounded-2xl shadow-soft border border-stone-200 px-4 py-4 text-left">
<div className="flex items-center justify-between">
<div className="text-[11px] uppercase tracking-wide text-stone-400">
Local model runtime
</div>
<button
onClick={handleStartCooking}
className="btn-primary w-full py-2.5 text-sm font-medium rounded-xl">
Message OpenHuman 🔥
onClick={() => navigate('/settings/local-model')}
className="text-xs text-primary-500 hover:text-primary-600 transition-colors">
Manage
</button>
</div>
{isTauri() && (
<div className="my-3 rounded-3xl border border-stone-700/80 bg-black/45 px-3 py-3 text-left">
<div className="flex items-center justify-between">
<div className="text-[11px] uppercase tracking-wide text-stone-400">
Local model runtime
</div>
<button
onClick={() => navigate('/settings/local-model')}
className="text-xs text-cyan-300 hover:text-cyan-200 transition-colors">
Manage
</button>
</div>
<div className="mt-2 flex items-center justify-between text-xs">
<span className="text-stone-600">
{localAiStatus?.model_id ?? 'gemma3:4b-it-qat'}
</span>
<span className="text-stone-700 capitalize">
{localAiStatus?.state ?? 'starting'}
</span>
</div>
<div className="mt-2 flex items-center justify-between text-xs">
<span className="text-stone-300">
{localAiStatus?.model_id ?? 'gemma3:4b-it-qat'}
</span>
<span className="text-stone-200 capitalize">
{localAiStatus?.state ?? 'starting'}
</span>
</div>
<div className="mt-2 h-2 rounded-full bg-stone-100 overflow-hidden">
<div
className={`h-full bg-gradient-to-r from-primary-500 to-primary-400 transition-all duration-500 ${
indeterminateDownload ? 'animate-pulse' : ''
}`}
style={{
width: `${Math.round((indeterminateDownload ? 1 : modelProgress) * 100)}%`,
}}
/>
</div>
<div className="mt-2 h-2 rounded-full bg-stone-800 overflow-hidden">
<div
className={`h-full bg-gradient-to-r from-blue-500 to-cyan-400 transition-all duration-500 ${
indeterminateDownload ? 'animate-pulse' : ''
}`}
style={{
width: `${Math.round((indeterminateDownload ? 1 : modelProgress) * 100)}%`,
}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-[11px] text-stone-400">
<span>
{isInstalling
? 'Installing Ollama runtime...'
: indeterminateDownload
? 'Downloading...'
: `${Math.round(modelProgress * 100)}%`}
</span>
{downloadedText && (
<span className="truncate text-stone-300" title={downloadedText}>
{downloadedText}
</span>
)}
{speedText && <span className="text-blue-300">{speedText}</span>}
{etaText && <span className="text-cyan-300">ETA {etaText}</span>}
</div>
{localAiStatus?.warning && (
<div
className="mt-1 text-[11px] text-stone-400 truncate"
title={localAiStatus.warning}>
{localAiStatus.warning}
</div>
)}
{isInstallError && localAiStatus?.error_detail && (
<div className="mt-2">
<button
onClick={() => setShowErrorDetail(v => !v)}
className="text-[11px] text-red-400 hover:text-red-300 underline">
{showErrorDetail ? 'Hide error details' : 'Show error details'}
</button>
{showErrorDetail && (
<pre className="mt-1 max-h-32 overflow-auto rounded bg-stone-900/80 p-2 text-[10px] text-red-300 leading-tight whitespace-pre-wrap break-words">
{localAiStatus.error_detail}
</pre>
)}
<p className="mt-1 text-[11px] text-stone-400">
Install Ollama manually from{' '}
<a
href="https://ollama.com"
target="_blank"
rel="noopener noreferrer"
className="text-cyan-300 hover:text-cyan-200 underline">
ollama.com
</a>{' '}
then set its path in{' '}
<button
onClick={() => navigate('/settings/local-model')}
className="text-cyan-300 hover:text-cyan-200 underline">
Settings
</button>
.
</p>
</div>
)}
<div className="mt-2 flex items-center gap-2">
<button
onClick={() => void runManualBootstrap(false)}
disabled={downloadBusy}
className="rounded-md bg-blue-600 px-2.5 py-1.5 text-[11px] font-medium text-white hover:bg-blue-700 disabled:opacity-60">
{downloadBusy ? 'Working...' : 'Bootstrap'}
</button>
<button
onClick={() => void runManualBootstrap(true)}
disabled={downloadBusy}
className="rounded-md border border-stone-600 px-2.5 py-1.5 text-[11px] font-medium text-stone-200 hover:border-stone-500 disabled:opacity-60">
Re-bootstrap
</button>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-[11px] text-stone-400">
<span>
{isInstalling
? 'Installing Ollama runtime...'
: indeterminateDownload
? 'Downloading...'
: `${Math.round(modelProgress * 100)}%`}
</span>
{downloadedText && (
<span className="truncate text-stone-500" title={downloadedText}>
{downloadedText}
</span>
)}
{speedText && <span className="text-primary-500">{speedText}</span>}
{etaText && <span className="text-primary-600">ETA {etaText}</span>}
</div>
{localAiStatus?.warning && (
<div
className="mt-1 text-[11px] text-stone-400 truncate"
title={localAiStatus.warning}>
{localAiStatus.warning}
</div>
)}
<div className="mt-4 mb-8">
{isInstallError && localAiStatus?.error_detail && (
<div className="mt-2">
<button
onClick={() => setShowErrorDetail(v => !v)}
className="text-[11px] text-coral-500 hover:text-coral-600 underline">
{showErrorDetail ? 'Hide error details' : 'Show error details'}
</button>
{showErrorDetail && (
<pre className="mt-1 max-h-32 overflow-auto rounded bg-stone-50 p-2 text-[10px] text-coral-600 leading-tight whitespace-pre-wrap break-words">
{localAiStatus.error_detail}
</pre>
)}
<p className="mt-1 text-[11px] text-stone-400">
Install Ollama manually from{' '}
<a
href="https://ollama.com"
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-primary-600 underline">
ollama.com
</a>{' '}
then set its path in{' '}
<button
onClick={() => navigate('/settings/local-model')}
className="text-primary-500 hover:text-primary-600 underline">
Settings
</button>
.
</p>
</div>
)}
<div className="mt-2 flex items-center gap-2">
<button
onClick={() => navigate('/skills')}
className="btn-secondary w-full py-2.5 text-sm font-medium rounded-xl">
Open Skills Page
onClick={() => void runManualBootstrap(false)}
disabled={downloadBusy}
className="rounded-md bg-primary-500 px-2.5 py-1.5 text-[11px] font-medium text-white hover:bg-primary-600 disabled:opacity-60">
{downloadBusy ? 'Working...' : 'Bootstrap'}
</button>
<button
onClick={() => void runManualBootstrap(true)}
disabled={downloadBusy}
className="rounded-md border border-stone-200 px-2.5 py-1.5 text-[11px] font-medium text-stone-600 hover:border-stone-300 disabled:opacity-60">
Re-bootstrap
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
+27 -21
View File
@@ -250,16 +250,16 @@ export default function Intelligence() {
];
return (
<div className="min-h-full relative">
<div className="relative z-10 min-h-full flex flex-col">
<div className="flex-1 p-6">
<div className="max-w-6xl mx-auto">
<div className="min-h-full bg-[#F5F5F5] 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>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold text-white">Intelligence</h1>
<h1 className="text-xl font-bold text-stone-900">Intelligence</h1>
{activeTab === 'memory' && stats.total > 0 && (
<div className="text-xs bg-white/10 text-white px-2 py-1 rounded-full">
<div className="text-xs bg-stone-100 text-stone-900 px-2 py-1 rounded-full">
{stats.total}
</div>
)}
@@ -273,7 +273,7 @@ export default function Intelligence() {
<button
onClick={usingMemoryData ? refreshConscious : handleAnalyzeNow}
disabled={isRunning || itemsLoading}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-white/5 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed border border-white/10 rounded-lg text-stone-300 transition-colors">
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-stone-50 hover:bg-stone-100 disabled:opacity-40 disabled:cursor-not-allowed border border-stone-200 rounded-lg text-stone-600 transition-colors">
{isRunning || itemsLoading ? (
<div className="w-3 h-3 border border-stone-400 border-t-transparent rounded-full animate-spin" />
) : (
@@ -297,7 +297,7 @@ export default function Intelligence() {
</div>
{/* Tabs */}
<div className="flex border-b border-white/10 mb-6">
<div className="flex border-b border-stone-200 mb-6">
{tabs.map(tab => (
<button
key={tab.id}
@@ -305,11 +305,11 @@ export default function Intelligence() {
className={`relative px-4 py-2.5 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'text-primary-400 border-b-2 border-primary-400'
: 'text-stone-400 hover:text-stone-300'
: 'text-stone-400 hover:text-stone-700'
}`}>
{tab.label}
{tab.comingSoon && (
<span className="ml-1.5 text-[10px] px-1.5 py-0.5 rounded-full bg-white/5 text-stone-500 border border-white/10">
<span className="ml-1.5 text-[10px] px-1.5 py-0.5 rounded-full bg-stone-50 text-stone-500 border border-stone-200">
Soon
</span>
)}
@@ -330,7 +330,7 @@ export default function Intelligence() {
placeholder="Search actionable items..."
value={filters.search}
onChange={e => dispatch(setSearchFilter(e.target.value))}
className="w-full px-3 py-2 text-sm bg-white/5 border border-white/10 rounded-lg text-white placeholder-stone-500 focus:outline-none focus:border-primary-500/50 transition-colors"
className="w-full px-3 py-2 text-sm bg-white border border-stone-200 rounded-lg text-stone-900 placeholder-stone-400 focus:outline-none focus:border-primary-500/50 transition-colors"
/>
</div>
<select
@@ -338,7 +338,7 @@ export default function Intelligence() {
onChange={e =>
dispatch(setSourceFilter(e.target.value as ActionableItemSource | 'all'))
}
className="px-3 py-2 text-sm bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500/50 transition-colors">
className="px-3 py-2 text-sm bg-white border border-stone-200 rounded-lg text-stone-900 focus:outline-none focus:border-primary-500/50 transition-colors">
<option value="all">All Sources</option>
<option value="email">Email</option>
<option value="calendar">Calendar</option>
@@ -356,7 +356,7 @@ export default function Intelligence() {
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-primary-500/10">
<div className="w-8 h-8 border-2 border-primary-400 border-t-transparent rounded-full animate-spin" />
</div>
<h2 className="text-lg font-semibold text-white mb-2">
<h2 className="text-lg font-semibold text-stone-900 mb-2">
Loading Intelligence...
</h2>
<p className="text-stone-400 text-sm">Fetching your actionable items</p>
@@ -366,7 +366,9 @@ export default function Intelligence() {
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-primary-500/10">
<div className="w-8 h-8 border-2 border-primary-400 border-t-transparent rounded-full animate-spin" />
</div>
<h2 className="text-lg font-semibold text-white mb-2">Analyzing your data</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">
Analyzing your data
</h2>
<p className="text-stone-400 text-sm">
The conscious loop is reviewing your connected skills
</p>
@@ -389,19 +391,23 @@ export default function Intelligence() {
</div>
{filters.search || filters.source !== 'all' ? (
<>
<h2 className="text-lg font-semibold text-white mb-2">No matches</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">No matches</h2>
<p className="text-stone-400 text-sm">
No items match your current filters.
</p>
</>
) : usingMemoryData ? (
<>
<h2 className="text-lg font-semibold text-white mb-2">All caught up!</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">
All caught up!
</h2>
<p className="text-stone-400 text-sm">No actionable items at the moment.</p>
</>
) : (
<>
<h2 className="text-lg font-semibold text-white mb-2">No analysis yet</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">
No analysis yet
</h2>
<p className="text-stone-400 text-sm mb-4">
Run an analysis to extract actionable items from your connected skills.
</p>
@@ -428,10 +434,10 @@ export default function Intelligence() {
className="animate-fade-up"
style={{ animationDelay: `${groupIndex * 50}ms` }}>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-white opacity-80">
<h2 className="text-sm font-semibold text-stone-900 opacity-80">
{group.label}
</h2>
<div className="text-xs bg-white/10 text-white px-2 py-1 rounded-full">
<div className="text-xs bg-stone-100 text-stone-900 px-2 py-1 rounded-full">
{group.count}
</div>
</div>
@@ -479,7 +485,7 @@ export default function Intelligence() {
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-white mb-2">Subconscious</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">Subconscious</h2>
<p className="text-stone-400 text-sm mb-1">
OpenHuman will constantly have subconscious thoughts based on all the information
it has access to and the activity you have engaged with it in.
@@ -510,7 +516,7 @@ export default function Intelligence() {
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-white mb-2">Dreams</h2>
<h2 className="text-lg font-semibold text-stone-900 mb-2">Dreams</h2>
<p className="text-stone-400 text-sm mb-1">
Twice everyday, OpenHuman will generate a dream (or a summary) based on everything
that has happened in your life today. These dreams re then indexed and can be used
+6 -6
View File
@@ -94,13 +94,13 @@ const Invites = () => {
};
return (
<div className="min-h-full relative">
<div className="relative z-10 min-h-full flex flex-col">
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-md w-full space-y-4">
<div className="min-h-full bg-[#F5F5F5] 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">
{/* Redeem Section — shown only if user hasn't redeemed yet */}
{!hasBeenInvited && (
<div className="glass rounded-3xl p-6 shadow-large animate-fade-up">
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6 animate-fade-up">
<h2 className="text-lg font-bold mb-1">Redeem an Invite Code</h2>
<p className="text-xs opacity-70 mb-4">
Got a code from a friend? Enter it below to unlock free credits.
@@ -132,7 +132,7 @@ const Invites = () => {
)}
{/* Your Invite Codes */}
<div className="glass rounded-3xl p-6 shadow-large animate-fade-up">
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6 animate-fade-up">
<div className="mb-4">
<h2 className="text-lg font-bold mb-1">Your Invite Codes</h2>
<p className="text-xs opacity-70">
+3 -3
View File
@@ -184,13 +184,13 @@ const Mnemonic = () => {
const canContinue = mode === 'generate' ? confirmed : isImportComplete;
return (
<div className="min-h-full relative flex items-center justify-center">
<div className="relative z-10 max-w-lg w-full mx-4">
<div className="min-h-full bg-[#F5F5F5] 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} />
</div>
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-8 animate-fade-up">
{mode === 'generate' ? (
<>
<div className="text-center mb-4">
+33 -29
View File
@@ -27,35 +27,39 @@ import SettingsHome from '../components/settings/SettingsHome';
const Settings = () => {
return (
<div className="h-full overflow-hidden flex flex-col z-10 relative">
<Routes>
<Route index element={<SettingsHome />} />
<Route path="connections" element={<ConnectionsPanel />} />
<Route path="messaging" element={<MessagingPanel />} />
<Route path="cron-jobs" element={<CronJobsPanel />} />
<Route path="screen-intelligence" element={<ScreenIntelligencePanel />} />
<Route path="autocomplete" element={<AutocompletePanel />} />
<Route path="privacy" element={<PrivacyPanel />} />
<Route path="profile" element={<ProfilePanel />} />
<Route path="advanced" element={<AdvancedPanel />} />
<Route path="agent-chat" element={<AgentChatPanel />} />
<Route path="ai" element={<AIPanel />} />
<Route path="accessibility" element={<AccessibilityPanel />} />
<Route path="dictation" element={<DictationPanel />} />
<Route path="local-model" element={<LocalModelPanel />} />
<Route path="billing" element={<BillingPanel />} />
<Route path="skills" element={<SkillsPanel />} />
<Route path="team" element={<TeamPanel />} />
<Route path="team/manage/:teamId" element={<TeamManagementPanel />} />
<Route path="team/manage/:teamId/members" element={<TeamMembersPanel />} />
<Route path="team/manage/:teamId/invites" element={<TeamInvitesPanel />} />
<Route path="team/members" element={<TeamMembersPanel />} />
<Route path="team/invites" element={<TeamInvitesPanel />} />
<Route path="developer-options" element={<DeveloperOptionsPanel />} />
<Route path="tauri-commands" element={<TauriCommandsPanel />} />
<Route path="memory-debug" element={<MemoryDebugPanel />} />
<Route path="recovery-phrase" element={<RecoveryPhrasePanel />} />
</Routes>
<div className="h-full overflow-y-auto bg-[#F5F5F5] 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"
style={{ maxHeight: 'calc(100vh - 7rem)' }}>
<Routes>
<Route index element={<SettingsHome />} />
<Route path="connections" element={<ConnectionsPanel />} />
<Route path="messaging" element={<MessagingPanel />} />
<Route path="cron-jobs" element={<CronJobsPanel />} />
<Route path="screen-intelligence" element={<ScreenIntelligencePanel />} />
<Route path="autocomplete" element={<AutocompletePanel />} />
<Route path="privacy" element={<PrivacyPanel />} />
<Route path="profile" element={<ProfilePanel />} />
<Route path="advanced" element={<AdvancedPanel />} />
<Route path="agent-chat" element={<AgentChatPanel />} />
<Route path="ai" element={<AIPanel />} />
<Route path="accessibility" element={<AccessibilityPanel />} />
<Route path="dictation" element={<DictationPanel />} />
<Route path="local-model" element={<LocalModelPanel />} />
<Route path="billing" element={<BillingPanel />} />
<Route path="skills" element={<SkillsPanel />} />
<Route path="team" element={<TeamPanel />} />
<Route path="team/manage/:teamId" element={<TeamManagementPanel />} />
<Route path="team/manage/:teamId/members" element={<TeamMembersPanel />} />
<Route path="team/manage/:teamId/invites" element={<TeamInvitesPanel />} />
<Route path="team/members" element={<TeamMembersPanel />} />
<Route path="team/invites" element={<TeamInvitesPanel />} />
<Route path="developer-options" element={<DeveloperOptionsPanel />} />
<Route path="tauri-commands" element={<TauriCommandsPanel />} />
<Route path="memory-debug" element={<MemoryDebugPanel />} />
<Route path="recovery-phrase" element={<RecoveryPhrasePanel />} />
</Routes>
</div>
</div>
);
};
+27 -30
View File
@@ -29,13 +29,13 @@ import {
function statusDotClass(status: SkillConnectionStatus): string {
switch (status) {
case 'connected':
return 'bg-sage-400';
return 'bg-sage-500';
case 'connecting':
return 'bg-amber-400 animate-pulse';
return 'bg-amber-500 animate-pulse';
case 'error':
return 'bg-coral-400';
return 'bg-coral-500';
default:
return 'bg-stone-600';
return 'bg-stone-400';
}
}
@@ -97,16 +97,16 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
};
return (
<div className="flex items-center gap-3 p-3 rounded-xl bg-white/[0.03] border border-white/[0.06] hover:bg-white/[0.06] transition-colors">
<div className="flex items-center gap-3 p-3 rounded-xl bg-white border border-stone-100 hover:bg-stone-50 transition-colors">
{/* Icon */}
<div className="w-8 h-8 flex items-center justify-center text-white opacity-70 flex-shrink-0">
<div className="w-8 h-8 flex items-center justify-center text-stone-600 flex-shrink-0">
{skill.icon || <DefaultIcon />}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white truncate">{skill.name}</span>
<span className="text-sm font-medium text-stone-900 truncate">{skill.name}</span>
<div
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${statusDotClass(connectionStatus)}`}
/>
@@ -119,7 +119,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
)}
{isSyncing && (
<div className="mt-1.5">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-stone-800">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-stone-100">
{syncUi.progressPercent != null ? (
<div
className="h-full rounded-full bg-primary-400 transition-all duration-300"
@@ -130,7 +130,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
)}
</div>
{syncUi.progressMessage && (
<p className="text-[11px] text-primary-300 truncate mt-1">{syncUi.progressMessage}</p>
<p className="text-[11px] text-primary-600 truncate mt-1">{syncUi.progressMessage}</p>
)}
{syncUi.metricsText && (
<p className="text-[11px] text-stone-500 truncate mt-0.5">{syncUi.metricsText}</p>
@@ -147,7 +147,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
<button
onClick={isSyncing ? undefined : handleSync}
disabled={isSyncing}
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-40"
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-stone-700 hover:bg-stone-100 transition-colors disabled:opacity-40"
title="Sync">
<svg
className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`}
@@ -168,7 +168,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
e.stopPropagation();
onSetup();
}}
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-white hover:bg-white/10 transition-colors"
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-stone-700 hover:bg-stone-100 transition-colors"
title="Settings">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -191,7 +191,7 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
e.stopPropagation();
setDebugOpen(true);
}}
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-amber-400 hover:bg-amber-500/10 transition-colors"
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-amber-600 hover:bg-amber-50 transition-colors"
title="Debug">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -278,33 +278,30 @@ export default function Skills() {
};
return (
<div className="min-h-full relative">
<div className="relative z-10 min-h-full flex flex-col">
<div className="flex-1 p-6">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-bold text-white">Skills</h1>
</div>
{/* Active Skills */}
<div className="animate-fade-up" style={{ animationDelay: '100ms' }}>
<div className="mb-3">
<h2 className="text-sm font-semibold text-white opacity-80">Active Skills</h2>
<div className="min-h-full bg-[#F5F5F5]">
<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">
{/* 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>
{/* Skills list */}
{skillsLoading || installing ? (
<div className="glass rounded-2xl p-6 text-center">
<p className="text-sm text-stone-500">
<div className="py-8 text-center">
<p className="text-sm text-stone-400">
{installing ? `Installing ${installing}...` : 'Loading skills...'}
</p>
</div>
) : sortedSkillsList.length === 0 ? (
<div className="glass rounded-2xl p-6 text-center">
<p className="text-sm text-stone-500">No skills discovered</p>
<div className="py-8 text-center">
<p className="text-sm text-stone-400">No skills discovered</p>
</div>
) : (
<div className="space-y-2">
<div className="space-y-1">
{sortedSkillsList.map(skill => (
<SkillCard key={skill.id} skill={skill} onSetup={() => openSkillSetup(skill)} />
))}
+19 -17
View File
@@ -16,32 +16,34 @@ export default function Webhooks() {
if (loading && tunnels.length === 0) {
return (
<div className="flex flex-col h-full overflow-hidden">
<div className="flex-1 flex items-center justify-center">
<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>
</div>
<div className="h-full bg-[#F5F5F5] 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>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full overflow-hidden">
<div className="flex-1 overflow-y-auto p-6 space-y-8">
<div className="h-full bg-[#F5F5F5] overflow-y-auto p-4 pt-6">
<div className="max-w-2xl mx-auto space-y-4">
{error && <div className="p-3 rounded-lg bg-coral-50 text-coral-700 text-sm">{error}</div>}
<TunnelList
tunnels={tunnels}
registrations={registrations}
loading={loading}
onCreateTunnel={createTunnel}
onDeleteTunnel={deleteTunnel}
onRefresh={refreshTunnels}
/>
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6">
<TunnelList
tunnels={tunnels}
registrations={registrations}
loading={loading}
onCreateTunnel={createTunnel}
onDeleteTunnel={deleteTunnel}
onRefresh={refreshTunnels}
/>
</div>
<WebhookActivity activity={activity} />
<div className="bg-white rounded-2xl shadow-soft border border-stone-200 p-6">
<WebhookActivity activity={activity} />
</div>
</div>
</div>
);
+5 -5
View File
@@ -4,8 +4,8 @@ import TypewriterGreeting from '../components/TypewriterGreeting';
const Welcome = () => {
return (
<div className="min-h-full relative flex items-center justify-center">
<div className="relative z-10 flex w-full max-w-md flex-col items-center gap-7 text-center mx-4 animate-fade-up">
<div className="min-h-full bg-[#F5F5F5] 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 />
</div>
@@ -14,9 +14,9 @@ const Welcome = () => {
greetings={['Hello HAL9000! 👋', "Let's cook! 🔥", 'The A-Team is here! 👊']}
/>
<p className="max-w-xl text-sm text-gray-400 md:text-base">
Welcome to <span className="color-white">OpenHuman</span>! Your Personal AI super
intelligence. Private, Simple and extremely powerful.
<p className="max-w-xl text-sm text-stone-500 md:text-base">
Welcome to <span className="font-medium text-stone-900">OpenHuman</span>! Your Personal AI
super intelligence. Private, Simple and extremely powerful.
</p>
{/* <div className="glass rounded-3xl p-8 text-center animate-fade-up shadow-large"> */}
+6 -6
View File
@@ -176,7 +176,7 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
<button
type="button"
onClick={onDefer}
className="text-sm text-white hover:text-stone-200 transition-colors">
className="text-sm text-stone-600 hover:text-stone-900 transition-colors">
Skip
</button>
</div>
@@ -191,7 +191,7 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
role="alert"
aria-live="assertive"
className="fixed bottom-4 left-4 z-[9997] w-[320px] animate-fade-up">
<div className="bg-stone-900 border border-coral-500/30 rounded-2xl shadow-large p-4">
<div className="bg-white border border-coral-500/30 rounded-2xl shadow-large p-4">
<div className="flex items-start gap-3">
{/* Warning icon */}
<svg
@@ -206,14 +206,14 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
/>
</svg>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-stone-100">{downloadError}</p>
<p className="mt-0.5 text-xs text-stone-400">
<p className="text-sm font-medium text-stone-900">{downloadError}</p>
<p className="mt-0.5 text-xs text-stone-500">
You can retry or continue downloads can be resumed later.
</p>
<button
type="button"
onClick={retryDownload}
className="mt-2 text-xs font-medium text-primary-400 hover:text-primary-300 transition-colors">
className="mt-2 text-xs font-medium text-primary-600 hover:text-primary-500 transition-colors">
Retry
</button>
</div>
@@ -222,7 +222,7 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => {
type="button"
aria-label="Dismiss Local AI error"
onClick={() => setDownloadError(null)}
className="flex-shrink-0 text-stone-500 hover:text-stone-300 transition-colors">
className="flex-shrink-0 text-stone-400 hover:text-stone-600 transition-colors">
<svg
aria-hidden="true"
className="w-4 h-4"
@@ -17,7 +17,7 @@ const OnboardingNextButton = ({
type="button"
onClick={onClick}
disabled={disabled || loading}
className="w-full py-2.5 btn-primary text-sm font-medium rounded-xl border transition-colors border-stone-600 hover:border-sage-500 hover:bg-sage-500/10">
className="w-full py-2.5 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white text-sm font-medium rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{loading ? (loadingLabel ?? label) : label}
</button>
);
@@ -23,10 +23,10 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
};
return (
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Anonymized Analytics</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Anonymized Analytics</h1>
<p className="text-stone-600 text-sm">
We collect fully anonymized usage data to help improve the app. No personal data,
messages, or wallet keys are ever collected. You can change this anytime in Settings.
</p>
@@ -36,8 +36,8 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
<div
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedOption === 'shareAnalytics'
? 'border-primary-500 bg-primary-500/20'
: 'border-stone-700 bg-black/50 hover:border-stone-600'
? 'border-primary-500 bg-primary-50'
: 'border-stone-200 bg-white hover:border-stone-300'
}`}
onClick={() => setSelectedOption('shareAnalytics')}>
<div className="flex items-start space-x-4">
@@ -46,7 +46,7 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedOption === 'shareAnalytics'
? 'border-primary-500 bg-primary-500'
: 'border-stone-600 bg-black'
: 'border-stone-300 bg-white'
}`}>
{selectedOption === 'shareAnalytics' && (
<div className="w-2 h-2 bg-white rounded-full"></div>
@@ -54,8 +54,10 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
</div>
</div>
<div>
<h3 className="font-semibold mb-1 text-sm">Share Anonymized Usage Data</h3>
<p className="opacity-70 text-xs leading-relaxed">
<h3 className="font-semibold mb-1 text-sm text-stone-900">
Share Anonymized Usage Data
</h3>
<p className="text-stone-600 text-xs leading-relaxed">
Share anonymized crash reports and usage analytics to help us improve features and
performance. All data is fully anonymized.
</p>
@@ -66,8 +68,8 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
<div
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedOption === 'maximumPrivacy'
? 'border-primary-500 bg-primary-500/20'
: 'border-stone-700 bg-black/50 hover:border-stone-600'
? 'border-primary-500 bg-primary-50'
: 'border-stone-200 bg-white hover:border-stone-300'
}`}
onClick={() => setSelectedOption('maximumPrivacy')}>
<div className="flex items-start space-x-4">
@@ -76,7 +78,7 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedOption === 'maximumPrivacy'
? 'border-primary-500 bg-primary-500'
: 'border-stone-600 bg-black'
: 'border-stone-300 bg-white'
}`}>
{selectedOption === 'maximumPrivacy' && (
<div className="w-2 h-2 bg-white rounded-full"></div>
@@ -84,8 +86,8 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
</div>
</div>
<div>
<h3 className="font-semibold mb-1 text-sm">Don't Collect Anything</h3>
<p className="opacity-70 text-xs leading-relaxed">
<h3 className="font-semibold mb-1 text-sm text-stone-900">Don't Collect Anything</h3>
<p className="text-stone-600 text-xs leading-relaxed">
We won't collect any usage analytics or crash reports. Keep all your data completely
private.
</p>
@@ -94,9 +96,9 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
</div>
</div>
<div className="p-4 bg-sage-500/10 rounded-xl border border-sage-500/30 mb-4">
<div className="p-4 bg-stone-50 rounded-xl border border-stone-200 mb-4">
<div className="flex items-start space-x-2">
<svg className="w-5 h-5 text-sage-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-5 h-5 text-sage-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
@@ -104,8 +106,10 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {
/>
</svg>
<div>
<p className="font-medium text-sm">You can change this setting anytime</p>
<p className="opacity-70 text-xs mt-1">
<p className="font-medium text-sm text-stone-900">
You can change this setting anytime
</p>
<p className="text-stone-600 text-xs mt-1">
Your privacy preferences can be updated in Settings &gt; Privacy &amp; Security
</p>
</div>
+13 -11
View File
@@ -43,7 +43,7 @@ function ConnectOptionRow({
let badge: React.ReactElement;
if (option.comingSoon) {
badge = (
<span className="text-xs opacity-60 bg-stone-700 px-2 py-0.5 rounded">Coming Soon</span>
<span className="text-xs text-stone-500 bg-stone-100 px-2 py-0.5 rounded">Coming Soon</span>
);
} else if (option.skillId) {
const cfg = STATUS_BADGE[connectionStatus];
@@ -60,16 +60,16 @@ function ConnectOptionRow({
<button
disabled={disabled}
onClick={() => onConnect(option)}
className={`w-full flex items-start space-x-3 p-3 bg-black/50 border border-stone-700 rounded-xl transition-all duration-200 text-left ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-stone-600 hover:shadow-medium'
className={`w-full flex items-start space-x-3 p-3 bg-white border border-stone-200 rounded-xl transition-all duration-200 text-left ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-stone-300 hover:shadow-medium'
}`}>
<div className="flex-shrink-0 mt-0.5">{option.icon}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{option.name}</span>
<span className="font-medium text-sm text-stone-900">{option.name}</span>
{badge}
</div>
<p className="opacity-70 text-xs mt-1">{option.description}</p>
<p className="text-stone-600 text-xs mt-1">{option.description}</p>
</div>
</button>
);
@@ -123,10 +123,10 @@ const ConnectStep = ({ onNext }: ConnectStepProps) => {
};
return (
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Connect Accounts</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Connect Accounts</h1>
<p className="text-stone-600 text-sm">
The more accounts you connect, the more powerful the intelligence will be.
</p>
</div>
@@ -137,11 +137,13 @@ const ConnectStep = ({ onNext }: ConnectStepProps) => {
))}
</div>
<div className="mt-4 p-4 bg-sage-500/10 rounded-xl border border-sage-500/30">
<div className="mt-4 p-4 bg-stone-50 rounded-xl border border-stone-200">
<div className="flex items-start space-x-2">
<div>
<p className="font-medium text-sm">Remember everything is private &amp; encrypted!</p>
<p className="opacity-70 text-xs mt-1">
<p className="font-medium text-sm text-stone-900">
Remember everything is private &amp; encrypted!
</p>
<p className="text-stone-600 text-xs mt-1">
All data and credentials are stored locally and follows a strict zero-data retention
policy so you won't have to worry about anything getting leaked.
</p>
@@ -35,17 +35,17 @@ const InviteCodeStep = ({ onNext }: InviteCodeStepProps) => {
};
return (
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-6">
<h1 className="text-xl font-bold mb-2">Have an Invite Code?</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Have an Invite Code?</h1>
<p className="text-stone-600 text-sm">
Enter an invite code from a friend to unlock free credits. You can also skip this step.
</p>
</div>
{success ? (
<div className="text-center py-4">
<div className="w-12 h-12 bg-sage-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
<div className="w-12 h-12 bg-sage-50 rounded-full flex items-center justify-center mx-auto mb-3">
<svg
className="w-6 h-6 text-sage-500"
fill="none"
@@ -70,7 +70,7 @@ const InviteCodeStep = ({ onNext }: InviteCodeStepProps) => {
onChange={e => setCode(e.target.value.toUpperCase())}
onKeyDown={e => e.key === 'Enter' && handleRedeem()}
placeholder="Enter invite code"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-center font-mono text-lg tracking-widest placeholder:text-stone-500 placeholder:tracking-normal placeholder:font-sans placeholder:text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500/50 transition-all"
className="w-full px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl text-center font-mono text-lg tracking-widest text-stone-900 placeholder:text-stone-400 placeholder:tracking-normal placeholder:font-sans placeholder:text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500/50 transition-all"
disabled={isLoading}
/>
{error && <p className="text-coral-500 text-xs mt-2 text-center">{error}</p>}
@@ -86,7 +86,7 @@ const InviteCodeStep = ({ onNext }: InviteCodeStepProps) => {
<button
onClick={onNext}
disabled={isLoading}
className="w-full py-2.5 text-sm font-medium rounded-xl text-stone-400 hover:text-stone-200 transition-colors">
className="w-full py-2.5 text-sm font-medium rounded-xl text-stone-400 hover:text-stone-700 transition-colors">
Skip for now
</button>
</div>
+12 -12
View File
@@ -30,37 +30,37 @@ const LocalAIStep = ({ onNext, onBack: _onBack, onDownloadError }: LocalAIStepPr
}, [onNext, onDownloadError]);
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="flex flex-col items-center mb-5">
<img src="/ollama.svg" alt="Ollama" className="w-16 h-16 mb-3" />
<h1 className="text-xl font-bold mb-2">Run AI Models Locally with Ollama</h1>
<p className="opacity-70 text-sm text-center">
<h1 className="text-xl font-bold mb-2 text-stone-900">Run AI Models Locally with Ollama</h1>
<p className="text-stone-600 text-sm text-center">
OpenHuman will auto-install Ollama for you so that you can download and run AI models
locally on your device.
</p>
</div>
<div className="space-y-2 mb-5">
<div className="rounded-xl border border-sage-500/30 bg-sage-500/10 px-3 py-2">
<p className="text-xs">
<div className="rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<p className="text-xs text-stone-700">
<span className="font-semibold">Complete Privacy</span>
<span className="opacity-80">
<span className="text-stone-600">
&nbsp;- all data stays on your device. Nothing is sent to any third party or cloud.
</span>
</p>
</div>
<div className="rounded-xl border border-sage-500/30 bg-sage-500/10 px-3 py-2">
<p className="text-xs">
<div className="rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
<p className="text-xs text-stone-700">
<span className="font-semibold">Absolutely Free</span>
<span className="opacity-80">
<span className="text-stone-600">
&nbsp;- Ollama and the AI models are open-source. No subscription needed.
</span>
</p>
</div>
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-3 py-2">
<p className="text-xs">
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2">
<p className="text-xs text-stone-700">
<span className="font-semibold">Resource impact</span>
<span className="opacity-80">
<span className="text-stone-600">
&nbsp;- uses some disk space and RAM. We will optimize this for your device.
</span>
</p>
+21 -21
View File
@@ -181,28 +181,28 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
const canContinue = mode === 'generate' ? confirmed : isImportComplete;
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
{mode === 'generate' ? (
<>
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Lastly, your Recovery Phrase</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Lastly, your Recovery Phrase</h1>
<p className="text-stone-600 text-sm">
Write down these {MNEMONIC_GENERATE_WORD_COUNT} words in order and store them
somewhere safe. This phrase encrypts your data and can never be recovered if lost. You
can always back up later.
</p>
</div>
<div className="bg-black/20 rounded-2xl p-4 mb-4">
<div className="bg-stone-50 rounded-2xl p-4 mb-4">
<div className="grid grid-cols-3 gap-2">
{words.map((word, index) => (
<div
key={index}
className="flex items-center gap-2 bg-white/10 rounded-lg px-3 py-2 text-sm">
<span className="text-stone-500 font-mono text-xs w-5 text-right">
className="flex items-center gap-2 bg-white border border-stone-200 rounded-lg px-3 py-2 text-sm">
<span className="text-stone-400 font-mono text-xs w-5 text-right">
{index + 1}.
</span>
<span className="font-mono font-medium">{word}</span>
<span className="font-mono font-medium text-stone-900">{word}</span>
</div>
))}
</div>
@@ -210,18 +210,18 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
<button
onClick={handleCopy}
className="w-full flex items-center justify-center gap-2 border border-stone-600 hover:border-stone-500 font-medium py-2.5 text-sm rounded-xl transition-all duration-200 mb-3">
className="w-full flex items-center justify-center gap-2 border border-stone-200 hover:border-stone-400 text-stone-700 font-medium py-2.5 text-sm rounded-xl transition-all duration-200 mb-3">
{copied ? (
<>
<svg
className="w-4 h-4 text-sage-400"
className="w-4 h-4 text-sage-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span className="text-sage-400">Copied to Clipboard</span>
<span className="text-sage-500">Copied to Clipboard</span>
</>
) : (
<>
@@ -244,7 +244,7 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
<button
onClick={() => setMode('import')}
className="w-full text-center text-sm text-primary-400 hover:text-primary-300 transition-colors mb-3">
className="w-full text-center text-sm text-primary-600 hover:text-primary-500 transition-colors mb-3">
I already have a recovery phrase
</button>
@@ -253,9 +253,9 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
type="checkbox"
checked={confirmed}
onChange={e => setConfirmed(e.target.checked)}
className="mt-0.5 w-4 h-4 rounded border-stone-500 text-primary-500 focus:ring-primary-500"
className="mt-0.5 w-4 h-4 rounded border-stone-300 text-primary-500 focus:ring-primary-500"
/>
<span className="text-sm opacity-80">
<span className="text-sm text-stone-700">
I have saved my recovery phrase in a safe place
</span>
</label>
@@ -263,18 +263,18 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
) : (
<>
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Import Recovery Phrase</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Import Recovery Phrase</h1>
<p className="text-stone-600 text-sm">
Enter your recovery phrase below, or paste the full phrase into any field (12 words
for new backups; 24-word phrases from older versions still work).
</p>
</div>
<div className="bg-black/20 rounded-2xl p-4 mb-4">
<div className="bg-stone-50 rounded-2xl p-4 mb-4">
<div className="grid grid-cols-3 gap-2">
{importWords.map((word, index) => (
<div key={index} className="flex items-center gap-1.5">
<span className="text-stone-500 font-mono text-xs w-5 text-right shrink-0">
<span className="text-stone-400 font-mono text-xs w-5 text-right shrink-0">
{index + 1}.
</span>
<input
@@ -287,12 +287,12 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
onKeyDown={e => handleImportKeyDown(index, e)}
autoComplete="off"
spellCheck={false}
className={`w-full font-mono text-sm font-medium px-2 py-1.5 rounded-lg border bg-white/10 outline-none transition-colors ${
className={`w-full font-mono text-sm font-medium px-2 py-1.5 rounded-lg border bg-white outline-none transition-colors text-stone-900 ${
importValid === false && word.trim()
? 'border-coral-400 focus:border-coral-300'
: importValid === true
? 'border-sage-400 focus:border-sage-300'
: 'border-stone-600 focus:border-primary-400'
: 'border-stone-300 focus:border-primary-400'
}`}
/>
</div>
@@ -301,7 +301,7 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
</div>
{importValid === true && (
<div className="flex items-center gap-2 text-sage-400 text-sm mb-3 justify-center">
<div className="flex items-center gap-2 text-sage-500 text-sm mb-3 justify-center">
<svg
className="w-4 h-4"
fill="none"
@@ -316,7 +316,7 @@ const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => {
<button
onClick={() => setMode('generate')}
className="w-full text-center text-sm text-primary-400 hover:text-primary-300 transition-colors mb-3">
className="w-full text-center text-sm text-primary-600 hover:text-primary-500 transition-colors mb-3">
Generate a new recovery phrase instead
</button>
</>
@@ -26,34 +26,36 @@ const ScreenPermissionsStep = ({ onNext, onBack: _onBack }: ScreenPermissionsSte
const isGranted = accessibilityPermission === 'granted';
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-5">
<h1 className="text-xl font-bold mb-2">Screen & Accessibility Permissions</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">
Screen & Accessibility Permissions
</h1>
<p className="text-stone-600 text-sm">
OpenHuman uses information from your screen to constantly build context about your
workflow and assist you with desktop actions.
</p>
</div>
<div className="space-y-3 mb-5">
<div className="rounded-2xl border border-sage-500/30 bg-sage-500/10 p-3">
<p className="text-sm font-medium mb-1">Complete Privacy</p>
<p className="text-xs opacity-80">
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-3">
<p className="text-sm font-medium mb-1 text-stone-900">Complete Privacy</p>
<p className="text-xs text-stone-600">
All screenshots and accessibility information gets processed locally by your local AI
model. No data is sent to any third party or cloud.
</p>
</div>
<div className="rounded-2xl border border-stone-700 bg-stone-900 p-3">
<p className="text-xs uppercase tracking-wide opacity-60 mb-2">
<div className="rounded-2xl border border-stone-200 bg-white p-3">
<p className="text-xs uppercase tracking-wide text-stone-400 mb-2">
Current permission state
</p>
<div className="flex items-center justify-between">
<span className="text-sm">Accessibility</span>
<span className="text-sm text-stone-900">Accessibility</span>
<span
className={`text-xs px-2 py-1 rounded-md border ${
isGranted
? 'bg-sage-500/20 border-sage-500/30 text-sage-300'
: 'bg-amber-500/20 border-amber-500/30 text-amber-300'
? 'bg-sage-50 border-sage-200 text-sage-600'
: 'bg-amber-50 border-amber-200 text-amber-600'
}`}>
{accessibilityPermission}
</span>
@@ -74,7 +76,7 @@ const ScreenPermissionsStep = ({ onNext, onBack: _onBack }: ScreenPermissionsSte
type="button"
onClick={() => void dispatch(refreshPermissionsWithRestart())}
disabled={isRestartingCore || isLoading}
className="w-full py-2 text-sm font-medium rounded-xl border border-stone-700 hover:border-stone-500 opacity-70 hover:opacity-100 transition-all disabled:opacity-40">
className="w-full py-2 text-sm font-medium rounded-xl border border-stone-200 hover:border-stone-400 text-stone-600 hover:text-stone-900 opacity-70 hover:opacity-100 transition-all disabled:opacity-40">
{isRestartingCore ? 'Restarting core...' : 'Restart & Refresh Permissions'}
</button>
{(lastError || status?.permission_check_process_path) && (
@@ -29,7 +29,7 @@ function statusDotClass(status: SkillConnectionStatus): string {
case 'error':
return 'bg-coral-400';
default:
return 'bg-stone-600';
return 'bg-stone-300';
}
}
@@ -38,13 +38,13 @@ function SkillRow({ skill, onSetup }: { skill: SkillListEntry; onSetup: () => vo
const statusDisplay = STATUS_DISPLAY[connectionStatus] || STATUS_DISPLAY.offline;
return (
<div className="flex items-center gap-3 p-3 rounded-xl border border-stone-700 bg-stone-900 hover:border-stone-600 transition-colors">
<div className="w-6 h-6 flex items-center justify-center text-white opacity-70 flex-shrink-0">
<div className="flex items-center gap-3 p-3 rounded-xl border border-stone-200 bg-white hover:border-stone-300 transition-colors">
<div className="w-6 h-6 flex items-center justify-center text-stone-600 flex-shrink-0">
{skill.icon || <DefaultIcon />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white truncate">{skill.name}</span>
<span className="text-sm font-medium text-stone-900 truncate">{skill.name}</span>
<div
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${statusDotClass(connectionStatus)}`}
/>
@@ -53,7 +53,7 @@ function SkillRow({ skill, onSetup }: { skill: SkillListEntry; onSetup: () => vo
</span>
</div>
{skill.description && (
<p className="text-xs opacity-50 mt-0.5 truncate">{skill.description}</p>
<p className="text-xs text-stone-500 mt-0.5 truncate">{skill.description}</p>
)}
</div>
<SkillActionButton skill={skill} connectionStatus={connectionStatus} onOpenModal={onSetup} />
@@ -124,10 +124,10 @@ const SkillsStep = ({ onNext, onBack: _onBack }: SkillsStepProps) => {
};
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Install Skills</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Install Skills</h1>
<p className="text-stone-600 text-sm">
Skills give OpenHuman richer context & access to your workflow. All data consumed by
skills is saved and processed locally. You can connect as many skills as you want.
</p>
+9 -9
View File
@@ -44,10 +44,10 @@ const ToolsStep = ({ onNext, onBack: _onBack }: ToolsStepProps) => {
.map(([k]) => k);
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-5">
<h1 className="text-xl font-bold mb-2">Enable Tools</h1>
<p className="opacity-70 text-sm">
<h1 className="text-xl font-bold mb-2 text-stone-900">Enable Tools</h1>
<p className="text-stone-600 text-sm">
Choose which capabilities OpenHuman can use on your behalf.
</p>
</div>
@@ -59,10 +59,10 @@ const ToolsStep = ({ onNext, onBack: _onBack }: ToolsStepProps) => {
return (
<div key={category}>
<div className="mb-2">
<h2 className="text-xs font-semibold uppercase tracking-wide opacity-60">
<h2 className="text-xs font-semibold uppercase tracking-wide text-stone-500">
{category}
</h2>
<p className="text-xs opacity-40">{CATEGORY_DESCRIPTIONS[category]}</p>
<p className="text-xs text-stone-400">{CATEGORY_DESCRIPTIONS[category]}</p>
</div>
<div className="space-y-1">
{tools.map(tool => (
@@ -70,14 +70,14 @@ const ToolsStep = ({ onNext, onBack: _onBack }: ToolsStepProps) => {
key={tool.id}
type="button"
onClick={() => toggle(tool.id)}
className="w-full flex items-center justify-between p-2.5 rounded-xl border border-stone-700 bg-stone-900 hover:border-stone-600 transition-colors text-left">
className="w-full flex items-center justify-between p-2.5 rounded-xl border border-stone-200 bg-white hover:border-stone-300 transition-colors text-left">
<div className="min-w-0 flex-1">
<span className="text-sm font-medium">{tool.displayName}</span>
<p className="text-xs opacity-60 mt-0.5">{tool.description}</p>
<span className="text-sm font-medium text-stone-900">{tool.displayName}</span>
<p className="text-xs text-stone-500 mt-0.5">{tool.description}</p>
</div>
<div
className={`ml-3 flex-shrink-0 w-9 h-5 rounded-full transition-colors relative ${
enabled[tool.id] ? 'bg-sage-500' : 'bg-stone-600'
enabled[tool.id] ? 'bg-sage-500' : 'bg-stone-200'
}`}>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
@@ -6,15 +6,15 @@ interface WelcomeStepProps {
const WelcomeStep = ({ onNext }: WelcomeStepProps) => {
return (
<div className="rounded-3xl border border-stone-700 bg-stone-900 p-8 shadow-large animate-fade-up">
<div className="rounded-2xl border border-stone-200 bg-white p-8 shadow-soft animate-fade-up">
<div className="text-center mb-6">
<p className="text-xs font-semibold uppercase tracking-wider text-primary-400 mb-2">
<p className="text-xs font-semibold uppercase tracking-wider text-primary-600 mb-2">
OpenHuman
</p>
<h1 className="text-2xl font-bold font-display mb-3">Welcome</h1>
<p className="opacity-70 text-sm leading-relaxed">
<h1 className="text-2xl font-bold font-display mb-3 text-stone-900">Welcome</h1>
<p className="text-stone-600 text-sm leading-relaxed">
We&apos;ll walk you through a short setup:{' '}
<span className="text-stone-200 font-medium">local AI</span>, permissions, tools, and
<span className="text-stone-700 font-medium">local AI</span>, permissions, tools, and
skills. Nothing is permanentyou can adjust everything later in Settings.
</p>
</div>
+10 -15
View File
@@ -555,22 +555,17 @@ body::before {
}
/* ============================================
Dark Mode Support (Always Enabled)
Light Mode (Default)
============================================ */
:root {
--color-background: theme('colors.slate.950');
--color-surface: theme('colors.slate.900');
--color-surface-elevated: theme('colors.slate.800');
--color-border: theme('colors.slate.700');
--color-border-subtle: theme('colors.slate.800');
--color-background: #f5f5f5;
--color-surface: #ffffff;
--color-surface-elevated: #fafafa;
--color-border: #e5e5e5;
--color-border-subtle: #f5f5f5;
--color-text-primary: theme('colors.slate.50');
--color-text-secondary: theme('colors.slate.300');
--color-text-tertiary: theme('colors.slate.400');
--color-text-muted: theme('colors.slate.500');
}
.glass-surface {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
--color-text-primary: #171717;
--color-text-secondary: #525252;
--color-text-tertiary: #737373;
--color-text-muted: #a3a3a3;
}
+83 -67
View File
@@ -15,90 +15,106 @@ module.exports = {
'serif': ['Newsreader', 'Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'],
},
// Elevated color system - Calm, trustworthy, and sophisticated
// Elevated color system - Clean, light, professional
colors: {
// Canvas - Background layers with subtle warmth
// Neutral - Light theme grayscale (from Figma design tokens)
neutral: {
0: '#FFFFFF', // Base / surface
50: '#FAFAFA',
100: '#F5F5F5', // App background
200: '#E5E5E5',
300: '#D4D4D4',
400: '#A3A3A3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0A0A0A',
},
// Canvas - Background layers (mapped to neutral for compat)
canvas: {
50: '#FAFAF9', // Base background
100: '#F5F5F4', // Secondary background
150: '#EDEDEC', // Tertiary background
200: '#E5E5E3', // Card background
300: '#D4D4D1', // Hover states
50: '#FAFAFA', // Base background
100: '#F5F5F5', // Secondary background
150: '#EFEFEF', // Tertiary background
200: '#E5E5E5', // Card background
300: '#D4D4D4', // Hover states
},
// Primary - Premium ocean blue with depth (optimized for dark backgrounds)
// Primary - Complementary blue from Figma
primary: {
50: '#F0F7FF',
100: '#E0EFFF',
200: '#C7E2FF',
300: '#A5D0FF',
400: '#7AB5FF',
500: '#4A83DD', // Main brand - darker blue for dark backgrounds
600: '#3D6DC4', // Hover state
700: '#345A9F', // Active state
800: '#2D4B7F',
900: '#1E3052',
950: '#0F1A2E',
50: '#EFF6FF',
100: '#DBEAFE',
200: '#BFDBFE',
300: '#93C5FD',
400: '#60A5FA',
500: '#2F6EF4', // Complementary Blue (Figma)
600: '#2563EB', // Gradient end
700: '#1D4ED8', // Active state
800: '#1E40AF',
900: '#1E3A8A',
950: '#172554',
},
// Sage - Success and growth (sophisticated green)
// Sage - Success (from Figma: #34C759)
sage: {
50: '#F7FDF9',
100: '#ECFAEF',
200: '#D4F4DC',
300: '#AEEAC1',
400: '#72D892',
500: '#4DC46F', // Main success - refined green
600: '#3BA858',
700: '#318B48',
800: '#2B6F3C',
900: '#255933',
950: '#14371E',
50: '#F0FDF4',
100: '#DCFCE7',
200: '#BBF7D0',
300: '#86EFAC',
400: '#4ADE80',
500: '#34C759', // Success Green (Figma)
600: '#16A34A',
700: '#15803D',
800: '#166534',
900: '#14532D',
950: '#052E16',
},
// Amber - Attention and caution (warm, muted)
// Amber - Attention and caution (from Figma: #E8A728)
amber: {
50: '#FEFDF8',
100: '#FEF8E7',
200: '#FDEEC8',
300: '#FBDF9A',
400: '#F7C960',
500: '#E8A838', // Main warning - sophisticated amber
600: '#D18B1F',
700: '#B06F1A',
800: '#8D581B',
900: '#744919',
950: '#4A2B0B',
50: '#FFFBEB',
100: '#FEF3C7',
200: '#FDE68A',
300: '#FCD34D',
400: '#FBBF24',
500: '#E8A728', // Alert Orange (Figma)
600: '#D97706',
700: '#B45309',
800: '#92400E',
900: '#78350F',
950: '#451A03',
},
// Coral - Errors and dangers (soft, professional)
// Coral - Errors and dangers (from Figma: #EF4444)
coral: {
50: '#FFF5F5',
100: '#FFEBEB',
200: '#FFD6D6',
300: '#FFB3B3',
400: '#FF8585',
500: '#F56565', // Main error - soft coral red
600: '#E84855',
700: '#D13742',
800: '#B02937',
900: '#922330',
950: '#5C1419',
50: '#FEF2F2',
100: '#FEE2E2',
200: '#FECACA',
300: '#FCA5A5',
400: '#F87171',
500: '#EF4444', // Error Red (Figma)
600: '#DC2626',
700: '#B91C1C',
800: '#991B1B',
900: '#7F1D1D',
950: '#450A0A',
},
// Stone - Neutral scale with subtle warmth
// Stone - Neutral scale (keeping for backward compat, mapped to neutral)
stone: {
50: '#FAFAF9',
100: '#F5F5F4',
200: '#E7E5E4',
300: '#D6D3D1',
400: '#A8A29E',
500: '#78716C',
600: '#57534E',
700: '#44403C',
800: '#292524',
900: '#1C1917',
950: '#0C0A09',
50: '#FAFAFA',
100: '#F5F5F5',
200: '#E5E5E5',
300: '#D4D4D4',
400: '#A3A3A3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0A0A0A',
},
// Slate - Cool grays for data and charts