mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
* feat: add initial project structure and documentation - Introduced the GNU General Public License (GPL) v3 in LICENSE file. - Added MCP configuration in .claude/mcp.json for server integration. - Created architecture documentation in docs/ARCHITECTURE.md outlining the platform's design and components. - Defined MVP specifications in docs/MVP.md for the Telegram-based Agent Assistant. - Established API reference for team management in docs/teams-api-reference.md. - Set up basic HTML structure in public/index.html and added logo image in public/logo.png. * feat: add initial project documentation and HTML structure - Introduced CODE_OF_CONDUCT.md to establish community guidelines and standards for behavior. - Created CONTRIBUTING.md to outline contribution process, development setup, and project conventions. - Added SECURITY.md to define the security policy, supported versions, and reporting procedures for vulnerabilities. - Established basic HTML structure in index.html for the application interface. * chore: remove hello-python skill files - Deleted skill.json and skill.py files for the Hello Python example runtime skill, as they are no longer needed in the project. * feat: port tinyhuman agent runtime from ZeroClaw into Tauri backend Port daemon supervisor, health registry, security (policy, secrets, audit, pairing), agent traits, and config modules from ZeroClaw (MIT) into a new tinyhuman/ module under src-tauri/src/. The daemon auto-starts on desktop and shuts down gracefully on app exit via CancellationToken. - health: global HealthRegistry with component tracking and JSON snapshots - security/policy: SecurityPolicy with command validation, risk levels, rate limiting - security/secrets: ChaCha20-Poly1305 SecretStore with legacy XOR migration - security/audit: AuditLogger with JSON-line events and log rotation - security/pairing: PairingGuard with brute-force protection and SHA-256 hashing - security/traits: Sandbox trait + NoopSandbox - config: minimal DaemonConfig with autonomy, reliability, secrets, audit sub-configs - daemon: supervisor with health state writer emitting Tauri events - agent/traits: Provider, Tool, Memory, Observer, RuntimeAdapter traits + Noop impls - commands/tinyhuman: Tauri commands for health, security policy, encrypt/decrypt - 185 inline unit tests across all modules - README updated with custom inference/tunneling/memory positioning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update README to reflect AlphaHuman Mk1 branding and enhanced description - Changed project title to "AlphaHuman Mk1" for clarity. - Revised project description to emphasize user-friendly AI capabilities and the use of the Neocortex Mk1 model. - Removed outdated sections on custom inference, tunneling, and memory, streamlining the content for better readability. * update readme * Port zeroclaw runtime into tinyhuman * Replace CLI mentions with UI language * Split gateway module into smaller units * Split channels and config schema modules * Fix tinyhuman build, tests, and tunnel integration * feat(tinyhuman): add missing modules and ui-friendly services * refactor: rename tinyhuman to alphahuman * chore: remove bottom text from Welcome component * feat(settings): add tauri command console * feat(daemon): enhance daemon mode handling and integrate rustls with ring feature * feat(settings): implement comprehensive configuration management in TauriCommandsPanel * refactor(TauriCommandsPanel): streamline error handling and enhance async function usage * feat(settings): add skill management functionality to TauriCommandsPanel * style(TauriCommandsPanel): update input styles for improved readability and user experience * feat(settings): add Skills and Agent Chat panels with navigation and integration management * feat(settings): implement browser access management in SkillsPanel and enhance AgentChatPanel with local storage functionality * Implement inference API integration in Conversations component - Added inference API service to handle chat completions and model management. - Updated Conversations component to fetch available models on mount and allow model selection. - Enhanced message sending functionality to utilize the inference API for generating responses. - Introduced state management for loading models and handling errors during API calls. - Refactored optimistic message handling and send status management for improved user experience. --------- Co-authored-by: Steven Enamakel <enamakel@vezures.xyz> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
1532 lines
69 KiB
TypeScript
1532 lines
69 KiB
TypeScript
import {
|
|
ChatBubbleLeftRightIcon,
|
|
CogIcon,
|
|
CpuChipIcon,
|
|
DocumentTextIcon,
|
|
ServerIcon,
|
|
ShieldCheckIcon,
|
|
WrenchScrewdriverIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { useMemo, useState } from 'react';
|
|
|
|
import SettingsHeader from '../components/SettingsHeader';
|
|
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
|
import SectionCard from './components/SectionCard';
|
|
import InputGroup, { Field, CheckboxField } from './components/InputGroup';
|
|
import ValidatedField, { ValidatedSelect } from './components/ValidatedField';
|
|
import ActionPanel, { PrimaryButton } from './components/ActionPanel';
|
|
import DaemonHealthIndicator from '../../daemon/DaemonHealthIndicator';
|
|
import { useDaemonHealth, formatRelativeTime } from '../../../hooks/useDaemonHealth';
|
|
import {
|
|
alphahumanAgentChat,
|
|
alphahumanDecryptSecret,
|
|
alphahumanDoctorModels,
|
|
alphahumanDoctorReport,
|
|
alphahumanEncryptSecret,
|
|
alphahumanGetConfig,
|
|
alphahumanGetIntegrationInfo,
|
|
alphahumanHardwareDiscover,
|
|
alphahumanHardwareIntrospect,
|
|
alphahumanListIntegrations,
|
|
alphahumanMigrateOpenclaw,
|
|
alphahumanModelsRefresh,
|
|
alphahumanServiceInstall,
|
|
alphahumanServiceStatus,
|
|
alphahumanServiceUninstall,
|
|
alphahumanUpdateGatewaySettings,
|
|
alphahumanUpdateMemorySettings,
|
|
alphahumanUpdateModelSettings,
|
|
alphahumanUpdateRuntimeSettings,
|
|
alphahumanUpdateTunnelSettings,
|
|
isTauri,
|
|
runtimeDisableSkill,
|
|
runtimeEnableSkill,
|
|
runtimeIsSkillEnabled,
|
|
runtimeListSkills,
|
|
SkillSnapshot,
|
|
TunnelConfig,
|
|
} from '../../../utils/tauriCommands';
|
|
import SettingsHeader from '../components/SettingsHeader';
|
|
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
|
import ActionPanel, { PrimaryButton } from './components/ActionPanel';
|
|
import InputGroup, { CheckboxField, Field } from './components/InputGroup';
|
|
import SectionCard from './components/SectionCard';
|
|
|
|
const formatJson = (value: unknown) => JSON.stringify(value, null, 2);
|
|
|
|
const TauriCommandsPanel = () => {
|
|
const { navigateBack } = useSettingsNavigation();
|
|
const daemonHealth = useDaemonHealth();
|
|
|
|
// View mode removed - always show all sections
|
|
const [expandedSections] = useState<Set<string>>(
|
|
new Set([
|
|
'system-configuration',
|
|
'runtime-execution',
|
|
'security-data',
|
|
'network-infrastructure',
|
|
'development-operations',
|
|
'interactive-tools',
|
|
])
|
|
);
|
|
|
|
// Output and error states
|
|
const [output, setOutput] = useState<string>('');
|
|
const [error, setError] = useState<string>('');
|
|
|
|
// Form states (preserved from original)
|
|
const [providerOverride, setProviderOverride] = useState<string>('');
|
|
const [integrationName, setIntegrationName] = useState<string>('');
|
|
const [hardwarePath, setHardwarePath] = useState<string>('');
|
|
const [migrationSource, setMigrationSource] = useState<string>('');
|
|
const [encryptInput, setEncryptInput] = useState<string>('');
|
|
const [decryptInput, setDecryptInput] = useState<string>('');
|
|
const [apiKey, setApiKey] = useState<string>('');
|
|
const [apiUrl, setApiUrl] = useState<string>('');
|
|
const [defaultProvider, setDefaultProvider] = useState<string>('');
|
|
const [defaultModel, setDefaultModel] = useState<string>('');
|
|
const [defaultTemp, setDefaultTemp] = useState<string>('0.7');
|
|
const [tunnelProvider, setTunnelProvider] = useState<string>('none');
|
|
const [cloudflareToken, setCloudflareToken] = useState<string>('');
|
|
const [ngrokToken, setNgrokToken] = useState<string>('');
|
|
const [tailscaleHostname, setTailscaleHostname] = useState<string>('');
|
|
const [customCommand, setCustomCommand] = useState<string>('');
|
|
const [gatewayHost, setGatewayHost] = useState<string>('127.0.0.1');
|
|
const [gatewayPort, setGatewayPort] = useState<string>('3000');
|
|
const [gatewayPairing, setGatewayPairing] = useState<boolean>(true);
|
|
const [gatewayPublic, setGatewayPublic] = useState<boolean>(false);
|
|
const [memoryBackend, setMemoryBackend] = useState<string>('sqlite');
|
|
const [memoryAutoSave, setMemoryAutoSave] = useState<boolean>(true);
|
|
const [embeddingProvider, setEmbeddingProvider] = useState<string>('none');
|
|
const [embeddingModel, setEmbeddingModel] = useState<string>('text-embedding-3-small');
|
|
const [embeddingDims, setEmbeddingDims] = useState<string>('1536');
|
|
const [runtimeKind, setRuntimeKind] = useState<string>('native');
|
|
const [reasoningEnabled, setReasoningEnabled] = useState<boolean>(false);
|
|
const [skills, setSkills] = useState<Array<{ snapshot: SkillSnapshot; enabled: boolean }>>([]);
|
|
const [skillsLoading, setSkillsLoading] = useState<boolean>(false);
|
|
const [chatInput, setChatInput] = useState<string>('');
|
|
const [chatProvider, setChatProvider] = useState<string>('');
|
|
const [chatModel, setChatModel] = useState<string>('');
|
|
const [chatTemperature, setChatTemperature] = useState<string>('0.7');
|
|
const [chatLog, setChatLog] = useState<Array<{ role: 'user' | 'agent'; text: string }>>([]);
|
|
|
|
// Loading states
|
|
const [operationLoading, setOperationLoading] = useState<string>('');
|
|
|
|
// Enhanced System Configuration state management
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
const [originalConfig, setOriginalConfig] = useState<Record<string, any>>({});
|
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
|
const [lastSaveTime, setLastSaveTime] = useState<Date | null>(null);
|
|
const [validationLoading, setValidationLoading] = useState(false);
|
|
const [configLoaded, setConfigLoaded] = useState(false);
|
|
|
|
const tauriAvailable = useMemo(() => isTauri(), []);
|
|
const parseOptionalNumber = (value: string): number | null => {
|
|
if (!value.trim()) {
|
|
return null;
|
|
}
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
// Provider configurations for smart defaults
|
|
const providerConfigs = useMemo(() => ({
|
|
openai: {
|
|
defaultUrl: 'https://api.openai.com/v1',
|
|
keyPattern: /^sk-[a-zA-Z0-9]{32,}$/,
|
|
models: ['gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-3.5-turbo']
|
|
},
|
|
anthropic: {
|
|
defaultUrl: 'https://api.anthropic.com',
|
|
keyPattern: /^sk-ant-[a-zA-Z0-9_-]{32,}$/,
|
|
models: ['claude-sonnet-4-5-20250929', 'claude-opus-4-6', 'claude-haiku-3-5']
|
|
},
|
|
ollama: {
|
|
defaultUrl: 'http://localhost:11434',
|
|
keyPattern: null, // No API key required
|
|
models: ['llama3', 'llama3:8b', 'codellama', 'mistral', 'phi3']
|
|
},
|
|
groq: {
|
|
defaultUrl: 'https://api.groq.com/openai/v1',
|
|
keyPattern: /^gsk_[a-zA-Z0-9]{32,}$/,
|
|
models: ['llama-3.1-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768']
|
|
},
|
|
cohere: {
|
|
defaultUrl: 'https://api.cohere.ai/v1',
|
|
keyPattern: /^[a-zA-Z0-9]{32,}$/,
|
|
models: ['command-r', 'command-r-plus', 'command-light']
|
|
}
|
|
}), []);
|
|
|
|
// Validation functions
|
|
const validateApiKey = useCallback((key: string, provider: string): string | null => {
|
|
if (!provider || provider === 'none') return null;
|
|
if (!key.trim() && provider && provider !== 'none' && provider !== 'ollama') {
|
|
return 'API key is required for this provider';
|
|
}
|
|
if (key.trim() && provider && providerConfigs[provider as keyof typeof providerConfigs]?.keyPattern) {
|
|
const pattern = providerConfigs[provider as keyof typeof providerConfigs].keyPattern;
|
|
if (pattern && !pattern.test(key)) {
|
|
return `Invalid API key format for ${provider}`;
|
|
}
|
|
}
|
|
return null;
|
|
}, [providerConfigs]);
|
|
|
|
const validateApiUrl = useCallback((url: string): string | null => {
|
|
if (!url.trim()) return null;
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
return 'URL must use HTTP or HTTPS protocol';
|
|
}
|
|
if (parsedUrl.protocol === 'http:' && !parsedUrl.hostname.includes('localhost') && !parsedUrl.hostname.includes('127.0.0.1')) {
|
|
return 'HTTP URLs are only allowed for localhost';
|
|
}
|
|
return null;
|
|
} catch {
|
|
return 'Invalid URL format';
|
|
}
|
|
}, []);
|
|
|
|
const validateProvider = useCallback((provider: string): string | null => {
|
|
if (!provider.trim()) return null;
|
|
const supportedProviders = Object.keys(providerConfigs);
|
|
if (!supportedProviders.includes(provider)) {
|
|
return `Unsupported provider. Supported: ${supportedProviders.join(', ')}`;
|
|
}
|
|
return null;
|
|
}, [providerConfigs]);
|
|
|
|
const validateModel = useCallback((model: string, provider: string): string | null => {
|
|
if (!model.trim() || !provider.trim()) return null;
|
|
const config = providerConfigs[provider as keyof typeof providerConfigs];
|
|
if (config && !config.models.includes(model)) {
|
|
return `Model not available for ${provider}. Try: ${config.models.slice(0, 3).join(', ')}`;
|
|
}
|
|
return null;
|
|
}, [providerConfigs]);
|
|
|
|
const validateTemperature = useCallback((temp: string): string | null => {
|
|
if (!temp.trim()) return null;
|
|
const value = parseFloat(temp);
|
|
if (isNaN(value)) return 'Temperature must be a number';
|
|
if (value < 0 || value > 2) return 'Temperature must be between 0.0 and 2.0';
|
|
return null;
|
|
}, []);
|
|
|
|
// Real-time validation
|
|
const performValidation = useCallback(() => {
|
|
const errors: Record<string, string> = {};
|
|
|
|
const apiKeyError = validateApiKey(apiKey, defaultProvider);
|
|
if (apiKeyError) errors.apiKey = apiKeyError;
|
|
|
|
const apiUrlError = validateApiUrl(apiUrl);
|
|
if (apiUrlError) errors.apiUrl = apiUrlError;
|
|
|
|
const providerError = validateProvider(defaultProvider);
|
|
if (providerError) errors.defaultProvider = providerError;
|
|
|
|
const modelError = validateModel(defaultModel, defaultProvider);
|
|
if (modelError) errors.defaultModel = modelError;
|
|
|
|
const tempError = validateTemperature(defaultTemp);
|
|
if (tempError) errors.defaultTemp = tempError;
|
|
|
|
setFieldErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
}, [apiKey, apiUrl, defaultProvider, defaultModel, defaultTemp, validateApiKey, validateApiUrl, validateProvider, validateModel, validateTemperature]);
|
|
|
|
// Format timestamp for display
|
|
const formatTime = useCallback((date: Date): string => {
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
}, []);
|
|
|
|
// Track changes
|
|
useEffect(() => {
|
|
if (!configLoaded) return;
|
|
|
|
const currentConfig = {
|
|
api_key: apiKey,
|
|
api_url: apiUrl,
|
|
default_provider: defaultProvider,
|
|
default_model: defaultModel,
|
|
default_temperature: defaultTemp,
|
|
};
|
|
|
|
const hasChanges = JSON.stringify(currentConfig) !== JSON.stringify(originalConfig);
|
|
setHasUnsavedChanges(hasChanges);
|
|
|
|
// Perform validation on changes
|
|
performValidation();
|
|
}, [apiKey, apiUrl, defaultProvider, defaultModel, defaultTemp, originalConfig, configLoaded, performValidation]);
|
|
|
|
// Auto-populate URL based on provider
|
|
useEffect(() => {
|
|
if (defaultProvider && !apiUrl && providerConfigs[defaultProvider as keyof typeof providerConfigs]) {
|
|
const config = providerConfigs[defaultProvider as keyof typeof providerConfigs];
|
|
setApiUrl(config.defaultUrl);
|
|
}
|
|
}, [defaultProvider, apiUrl, providerConfigs]);
|
|
|
|
const run = async (fn: () => Promise<unknown>, operationName?: string) => {
|
|
setError('');
|
|
if (operationName) setOperationLoading(operationName);
|
|
try {
|
|
const result = await fn();
|
|
setOutput(formatJson(result));
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setError(message);
|
|
} finally {
|
|
setOperationLoading('');
|
|
}
|
|
};
|
|
|
|
const runWithResult = async <T,>(
|
|
fn: () => Promise<T>,
|
|
operationName?: string
|
|
): Promise<T | null> => {
|
|
setError('');
|
|
if (operationName) setOperationLoading(operationName);
|
|
try {
|
|
const result = await fn();
|
|
setOutput(formatJson(result));
|
|
return result;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setError(message);
|
|
return null;
|
|
} finally {
|
|
setOperationLoading('');
|
|
}
|
|
};
|
|
|
|
const loadConfig = async () => {
|
|
const response = await runWithResult(() => alphahumanGetConfig(), 'loadConfig');
|
|
if (!response) {
|
|
setError('Failed to load configuration');
|
|
return;
|
|
}
|
|
try {
|
|
const snapshot = response.result;
|
|
const config = snapshot.config as Record<string, unknown>;
|
|
|
|
// Extract model configuration
|
|
const modelApiKey = (config.api_key as string) ?? '';
|
|
const modelApiUrl = (config.api_url as string) ?? '';
|
|
const modelProvider = (config.default_provider as string) ?? '';
|
|
const modelModel = (config.default_model as string) ?? '';
|
|
const modelTemp = String((config.default_temperature as number) ?? 0.7);
|
|
|
|
// Set state
|
|
setApiKey(modelApiKey);
|
|
setApiUrl(modelApiUrl);
|
|
setDefaultProvider(modelProvider);
|
|
setDefaultModel(modelModel);
|
|
setDefaultTemp(modelTemp);
|
|
|
|
// Store original config for change tracking
|
|
const systemConfig = {
|
|
api_key: modelApiKey,
|
|
api_url: modelApiUrl,
|
|
default_provider: modelProvider,
|
|
default_model: modelModel,
|
|
default_temperature: modelTemp,
|
|
};
|
|
setOriginalConfig(systemConfig);
|
|
setConfigLoaded(true);
|
|
|
|
// Load other configuration sections
|
|
const tunnel = (config.tunnel as Record<string, unknown>) ?? {};
|
|
setTunnelProvider((tunnel.provider as string) ?? 'none');
|
|
setCloudflareToken(((tunnel.cloudflare as Record<string, unknown>)?.token as string) ?? '');
|
|
setNgrokToken(((tunnel.ngrok as Record<string, unknown>)?.auth_token as string) ?? '');
|
|
setTailscaleHostname(
|
|
((tunnel.tailscale as Record<string, unknown>)?.hostname as string) ?? ''
|
|
);
|
|
setCustomCommand(((tunnel.custom as Record<string, unknown>)?.start_command as string) ?? '');
|
|
|
|
const gateway = (config.gateway as Record<string, unknown>) ?? {};
|
|
setGatewayHost((gateway.host as string) ?? '127.0.0.1');
|
|
setGatewayPort(String((gateway.port as number) ?? 3000));
|
|
setGatewayPairing((gateway.require_pairing as boolean) ?? true);
|
|
setGatewayPublic((gateway.allow_public_bind as boolean) ?? false);
|
|
|
|
const memory = (config.memory as Record<string, unknown>) ?? {};
|
|
setMemoryBackend((memory.backend as string) ?? 'sqlite');
|
|
setMemoryAutoSave((memory.auto_save as boolean) ?? true);
|
|
setEmbeddingProvider((memory.embedding_provider as string) ?? 'none');
|
|
setEmbeddingModel((memory.embedding_model as string) ?? 'text-embedding-3-small');
|
|
setEmbeddingDims(String((memory.embedding_dimensions as number) ?? 1536));
|
|
|
|
const runtime = (config.runtime as Record<string, unknown>) ?? {};
|
|
setRuntimeKind((runtime.kind as string) ?? 'native');
|
|
setReasoningEnabled((runtime.reasoning_enabled as boolean) ?? false);
|
|
|
|
// Clear any previous errors
|
|
setFieldErrors({});
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setError(`Failed to parse configuration: ${message}`);
|
|
}
|
|
};
|
|
|
|
const buildTunnelConfig = (): TunnelConfig => {
|
|
if (tunnelProvider === 'cloudflare') {
|
|
return { provider: 'cloudflare', cloudflare: { token: cloudflareToken } };
|
|
}
|
|
if (tunnelProvider === 'ngrok') {
|
|
return { provider: 'ngrok', ngrok: { auth_token: ngrokToken } };
|
|
}
|
|
if (tunnelProvider === 'tailscale') {
|
|
return { provider: 'tailscale', tailscale: { hostname: tailscaleHostname || null } };
|
|
}
|
|
if (tunnelProvider === 'custom') {
|
|
return { provider: 'custom', custom: { start_command: customCommand } };
|
|
}
|
|
return { provider: 'none' };
|
|
};
|
|
|
|
const saveModelSettings = async () => {
|
|
// Pre-save validation
|
|
if (!performValidation()) {
|
|
setError('Please fix validation errors before saving');
|
|
return;
|
|
}
|
|
|
|
setError('');
|
|
setOperationLoading('saveModelSettings');
|
|
|
|
try {
|
|
const result = await alphahumanUpdateModelSettings({
|
|
api_key: apiKey.trim() ? apiKey : null,
|
|
api_url: apiUrl.trim() ? apiUrl : null,
|
|
default_provider: defaultProvider.trim() ? defaultProvider : null,
|
|
default_model: defaultModel.trim() ? defaultModel : null,
|
|
default_temperature: parseOptionalNumber(defaultTemp),
|
|
});
|
|
|
|
setOutput(formatJson(result));
|
|
|
|
// Success feedback
|
|
const now = new Date();
|
|
setLastSaveTime(now);
|
|
setHasUnsavedChanges(false);
|
|
|
|
// Update original config
|
|
setOriginalConfig({
|
|
api_key: apiKey,
|
|
api_url: apiUrl,
|
|
default_provider: defaultProvider,
|
|
default_model: defaultModel,
|
|
default_temperature: defaultTemp,
|
|
});
|
|
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
if (message.includes('API key')) {
|
|
setFieldErrors(prev => ({ ...prev, apiKey: 'Invalid API key or authentication failed' }));
|
|
} else if (message.includes('provider')) {
|
|
setFieldErrors(prev => ({ ...prev, defaultProvider: 'Provider not supported or misconfigured' }));
|
|
} else if (message.includes('model')) {
|
|
setFieldErrors(prev => ({ ...prev, defaultModel: 'Model not available for this provider' }));
|
|
}
|
|
setError(message);
|
|
} finally {
|
|
setOperationLoading('');
|
|
}
|
|
};
|
|
|
|
const testConnection = async () => {
|
|
if (!performValidation()) {
|
|
setError('Please fix validation errors before testing connection');
|
|
return;
|
|
}
|
|
|
|
if (!defaultProvider || (!apiKey && defaultProvider !== 'ollama')) {
|
|
setError('Provider and API key are required to test connection (except for Ollama)');
|
|
return;
|
|
}
|
|
|
|
// Check if running in Tauri environment
|
|
if (!isTauri()) {
|
|
setError('Test Connection is only available in the desktop application');
|
|
return;
|
|
}
|
|
|
|
setValidationLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Add timeout to prevent infinite loading
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Connection test timed out after 30 seconds')), 30000)
|
|
);
|
|
|
|
// Test connection by attempting to refresh models with current settings
|
|
const result = await Promise.race([
|
|
alphahumanModelsRefresh(defaultProvider, false),
|
|
timeoutPromise
|
|
]);
|
|
|
|
setOutput(formatJson(result));
|
|
|
|
// If we get here, connection is successful
|
|
const successMessage = `Connection test successful for ${defaultProvider}`;
|
|
setOutput(prev => prev + '\n\n' + successMessage);
|
|
|
|
// Clear any previous connection errors
|
|
setFieldErrors(prev => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors.apiKey;
|
|
delete newErrors.defaultProvider;
|
|
return newErrors;
|
|
});
|
|
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
console.error('Test connection error:', err);
|
|
|
|
// Set provider-specific errors
|
|
if (message.includes('authentication') || message.includes('401')) {
|
|
setFieldErrors(prev => ({ ...prev, apiKey: 'Authentication failed - check API key' }));
|
|
} else if (message.includes('provider') || message.includes('404')) {
|
|
setFieldErrors(prev => ({ ...prev, defaultProvider: 'Provider not found or unavailable' }));
|
|
} else if (message.includes('network') || message.includes('timeout')) {
|
|
setFieldErrors(prev => ({ ...prev, apiUrl: 'Network error - check API URL and connectivity' }));
|
|
} else if (message.includes('Not running in Tauri')) {
|
|
setFieldErrors(prev => ({ ...prev, defaultProvider: 'Desktop application required for testing' }));
|
|
}
|
|
|
|
setError(`Connection test failed: ${message}`);
|
|
} finally {
|
|
setValidationLoading(false);
|
|
}
|
|
};
|
|
|
|
const saveTunnelSettings = () =>
|
|
run(() => alphahumanUpdateTunnelSettings(buildTunnelConfig()), 'saveTunnelSettings');
|
|
|
|
const saveGatewaySettings = () =>
|
|
run(
|
|
() =>
|
|
alphahumanUpdateGatewaySettings({
|
|
host: gatewayHost.trim() ? gatewayHost : null,
|
|
port: parseOptionalNumber(gatewayPort),
|
|
require_pairing: gatewayPairing,
|
|
allow_public_bind: gatewayPublic,
|
|
}),
|
|
'saveGatewaySettings'
|
|
);
|
|
|
|
const saveMemorySettings = () =>
|
|
run(
|
|
() =>
|
|
alphahumanUpdateMemorySettings({
|
|
backend: memoryBackend.trim() ? memoryBackend : null,
|
|
auto_save: memoryAutoSave,
|
|
embedding_provider: embeddingProvider.trim() ? embeddingProvider : null,
|
|
embedding_model: embeddingModel.trim() ? embeddingModel : null,
|
|
embedding_dimensions: parseOptionalNumber(embeddingDims),
|
|
}),
|
|
'saveMemorySettings'
|
|
);
|
|
|
|
const saveRuntimeSettings = () =>
|
|
run(
|
|
() =>
|
|
alphahumanUpdateRuntimeSettings({
|
|
kind: runtimeKind.trim() ? runtimeKind : null,
|
|
reasoning_enabled: reasoningEnabled,
|
|
}),
|
|
'saveRuntimeSettings'
|
|
);
|
|
|
|
const loadSkills = async () => {
|
|
setSkillsLoading(true);
|
|
try {
|
|
const snapshots = await runtimeListSkills();
|
|
const enriched = await Promise.all(
|
|
snapshots.map(async snapshot => ({
|
|
snapshot,
|
|
enabled: await runtimeIsSkillEnabled(snapshot.skill_id),
|
|
}))
|
|
);
|
|
setSkills(enriched);
|
|
setOutput(formatJson(enriched));
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setError(message);
|
|
} finally {
|
|
setSkillsLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleSkill = async (skillId: string, nextEnabled: boolean) => {
|
|
if (nextEnabled) {
|
|
await run(() => runtimeEnableSkill(skillId), 'enableSkill');
|
|
} else {
|
|
await run(() => runtimeDisableSkill(skillId), 'disableSkill');
|
|
}
|
|
setSkills(prev =>
|
|
prev.map(item =>
|
|
item.snapshot.skill_id === skillId ? { ...item, enabled: nextEnabled } : item
|
|
)
|
|
);
|
|
};
|
|
|
|
const sendChat = async () => {
|
|
if (!chatInput.trim()) {
|
|
return;
|
|
}
|
|
const userMessage = chatInput.trim();
|
|
setChatLog(prev => [...prev, { role: 'user', text: userMessage }]);
|
|
setChatInput('');
|
|
const response = await runWithResult(
|
|
() =>
|
|
alphahumanAgentChat(
|
|
userMessage,
|
|
chatProvider.trim() ? chatProvider : undefined,
|
|
chatModel.trim() ? chatModel : undefined,
|
|
parseOptionalNumber(chatTemperature) ?? undefined
|
|
),
|
|
'sendChat'
|
|
);
|
|
if (response) {
|
|
setChatLog(prev => [...prev, { role: 'agent', text: response.result }]);
|
|
}
|
|
};
|
|
|
|
// Always show all sections
|
|
const isSectionVisible = () => {
|
|
return true; // Always show all sections
|
|
};
|
|
|
|
const isCollapsed = (sectionId: string) => {
|
|
return !expandedSections.has(sectionId);
|
|
};
|
|
|
|
// Helper to check if a section is collapsed (currently unused but kept for future expansion)
|
|
// const toggleSection = (sectionId: string) => {
|
|
// const newExpanded = new Set(expandedSections);
|
|
// if (newExpanded.has(sectionId)) {
|
|
// newExpanded.delete(sectionId);
|
|
// } else {
|
|
// newExpanded.add(sectionId);
|
|
// }
|
|
// setExpandedSections(newExpanded);
|
|
// };
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<SettingsHeader title="Tauri Command Console" showBackButton={true} onBack={navigateBack} />
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 pb-12 space-y-10">
|
|
{!tauriAvailable && (
|
|
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
|
|
Tauri runtime not detected. Commands will fail in browser mode.
|
|
</div>
|
|
)}
|
|
|
|
{operationLoading && (
|
|
<div className="flex items-center justify-end">
|
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
|
<div className="h-4 w-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
|
{operationLoading}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Critical Path - Always visible in grid on desktop */}
|
|
<div className="grid gap-8 lg:grid-cols-2">
|
|
{/* Category 1: System Configuration */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="System Configuration"
|
|
priority="infrastructure"
|
|
icon={<CogIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('system-configuration')}
|
|
hasChanges={hasUnsavedChanges}
|
|
loading={operationLoading === 'loadConfig' || operationLoading === 'saveModelSettings' || validationLoading}
|
|
>
|
|
<InputGroup
|
|
title="Model API Keys"
|
|
description="Configure your AI model provider settings with real-time validation"
|
|
>
|
|
<ValidatedSelect
|
|
label="Default Provider"
|
|
value={defaultProvider}
|
|
onChange={(value) => {
|
|
setDefaultProvider(value);
|
|
// Auto-populate URL when provider changes
|
|
if (value && providerConfigs[value as keyof typeof providerConfigs]) {
|
|
const config = providerConfigs[value as keyof typeof providerConfigs];
|
|
if (!apiUrl || Object.values(providerConfigs).some(c => c.defaultUrl === apiUrl)) {
|
|
setApiUrl(config.defaultUrl);
|
|
}
|
|
}
|
|
}}
|
|
options={[
|
|
{ value: '', label: 'Select provider...', description: 'Choose your AI service provider' },
|
|
{ value: 'openai', label: 'OpenAI', description: 'GPT models with high performance' },
|
|
{ value: 'anthropic', label: 'Anthropic', description: 'Claude models with safety focus' },
|
|
{ value: 'ollama', label: 'Ollama', description: 'Local models, no API key needed' },
|
|
{ value: 'groq', label: 'Groq', description: 'Fast inference with LPU acceleration' },
|
|
{ value: 'cohere', label: 'Cohere', description: 'Enterprise-grade language models' },
|
|
]}
|
|
error={fieldErrors.defaultProvider}
|
|
required={true}
|
|
helpText="Primary AI provider for agent operations. Each provider offers different models with unique capabilities and pricing."
|
|
/>
|
|
|
|
<ValidatedField
|
|
label="API Key"
|
|
value={apiKey}
|
|
onChange={setApiKey}
|
|
error={fieldErrors.apiKey}
|
|
required={!!defaultProvider && defaultProvider !== 'ollama'}
|
|
type="password"
|
|
placeholder={
|
|
defaultProvider === 'openai' ? 'sk-...' :
|
|
defaultProvider === 'anthropic' ? 'sk-ant-...' :
|
|
defaultProvider === 'groq' ? 'gsk_...' :
|
|
defaultProvider === 'ollama' ? 'Not required for Ollama' :
|
|
'Enter your API key...'
|
|
}
|
|
helpText={
|
|
defaultProvider === 'ollama'
|
|
? 'API key not required for Ollama (local models)'
|
|
: 'Enter your AI provider\'s API key for authentication. Keep this secure and never share it publicly.'
|
|
}
|
|
validation={
|
|
!apiKey ? 'none' :
|
|
fieldErrors.apiKey ? 'invalid' :
|
|
defaultProvider && validateApiKey(apiKey, defaultProvider) === null ? 'valid' : 'none'
|
|
}
|
|
disabled={defaultProvider === 'ollama'}
|
|
/>
|
|
|
|
<ValidatedField
|
|
label="API Base URL"
|
|
value={apiUrl}
|
|
onChange={setApiUrl}
|
|
error={fieldErrors.apiUrl}
|
|
type="url"
|
|
placeholder="https://api.openai.com/v1"
|
|
helpText="Custom API endpoint for your AI provider. Auto-populated when you select a provider. Change only for custom deployments or proxy servers."
|
|
validation={
|
|
!apiUrl ? 'none' :
|
|
fieldErrors.apiUrl ? 'invalid' :
|
|
validateApiUrl(apiUrl) === null ? 'valid' : 'none'
|
|
}
|
|
/>
|
|
|
|
<ValidatedSelect
|
|
label="Default Model"
|
|
value={defaultModel}
|
|
onChange={setDefaultModel}
|
|
options={[
|
|
{ value: '', label: 'Select model...', description: 'Choose specific model for this provider' },
|
|
...(defaultProvider && providerConfigs[defaultProvider as keyof typeof providerConfigs]?.models.map(model => ({
|
|
value: model,
|
|
label: model,
|
|
description:
|
|
model.includes('gpt-4') ? 'Advanced reasoning, higher cost' :
|
|
model.includes('gpt-3.5') ? 'Fast and economical' :
|
|
model.includes('claude') ? 'Excellent analysis and safety' :
|
|
model.includes('llama') ? 'Open source, good performance' :
|
|
model.includes('mixtral') ? 'Mixture of experts model' :
|
|
'High-quality language model'
|
|
})) || [])
|
|
]}
|
|
error={fieldErrors.defaultModel}
|
|
helpText="Primary language model for agent interactions. Available models are filtered based on your selected provider."
|
|
disabled={!defaultProvider}
|
|
validation={
|
|
!defaultModel ? 'none' :
|
|
fieldErrors.defaultModel ? 'invalid' :
|
|
defaultProvider && validateModel(defaultModel, defaultProvider) === null ? 'valid' : 'none'
|
|
}
|
|
/>
|
|
|
|
<ValidatedField
|
|
label="Temperature"
|
|
value={defaultTemp}
|
|
onChange={setDefaultTemp}
|
|
error={fieldErrors.defaultTemp}
|
|
type="number"
|
|
placeholder="0.7"
|
|
helpText="Controls randomness in AI responses (0.0-2.0). Lower values (0.1-0.3) for factual tasks, medium (0.5-0.8) for balanced responses, higher (0.8-1.5) for creative tasks."
|
|
validation={
|
|
!defaultTemp ? 'none' :
|
|
fieldErrors.defaultTemp ? 'invalid' :
|
|
validateTemperature(defaultTemp) === null ? 'valid' : 'none'
|
|
}
|
|
/>
|
|
</InputGroup>
|
|
|
|
<ActionPanel
|
|
hasChanges={hasUnsavedChanges}
|
|
success={lastSaveTime ? `Settings saved at ${formatTime(lastSaveTime)}` : false}
|
|
error={Object.values(fieldErrors).find(Boolean)}
|
|
>
|
|
<PrimaryButton
|
|
onClick={loadConfig}
|
|
loading={operationLoading === 'loadConfig'}
|
|
variant="outline"
|
|
>
|
|
Load Config
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={testConnection}
|
|
loading={validationLoading}
|
|
disabled={!defaultProvider || (!apiKey && defaultProvider !== 'ollama')}
|
|
variant="outline"
|
|
>
|
|
Test Connection
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={saveModelSettings}
|
|
loading={operationLoading === 'saveModelSettings'}
|
|
disabled={Object.keys(fieldErrors).length > 0 || !hasUnsavedChanges}
|
|
>
|
|
Save Settings
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{/* Category 2: Runtime & Execution */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="Runtime & Execution"
|
|
priority="infrastructure"
|
|
icon={<CpuChipIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('runtime-execution')}
|
|
hasChanges={false}
|
|
loading={operationLoading === 'saveRuntimeSettings' || skillsLoading}>
|
|
<InputGroup
|
|
title="Runtime Settings"
|
|
description="Configure V8 runtime and skill execution">
|
|
<Field
|
|
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"
|
|
placeholder="native"
|
|
value={runtimeKind}
|
|
onChange={event => setRuntimeKind(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<CheckboxField
|
|
label="Reasoning enabled"
|
|
helpText="Activates advanced step-by-step reasoning capabilities in AI responses. Improves accuracy for complex tasks but increases response time and token usage. Recommended for analytical and problem-solving tasks."
|
|
checked={reasoningEnabled}
|
|
onChange={setReasoningEnabled}
|
|
/>
|
|
</InputGroup>
|
|
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={saveRuntimeSettings}
|
|
loading={operationLoading === 'saveRuntimeSettings'}>
|
|
Save Runtime Settings
|
|
</PrimaryButton>
|
|
<PrimaryButton onClick={loadSkills} loading={skillsLoading} variant="outline">
|
|
{skillsLoading ? 'Loading Skills…' : 'Load Skills'}
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
|
|
{skills.length > 0 && (
|
|
<div className="space-y-4">
|
|
<h5 className="text-sm font-medium text-gray-300">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-xs text-gray-400 truncate">
|
|
{item.snapshot.skill_id}
|
|
</div>
|
|
</div>
|
|
<CheckboxField
|
|
label={item.enabled ? 'Active' : 'Inactive'}
|
|
checked={item.enabled}
|
|
onChange={checked => toggleSkill(item.snapshot.skill_id, checked)}
|
|
className="text-xs ml-4 flex-shrink-0"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<InputGroup title="Daemon Service Management">
|
|
<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 gap-3">
|
|
<DaemonHealthIndicator size="md" />
|
|
<div>
|
|
<div className="text-white font-medium">Daemon Status: {daemonHealth.status}</div>
|
|
<div className="text-xs text-gray-400">
|
|
Last update: {daemonHealth.lastUpdate ? formatRelativeTime(daemonHealth.lastUpdate) : 'Never'}
|
|
</div>
|
|
{daemonHealth.healthSnapshot && (
|
|
<div className="text-xs text-gray-500">
|
|
PID: {daemonHealth.healthSnapshot.pid} • Uptime: {daemonHealth.uptimeText}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{daemonHealth.status === 'error' && (
|
|
<PrimaryButton
|
|
onClick={() => daemonHealth.restartDaemon()}
|
|
variant="outline"
|
|
loading={daemonHealth.isRecovering}
|
|
>
|
|
Restart
|
|
</PrimaryButton>
|
|
)}
|
|
</div>
|
|
|
|
{/* Component Health */}
|
|
{daemonHealth.componentCount > 0 && (
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{Object.entries(daemonHealth.components).map(([name, health]) => (
|
|
<div key={name} className="flex items-center gap-2 p-2 rounded bg-stone-800/40">
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
health.status === 'ok' ? 'bg-green-500' :
|
|
health.status === 'starting' ? 'bg-yellow-500' : 'bg-red-500'
|
|
}`} />
|
|
<span className="capitalize text-gray-300">{name}</span>
|
|
{health.restart_count > 0 && (
|
|
<span className="text-xs text-yellow-400">({health.restart_count})</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Service Controls */}
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={() => daemonHealth.startDaemon()}
|
|
loading={operationLoading === 'serviceStart'}
|
|
disabled={daemonHealth.status === 'running'}
|
|
>
|
|
Start
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => daemonHealth.stopDaemon()}
|
|
loading={operationLoading === 'serviceStop'}
|
|
disabled={daemonHealth.status === 'disconnected'}
|
|
variant="outline"
|
|
>
|
|
Stop
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanServiceStatus, 'serviceStatus')}
|
|
loading={operationLoading === 'serviceStatus'}
|
|
variant="outline"
|
|
>
|
|
Status
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanServiceInstall, 'serviceInstall')}
|
|
loading={operationLoading === 'serviceInstall'}
|
|
variant="outline"
|
|
>
|
|
Install
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanServiceUninstall, 'serviceUninstall')}
|
|
loading={operationLoading === 'serviceUninstall'}
|
|
variant="outline"
|
|
>
|
|
Uninstall
|
|
</PrimaryButton>
|
|
</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>
|
|
<div className="text-sm font-medium text-gray-300">Auto-start Daemon</div>
|
|
<div className="text-xs text-gray-500">Automatically start daemon on app launch</div>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="sr-only peer"
|
|
checked={daemonHealth.isAutoStartEnabled}
|
|
onChange={(e) => daemonHealth.setAutoStart(e.target.checked)}
|
|
/>
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Connection Info */}
|
|
{daemonHealth.connectionAttempts > 0 && (
|
|
<div className="p-3 rounded-lg bg-yellow-900/20 border border-yellow-500/30">
|
|
<div className="text-sm text-yellow-400">
|
|
Connection attempts: {daemonHealth.connectionAttempts}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</InputGroup>
|
|
</SectionCard>
|
|
)}
|
|
</div>
|
|
|
|
{/* Category 3: Security & Data - Full width in basic mode, grid in advanced+ */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="Security & Data"
|
|
priority="infrastructure"
|
|
icon={<ShieldCheckIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('security-data')}
|
|
hasChanges={false}
|
|
loading={
|
|
operationLoading?.includes('Secret') ||
|
|
operationLoading?.includes('Models') ||
|
|
operationLoading?.includes('Integration')
|
|
}>
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<InputGroup
|
|
title="Secrets Management"
|
|
description="Encrypt and decrypt sensitive data">
|
|
<Field
|
|
label="Encrypt"
|
|
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"
|
|
placeholder="Plaintext"
|
|
value={encryptInput}
|
|
onChange={event => setEncryptInput(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Decrypt"
|
|
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"
|
|
placeholder="Ciphertext"
|
|
value={decryptInput}
|
|
onChange={event => setDecryptInput(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
|
|
<div className="space-y-6">
|
|
<InputGroup title="Models">
|
|
<Field
|
|
label="Provider Override"
|
|
helpText="Temporarily override the default AI provider for model operations. Leave empty to use the configured default provider. Useful for testing different providers or accessing provider-specific models."
|
|
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"
|
|
placeholder="Provider override (optional)"
|
|
value={providerOverride}
|
|
onChange={event => setProviderOverride(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
|
|
<InputGroup title="Integrations">
|
|
<Field
|
|
label="Integration Name"
|
|
helpText="Name of the platform integration to query or manage. Examples: 'telegram', 'gmail', 'discord'. Use this to get detailed information about specific platform connections and their status."
|
|
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"
|
|
placeholder="Integration name"
|
|
value={integrationName}
|
|
onChange={event => setIntegrationName(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
</div>
|
|
</div>
|
|
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={() => run(() => alphahumanEncryptSecret(encryptInput), 'encryptSecret')}
|
|
loading={operationLoading === 'encryptSecret'}
|
|
disabled={!encryptInput.trim()}>
|
|
Encrypt
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => run(() => alphahumanDecryptSecret(decryptInput), 'decryptSecret')}
|
|
loading={operationLoading === 'decryptSecret'}
|
|
disabled={!decryptInput.trim()}
|
|
variant="outline">
|
|
Decrypt
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(
|
|
() => alphahumanModelsRefresh(providerOverride || undefined, false),
|
|
'modelsRefresh'
|
|
)
|
|
}
|
|
loading={operationLoading === 'modelsRefresh'}>
|
|
Refresh Models
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(
|
|
() => alphahumanModelsRefresh(providerOverride || undefined, true),
|
|
'modelsForceRefresh'
|
|
)
|
|
}
|
|
loading={operationLoading === 'modelsForceRefresh'}
|
|
variant="outline">
|
|
Force Refresh
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanListIntegrations, 'listIntegrations')}
|
|
loading={operationLoading === 'listIntegrations'}>
|
|
List Integrations
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(() => alphahumanGetIntegrationInfo(integrationName), 'getIntegrationInfo')
|
|
}
|
|
loading={operationLoading === 'getIntegrationInfo'}
|
|
disabled={!integrationName.trim()}
|
|
variant="outline">
|
|
Get Integration Info
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{/* Category 4: Network & Infrastructure */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="Network & Infrastructure"
|
|
priority="infrastructure"
|
|
icon={<ServerIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('network-infrastructure')}
|
|
hasChanges={false}
|
|
loading={
|
|
operationLoading?.includes('Gateway') ||
|
|
operationLoading?.includes('Tunnel') ||
|
|
operationLoading?.includes('Memory')
|
|
}>
|
|
<div className="grid gap-8 lg:grid-cols-2">
|
|
<InputGroup
|
|
title="Gateway Settings"
|
|
description="Configure API gateway host and port">
|
|
<Field
|
|
label="Host"
|
|
helpText="IP address for the API gateway server. Use '127.0.0.1' for local-only access or '0.0.0.0' to accept connections from any network interface. Change with caution for security.">
|
|
<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"
|
|
placeholder="127.0.0.1"
|
|
value={gatewayHost}
|
|
onChange={event => setGatewayHost(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Port"
|
|
helpText="Network port for the agent's API server. Default 3000 works for most setups. Change if this port conflicts with other services. Ports below 1024 may require administrator privileges.">
|
|
<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"
|
|
placeholder="3000"
|
|
value={gatewayPort}
|
|
onChange={event => setGatewayPort(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<CheckboxField
|
|
label="Require pairing"
|
|
helpText="Enforces device pairing before API access. Provides additional security by requiring explicit authorization. Disable only for development or trusted network environments."
|
|
checked={gatewayPairing}
|
|
onChange={setGatewayPairing}
|
|
/>
|
|
<CheckboxField
|
|
label="Allow public bind"
|
|
helpText="Permits connections from external networks when enabled. WARNING: Only enable in secure environments with proper firewall protection. Disabled by default for security."
|
|
checked={gatewayPublic}
|
|
onChange={setGatewayPublic}
|
|
/>
|
|
</InputGroup>
|
|
|
|
<InputGroup
|
|
title="Tunnel Configuration"
|
|
description="Configure external tunnel providers">
|
|
<Field label="Provider" fullWidth>
|
|
<select
|
|
className="w-full px-4 py-3 rounded-lg bg-stone-900/40 border border-stone-800/60 text-white focus:border-primary-500/50 focus:ring-2 focus:ring-primary-500/30 focus:outline-none transition-all duration-200"
|
|
value={tunnelProvider}
|
|
onChange={event => setTunnelProvider(event.target.value)}>
|
|
<option value="none">none</option>
|
|
<option value="cloudflare">cloudflare</option>
|
|
<option value="ngrok">ngrok</option>
|
|
<option value="tailscale">tailscale</option>
|
|
<option value="custom">custom</option>
|
|
</select>
|
|
</Field>
|
|
{tunnelProvider === 'cloudflare' && (
|
|
<Field label="Cloudflare Token" 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"
|
|
placeholder="cloudflare token"
|
|
value={cloudflareToken}
|
|
onChange={event => setCloudflareToken(event.target.value)}
|
|
/>
|
|
</Field>
|
|
)}
|
|
{tunnelProvider === 'ngrok' && (
|
|
<Field label="Ngrok Token" 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"
|
|
placeholder="ngrok token"
|
|
value={ngrokToken}
|
|
onChange={event => setNgrokToken(event.target.value)}
|
|
/>
|
|
</Field>
|
|
)}
|
|
{tunnelProvider === 'tailscale' && (
|
|
<Field label="Tailscale Hostname" 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"
|
|
placeholder="alpha.local"
|
|
value={tailscaleHostname}
|
|
onChange={event => setTailscaleHostname(event.target.value)}
|
|
/>
|
|
</Field>
|
|
)}
|
|
{tunnelProvider === 'custom' && (
|
|
<Field label="Custom Start Command" 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"
|
|
placeholder="ngrok http 3000"
|
|
value={customCommand}
|
|
onChange={event => setCustomCommand(event.target.value)}
|
|
/>
|
|
</Field>
|
|
)}
|
|
</InputGroup>
|
|
</div>
|
|
|
|
<InputGroup
|
|
title="Memory Settings"
|
|
description="Configure memory backend and embedding models">
|
|
<Field
|
|
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"
|
|
placeholder="sqlite"
|
|
value={memoryBackend}
|
|
onChange={event => setMemoryBackend(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<CheckboxField
|
|
label="Auto-save"
|
|
helpText="Automatically save conversation history and agent memory to the configured backend storage. Recommended for persistent memory across sessions and system restarts."
|
|
checked={memoryAutoSave}
|
|
onChange={setMemoryAutoSave}
|
|
/>
|
|
<Field
|
|
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"
|
|
placeholder="openai"
|
|
value={embeddingProvider}
|
|
onChange={event => setEmbeddingProvider(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
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"
|
|
placeholder="text-embedding-3-small"
|
|
value={embeddingModel}
|
|
onChange={event => setEmbeddingModel(event.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
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"
|
|
placeholder="1536"
|
|
value={embeddingDims}
|
|
onChange={event => setEmbeddingDims(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={saveGatewaySettings}
|
|
loading={operationLoading === 'saveGatewaySettings'}>
|
|
Save Gateway Settings
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={saveTunnelSettings}
|
|
loading={operationLoading === 'saveTunnelSettings'}>
|
|
Save Tunnel Settings
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={saveMemorySettings}
|
|
loading={operationLoading === 'saveMemorySettings'}>
|
|
Save Memory Settings
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{/* Category 5: Development & Operations */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="Development & Operations"
|
|
priority="infrastructure"
|
|
icon={<WrenchScrewdriverIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('development-operations')}
|
|
hasChanges={false}
|
|
loading={
|
|
operationLoading?.includes('Doctor') ||
|
|
operationLoading?.includes('Hardware') ||
|
|
operationLoading?.includes('Migration')
|
|
}>
|
|
<div className="grid gap-8 lg:grid-cols-2">
|
|
<InputGroup title="Diagnostics" description="System health checks and model probing">
|
|
<div className="md:col-span-2">
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanDoctorReport, 'doctorReport')}
|
|
loading={operationLoading === 'doctorReport'}>
|
|
Run Doctor Report
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(
|
|
() => alphahumanDoctorModels(providerOverride || undefined, true),
|
|
'probeModels'
|
|
)
|
|
}
|
|
loading={operationLoading === 'probeModels'}
|
|
variant="outline">
|
|
Probe Models
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
</div>
|
|
</InputGroup>
|
|
|
|
<InputGroup title="Hardware" description="Discover and introspect hardware devices">
|
|
<Field
|
|
label="Device Path"
|
|
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"
|
|
placeholder="Device path (e.g. /dev/tty.usbmodem)"
|
|
value={hardwarePath}
|
|
onChange={event => setHardwarePath(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
</div>
|
|
|
|
<InputGroup title="Migration" description="Migrate data from external sources">
|
|
<Field
|
|
label="Source Workspace"
|
|
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"
|
|
placeholder="Source workspace (optional)"
|
|
value={migrationSource}
|
|
onChange={event => setMigrationSource(event.target.value)}
|
|
/>
|
|
</Field>
|
|
</InputGroup>
|
|
|
|
<ActionPanel>
|
|
<PrimaryButton
|
|
onClick={() => run(alphahumanHardwareDiscover, 'hardwareDiscover')}
|
|
loading={operationLoading === 'hardwareDiscover'}>
|
|
Discover Devices
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(() => alphahumanHardwareIntrospect(hardwarePath), 'hardwareIntrospect')
|
|
}
|
|
loading={operationLoading === 'hardwareIntrospect'}
|
|
disabled={!hardwarePath.trim()}
|
|
variant="outline">
|
|
Introspect Device
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(
|
|
() => alphahumanMigrateOpenclaw(migrationSource || undefined, true),
|
|
'migrationDryRun'
|
|
)
|
|
}
|
|
loading={operationLoading === 'migrationDryRun'}>
|
|
Dry Run Migration
|
|
</PrimaryButton>
|
|
<PrimaryButton
|
|
onClick={() =>
|
|
run(
|
|
() => alphahumanMigrateOpenclaw(migrationSource || undefined, false),
|
|
'runMigration'
|
|
)
|
|
}
|
|
loading={operationLoading === 'runMigration'}
|
|
variant="outline">
|
|
Run Migration
|
|
</PrimaryButton>
|
|
</ActionPanel>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{/* Category 6: Interactive Tools */}
|
|
{isSectionVisible() && (
|
|
<SectionCard
|
|
title="Interactive Tools"
|
|
priority="infrastructure"
|
|
icon={<ChatBubbleLeftRightIcon />}
|
|
collapsible={true}
|
|
defaultExpanded={!isCollapsed('interactive-tools')}
|
|
hasChanges={false}
|
|
loading={operationLoading === 'sendChat'}>
|
|
{/* Agent Chat - Preserve original styling */}
|
|
<div className="space-y-6">
|
|
<h4 className="text-lg font-medium text-white">Agent Chat</h4>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<label className="space-y-2 text-sm text-gray-300">
|
|
Provider 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"
|
|
placeholder="openai"
|
|
value={chatProvider}
|
|
onChange={event => setChatProvider(event.target.value)}
|
|
/>
|
|
<p className="text-xs text-gray-400">
|
|
Override the default AI provider for this chat session. Leave empty to use
|
|
system default. Useful for testing different AI providers.
|
|
</p>
|
|
</label>
|
|
<label className="space-y-2 text-sm text-gray-300">
|
|
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"
|
|
placeholder="gpt-4.1-mini"
|
|
value={chatModel}
|
|
onChange={event => setChatModel(event.target.value)}
|
|
/>
|
|
<p className="text-xs text-gray-400">
|
|
Specific AI model for this chat session. Examples: gpt-4, gpt-3.5-turbo,
|
|
claude-3-sonnet. Leave empty for system default.
|
|
</p>
|
|
</label>
|
|
<label className="space-y-2 text-sm text-gray-300">
|
|
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"
|
|
placeholder="0.7"
|
|
value={chatTemperature}
|
|
onChange={event => setChatTemperature(event.target.value)}
|
|
/>
|
|
<p className="text-xs text-gray-400">
|
|
Creativity level for responses (0.0-2.0). Lower = more focused, higher = more
|
|
creative. Leave empty for system default.
|
|
</p>
|
|
</label>
|
|
</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"
|
|
placeholder="Send a message to the agent..."
|
|
value={chatInput}
|
|
onChange={event => setChatInput(event.target.value)}
|
|
/>
|
|
<p className="text-xs text-gray-400 leading-relaxed">
|
|
Direct chat interface with the AI agent. Test conversations, debug responses, or
|
|
interact with the agent using the configured settings above.
|
|
</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"
|
|
onClick={sendChat}>
|
|
Send Message
|
|
</button>
|
|
{chatLog.length > 0 && (
|
|
<div className="space-y-2 rounded-lg border border-white/10 bg-white/5 backdrop-blur-sm p-3">
|
|
{chatLog.map((entry, index) => (
|
|
<div
|
|
key={`${entry.role}-${index}`}
|
|
className={`text-sm ${
|
|
entry.role === 'user' ? 'text-white' : 'text-emerald-200'
|
|
}`}>
|
|
<span className="font-semibold uppercase text-[10px] tracking-wide">
|
|
{entry.role}
|
|
</span>
|
|
<div className="whitespace-pre-wrap">{entry.text}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Output Console */}
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h4 className="text-lg font-medium text-white flex items-center gap-2">
|
|
<DocumentTextIcon className="h-5 w-5" />
|
|
Output Console
|
|
</h4>
|
|
<p className="text-sm text-gray-400 mt-2">
|
|
Real-time command results and system responses. Shows JSON output, error messages,
|
|
and operation status from all Tauri commands.
|
|
</p>
|
|
</div>
|
|
{error && (
|
|
<div className="rounded-lg border border-coral-500/40 bg-coral-500/10 px-4 py-3 text-sm text-coral-200">
|
|
{error}
|
|
</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-[240px] font-mono text-xs resize-y"
|
|
value={output}
|
|
readOnly
|
|
placeholder="Command output will appear here..."
|
|
/>
|
|
<p className="text-xs text-gray-400 leading-relaxed">
|
|
Read-only console showing formatted JSON responses, error details, and debugging
|
|
information from system operations.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TauriCommandsPanel;
|