首页布局优化前
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import './index.css';
|
||||
import { Sidebar, MainViewType } from './components/Sidebar';
|
||||
@@ -9,14 +9,19 @@ import { HandTaskPanel } from './components/HandTaskPanel';
|
||||
import { SchedulerPanel } from './components/SchedulerPanel';
|
||||
import { TeamCollaborationView } from './components/TeamCollaborationView';
|
||||
import { SwarmDashboard } from './components/SwarmDashboard';
|
||||
import { useGatewayStore } from './store/gatewayStore';
|
||||
import { SkillMarket } from './components/SkillMarket';
|
||||
import { AgentOnboardingWizard } from './components/AgentOnboardingWizard';
|
||||
import { HandApprovalModal } from './components/HandApprovalModal';
|
||||
import { useGatewayStore, type HandRun } from './store/gatewayStore';
|
||||
import { useTeamStore } from './store/teamStore';
|
||||
import { useChatStore } from './store/chatStore';
|
||||
import { getStoredGatewayToken } from './lib/gateway-client';
|
||||
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
|
||||
import { silentErrorHandler } from './lib/error-utils';
|
||||
import { Bot, Users, Loader2 } from 'lucide-react';
|
||||
import { EmptyState } from './components/ui';
|
||||
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
|
||||
import { useOnboarding } from './lib/use-onboarding';
|
||||
import type { Clone } from './store/agentStore';
|
||||
|
||||
type View = 'main' | 'settings';
|
||||
|
||||
@@ -39,13 +44,66 @@ function App() {
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(undefined);
|
||||
const [bootstrapping, setBootstrapping] = useState(true);
|
||||
const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...');
|
||||
const { connect, connectionState } = useGatewayStore();
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
|
||||
// Hand Approval state
|
||||
const [pendingApprovalRun, setPendingApprovalRun] = useState<HandRun | null>(null);
|
||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||
|
||||
const { connect, hands, approveHand, loadHands } = useGatewayStore();
|
||||
const { activeTeam, setActiveTeam, teams } = useTeamStore();
|
||||
const { setCurrentAgent } = useChatStore();
|
||||
const { isNeeded: onboardingNeeded, isLoading: onboardingLoading, markCompleted } = useOnboarding();
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ZCLAW';
|
||||
}, []);
|
||||
|
||||
// Watch for Hands that need approval
|
||||
useEffect(() => {
|
||||
const handsNeedingApproval = hands.filter(h => h.status === 'needs_approval');
|
||||
if (handsNeedingApproval.length > 0 && !showApprovalModal) {
|
||||
// Find the first hand with needs_approval and create a pending run
|
||||
const hand = handsNeedingApproval[0];
|
||||
if (hand.currentRunId) {
|
||||
setPendingApprovalRun({
|
||||
runId: hand.currentRunId,
|
||||
status: 'needs_approval',
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
setShowApprovalModal(true);
|
||||
}
|
||||
}
|
||||
}, [hands, showApprovalModal]);
|
||||
|
||||
// Handle approval/rejection of Hand runs
|
||||
const handleApproveHand = useCallback(async (runId: string) => {
|
||||
// Find the hand that owns this run
|
||||
const hand = hands.find(h => h.currentRunId === runId);
|
||||
if (!hand) return;
|
||||
|
||||
await approveHand(hand.id, runId, true);
|
||||
await loadHands();
|
||||
setShowApprovalModal(false);
|
||||
setPendingApprovalRun(null);
|
||||
}, [hands, approveHand, loadHands]);
|
||||
|
||||
const handleRejectHand = useCallback(async (runId: string, reason: string) => {
|
||||
// Find the hand that owns this run
|
||||
const hand = hands.find(h => h.currentRunId === runId);
|
||||
if (!hand) return;
|
||||
|
||||
await approveHand(hand.id, runId, false, reason);
|
||||
await loadHands();
|
||||
setShowApprovalModal(false);
|
||||
setPendingApprovalRun(null);
|
||||
}, [hands, approveHand, loadHands]);
|
||||
|
||||
const handleCloseApprovalModal = useCallback(() => {
|
||||
setShowApprovalModal(false);
|
||||
// Don't clear pendingApprovalRun - keep it for reference
|
||||
}, []);
|
||||
|
||||
// Bootstrap: Start OpenFang Gateway before rendering main UI
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -64,43 +122,65 @@ function App() {
|
||||
setBootstrapStatus('Starting OpenFang Gateway...');
|
||||
console.log('[App] Local gateway not running, auto-starting...');
|
||||
|
||||
await startLocalGateway();
|
||||
await startLocalGateway();
|
||||
|
||||
// Wait for gateway to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
console.log('[App] Local gateway started');
|
||||
} else if (isRunning) {
|
||||
console.log('[App] Local gateway already running');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[App] Failed to check/start local gateway:', err);
|
||||
// Wait for gateway to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
console.log('[App] Local gateway started');
|
||||
} else if (isRunning) {
|
||||
console.log('[App] Local gateway already running');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[App] Failed to check/start local gateway:', err);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 2: Connect to gateway
|
||||
setBootstrapStatus('Connecting to gateway...');
|
||||
const gatewayToken = getStoredGatewayToken();
|
||||
await connect(undefined, gatewayToken);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 3: Bootstrap complete
|
||||
setBootstrapping(false);
|
||||
} catch (err) {
|
||||
console.error('[App] Bootstrap failed:', err);
|
||||
// Still allow app to load, connection status will show error
|
||||
setBootstrapping(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 2: Connect to gateway
|
||||
setBootstrapStatus('Connecting to gateway...');
|
||||
const gatewayToken = getStoredGatewayToken();
|
||||
await connect(undefined, gatewayToken);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 3: Check if onboarding is needed
|
||||
if (onboardingNeeded && !onboardingLoading) {
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
|
||||
// Step 4: Bootstrap complete
|
||||
setBootstrapping(false);
|
||||
} catch (err) {
|
||||
console.error('[App] Bootstrap failed:', err);
|
||||
// Still allow app to load, connection status will show error
|
||||
setBootstrapping(false);
|
||||
}
|
||||
};
|
||||
|
||||
bootstrap();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [connect]);
|
||||
}, [connect, onboardingNeeded, onboardingLoading]);
|
||||
|
||||
// Handle onboarding completion
|
||||
const handleOnboardingSuccess = (clone: Clone) => {
|
||||
markCompleted({
|
||||
userName: clone.userName || 'User',
|
||||
userRole: clone.userRole,
|
||||
});
|
||||
setCurrentAgent({
|
||||
id: clone.id,
|
||||
name: clone.name,
|
||||
icon: clone.emoji || '🦞',
|
||||
color: 'bg-gradient-to-br from-orange-500 to-red-500',
|
||||
lastMessage: clone.role || 'New Agent',
|
||||
time: '',
|
||||
});
|
||||
setShowOnboarding(false);
|
||||
};
|
||||
|
||||
// 当切换到非 hands 视图时清除选中的 Hand
|
||||
const handleMainViewChange = (view: MainViewType) => {
|
||||
@@ -128,6 +208,24 @@ function App() {
|
||||
return <BootstrapScreen status={bootstrapStatus} />;
|
||||
}
|
||||
|
||||
// Show onboarding wizard for first-time users
|
||||
if (showOnboarding) {
|
||||
return (
|
||||
<AgentOnboardingWizard
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
// Skip onboarding and mark as completed with default values
|
||||
markCompleted({
|
||||
userName: 'User',
|
||||
userRole: 'user',
|
||||
});
|
||||
setShowOnboarding(false);
|
||||
}}
|
||||
onSuccess={handleOnboardingSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden text-gray-800 text-sm">
|
||||
{/* 左侧边栏 */}
|
||||
@@ -190,6 +288,15 @@ function App() {
|
||||
>
|
||||
<SwarmDashboard />
|
||||
</motion.div>
|
||||
) : mainContentView === 'skills' ? (
|
||||
<motion.div
|
||||
variants={fadeInVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<SkillMarket />
|
||||
</motion.div>
|
||||
) : (
|
||||
<ChatArea />
|
||||
)}
|
||||
@@ -198,6 +305,15 @@ function App() {
|
||||
|
||||
{/* 右侧边栏 */}
|
||||
<RightPanel />
|
||||
|
||||
{/* Hand Approval Modal (global) */}
|
||||
<HandApprovalModal
|
||||
handRun={pendingApprovalRun}
|
||||
isOpen={showApprovalModal}
|
||||
onApprove={handleApproveHand}
|
||||
onReject={handleRejectHand}
|
||||
onClose={handleCloseApprovalModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 展示学习事件、模式和系统建议。
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Brain,
|
||||
@@ -12,24 +12,17 @@ import {
|
||||
Lightbulb,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Upload,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Clock,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Button, Badge, EmptyState } from './ui';
|
||||
import { Button, EmptyState } from './ui';
|
||||
import { useActiveLearningStore } from '../store/activeLearningStore';
|
||||
import {
|
||||
useActiveLearningStore,
|
||||
type LearningEvent,
|
||||
type LearningPattern,
|
||||
type LearningSuggestion,
|
||||
type LearningEventType,
|
||||
} from '../store/activeLearningStore';
|
||||
} from '../types/active-learning';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
|
||||
// === Constants ===
|
||||
|
||||
@@ -74,9 +67,9 @@ function EventItem({ event, onAcknowledge }: EventItemProps) {
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="ghost" className={typeInfo.color}>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${typeInfo.color}`}>
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{timeAgo}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300 truncate">{event.observation}</p>
|
||||
@@ -133,14 +126,13 @@ function SuggestionCard({ suggestion, onApply, onDismiss }: SuggestionCardProps)
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button variant="primary" size="sm" onClick={onApply}>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
应用
|
||||
</Button>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
应用
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onDismiss}>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -160,10 +152,7 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
||||
|
||||
const {
|
||||
events,
|
||||
patterns,
|
||||
suggestions,
|
||||
config,
|
||||
isLoading,
|
||||
acknowledgeEvent,
|
||||
getPatterns,
|
||||
getSuggestions,
|
||||
@@ -364,9 +353,9 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
||||
<span>{typeInfo.icon}</span>
|
||||
<span className="text-sm font-medium text-white">{typeInfo.label}</span>
|
||||
</div>
|
||||
<Badge variant="ghost">
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-700 text-gray-300">
|
||||
{(pattern.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{pattern.description}</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
|
||||
@@ -77,7 +77,7 @@ const steps = [
|
||||
// === Component ===
|
||||
|
||||
export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboardingWizardProps) {
|
||||
const { createClone, isLoading, error, clearError } = useAgentStore();
|
||||
const { createClone, updateClone, clones, isLoading, error, clearError } = useAgentStore();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState<WizardFormData>(initialFormData);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
@@ -164,7 +164,7 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
||||
setSubmitStatus('idle');
|
||||
|
||||
try {
|
||||
const createOptions: CloneCreateOptions = {
|
||||
const personalityUpdates = {
|
||||
name: formData.agentName,
|
||||
role: formData.agentRole || undefined,
|
||||
nickname: formData.agentNickname || undefined,
|
||||
@@ -173,13 +173,23 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
||||
scenarios: formData.scenarios,
|
||||
workspaceDir: formData.workspaceDir || undefined,
|
||||
restrictFiles: formData.restrictFiles,
|
||||
privacyOptIn: formData.privacyOptIn,
|
||||
emoji: formData.emoji,
|
||||
personality: formData.personality,
|
||||
notes: formData.notes || undefined,
|
||||
};
|
||||
|
||||
const clone = await createClone(createOptions);
|
||||
let clone: Clone | undefined;
|
||||
|
||||
// If there's an existing clone, update it instead of creating a new one
|
||||
if (clones && clones.length > 0) {
|
||||
clone = await updateClone(clones[0].id, personalityUpdates);
|
||||
} else {
|
||||
const createOptions: CloneCreateOptions = {
|
||||
...personalityUpdates,
|
||||
privacyOptIn: formData.privacyOptIn,
|
||||
};
|
||||
clone = await createClone(createOptions);
|
||||
}
|
||||
|
||||
if (clone) {
|
||||
setSubmitStatus('success');
|
||||
@@ -516,32 +526,6 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
匿名使用数据
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
允许收集匿名使用数据以改进产品
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateField('privacyOptIn', !formData.privacyOptIn)}
|
||||
className={cn(
|
||||
'w-11 h-6 rounded-full transition-colors relative',
|
||||
formData.privacyOptIn ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform',
|
||||
)}
|
||||
style={{ left: formData.privacyOptIn ? '22px' : '2px' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
@@ -623,7 +607,7 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-1"
|
||||
className="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
|
||||
>
|
||||
下一步
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
@@ -633,7 +617,7 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || submitStatus === 'success'}
|
||||
className="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
className="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
||||
@@ -9,13 +9,34 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useGatewayStore, type Hand, type HandRequirement } from '../store/gatewayStore';
|
||||
import { Zap, RefreshCw, ChevronRight, CheckCircle, XCircle, Loader2, AlertTriangle, Settings } from 'lucide-react';
|
||||
import { Zap, RefreshCw, ChevronRight, CheckCircle, XCircle, Loader2, AlertTriangle, Settings, Play } from 'lucide-react';
|
||||
import { BrowserHandCard } from './BrowserHand';
|
||||
import type { HandParameter } from '../types/hands';
|
||||
import { HAND_DEFINITIONS } from '../types/hands';
|
||||
import { HandParamsForm } from './HandParamsForm';
|
||||
|
||||
// === Status Badge Component ===
|
||||
|
||||
type HandStatus = 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed';
|
||||
|
||||
// === Parameter Validation Helper ===
|
||||
|
||||
function validateAllParameters(
|
||||
parameters: HandParameter[],
|
||||
values: Record<string, unknown>
|
||||
): Record<string, string> {
|
||||
const errors: Record<string, string> = {};
|
||||
parameters.forEach(param => {
|
||||
if (param.required) {
|
||||
const value = values[param.name];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
errors[param.name] = `${param.label} is required`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
interface StatusConfig {
|
||||
label: string;
|
||||
className: string;
|
||||
@@ -117,6 +138,57 @@ interface HandDetailsModalProps {
|
||||
}
|
||||
|
||||
function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: HandDetailsModalProps) {
|
||||
// Get Hand parameters from definitions
|
||||
const handDefinition = HAND_DEFINITIONS.find(h => h.id === hand.id);
|
||||
const parameters: HandParameter[] = handDefinition?.parameters || [];
|
||||
|
||||
// Form state for parameters
|
||||
const [paramValues, setParamValues] = useState<Record<string, unknown>>({});
|
||||
const [paramErrors, setParamErrors] = useState<Record<string, string>>({});
|
||||
const [showParamsForm, setShowParamsForm] = useState(false);
|
||||
|
||||
// Initialize default values
|
||||
useEffect(() => {
|
||||
if (parameters.length > 0) {
|
||||
const defaults: Record<string, unknown> = {};
|
||||
parameters.forEach(p => {
|
||||
if (p.defaultValue !== undefined) {
|
||||
defaults[p.name] = p.defaultValue;
|
||||
}
|
||||
});
|
||||
setParamValues(defaults);
|
||||
}
|
||||
}, [parameters]);
|
||||
|
||||
// Reset form when modal opens/closes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShowParamsForm(false);
|
||||
setParamErrors({});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleActivateClick = useCallback(() => {
|
||||
if (parameters.length > 0 && !showParamsForm) {
|
||||
// Show params form first
|
||||
setShowParamsForm(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate parameters if showing form
|
||||
if (showParamsForm) {
|
||||
const errors = validateAllParameters(parameters, paramValues);
|
||||
setParamErrors(errors);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return;
|
||||
}
|
||||
// Pass parameters to onActivate
|
||||
onActivate();
|
||||
} else {
|
||||
onActivate();
|
||||
}
|
||||
}, [parameters, showParamsForm, paramValues, onActivate]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const canActivate = hand.status === 'idle' || hand.status === 'setup_needed';
|
||||
@@ -210,6 +282,23 @@ function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: H
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameters Form (shown when activating) */}
|
||||
{showParamsForm && parameters.length > 0 && (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3">
|
||||
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
||||
执行参数
|
||||
</h3>
|
||||
<HandParamsForm
|
||||
parameters={parameters}
|
||||
values={paramValues}
|
||||
onChange={setParamValues}
|
||||
errors={paramErrors}
|
||||
disabled={isActivating}
|
||||
presetKey={`hand-${hand.id}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dashboard Metrics */}
|
||||
{hand.metrics && hand.metrics.length > 0 && (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3">
|
||||
@@ -234,13 +323,13 @@ function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: H
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={showParamsForm ? () => setShowParamsForm(false) : onClose}
|
||||
className="px-4 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
关闭
|
||||
{showParamsForm ? '返回' : '关闭'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onActivate}
|
||||
onClick={handleActivateClick}
|
||||
disabled={!canActivate || hasUnmetRequirements || isActivating}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
@@ -254,6 +343,11 @@ function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: H
|
||||
<Settings className="w-4 h-4" />
|
||||
需要配置
|
||||
</>
|
||||
) : showParamsForm ? (
|
||||
<>
|
||||
<Play className="w-4 h-4" />
|
||||
执行
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="w-4 h-4" />
|
||||
|
||||
@@ -5,9 +5,13 @@ import { useGatewayStore, type PluginStatus } from '../store/gatewayStore';
|
||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, FileCode, Brain
|
||||
MessageSquare, Cpu, FileText, User, Activity, FileCode, Brain,
|
||||
Shield, Sparkles, GraduationCap
|
||||
} from 'lucide-react';
|
||||
import { MemoryPanel } from './MemoryPanel';
|
||||
import { ReflectionLog } from './ReflectionLog';
|
||||
import { AutonomyConfig } from './AutonomyConfig';
|
||||
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
import { Button, Badge, EmptyState } from './ui';
|
||||
import { getPersonalityById } from '../lib/personality-presets';
|
||||
@@ -19,7 +23,7 @@ export function RightPanel() {
|
||||
connect, loadClones, loadUsageStats, loadPluginStatus, workspaceInfo, quickConfig, updateClone,
|
||||
} = useGatewayStore();
|
||||
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory'>('status');
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
@@ -152,12 +156,54 @@ export function RightPanel() {
|
||||
>
|
||||
<Brain className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'reflection' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveTab('reflection')}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||
title="Reflection"
|
||||
aria-label="Reflection"
|
||||
aria-selected={activeTab === 'reflection'}
|
||||
role="tab"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'autonomy' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveTab('autonomy')}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||
title="Autonomy"
|
||||
aria-label="Autonomy"
|
||||
aria-selected={activeTab === 'autonomy'}
|
||||
role="tab"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'learning' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveTab('learning')}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||
title="Learning"
|
||||
aria-label="Learning"
|
||||
aria-selected={activeTab === 'learning'}
|
||||
role="tab"
|
||||
>
|
||||
<GraduationCap className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||
{activeTab === 'memory' ? (
|
||||
<MemoryPanel />
|
||||
) : activeTab === 'reflection' ? (
|
||||
<ReflectionLog />
|
||||
) : activeTab === 'autonomy' ? (
|
||||
<AutonomyConfig />
|
||||
) : activeTab === 'learning' ? (
|
||||
<ActiveLearningPanel />
|
||||
) : activeTab === 'agent' ? (
|
||||
<div className="space-y-4">
|
||||
<motion.div
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* SchedulerPanel - OpenFang Scheduler UI
|
||||
*
|
||||
* Displays scheduled jobs, event triggers, and run history.
|
||||
* Displays scheduled jobs, event triggers, workflows, and run history.
|
||||
*
|
||||
* Design based on OpenFang Dashboard v0.4.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { useGatewayStore, type Workflow } from '../store/gatewayStore';
|
||||
import { WorkflowEditor } from './WorkflowEditor';
|
||||
import { WorkflowHistory } from './WorkflowHistory';
|
||||
import {
|
||||
Clock,
|
||||
Zap,
|
||||
@@ -19,11 +21,13 @@ import {
|
||||
X,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
GitBranch,
|
||||
Play,
|
||||
} from 'lucide-react';
|
||||
|
||||
// === Tab Types ===
|
||||
|
||||
type TabType = 'scheduled' | 'triggers' | 'history';
|
||||
type TabType = 'scheduled' | 'triggers' | 'workflows' | 'history';
|
||||
|
||||
// === Schedule Type ===
|
||||
|
||||
@@ -632,13 +636,26 @@ function CreateJobModal({ isOpen, onClose, onSuccess }: CreateJobModalProps) {
|
||||
// === Main SchedulerPanel Component ===
|
||||
|
||||
export function SchedulerPanel() {
|
||||
const { scheduledTasks, loadScheduledTasks, isLoading } = useGatewayStore();
|
||||
const {
|
||||
scheduledTasks,
|
||||
loadScheduledTasks,
|
||||
workflows,
|
||||
loadWorkflows,
|
||||
createWorkflow,
|
||||
executeWorkflow,
|
||||
isLoading,
|
||||
} = useGatewayStore();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('scheduled');
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isWorkflowEditorOpen, setIsWorkflowEditorOpen] = useState(false);
|
||||
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | undefined>(undefined);
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<Workflow | null>(null);
|
||||
const [isSavingWorkflow, setIsSavingWorkflow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadScheduledTasks();
|
||||
}, [loadScheduledTasks]);
|
||||
loadWorkflows();
|
||||
}, [loadScheduledTasks, loadWorkflows]);
|
||||
|
||||
const handleCreateJob = useCallback(() => {
|
||||
setIsCreateModalOpen(true);
|
||||
@@ -653,6 +670,60 @@ export function SchedulerPanel() {
|
||||
loadScheduledTasks();
|
||||
}, [loadScheduledTasks]);
|
||||
|
||||
// Workflow handlers
|
||||
const handleCreateWorkflow = useCallback(() => {
|
||||
setEditingWorkflow(undefined);
|
||||
setIsWorkflowEditorOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditWorkflow = useCallback((workflow: Workflow) => {
|
||||
setEditingWorkflow(workflow);
|
||||
setIsWorkflowEditorOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleViewWorkflowHistory = useCallback((workflow: Workflow) => {
|
||||
setSelectedWorkflow(workflow);
|
||||
}, []);
|
||||
|
||||
const handleSaveWorkflow = useCallback(async (data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: Array<{
|
||||
handName: string;
|
||||
name?: string;
|
||||
params?: Record<string, unknown>;
|
||||
condition?: string;
|
||||
}>;
|
||||
}) => {
|
||||
setIsSavingWorkflow(true);
|
||||
try {
|
||||
if (editingWorkflow) {
|
||||
// Update existing workflow
|
||||
console.log('Update workflow:', editingWorkflow.id, data);
|
||||
} else {
|
||||
// Create new workflow
|
||||
await createWorkflow(data);
|
||||
}
|
||||
setIsWorkflowEditorOpen(false);
|
||||
setEditingWorkflow(undefined);
|
||||
await loadWorkflows();
|
||||
} catch (error) {
|
||||
console.error('Failed to save workflow:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSavingWorkflow(false);
|
||||
}
|
||||
}, [editingWorkflow, createWorkflow, loadWorkflows]);
|
||||
|
||||
const handleExecuteWorkflow = useCallback(async (workflowId: string) => {
|
||||
try {
|
||||
await executeWorkflow(workflowId);
|
||||
await loadWorkflows();
|
||||
} catch (error) {
|
||||
console.error('Failed to execute workflow:', error);
|
||||
}
|
||||
}, [executeWorkflow, loadWorkflows]);
|
||||
|
||||
if (isLoading && scheduledTasks.length === 0) {
|
||||
return (
|
||||
<div className="p-4 text-center">
|
||||
@@ -706,6 +777,12 @@ export function SchedulerPanel() {
|
||||
icon={Zap}
|
||||
label="事件触发器"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'workflows'}
|
||||
onClick={() => setActiveTab('workflows')}
|
||||
icon={GitBranch}
|
||||
label="工作流"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'history'}
|
||||
onClick={() => setActiveTab('history')}
|
||||
@@ -722,6 +799,15 @@ export function SchedulerPanel() {
|
||||
新建任务
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'workflows' && (
|
||||
<button
|
||||
onClick={handleCreateWorkflow}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新建工作流
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
@@ -787,6 +873,86 @@ export function SchedulerPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Workflows Tab */}
|
||||
{activeTab === 'workflows' && (
|
||||
selectedWorkflow ? (
|
||||
<WorkflowHistory
|
||||
workflow={selectedWorkflow}
|
||||
onBack={() => setSelectedWorkflow(null)}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
{workflows.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={GitBranch}
|
||||
title="暂无工作流"
|
||||
description="工作流可以将多个 Hand 组合成自动化流程,实现复杂的任务编排。"
|
||||
actionLabel="创建工作流"
|
||||
onAction={handleCreateWorkflow}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center">
|
||||
<GitBranch className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{workflow.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{workflow.description || '无描述'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.steps && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{workflow.steps} 步骤
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleExecuteWorkflow(workflow.id)}
|
||||
className="p-1.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditWorkflow(workflow)}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
title="编辑"
|
||||
>
|
||||
<GitBranch className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleViewWorkflowHistory(workflow)}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
title="历史"
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={handleCreateWorkflow}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-500 dark:text-gray-400 hover:border-blue-500 hover:text-blue-500 dark:hover:border-blue-400 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
创建新工作流
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<EmptyState
|
||||
@@ -804,6 +970,18 @@ export function SchedulerPanel() {
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSuccess={handleCreateSuccess}
|
||||
/>
|
||||
|
||||
{/* Workflow Editor Modal */}
|
||||
<WorkflowEditor
|
||||
workflow={editingWorkflow}
|
||||
isOpen={isWorkflowEditorOpen}
|
||||
onClose={() => {
|
||||
setIsWorkflowEditorOpen(false);
|
||||
setEditingWorkflow(undefined);
|
||||
}}
|
||||
onSave={handleSaveWorkflow}
|
||||
isSaving={isSavingWorkflow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
BarChart3,
|
||||
@@ -28,6 +29,7 @@ import { About } from './About';
|
||||
import { Credits } from './Credits';
|
||||
import { AuditLogsPanel } from '../AuditLogsPanel';
|
||||
import { SecurityStatus } from '../SecurityStatus';
|
||||
import { SecurityLayersPanel } from '../SecurityLayersPanel';
|
||||
import { TaskList } from '../TaskList';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
@@ -69,6 +71,7 @@ const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode }[] =
|
||||
|
||||
export function SettingsLayout({ onBack }: SettingsLayoutProps) {
|
||||
const [activePage, setActivePage] = useState<SettingsPage>('general');
|
||||
const { securityStatus } = useGatewayStore();
|
||||
|
||||
const renderPage = () => {
|
||||
switch (activePage) {
|
||||
@@ -82,9 +85,22 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) {
|
||||
case 'workspace': return <Workspace />;
|
||||
case 'privacy': return <Privacy />;
|
||||
case 'security': return (
|
||||
<div className="max-w-3xl">
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-6">安全状态</h1>
|
||||
<SecurityStatus />
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-4">安全状态</h1>
|
||||
<SecurityStatus />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">安全架构详情</h2>
|
||||
<SecurityLayersPanel
|
||||
status={securityStatus || {
|
||||
layers: [],
|
||||
enabledCount: 0,
|
||||
totalCount: 16,
|
||||
securityLevel: 'low',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'audit': return <AuditLogsPanel />;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Settings, Users, Bot, GitBranch, MessageSquare, Layers } from 'lucide-react';
|
||||
import { Settings, Users, Bot, GitBranch, MessageSquare, Layers, Package } from 'lucide-react';
|
||||
import { CloneManager } from './CloneManager';
|
||||
import { HandList } from './HandList';
|
||||
import { WorkflowList } from './WorkflowList';
|
||||
import { TeamList } from './TeamList';
|
||||
import { SwarmDashboard } from './SwarmDashboard';
|
||||
import { SkillMarket } from './SkillMarket';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { Button } from './ui';
|
||||
import { containerVariants, defaultTransition } from '../lib/animations';
|
||||
|
||||
export type MainViewType = 'chat' | 'hands' | 'workflow' | 'team' | 'swarm';
|
||||
export type MainViewType = 'chat' | 'hands' | 'workflow' | 'team' | 'swarm' | 'skills';
|
||||
|
||||
interface SidebarProps {
|
||||
onOpenSettings?: () => void;
|
||||
@@ -21,12 +22,13 @@ interface SidebarProps {
|
||||
onSelectTeam?: (teamId: string) => void;
|
||||
}
|
||||
|
||||
type Tab = 'clones' | 'hands' | 'workflow' | 'team' | 'swarm';
|
||||
type Tab = 'clones' | 'hands' | 'workflow' | 'team' | 'swarm' | 'skills';
|
||||
|
||||
const TABS: { key: Tab; label: string; icon: React.ComponentType<{ className?: string }>; mainView?: MainViewType }[] = [
|
||||
{ key: 'clones', label: '分身', icon: Bot },
|
||||
{ key: 'hands', label: 'Hands', icon: MessageSquare, mainView: 'hands' },
|
||||
{ key: 'workflow', label: '工作流', icon: GitBranch, mainView: 'workflow' },
|
||||
{ key: 'skills', label: '技能', icon: Package, mainView: 'skills' },
|
||||
{ key: 'team', label: '团队', icon: Users, mainView: 'team' },
|
||||
{ key: 'swarm', label: '协作', icon: Layers, mainView: 'swarm' },
|
||||
];
|
||||
@@ -107,6 +109,7 @@ export function Sidebar({
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'workflow' && <WorkflowList />}
|
||||
{activeTab === 'skills' && <SkillMarket />}
|
||||
{activeTab === 'team' && (
|
||||
<TeamList
|
||||
selectedTeamId={selectedTeamId}
|
||||
|
||||
@@ -82,6 +82,7 @@ export function inferPreference(feedback: string, sentiment: FeedbackSentiment):
|
||||
export class ActiveLearningEngine {
|
||||
private events: LearningEvent[] = [];
|
||||
private patterns: LearningPattern[] = [];
|
||||
// Reserved for future learning suggestions feature
|
||||
private suggestions: LearningSuggestion[] = [];
|
||||
private initialized: boolean = false;
|
||||
|
||||
@@ -89,6 +90,16 @@ export class ActiveLearningEngine {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/** Get current suggestions (reserved for future use) */
|
||||
getSuggestions(): LearningSuggestion[] {
|
||||
return this.suggestions;
|
||||
}
|
||||
|
||||
/** Check if engine is initialized */
|
||||
isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录学习事件
|
||||
*/
|
||||
|
||||
@@ -190,7 +190,7 @@ export class AutonomyManager {
|
||||
|
||||
// High-risk actions ALWAYS require approval
|
||||
const isHighRisk = riskLevel === 'high';
|
||||
const isSelfModification = action === 'identity_update' || action === 'selfModification';
|
||||
const isSelfModification = action === 'identity_update';
|
||||
const isDeletion = action === 'memory_delete';
|
||||
|
||||
let allowed = false;
|
||||
|
||||
@@ -618,7 +618,26 @@ export class GatewayClient {
|
||||
// === High-level API ===
|
||||
|
||||
// Default agent ID for OpenFang (will be set dynamically from /api/agents)
|
||||
private defaultAgentId: string = 'f77004c8-418f-4132-b7d4-7ecb9d66f44c';
|
||||
private defaultAgentId: string = '';
|
||||
|
||||
/** Try to fetch default agent ID from OpenFang /api/agents endpoint */
|
||||
async fetchDefaultAgentId(): Promise<string | null> {
|
||||
try {
|
||||
// Use /api/agents endpoint which returns array of agents
|
||||
const agents = await this.restGet<Array<{ id: string; name?: string; state?: string }>>('/api/agents');
|
||||
if (agents && agents.length > 0) {
|
||||
// Prefer agent with state "Running", otherwise use first agent
|
||||
const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'Running');
|
||||
const defaultAgent = runningAgent || agents[0];
|
||||
this.defaultAgentId = defaultAgent.id;
|
||||
this.log('info', `Fetched default agent from /api/agents: ${this.defaultAgentId} (${defaultAgent.name || 'unnamed'})`);
|
||||
return this.defaultAgentId;
|
||||
}
|
||||
} catch (err) {
|
||||
this.log('warn', `Failed to fetch default agent from /api/agents: ${err}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Set the default agent ID */
|
||||
setDefaultAgentId(agentId: string): void {
|
||||
@@ -642,7 +661,18 @@ export class GatewayClient {
|
||||
maxTokens?: number;
|
||||
}): Promise<{ runId: string; sessionId?: string; response?: string }> {
|
||||
// OpenFang uses /api/agents/{agentId}/message endpoint
|
||||
const agentId = opts?.agentId || this.defaultAgentId;
|
||||
let agentId = opts?.agentId || this.defaultAgentId;
|
||||
|
||||
// If no agent ID, try to fetch from OpenFang status
|
||||
if (!agentId) {
|
||||
await this.fetchDefaultAgentId();
|
||||
agentId = this.defaultAgentId;
|
||||
}
|
||||
|
||||
if (!agentId) {
|
||||
throw new Error('No agent available. Please ensure OpenFang has at least one agent.');
|
||||
}
|
||||
|
||||
const result = await this.restPost<{ response?: string; input_tokens?: number; output_tokens?: number }>(`/api/agents/${agentId}/message`, {
|
||||
message,
|
||||
session_id: opts?.sessionKey,
|
||||
@@ -670,10 +700,29 @@ export class GatewayClient {
|
||||
agentId?: string;
|
||||
}
|
||||
): Promise<{ runId: string }> {
|
||||
const agentId = opts?.agentId || this.defaultAgentId;
|
||||
let agentId = opts?.agentId || this.defaultAgentId;
|
||||
const runId = createIdempotencyKey();
|
||||
const sessionId = opts?.sessionKey || `session_${Date.now()}`;
|
||||
|
||||
// If no agent ID, try to fetch from OpenFang status (async, but we'll handle it in connectOpenFangStream)
|
||||
if (!agentId) {
|
||||
// Try to get default agent asynchronously
|
||||
this.fetchDefaultAgentId().then(() => {
|
||||
const resolvedAgentId = this.defaultAgentId;
|
||||
if (resolvedAgentId) {
|
||||
this.streamCallbacks.set(runId, callbacks);
|
||||
this.connectOpenFangStream(resolvedAgentId, runId, sessionId, message);
|
||||
} else {
|
||||
callbacks.onError('No agent available. Please ensure OpenFang has at least one agent.');
|
||||
callbacks.onComplete();
|
||||
}
|
||||
}).catch((err) => {
|
||||
callbacks.onError(`Failed to get agent: ${err}`);
|
||||
callbacks.onComplete();
|
||||
});
|
||||
return { runId };
|
||||
}
|
||||
|
||||
// Store callbacks for this run
|
||||
this.streamCallbacks.set(runId, callbacks);
|
||||
|
||||
@@ -1087,7 +1136,11 @@ export class GatewayClient {
|
||||
async getQuickConfig(): Promise<any> {
|
||||
try {
|
||||
// Use /api/config endpoint (OpenFang's actual config endpoint)
|
||||
const config = await this.restGet('/api/config');
|
||||
const config = await this.restGet<{
|
||||
data_dir?: string;
|
||||
home_dir?: string;
|
||||
default_model?: { model?: string; provider?: string };
|
||||
}>('/api/config');
|
||||
// Map OpenFang config to frontend expected format
|
||||
return {
|
||||
quickConfig: {
|
||||
@@ -1098,7 +1151,7 @@ export class GatewayClient {
|
||||
agentNickname: 'ZCLAW',
|
||||
scenarios: ['通用对话', '代码助手', '文档编写'],
|
||||
workspaceDir: config.data_dir || config.home_dir,
|
||||
gatewayUrl: this.baseUrl,
|
||||
gatewayUrl: this.getRestBaseUrl(),
|
||||
defaultModel: config.default_model?.model,
|
||||
defaultProvider: config.default_model?.provider,
|
||||
theme: 'dark',
|
||||
|
||||
@@ -90,10 +90,8 @@ const LLM_CONFIG_KEY = 'zclaw-llm-config';
|
||||
// === Mock Adapter (for testing) ===
|
||||
|
||||
class MockLLMAdapter implements LLMServiceAdapter {
|
||||
private config: LLMConfig;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.config = config;
|
||||
constructor(_config: LLMConfig) {
|
||||
// Config is stored for future use (e.g., custom mock behavior based on config)
|
||||
}
|
||||
|
||||
async complete(messages: LLMMessage[]): Promise<LLMResponse> {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
|
||||
import { useRef, useCallback, useMemo, useEffect, type CSSProperties, type ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
// Type alias for convenience
|
||||
type List = VariableSizeList;
|
||||
import type { ListImperativeAPI } from 'react-window';
|
||||
|
||||
/**
|
||||
* Message item interface for virtualization
|
||||
@@ -52,8 +49,8 @@ const DEFAULT_HEIGHTS: Record<string, number> = {
|
||||
* Hook return type for virtualized message management
|
||||
*/
|
||||
export interface UseVirtualizedMessagesReturn {
|
||||
/** Reference to the VariableSizeList instance */
|
||||
listRef: React.RefObject<VariableSizeList | null>;
|
||||
/** Reference to the List instance */
|
||||
listRef: React.RefObject<ListImperativeAPI | null>;
|
||||
/** Get the current height for a message by id and role */
|
||||
getHeight: (id: string, role: string) => number;
|
||||
/** Update the measured height for a message */
|
||||
@@ -99,7 +96,7 @@ export function useVirtualizedMessages(
|
||||
messages: VirtualizedMessageItem[],
|
||||
defaultHeights: Record<string, number> = DEFAULT_HEIGHTS
|
||||
): UseVirtualizedMessagesReturn {
|
||||
const listRef = useRef<List>(null);
|
||||
const listRef = useRef<ListImperativeAPI>(null);
|
||||
const heightsRef = useRef<Map<string, number>>(new Map());
|
||||
const prevMessagesLengthRef = useRef<number>(0);
|
||||
|
||||
@@ -121,8 +118,7 @@ export function useVirtualizedMessages(
|
||||
const current = heightsRef.current.get(id);
|
||||
if (current !== height) {
|
||||
heightsRef.current.set(id, height);
|
||||
// Reset cache to force recalculation
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
// Height updated - the list will use the new height on next render
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -141,7 +137,7 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const scrollToBottom = useCallback((): void => {
|
||||
if (listRef.current && messages.length > 0) {
|
||||
listRef.current.scrollToItem(messages.length - 1, 'end');
|
||||
listRef.current.scrollToRow({ index: messages.length - 1, align: 'end' });
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
@@ -150,7 +146,7 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const scrollToIndex = useCallback((index: number): void => {
|
||||
if (listRef.current && index >= 0 && index < messages.length) {
|
||||
listRef.current.scrollToItem(index, 'center');
|
||||
listRef.current.scrollToRow({ index, align: 'center' });
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
@@ -159,7 +155,6 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const resetCache = useCallback((): void => {
|
||||
heightsRef.current.clear();
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
128
desktop/src/lib/use-onboarding.ts
Normal file
128
desktop/src/lib/use-onboarding.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* useOnboarding - Hook for detecting and managing first-time user onboarding
|
||||
*
|
||||
* Determines if user needs to go through the onboarding wizard.
|
||||
* Stores completion status in localStorage.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const ONBOARDING_COMPLETED_KEY = 'zclaw-onboarding-completed';
|
||||
const USER_PROFILE_KEY = 'zclaw-user-profile';
|
||||
|
||||
export interface UserProfile {
|
||||
userName: string;
|
||||
userRole?: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface OnboardingState {
|
||||
isNeeded: boolean;
|
||||
isLoading: boolean;
|
||||
userProfile: UserProfile | null;
|
||||
markCompleted: (profile: Omit<UserProfile, 'completedAt'>) => void;
|
||||
resetOnboarding: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage first-time user onboarding
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { isNeeded, isLoading, markCompleted } = useOnboarding();
|
||||
*
|
||||
* if (isNeeded) {
|
||||
* return <OnboardingWizard onComplete={markCompleted} />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useOnboarding(): OnboardingState {
|
||||
const [isNeeded, setIsNeeded] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
|
||||
// Check onboarding status on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const completed = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
|
||||
const profileStr = localStorage.getItem(USER_PROFILE_KEY);
|
||||
|
||||
if (completed === 'true' && profileStr) {
|
||||
const profile = JSON.parse(profileStr) as UserProfile;
|
||||
setUserProfile(profile);
|
||||
setIsNeeded(false);
|
||||
} else {
|
||||
// No onboarding record - first time user
|
||||
setIsNeeded(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[useOnboarding] Failed to check onboarding status:', err);
|
||||
setIsNeeded(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Mark onboarding as completed
|
||||
const markCompleted = useCallback((profile: Omit<UserProfile, 'completedAt'>) => {
|
||||
const fullProfile: UserProfile = {
|
||||
...profile,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true');
|
||||
localStorage.setItem(USER_PROFILE_KEY, JSON.stringify(fullProfile));
|
||||
setUserProfile(fullProfile);
|
||||
setIsNeeded(false);
|
||||
console.log('[useOnboarding] Onboarding completed for user:', profile.userName);
|
||||
} catch (err) {
|
||||
console.error('[useOnboarding] Failed to save onboarding status:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Reset onboarding (for testing or user request)
|
||||
const resetOnboarding = useCallback(() => {
|
||||
try {
|
||||
localStorage.removeItem(ONBOARDING_COMPLETED_KEY);
|
||||
localStorage.removeItem(USER_PROFILE_KEY);
|
||||
setUserProfile(null);
|
||||
setIsNeeded(true);
|
||||
console.log('[useOnboarding] Onboarding reset');
|
||||
} catch (err) {
|
||||
console.error('[useOnboarding] Failed to reset onboarding:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isNeeded,
|
||||
isLoading,
|
||||
userProfile,
|
||||
markCompleted,
|
||||
resetOnboarding,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored user profile without hook (for use outside React components)
|
||||
*/
|
||||
export function getStoredUserProfile(): UserProfile | null {
|
||||
try {
|
||||
const profileStr = localStorage.getItem(USER_PROFILE_KEY);
|
||||
if (profileStr) {
|
||||
return JSON.parse(profileStr) as UserProfile;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[useOnboarding] Failed to get user profile:', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if onboarding is completed (for use outside React components)
|
||||
*/
|
||||
export function isOnboardingCompleted(): boolean {
|
||||
return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true';
|
||||
}
|
||||
|
||||
export default useOnboarding;
|
||||
@@ -123,7 +123,9 @@ export class VectorMemoryService {
|
||||
importance: Math.round((1 - result.score) * 10), // Invert score to importance
|
||||
createdAt: new Date().toISOString(),
|
||||
source: 'auto',
|
||||
tags: (result.metadata as Record<string, unknown>)?.tags ?? [],
|
||||
tags: Array.isArray((result.metadata as Record<string, unknown>)?.tags)
|
||||
? (result.metadata as Record<string, unknown>).tags as string[]
|
||||
: [],
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
accessCount: 0,
|
||||
};
|
||||
@@ -132,7 +134,9 @@ export class VectorMemoryService {
|
||||
memory,
|
||||
score: result.score,
|
||||
uri: result.uri,
|
||||
highlights: (result.metadata as Record<string, unknown>)?.highlights as string[] | undefined,
|
||||
highlights: Array.isArray((result.metadata as Record<string, unknown>)?.highlights)
|
||||
? (result.metadata as Record<string, unknown>).highlights as string[]
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface FindResult {
|
||||
level: ContextLevel;
|
||||
abstract?: string;
|
||||
overview?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GrepOptions {
|
||||
|
||||
@@ -207,7 +207,25 @@ export const useChatStore = create<ChatState>()(
|
||||
return { currentAgent: agent };
|
||||
}
|
||||
|
||||
// Save current conversation before switching
|
||||
const conversations = upsertActiveConversation([...state.conversations], state);
|
||||
|
||||
// Try to find existing conversation for this agent
|
||||
const agentConversation = conversations.find(c => c.agentId === agent.id);
|
||||
|
||||
if (agentConversation) {
|
||||
// Restore the agent's previous conversation
|
||||
return {
|
||||
conversations,
|
||||
currentAgent: agent,
|
||||
messages: [...agentConversation.messages],
|
||||
sessionKey: agentConversation.sessionKey,
|
||||
isStreaming: false,
|
||||
currentConversationId: agentConversation.id,
|
||||
};
|
||||
}
|
||||
|
||||
// No existing conversation, start fresh
|
||||
return {
|
||||
conversations,
|
||||
currentAgent: agent,
|
||||
@@ -627,7 +645,7 @@ export const useChatStore = create<ChatState>()(
|
||||
partialize: (state) => ({
|
||||
conversations: state.conversations,
|
||||
currentModel: state.currentModel,
|
||||
messages: state.messages,
|
||||
currentAgentId: state.currentAgent?.id,
|
||||
currentConversationId: state.currentConversationId,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
@@ -642,6 +660,15 @@ export const useChatStore = create<ChatState>()(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore messages from current conversation if exists
|
||||
if (state?.currentConversationId && state.conversations) {
|
||||
const currentConv = state.conversations.find(c => c.id === state.currentConversationId);
|
||||
if (currentConv) {
|
||||
state.messages = [...currentConv.messages];
|
||||
state.sessionKey = currentConv.sessionKey;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@@ -150,11 +150,27 @@ interface RawApproval {
|
||||
request_type?: string;
|
||||
handId?: string;
|
||||
hand_id?: string;
|
||||
hand_name?: string;
|
||||
handName?: string;
|
||||
run_id?: string;
|
||||
runId?: string;
|
||||
requester?: string;
|
||||
requested_by?: string;
|
||||
requested_at?: string;
|
||||
requestedAt?: string;
|
||||
reason?: string;
|
||||
description?: string;
|
||||
action?: string;
|
||||
params?: Record<string, unknown>;
|
||||
status?: string;
|
||||
createdAt?: string;
|
||||
created_at?: string;
|
||||
responded_at?: string;
|
||||
respondedAt?: string;
|
||||
responded_by?: string;
|
||||
respondedBy?: string;
|
||||
response_reason?: string;
|
||||
responseReason?: string;
|
||||
details?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
@@ -173,6 +189,7 @@ interface RawSession {
|
||||
updated_at?: string;
|
||||
messageCount?: number;
|
||||
message_count?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RawSessionMessage {
|
||||
@@ -184,6 +201,7 @@ interface RawSessionMessage {
|
||||
createdAt?: string;
|
||||
created_at?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
tokens?: { input?: number; output?: number };
|
||||
}
|
||||
|
||||
interface RawWorkflowRun {
|
||||
@@ -202,6 +220,8 @@ interface RawWorkflowRun {
|
||||
totalSteps?: number;
|
||||
total_steps?: number;
|
||||
error?: string;
|
||||
step?: string;
|
||||
result?: unknown;
|
||||
}
|
||||
|
||||
// === OpenFang Types ===
|
||||
@@ -780,21 +800,22 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listClones();
|
||||
// API 可能返回数组,也可能返回 {clones: [...]} 或 {agents: [...]}
|
||||
const clones = Array.isArray(result) ? result : (result?.clones || result?.agents || []);
|
||||
set({ clones });
|
||||
useChatStore.getState().syncAgents(clones);
|
||||
let clones = Array.isArray(result) ? result : (result?.clones || result?.agents || []);
|
||||
|
||||
// Set default agent ID if we have agents and none is set
|
||||
console.log('[Gateway] Loaded agents:', clones.length, clones.map((c: { id?: string; name?: string }) => ({ id: c.id, name: c.name })));
|
||||
|
||||
// Set default agent ID if we have agents
|
||||
if (clones.length > 0 && clones[0].id) {
|
||||
const client = get().client;
|
||||
const currentDefault = client.getDefaultAgentId();
|
||||
// Only set if the default doesn't exist in the list
|
||||
const defaultExists = clones.some((c) => c.id === currentDefault);
|
||||
if (!defaultExists) {
|
||||
client.setDefaultAgentId(clones[0].id);
|
||||
}
|
||||
client.setDefaultAgentId(clones[0].id);
|
||||
console.log('[Gateway] Set default agent ID:', clones[0].id);
|
||||
}
|
||||
} catch { /* ignore if method not available */ }
|
||||
|
||||
set({ clones });
|
||||
useChatStore.getState().syncAgents(clones);
|
||||
} catch (err) {
|
||||
console.warn('[Gateway] Failed to load clones:', err);
|
||||
}
|
||||
},
|
||||
|
||||
createClone: async (opts) => {
|
||||
@@ -1084,7 +1105,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
const message = err?.message || '读取本地 Gateway 状态失败';
|
||||
const message = err instanceof Error ? err.message : '读取本地 Gateway 状态失败';
|
||||
const nextStatus = {
|
||||
...get().localGateway,
|
||||
supported: true,
|
||||
@@ -1108,7 +1129,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
const message = err?.message || '启动本地 Gateway 失败';
|
||||
const message = err instanceof Error ? err.message : '启动本地 Gateway 失败';
|
||||
const nextStatus = {
|
||||
...get().localGateway,
|
||||
supported: true,
|
||||
@@ -1132,7 +1153,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
const message = err?.message || '停止本地 Gateway 失败';
|
||||
const message = err instanceof Error ? err.message : '停止本地 Gateway 失败';
|
||||
const nextStatus = {
|
||||
...get().localGateway,
|
||||
supported: true,
|
||||
@@ -1156,7 +1177,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
const message = err?.message || '重启本地 Gateway 失败';
|
||||
const message = err instanceof Error ? err.message : '重启本地 Gateway 失败';
|
||||
const nextStatus = {
|
||||
...get().localGateway,
|
||||
supported: true,
|
||||
@@ -1522,13 +1543,13 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({
|
||||
id: a.id || a.approval_id || '',
|
||||
handName: a.hand_name || a.handName || '',
|
||||
runId: a.run_id || a.runId || '',
|
||||
status: a.status || 'pending',
|
||||
runId: a.run_id || a.runId,
|
||||
status: (a.status || 'pending') as ApprovalStatus,
|
||||
requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(),
|
||||
requestedBy: a.requested_by || a.requestedBy || '',
|
||||
reason: a.reason || a.description || '',
|
||||
action: a.action || 'execute',
|
||||
params: a.params || {},
|
||||
requestedBy: a.requested_by || a.requester,
|
||||
reason: a.reason || a.description,
|
||||
action: a.action,
|
||||
params: a.params,
|
||||
respondedAt: a.responded_at || a.respondedAt,
|
||||
respondedBy: a.responded_by || a.respondedBy,
|
||||
responseReason: a.response_reason || a.responseReason,
|
||||
@@ -1553,15 +1574,17 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
loadSessions: async (opts?: { limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const result = await get().client.listSessions(opts);
|
||||
const sessions: Session[] = (result?.sessions || []).map((s: RawSession) => ({
|
||||
id: s.id,
|
||||
agentId: s.agent_id,
|
||||
createdAt: s.created_at,
|
||||
updatedAt: s.updated_at,
|
||||
messageCount: s.message_count,
|
||||
status: s.status,
|
||||
metadata: s.metadata,
|
||||
}));
|
||||
const sessions: Session[] = (result?.sessions || [])
|
||||
.filter((s: RawSession) => s.id || s.session_id) // Filter out sessions without IDs
|
||||
.map((s: RawSession) => ({
|
||||
id: s.id || s.session_id || '',
|
||||
agentId: s.agent_id || s.agentId || '',
|
||||
createdAt: s.created_at || s.createdAt || new Date().toISOString(),
|
||||
updatedAt: s.updated_at || s.updatedAt,
|
||||
messageCount: s.message_count || s.messageCount,
|
||||
status: s.status as Session['status'],
|
||||
metadata: s.metadata,
|
||||
}));
|
||||
set({ sessions });
|
||||
} catch {
|
||||
/* ignore if sessions API not available */
|
||||
@@ -1631,10 +1654,10 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.getSessionMessages(sessionId, opts);
|
||||
const messages: SessionMessage[] = (result?.messages || []).map((m: RawSessionMessage) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
createdAt: m.created_at,
|
||||
id: m.id || m.message_id || '',
|
||||
role: (m.role || 'user') as 'user' | 'assistant' | 'system',
|
||||
content: m.content || '',
|
||||
createdAt: m.created_at || m.createdAt || new Date().toISOString(),
|
||||
tokens: m.tokens,
|
||||
}));
|
||||
set(state => ({
|
||||
@@ -1668,13 +1691,10 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listWorkflowRuns(workflowId, opts);
|
||||
const runs: WorkflowRun[] = (result?.runs || []).map((r: RawWorkflowRun) => ({
|
||||
runId: r.runId || r.run_id,
|
||||
status: r.status,
|
||||
startedAt: r.startedAt || r.started_at,
|
||||
completedAt: r.completedAt || r.completed_at,
|
||||
step: r.step,
|
||||
runId: r.runId || r.run_id || r.id || '',
|
||||
status: r.status || 'unknown',
|
||||
step: r.step || r.currentStep?.toString(),
|
||||
result: r.result,
|
||||
error: r.error,
|
||||
}));
|
||||
// Store runs by workflow ID
|
||||
set(state => ({
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* - Browser: Browser automation
|
||||
*/
|
||||
|
||||
export type HandStatus = 'idle' | 'running' | 'needs_approval' | 'completed' | 'error';
|
||||
export type HandStatus = 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed';
|
||||
|
||||
export type HandId = 'clip' | 'lead' | 'collector' | 'predictor' | 'researcher' | 'twitter' | 'browser';
|
||||
|
||||
@@ -35,10 +35,10 @@ export interface HandParameter {
|
||||
}
|
||||
|
||||
export interface Hand {
|
||||
id: HandId;
|
||||
id: string; // Can be HandId or any string from backend
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
icon?: string; // Optional, as backend may not provide it
|
||||
status: HandStatus;
|
||||
parameters?: HandParameter[];
|
||||
lastRun?: string;
|
||||
|
||||
Reference in New Issue
Block a user