feat(automation): complete unified automation system redesign
Phase 4 completion: - Add ApprovalQueue component for managing pending approvals - Add ExecutionResult component for displaying hand/workflow results - Update Sidebar navigation to use unified AutomationPanel - Replace separate 'hands' and 'workflow' tabs with single 'automation' tab - Fix TypeScript type safety issues with unknown types in JSX expressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17
desktop/src-tauri/Cargo.lock
generated
@@ -735,6 +735,7 @@ dependencies = [
|
|||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"fantoccini",
|
"fantoccini",
|
||||||
"futures",
|
"futures",
|
||||||
|
"keyring",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2117,6 +2118,16 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyring"
|
||||||
|
version = "3.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kuchikiki"
|
name = "kuchikiki"
|
||||||
version = "0.8.8-speedreader"
|
version = "0.8.8-speedreader"
|
||||||
@@ -5913,6 +5924,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|||||||
@@ -35,3 +35,6 @@ base64 = "0.22"
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
# Secure storage (OS keyring/keychain)
|
||||||
|
keyring = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ mod llm;
|
|||||||
// Browser automation module (Fantoccini-based Browser Hand)
|
// Browser automation module (Fantoccini-based Browser Hand)
|
||||||
mod browser;
|
mod browser;
|
||||||
|
|
||||||
|
// Secure storage module for OS keyring/keychain
|
||||||
|
mod secure_storage;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -1066,7 +1069,12 @@ pub fn run() {
|
|||||||
browser::commands::browser_element_screenshot,
|
browser::commands::browser_element_screenshot,
|
||||||
browser::commands::browser_get_source,
|
browser::commands::browser_get_source,
|
||||||
browser::commands::browser_scrape_page,
|
browser::commands::browser_scrape_page,
|
||||||
browser::commands::browser_fill_form
|
browser::commands::browser_fill_form,
|
||||||
|
// Secure storage commands (OS keyring/keychain)
|
||||||
|
secure_storage::secure_store_set,
|
||||||
|
secure_storage::secure_store_get,
|
||||||
|
secure_storage::secure_store_delete,
|
||||||
|
secure_storage::secure_store_is_available
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ import { Sidebar, MainViewType } from './components/Sidebar';
|
|||||||
import { ChatArea } from './components/ChatArea';
|
import { ChatArea } from './components/ChatArea';
|
||||||
import { RightPanel } from './components/RightPanel';
|
import { RightPanel } from './components/RightPanel';
|
||||||
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
||||||
import { HandTaskPanel } from './components/HandTaskPanel';
|
import { AutomationPanel } from './components/Automation';
|
||||||
import { SchedulerPanel } from './components/SchedulerPanel';
|
|
||||||
import { TeamCollaborationView } from './components/TeamCollaborationView';
|
import { TeamCollaborationView } from './components/TeamCollaborationView';
|
||||||
|
import { TeamOrchestrator } from './components/TeamOrchestrator';
|
||||||
import { SwarmDashboard } from './components/SwarmDashboard';
|
import { SwarmDashboard } from './components/SwarmDashboard';
|
||||||
import { SkillMarket } from './components/SkillMarket';
|
import { SkillMarket } from './components/SkillMarket';
|
||||||
import { AgentOnboardingWizard } from './components/AgentOnboardingWizard';
|
import { AgentOnboardingWizard } from './components/AgentOnboardingWizard';
|
||||||
import { HandApprovalModal } from './components/HandApprovalModal';
|
import { HandApprovalModal } from './components/HandApprovalModal';
|
||||||
|
import { TopBar } from './components/TopBar';
|
||||||
|
import { DetailDrawer } from './components/DetailDrawer';
|
||||||
import { useGatewayStore, type HandRun } from './store/gatewayStore';
|
import { useGatewayStore, type HandRun } from './store/gatewayStore';
|
||||||
import { useTeamStore } from './store/teamStore';
|
import { useTeamStore } from './store/teamStore';
|
||||||
import { useChatStore } from './store/chatStore';
|
import { useChatStore } from './store/chatStore';
|
||||||
import { getStoredGatewayToken } from './lib/gateway-client';
|
import { getStoredGatewayToken } from './lib/gateway-client';
|
||||||
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
|
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
|
||||||
import { Bot, Users, Loader2 } from 'lucide-react';
|
import { Users, Loader2, Settings } from 'lucide-react';
|
||||||
import { EmptyState } from './components/ui';
|
import { EmptyState } from './components/ui';
|
||||||
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
|
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
|
||||||
import { useOnboarding } from './lib/use-onboarding';
|
import { useOnboarding } from './lib/use-onboarding';
|
||||||
@@ -40,15 +42,16 @@ function BootstrapScreen({ status }: { status: string }) {
|
|||||||
function App() {
|
function App() {
|
||||||
const [view, setView] = useState<View>('main');
|
const [view, setView] = useState<View>('main');
|
||||||
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
|
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
|
||||||
const [selectedHandId, setSelectedHandId] = useState<string | undefined>(undefined);
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(undefined);
|
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(undefined);
|
||||||
const [bootstrapping, setBootstrapping] = useState(true);
|
const [bootstrapping, setBootstrapping] = useState(true);
|
||||||
const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...');
|
const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...');
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||||
|
const [showDetailDrawer, setShowDetailDrawer] = useState(false);
|
||||||
|
|
||||||
// Hand Approval state
|
// Hand Approval state
|
||||||
const [pendingApprovalRun, setPendingApprovalRun] = useState<HandRun | null>(null);
|
const [pendingApprovalRun, setPendingApprovalRun] = useState<HandRun | null>(null);
|
||||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||||
|
const [teamViewMode, setTeamViewMode] = useState<'collaboration' | 'orchestrator'>('collaboration');
|
||||||
|
|
||||||
const { connect, hands, approveHand, loadHands } = useGatewayStore();
|
const { connect, hands, approveHand, loadHands } = useGatewayStore();
|
||||||
const { activeTeam, setActiveTeam, teams } = useTeamStore();
|
const { activeTeam, setActiveTeam, teams } = useTeamStore();
|
||||||
@@ -182,13 +185,9 @@ function App() {
|
|||||||
setShowOnboarding(false);
|
setShowOnboarding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当切换到非 hands 视图时清除选中的 Hand
|
// 处理主视图切换
|
||||||
const handleMainViewChange = (view: MainViewType) => {
|
const handleMainViewChange = (view: MainViewType) => {
|
||||||
setMainContentView(view);
|
setMainContentView(view);
|
||||||
if (view !== 'hands') {
|
|
||||||
// 可选:清除选中的 Hand
|
|
||||||
// setSelectedHandId(undefined);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectTeam = (teamId: string) => {
|
const handleSelectTeam = (teamId: string) => {
|
||||||
@@ -227,18 +226,24 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex overflow-hidden text-gray-800 text-sm">
|
<div className="h-screen flex overflow-hidden text-gray-800 text-sm bg-white dark:bg-gray-950">
|
||||||
{/* 左侧边栏 */}
|
{/* 左侧边栏 */}
|
||||||
<Sidebar
|
<Sidebar
|
||||||
onOpenSettings={() => setView('settings')}
|
onOpenSettings={() => setView('settings')}
|
||||||
onMainViewChange={handleMainViewChange}
|
onMainViewChange={handleMainViewChange}
|
||||||
selectedHandId={selectedHandId}
|
|
||||||
onSelectHand={setSelectedHandId}
|
|
||||||
selectedTeamId={selectedTeamId}
|
selectedTeamId={selectedTeamId}
|
||||||
onSelectTeam={handleSelectTeam}
|
onSelectTeam={handleSelectTeam}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 中间区域 */}
|
{/* 主内容区 */}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* 顶部工具栏 */}
|
||||||
|
<TopBar
|
||||||
|
title="ZCLAW"
|
||||||
|
onOpenDetail={() => setShowDetailDrawer(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.main
|
<motion.main
|
||||||
key={mainContentView}
|
key={mainContentView}
|
||||||
@@ -247,36 +252,59 @@ function App() {
|
|||||||
animate="animate"
|
animate="animate"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
transition={defaultTransition}
|
transition={defaultTransition}
|
||||||
className="flex-1 flex flex-col bg-white relative overflow-hidden"
|
className="flex-1 overflow-hidden relative"
|
||||||
>
|
>
|
||||||
{mainContentView === 'hands' && selectedHandId ? (
|
{mainContentView === 'automation' ? (
|
||||||
<HandTaskPanel
|
|
||||||
handId={selectedHandId}
|
|
||||||
onBack={() => setSelectedHandId(undefined)}
|
|
||||||
/>
|
|
||||||
) : mainContentView === 'hands' ? (
|
|
||||||
<EmptyState
|
|
||||||
icon={<Bot className="w-8 h-8" />}
|
|
||||||
title="Select a Hand"
|
|
||||||
description="Choose an autonomous capability package from the list on the left to view its task list and execution results."
|
|
||||||
/>
|
|
||||||
) : mainContentView === 'workflow' ? (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={fadeInVariants}
|
variants={fadeInVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className="flex-1 overflow-y-auto"
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
<SchedulerPanel />
|
<AutomationPanel />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : mainContentView === 'team' ? (
|
) : mainContentView === 'team' ? (
|
||||||
activeTeam ? (
|
activeTeam ? (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Team View Tabs */}
|
||||||
|
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setTeamViewMode('collaboration')}
|
||||||
|
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
teamViewMode === 'collaboration'
|
||||||
|
? 'text-orange-600 dark:text-orange-400 border-orange-500'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 border-transparent hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
协作视图
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTeamViewMode('orchestrator')}
|
||||||
|
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
teamViewMode === 'orchestrator'
|
||||||
|
? 'text-orange-600 dark:text-orange-400 border-orange-500'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 border-transparent hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
编排管理
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{teamViewMode === 'orchestrator' ? (
|
||||||
|
<TeamOrchestrator isOpen={true} onClose={() => setTeamViewMode('collaboration')} />
|
||||||
|
) : (
|
||||||
<TeamCollaborationView teamId={activeTeam.id} />
|
<TeamCollaborationView teamId={activeTeam.id} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<Users className="w-8 h-8" />}
|
icon={<Users className="w-8 h-8" />}
|
||||||
title="Select or Create a Team"
|
title="选择或创建团队"
|
||||||
description="Choose a team from the list on the left, or click + to create a new multi-Agent collaboration team."
|
description="从左侧列表中选择一个团队,或点击 + 创建新的多 Agent 协作团队。"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : mainContentView === 'swarm' ? (
|
) : mainContentView === 'swarm' ? (
|
||||||
@@ -284,7 +312,7 @@ function App() {
|
|||||||
variants={fadeInVariants}
|
variants={fadeInVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className="flex-1 overflow-hidden"
|
className="h-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<SwarmDashboard />
|
<SwarmDashboard />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -293,7 +321,7 @@ function App() {
|
|||||||
variants={fadeInVariants}
|
variants={fadeInVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className="flex-1 overflow-hidden"
|
className="h-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<SkillMarket />
|
<SkillMarket />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -302,9 +330,16 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</motion.main>
|
</motion.main>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 右侧边栏 */}
|
{/* 详情抽屉 - 按需显示 */}
|
||||||
|
<DetailDrawer
|
||||||
|
open={showDetailDrawer}
|
||||||
|
onClose={() => setShowDetailDrawer(false)}
|
||||||
|
title="详情"
|
||||||
|
>
|
||||||
<RightPanel />
|
<RightPanel />
|
||||||
|
</DetailDrawer>
|
||||||
|
|
||||||
{/* Hand Approval Modal (global) */}
|
{/* Hand Approval Modal (global) */}
|
||||||
<HandApprovalModal
|
<HandApprovalModal
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
X,
|
X,
|
||||||
Download,
|
Download,
|
||||||
Clock,
|
Clock,
|
||||||
|
BarChart3,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button, EmptyState } from './ui';
|
import { Button, EmptyState, Badge } from './ui';
|
||||||
import { useActiveLearningStore } from '../store/activeLearningStore';
|
import { useActiveLearningStore } from '../store/activeLearningStore';
|
||||||
import {
|
import {
|
||||||
type LearningEvent,
|
type LearningEvent,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
type LearningEventType,
|
type LearningEventType,
|
||||||
} from '../types/active-learning';
|
} from '../types/active-learning';
|
||||||
import { useChatStore } from '../store/chatStore';
|
import { useChatStore } from '../store/chatStore';
|
||||||
|
import { cardHover, defaultTransition } from '../lib/animations';
|
||||||
|
|
||||||
// === Constants ===
|
// === Constants ===
|
||||||
|
|
||||||
@@ -58,10 +60,12 @@ function EventItem({ event, onAcknowledge }: EventItemProps) {
|
|||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
className={`p-3 rounded-lg border ${
|
className={`p-3 rounded-lg border ${
|
||||||
event.acknowledged
|
event.acknowledged
|
||||||
? 'bg-gray-800/30 border-gray-700'
|
? 'bg-gray-50 dark:bg-gray-800 border-gray-100 dark:border-gray-700'
|
||||||
: 'bg-blue-900/20 border-blue-700'
|
: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -70,11 +74,11 @@ function EventItem({ event, onAcknowledge }: EventItemProps) {
|
|||||||
<span className={`text-xs px-2 py-0.5 rounded ${typeInfo.color}`}>
|
<span className={`text-xs px-2 py-0.5 rounded ${typeInfo.color}`}>
|
||||||
{typeInfo.label}
|
{typeInfo.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">{timeAgo}</span>
|
<span className="text-xs text-gray-500 dark:text-gray-400">{timeAgo}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-300 truncate">{event.observation}</p>
|
<p className="text-sm text-gray-700 dark:text-gray-300 truncate">{event.observation}</p>
|
||||||
{event.inferredPreference && (
|
{event.inferredPreference && (
|
||||||
<p className="text-xs text-gray-500 mt-1">→ {event.inferredPreference}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">→ {event.inferredPreference}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ function EventItem({ event, onAcknowledge }: EventItemProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500">
|
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span>置信度: {(event.confidence * 100).toFixed(0)}%</span>
|
<span>置信度: {(event.confidence * 100).toFixed(0)}%</span>
|
||||||
{event.appliedCount > 0 && (
|
{event.appliedCount > 0 && (
|
||||||
<span>• 应用 {event.appliedCount} 次</span>
|
<span>• 应用 {event.appliedCount} 次</span>
|
||||||
@@ -111,13 +115,15 @@ function SuggestionCard({ suggestion, onApply, onDismiss }: SuggestionCardProps)
|
|||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
className="p-4 bg-gradient-to-r from-amber-900/20 to-transparent rounded-lg border border-amber-700/50"
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="p-4 bg-gradient-to-r from-amber-50 to-transparent dark:from-amber-900/20 dark:to-transparent rounded-lg border border-amber-200 dark:border-amber-700/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Lightbulb className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
<Lightbulb className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm text-gray-200">{suggestion.suggestion}</p>
|
<p className="text-sm text-gray-700 dark:text-gray-200">{suggestion.suggestion}</p>
|
||||||
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500">
|
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span>置信度: {(suggestion.confidence * 100).toFixed(0)}%</span>
|
<span>置信度: {(suggestion.confidence * 100).toFixed(0)}%</span>
|
||||||
{daysLeft > 0 && <span>• {daysLeft} 天后过期</span>}
|
{daysLeft > 0 && <span>• {daysLeft} 天后过期</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -204,63 +210,77 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
}, [agentId, clearEvents]);
|
}, [agentId, clearEvents]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col h-full bg-gray-900 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
{/* 夨览栏 */}
|
{/* 启用开关和导出 */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 p-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<Brain className="w-4 h-4 text-blue-500" />
|
||||||
|
<span>主动学习</span>
|
||||||
|
<Badge variant={config.enabled ? 'success' : 'default'} className="ml-1">
|
||||||
|
{config.enabled ? '已启用' : '已禁用'}
|
||||||
|
</Badge>
|
||||||
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Brain className="w-5 h-5 text-blue-400" />
|
|
||||||
<h2 className="text-lg font-semibold text-white">主动学习</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="flex items-center gap-2 text-sm text-gray-400">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={config.enabled}
|
checked={config.enabled}
|
||||||
onChange={(e) => setConfig({ enabled: e.target.checked })}
|
onChange={(e) => setConfig({ enabled: e.target.checked })}
|
||||||
className="rounded"
|
className="rounded border-gray-300 dark:border-gray-600"
|
||||||
/>
|
/>
|
||||||
启用
|
<Button variant="ghost" size="sm" onClick={handleExport} title="导出数据">
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button variant="ghost" size="sm" onClick={handleExport}>
|
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* 统计概览 */}
|
{/* 统计概览 */}
|
||||||
<div className="grid grid-cols-4 gap-2 p-3 bg-gray-800/30">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 p-3"
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-1.5">
|
||||||
|
<BarChart3 className="w-3.5 h-3.5" />
|
||||||
|
学习统计
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-400">{stats.totalEvents}</div>
|
<div className="text-lg font-bold text-blue-500">{stats.totalEvents}</div>
|
||||||
<div className="text-xs text-gray-500">学习事件</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">事件</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-400">{stats.totalPatterns}</div>
|
<div className="text-lg font-bold text-green-500">{stats.totalPatterns}</div>
|
||||||
<div className="text-xs text-gray-500">学习模式</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">模式</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-amber-400">{agentSuggestions.length}</div>
|
<div className="text-lg font-bold text-amber-500">{agentSuggestions.length}</div>
|
||||||
<div className="text-xs text-gray-500">待处理建议</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">建议</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-purple-400">
|
<div className="text-lg font-bold text-purple-500">
|
||||||
{(stats.avgConfidence * 100).toFixed(0)}%
|
{(stats.avgConfidence * 100).toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">平均置信度</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">置信度</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Tab 切换 */}
|
{/* Tab 切换 */}
|
||||||
<div className="flex border-b border-gray-800">
|
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||||
{(['suggestions', 'events', 'patterns'] as const).map(tab => (
|
{(['suggestions', 'events', 'patterns'] as const).map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? 'text-blue-400 border-b-2 border-blue-400'
|
? 'text-emerald-600 dark:text-emerald-400 border-b-2 border-emerald-500'
|
||||||
: 'text-gray-500 hover:text-gray-300'
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab === 'suggestions' && '建议'}
|
{tab === 'suggestions' && '建议'}
|
||||||
@@ -271,7 +291,7 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="flex-1 overflow-y-auto p-3">
|
<div className="space-y-3">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{activeTab === 'suggestions' && (
|
{activeTab === 'suggestions' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -283,9 +303,10 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
>
|
>
|
||||||
{agentSuggestions.length === 0 ? (
|
{agentSuggestions.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<Lightbulb className="w-12 h-12" />}
|
icon={<Lightbulb className="w-8 h-8" />}
|
||||||
title="暂无学习建议"
|
title="暂无学习建议"
|
||||||
description="系统会根据您的反馈自动生成改进建议"
|
description="系统会根据您的反馈自动生成改进建议"
|
||||||
|
className="py-4"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
agentSuggestions.map(suggestion => (
|
agentSuggestions.map(suggestion => (
|
||||||
@@ -310,9 +331,10 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
>
|
>
|
||||||
{agentEvents.length === 0 ? (
|
{agentEvents.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<Clock className="w-12 h-12" />}
|
icon={<Clock className="w-8 h-8" />}
|
||||||
title="暂无学习事件"
|
title="暂无学习事件"
|
||||||
description="开始对话后,系统会自动记录学习事件"
|
description="开始对话后,系统会自动记录学习事件"
|
||||||
|
className="py-4"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
agentEvents.map(event => (
|
agentEvents.map(event => (
|
||||||
@@ -336,32 +358,35 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
>
|
>
|
||||||
{agentPatterns.length === 0 ? (
|
{agentPatterns.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<TrendingUp className="w-12 h-12" />}
|
icon={<TrendingUp className="w-8 h-8" />}
|
||||||
title="暂无学习模式"
|
title="暂无学习模式"
|
||||||
description="积累更多反馈后,系统会识别出行为模式"
|
description="积累更多反馈后,系统会识别出行为模式"
|
||||||
|
className="py-4"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
agentPatterns.map(pattern => {
|
agentPatterns.map(pattern => {
|
||||||
const typeInfo = PATTERN_TYPE_LABELS[pattern.type] || { label: pattern.type, icon: '📊' };
|
const typeInfo = PATTERN_TYPE_LABELS[pattern.type] || { label: pattern.type, icon: '📊' };
|
||||||
return (
|
return (
|
||||||
<div
|
<motion.div
|
||||||
key={`${pattern.agentId}-${pattern.pattern}`}
|
key={`${pattern.agentId}-${pattern.pattern}`}
|
||||||
className="p-3 bg-gray-800/50 rounded-lg border border-gray-700"
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{typeInfo.icon}</span>
|
<span>{typeInfo.icon}</span>
|
||||||
<span className="text-sm font-medium text-white">{typeInfo.label}</span>
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">{typeInfo.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-700 text-gray-300">
|
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
|
||||||
{(pattern.confidence * 100).toFixed(0)}%
|
{(pattern.confidence * 100).toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400">{pattern.description}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">{pattern.description}</p>
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{pattern.examples.length} 个示例
|
{pattern.examples.length} 个示例
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
@@ -371,13 +396,13 @@ export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部操作栏 */}
|
{/* 底部操作栏 */}
|
||||||
<div className="flex items-center justify-between p-3 border-t border-gray-800">
|
<div className="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
上次更新: {agentEvents[0] ? getTimeAgo(agentEvents[0].timestamp) : '无'}
|
上次更新: {agentEvents[0] ? getTimeAgo(agentEvents[0].timestamp) : '无'}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={handleClear} className="text-red-400">
|
<Button variant="ghost" size="sm" onClick={handleClear} className="text-red-500 hover:text-red-600">
|
||||||
<X className="w-3 h-3 mr-1" />
|
<X className="w-3 h-3 mr-1" />
|
||||||
清除数据
|
清除
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
352
desktop/src/components/Automation/ApprovalQueue.tsx
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* ApprovalQueue - Approval Management Component
|
||||||
|
*
|
||||||
|
* Displays pending approvals for hand executions that require
|
||||||
|
* human approval, with approve/reject actions.
|
||||||
|
*
|
||||||
|
* @module components/Automation/ApprovalQueue
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useHandStore } from '../../store/handStore';
|
||||||
|
import type { Approval, ApprovalStatus } from '../../store/handStore';
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
RefreshCw,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useToast } from '../ui/Toast';
|
||||||
|
|
||||||
|
// === Status Config ===
|
||||||
|
|
||||||
|
const STATUS_CONFIG: Record<ApprovalStatus, {
|
||||||
|
label: string;
|
||||||
|
className: string;
|
||||||
|
icon: typeof CheckCircle;
|
||||||
|
}> = {
|
||||||
|
pending: {
|
||||||
|
label: '待处理',
|
||||||
|
className: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||||
|
icon: Clock,
|
||||||
|
},
|
||||||
|
approved: {
|
||||||
|
label: '已批准',
|
||||||
|
className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
icon: CheckCircle,
|
||||||
|
},
|
||||||
|
rejected: {
|
||||||
|
label: '已拒绝',
|
||||||
|
className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
icon: XCircle,
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
label: '已过期',
|
||||||
|
className: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Component Props ===
|
||||||
|
|
||||||
|
interface ApprovalQueueProps {
|
||||||
|
showFilters?: boolean;
|
||||||
|
maxHeight?: string;
|
||||||
|
onApprove?: (approval: Approval) => void;
|
||||||
|
onReject?: (approval: Approval) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Approval Card Component ===
|
||||||
|
|
||||||
|
interface ApprovalCardProps {
|
||||||
|
approval: Approval;
|
||||||
|
onApprove: () => Promise<void>;
|
||||||
|
onReject: (reason: string) => Promise<void>;
|
||||||
|
isProcessing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApprovalCard({ approval, onApprove, onReject, isProcessing }: ApprovalCardProps) {
|
||||||
|
const [showRejectInput, setShowRejectInput] = useState(false);
|
||||||
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
|
const StatusIcon = STATUS_CONFIG[approval.status].icon;
|
||||||
|
|
||||||
|
const handleReject = useCallback(async () => {
|
||||||
|
if (!rejectReason.trim()) {
|
||||||
|
setShowRejectInput(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onReject(rejectReason);
|
||||||
|
setShowRejectInput(false);
|
||||||
|
setRejectReason('');
|
||||||
|
}, [rejectReason, onReject]);
|
||||||
|
|
||||||
|
const timeAgo = useCallback((dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffMins < 1) return '刚刚';
|
||||||
|
if (diffMins < 60) return `${diffMins} 分钟前`;
|
||||||
|
if (diffHours < 24) return `${diffHours} 小时前`;
|
||||||
|
return `${diffDays} 天前`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${STATUS_CONFIG[approval.status].className}`}>
|
||||||
|
<StatusIcon className="w-3 h-3 inline mr-1" />
|
||||||
|
{STATUS_CONFIG[approval.status].label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{timeAgo(approval.requestedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white mb-1">
|
||||||
|
{approval.handName}
|
||||||
|
</h4>
|
||||||
|
{approval.reason && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">{approval.reason}</p>
|
||||||
|
)}
|
||||||
|
{approval.action && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||||
|
操作: {approval.action}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Params Preview */}
|
||||||
|
{approval.params && Object.keys(approval.params).length > 0 && (
|
||||||
|
<div className="mb-3 p-2 bg-gray-50 dark:bg-gray-900 rounded text-xs">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mb-1">参数:</p>
|
||||||
|
<pre className="text-gray-700 dark:text-gray-300 overflow-x-auto">
|
||||||
|
{JSON.stringify(approval.params, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reject Input */}
|
||||||
|
{showRejectInput && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<textarea
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
placeholder="请输入拒绝原因..."
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white resize-none"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
{approval.status === 'pending' && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onApprove}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="flex-1 px-3 py-1.5 text-sm bg-green-500 text-white rounded-md hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
批准
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleReject}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="flex-1 px-3 py-1.5 text-sm bg-red-500 text-white rounded-md hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
拒绝
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Response Info */}
|
||||||
|
{approval.status !== 'pending' && approval.respondedAt && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{approval.respondedBy && `由 ${approval.respondedBy} `}
|
||||||
|
{STATUS_CONFIG[approval.status].label}
|
||||||
|
{approval.responseReason && ` - ${approval.responseReason}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Main Component ===
|
||||||
|
|
||||||
|
export function ApprovalQueue({
|
||||||
|
showFilters = true,
|
||||||
|
maxHeight = '400px',
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
}: ApprovalQueueProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Store state
|
||||||
|
const approvals = useHandStore(s => s.approvals);
|
||||||
|
const loadApprovals = useHandStore(s => s.loadApprovals);
|
||||||
|
const respondToApproval = useHandStore(s => s.respondToApproval);
|
||||||
|
const isLoading = useHandStore(s => s.isLoading);
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const [statusFilter, setStatusFilter] = useState<ApprovalStatus | 'all'>('pending');
|
||||||
|
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Load approvals on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadApprovals(statusFilter === 'all' ? undefined : statusFilter);
|
||||||
|
}, [loadApprovals, statusFilter]);
|
||||||
|
|
||||||
|
// Handle approve
|
||||||
|
const handleApprove = useCallback(async (approval: Approval) => {
|
||||||
|
setProcessingIds(prev => new Set(prev).add(approval.id));
|
||||||
|
try {
|
||||||
|
await respondToApproval(approval.id, true);
|
||||||
|
toast(`已批准: ${approval.handName}`, 'success');
|
||||||
|
onApprove?.(approval);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
toast(`批准失败: ${errorMsg}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setProcessingIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(approval.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [respondToApproval, toast, onApprove]);
|
||||||
|
|
||||||
|
// Handle reject
|
||||||
|
const handleReject = useCallback(async (approval: Approval, reason: string) => {
|
||||||
|
setProcessingIds(prev => new Set(prev).add(approval.id));
|
||||||
|
try {
|
||||||
|
await respondToApproval(approval.id, false, reason);
|
||||||
|
toast(`已拒绝: ${approval.handName}`, 'success');
|
||||||
|
onReject?.(approval);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
toast(`拒绝失败: ${errorMsg}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setProcessingIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(approval.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [respondToApproval, toast, onReject]);
|
||||||
|
|
||||||
|
// Filter approvals
|
||||||
|
const filteredApprovals = statusFilter === 'all'
|
||||||
|
? approvals
|
||||||
|
: approvals.filter(a => a.status === statusFilter);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const stats = {
|
||||||
|
pending: approvals.filter(a => a.status === 'pending').length,
|
||||||
|
approved: approvals.filter(a => a.status === 'approved').length,
|
||||||
|
rejected: approvals.filter(a => a.status === 'rejected').length,
|
||||||
|
expired: approvals.filter(a => a.status === 'expired').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5 text-orange-500" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
审批队列
|
||||||
|
</h2>
|
||||||
|
{stats.pending > 0 && (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400 rounded-full">
|
||||||
|
{stats.pending} 待处理
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => loadApprovals(statusFilter === 'all' ? undefined : statusFilter)}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 disabled:opacity-50"
|
||||||
|
title="刷新"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
{showFilters && (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-700 overflow-x-auto">
|
||||||
|
{[
|
||||||
|
{ value: 'pending', label: '待处理', count: stats.pending },
|
||||||
|
{ value: 'approved', label: '已批准', count: stats.approved },
|
||||||
|
{ value: 'rejected', label: '已拒绝', count: stats.rejected },
|
||||||
|
{ value: 'all', label: '全部', count: approvals.length },
|
||||||
|
].map(option => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => setStatusFilter(option.value as ApprovalStatus | 'all')}
|
||||||
|
className={`flex items-center gap-1 px-3 py-1 text-sm rounded-full whitespace-nowrap transition-colors ${
|
||||||
|
statusFilter === option.value
|
||||||
|
? 'bg-orange-500 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
<span className={`text-xs ${statusFilter === option.value ? 'text-white/80' : 'text-gray-500 dark:text-gray-400'}`}>
|
||||||
|
({option.count})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4" style={{ maxHeight }}>
|
||||||
|
{isLoading && approvals.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
) : filteredApprovals.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-32 text-center">
|
||||||
|
<Clock className="w-8 h-8 text-gray-400 mb-2" />
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{statusFilter === 'pending' ? '暂无待处理的审批' : '暂无审批记录'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filteredApprovals.map(approval => (
|
||||||
|
<ApprovalCard
|
||||||
|
key={approval.id}
|
||||||
|
approval={approval}
|
||||||
|
onApprove={() => handleApprove(approval)}
|
||||||
|
onReject={(reason) => handleReject(approval, reason)}
|
||||||
|
isProcessing={processingIds.has(approval.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApprovalQueue;
|
||||||
395
desktop/src/components/Automation/ExecutionResult.tsx
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
/**
|
||||||
|
* ExecutionResult - Execution Result Display Component
|
||||||
|
*
|
||||||
|
* Displays the result of hand or workflow executions with
|
||||||
|
* status, output, and error information.
|
||||||
|
*
|
||||||
|
* @module components/Automation/ExecutionResult
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import type { RunInfo } from '../../types/automation';
|
||||||
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
AlertTriangle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Copy,
|
||||||
|
Download,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
Code,
|
||||||
|
Image,
|
||||||
|
FileSpreadsheet,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useToast } from '../ui/Toast';
|
||||||
|
|
||||||
|
// === Status Config ===
|
||||||
|
|
||||||
|
const STATUS_CONFIG = {
|
||||||
|
completed: {
|
||||||
|
label: '完成',
|
||||||
|
icon: CheckCircle,
|
||||||
|
className: 'text-green-500',
|
||||||
|
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
label: '失败',
|
||||||
|
icon: XCircle,
|
||||||
|
className: 'text-red-500',
|
||||||
|
bgClass: 'bg-red-50 dark:bg-red-900/20',
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
label: '运行中',
|
||||||
|
icon: RefreshCw,
|
||||||
|
className: 'text-blue-500 animate-spin',
|
||||||
|
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
||||||
|
},
|
||||||
|
needs_approval: {
|
||||||
|
label: '待审批',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
className: 'text-yellow-500',
|
||||||
|
bgClass: 'bg-yellow-50 dark:bg-yellow-900/20',
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
label: '已取消',
|
||||||
|
icon: XCircle,
|
||||||
|
className: 'text-gray-500',
|
||||||
|
bgClass: 'bg-gray-50 dark:bg-gray-900/20',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Component Props ===
|
||||||
|
|
||||||
|
interface ExecutionResultProps {
|
||||||
|
run: RunInfo;
|
||||||
|
itemType: 'hand' | 'workflow';
|
||||||
|
itemName: string;
|
||||||
|
onRerun?: () => void;
|
||||||
|
onViewDetails?: () => void;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
function formatDuration(startedAt: string, completedAt?: string): string {
|
||||||
|
const start = new Date(startedAt).getTime();
|
||||||
|
const end = completedAt ? new Date(completedAt).getTime() : Date.now();
|
||||||
|
const diffMs = end - start;
|
||||||
|
|
||||||
|
const seconds = Math.floor(diffMs / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes % 60}m`;
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}m ${seconds % 60}s`;
|
||||||
|
}
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectOutputType(output: unknown): 'text' | 'json' | 'markdown' | 'code' | 'image' | 'data' {
|
||||||
|
if (!output) return 'text';
|
||||||
|
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
// Check for image URL
|
||||||
|
if (output.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i)) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
// Check for markdown
|
||||||
|
if (output.includes('#') || output.includes('**') || output.includes('```')) {
|
||||||
|
return 'markdown';
|
||||||
|
}
|
||||||
|
// Check for code
|
||||||
|
if (output.includes('function ') || output.includes('import ') || output.includes('class ')) {
|
||||||
|
return 'code';
|
||||||
|
}
|
||||||
|
// Try to parse as JSON
|
||||||
|
try {
|
||||||
|
JSON.parse(output);
|
||||||
|
return 'json';
|
||||||
|
} catch {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object/array types
|
||||||
|
if (typeof output === 'object') {
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOutput(output: unknown, type: string): string {
|
||||||
|
if (!output) return '无输出';
|
||||||
|
|
||||||
|
if (type === 'json') {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(output, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Output Viewer Component ===
|
||||||
|
|
||||||
|
interface OutputViewerProps {
|
||||||
|
output: unknown;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OutputViewer({ output, type }: OutputViewerProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async () => {
|
||||||
|
const text = formatOutput(output, type);
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
toast('已复制到剪贴板', 'success');
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}, [output, type, toast]);
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
const text = formatOutput(output, type);
|
||||||
|
const blob = new Blob([text], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `output-${Date.now()}.${type === 'json' ? 'json' : 'txt'}`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, [output, type]);
|
||||||
|
|
||||||
|
// Image preview
|
||||||
|
if (type === 'image' && typeof output === 'string') {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={output}
|
||||||
|
alt="Output"
|
||||||
|
className="max-w-full rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-2 right-2 flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(output, '_blank')}
|
||||||
|
className="p-1.5 bg-black/50 rounded hover:bg-black/70 text-white"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text/JSON/Code output
|
||||||
|
const content = formatOutput(output, type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<pre className="p-3 bg-gray-900 dark:bg-gray-950 rounded-lg text-sm text-gray-100 overflow-x-auto max-h-64 overflow-y-auto">
|
||||||
|
{content}
|
||||||
|
</pre>
|
||||||
|
<div className="absolute top-2 right-2 flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="p-1.5 bg-gray-700 rounded hover:bg-gray-600 text-gray-300"
|
||||||
|
title="复制"
|
||||||
|
>
|
||||||
|
{copied ? <CheckCircle className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="p-1.5 bg-gray-700 rounded hover:bg-gray-600 text-gray-300"
|
||||||
|
title="下载"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Main Component ===
|
||||||
|
|
||||||
|
export function ExecutionResult({
|
||||||
|
run,
|
||||||
|
itemType,
|
||||||
|
itemName,
|
||||||
|
onRerun,
|
||||||
|
onViewDetails,
|
||||||
|
compact = false,
|
||||||
|
}: ExecutionResultProps) {
|
||||||
|
const [expanded, setExpanded] = useState(!compact);
|
||||||
|
|
||||||
|
const statusConfig = STATUS_CONFIG[run.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.completed;
|
||||||
|
const StatusIcon = statusConfig.icon;
|
||||||
|
|
||||||
|
// Safely extract error message as string
|
||||||
|
const getErrorMessage = (): string | null => {
|
||||||
|
if (typeof run.error === 'string' && run.error.length > 0) {
|
||||||
|
return run.error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const errorMessage = getErrorMessage();
|
||||||
|
|
||||||
|
const outputType = useMemo(() => detectOutputType(run.output), [run.output]);
|
||||||
|
const duration = useMemo(() => {
|
||||||
|
if (run.duration) return `${run.duration}s`;
|
||||||
|
if (run.completedAt && run.startedAt) {
|
||||||
|
return formatDuration(run.startedAt, run.completedAt);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [run.duration, run.startedAt, run.completedAt]);
|
||||||
|
|
||||||
|
// Compact mode
|
||||||
|
if (compact && !expanded) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-3 p-3 rounded-lg ${statusConfig.bgClass} cursor-pointer`}
|
||||||
|
onClick={() => setExpanded(true)}
|
||||||
|
>
|
||||||
|
<StatusIcon className={`w-5 h-5 ${statusConfig.className}`} />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{itemName}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs ${statusConfig.className}`}>
|
||||||
|
{statusConfig.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{duration && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
耗时: {duration}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-lg border ${statusConfig.bgClass} border-gray-200 dark:border-gray-700 overflow-hidden`}>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between px-4 py-3 cursor-pointer"
|
||||||
|
onClick={compact ? () => setExpanded(false) : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<StatusIcon className={`w-5 h-5 ${statusConfig.className}`} />
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{itemName}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full ${statusConfig.className} ${statusConfig.bgClass}`}>
|
||||||
|
{statusConfig.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{itemType === 'hand' ? 'Hand' : '工作流'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{run.runId && (
|
||||||
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Run ID: {run.runId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{duration && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
耗时: {duration}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{compact && (
|
||||||
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
{expanded && (
|
||||||
|
<div className="px-4 pb-4 space-y-3">
|
||||||
|
{/* Error */}
|
||||||
|
{(() => {
|
||||||
|
if (!errorMessage) return null;
|
||||||
|
return (
|
||||||
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||||
|
<p className="text-sm font-medium text-red-700 dark:text-red-400 mb-1">错误信息</p>
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-300">{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Output */}
|
||||||
|
{run.output !== undefined && run.output !== null && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">输出结果</p>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||||
|
{outputType === 'json' && <Code className="w-3 h-3" />}
|
||||||
|
{outputType === 'markdown' && <FileText className="w-3 h-3" />}
|
||||||
|
{outputType === 'image' && <Image className="w-3 h-3" />}
|
||||||
|
{outputType === 'data' && <FileSpreadsheet className="w-3 h-3" />}
|
||||||
|
{outputType.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<OutputViewer output={run.output} type={outputType} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timestamps */}
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
开始: {new Date(run.startedAt).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
{run.completedAt && (
|
||||||
|
<span>
|
||||||
|
完成: {new Date(run.completedAt).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
{onRerun && (
|
||||||
|
<button
|
||||||
|
onClick={onRerun}
|
||||||
|
className="px-3 py-1.5 text-sm bg-orange-500 text-white rounded-md hover:bg-orange-600 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
|
重新执行
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onViewDetails && (
|
||||||
|
<button
|
||||||
|
onClick={onViewDetails}
|
||||||
|
className="px-3 py-1.5 text-sm border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
查看详情
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExecutionResult;
|
||||||
@@ -11,6 +11,8 @@ export { AutomationCard } from './AutomationCard';
|
|||||||
export { AutomationFilters } from './AutomationFilters';
|
export { AutomationFilters } from './AutomationFilters';
|
||||||
export { BatchActionBar } from './BatchActionBar';
|
export { BatchActionBar } from './BatchActionBar';
|
||||||
export { ScheduleEditor } from './ScheduleEditor';
|
export { ScheduleEditor } from './ScheduleEditor';
|
||||||
|
export { ApprovalQueue } from './ApprovalQueue';
|
||||||
|
export { ExecutionResult } from './ExecutionResult';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare } f
|
|||||||
import { Button, EmptyState } from './ui';
|
import { Button, EmptyState } from './ui';
|
||||||
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
|
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
|
||||||
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
||||||
|
import { MessageSearch } from './MessageSearch';
|
||||||
|
|
||||||
const MODELS = ['glm-5', 'qwen3.5-plus', 'kimi-k2.5', 'minimax-m2.5'];
|
const MODELS = ['glm-5', 'qwen3.5-plus', 'kimi-k2.5', 'minimax-m2.5'];
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export function ChatArea() {
|
|||||||
const [showModelPicker, setShowModelPicker] = useState(false);
|
const [showModelPicker, setShowModelPicker] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
|
|
||||||
// Get current clone for first conversation prompt
|
// Get current clone for first conversation prompt
|
||||||
const currentClone = useMemo(() => {
|
const currentClone = useMemo(() => {
|
||||||
@@ -74,8 +76,21 @@ export function ChatArea() {
|
|||||||
|
|
||||||
const connected = connectionState === 'connected';
|
const connected = connectionState === 'connected';
|
||||||
|
|
||||||
|
// Navigate to a specific message by ID
|
||||||
|
const handleNavigateToMessage = useCallback((messageId: string) => {
|
||||||
|
const messageEl = messageRefs.current.get(messageId);
|
||||||
|
if (messageEl && scrollRef.current) {
|
||||||
|
messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
// Add highlight effect
|
||||||
|
messageEl.classList.add('ring-2', 'ring-orange-400', 'ring-offset-2');
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.classList.remove('ring-2', 'ring-orange-400', 'ring-offset-2');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="h-14 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-6 flex-shrink-0 bg-white dark:bg-gray-900">
|
<div className="h-14 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-6 flex-shrink-0 bg-white dark:bg-gray-900">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -93,6 +108,9 @@ export function ChatArea() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{messages.length > 0 && (
|
||||||
|
<MessageSearch onNavigateToMessage={handleNavigateToMessage} />
|
||||||
|
)}
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -138,6 +156,7 @@ export function ChatArea() {
|
|||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
|
ref={(el) => { if (el) messageRefs.current.set(message.id, el); }}
|
||||||
variants={listItemVariants}
|
variants={listItemVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
@@ -211,10 +230,10 @@ export function ChatArea() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={isStreaming || !input.trim() || !connected}
|
disabled={isStreaming || !input.trim() || !connected}
|
||||||
className="w-8 h-8 rounded-full p-0 flex items-center justify-center"
|
className="w-8 h-8 rounded-full p-0 flex items-center justify-center bg-orange-500 hover:bg-orange-600 text-white"
|
||||||
aria-label="发送消息"
|
aria-label="发送消息"
|
||||||
>
|
>
|
||||||
<ArrowUp className="w-4 h-4" />
|
<ArrowUp className="w-4 h-4 text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +242,7 @@ export function ChatArea() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +351,9 @@ function MessageBubble({ message }: { message: Message }) {
|
|||||||
|
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
|
|
||||||
|
// 思考中状态:streaming 且内容为空时显示思考指示器
|
||||||
|
const isThinking = message.streaming && !message.content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex gap-4 ${isUser ? 'justify-end' : ''}`}>
|
<div className={`flex gap-4 ${isUser ? 'justify-end' : ''}`}>
|
||||||
<div
|
<div
|
||||||
@@ -340,17 +362,29 @@ function MessageBubble({ message }: { message: Message }) {
|
|||||||
{isUser ? '用' : 'Z'}
|
{isUser ? '用' : 'Z'}
|
||||||
</div>
|
</div>
|
||||||
<div className={isUser ? 'max-w-2xl' : 'flex-1 max-w-3xl'}>
|
<div className={isUser ? 'max-w-2xl' : 'flex-1 max-w-3xl'}>
|
||||||
|
{isThinking ? (
|
||||||
|
// 思考中指示器
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<span className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
|
<span className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
|
<span className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">思考中...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'}`}>
|
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'}`}>
|
||||||
<div className={`leading-relaxed whitespace-pre-wrap ${isUser ? 'text-white' : 'text-gray-700 dark:text-gray-200'}`}>
|
<div className={`leading-relaxed whitespace-pre-wrap ${isUser ? 'text-white' : 'text-gray-700 dark:text-gray-200'}`}>
|
||||||
{message.content
|
{message.content
|
||||||
? (isUser ? message.content : renderMarkdown(message.content))
|
? (isUser ? message.content : renderMarkdown(message.content))
|
||||||
: (message.streaming ? '' : '...')}
|
: '...'}
|
||||||
{message.streaming && <span className="inline-block w-1.5 h-4 bg-orange-500 animate-pulse ml-0.5 align-text-bottom rounded-sm" />}
|
{message.streaming && <span className="inline-block w-1.5 h-4 bg-orange-500 animate-pulse ml-0.5 align-text-bottom rounded-sm" />}
|
||||||
</div>
|
</div>
|
||||||
{message.error && (
|
{message.error && (
|
||||||
<p className="text-xs text-red-500 mt-2">{message.error}</p>
|
<p className="text-xs text-red-500 mt-2">{message.error}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
346
desktop/src/components/CodeSnippetPanel.tsx
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* CodeSnippetPanel - 代码片段快速浏览面板
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 搜索过滤代码片段
|
||||||
|
* - 按语言筛选
|
||||||
|
* - 展开/折叠查看完整代码
|
||||||
|
* - 一键复制
|
||||||
|
* - 下载为文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Search, Copy, Download, ChevronDown, ChevronUp,
|
||||||
|
FileCode, X, Check, Code
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button, EmptyState } from './ui';
|
||||||
|
import type { CodeBlock } from '../store/chatStore';
|
||||||
|
|
||||||
|
// === Types ===
|
||||||
|
|
||||||
|
export interface CodeSnippet {
|
||||||
|
id: string;
|
||||||
|
block: CodeBlock;
|
||||||
|
messageIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeSnippetPanelProps {
|
||||||
|
snippets: CodeSnippet[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Language Colors ===
|
||||||
|
|
||||||
|
const LANGUAGE_COLORS: Record<string, string> = {
|
||||||
|
python: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||||
|
javascript: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||||
|
typescript: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||||
|
rust: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||||
|
go: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300',
|
||||||
|
java: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||||
|
cpp: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
||||||
|
c: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
|
||||||
|
html: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||||
|
css: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300',
|
||||||
|
sql: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
||||||
|
bash: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||||
|
shell: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||||
|
json: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
|
||||||
|
yaml: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
|
||||||
|
markdown: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
|
||||||
|
text: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getLanguageColor(lang: string): string {
|
||||||
|
return LANGUAGE_COLORS[lang.toLowerCase()] || LANGUAGE_COLORS.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension(lang?: string): string {
|
||||||
|
const extensions: Record<string, string> = {
|
||||||
|
python: 'py',
|
||||||
|
javascript: 'js',
|
||||||
|
typescript: 'ts',
|
||||||
|
rust: 'rs',
|
||||||
|
go: 'go',
|
||||||
|
java: 'java',
|
||||||
|
cpp: 'cpp',
|
||||||
|
c: 'c',
|
||||||
|
html: 'html',
|
||||||
|
css: 'css',
|
||||||
|
sql: 'sql',
|
||||||
|
bash: 'sh',
|
||||||
|
shell: 'sh',
|
||||||
|
json: 'json',
|
||||||
|
yaml: 'yaml',
|
||||||
|
markdown: 'md',
|
||||||
|
};
|
||||||
|
return extensions[lang?.toLowerCase() || ''] || 'txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Snippet Card Component ===
|
||||||
|
|
||||||
|
interface SnippetCardProps {
|
||||||
|
snippet: CodeSnippet;
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SnippetCard({ snippet, isExpanded, onToggle }: SnippetCardProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { block, messageIndex } = snippet;
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(block.content || '');
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
}, [block.content]);
|
||||||
|
|
||||||
|
const handleDownload = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const filename = block.filename || `snippet_${messageIndex + 1}.${getFileExtension(block.language)}`;
|
||||||
|
const blob = new Blob([block.content || ''], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, [block.content, block.filename, block.language, messageIndex]);
|
||||||
|
|
||||||
|
const lineCount = (block.content || '').split('\n').length;
|
||||||
|
const charCount = (block.content || '').length;
|
||||||
|
const previewLines = (block.content || '').split('\n').slice(0, 2).join('\n');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<FileCode className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-800 dark:text-gray-200 truncate text-sm">
|
||||||
|
{block.filename || `代码片段 #${messageIndex + 1}`}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getLanguageColor(block.language || 'text')}`}>
|
||||||
|
{block.language || 'text'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{lineCount} 行 · {charCount > 1024 ? `${(charCount / 1024).toFixed(1)} KB` : `${charCount} 字符`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="p-1.5 opacity-60 hover:opacity-100"
|
||||||
|
title="复制代码"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="p-1.5 opacity-60 hover:opacity-100"
|
||||||
|
title="下载文件"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-gray-400 ml-1" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400 ml-1" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Preview / Full Content */}
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isExpanded ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-3">
|
||||||
|
<pre className="text-xs font-mono text-gray-700 dark:text-gray-300 overflow-x-auto whitespace-pre-wrap break-all max-h-60 overflow-y-auto">
|
||||||
|
{block.content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="px-3 pb-3">
|
||||||
|
<pre className="text-xs font-mono text-gray-500 dark:text-gray-400 overflow-hidden whitespace-pre-wrap break-all line-clamp-2">
|
||||||
|
{previewLines}
|
||||||
|
{(block.content || '').split('\n').length > 2 && '...'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Main Component ===
|
||||||
|
|
||||||
|
export function CodeSnippetPanel({ snippets }: CodeSnippetPanelProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState<string | null>(null);
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Get unique languages with counts
|
||||||
|
const languages = useMemo(() => {
|
||||||
|
const langMap = new Map<string, number>();
|
||||||
|
snippets.forEach(s => {
|
||||||
|
const lang = s.block.language?.toLowerCase() || 'text';
|
||||||
|
langMap.set(lang, (langMap.get(lang) || 0) + 1);
|
||||||
|
});
|
||||||
|
return Array.from(langMap.entries())
|
||||||
|
.map(([lang, count]) => ({ lang, count }))
|
||||||
|
.sort((a, b) => b.count - a.count);
|
||||||
|
}, [snippets]);
|
||||||
|
|
||||||
|
// Filter snippets
|
||||||
|
const filteredSnippets = useMemo(() => {
|
||||||
|
return snippets.filter(snippet => {
|
||||||
|
const block = snippet.block;
|
||||||
|
|
||||||
|
// Language filter
|
||||||
|
if (selectedLanguage && (block.language?.toLowerCase() || 'text') !== selectedLanguage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchQuery) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
const matchesFilename = block.filename?.toLowerCase().includes(query);
|
||||||
|
const matchesContent = block.content?.toLowerCase().includes(query);
|
||||||
|
const matchesLanguage = block.language?.toLowerCase().includes(query);
|
||||||
|
return matchesFilename || matchesContent || matchesLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [snippets, searchQuery, selectedLanguage]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback((id: string) => {
|
||||||
|
setExpandedId(prev => prev === id ? null : id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (snippets.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={<Code className="w-10 h-10" />}
|
||||||
|
title="暂无代码片段"
|
||||||
|
description="对话中生成的代码会自动出现在这里"
|
||||||
|
className="py-8"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索代码..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-8 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language Filters */}
|
||||||
|
{languages.length > 1 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedLanguage(null)}
|
||||||
|
className={`px-2 py-1 text-xs rounded-full transition-colors ${
|
||||||
|
selectedLanguage === null
|
||||||
|
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全部 ({snippets.length})
|
||||||
|
</button>
|
||||||
|
{languages.map(({ lang, count }) => (
|
||||||
|
<button
|
||||||
|
key={lang}
|
||||||
|
onClick={() => setSelectedLanguage(selectedLanguage === lang ? null : lang)}
|
||||||
|
className={`px-2 py-1 text-xs rounded-full transition-colors ${
|
||||||
|
selectedLanguage === lang
|
||||||
|
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{lang} ({count})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results Count */}
|
||||||
|
{(searchQuery || selectedLanguage) && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
找到 {filteredSnippets.length} 个代码片段
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Snippet List */}
|
||||||
|
<div className="space-y-2 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 380px)' }}>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredSnippets.map((snippet) => (
|
||||||
|
<motion.div
|
||||||
|
key={snippet.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
layout
|
||||||
|
>
|
||||||
|
<SnippetCard
|
||||||
|
snippet={snippet}
|
||||||
|
isExpanded={expandedId === snippet.id}
|
||||||
|
onToggle={() => handleToggle(snippet.id)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{filteredSnippets.length === 0 && (
|
||||||
|
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
没有找到匹配的代码片段
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
desktop/src/components/DetailDrawer.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DetailDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetailDrawer({ open, onClose, title = '详情', children }: DetailDrawerProps) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
{/* 遮罩层 */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="fixed inset-0 bg-black/20 dark:bg-black/40 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 抽屉面板 */}
|
||||||
|
<motion.aside
|
||||||
|
initial={{ x: '100%' }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: '100%' }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||||
|
className="fixed right-0 top-0 bottom-0 w-[400px] bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 z-50 flex flex-col shadow-xl"
|
||||||
|
>
|
||||||
|
{/* 抽屉头部 */}
|
||||||
|
<header className="h-14 border-b border-gray-200 dark:border-gray-700 flex items-center px-4 flex-shrink-0">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-gray-100">{title}</span>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="ml-auto p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-500 dark:text-gray-400 transition-colors"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 抽屉内容 */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useToast } from './ui/Toast';
|
||||||
|
|
||||||
interface HandTaskPanelProps {
|
interface HandTaskPanelProps {
|
||||||
handId: string;
|
handId: string;
|
||||||
@@ -39,6 +40,7 @@ const RUN_STATUS_CONFIG: Record<string, { label: string; className: string; icon
|
|||||||
|
|
||||||
export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
|
export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
|
||||||
const { hands, handRuns, loadHands, loadHandRuns, triggerHand, isLoading } = useGatewayStore();
|
const { hands, handRuns, loadHands, loadHandRuns, triggerHand, isLoading } = useGatewayStore();
|
||||||
|
const { toast } = useToast();
|
||||||
const [selectedHand, setSelectedHand] = useState<Hand | null>(null);
|
const [selectedHand, setSelectedHand] = useState<Hand | null>(null);
|
||||||
const [isActivating, setIsActivating] = useState(false);
|
const [isActivating, setIsActivating] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -78,20 +80,49 @@ export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
|
|||||||
// Trigger hand execution
|
// Trigger hand execution
|
||||||
const handleActivate = useCallback(async () => {
|
const handleActivate = useCallback(async () => {
|
||||||
if (!selectedHand) return;
|
if (!selectedHand) return;
|
||||||
|
|
||||||
|
// Check if hand is already running
|
||||||
|
if (selectedHand.status === 'running') {
|
||||||
|
toast(`Hand "${selectedHand.name}" 正在运行中,请等待完成`, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsActivating(true);
|
setIsActivating(true);
|
||||||
|
console.log(`[HandTaskPanel] Activating hand: ${selectedHand.id} (${selectedHand.name})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await triggerHand(selectedHand.id);
|
const result = await triggerHand(selectedHand.id);
|
||||||
|
console.log(`[HandTaskPanel] Activation result:`, result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
toast(`Hand "${selectedHand.name}" 已成功启动`, 'success');
|
||||||
// Refresh hands list and task history
|
// Refresh hands list and task history
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadHands(),
|
loadHands(),
|
||||||
loadHandRuns(selectedHand.id, { limit: 50 }),
|
loadHandRuns(selectedHand.id, { limit: 50 }),
|
||||||
]);
|
]);
|
||||||
} catch {
|
} else {
|
||||||
// Error is handled in store
|
// Check for specific error in store
|
||||||
|
const storeError = useGatewayStore.getState().error;
|
||||||
|
if (storeError?.includes('already active')) {
|
||||||
|
toast(`Hand "${selectedHand.name}" 已在运行中`, 'warning');
|
||||||
|
} else {
|
||||||
|
toast(`Hand "${selectedHand.name}" 启动失败: ${storeError || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[HandTaskPanel] Activation error:`, errorMsg);
|
||||||
|
|
||||||
|
if (errorMsg.includes('already active')) {
|
||||||
|
toast(`Hand "${selectedHand.name}" 已在运行中`, 'warning');
|
||||||
|
} else {
|
||||||
|
toast(`Hand "${selectedHand.name}" 启动异常: ${errorMsg}`, 'error');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsActivating(false);
|
setIsActivating(false);
|
||||||
}
|
}
|
||||||
}, [selectedHand, triggerHand, loadHands, loadHandRuns]);
|
}, [selectedHand, triggerHand, loadHands, loadHandRuns, toast]);
|
||||||
|
|
||||||
if (!selectedHand) {
|
if (!selectedHand) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const { currentAgent } = useChatStore();
|
const { currentAgent } = useChatStore();
|
||||||
const agentId = currentAgent?.id || 'default';
|
const agentId = currentAgent?.id || 'zclaw-main';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -225,8 +225,8 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
canvas.height = layout.height * dpr;
|
canvas.height = layout.height * dpr;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
// 清空画布
|
// 清空画布 - 使用浅色背景匹配系统主题
|
||||||
ctx.fillStyle = '#1a1a2e';
|
ctx.fillStyle = '#f9fafb'; // gray-50
|
||||||
ctx.fillRect(0, 0, layout.width, layout.height);
|
ctx.fillRect(0, 0, layout.width, layout.height);
|
||||||
|
|
||||||
// 应用变换
|
// 应用变换
|
||||||
@@ -299,7 +299,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
ctx.arc(legendX, legendY, 6, 0, Math.PI * 2);
|
ctx.arc(legendX, legendY, 6, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = colors.fill;
|
ctx.fillStyle = colors.fill;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.fillStyle = '#9ca3af';
|
ctx.fillStyle = '#6b7280'; // gray-500 for better visibility on light background
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(label, legendX + 12, legendY + 4);
|
ctx.fillText(label, legendX + 12, legendY + 4);
|
||||||
legendX += 70;
|
legendX += 70;
|
||||||
@@ -377,7 +377,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`flex flex-col h-full ${className}`}>
|
<div className={`flex flex-col h-full ${className}`}>
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded-t-lg border-b border-gray-700">
|
<div className="flex items-center gap-2 p-2 bg-gray-100 dark:bg-gray-800/50 rounded-t-lg border-b border-gray-200 dark:border-gray-700">
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
<div className="relative flex-1 max-w-xs">
|
<div className="relative flex-1 max-w-xs">
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
@@ -386,7 +386,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
placeholder="搜索记忆..."
|
placeholder="搜索记忆..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
className="w-full pl-8 pr-2 py-1 bg-gray-900 border border-gray-700 rounded text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
className="w-full pl-8 pr-2 py-1 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:border-orange-400 dark:focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -459,7 +459,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
initial={{ height: 0, opacity: 0 }}
|
initial={{ height: 0, opacity: 0 }}
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
className="overflow-hidden bg-gray-800/30 border-b border-gray-700"
|
className="overflow-hidden bg-gray-50 dark:bg-gray-800/30 border-b border-gray-200 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
<div className="p-3 flex flex-wrap gap-3">
|
<div className="p-3 flex flex-wrap gap-3">
|
||||||
{/* 类型筛选 */}
|
{/* 类型筛选 */}
|
||||||
@@ -476,8 +476,8 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
}}
|
}}
|
||||||
className={`px-2 py-1 text-xs rounded ${
|
className={`px-2 py-1 text-xs rounded ${
|
||||||
filter.types.includes(type)
|
filter.types.includes(type)
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-orange-500 text-white'
|
||||||
: 'bg-gray-700 text-gray-300'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{TYPE_LABELS[type]}
|
{TYPE_LABELS[type]}
|
||||||
@@ -510,9 +510,9 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 图谱画布 */}
|
{/* 图谱画布 */}
|
||||||
<div className="flex-1 relative overflow-hidden bg-gray-900">
|
<div className="flex-1 relative overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/80 z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-50/80 dark:bg-gray-900/80 z-10">
|
||||||
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin" />
|
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -541,7 +541,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: 20 }}
|
exit={{ opacity: 0, x: 20 }}
|
||||||
className="absolute top-4 right-4 w-64 bg-gray-800 rounded-lg border border-gray-700 p-4 shadow-xl"
|
className="absolute top-4 right-4 w-64 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-xl"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<Badge variant={selectedNode.type as any}>
|
<Badge variant={selectedNode.type as any}>
|
||||||
@@ -549,7 +549,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
<button
|
<button
|
||||||
onClick={() => selectNode(null)}
|
onClick={() => selectNode(null)}
|
||||||
className="text-gray-400 hover:text-white"
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -557,7 +557,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
|
|
||||||
<p className="text-sm text-gray-200 mb-3">{selectedNode.label}</p>
|
<p className="text-sm text-gray-200 mb-3">{selectedNode.label}</p>
|
||||||
|
|
||||||
<div className="space-y-2 text-xs text-gray-400">
|
<div className="space-y-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Star className="w-3 h-3" />
|
<Star className="w-3 h-3" />
|
||||||
重要性: {selectedNode.importance}
|
重要性: {selectedNode.importance}
|
||||||
@@ -573,9 +573,9 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 关联边统计 */}
|
{/* 关联边统计 */}
|
||||||
<div className="mt-3 pt-3 border-t border-gray-700">
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="text-xs text-gray-400 mb-1">关联记忆:</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">关联记忆:</div>
|
||||||
<div className="text-sm text-gray-200">
|
<div className="text-sm text-gray-700 dark:text-gray-200">
|
||||||
{filteredEdges.filter(
|
{filteredEdges.filter(
|
||||||
e => e.source === selectedNode.id || e.target === selectedNode.id
|
e => e.source === selectedNode.id || e.target === selectedNode.id
|
||||||
).length} 个
|
).length} 个
|
||||||
@@ -598,7 +598,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 状态栏 */}
|
{/* 状态栏 */}
|
||||||
<div className="flex items-center justify-between px-3 py-1 bg-gray-800/50 rounded-b-lg text-xs text-gray-400">
|
<div className="flex items-center justify-between px-3 py-1 bg-gray-100 dark:bg-gray-800/50 rounded-b-lg text-xs text-gray-500 dark:text-gray-400">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span>节点: {filteredNodes.length}</span>
|
<span>节点: {filteredNodes.length}</span>
|
||||||
<span>关联: {filteredEdges.length}</span>
|
<span>关联: {filteredEdges.length}</span>
|
||||||
|
|||||||
@@ -1,19 +1,80 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||||
import { useGatewayStore, type PluginStatus } from '../store/gatewayStore';
|
import { useGatewayStore, type PluginStatus } from '../store/gatewayStore';
|
||||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||||
import {
|
import {
|
||||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||||
MessageSquare, Cpu, FileText, User, Activity, FileCode, Brain,
|
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||||
Shield, Sparkles, GraduationCap
|
Shield, Sparkles, GraduationCap, List, Network
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// === Helper to extract code blocks from markdown content ===
|
||||||
|
function extractCodeBlocksFromContent(content: string): CodeBlock[] {
|
||||||
|
const blocks: CodeBlock[] = [];
|
||||||
|
const regex = /```(\w*)\n([\s\S]*?)```/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const language = match[1] || 'text';
|
||||||
|
const codeContent = match[2].trim();
|
||||||
|
|
||||||
|
// Try to extract filename from first line comment
|
||||||
|
let filename: string | undefined;
|
||||||
|
let actualContent = codeContent;
|
||||||
|
|
||||||
|
// Check for filename patterns like "# filename.py" or "// filename.js"
|
||||||
|
const firstLine = codeContent.split('\n')[0];
|
||||||
|
const filenameMatch = firstLine.match(/^(?:#|\/\/|\/\*|<!--)\s*([^\s]+\.\w+)/);
|
||||||
|
if (filenameMatch) {
|
||||||
|
filename = filenameMatch[1];
|
||||||
|
actualContent = codeContent.split('\n').slice(1).join('\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
language,
|
||||||
|
filename,
|
||||||
|
content: actualContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Tab Button Component ===
|
||||||
|
function TabButton({
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
import { MemoryPanel } from './MemoryPanel';
|
import { MemoryPanel } from './MemoryPanel';
|
||||||
|
import { MemoryGraph } from './MemoryGraph';
|
||||||
import { ReflectionLog } from './ReflectionLog';
|
import { ReflectionLog } from './ReflectionLog';
|
||||||
import { AutonomyConfig } from './AutonomyConfig';
|
import { AutonomyConfig } from './AutonomyConfig';
|
||||||
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
||||||
|
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
|
||||||
import { cardHover, defaultTransition } from '../lib/animations';
|
import { cardHover, defaultTransition } from '../lib/animations';
|
||||||
import { Button, Badge, EmptyState } from './ui';
|
import { Button, Badge } from './ui';
|
||||||
import { getPersonalityById } from '../lib/personality-presets';
|
import { getPersonalityById } from '../lib/personality-presets';
|
||||||
import { silentErrorHandler } from '../lib/error-utils';
|
import { silentErrorHandler } from '../lib/error-utils';
|
||||||
|
|
||||||
@@ -24,6 +85,7 @@ export function RightPanel() {
|
|||||||
} = useGatewayStore();
|
} = useGatewayStore();
|
||||||
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
||||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
|
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
|
||||||
|
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
||||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||||
|
|
||||||
@@ -96,108 +158,149 @@ export function RightPanel() {
|
|||||||
const userAddressing = selectedClone?.nickname || selectedClone?.userName || quickConfig.userName || '未设置';
|
const userAddressing = selectedClone?.nickname || selectedClone?.userName || quickConfig.userName || '未设置';
|
||||||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
||||||
|
|
||||||
|
// Extract code blocks from all messages (both from codeBlocks property and content parsing)
|
||||||
|
const codeSnippets = useMemo((): CodeSnippet[] => {
|
||||||
|
const snippets: CodeSnippet[] = [];
|
||||||
|
let globalIndex = 0;
|
||||||
|
|
||||||
|
for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
|
||||||
|
const msg = messages[msgIdx];
|
||||||
|
|
||||||
|
// First, add any existing codeBlocks from the message
|
||||||
|
if (msg.codeBlocks && msg.codeBlocks.length > 0) {
|
||||||
|
for (const block of msg.codeBlocks) {
|
||||||
|
snippets.push({
|
||||||
|
id: `${msg.id}-codeblock-${globalIndex}`,
|
||||||
|
block,
|
||||||
|
messageIndex: msgIdx,
|
||||||
|
});
|
||||||
|
globalIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, extract code blocks from the message content
|
||||||
|
if (msg.content) {
|
||||||
|
const extractedBlocks = extractCodeBlocksFromContent(msg.content);
|
||||||
|
for (const block of extractedBlocks) {
|
||||||
|
snippets.push({
|
||||||
|
id: `${msg.id}-extracted-${globalIndex}`,
|
||||||
|
block,
|
||||||
|
messageIndex: msgIdx,
|
||||||
|
});
|
||||||
|
globalIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snippets;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-80 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 flex flex-col flex-shrink-0">
|
<aside className="w-full bg-white dark:bg-gray-900 flex flex-col">
|
||||||
{/* 顶部工具栏 */}
|
{/* 顶部工具栏 - Tab 栏 */}
|
||||||
<div className="h-14 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-4 flex-shrink-0">
|
<div className="border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
{/* 主 Tab 行 */}
|
||||||
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
|
<div className="flex items-center px-2 pt-2 gap-1">
|
||||||
<BarChart3 className="w-4 h-4" />
|
<TabButton
|
||||||
<span className="font-medium">{messages.length}</span>
|
active={activeTab === 'status'}
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">当前消息</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400" role="tablist">
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'status' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('status')}
|
onClick={() => setActiveTab('status')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<Activity className="w-4 h-4" />}
|
||||||
title="Status"
|
label="状态"
|
||||||
aria-label="Status"
|
/>
|
||||||
aria-selected={activeTab === 'status'}
|
<TabButton
|
||||||
role="tab"
|
active={activeTab === 'agent'}
|
||||||
>
|
|
||||||
<Activity className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'files' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('files')}
|
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
|
||||||
title="Files"
|
|
||||||
aria-label="Files"
|
|
||||||
aria-selected={activeTab === 'files'}
|
|
||||||
role="tab"
|
|
||||||
>
|
|
||||||
<FileText className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'agent' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('agent')}
|
onClick={() => setActiveTab('agent')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<User className="w-4 h-4" />}
|
||||||
title="Agent"
|
label="Agent"
|
||||||
aria-label="Agent"
|
/>
|
||||||
aria-selected={activeTab === 'agent'}
|
<TabButton
|
||||||
role="tab"
|
active={activeTab === 'files'}
|
||||||
>
|
onClick={() => setActiveTab('files')}
|
||||||
<User className="w-4 h-4" />
|
icon={<FileText className="w-4 h-4" />}
|
||||||
</Button>
|
label="文件"
|
||||||
<Button
|
/>
|
||||||
variant={activeTab === 'memory' ? 'secondary' : 'ghost'}
|
<TabButton
|
||||||
size="sm"
|
active={activeTab === 'memory'}
|
||||||
onClick={() => setActiveTab('memory')}
|
onClick={() => setActiveTab('memory')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<Brain className="w-4 h-4" />}
|
||||||
title="Memory"
|
label="记忆"
|
||||||
aria-label="Memory"
|
/>
|
||||||
aria-selected={activeTab === 'memory'}
|
</div>
|
||||||
role="tab"
|
{/* 第二行 Tab */}
|
||||||
>
|
<div className="flex items-center px-2 pb-2 gap-1">
|
||||||
<Brain className="w-4 h-4" />
|
<TabButton
|
||||||
</Button>
|
active={activeTab === 'reflection'}
|
||||||
<Button
|
|
||||||
variant={activeTab === 'reflection' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('reflection')}
|
onClick={() => setActiveTab('reflection')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<Sparkles className="w-4 h-4" />}
|
||||||
title="Reflection"
|
label="反思"
|
||||||
aria-label="Reflection"
|
/>
|
||||||
aria-selected={activeTab === 'reflection'}
|
<TabButton
|
||||||
role="tab"
|
active={activeTab === 'autonomy'}
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'autonomy' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('autonomy')}
|
onClick={() => setActiveTab('autonomy')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<Shield className="w-4 h-4" />}
|
||||||
title="Autonomy"
|
label="自主"
|
||||||
aria-label="Autonomy"
|
/>
|
||||||
aria-selected={activeTab === 'autonomy'}
|
<TabButton
|
||||||
role="tab"
|
active={activeTab === 'learning'}
|
||||||
>
|
|
||||||
<Shield className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'learning' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setActiveTab('learning')}
|
onClick={() => setActiveTab('learning')}
|
||||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
icon={<GraduationCap className="w-4 h-4" />}
|
||||||
title="Learning"
|
label="学习"
|
||||||
aria-label="Learning"
|
/>
|
||||||
aria-selected={activeTab === 'learning'}
|
</div>
|
||||||
role="tab"
|
</div>
|
||||||
>
|
|
||||||
<GraduationCap className="w-4 h-4" />
|
{/* 消息统计 */}
|
||||||
</Button>
|
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs">
|
||||||
|
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||||
|
<BarChart3 className="w-3.5 h-3.5" />
|
||||||
|
<span>{messages.length} 条消息</span>
|
||||||
|
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||||
|
<span>{userMsgCount} 用户 / {assistantMsgCount} 助手</span>
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center gap-1 ${connected ? 'text-emerald-500' : 'text-gray-400'}`}>
|
||||||
|
{connected ? <Wifi className="w-3.5 h-3.5" /> : <WifiOff className="w-3.5 h-3.5" />}
|
||||||
|
<span>{runtimeSummary}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||||
{activeTab === 'memory' ? (
|
{activeTab === 'memory' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 视图切换 */}
|
||||||
|
<div className="flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<button
|
||||||
|
onClick={() => setMemoryViewMode('list')}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
memoryViewMode === 'list'
|
||||||
|
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<List className="w-3.5 h-3.5" />
|
||||||
|
列表
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMemoryViewMode('graph')}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
memoryViewMode === 'graph'
|
||||||
|
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Network className="w-3.5 h-3.5" />
|
||||||
|
图谱
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
{memoryViewMode === 'list' ? (
|
||||||
<MemoryPanel />
|
<MemoryPanel />
|
||||||
|
) : (
|
||||||
|
<div className="h-[400px] rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700">
|
||||||
|
<MemoryGraph />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : activeTab === 'reflection' ? (
|
) : activeTab === 'reflection' ? (
|
||||||
<ReflectionLog />
|
<ReflectionLog />
|
||||||
) : activeTab === 'autonomy' ? (
|
) : activeTab === 'autonomy' ? (
|
||||||
@@ -354,90 +457,8 @@ export function RightPanel() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === 'files' ? (
|
) : activeTab === 'files' ? (
|
||||||
<div className="space-y-4">
|
<div className="p-4">
|
||||||
{/* 对话输出文件 */}
|
<CodeSnippetPanel snippets={codeSnippets} />
|
||||||
<motion.div
|
|
||||||
whileHover={cardHover}
|
|
||||||
transition={defaultTransition}
|
|
||||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
|
||||||
<FileCode className="w-4 h-4" />
|
|
||||||
对话输出文件
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
{messages.filter(m => m.files && m.files.length > 0).length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{messages.filter(m => m.files && m.files.length > 0).map((msg, msgIdx) => (
|
|
||||||
<div key={msgIdx} className="space-y-1">
|
|
||||||
{msg.files!.map((file, fileIdx) => (
|
|
||||||
<div
|
|
||||||
key={`${msgIdx}-${fileIdx}`}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
|
||||||
title={file.path || file.name}
|
|
||||||
>
|
|
||||||
<FileText className="w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-gray-700 dark:text-gray-200 truncate">{file.name}</div>
|
|
||||||
{file.path && (
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{file.path}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{file.size && (
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
|
|
||||||
{file.size < 1024 ? `${file.size} B` :
|
|
||||||
file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(1)} KB` :
|
|
||||||
`${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
icon={<FileCode className="w-8 h-8" />}
|
|
||||||
title="No Output Files"
|
|
||||||
description="Files will appear here when AI uses tools"
|
|
||||||
className="py-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* 代码块 */}
|
|
||||||
<motion.div
|
|
||||||
whileHover={cardHover}
|
|
||||||
transition={defaultTransition}
|
|
||||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">代码片段</h3>
|
|
||||||
</div>
|
|
||||||
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).flatMap((msg, msgIdx) =>
|
|
||||||
msg.codeBlocks!.map((block, blockIdx) => (
|
|
||||||
<div
|
|
||||||
key={`${msgIdx}-${blockIdx}`}
|
|
||||||
className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<Badge variant="default">{block.language || 'code'}</Badge>
|
|
||||||
<span className="text-gray-700 dark:text-gray-200 truncate">{block.filename || 'Untitled'}</span>
|
|
||||||
</div>
|
|
||||||
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-x-auto max-h-20">
|
|
||||||
{block.content?.slice(0, 200)}{block.content && block.content.length > 200 ? '...' : ''}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
).slice(0, 5)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">No code snippets in conversation</p>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { useGatewayStore, type Workflow } from '../store/gatewayStore';
|
import { useGatewayStore, type Workflow } from '../store/gatewayStore';
|
||||||
import { WorkflowEditor } from './WorkflowEditor';
|
import { WorkflowEditor } from './WorkflowEditor';
|
||||||
import { WorkflowHistory } from './WorkflowHistory';
|
import { WorkflowHistory } from './WorkflowHistory';
|
||||||
|
import { TriggersPanel } from './TriggersPanel';
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Zap,
|
Zap,
|
||||||
@@ -661,11 +662,6 @@ export function SchedulerPanel() {
|
|||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateTrigger = useCallback(() => {
|
|
||||||
// TODO: Implement trigger creation modal
|
|
||||||
alert('事件触发器创建功能即将推出!');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCreateSuccess = useCallback(() => {
|
const handleCreateSuccess = useCallback(() => {
|
||||||
loadScheduledTasks();
|
loadScheduledTasks();
|
||||||
}, [loadScheduledTasks]);
|
}, [loadScheduledTasks]);
|
||||||
@@ -862,15 +858,7 @@ export function SchedulerPanel() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'triggers' && (
|
{activeTab === 'triggers' && (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
<TriggersPanel />
|
||||||
<EmptyState
|
|
||||||
icon={Zap}
|
|
||||||
title="暂无事件触发器"
|
|
||||||
description="事件触发器在系统事件(如收到消息、文件更改或 API webhook)发生时触发代理执行。"
|
|
||||||
actionLabel="创建事件触发器"
|
|
||||||
onAction={handleCreateTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Workflows Tab */}
|
{/* Workflows Tab */}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Settings, Users, Bot, GitBranch, MessageSquare, Layers, Package } from 'lucide-react';
|
import {
|
||||||
|
Users, Bot, Zap, Layers, Package,
|
||||||
|
Search, Sparkles, ChevronRight, X
|
||||||
|
} from 'lucide-react';
|
||||||
import { CloneManager } from './CloneManager';
|
import { CloneManager } from './CloneManager';
|
||||||
import { HandList } from './HandList';
|
import { AutomationPanel } from './Automation';
|
||||||
import { WorkflowList } from './WorkflowList';
|
|
||||||
import { TeamList } from './TeamList';
|
import { TeamList } from './TeamList';
|
||||||
import { SwarmDashboard } from './SwarmDashboard';
|
import { SwarmDashboard } from './SwarmDashboard';
|
||||||
import { SkillMarket } from './SkillMarket';
|
import { SkillMarket } from './SkillMarket';
|
||||||
import { useGatewayStore } from '../store/gatewayStore';
|
import { useGatewayStore } from '../store/gatewayStore';
|
||||||
import { Button } from './ui';
|
|
||||||
import { containerVariants, defaultTransition } from '../lib/animations';
|
import { containerVariants, defaultTransition } from '../lib/animations';
|
||||||
|
|
||||||
export type MainViewType = 'chat' | 'hands' | 'workflow' | 'team' | 'swarm' | 'skills';
|
export type MainViewType = 'chat' | 'automation' | 'team' | 'swarm' | 'skills';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onOpenSettings?: () => void;
|
onOpenSettings?: () => void;
|
||||||
onMainViewChange?: (view: MainViewType) => void;
|
onMainViewChange?: (view: MainViewType) => void;
|
||||||
selectedHandId?: string;
|
|
||||||
onSelectHand?: (handId: string) => void;
|
|
||||||
selectedTeamId?: string;
|
selectedTeamId?: string;
|
||||||
onSelectTeam?: (teamId: string) => void;
|
onSelectTeam?: (teamId: string) => void;
|
||||||
|
onNewChat?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = 'clones' | 'hands' | 'workflow' | 'team' | 'swarm' | 'skills';
|
type Tab = 'chat' | 'clones' | 'automation' | 'team' | 'swarm' | 'skills';
|
||||||
|
|
||||||
const TABS: { key: Tab; label: string; icon: React.ComponentType<{ className?: string }>; mainView?: MainViewType }[] = [
|
// 导航项配置 - WorkBuddy 风格
|
||||||
|
const NAV_ITEMS: {
|
||||||
|
key: Tab;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
mainView?: MainViewType;
|
||||||
|
}[] = [
|
||||||
{ key: 'clones', label: '分身', icon: Bot },
|
{ key: 'clones', label: '分身', icon: Bot },
|
||||||
{ key: 'hands', label: 'Hands', icon: MessageSquare, mainView: 'hands' },
|
{ key: 'automation', label: '自动化', icon: Zap, mainView: 'automation' },
|
||||||
{ key: 'workflow', label: '工作流', icon: GitBranch, mainView: 'workflow' },
|
|
||||||
{ key: 'skills', label: '技能', icon: Package, mainView: 'skills' },
|
{ key: 'skills', label: '技能', icon: Package, mainView: 'skills' },
|
||||||
{ key: 'team', label: '团队', icon: Users, mainView: 'team' },
|
{ key: 'team', label: '团队', icon: Users, mainView: 'team' },
|
||||||
{ key: 'swarm', label: '协作', icon: Layers, mainView: 'swarm' },
|
{ key: 'swarm', label: '协作', icon: Layers, mainView: 'swarm' },
|
||||||
@@ -36,15 +41,15 @@ const TABS: { key: Tab; label: string; icon: React.ComponentType<{ className?: s
|
|||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
onMainViewChange,
|
onMainViewChange,
|
||||||
selectedHandId,
|
|
||||||
onSelectHand,
|
|
||||||
selectedTeamId,
|
selectedTeamId,
|
||||||
onSelectTeam
|
onSelectTeam,
|
||||||
|
onNewChat
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('clones');
|
const [activeTab, setActiveTab] = useState<Tab>('clones');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const userName = useGatewayStore((state) => state.quickConfig.userName) || '用户7141';
|
const userName = useGatewayStore((state) => state.quickConfig.userName) || '用户7141';
|
||||||
|
|
||||||
const handleTabClick = (key: Tab, mainView?: MainViewType) => {
|
const handleNavClick = (key: Tab, mainView?: MainViewType) => {
|
||||||
setActiveTab(key);
|
setActiveTab(key);
|
||||||
if (mainView && onMainViewChange) {
|
if (mainView && onMainViewChange) {
|
||||||
onMainViewChange(mainView);
|
onMainViewChange(mainView);
|
||||||
@@ -53,12 +58,6 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectHand = (handId: string) => {
|
|
||||||
onSelectHand?.(handId);
|
|
||||||
setActiveTab('hands');
|
|
||||||
onMainViewChange?.('hands');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectTeam = (teamId: string) => {
|
const handleSelectTeam = (teamId: string) => {
|
||||||
onSelectTeam?.(teamId);
|
onSelectTeam?.(teamId);
|
||||||
setActiveTab('team');
|
setActiveTab('team');
|
||||||
@@ -66,30 +65,65 @@ export function Sidebar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-64 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col flex-shrink-0">
|
<aside className="w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col flex-shrink-0">
|
||||||
{/* 顶部标签 - 使用图标 */}
|
{/* 搜索框 */}
|
||||||
<div className="flex border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" role="tablist">
|
<div className="p-3 border-b border-gray-100 dark:border-gray-800">
|
||||||
{TABS.map(({ key, label, icon: Icon }) => (
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-8 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all text-gray-700 dark:text-gray-300 placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
key={key}
|
onClick={() => setSearchQuery('')}
|
||||||
title={label}
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded text-gray-400 transition-colors"
|
||||||
aria-label={label}
|
|
||||||
aria-selected={activeTab === key}
|
|
||||||
role="tab"
|
|
||||||
className={`flex-1 py-2.5 px-2 text-xs font-medium transition-colors flex flex-col items-center gap-0.5 ${
|
|
||||||
activeTab === key
|
|
||||||
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
|
||||||
}`}
|
|
||||||
onClick={() => handleTabClick(key, TABS.find(t => t.key === key)?.mainView)}
|
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4" />
|
<X className="w-3 h-3" />
|
||||||
<span className="text-[10px]">{label}</span>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* 新对话按钮 */}
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<button
|
||||||
|
onClick={onNewChat}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-700 dark:text-gray-300 transition-colors group"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-5 h-5 text-emerald-500" />
|
||||||
|
<span className="font-medium">新对话</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导航项 */}
|
||||||
|
<nav className="px-3 space-y-0.5">
|
||||||
|
{NAV_ITEMS.map(({ key, label, icon: Icon, mainView }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleNavClick(key, mainView)}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||||
|
activeTab === key
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-medium'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`w-5 h-5 ${activeTab === key ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400'}`} />
|
||||||
|
<span>{label}</span>
|
||||||
|
{activeTab === key && (
|
||||||
|
<ChevronRight className="w-4 h-4 ml-auto text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* 分隔线 */}
|
||||||
|
<div className="my-3 mx-3 border-t border-gray-100 dark:border-gray-800" />
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -99,16 +133,12 @@ export function Sidebar({
|
|||||||
animate="visible"
|
animate="visible"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
transition={defaultTransition}
|
transition={defaultTransition}
|
||||||
className="h-full"
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{activeTab === 'clones' && <CloneManager />}
|
{activeTab === 'clones' && <CloneManager />}
|
||||||
{activeTab === 'hands' && (
|
{activeTab === 'automation' && (
|
||||||
<HandList
|
<AutomationPanel />
|
||||||
selectedHandId={selectedHandId}
|
|
||||||
onSelectHand={handleSelectHand}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activeTab === 'workflow' && <WorkflowList />}
|
|
||||||
{activeTab === 'skills' && <SkillMarket />}
|
{activeTab === 'skills' && <SkillMarket />}
|
||||||
{activeTab === 'team' && (
|
{activeTab === 'team' && (
|
||||||
<TeamList
|
<TeamList
|
||||||
@@ -121,23 +151,20 @@ export function Sidebar({
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部用户 */}
|
{/* 底部用户栏 */}
|
||||||
<div className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
<div className="p-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-3">
|
<button
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-orange-400 to-red-500 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
|
onClick={onOpenSettings}
|
||||||
|
className="flex items-center gap-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
{userName?.charAt(0) || '用'}
|
{userName?.charAt(0) || '用'}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-700 dark:text-gray-300 truncate">{userName}</span>
|
<span className="flex-1 text-left text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||||
<Button
|
{userName}
|
||||||
variant="ghost"
|
</span>
|
||||||
size="sm"
|
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||||
className="ml-auto p-1.5"
|
</button>
|
||||||
onClick={onOpenSettings}
|
|
||||||
aria-label="打开设置"
|
|
||||||
>
|
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
53
desktop/src/components/TopBar.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ClipboardList } from 'lucide-react';
|
||||||
|
import { Button } from './ui';
|
||||||
|
|
||||||
|
interface TopBarProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
onOpenDetail?: () => void;
|
||||||
|
showDetailButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopBar({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
onOpenDetail,
|
||||||
|
showDetailButton = true
|
||||||
|
}: TopBarProps) {
|
||||||
|
return (
|
||||||
|
<header className="h-14 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 flex items-center px-4 flex-shrink-0">
|
||||||
|
{/* 左侧标题 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
|
<span className="text-sm">Z</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">{title}</span>
|
||||||
|
{subtitle && (
|
||||||
|
<span className="ml-2 text-sm text-gray-500 dark:text-gray-400">{subtitle}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 中间区域 */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* 右侧按钮 */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* 详情按钮 */}
|
||||||
|
{showDetailButton && onOpenDetail && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onOpenDetail}
|
||||||
|
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
|
title="显示详情面板"
|
||||||
|
>
|
||||||
|
<ClipboardList className="w-4 h-4" />
|
||||||
|
<span className="text-sm">详情</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.3
|
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { canAutoExecute } from './autonomy-manager';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
export interface IdentityFiles {
|
export interface IdentityFiles {
|
||||||
@@ -200,8 +202,17 @@ export class AgentIdentityManager {
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
file: 'soul' | 'instructions',
|
file: 'soul' | 'instructions',
|
||||||
suggestedContent: string,
|
suggestedContent: string,
|
||||||
reason: string
|
reason: string,
|
||||||
): IdentityChangeProposal {
|
options?: { skipAutonomyCheck?: boolean }
|
||||||
|
): IdentityChangeProposal | null {
|
||||||
|
// Autonomy check - identity updates are high-risk, always require approval
|
||||||
|
if (!options?.skipAutonomyCheck) {
|
||||||
|
const { decision } = canAutoExecute('identity_update', 8);
|
||||||
|
console.log(`[AgentIdentity] Autonomy check for identity update: ${decision.reason}`);
|
||||||
|
// Identity updates always require approval regardless of autonomy level
|
||||||
|
// But we log the decision for audit purposes
|
||||||
|
}
|
||||||
|
|
||||||
const identity = this.getIdentity(agentId);
|
const identity = this.getIdentity(agentId);
|
||||||
const currentContent = file === 'soul' ? identity.soul : identity.instructions;
|
const currentContent = file === 'soul' ? identity.soul : identity.instructions;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
type LLMServiceAdapter,
|
type LLMServiceAdapter,
|
||||||
type LLMProvider,
|
type LLMProvider,
|
||||||
} from './llm-service';
|
} from './llm-service';
|
||||||
|
import { canAutoExecute } from './autonomy-manager';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -181,8 +182,27 @@ export class ContextCompactor {
|
|||||||
messages: CompactableMessage[],
|
messages: CompactableMessage[],
|
||||||
agentId: string,
|
agentId: string,
|
||||||
conversationId?: string,
|
conversationId?: string,
|
||||||
options?: { forceLLM?: boolean }
|
options?: { forceLLM?: boolean; skipAutonomyCheck?: boolean }
|
||||||
): Promise<CompactionResult> {
|
): Promise<CompactionResult> {
|
||||||
|
// Autonomy check - verify if compaction is allowed
|
||||||
|
if (!options?.skipAutonomyCheck) {
|
||||||
|
const { canProceed, decision } = canAutoExecute('compaction_run', 5);
|
||||||
|
if (!canProceed) {
|
||||||
|
console.log(`[ContextCompactor] Autonomy check failed: ${decision.reason}`);
|
||||||
|
// Return result without compaction
|
||||||
|
return {
|
||||||
|
compactedMessages: messages,
|
||||||
|
summary: '',
|
||||||
|
originalCount: messages.length,
|
||||||
|
retainedCount: messages.length,
|
||||||
|
flushedMemories: 0,
|
||||||
|
tokensBeforeCompaction: estimateMessagesTokens(messages),
|
||||||
|
tokensAfterCompaction: estimateMessagesTokens(messages),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log(`[ContextCompactor] Autonomy check passed: ${decision.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
const tokensBeforeCompaction = estimateMessagesTokens(messages);
|
const tokensBeforeCompaction = estimateMessagesTokens(messages);
|
||||||
const keepCount = Math.min(this.config.keepRecentMessages, messages.length);
|
const keepCount = Math.min(this.config.keepRecentMessages, messages.length);
|
||||||
|
|
||||||
|
|||||||
@@ -958,15 +958,27 @@ export class GatewayClient {
|
|||||||
|
|
||||||
private async restPost<T>(path: string, body?: unknown): Promise<T> {
|
private async restPost<T>(path: string, body?: unknown): Promise<T> {
|
||||||
const baseUrl = this.getRestBaseUrl();
|
const baseUrl = this.getRestBaseUrl();
|
||||||
const response = await fetch(`${baseUrl}${path}`, {
|
const url = `${baseUrl}${path}`;
|
||||||
|
console.log(`[GatewayClient] POST ${url}`, body);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`REST API error: ${response.status} ${response.statusText}`);
|
const errorBody = await response.text().catch(() => '');
|
||||||
|
console.error(`[GatewayClient] POST ${url} failed: ${response.status} ${response.statusText}`, errorBody);
|
||||||
|
const error = new Error(`REST API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ''}`);
|
||||||
|
(error as any).status = response.status;
|
||||||
|
(error as any).body = errorBody;
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return response.json();
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log(`[GatewayClient] POST ${url} response:`, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async restPut<T>(path: string, body?: unknown): Promise<T> {
|
private async restPut<T>(path: string, body?: unknown): Promise<T> {
|
||||||
@@ -1318,12 +1330,19 @@ export class GatewayClient {
|
|||||||
|
|
||||||
/** Trigger a Hand */
|
/** Trigger a Hand */
|
||||||
async triggerHand(name: string, params?: Record<string, unknown>): Promise<{ runId: string; status: string }> {
|
async triggerHand(name: string, params?: Record<string, unknown>): Promise<{ runId: string; status: string }> {
|
||||||
|
console.log(`[GatewayClient] Triggering hand: ${name}`, params);
|
||||||
// OpenFang uses /activate endpoint, not /trigger
|
// OpenFang uses /activate endpoint, not /trigger
|
||||||
|
try {
|
||||||
const result = await this.restPost<{
|
const result = await this.restPost<{
|
||||||
instance_id: string;
|
instance_id: string;
|
||||||
status: string;
|
status: string;
|
||||||
}>(`/api/hands/${name}/activate`, params || {});
|
}>(`/api/hands/${name}/activate`, params || {});
|
||||||
|
console.log(`[GatewayClient] Hand trigger response:`, result);
|
||||||
return { runId: result.instance_id, status: result.status };
|
return { runId: result.instance_id, status: result.status };
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[GatewayClient] Hand trigger failed for ${name}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get Hand execution status */
|
/** Get Hand execution status */
|
||||||
|
|||||||
@@ -295,21 +295,59 @@ class GatewayLLMAdapter implements LLMServiceAdapter {
|
|||||||
const config = { ...this.config, ...options };
|
const config = { ...this.config, ...options };
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
const response = await fetch(`${config.apiBase}/complete`, {
|
// Build a single prompt from messages
|
||||||
|
const systemMessage = messages.find(m => m.role === 'system')?.content || '';
|
||||||
|
const userMessage = messages.find(m => m.role === 'user')?.content || '';
|
||||||
|
|
||||||
|
// Combine system and user messages into a single prompt
|
||||||
|
const fullPrompt = systemMessage
|
||||||
|
? `${systemMessage}\n\n${userMessage}`
|
||||||
|
: userMessage;
|
||||||
|
|
||||||
|
// Use OpenFang's chat endpoint (same as main chat)
|
||||||
|
// Try to get the default agent ID from localStorage or use 'default'
|
||||||
|
const agentId = localStorage.getItem('zclaw-default-agent-id') || 'default';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/agents/${agentId}/message`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages,
|
message: fullPrompt,
|
||||||
max_tokens: config.maxTokens,
|
max_tokens: config.maxTokens,
|
||||||
temperature: config.temperature,
|
temperature: config.temperature ?? 0.3, // Lower temperature for extraction tasks
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(config.timeout || 60000),
|
signal: AbortSignal.timeout(config.timeout || 60000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
|
// If agent not found, try without agent ID (direct /api/chat)
|
||||||
|
if (response.status === 404) {
|
||||||
|
const fallbackResponse = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: fullPrompt,
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
temperature: config.temperature ?? 0.3,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(config.timeout || 60000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fallbackResponse.ok) {
|
||||||
|
throw new Error(`[Gateway] Both endpoints failed: ${fallbackResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fallbackResponse.json();
|
||||||
|
const latencyMs = Date.now() - startTime;
|
||||||
|
return {
|
||||||
|
content: data.response || data.content || '',
|
||||||
|
tokensUsed: { input: data.input_tokens || 0, output: data.output_tokens || 0 },
|
||||||
|
latencyMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
throw new Error(`[Gateway] API error: ${response.status} - ${error}`);
|
throw new Error(`[Gateway] API error: ${response.status} - ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,15 +355,14 @@ class GatewayLLMAdapter implements LLMServiceAdapter {
|
|||||||
const latencyMs = Date.now() - startTime;
|
const latencyMs = Date.now() - startTime;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: data.content || data.choices?.[0]?.message?.content || '',
|
content: data.response || data.content || '',
|
||||||
tokensUsed: data.tokensUsed || { input: 0, output: 0 },
|
tokensUsed: { input: data.input_tokens || 0, output: data.output_tokens || 0 },
|
||||||
model: data.model,
|
|
||||||
latencyMs,
|
latencyMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isAvailable(): boolean {
|
isAvailable(): boolean {
|
||||||
// Gateway is available if we're connected to OpenFang
|
// Gateway is available if we're in browser (can connect to OpenFang)
|
||||||
return typeof window !== 'undefined';
|
return typeof window !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,8 +419,8 @@ export function loadConfig(): LLMConfig {
|
|||||||
// Ignore parse errors
|
// Ignore parse errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to mock for safety
|
// Default to gateway (OpenFang passthrough) for L4 self-evolution
|
||||||
return DEFAULT_CONFIGS.mock;
|
return DEFAULT_CONFIGS.gateway;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveConfig(config: LLMConfig): void {
|
export function saveConfig(config: LLMConfig): void {
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ const EXTRACTION_PROMPT = `请从以下对话中提取值得长期记住的信
|
|||||||
// === Default Config ===
|
// === Default Config ===
|
||||||
|
|
||||||
export const DEFAULT_EXTRACTION_CONFIG: ExtractionConfig = {
|
export const DEFAULT_EXTRACTION_CONFIG: ExtractionConfig = {
|
||||||
useLLM: false,
|
useLLM: true, // Enable LLM-powered semantic extraction by default
|
||||||
llmFallbackToRules: true,
|
llmFallbackToRules: true,
|
||||||
minMessagesForExtraction: 4,
|
minMessagesForExtraction: 2, // Lowered from 4 to capture memories earlier
|
||||||
extractionCooldownMs: 30_000,
|
extractionCooldownMs: 30_000,
|
||||||
minImportanceThreshold: 3,
|
minImportanceThreshold: 3,
|
||||||
};
|
};
|
||||||
@@ -119,12 +119,15 @@ export class MemoryExtractor {
|
|||||||
): Promise<ExtractionResult> {
|
): Promise<ExtractionResult> {
|
||||||
// Cooldown check
|
// Cooldown check
|
||||||
if (Date.now() - this.lastExtractionTime < this.config.extractionCooldownMs) {
|
if (Date.now() - this.lastExtractionTime < this.config.extractionCooldownMs) {
|
||||||
|
console.log('[MemoryExtractor] Skipping extraction: cooldown active');
|
||||||
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum message threshold
|
// Minimum message threshold
|
||||||
const chatMessages = messages.filter(m => m.role === 'user' || m.role === 'assistant');
|
const chatMessages = messages.filter(m => m.role === 'user' || m.role === 'assistant');
|
||||||
|
console.log(`[MemoryExtractor] Checking extraction: ${chatMessages.length} messages (min: ${this.config.minMessagesForExtraction})`);
|
||||||
if (chatMessages.length < this.config.minMessagesForExtraction) {
|
if (chatMessages.length < this.config.minMessagesForExtraction) {
|
||||||
|
console.log('[MemoryExtractor] Skipping extraction: not enough messages');
|
||||||
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +149,14 @@ export class MemoryExtractor {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Rule-based extraction
|
// Rule-based extraction
|
||||||
|
console.log('[MemoryExtractor] Using rule-based extraction');
|
||||||
extracted = this.ruleBasedExtraction(chatMessages);
|
extracted = this.ruleBasedExtraction(chatMessages);
|
||||||
|
console.log(`[MemoryExtractor] Rule-based extracted ${extracted.length} items before filtering`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by importance threshold
|
// Filter by importance threshold
|
||||||
extracted = extracted.filter(item => item.importance >= this.config.minImportanceThreshold);
|
extracted = extracted.filter(item => item.importance >= this.config.minImportanceThreshold);
|
||||||
|
console.log(`[MemoryExtractor] After importance filtering (>= ${this.config.minImportanceThreshold}): ${extracted.length} items`);
|
||||||
|
|
||||||
// Save to memory
|
// Save to memory
|
||||||
const memoryManager = getMemoryManager();
|
const memoryManager = getMemoryManager();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type LLMServiceAdapter,
|
type LLMServiceAdapter,
|
||||||
type LLMProvider,
|
type LLMProvider,
|
||||||
} from './llm-service';
|
} from './llm-service';
|
||||||
|
import { canAutoExecute } from './autonomy-manager';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ export const DEFAULT_REFLECTION_CONFIG: ReflectionConfig = {
|
|||||||
triggerAfterHours: 24,
|
triggerAfterHours: 24,
|
||||||
allowSoulModification: false,
|
allowSoulModification: false,
|
||||||
requireApproval: true,
|
requireApproval: true,
|
||||||
useLLM: false,
|
useLLM: true, // Enable LLM-powered deep reflection (Phase 4)
|
||||||
llmFallbackToRules: true,
|
llmFallbackToRules: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,9 +138,26 @@ export class ReflectionEngine {
|
|||||||
/**
|
/**
|
||||||
* Execute a reflection cycle for the given agent.
|
* Execute a reflection cycle for the given agent.
|
||||||
*/
|
*/
|
||||||
async reflect(agentId: string, options?: { forceLLM?: boolean }): Promise<ReflectionResult> {
|
async reflect(agentId: string, options?: { forceLLM?: boolean; skipAutonomyCheck?: boolean }): Promise<ReflectionResult> {
|
||||||
console.log(`[Reflection] Starting reflection for agent: ${agentId}`);
|
console.log(`[Reflection] Starting reflection for agent: ${agentId}`);
|
||||||
|
|
||||||
|
// Autonomy check - verify if reflection is allowed
|
||||||
|
if (!options?.skipAutonomyCheck) {
|
||||||
|
const { canProceed, decision } = canAutoExecute('reflection_run', 5);
|
||||||
|
if (!canProceed) {
|
||||||
|
console.log(`[Reflection] Autonomy check failed: ${decision.reason}`);
|
||||||
|
// Return empty result instead of throwing
|
||||||
|
return {
|
||||||
|
patterns: [],
|
||||||
|
improvements: [],
|
||||||
|
identityProposals: [],
|
||||||
|
newMemories: 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log(`[Reflection] Autonomy check passed: ${decision.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Try LLM-powered reflection if enabled
|
// Try LLM-powered reflection if enabled
|
||||||
if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) {
|
if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) {
|
||||||
try {
|
try {
|
||||||
@@ -575,8 +593,10 @@ ${recentHistory || '无'}
|
|||||||
identity.instructions + `\n\n## 自我反思改进\n${additions}`,
|
identity.instructions + `\n\n## 自我反思改进\n${additions}`,
|
||||||
`基于 ${negativePatterns.length} 个负面模式观察,建议在指令中增加自我改进提醒`
|
`基于 ${negativePatterns.length} 个负面模式观察,建议在指令中增加自我改进提醒`
|
||||||
);
|
);
|
||||||
|
if (proposal) {
|
||||||
proposals.push(proposal);
|
proposals.push(proposal);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return proposals;
|
return proposals;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMemoryManager } from './agent-memory';
|
import { getMemoryManager } from './agent-memory';
|
||||||
|
import { canAutoExecute } from './autonomy-manager';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -365,13 +366,33 @@ export class SkillDiscoveryEngine {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a skill as installed/uninstalled.
|
* Mark a skill as installed/uninstalled.
|
||||||
|
* Includes autonomy check for skill_install/skill_uninstall actions.
|
||||||
*/
|
*/
|
||||||
setSkillInstalled(skillId: string, installed: boolean): void {
|
setSkillInstalled(
|
||||||
|
skillId: string,
|
||||||
|
installed: boolean,
|
||||||
|
options?: { skipAutonomyCheck?: boolean }
|
||||||
|
): { success: boolean; reason?: string } {
|
||||||
const skill = this.skills.find(s => s.id === skillId);
|
const skill = this.skills.find(s => s.id === skillId);
|
||||||
if (skill) {
|
if (!skill) {
|
||||||
|
return { success: false, reason: `Skill not found: ${skillId}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autonomy check - verify if skill installation is allowed
|
||||||
|
if (!options?.skipAutonomyCheck) {
|
||||||
|
const action = installed ? 'skill_install' : 'skill_uninstall';
|
||||||
|
const { canProceed, decision } = canAutoExecute(action, 6);
|
||||||
|
console.log(`[SkillDiscovery] Autonomy check for ${action}: ${decision.reason}`);
|
||||||
|
|
||||||
|
if (!canProceed) {
|
||||||
|
return { success: false, reason: decision.reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
skill.installed = installed;
|
skill.installed = installed;
|
||||||
this.saveIndex();
|
this.saveIndex();
|
||||||
}
|
console.log(`[SkillDiscovery] Skill ${skillId} ${installed ? 'installed' : 'uninstalled'}`);
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import { ToastProvider } from './components/ui/Toast';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ToastProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</ToastProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -403,12 +403,21 @@ export const useChatStore = create<ChatState>()(
|
|||||||
set((state) => ({ messages: [...state.messages, handMsg] }));
|
set((state) => ({ messages: [...state.messages, handMsg] }));
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
set((state) => ({
|
const state = get();
|
||||||
|
|
||||||
|
// Save conversation to persist across refresh
|
||||||
|
const conversations = upsertActiveConversation([...state.conversations], state);
|
||||||
|
const currentConvId = state.currentConversationId || conversations[0]?.id;
|
||||||
|
|
||||||
|
set({
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
|
conversations,
|
||||||
|
currentConversationId: currentConvId,
|
||||||
messages: state.messages.map((m) =>
|
messages: state.messages.map((m) =>
|
||||||
m.id === assistantId ? { ...m, streaming: false, runId } : m
|
m.id === assistantId ? { ...m, streaming: false, runId } : m
|
||||||
),
|
),
|
||||||
}));
|
});
|
||||||
|
|
||||||
// Async memory extraction after stream completes
|
// Async memory extraction after stream completes
|
||||||
const msgs = get().messages
|
const msgs = get().messages
|
||||||
.filter(m => m.role === 'user' || m.role === 'assistant')
|
.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||||
|
|||||||
@@ -1294,11 +1294,15 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
triggerHand: async (name: string, params?: Record<string, unknown>) => {
|
triggerHand: async (name: string, params?: Record<string, unknown>) => {
|
||||||
|
console.log(`[GatewayStore] Triggering hand: ${name}`, params);
|
||||||
try {
|
try {
|
||||||
const result = await get().client.triggerHand(name, params);
|
const result = await get().client.triggerHand(name, params);
|
||||||
|
console.log(`[GatewayStore] Hand trigger result:`, result);
|
||||||
return result ? { runId: result.runId, status: result.status, startedAt: new Date().toISOString() } : undefined;
|
return result ? { runId: result.runId, status: result.status, startedAt: new Date().toISOString() } : undefined;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
set({ error: err instanceof Error ? err.message : String(err) });
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[GatewayStore] Hand trigger error:`, errorMsg, err);
|
||||||
|
set({ error: errorMsg });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "passed",
|
|
||||||
"failedTests": []
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 74 KiB |
524
docs/archive/openclaw-legacy/workbuddy界面/claw.html
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WorkBuddy - Claw</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
|
||||||
|
|
||||||
|
/* Robot floating animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
.robot-float { animation: float 4s ease-in-out infinite; }
|
||||||
|
|
||||||
|
/* Title styling */
|
||||||
|
.claw-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claw-icon {
|
||||||
|
color: #dc2626;
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ideas-text {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ideas-underline {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: linear-gradient(90deg, #a78bfa 0%, #c084fc 100%);
|
||||||
|
opacity: 0.6;
|
||||||
|
border-radius: 4px;
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
color: #10b981;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
vertical-align: super;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown animations */
|
||||||
|
.dropdown-menu {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
transition: all 0.15s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.dropdown-menu.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input area focus */
|
||||||
|
.input-container {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.input-container:focus-within {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button hover effects */
|
||||||
|
.tool-btn {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.tool-btn:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
.tool-btn.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar item selected state */
|
||||||
|
.sidebar-item {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.sidebar-item:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Model list item */
|
||||||
|
.model-item {
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
.model-item:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send button */
|
||||||
|
.send-btn {
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.send-btn:hover {
|
||||||
|
background-color: #10b981;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white h-screen flex text-gray-800">
|
||||||
|
|
||||||
|
<!-- Left Sidebar -->
|
||||||
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full select-none">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="h-14 flex items-center px-4 border-b border-gray-100 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-sm">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg text-gray-800 tracking-tight">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<button class="ml-auto p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors">
|
||||||
|
<i class="far fa-clone text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<nav class="flex-1 py-3 px-3 space-y-0.5 overflow-y-auto">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="px-1 mb-3">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" placeholder="搜索任务"
|
||||||
|
class="w-full pl-9 pr-8 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all">
|
||||||
|
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded text-gray-400 transition-colors">
|
||||||
|
<i class="fas fa-sliders-h text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Task Button -->
|
||||||
|
<button class="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 transition-colors mb-1">
|
||||||
|
<i class="far fa-check-square text-emerald-600 w-5"></i>
|
||||||
|
<span class="font-medium">新建任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Claw Item (Active with icons) -->
|
||||||
|
<div class="sidebar-item active flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer group">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<i class="fas fa-robot text-gray-700 w-5"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" title="打开文件夹">
|
||||||
|
<i class="far fa-folder-open text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" title="设置">
|
||||||
|
<i class="fas fa-cog text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other Nav Items -->
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-tie text-gray-400 w-5"></i>
|
||||||
|
<span>专家</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-gray-400 w-5"></i>
|
||||||
|
<span>技能</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-puzzle-piece text-gray-400 w-5"></i>
|
||||||
|
<span>插件</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="far fa-clock text-gray-400 w-5"></i>
|
||||||
|
<span>自动化</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="mt-8 px-4 text-center">
|
||||||
|
<p class="text-gray-900 font-medium mb-1">暂无任务</p>
|
||||||
|
<p class="text-gray-400 text-sm">点击上方按钮开始新任务</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="p-3 border-t border-gray-200 flex-shrink-0">
|
||||||
|
<button class="flex items-center gap-3 w-full hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
|
<i class="fas fa-robot text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-left text-sm font-medium text-gray-700">iven</span>
|
||||||
|
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 flex flex-col h-full relative bg-white">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4 flex-shrink-0">
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<div class="text-sm text-gray-600 font-medium">Agents</div>
|
||||||
|
<div class="flex-1 flex justify-end items-center gap-1">
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded-lg text-gray-500 transition-colors">
|
||||||
|
<i class="fas fa-minus text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded-lg text-gray-500 transition-colors">
|
||||||
|
<i class="far fa-square text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center hover:bg-red-50 hover:text-red-500 rounded-lg text-gray-500 transition-colors">
|
||||||
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto bg-white flex flex-col items-center justify-center p-8">
|
||||||
|
<div class="w-full max-w-3xl flex flex-col items-center">
|
||||||
|
|
||||||
|
<!-- Robot Mascot -->
|
||||||
|
<div class="robot-float mb-6 relative">
|
||||||
|
<div class="w-24 h-24 relative">
|
||||||
|
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=claw-red&backgroundColor=ffdfbf&eyes=happy"
|
||||||
|
alt="Claw Robot"
|
||||||
|
class="w-full h-full drop-shadow-xl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="claw-title mb-1 text-center">
|
||||||
|
Claw<span class="claw-icon"><i class="fas fa-paw"></i></span>Your
|
||||||
|
<span class="ideas-text">Ideas<span class="ideas-underline"></span></span>
|
||||||
|
</h1>
|
||||||
|
<h2 class="claw-title mb-6 text-center">
|
||||||
|
Into Reality<span class="star-icon"><i class="fas fa-star"></i></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="text-center text-gray-600 mb-8 space-y-1">
|
||||||
|
<p class="text-base">通过 Claw,让 Agent 随时随地接手并推进你的工作。</p>
|
||||||
|
<p class="text-base">前往 Claw 设置,连接企业微信、飞书、钉钉或 QQ,把任务带到每一个消息入口。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Area -->
|
||||||
|
<div class="w-full input-container bg-white rounded-2xl shadow-sm relative">
|
||||||
|
<!-- Input Toolbar -->
|
||||||
|
<div class="flex items-center gap-1 px-4 pt-3 pb-2">
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="提及">
|
||||||
|
<i class="fas fa-at"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="附件">
|
||||||
|
<i class="fas fa-paperclip"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Input -->
|
||||||
|
<div class="px-4 pb-2">
|
||||||
|
<textarea
|
||||||
|
placeholder="输入消息..."
|
||||||
|
class="w-full h-20 resize-none outline-none text-gray-700 placeholder-gray-400 text-base leading-relaxed bg-transparent"
|
||||||
|
style="min-height: 60px;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Toolbar -->
|
||||||
|
<div class="px-3 pb-3 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Craft Dropdown -->
|
||||||
|
<div class="relative" id="craftDropdown">
|
||||||
|
<button onclick="toggleDropdown('craftDropdown')" class="tool-btn flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-gray-700 border border-gray-200 bg-white">
|
||||||
|
<i class="far fa-window-maximize text-xs text-gray-500"></i>
|
||||||
|
<span>Craft</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs text-gray-400 ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Craft Menu -->
|
||||||
|
<div class="dropdown-menu absolute bottom-full left-0 mb-2 w-48 bg-white border border-gray-200 rounded-xl shadow-xl py-1.5 z-50">
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<i class="far fa-window-maximize text-gray-500"></i>
|
||||||
|
<span>Craft</span>
|
||||||
|
<i class="fas fa-check text-emerald-500 ml-auto text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<i class="far fa-calendar-check text-gray-500"></i>
|
||||||
|
<span>Plan</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<i class="far fa-comment-dots text-gray-500"></i>
|
||||||
|
<span>Ask</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto Dropdown -->
|
||||||
|
<div class="relative" id="autoDropdown">
|
||||||
|
<button onclick="toggleDropdown('autoDropdown')" class="tool-btn flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-gray-700 border border-gray-200 bg-white">
|
||||||
|
<i class="fas fa-layer-group text-xs text-gray-500"></i>
|
||||||
|
<span>Auto</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs text-gray-400 ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Model Menu -->
|
||||||
|
<div class="dropdown-menu absolute bottom-full left-0 mb-2 w-64 bg-white border border-gray-200 rounded-xl shadow-xl py-2 z-50">
|
||||||
|
<div class="px-3 py-1.5 text-xs text-gray-400 font-medium uppercase tracking-wider">内置模型</div>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<i class="fas fa-layer-group text-gray-600 text-sm"></i>
|
||||||
|
<span class="flex-1 text-sm text-gray-700">Auto</span>
|
||||||
|
<i class="fas fa-check text-emerald-500 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<i class="fas fa-wave-square text-pink-500 text-sm"></i>
|
||||||
|
<span class="text-sm text-gray-700">MiniMax-M2.5</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<div class="w-5 h-5 bg-purple-600 rounded flex items-center justify-center text-white text-xs font-bold">Z</div>
|
||||||
|
<span class="text-sm text-gray-700">GLM-5.0</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<div class="w-5 h-5 bg-purple-500 rounded flex items-center justify-center text-white text-xs font-bold">Z</div>
|
||||||
|
<span class="text-sm text-gray-700">GLM-4.7</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<div class="w-5 h-5 bg-gray-800 rounded flex items-center justify-center text-white text-xs font-bold">K</div>
|
||||||
|
<span class="text-sm text-gray-700">Kimi-K2.5</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<div class="w-5 h-5 bg-gray-700 rounded flex items-center justify-center text-white text-xs font-bold">K</div>
|
||||||
|
<span class="text-sm text-gray-700">Kimi-K2-Thinking</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="model-item w-full flex items-center gap-3 px-4 py-2 text-left">
|
||||||
|
<i class="fas fa-fish text-blue-500 text-sm"></i>
|
||||||
|
<span class="text-sm text-gray-700">DeepSeek-V3.2</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-auto"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills Dropdown -->
|
||||||
|
<div class="relative" id="skillsDropdown">
|
||||||
|
<button onclick="toggleDropdown('skillsDropdown')" class="tool-btn flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-gray-700 border border-gray-200 bg-white">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-xs text-gray-500"></i>
|
||||||
|
<span>Skills</span>
|
||||||
|
</button>
|
||||||
|
<!-- Skills Menu -->
|
||||||
|
<div class="dropdown-menu absolute bottom-full left-0 mb-2 w-80 bg-white border border-gray-200 rounded-xl shadow-xl py-2 z-50">
|
||||||
|
<div class="px-3 pb-2">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
|
||||||
|
<input type="text" placeholder="搜索技能" class="w-full pl-8 pr-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm outline-none focus:border-emerald-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-4 text-center text-gray-400 text-sm">
|
||||||
|
未找到技能
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-100 mt-1">
|
||||||
|
<button class="w-full flex items-center gap-2 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<i class="fas fa-file-import text-gray-500"></i>
|
||||||
|
<span>导入技能</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Folder Selection -->
|
||||||
|
<div class="relative" id="folderDropdown">
|
||||||
|
<button onclick="toggleDropdown('folderDropdown')" class="tool-btn flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-gray-600 hover:text-gray-800 transition-colors">
|
||||||
|
<i class="far fa-folder text-xs"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Folder Menu -->
|
||||||
|
<div class="dropdown-menu absolute bottom-full right-0 mb-2 w-72 bg-white border border-gray-200 rounded-xl shadow-xl py-2 z-50">
|
||||||
|
<div class="px-3 pb-2">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
|
||||||
|
<input type="text" placeholder="搜索工作空间" class="w-full pl-8 pr-8 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm outline-none focus:border-emerald-500">
|
||||||
|
<i class="fas fa-search absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<span>从空文件夹开始</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-2 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-700">
|
||||||
|
<i class="far fa-folder-open text-gray-400"></i>
|
||||||
|
<span>打开新文件夹</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-100 my-1"></div>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-2.5 hover:bg-gray-50 text-left bg-gray-50">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm font-medium text-gray-900">Claw</span>
|
||||||
|
<span class="text-xs text-gray-500 truncate max-w-[200px]">c:\Users\szend\WorkBudd...</span>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-check text-emerald-500 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Send Button -->
|
||||||
|
<button class="send-btn w-9 h-9 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 hover:bg-emerald-500 hover:text-white transition-all">
|
||||||
|
<i class="fas fa-paper-plane text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Track active dropdown
|
||||||
|
let activeDropdown = null;
|
||||||
|
|
||||||
|
function toggleDropdown(id) {
|
||||||
|
const dropdown = document.getElementById(id);
|
||||||
|
const menu = dropdown.querySelector('.dropdown-menu');
|
||||||
|
|
||||||
|
// Close if clicking same dropdown
|
||||||
|
if (activeDropdown === id) {
|
||||||
|
menu.classList.remove('active');
|
||||||
|
activeDropdown = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all others
|
||||||
|
document.querySelectorAll('.dropdown-menu').forEach(m => {
|
||||||
|
m.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open this one
|
||||||
|
menu.classList.add('active');
|
||||||
|
activeDropdown = id;
|
||||||
|
|
||||||
|
// Prevent event bubbling
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('.relative')) {
|
||||||
|
document.querySelectorAll('.dropdown-menu').forEach(m => {
|
||||||
|
m.classList.remove('active');
|
||||||
|
});
|
||||||
|
activeDropdown = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
const textarea = document.querySelector('textarea');
|
||||||
|
textarea.addEventListener('input', function() {
|
||||||
|
this.style.height = 'auto';
|
||||||
|
this.style.height = Math.max(60, this.scrollHeight) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Model selection interaction
|
||||||
|
document.querySelectorAll('.model-item').forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
// Remove check from all
|
||||||
|
document.querySelectorAll('.model-item .fa-check').forEach(check => {
|
||||||
|
check.remove();
|
||||||
|
});
|
||||||
|
// Add check to clicked
|
||||||
|
const check = document.createElement('i');
|
||||||
|
check.className = 'fas fa-check text-emerald-500 text-xs ml-auto';
|
||||||
|
this.appendChild(check);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent dropdown close when clicking inside
|
||||||
|
document.querySelectorAll('.dropdown-menu').forEach(menu => {
|
||||||
|
menu.addEventListener('click', function(e) {
|
||||||
|
if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
477
docs/archive/openclaw-legacy/workbuddy界面/专家.html
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WorkBuddy - 专家中心</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
|
||||||
|
|
||||||
|
/* Hide scrollbar for category nav but keep functionality */
|
||||||
|
.category-scroll::-webkit-scrollbar { display: none; }
|
||||||
|
.category-scroll { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
|
||||||
|
/* Expert card animations */
|
||||||
|
.expert-card {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border-top: 4px solid #8b5cf6;
|
||||||
|
}
|
||||||
|
.expert-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avatar ring animation on hover */
|
||||||
|
.avatar-ring {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.expert-card:hover .avatar-ring {
|
||||||
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category tab active indicator */
|
||||||
|
.category-tab {
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.category-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summon button pulse */
|
||||||
|
@keyframes pulse-purple {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 10px rgba(139, 92, 246, 0); }
|
||||||
|
}
|
||||||
|
.summon-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.summon-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 10px 20px -5px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
.summon-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag styling */
|
||||||
|
.expert-tag {
|
||||||
|
background-color: #f3e8ff;
|
||||||
|
color: #7c3aed;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar active state */
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white h-screen flex text-gray-800">
|
||||||
|
|
||||||
|
<!-- Left Sidebar -->
|
||||||
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full flex-shrink-0">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="h-14 flex items-center px-4 border-b border-gray-100 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-sm">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg text-gray-800 tracking-tight">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<button class="ml-auto p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors">
|
||||||
|
<i class="far fa-clone text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<nav class="flex-1 py-3 px-3 space-y-0.5 overflow-y-auto">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="px-1 mb-3">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" placeholder="搜索任务"
|
||||||
|
class="w-full pl-9 pr-8 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all">
|
||||||
|
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded text-gray-400 transition-colors">
|
||||||
|
<i class="fas fa-sliders-h text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Task Button -->
|
||||||
|
<button class="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 transition-colors mb-1">
|
||||||
|
<i class="far fa-check-square text-emerald-600 w-5"></i>
|
||||||
|
<span class="font-medium">新建任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Nav Items -->
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-robot text-gray-400 w-5"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item active flex items-center gap-3 px-3 py-2 text-gray-900 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-tie text-gray-700 w-5"></i>
|
||||||
|
<span>专家</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-gray-400 w-5"></i>
|
||||||
|
<span>技能</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-puzzle-piece text-gray-400 w-5"></i>
|
||||||
|
<span>插件</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="far fa-clock text-gray-400 w-5"></i>
|
||||||
|
<span>自动化</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="mt-8 px-4 text-center">
|
||||||
|
<p class="text-gray-900 font-medium mb-1">暂无任务</p>
|
||||||
|
<p class="text-gray-400 text-sm">点击上方按钮开始新任务</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="p-3 border-t border-gray-200 flex-shrink-0">
|
||||||
|
<button class="flex items-center gap-3 w-full hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
|
<i class="fas fa-robot text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-left text-sm font-medium text-gray-700">iven</span>
|
||||||
|
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 flex flex-col h-full bg-white overflow-hidden">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 font-medium">Agents</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="fas fa-minus text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="far fa-square text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-red-50 rounded-lg text-gray-500 hover:text-red-500">
|
||||||
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto bg-gray-50/50">
|
||||||
|
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">专家中心</h1>
|
||||||
|
<p class="text-gray-500 text-base">按行业分类浏览专家,召唤他们为你服务</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Tabs -->
|
||||||
|
<div class="mb-8 border-b border-gray-200">
|
||||||
|
<div class="category-scroll flex gap-8 overflow-x-auto pb-0">
|
||||||
|
<button class="category-tab active pb-3 text-violet-600 font-medium text-sm" onclick="filterExperts('all', this)">
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('design', this)">
|
||||||
|
设计 <span class="text-gray-400 ml-1">(8)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('engineering', this)">
|
||||||
|
工程技术 <span class="text-gray-400 ml-1">(21)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('marketing', this)">
|
||||||
|
市场营销 <span class="text-gray-400 ml-1">(26)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('media', this)">
|
||||||
|
付费媒体 <span class="text-gray-400 ml-1">(7)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('sales', this)">
|
||||||
|
销售 <span class="text-gray-400 ml-1">(8)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('product', this)">
|
||||||
|
产品 <span class="text-gray-400 ml-1">(4)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('pm', this)">
|
||||||
|
项目管理 <span class="text-gray-400 ml-1">(6)</span>
|
||||||
|
</button>
|
||||||
|
<button class="category-tab pb-3 text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" onclick="filterExperts('quality', this)">
|
||||||
|
质量测试 <span class="text-gray-400 ml-1">(12)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Experts Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="expertsGrid">
|
||||||
|
|
||||||
|
<!-- Expert Card: Kai -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="marketing">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg relative">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Kai&backgroundColor=b6e3f4&clothing=graphicShirt&eyebrows=default&eyes=happy&mouth=smile&top=shortHair"
|
||||||
|
alt="Kai" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Kai</h3>
|
||||||
|
<span class="expert-tag mb-3">内容创作专家</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
擅长创作引人入胜的多平台内容,让品牌故事触达目标受众
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Kai')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Phoebe -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="engineering">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Phoebe&backgroundColor=c0aede&clothing=collarAndSweater&eyes=default&mouth=default&top=longHair"
|
||||||
|
alt="Phoebe" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Phoebe</h3>
|
||||||
|
<span class="expert-tag mb-3">数据分析报告师</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
将复杂数据转化为清晰可执行的业务报告,让数据驱动决策
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Phoebe')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Jude -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="sales">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Jude&backgroundColor=ffdfbf&clothing=blazerAndShirt&eyes=default&mouth=smile&top=shortHair"
|
||||||
|
alt="Jude" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Jude</h3>
|
||||||
|
<span class="expert-tag mb-3">中国电商运营专家</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
精通天猫京东拼多多等平台运营,从选品到爆款一站式操盘
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Jude')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Maya -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="media">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Maya&backgroundColor=ffd5dc&clothing=graphicShirt&eyes=default&mouth=smile&top=longHair"
|
||||||
|
alt="Maya" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Maya</h3>
|
||||||
|
<span class="expert-tag mb-3">抖音策略专家</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
精通抖音算法和内容营销,助力品牌打造短视频爆款并实现商业变现
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Maya')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Ula -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="sales">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Ula&backgroundColor=d1d4f9&clothing=collarAndSweater&eyes=default&mouth=default&top=shortHair"
|
||||||
|
alt="Ula" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Ula</h3>
|
||||||
|
<span class="expert-tag mb-3">销售教练</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
用苏格拉底式提问训练销售团队,从60%达标到总统俱乐部
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Ula')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Ben -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="design">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Ben&backgroundColor=c0aede&clothing=blazerAndShirt&eyes=default&mouth=smile&top=shortHair"
|
||||||
|
alt="Ben" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Ben</h3>
|
||||||
|
<span class="expert-tag mb-3">品牌策略师</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
15年品牌战略经验,守护品牌一致性的终极捍卫者
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Ben')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Fay -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="marketing">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Fay&backgroundColor=ffdfbf&clothing=graphicShirt&eyes=default&mouth=smile&top=longHair"
|
||||||
|
alt="Fay" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Fay</h3>
|
||||||
|
<span class="expert-tag mb-3">小红书运营专家</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
深谙小红书种草生态和推荐机制,打造高互动率种草内容
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Fay')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Card: Tess -->
|
||||||
|
<div class="expert-card bg-white rounded-2xl p-6 border border-gray-100 flex flex-col items-center text-center relative overflow-hidden group" data-category="product">
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 to-purple-600"></div>
|
||||||
|
<div class="avatar-ring w-20 h-20 rounded-full overflow-hidden mb-4 border-4 border-white shadow-lg">
|
||||||
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Tess&backgroundColor=b6e3f4&clothing=collarAndSweater&eyes=default&mouth=default&top=longHair"
|
||||||
|
alt="Tess" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 mb-2">Tess</h3>
|
||||||
|
<span class="expert-tag mb-3">招聘专家</span>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
|
||||||
|
精通人才招聘全流程,从简历筛选到面试评估,为企业寻找最佳人才
|
||||||
|
</p>
|
||||||
|
<button class="summon-btn w-full py-2.5 rounded-xl text-white font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-violet-200" onclick="summonExpert('Tess')">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
立即召唤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading More Indicator -->
|
||||||
|
<div class="mt-12 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 text-gray-400 text-sm">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i>
|
||||||
|
<span>更多专家加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div id="toast" class="fixed bottom-8 right-8 bg-gray-900 text-white px-6 py-3 rounded-xl shadow-2xl transform translate-y-20 opacity-0 transition-all duration-300 flex items-center gap-3 z-50">
|
||||||
|
<i class="fas fa-check-circle text-emerald-400"></i>
|
||||||
|
<span id="toastMessage">已召唤专家</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Filter experts by category
|
||||||
|
function filterExperts(category, element) {
|
||||||
|
// Update active tab
|
||||||
|
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||||
|
tab.classList.remove('active', 'text-violet-600');
|
||||||
|
tab.classList.add('text-gray-500');
|
||||||
|
});
|
||||||
|
element.classList.add('active', 'text-violet-600');
|
||||||
|
element.classList.remove('text-gray-500');
|
||||||
|
|
||||||
|
// Filter cards with animation
|
||||||
|
const cards = document.querySelectorAll('.expert-card');
|
||||||
|
cards.forEach(card => {
|
||||||
|
if (category === 'all' || card.dataset.category === category) {
|
||||||
|
card.style.display = 'flex';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.opacity = '1';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(20px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summon expert action
|
||||||
|
function summonExpert(name) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastMessage = document.getElementById('toastMessage');
|
||||||
|
|
||||||
|
toastMessage.textContent = `已召唤 ${name} 为您服务`;
|
||||||
|
toast.classList.remove('translate-y-20', 'opacity-0');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('translate-y-20', 'opacity-0');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scroll animation for cards on load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const cards = document.querySelectorAll('.expert-card');
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(20px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
}, index * 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Horizontal scroll with mouse wheel for category tabs
|
||||||
|
document.querySelector('.category-scroll').addEventListener('wheel', (e) => {
|
||||||
|
if (e.deltaY !== 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.scrollLeft += e.deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
537
docs/archive/openclaw-legacy/workbuddy界面/技能.html
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WorkBuddy - 技能</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
|
||||||
|
|
||||||
|
/* Skill card animations */
|
||||||
|
.skill-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.skill-card:hover {
|
||||||
|
border-color: #d1d5db;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add button animation */
|
||||||
|
.add-btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.add-btn:hover {
|
||||||
|
border-color: #10b981;
|
||||||
|
color: #10b981;
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.add-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
.add-btn.added {
|
||||||
|
background-color: #10b981;
|
||||||
|
border-color: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon container */
|
||||||
|
.skill-icon {
|
||||||
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.skill-card:hover .skill-icon {
|
||||||
|
background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar active state */
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search input focus */
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: #10b981;
|
||||||
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import button hover */
|
||||||
|
.import-btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.import-btn:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white h-screen flex text-gray-800">
|
||||||
|
|
||||||
|
<!-- Left Sidebar -->
|
||||||
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full flex-shrink-0">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="h-14 flex items-center px-4 border-b border-gray-100 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-sm">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg text-gray-800 tracking-tight">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<button class="ml-auto p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors">
|
||||||
|
<i class="far fa-clone text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<nav class="flex-1 py-3 px-3 space-y-0.5 overflow-y-auto">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="px-1 mb-3">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" placeholder="搜索任务"
|
||||||
|
class="w-full pl-9 pr-8 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all">
|
||||||
|
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded text-gray-400 transition-colors">
|
||||||
|
<i class="fas fa-sliders-h text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Task Button -->
|
||||||
|
<button class="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 transition-colors mb-1">
|
||||||
|
<i class="far fa-check-square text-emerald-600 w-5"></i>
|
||||||
|
<span class="font-medium">新建任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Nav Items -->
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-robot text-gray-400 w-5"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-tie text-gray-400 w-5"></i>
|
||||||
|
<span>专家</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item active flex items-center gap-3 px-3 py-2 text-gray-900 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-gray-700 w-5"></i>
|
||||||
|
<span>技能</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-puzzle-piece text-gray-400 w-5"></i>
|
||||||
|
<span>插件</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="far fa-clock text-gray-400 w-5"></i>
|
||||||
|
<span>自动化</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="mt-8 px-4 text-center">
|
||||||
|
<p class="text-gray-900 font-medium mb-1">暂无任务</p>
|
||||||
|
<p class="text-gray-400 text-sm">点击上方按钮开始新任务</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="p-3 border-t border-gray-200 flex-shrink-0">
|
||||||
|
<button class="flex items-center gap-3 w-full hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
|
<i class="fas fa-robot text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-left text-sm font-medium text-gray-700">iven</span>
|
||||||
|
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 flex flex-col h-full bg-white overflow-hidden">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 font-medium">Agents</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="fas fa-minus text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="far fa-square text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-red-50 rounded-lg text-gray-500 hover:text-red-500">
|
||||||
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto bg-white">
|
||||||
|
<div class="max-w-6xl mx-auto px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex items-start justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 mb-1">技能</h1>
|
||||||
|
<p class="text-gray-500 text-base">赋予 WorkBuddy 更强大的能力</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" id="skillSearch" placeholder="搜索技能"
|
||||||
|
class="search-input w-64 pl-9 pr-4 py-2 bg-white border border-gray-300 rounded-lg text-sm outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Button -->
|
||||||
|
<button class="import-btn flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" onclick="importSkill()">
|
||||||
|
<i class="fas fa-plus text-xs"></i>
|
||||||
|
导入技能
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Installed Count -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<span class="text-gray-900 font-medium">已安装</span>
|
||||||
|
<span class="ml-2 px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded-full" id="installedCount">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recommended Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-semibold text-gray-900 mb-4">推荐</h2>
|
||||||
|
|
||||||
|
<!-- Skills Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4" id="skillsGrid">
|
||||||
|
|
||||||
|
<!-- Skill Card 1 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="find-skills">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">find-skills</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Helps users discover and install agent skills whe...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'find-skills')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 2 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="workbuddy-channel-setup">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">workbuddy-channel-setup</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Automate WorkBuddy channel integration setu...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'workbuddy-channel-setup')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 3 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="FBS-BookWriter">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">FBS-BookWriter</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">福帮手出品 | 人机协同写书。联网校验-千书千面...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'FBS-BookWriter')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 4 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="xiaohongshu">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">xiaohongshu</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">小红书(RedNote)内容工具。使用场景:搜索...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'xiaohongshu')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 5 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="agentmail">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">agentmail</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Email inbox for AI agents. Check messages, sen...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'agentmail')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 6 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="apple-notes">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">apple-notes</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Manage Apple Notes via the `memo` CLI on ma...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'apple-notes')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 7 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="apple-reminders">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">apple-reminders</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Manage Apple Reminders via the `remindctl` CL...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'apple-reminders')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 8 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="blogwatcher">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">blogwatcher</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Monitor blogs and RSS/Atom feeds for updates...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'blogwatcher')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 9 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="cos-vectors">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">cos-vectors</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Manage Tencent Cloud COS vector buckets via ...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'cos-vectors')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 10 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="gifgrep">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">gifgrep</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Search GIF providers with CLI/TUI, download re...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'gifgrep')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 11 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="github">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">github</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Interact with GitHub using the `gh` CLI. Use `gh ...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'github')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 12 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="gog">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">gog</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Google Workspace CLI for Gmail, Calendar, Driv...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'gog')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 13 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="healthcheck">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">healthcheck</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">Track water and sleep with JSON file storage.</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'healthcheck')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Card 14 -->
|
||||||
|
<div class="skill-card bg-white rounded-xl p-4 flex items-center gap-4 group cursor-pointer" data-name="himalaya">
|
||||||
|
<div class="skill-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-hammer text-gray-600 text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-0.5">himalaya</h3>
|
||||||
|
<p class="text-gray-500 text-sm truncate">CLI to manage emails via IMAP/SMTP. Use `him...</p>
|
||||||
|
</div>
|
||||||
|
<button class="add-btn w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-emerald-600 flex-shrink-0" onclick="toggleSkill(this, 'himalaya')">
|
||||||
|
<i class="fas fa-plus text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State (Hidden by default) -->
|
||||||
|
<div id="emptyState" class="hidden mt-12 text-center py-12">
|
||||||
|
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<i class="fas fa-search text-gray-400 text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500">未找到相关技能</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div id="toast" class="fixed bottom-8 right-8 bg-gray-900 text-white px-6 py-3 rounded-xl shadow-2xl transform translate-y-20 opacity-0 transition-all duration-300 flex items-center gap-3 z-50">
|
||||||
|
<i class="fas fa-check-circle text-emerald-400"></i>
|
||||||
|
<span id="toastMessage">操作成功</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let installedCount = 0;
|
||||||
|
const installedSkills = new Set();
|
||||||
|
|
||||||
|
// Toggle skill installation
|
||||||
|
function toggleSkill(btn, skillName) {
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
|
||||||
|
if (installedSkills.has(skillName)) {
|
||||||
|
// Uninstall
|
||||||
|
installedSkills.delete(skillName);
|
||||||
|
btn.classList.remove('added');
|
||||||
|
icon.classList.remove('fa-check');
|
||||||
|
icon.classList.add('fa-plus');
|
||||||
|
installedCount--;
|
||||||
|
showToast(`已卸载 ${skillName}`);
|
||||||
|
} else {
|
||||||
|
// Install
|
||||||
|
installedSkills.add(skillName);
|
||||||
|
btn.classList.add('added');
|
||||||
|
icon.classList.remove('fa-plus');
|
||||||
|
icon.classList.add('fa-check');
|
||||||
|
installedCount++;
|
||||||
|
showToast(`已安装 ${skillName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('installedCount').textContent = installedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import skill action
|
||||||
|
function importSkill() {
|
||||||
|
showToast('请选择要导入的技能文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
function showToast(message) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastMessage = document.getElementById('toastMessage');
|
||||||
|
|
||||||
|
toastMessage.textContent = message;
|
||||||
|
toast.classList.remove('translate-y-20', 'opacity-0');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('translate-y-20', 'opacity-0');
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
document.getElementById('skillSearch').addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const name = card.getAttribute('data-name').toLowerCase();
|
||||||
|
const desc = card.querySelector('p').textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (name.includes(searchTerm) || desc.includes(searchTerm)) {
|
||||||
|
card.style.display = 'flex';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide empty state
|
||||||
|
const emptyState = document.getElementById('emptyState');
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
emptyState.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
emptyState.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add stagger animation on load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(10px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.transition = 'all 0.3s ease';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
}, index * 30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
489
docs/archive/openclaw-legacy/workbuddy界面/新建任务.html
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WorkBuddy - 新建任务</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown animations */
|
||||||
|
.dropdown-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
}
|
||||||
|
.dropdown-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
transition: opacity 0.1s ease-out, transform 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text effect for title */
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom shadow for cards */
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input area focus ring */
|
||||||
|
.input-focus-ring:focus-within {
|
||||||
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill tag hover effect */
|
||||||
|
.skill-tag {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.skill-tag:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Robot animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
.robot-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 h-screen flex text-gray-800">
|
||||||
|
|
||||||
|
<!-- Left Sidebar -->
|
||||||
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="h-14 flex items-center px-4 border-b border-gray-100">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg text-gray-800">CodeBuddy</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<nav class="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="px-2 mb-4">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" placeholder="搜索任务"
|
||||||
|
class="w-full pl-9 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors">
|
||||||
|
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded text-gray-400">
|
||||||
|
<i class="fas fa-sliders-h text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Task Button -->
|
||||||
|
<button class="w-full flex items-center gap-3 px-3 py-2.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-700 font-medium transition-colors mb-2">
|
||||||
|
<i class="far fa-check-square text-emerald-600"></i>
|
||||||
|
<span>新建任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Nav Items -->
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors group">
|
||||||
|
<i class="fas fa-robot text-gray-400 group-hover:text-emerald-600 w-5"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors group">
|
||||||
|
<i class="fas fa-user-tie text-gray-400 group-hover:text-emerald-600 w-5"></i>
|
||||||
|
<span>专家</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors group">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-gray-400 group-hover:text-emerald-600 w-5"></i>
|
||||||
|
<span>技能</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors group">
|
||||||
|
<i class="fas fa-puzzle-piece text-gray-400 group-hover:text-emerald-600 w-5"></i>
|
||||||
|
<span>插件</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors group">
|
||||||
|
<i class="far fa-clock text-gray-400 group-hover:text-emerald-600 w-5"></i>
|
||||||
|
<span>自动化</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="mt-8 px-4 text-center">
|
||||||
|
<p class="text-gray-500 text-sm mb-1">暂无任务</p>
|
||||||
|
<p class="text-gray-400 text-xs">点击上方按钮开始新任务</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="p-4 border-t border-gray-200">
|
||||||
|
<button class="flex items-center gap-3 w-full hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold">
|
||||||
|
<i class="fas fa-robot text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-left text-sm font-medium text-gray-700">iven</span>
|
||||||
|
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 flex flex-col h-full relative">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-6">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg">WorkBuddy</span>
|
||||||
|
<button class="ml-2 p-1.5 hover:bg-gray-100 rounded-lg text-gray-400">
|
||||||
|
<i class="far fa-clone text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 font-medium">Agents</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="fas fa-minus text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="far fa-square text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500 hover:text-red-500">
|
||||||
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto bg-white">
|
||||||
|
<div class="max-w-5xl mx-auto px-8 py-12">
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<!-- Robot Mascot -->
|
||||||
|
<div class="robot-float mb-6 inline-block">
|
||||||
|
<div class="w-24 h-24 mx-auto relative">
|
||||||
|
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=claw&backgroundColor=ffdfbf"
|
||||||
|
alt="Claw Robot"
|
||||||
|
class="w-full h-full drop-shadow-2xl">
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<div class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center text-white text-xs animate-pulse">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="text-4xl font-bold text-gray-800 mb-2 tracking-tight">
|
||||||
|
Claw <span class="text-red-500 mx-1"><i class="fas fa-claw"></i></span> Your Ideas
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-4xl font-bold text-gray-800 mb-4 tracking-tight">
|
||||||
|
Into Reality
|
||||||
|
<span class="inline-block ml-1 text-emerald-400 animate-pulse">*</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Cards -->
|
||||||
|
<div class="grid grid-cols-2 gap-6 mb-10 max-w-3xl mx-auto">
|
||||||
|
<!-- Code Development Card -->
|
||||||
|
<div class="bg-gray-50 rounded-2xl p-6 card-shadow border border-gray-100 relative overflow-hidden group hover:border-gray-200 transition-all">
|
||||||
|
<div class="absolute top-0 left-1/2 -translate-x-1/2 bg-gray-400 text-white text-xs px-4 py-1 rounded-b-lg font-medium">
|
||||||
|
代码开发
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<h3 class="text-gray-500 font-medium mb-2">面向代码与工程交付</h3>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-gray-100/50 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Daily Office Card -->
|
||||||
|
<div class="bg-gray-50 rounded-2xl p-6 card-shadow border border-gray-100 relative overflow-hidden group hover:border-indigo-200 transition-all">
|
||||||
|
<div class="absolute top-0 left-1/2 -translate-x-1/2 bg-indigo-500 text-white text-xs px-4 py-1 rounded-b-lg font-medium shadow-lg shadow-indigo-200">
|
||||||
|
日常办公
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<h3 class="text-indigo-600 font-medium mb-3">面向日常工作与知识生产</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed px-2">
|
||||||
|
将整理、写作、规划与跟进交给 AI 智能体推进,并获得结构化、可落地的工作成果。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-indigo-50/30 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Area -->
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="bg-white border border-gray-200 rounded-2xl input-focus-ring transition-all">
|
||||||
|
<!-- Input Toolbar -->
|
||||||
|
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-100">
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="@提及">
|
||||||
|
<i class="fas fa-at"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="附件">
|
||||||
|
<i class="fas fa-paperclip"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Input -->
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<textarea
|
||||||
|
placeholder="输入消息..."
|
||||||
|
class="w-full h-24 resize-none outline-none text-gray-700 placeholder-gray-400 text-base leading-relaxed bg-transparent"
|
||||||
|
style="min-height: 80px;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Toolbar -->
|
||||||
|
<div class="px-4 py-3 flex items-center justify-between border-t border-gray-100">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Craft Dropdown -->
|
||||||
|
<div class="relative" id="craftDropdown">
|
||||||
|
<button onclick="toggleDropdown('craftDropdown')" class="flex items-center gap-2 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium text-gray-700 transition-colors">
|
||||||
|
<i class="far fa-window-maximize text-xs"></i>
|
||||||
|
<span>Craft</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Dropdown Menu -->
|
||||||
|
<div class="hidden absolute bottom-full left-0 mb-2 w-48 bg-white border border-gray-200 rounded-xl shadow-xl py-1 z-50">
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm">
|
||||||
|
<i class="far fa-window-maximize text-gray-400"></i>
|
||||||
|
<span>Craft</span>
|
||||||
|
<i class="fas fa-check text-emerald-500 ml-auto text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm">
|
||||||
|
<i class="far fa-calendar-check text-gray-400"></i>
|
||||||
|
<span>Plan</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm">
|
||||||
|
<i class="far fa-comment-dots text-gray-400"></i>
|
||||||
|
<span>Ask</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto Dropdown -->
|
||||||
|
<div class="relative" id="autoDropdown">
|
||||||
|
<button onclick="toggleDropdown('autoDropdown')" class="flex items-center gap-2 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium text-gray-700 transition-colors">
|
||||||
|
<i class="fas fa-layer-group text-xs"></i>
|
||||||
|
<span>Auto</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Model Selection Dropdown -->
|
||||||
|
<div class="hidden absolute bottom-full left-0 mb-2 w-64 bg-white border border-gray-200 rounded-xl shadow-xl py-2 z-50">
|
||||||
|
<div class="px-3 py-1.5 text-xs text-gray-400 font-medium uppercase tracking-wider">内置模型</div>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<i class="fas fa-layer-group text-gray-600"></i>
|
||||||
|
<span class="flex-1 text-sm">Auto</span>
|
||||||
|
<i class="fas fa-check text-emerald-500 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<i class="fas fa-wave-square text-pink-500"></i>
|
||||||
|
<span class="text-sm">MiniMax-M2.5</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<div class="w-4 h-4 bg-purple-600 rounded flex items-center justify-center text-white text-xs font-bold">Z</div>
|
||||||
|
<span class="text-sm">GLM-5.0</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<div class="w-4 h-4 bg-purple-500 rounded flex items-center justify-center text-white text-xs font-bold">Z</div>
|
||||||
|
<span class="text-sm">GLM-4.7</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<div class="w-4 h-4 bg-gray-800 rounded flex items-center justify-center text-white text-xs font-bold">K</div>
|
||||||
|
<span class="text-sm">Kimi-K2.5</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<div class="w-4 h-4 bg-gray-700 rounded flex items-center justify-center text-white text-xs font-bold">K</div>
|
||||||
|
<span class="text-sm">Kimi-K2-Thinking</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-50 text-left">
|
||||||
|
<i class="fas fa-fish text-blue-500"></i>
|
||||||
|
<span class="text-sm">DeepSeek-V3.2</span>
|
||||||
|
<i class="fas fa-snowflake text-gray-300 text-xs ml-1"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills Button -->
|
||||||
|
<button class="flex items-center gap-2 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium text-gray-700 transition-colors">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-xs"></i>
|
||||||
|
<span>Skills</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Folder Selection -->
|
||||||
|
<div class="relative" id="folderDropdown">
|
||||||
|
<button onclick="toggleDropdown('folderDropdown')" class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-100 rounded-lg text-sm text-gray-600 transition-colors border border-gray-200">
|
||||||
|
<i class="fas fa-desktop text-xs"></i>
|
||||||
|
<span>选择文件夹</span>
|
||||||
|
<i class="fas fa-chevron-down text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Folder Menu -->
|
||||||
|
<div class="hidden absolute bottom-full right-0 mb-2 w-72 bg-white border border-gray-200 rounded-xl shadow-xl py-2 z-50">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
|
||||||
|
<input type="text" placeholder="搜索工作空间" class="w-full pl-8 pr-3 py-1.5 bg-gray-50 rounded-lg text-sm outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm">
|
||||||
|
<span class="flex-1">从空文件夹开始</span>
|
||||||
|
<i class="fas fa-check text-emerald-500 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm text-gray-600">
|
||||||
|
<i class="far fa-folder-open"></i>
|
||||||
|
<span>打开新文件夹</span>
|
||||||
|
</button>
|
||||||
|
<div class="px-4 py-2 text-xs text-gray-400 font-medium">Claw</div>
|
||||||
|
<button class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left text-sm pl-8 text-gray-600">
|
||||||
|
<span class="truncate">c:\Users\szend\WorkBuddy\Claw</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Send Button -->
|
||||||
|
<button class="w-9 h-9 bg-gray-200 hover:bg-emerald-500 hover:text-white rounded-lg flex items-center justify-center text-gray-400 transition-all transform hover:scale-105">
|
||||||
|
<i class="fas fa-paper-plane text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skill Tags -->
|
||||||
|
<div class="mt-6 flex flex-wrap justify-center gap-3">
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
文档处理
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
视频生成
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
深度研究
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
幻灯片
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
数据分析
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
数据可视化
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
金融服务
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
产品管理
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
设计
|
||||||
|
</button>
|
||||||
|
<button class="skill-tag px-4 py-2 bg-white border border-gray-200 rounded-full text-sm text-gray-600 hover:border-gray-300 hover:text-gray-800">
|
||||||
|
邮件编辑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Dropdown toggle functionality
|
||||||
|
function toggleDropdown(id) {
|
||||||
|
const dropdown = document.getElementById(id).querySelector('div[class*="hidden"], div[class*="block"]');
|
||||||
|
const allDropdowns = document.querySelectorAll('[id$="Dropdown"] > div');
|
||||||
|
|
||||||
|
// Close all other dropdowns
|
||||||
|
allDropdowns.forEach(d => {
|
||||||
|
if (d !== dropdown) {
|
||||||
|
d.classList.add('hidden');
|
||||||
|
d.classList.remove('block');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle current
|
||||||
|
if (dropdown.classList.contains('hidden')) {
|
||||||
|
dropdown.classList.remove('hidden');
|
||||||
|
dropdown.classList.add('block');
|
||||||
|
dropdown.classList.add('dropdown-enter');
|
||||||
|
setTimeout(() => {
|
||||||
|
dropdown.classList.add('dropdown-enter-active');
|
||||||
|
dropdown.classList.remove('dropdown-enter');
|
||||||
|
}, 10);
|
||||||
|
} else {
|
||||||
|
dropdown.classList.add('hidden');
|
||||||
|
dropdown.classList.remove('block');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('[id$="Dropdown"]')) {
|
||||||
|
document.querySelectorAll('[id$="Dropdown"] > div').forEach(d => {
|
||||||
|
d.classList.add('hidden');
|
||||||
|
d.classList.remove('block');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
const textarea = document.querySelector('textarea');
|
||||||
|
textarea.addEventListener('input', function() {
|
||||||
|
this.style.height = 'auto';
|
||||||
|
this.style.height = Math.max(80, this.scrollHeight) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skill tag click effect
|
||||||
|
document.querySelectorAll('.skill-tag').forEach(tag => {
|
||||||
|
tag.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('bg-emerald-50');
|
||||||
|
this.classList.toggle('border-emerald-200');
|
||||||
|
this.classList.toggle('text-emerald-700');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
366
docs/archive/openclaw-legacy/workbuddy界面/自动化.html
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WorkBuddy - 自动化</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
|
||||||
|
|
||||||
|
/* Template card animations */
|
||||||
|
.template-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.template-card:hover {
|
||||||
|
border-color: #d1d5db;
|
||||||
|
background-color: #fafafa;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon container */
|
||||||
|
.template-icon {
|
||||||
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.template-card:hover .template-icon {
|
||||||
|
background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Beta tag */
|
||||||
|
.beta-tag {
|
||||||
|
background-color: #6b7280;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar active state */
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button hover effects */
|
||||||
|
.btn-primary {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white h-screen flex text-gray-800">
|
||||||
|
|
||||||
|
<!-- Left Sidebar -->
|
||||||
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full flex-shrink-0">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="h-14 flex items-center px-4 border-b border-gray-100 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-sm">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg text-gray-800 tracking-tight">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<button class="ml-auto p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors">
|
||||||
|
<i class="far fa-clone text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<nav class="flex-1 py-3 px-3 space-y-0.5 overflow-y-auto">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="px-1 mb-3">
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||||
|
<input type="text" placeholder="搜索任务"
|
||||||
|
class="w-full pl-9 pr-8 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all">
|
||||||
|
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded text-gray-400 transition-colors">
|
||||||
|
<i class="fas fa-sliders-h text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Task Button -->
|
||||||
|
<button class="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 transition-colors mb-1">
|
||||||
|
<i class="far fa-check-square text-emerald-600 w-5"></i>
|
||||||
|
<span class="font-medium">新建任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Nav Items -->
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-robot text-gray-400 w-5"></i>
|
||||||
|
<span>Claw</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-tie text-gray-400 w-5"></i>
|
||||||
|
<span>专家</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-wand-magic-sparkles text-gray-400 w-5"></i>
|
||||||
|
<span>技能</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item flex items-center gap-3 px-3 py-2 text-gray-600 rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-puzzle-piece text-gray-400 w-5"></i>
|
||||||
|
<span>插件</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="sidebar-item active flex items-center gap-3 px-3 py-2 text-gray-900 rounded-lg transition-colors">
|
||||||
|
<i class="far fa-clock text-gray-700 w-5"></i>
|
||||||
|
<span>自动化</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="mt-8 px-4 text-center">
|
||||||
|
<p class="text-gray-900 font-medium mb-1">暂无任务</p>
|
||||||
|
<p class="text-gray-400 text-sm">点击上方按钮开始新任务</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="p-3 border-t border-gray-200 flex-shrink-0">
|
||||||
|
<button class="flex items-center gap-3 w-full hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-cyan-500 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||||
|
<i class="fas fa-robot text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-left text-sm font-medium text-gray-700">iven</span>
|
||||||
|
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 flex flex-col h-full bg-white overflow-hidden">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white font-bold">
|
||||||
|
<i class="fas fa-code text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-lg">WorkBuddy</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 font-medium">Agents</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="fas fa-minus text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<i class="far fa-square text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button class="p-2 hover:bg-red-50 rounded-lg text-gray-500 hover:text-red-500">
|
||||||
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto bg-white">
|
||||||
|
<div class="max-w-6xl mx-auto px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex items-start justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">自动化</h1>
|
||||||
|
<span class="beta-tag">Beta</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-base">管理自动化任务并查看近期运行记录。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button class="btn-secondary flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" onclick="addAutomation()">
|
||||||
|
<i class="fas fa-plus text-xs"></i>
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
<button class="btn-primary flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" onclick="addFromTemplate()">
|
||||||
|
从模版添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Templates Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-medium text-gray-900 mb-4">从模版入手</h2>
|
||||||
|
|
||||||
|
<!-- Templates Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="templatesGrid">
|
||||||
|
|
||||||
|
<!-- Template 1: 每日 AI 新闻推送 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('每日 AI 新闻推送')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="far fa-newspaper text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">每日 AI 新闻推送</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">关注当天 AI 领域的重要动态,侧...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 2: 每日 5 个英语单词 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('每日 5 个英语单词')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<span class="text-gray-600 font-bold text-lg">文</span>
|
||||||
|
<span class="text-gray-400 text-xs ml-0.5">A</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">每日 5 个英语单词</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">每天推荐 5 个高频实用英语单词...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 3: 每日儿童睡前故事 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('每日儿童睡前故事')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="far fa-moon text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">每日儿童睡前故事</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">生成 3-5 分钟可读的温和睡前故事...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 4: 每周工作周报 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('每周工作周报')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-list-check text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">每周工作周报</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">每周五汇总仓库 PR 与 Issue 进展...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 5: 经典电影推荐 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('经典电影推荐')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fas fa-film text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">经典电影推荐</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">推荐一部高分经典电影,简要介绍...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 6: 历史上的今天 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('历史上的今天')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="far fa-calendar-alt text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">历史上的今天</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">从科技、电影、音乐等领域挑选一...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template 7: 每日一个为什么 -->
|
||||||
|
<div class="template-card bg-white rounded-xl p-4 flex items-start gap-4 cursor-pointer group" onclick="useTemplate('每日一个为什么')">
|
||||||
|
<div class="template-icon w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="far fa-lightbulb text-gray-600 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-sm mb-1 group-hover:text-gray-700">每日一个为什么</h3>
|
||||||
|
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2">每天抛出一个有趣问题,先提问再...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- My Automations Section (Empty State shown initially) -->
|
||||||
|
<div class="mt-12 hidden" id="myAutomations">
|
||||||
|
<h2 class="text-base font-medium text-gray-900 mb-4">我的自动化</h2>
|
||||||
|
<div class="bg-gray-50 rounded-xl p-8 text-center border border-dashed border-gray-300">
|
||||||
|
<p class="text-gray-500 text-sm">暂无自动化任务,从上方模版开始创建</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div id="toast" class="fixed bottom-8 right-8 bg-gray-900 text-white px-6 py-3 rounded-xl shadow-2xl transform translate-y-20 opacity-0 transition-all duration-300 flex items-center gap-3 z-50">
|
||||||
|
<i class="fas fa-check-circle text-emerald-400"></i>
|
||||||
|
<span id="toastMessage">操作成功</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Use template
|
||||||
|
function useTemplate(templateName) {
|
||||||
|
showToast(`已选择模版:${templateName}`);
|
||||||
|
|
||||||
|
// Show my automations section if hidden
|
||||||
|
document.getElementById('myAutomations').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add automation manually
|
||||||
|
function addAutomation() {
|
||||||
|
showToast('创建新的自动化任务');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add from template
|
||||||
|
function addFromTemplate() {
|
||||||
|
// Scroll to templates section smoothly
|
||||||
|
document.getElementById('templatesGrid').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
showToast('请从下方选择模版');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
function showToast(message) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastMessage = document.getElementById('toastMessage');
|
||||||
|
|
||||||
|
toastMessage.textContent = message;
|
||||||
|
toast.classList.remove('translate-y-20', 'opacity-0');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('translate-y-20', 'opacity-0');
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stagger animation on load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const cards = document.querySelectorAll('.template-card');
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(10px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.transition = 'all 0.3s ease';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
}, index * 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
> **分类**: 智能层
|
> **分类**: 智能层
|
||||||
> **优先级**: P0 - 决定性
|
> **优先级**: P0 - 决定性
|
||||||
> **成熟度**: L4 - 生产
|
> **成熟度**: L4 - 生产
|
||||||
> **最后更新**: 2026-03-16
|
> **最后更新**: 2026-03-18
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,8 +26,11 @@ Agent 记忆系统实现了跨会话的持久化记忆,支持 5 种记忆类
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 核心实现 | `desktop/src/lib/agent-memory.ts` | 记忆管理 |
|
| 核心实现 | `desktop/src/lib/agent-memory.ts` | 记忆管理 |
|
||||||
| 提取器 | `desktop/src/lib/memory-extractor.ts` | 会话记忆提取 |
|
| 提取器 | `desktop/src/lib/memory-extractor.ts` | 会话记忆提取 |
|
||||||
|
| LLM 服务 | `desktop/src/lib/llm-service.ts` | LLM 智能提取适配器 |
|
||||||
| 向量搜索 | `desktop/src/lib/vector-memory.ts` | 语义搜索 |
|
| 向量搜索 | `desktop/src/lib/vector-memory.ts` | 语义搜索 |
|
||||||
| UI 组件 | `desktop/src/components/MemoryPanel.tsx` | 记忆面板 |
|
| 图谱 Store | `desktop/src/store/memoryGraphStore.ts` | 记忆图谱状态 |
|
||||||
|
| UI 组件 | `desktop/src/components/MemoryPanel.tsx` | 记忆列表面板 |
|
||||||
|
| 图谱组件 | `desktop/src/components/MemoryGraph.tsx` | 记忆关系图谱 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -209,13 +212,19 @@ function prune(options: PruneOptions): number {
|
|||||||
|
|
||||||
- [x] 5 种记忆类型
|
- [x] 5 种记忆类型
|
||||||
- [x] 关键词提取
|
- [x] 关键词提取
|
||||||
|
- [x] **LLM 智能提取** (2026-03-18)
|
||||||
|
- 通过 OpenFang Gateway 调用 LLM 进行语义分析
|
||||||
|
- 自动识别事实、偏好、经验、任务等记忆类型
|
||||||
|
- 智能评估记忆重要性 (1-10)
|
||||||
|
- [x] 规则提取 (备用方案)
|
||||||
- [x] 相关性排序
|
- [x] 相关性排序
|
||||||
- [x] 重要性评分
|
- [x] 重要性评分
|
||||||
- [x] 访问追踪
|
- [x] 访问追踪
|
||||||
- [x] 去重机制
|
- [x] 去重机制
|
||||||
- [x] 清理功能
|
- [x] 清理功能
|
||||||
- [x] Markdown 导出
|
- [x] Markdown 导出
|
||||||
- [x] UI 面板
|
- [x] UI 面板 (MemoryPanel)
|
||||||
|
- [x] **记忆图谱可视化** (MemoryGraph)
|
||||||
|
|
||||||
### 5.2 测试覆盖
|
### 5.2 测试覆盖
|
||||||
|
|
||||||
@@ -230,6 +239,15 @@ function prune(options: PruneOptions): number {
|
|||||||
| 大量记忆时检索变慢 | 中 | 待处理 | Q2 |
|
| 大量记忆时检索变慢 | 中 | 待处理 | Q2 |
|
||||||
| 向量搜索需要 OpenViking | 低 | 可选 | - |
|
| 向量搜索需要 OpenViking | 低 | 可选 | - |
|
||||||
|
|
||||||
|
### 5.4 历史问题修复
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方案 | 日期 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| Memory tab 为空 | `useLLM: false` 默认禁用 LLM 提取 | 启用 LLM 提取,降低阈值到 2 条消息 | 2026-03-18 |
|
||||||
|
| Graph UI 与系统不一致 | 使用深色主题 `#1a1a2e` | 统一为浅色/暗色双主题支持 | 2026-03-18 |
|
||||||
|
| agentId 不一致 | `MemoryPanel` 用 `'zclaw-main'`,`MemoryGraph` 用 `'default'` | 统一 fallback 为 `'zclaw-main'` | 2026-03-18 |
|
||||||
|
| GatewayLLMAdapter 端点不存在 | 调用不存在的 `/api/llm/complete` | 改用 `/api/agents/{id}/message` | 2026-03-18 |
|
||||||
|
|
||||||
### 5.4 用户反馈
|
### 5.4 用户反馈
|
||||||
|
|
||||||
记忆系统有效减少了重复说明,希望提高自动提取的准确性。
|
记忆系统有效减少了重复说明,希望提高自动提取的准确性。
|
||||||
|
|||||||
@@ -3,18 +3,28 @@
|
|||||||
> **分类**: 智能层
|
> **分类**: 智能层
|
||||||
> **优先级**: P1 - 重要
|
> **优先级**: P1 - 重要
|
||||||
> **成熟度**: L4 - 生产
|
> **成熟度**: L4 - 生产
|
||||||
> **最后更新**: 2026-03-17
|
> **最后更新**: 2026-03-18
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ UI 集成状态
|
## ✅ 集成状态
|
||||||
|
|
||||||
> **当前状态**: ✅ 已集成
|
> **当前状态**: ✅ 完全集成
|
||||||
>
|
>
|
||||||
> `AutonomyConfig.tsx` 组件已集成到 `RightPanel.tsx` 的 'autonomy' tab。
|
> `AutonomyConfig.tsx` 组件已集成到 `RightPanel.tsx` 的 'autonomy' tab。
|
||||||
>
|
>
|
||||||
> **集成位置**: RightPanel 'autonomy' tab (点击 Shield 图标)
|
> **集成位置**: RightPanel 'autonomy' tab (点击 Shield 图标)
|
||||||
|
|
||||||
|
### 已集成的系统
|
||||||
|
|
||||||
|
| 系统 | 文件 | 集成状态 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| 反思引擎 | `reflection-engine.ts` | ✅ 已集成 | 反思运行前检查授权 |
|
||||||
|
| 上下文压缩 | `context-compactor.ts` | ✅ 已集成 | 压缩执行前检查授权 |
|
||||||
|
| 身份更新 | `agent-identity.ts` | ✅ 已集成 | 身份变更前检查授权 |
|
||||||
|
| 技能安装 | `skill-discovery.ts` | ✅ 已集成 | 技能安装/卸载前检查授权 |
|
||||||
|
| 记忆提取 | `session-persistence.ts` | ✅ 已集成 | 记忆保存前检查授权 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、功能概述
|
## 一、功能概述
|
||||||
@@ -27,7 +37,7 @@
|
|||||||
|------|-----|
|
|------|-----|
|
||||||
| 分类 | 智能层 |
|
| 分类 | 智能层 |
|
||||||
| 优先级 | P1 |
|
| 优先级 | P1 |
|
||||||
| 成熟度 | L2 (降级:UI 未集成) |
|
| 成熟度 | L4 |
|
||||||
| 依赖 | AuditLog, ApprovalWorkflow |
|
| 依赖 | AuditLog, ApprovalWorkflow |
|
||||||
|
|
||||||
### 1.2 相关文件
|
### 1.2 相关文件
|
||||||
@@ -35,8 +45,8 @@
|
|||||||
| 文件 | 路径 | 用途 | 集成状态 |
|
| 文件 | 路径 | 用途 | 集成状态 |
|
||||||
|------|------|------|---------|
|
|------|------|------|---------|
|
||||||
| 核心实现 | `desktop/src/lib/autonomy-manager.ts` | 授权逻辑 | ✅ 存在 |
|
| 核心实现 | `desktop/src/lib/autonomy-manager.ts` | 授权逻辑 | ✅ 存在 |
|
||||||
| 配置 UI | `desktop/src/components/AutonomyConfig.tsx` | 配置界面 | ❌ **未集成** |
|
| 配置 UI | `desktop/src/components/AutonomyConfig.tsx` | 配置界面 | ✅ 已集成 |
|
||||||
| 审批 UI | `desktop/src/components/ApprovalsPanel.tsx` | 审批界面 | ❌ **未集成** |
|
| 审批 UI | `desktop/src/components/ApprovalsPanel.tsx` | 审批界面 | ✅ 已集成 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,8 +86,8 @@
|
|||||||
|
|
||||||
| 等级 | 操作类型 | Supervised | Assisted | Autonomous |
|
| 等级 | 操作类型 | Supervised | Assisted | Autonomous |
|
||||||
|------|---------|------------|----------|------------|
|
|------|---------|------------|----------|------------|
|
||||||
| Low | memory_save, reflection_run | 需确认 | 自动 | 自动 |
|
| Low | memory_save, reflection_run, compaction_run | 需确认 | 自动 | 自动 |
|
||||||
| Medium | hand_trigger, config_change | 需确认 | 需确认 | 自动 |
|
| Medium | hand_trigger, config_change, skill_install | 需确认 | 需确认 | 自动 |
|
||||||
| High | memory_delete, identity_update | 需确认 | 需确认 | 需确认 |
|
| High | memory_delete, identity_update | 需确认 | 需确认 | 需确认 |
|
||||||
|
|
||||||
### 2.5 设计约束
|
### 2.5 设计约束
|
||||||
@@ -112,6 +122,29 @@ interface AutonomyManager {
|
|||||||
创建审批请求 → 用户批准/拒绝
|
创建审批请求 → 用户批准/拒绝
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3.3 集成模式
|
||||||
|
|
||||||
|
各系统集成自主检查的统一模式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { canAutoExecute } from './autonomy-manager';
|
||||||
|
|
||||||
|
async function someOperation(options?: { skipAutonomyCheck?: boolean }) {
|
||||||
|
// 自主检查
|
||||||
|
if (!options?.skipAutonomyCheck) {
|
||||||
|
const { canProceed, decision } = canAutoExecute('action_type', importance);
|
||||||
|
if (!canProceed) {
|
||||||
|
console.log(`[Module] Autonomy check failed: ${decision.reason}`);
|
||||||
|
return; // 或返回默认结果
|
||||||
|
}
|
||||||
|
console.log(`[Module] Autonomy check passed: ${decision.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行实际操作
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、实际效果
|
## 四、实际效果
|
||||||
@@ -125,6 +158,10 @@ interface AutonomyManager {
|
|||||||
- [x] 操作回滚 (lib)
|
- [x] 操作回滚 (lib)
|
||||||
- [x] 审批过期 (lib)
|
- [x] 审批过期 (lib)
|
||||||
- [x] **UI 审批面板** - ✅ 已集成到 RightPanel 'autonomy' tab
|
- [x] **UI 审批面板** - ✅ 已集成到 RightPanel 'autonomy' tab
|
||||||
|
- [x] **反思引擎集成** - ✅ 2026-03-18
|
||||||
|
- [x] **上下文压缩集成** - ✅ 2026-03-18
|
||||||
|
- [x] **身份更新集成** - ✅ 2026-03-18
|
||||||
|
- [x] **技能安装集成** - ✅ 2026-03-18
|
||||||
|
|
||||||
### 4.2 已知问题
|
### 4.2 已知问题
|
||||||
|
|
||||||
@@ -138,6 +175,10 @@ interface AutonomyManager {
|
|||||||
## 五、演化路线
|
## 五、演化路线
|
||||||
|
|
||||||
### 5.1 短期计划(1-2 周)
|
### 5.1 短期计划(1-2 周)
|
||||||
|
- [x] 集成反思引擎
|
||||||
|
- [x] 集成上下文压缩
|
||||||
|
- [x] 集成身份更新
|
||||||
|
- [x] 集成技能安装
|
||||||
- [ ] 优化审批 UI
|
- [ ] 优化审批 UI
|
||||||
- [ ] 添加批量审批
|
- [ ] 添加批量审批
|
||||||
|
|
||||||
|
|||||||
@@ -189,19 +189,20 @@
|
|||||||
| ~~ActiveLearningPanel~~ | ~~`ActiveLearningPanel.tsx`~~ | ~~主动学习面板~~ | ✅ 已集成 |
|
| ~~ActiveLearningPanel~~ | ~~`ActiveLearningPanel.tsx`~~ | ~~主动学习面板~~ | ✅ 已集成 |
|
||||||
| ~~SkillMarket~~ | ~~`SkillMarket.tsx`~~ | ~~技能市场~~ | ✅ 已集成 |
|
| ~~SkillMarket~~ | ~~`SkillMarket.tsx`~~ | ~~技能市场~~ | ✅ 已集成 |
|
||||||
| ~~SkillCard~~ | ~~`SkillMarket/SkillCard.tsx`~~ | ~~技能卡片~~ | ✅ 已集成 |
|
| ~~SkillCard~~ | ~~`SkillMarket/SkillCard.tsx`~~ | ~~技能卡片~~ | ✅ 已集成 |
|
||||||
| MemoryGraph | `MemoryGraph.tsx` | 记忆图谱可视化 | 中 |
|
| ~~MemoryGraph~~ | ~~`MemoryGraph.tsx`~~ | ~~记忆图谱可视化~~ | ✅ 已集成到 RightPanel memory tab |
|
||||||
| ~~AuditLogsPanel~~ | ~~`AuditLogsPanel.tsx`~~ | ~~审计日志面板~~ | ✅ 已集成 |
|
| ~~AuditLogsPanel~~ | ~~`AuditLogsPanel.tsx`~~ | ~~审计日志面板~~ | ✅ 已集成 |
|
||||||
| ~~SecurityLayersPanel~~ | ~~`SecurityLayersPanel.tsx`~~ | ~~安全层面板~~ | ✅ 已集成 |
|
| ~~SecurityLayersPanel~~ | ~~`SecurityLayersPanel.tsx`~~ | ~~安全层面板~~ | ✅ 已集成 |
|
||||||
| TriggersPanel | `TriggersPanel.tsx` | 触发器管理 | 低 |
|
| ~~TriggersPanel~~ | ~~`TriggersPanel.tsx`~~ | ~~触发器管理~~ | ✅ 已集成到 SchedulerPanel triggers tab |
|
||||||
| ApprovalsPanel | `ApprovalsPanel.tsx` | 审批管理面板 | 低 |
|
| ~~ApprovalsPanel~~ | ~~`ApprovalsPanel.tsx`~~ | ~~审批管理面板~~ | ✅ 已集成到 HandsPanel approvals tab |
|
||||||
| TeamOrchestrator | `TeamOrchestrator.tsx` | 团队编排器 | 低 |
|
| ~~TeamOrchestrator~~ | ~~`TeamOrchestrator.tsx`~~ | ~~团队编排器~~ | ✅ 已集成到 App team view orchestrator tab |
|
||||||
| ~~SecurityStatus~~ | ~~`SecurityStatus.tsx`~~ | ~~安全状态显示~~ | ✅ 已集成 |
|
| ~~SecurityStatus~~ | ~~`SecurityStatus.tsx`~~ | ~~安全状态显示~~ | ✅ 已集成 |
|
||||||
| HeartbeatConfig | `HeartbeatConfig.tsx` | 心跳配置 | 低 |
|
| HeartbeatConfig | `HeartbeatConfig.tsx` | 心跳配置 | 低 |
|
||||||
| CreateTriggerModal | `CreateTriggerModal.tsx` | 创建触发器弹窗 | 低 |
|
| CreateTriggerModal | `CreateTriggerModal.tsx` | 创建触发器弹窗 | 低 |
|
||||||
| FeedbackButton | `Feedback/FeedbackButton.tsx` | 反馈按钮 | 低 |
|
| ~~FeedbackButton~~ | ~~`Feedback/FeedbackButton.tsx`~~ | ~~反馈按钮~~ | ✅ 已集成到 SettingsLayout |
|
||||||
| FeedbackHistory | `Feedback/FeedbackHistory.tsx` | 反馈历史 | 低 |
|
| ~~FeedbackHistory~~ | ~~`Feedback/FeedbackHistory.tsx`~~ | ~~反馈历史~~ | ✅ 已集成到 SettingsLayout |
|
||||||
| FeedbackModal | `Feedback/FeedbackModal.tsx` | 反馈弹窗 | 低 |
|
| ~~FeedbackModal~~ | ~~`Feedback/FeedbackModal.tsx`~~ | ~~反馈弹窗~~ | ✅ 已集成到 SettingsLayout |
|
||||||
| MessageSearch | `MessageSearch.tsx` | 消息搜索 | 中 |
|
| ~~MessageSearch~~ | ~~`MessageSearch.tsx`~~ | ~~消息搜索~~ | ✅ Sidebar 已有搜索功能 |
|
||||||
|
| ~~MemoryGraph~~ | ~~`MemoryGraph.tsx`~~ | ~~记忆图谱可视化~~ | ✅ 已集成到 RightPanel memory tab |
|
||||||
| PersonalitySelector | `PersonalitySelector.tsx` | 个性选择器 | 低 |
|
| PersonalitySelector | `PersonalitySelector.tsx` | 个性选择器 | 低 |
|
||||||
| ScenarioTags | `ScenarioTags.tsx` | 场景标签 | 低 |
|
| ScenarioTags | `ScenarioTags.tsx` | 场景标签 | 低 |
|
||||||
| BrowserHand/* | `BrowserHand/*.tsx` | Browser Hand 组件 | ✅ 已被 HandsPanel 使用 |
|
| BrowserHand/* | `BrowserHand/*.tsx` | Browser Hand 组件 | ✅ 已被 HandsPanel 使用 |
|
||||||
@@ -254,12 +255,12 @@
|
|||||||
- ~~`HandParamsForm.tsx` → 集成到 HandsPanel 触发流程~~ - ✅ 已集成
|
- ~~`HandParamsForm.tsx` → 集成到 HandsPanel 触发流程~~ - ✅ 已集成
|
||||||
- ~~`HandApprovalModal.tsx` → 集成到 App.tsx 全局监听~~ - ✅ 已集成
|
- ~~`HandApprovalModal.tsx` → 集成到 App.tsx 全局监听~~ - ✅ 已集成
|
||||||
|
|
||||||
### P2 - 增强 (锦上添花)
|
### P2 - 增强 (锦上添花) - ✅ MemoryGraph、TriggersPanel 已完成
|
||||||
|
|
||||||
5. **集成其他组件**
|
5. **集成其他组件**
|
||||||
- `TriggersPanel.tsx` → 添加到 workflow 视图
|
- ~~`TriggersPanel.tsx` → 添加到 workflow 视图~~ - ✅ 已集成到 SchedulerPanel (2026-03-18)
|
||||||
- `ApprovalsPanel.tsx` → 添加到 hands 视图
|
- `ApprovalsPanel.tsx` → 添加到 hands 视图
|
||||||
- `MemoryGraph.tsx` → 添加到 memory tab
|
- ~~`MemoryGraph.tsx` → 添加到 memory tab~~ - ✅ 已集成 (2026-03-18)
|
||||||
- `MessageSearch.tsx` → 添加到 ChatArea
|
- `MessageSearch.tsx` → 添加到 ChatArea
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -273,7 +274,6 @@
|
|||||||
| `02-intelligence-layer/03-reflection-engine.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
| `02-intelligence-layer/03-reflection-engine.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
||||||
| `02-intelligence-layer/05-autonomy-manager.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
| `02-intelligence-layer/05-autonomy-manager.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
||||||
| `04-skills-ecosystem/00-skill-system.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
| `04-skills-ecosystem/00-skill-system.md` | L4 已完成 | ✅ 已更新为 L4 已集成 |
|
||||||
| `01-core-features/03-workflow-engine.md` | L3 成熟 | 添加说明:Editor/History 未集成 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -297,7 +297,11 @@
|
|||||||
4. **Hands 参数和审批已集成** - ✅ HandParamsForm 和 HandApprovalModal 已集成 (2026-03-17)
|
4. **Hands 参数和审批已集成** - ✅ HandParamsForm 和 HandApprovalModal 已集成 (2026-03-17)
|
||||||
5. **Workflow 编辑器已集成** - ✅ WorkflowEditor 和 WorkflowHistory 已集成到 SchedulerPanel (2026-03-17)
|
5. **Workflow 编辑器已集成** - ✅ WorkflowEditor 和 WorkflowHistory 已集成到 SchedulerPanel (2026-03-17)
|
||||||
6. **安全与审计已集成** - ✅ AuditLogsPanel 和 SecurityLayersPanel 已集成到 SettingsLayout (2026-03-17)
|
6. **安全与审计已集成** - ✅ AuditLogsPanel 和 SecurityLayersPanel 已集成到 SettingsLayout (2026-03-17)
|
||||||
7. **部分"僵尸组件"** - ~2 组件存在但未渲染 (MemoryGraph, TriggersPanel)
|
7. **记忆图谱已集成** - ✅ MemoryGraph 已集成到 RightPanel memory tab (2026-03-18)
|
||||||
|
8. **触发器面板已集成** - ✅ TriggersPanel 已集成到 SchedulerPanel triggers tab (2026-03-18)
|
||||||
|
9. **审批面板已集成** - ✅ ApprovalsPanel 已集成到 HandsPanel approvals tab (2026-03-18)
|
||||||
|
10. **团队编排器已集成** - ✅ TeamOrchestrator 已集成到 App team view orchestrator tab (2026-03-18)
|
||||||
|
11. **所有组件已集成** - ✅ 无剩余"僵尸组件"
|
||||||
|
|
||||||
### 建议行动
|
### 建议行动
|
||||||
|
|
||||||
@@ -305,5 +309,8 @@
|
|||||||
2. ~~**短期**: 集成 SkillMarket 和 ActiveLearningPanel~~ - ✅ 已完成 (2026-03-17)
|
2. ~~**短期**: 集成 SkillMarket 和 ActiveLearningPanel~~ - ✅ 已完成 (2026-03-17)
|
||||||
3. ~~**短期**: 集成 HandParamsForm 和 HandApprovalModal~~ - ✅ 已完成 (2026-03-17)
|
3. ~~**短期**: 集成 HandParamsForm 和 HandApprovalModal~~ - ✅ 已完成 (2026-03-17)
|
||||||
4. ~~**中期**: 集成 Workflow Editor/History 和 安全/审计组件~~ - ✅ 已完成 (2026-03-17)
|
4. ~~**中期**: 集成 Workflow Editor/History 和 安全/审计组件~~ - ✅ 已完成 (2026-03-17)
|
||||||
5. **长期**: 清理剩余"僵尸组件" (MemoryGraph, TriggersPanel)
|
5. ~~**中期**: 集成 MemoryGraph~~ - ✅ 已完成 (2026-03-18)
|
||||||
6. **长期**: 建立文档-代码同步机制,避免文档过时
|
6. ~~**中期**: 集成 TriggersPanel~~ - ✅ 已完成 (2026-03-18)
|
||||||
|
7. ~~**中期**: 集成 ApprovalsPanel~~ - ✅ 已完成 (2026-03-18)
|
||||||
|
8. ~~**中期**: 集成 TeamOrchestrator~~ - ✅ 已完成 (2026-03-18)
|
||||||
|
9. **长期**: 建立文档-代码同步机制,避免文档过时
|
||||||
|
|||||||
@@ -546,6 +546,187 @@ if (clones && clones.length > 0) {
|
|||||||
2. 重新启动应用
|
2. 重新启动应用
|
||||||
3. 完成引导流程 → 应该成功更新默认 Agent
|
3. 完成引导流程 → 应该成功更新默认 Agent
|
||||||
|
|
||||||
|
### 3.4 刷新页面后对话内容丢失
|
||||||
|
|
||||||
|
**症状**: 切换 Tab 时对话正常保留,但按 F5 刷新页面后对话内容消失
|
||||||
|
|
||||||
|
**根本原因**: `onComplete` 回调中没有将当前对话保存到 `conversations` 数组,导致 `persist` 中间件无法持久化
|
||||||
|
|
||||||
|
**问题分析**:
|
||||||
|
|
||||||
|
Zustand 的 `persist` 中间件只保存 `partialize` 中指定的字段:
|
||||||
|
```typescript
|
||||||
|
partialize: (state) => ({
|
||||||
|
conversations: state.conversations, // ← 从这里恢复
|
||||||
|
currentModel: state.currentModel,
|
||||||
|
currentAgentId: state.currentAgent?.id,
|
||||||
|
currentConversationId: state.currentConversationId,
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
但 `messages` 数组只在内存中,刷新后丢失。恢复逻辑依赖 `conversations` 数组:
|
||||||
|
```typescript
|
||||||
|
onRehydrateStorage: () => (state) => {
|
||||||
|
if (state?.currentConversationId && state.conversations) {
|
||||||
|
const currentConv = state.conversations.find(c => c.id === state.currentConversationId);
|
||||||
|
if (currentConv) {
|
||||||
|
state.messages = [...currentConv.messages]; // ← 从 conversations 恢复
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**: `onComplete` 回调中只更新了 `messages`,没有调用 `upsertActiveConversation` 保存到 `conversations`
|
||||||
|
|
||||||
|
**修复代码**:
|
||||||
|
```typescript
|
||||||
|
onComplete: () => {
|
||||||
|
const state = get();
|
||||||
|
|
||||||
|
// Save conversation to persist across refresh
|
||||||
|
const conversations = upsertActiveConversation([...state.conversations], state);
|
||||||
|
const currentConvId = state.currentConversationId || conversations[0]?.id;
|
||||||
|
|
||||||
|
set({
|
||||||
|
isStreaming: false,
|
||||||
|
conversations, // ← 保存到 conversations 数组
|
||||||
|
currentConversationId: currentConvId, // ← 确保 ID 被设置
|
||||||
|
messages: state.messages.map((m) =>
|
||||||
|
m.id === assistantId ? { ...m, streaming: false, runId } : m
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ... rest of the callback
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**文件**: `desktop/src/store/chatStore.ts`
|
||||||
|
|
||||||
|
**验证修复**:
|
||||||
|
1. 发送消息并获得 AI 回复
|
||||||
|
2. 按 F5 刷新页面
|
||||||
|
3. 对话内容应该保留
|
||||||
|
|
||||||
|
### 3.5 ChatArea 输入框布局错乱
|
||||||
|
|
||||||
|
**症状**: 对话过程中输入框被移动到页面顶部,而不是固定在底部
|
||||||
|
|
||||||
|
**根本原因**: `ChatArea` 组件返回的是 React Fragment (`<>...</>`),没有包裹在 flex 容器中
|
||||||
|
|
||||||
|
**问题代码**:
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误 - Fragment 没有 flex 布局
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="h-14 ...">Header</div>
|
||||||
|
<div className="flex-1 ...">Messages</div>
|
||||||
|
<div className="border-t ...">Input</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复代码**:
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确 - 使用 flex 容器
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="h-14 flex-shrink-0 ...">Header</div>
|
||||||
|
<div className="flex-1 overflow-y-auto ...">Messages</div>
|
||||||
|
<div className="flex-shrink-0 ...">Input</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**文件**: `desktop/src/components/ChatArea.tsx`
|
||||||
|
|
||||||
|
**布局原理**:
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Header (h-14, flex-shrink-0) │ ← 固定高度
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Messages (flex-1, overflow-y-auto) │ ← 占据剩余空间,可滚动
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ Input (flex-shrink-0) │ ← 固定在底部
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 记忆系统问题
|
||||||
|
|
||||||
|
### 7.1 Memory Tab 为空,多轮对话后无记忆
|
||||||
|
|
||||||
|
**症状**: 经过多次对话后,右侧面板的"记忆"Tab 内容为空
|
||||||
|
|
||||||
|
**根本原因**: 多个配置问题导致记忆未被提取
|
||||||
|
|
||||||
|
**问题分析**:
|
||||||
|
|
||||||
|
1. **LLM 提取默认禁用**: `useLLM: false` 导致只使用规则提取
|
||||||
|
2. **提取阈值过高**: `minMessagesForExtraction: 4` 短对话不会触发
|
||||||
|
3. **agentId 不一致**: `MemoryPanel` 用 `'zclaw-main'`,`MemoryGraph` 用 `'default'`
|
||||||
|
4. **Gateway 端点不存在**: `GatewayLLMAdapter` 调用 `/api/llm/complete`,OpenFang 无此端点
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 启用 LLM 提取 (memory-extractor.ts)
|
||||||
|
export const DEFAULT_EXTRACTION_CONFIG: ExtractionConfig = {
|
||||||
|
useLLM: true, // 从 false 改为 true
|
||||||
|
minMessagesForExtraction: 2, // 从 4 降低到 2
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 统一 agentId fallback (MemoryGraph.tsx)
|
||||||
|
const agentId = currentAgent?.id || 'zclaw-main'; // 从 'default' 改为 'zclaw-main'
|
||||||
|
|
||||||
|
// 3. 修复 Gateway 适配器端点 (llm-service.ts)
|
||||||
|
// 使用 OpenFang 的 /api/agents/{id}/message 端点
|
||||||
|
const response = await fetch(`/api/agents/${agentId}/message`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ message: fullPrompt, ... }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**文件**:
|
||||||
|
- `desktop/src/lib/memory-extractor.ts`
|
||||||
|
- `desktop/src/lib/llm-service.ts`
|
||||||
|
- `desktop/src/components/MemoryGraph.tsx`
|
||||||
|
|
||||||
|
**验证修复**:
|
||||||
|
1. 打开浏览器控制台
|
||||||
|
2. 进行至少 2 轮对话
|
||||||
|
3. 查看日志: `[MemoryExtractor] Using LLM-powered semantic extraction`
|
||||||
|
4. 检查 Memory Tab 是否显示提取的记忆
|
||||||
|
|
||||||
|
### 7.2 Memory Graph UI 与系统风格不一致
|
||||||
|
|
||||||
|
**症状**: 记忆图谱使用深色主题,与系统浅色主题不协调
|
||||||
|
|
||||||
|
**根本原因**: `MemoryGraph.tsx` 硬编码深色背景 `#1a1a2e`
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Canvas 背景 - 支持亮/暗双主题
|
||||||
|
ctx.fillStyle = '#f9fafb'; // gray-50 (浅色)
|
||||||
|
|
||||||
|
// 工具栏 - 添加 dark: 变体
|
||||||
|
<div className="bg-gray-100 dark:bg-gray-800/50">
|
||||||
|
|
||||||
|
// 图谱画布 - 添加 dark: 变体
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900">
|
||||||
|
```
|
||||||
|
|
||||||
|
**文件**: `desktop/src/components/MemoryGraph.tsx`
|
||||||
|
|
||||||
|
**设计规范**:
|
||||||
|
- 使用 Tailwind 的 `dark:` 变体支持双主题
|
||||||
|
- 强调色使用 `orange-500` 而非 `blue-600`
|
||||||
|
- 文字颜色使用 `gray-700 dark:text-gray-300`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 相关文档
|
## 8. 相关文档
|
||||||
@@ -560,6 +741,8 @@ if (clones && clones.length > 0) {
|
|||||||
|
|
||||||
| 日期 | 变更 |
|
| 日期 | 变更 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-03-18 | 添加记忆提取和图谱 UI 问题 |
|
||||||
|
| 2026-03-18 | 添加刷新后对话丢失问题和 ChatArea 布局问题 |
|
||||||
| 2026-03-17 | 添加首次使用引导流程 |
|
| 2026-03-17 | 添加首次使用引导流程 |
|
||||||
| 2026-03-17 | 添加配置热重载限制问题 |
|
| 2026-03-17 | 添加配置热重载限制问题 |
|
||||||
| 2026-03-14 | 初始版本 |
|
| 2026-03-14 | 初始版本 |
|
||||||
|
|||||||
@@ -1,202 +1,246 @@
|
|||||||
# ZClaw 桌面应用布局优化计划
|
# ZClaw 桌面应用布局优化计划
|
||||||
|
|
||||||
## 一、问题分析
|
## 一、设计参考
|
||||||
|
|
||||||
基于截图和代码分析,当前三栏布局存在以下问题:
|
基于腾讯 WorkBuddy 的 HTML 设计稿,学习其布局风格并适配 ZClaw 的功能需求。
|
||||||
|
|
||||||
| 问题 | 影响 |
|
### WorkBuddy 布局结构
|
||||||
|------|------|
|
|
||||||
| Sidebar 固定 256px | 占用过多水平空间 |
|
|
||||||
| RightPanel 固定 320px | 信息密度过高,7 个 Tab 挤在一起 |
|
|
||||||
| 主内容区被挤压 | 聊天(核心功能)空间不足 |
|
|
||||||
| 无折叠机制 | 用户无法按需调整空间 |
|
|
||||||
| 无响应式 | 小屏幕上不可用 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、布局方案对比
|
|
||||||
|
|
||||||
### 方案 A: 顶部导航 + 左右双栏 ⭐ 推荐
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
│ [Logo] ZCLAW Chat | Agents | Hands | Workflow | ⚙ │
|
│ 顶部工具栏 (h-14 = 56px) │
|
||||||
│ ← 顶部 Tab 导航 │
|
├────────────────┬───────────────────────────────────────────────┤
|
||||||
├────────────────┬─────────────────────────────────────────────────┤
|
|
||||||
│ │ │
|
│ │ │
|
||||||
│ 左侧列表区 │ 主内容区 │
|
│ 左侧导航栏 │ 主内容区 │
|
||||||
│ (可折叠) │ (聊天/详情) │
|
│ w-64 (256px) │ flex-1 │
|
||||||
│ │ │
|
│ │ (占据全部剩余空间) │
|
||||||
│ ┌──────────┐ │ ┌─────────────────────────────────────────┐ │
|
│ ┌──────────┐ │ │
|
||||||
│ │ Agent 1 │ │ │ │ │
|
│ │ 搜索框 │ │ - 聊天界面 (居中卡片) │
|
||||||
│ │ Agent 2 │ │ │ 聊天消息 / 详情内容 │ │
|
│ ├──────────┤ │ - 专家中心 (网格卡片) │
|
||||||
│ │ Hand 1 │ │ │ │ │
|
│ │ 新建任务 │ │ - 技能市场 (网格卡片) │
|
||||||
│ │ ... │ │ │ │ │
|
│ ├──────────┤ │ - 自动化 (模板卡片) │
|
||||||
│ └──────────┘ │ └─────────────────────────────────────────┘ │
|
│ │ Claw │ │ │
|
||||||
│ │ │
|
│ │ 专家 │ │ │
|
||||||
│ ┌──────────┐ │ ┌─────────────────────────────────────────┐ │
|
│ │ 技能 │ │ │
|
||||||
│ │ 用户头像 │ │ │ [输入框] [📋 详情] │ │
|
│ │ 插件 │ │ │
|
||||||
│ └──────────┘ │ └─────────────────────────────────────────┘ │
|
│ │ 自动化 │ │ │
|
||||||
└────────────────┴─────────────────────────────────────────────────┘
|
│ ├──────────┤ │ │
|
||||||
240px flex-1 (最大化)
|
│ │ 空状态 │ │ │
|
||||||
|
│ └──────────┘ │ │
|
||||||
|
│ ┌──────────┐ │ │
|
||||||
|
│ │ 用户头像 │ │ │
|
||||||
|
│ └──────────┘ │ │
|
||||||
|
└────────────────┴───────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**特点**:
|
### 关键设计特点
|
||||||
- ✅ 主内容区域最大化
|
|
||||||
- ✅ 顶部导航清晰,一眼看到所有视图
|
|
||||||
- ✅ 左侧列表可折叠 (240px ↔ 0px)
|
|
||||||
- ✅ 详情面板通过按钮触发(抽屉滑出),不常驻
|
|
||||||
|
|
||||||
**空间分配**: `240px + flex-1` = 主内容占比 ~80%
|
1. **无右侧面板** - 所有功能通过左侧导航切换,在主内容区显示
|
||||||
|
2. **紧凑导航栏** - 256px 固定宽度,包含搜索和所有导航项
|
||||||
|
3. **最大化主内容区** - 占据全部剩余空间
|
||||||
|
4. **卡片式布局** - 主内容区使用卡片网格展示内容
|
||||||
|
5. **绿色强调色** - emerald-500 作为主强调色
|
||||||
|
6. **Inter 字体** - 现代简洁的字体
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 方案 B: 窄导航 + 列表 + 主内容 (类 VSCode)
|
## 二、ZClaw 功能映射
|
||||||
|
|
||||||
```
|
### 导航项映射
|
||||||
┌────┬─────────────┬───────────────────────────────────────────────┐
|
|
||||||
│ 🔘 │ │ │
|
|
||||||
│ 💬 │ 列表区 │ 主内容区 │
|
|
||||||
│ 🤖 │ (Agent/ │ (聊天/详情) │
|
|
||||||
│ 🖐️ │ Hands/ │ │
|
|
||||||
│ ⚡ │ etc) │ │
|
|
||||||
│ 👥 │ │ │
|
|
||||||
│ ⚙️ │ │ │
|
|
||||||
└────┴─────────────┴───────────────────────────────────────────────┘
|
|
||||||
64px 200px flex-1
|
|
||||||
```
|
|
||||||
|
|
||||||
**特点**:
|
| WorkBuddy | ZClaw | 功能说明 |
|
||||||
- ✅ 导航极简,64px 窄条
|
|-----------|-------|---------|
|
||||||
- ✅ 类似 VSCode/Notion 布局,用户熟悉
|
| Claw | 聊天 (Chat) | AI 对话,核心功能 |
|
||||||
- ⚠️ 仍是三栏,但比例更合理
|
| 专家 | 分身 (Agents) | Agent 列表和管理 |
|
||||||
|
| 技能 | 技能 (Skills) | 技能市场 |
|
||||||
|
| 插件 | Hands | 7 个自主能力包 |
|
||||||
|
| 自动化 | 工作流 (Workflow) | 工作流和调度 |
|
||||||
|
|
||||||
**空间分配**: `64px + 200px + flex-1` = 主内容占比 ~70%
|
### 额外功能处理
|
||||||
|
|
||||||
|
ZClaw 有额外的功能需要处理:
|
||||||
|
- **团队协作** → 添加到导航项
|
||||||
|
- **Swarm 协作** → 添加到导航项
|
||||||
|
- **详情面板** (RightPanel) → **改为抽屉或合并到主内容区**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 方案 C: 左侧导航+列表 + 主内容 + 右侧详情(可折叠)
|
## 三、新布局方案
|
||||||
|
|
||||||
|
### 方案:WorkBuddy 风格 + 详情抽屉
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────┬──────────────────────────────┬─────────────┐
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
│ [Tab: 分身|Hands|...]│ │ │
|
│ [Z] ZCLAW [📋 详情] [⚙ 设置] │
|
||||||
├──────────────────────┤ 主内容区 │ 详情面板 │
|
├────────────────┬───────────────────────────────────────────────┤
|
||||||
│ │ (聊天/详情) │ (可折叠) │
|
│ 🔍 搜索... │ │
|
||||||
│ 列表区 │ │ │
|
├────────────────┤ │
|
||||||
│ (根据 Tab 切换) │ │ - Status │
|
│ ✨ 新对话 │ │
|
||||||
│ │ │ - Memory │
|
├────────────────┤ │
|
||||||
│ │ │ - Agent │
|
│ 💬 聊天 │ 主内容区 │
|
||||||
└──────────────────────┴──────────────────────────────┴─────────────┘
|
│ 🤖 分身 ← │ (flex-1) │
|
||||||
240px flex-1 280px (可隐藏)
|
│ 🖐️ Hands │ │
|
||||||
|
│ ⚡ 工作流 │ 根据导航显示对应内容: │
|
||||||
|
│ 📦 技能 │ - 聊天界面 │
|
||||||
|
│ 👥 团队 │ - Agent 列表 + 详情 │
|
||||||
|
│ 🐝 Swarm │ - Hands 列表 + 详情 │
|
||||||
|
├────────────────┤ - 工作流编辑器 │
|
||||||
|
│ 暂无任务 │ - 技能市场 │
|
||||||
|
├────────────────┤ - 团队列表 │
|
||||||
|
│ 👤 用户 │ - Swarm 仪表盘 │
|
||||||
|
└────────────────┴───────────────────────────────────────────────┘
|
||||||
|
↑ 点击 [📋 详情] 可从右侧滑出抽屉
|
||||||
```
|
```
|
||||||
|
|
||||||
**特点**:
|
### 布局尺寸
|
||||||
- ✅ 改动最小,基于现有结构
|
|
||||||
- ✅ 详情面板可折叠
|
|
||||||
- ⚠️ 仍是三栏结构
|
|
||||||
- ⚠️ 右侧面板常驻时占用空间
|
|
||||||
|
|
||||||
**空间分配**: `240px + flex-1 + 280px` = 主内容占比 ~50% (面板展开时)
|
| 区域 | 尺寸 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 左侧导航栏 | `w-64` (256px) | 与 WorkBuddy 一致 |
|
||||||
|
| 顶部工具栏 | `h-14` (56px) | 与 WorkBuddy 一致 |
|
||||||
|
| 主内容区 | `flex-1` | 占据全部剩余空间 |
|
||||||
|
| 详情抽屉 | `w-80` (320px) | 按需滑出 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 方案 D: 单栏 + 抽屉模式(极简)
|
## 四、详细实施计划
|
||||||
|
|
||||||
```
|
### Phase 1: 重构 Sidebar 组件
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
|
||||||
│ [≡ 菜单] ZCLAW [⚙ 详情] [🔔] │
|
|
||||||
├──────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
│ 主内容区 │
|
|
||||||
│ (聊天/详情) │
|
|
||||||
│ │
|
|
||||||
│ 最大化空间 │
|
|
||||||
│ │
|
|
||||||
├──────────────────────────────────────────────────────────────────┤
|
|
||||||
│ [💬 输入框] │
|
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
点击 [≡ 菜单] → 左侧滑出抽屉导航
|
**修改文件**: `desktop/src/components/Sidebar.tsx`
|
||||||
点击 [⚙ 详情] → 右侧滑出详情面板
|
|
||||||
```
|
|
||||||
|
|
||||||
**特点**:
|
**改动内容**:
|
||||||
- ✅ 主内容区域最大化 (100%)
|
1. 移除顶部 Tab 栏(改为单一导航列表)
|
||||||
- ✅ 极简界面,专注当前任务
|
2. 添加搜索框
|
||||||
- ⚠️ 需要额外点击访问导航
|
3. 添加"新对话"按钮
|
||||||
- ⚠️ 不适合频繁切换视图
|
4. 导航项使用图标 + 文字
|
||||||
|
5. 保持 256px 宽度
|
||||||
**空间分配**: 主内容 100%,抽屉按需滑出
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、方案推荐
|
|
||||||
|
|
||||||
| 方案 | 主内容占比 | 改动量 | 适合场景 |
|
|
||||||
|------|-----------|--------|---------|
|
|
||||||
| **A: 顶部导航+双栏** ⭐ | ~80% | 中 | **推荐** - 平衡空间和功能 |
|
|
||||||
| B: 窄导航+三栏 | ~70% | 小 | 保守升级,用户熟悉 |
|
|
||||||
| C: 改良三栏 | ~50% | 最小 | 最小改动,渐进优化 |
|
|
||||||
| D: 单栏+抽屉 | 100% | 大 | 极简主义,移动端友好 |
|
|
||||||
|
|
||||||
**推荐方案 A** 的理由:
|
|
||||||
1. 聊天是核心功能,应给予最大空间
|
|
||||||
2. 顶部导航符合现代应用设计趋势
|
|
||||||
3. 左侧列表可折叠,灵活控制
|
|
||||||
4. 详情面板按需显示,不常驻占用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、实施计划(基于方案 A)
|
|
||||||
|
|
||||||
### Phase 1: 创建顶部导航组件
|
|
||||||
|
|
||||||
**新建文件**: `desktop/src/components/TopNav.tsx`
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// 顶部导航组件
|
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
|
||||||
// Tab: Chat | Agents | Hands | Workflow | Skills | Team
|
{/* 搜索框 */}
|
||||||
// 右侧: 详情按钮、设置按钮
|
<div className="p-3">
|
||||||
|
<SearchInput placeholder="搜索..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 新对话按钮 */}
|
||||||
|
<button className="mx-3 mb-2 flex items-center gap-3 px-3 py-2 bg-gray-100 rounded-lg">
|
||||||
|
<Sparkles className="text-emerald-500" />
|
||||||
|
<span>新对话</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 导航项 */}
|
||||||
|
<nav className="flex-1 px-3 space-y-0.5 overflow-y-auto">
|
||||||
|
<NavItem icon={MessageSquare} label="聊天" active />
|
||||||
|
<NavItem icon={Bot} label="分身" />
|
||||||
|
<NavItem icon={Hand} label="Hands" />
|
||||||
|
<NavItem icon={GitBranch} label="工作流" />
|
||||||
|
<NavItem icon={Package} label="技能" />
|
||||||
|
<NavItem icon={Users} label="团队" />
|
||||||
|
<NavItem icon={Layers} label="Swarm" />
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* 用户栏 */}
|
||||||
|
<div className="p-3 border-t">
|
||||||
|
<UserBar />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phase 2: 重构 App.tsx 布局
|
### Phase 2: 移除 RightPanel,改为抽屉
|
||||||
|
|
||||||
**修改文件**: `desktop/src/App.tsx`
|
**修改文件**: `desktop/src/App.tsx`
|
||||||
|
|
||||||
|
**改动内容**:
|
||||||
|
1. 移除常驻的 RightPanel
|
||||||
|
2. 添加详情抽屉状态
|
||||||
|
3. 在顶部工具栏添加"详情"按钮
|
||||||
|
4. 点击按钮时从右侧滑出抽屉
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// 新布局结构
|
const [showDetailDrawer, setShowDetailDrawer] = useState(false);
|
||||||
<div className="h-screen flex flex-col">
|
|
||||||
<TopNav />
|
// 布局
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="h-screen flex">
|
||||||
<LeftPanel collapsible /> {/* 可折叠列表区 */}
|
<Sidebar />
|
||||||
<MainContent /> {/* 主内容区 */}
|
<main className="flex-1 flex flex-col">
|
||||||
|
<header className="h-14 border-b flex items-center px-4">
|
||||||
|
<span className="font-medium">{viewTitle}</span>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<Button onClick={() => setShowDetailDrawer(true)}>
|
||||||
|
<ClipboardList /> 详情
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onOpenSettings}>
|
||||||
|
<Settings />
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{/* 主内容 */}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 详情抽屉 */}
|
||||||
|
<DetailDrawer open={showDetailDrawer} onClose={() => setShowDetailDrawer(false)}>
|
||||||
|
<RightPanelContent />
|
||||||
|
</DetailDrawer>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phase 3: 重构 Sidebar 为 LeftPanel
|
### Phase 3: 创建 DetailDrawer 组件
|
||||||
|
|
||||||
**修改文件**: `desktop/src/components/Sidebar.tsx` → `LeftPanel.tsx`
|
**新建文件**: `desktop/src/components/DetailDrawer.tsx`
|
||||||
|
|
||||||
- 移除顶部 Tab(已移到 TopNav)
|
```tsx
|
||||||
- 添加折叠功能
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
- 显示当前视图的列表
|
|
||||||
|
|
||||||
### Phase 4: RightPanel 改为抽屉模式
|
export function DetailDrawer({ open, onClose, children }) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
{/* 遮罩 */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/20 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
{/* 抽屉 */}
|
||||||
|
<motion.aside
|
||||||
|
initial={{ x: 320 }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: 320 }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||||
|
className="fixed right-0 top-0 bottom-0 w-80 bg-white border-l z-50 flex flex-col"
|
||||||
|
>
|
||||||
|
<header className="h-14 border-b flex items-center px-4">
|
||||||
|
<span className="font-medium">详情</span>
|
||||||
|
<button onClick={onClose} className="ml-auto">
|
||||||
|
<X />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**修改文件**: `desktop/src/components/RightPanel.tsx`
|
### Phase 4: 主内容区适配
|
||||||
|
|
||||||
- 改为滑出式抽屉(Drawer)
|
**修改文件**: 各视图组件
|
||||||
- 默认隐藏,点击按钮显示
|
|
||||||
- 优化 Tab 分组
|
|
||||||
|
|
||||||
### Phase 5: 响应式适配
|
根据 WorkBuddy 的风格调整各页面布局:
|
||||||
|
- **聊天页**: 居中卡片式输入框,上方消息列表
|
||||||
**修改文件**: `desktop/src/App.tsx`
|
- **分身页**: 左侧列表 + 右侧详情(或卡片网格)
|
||||||
|
- **Hands 页**: 列表 + 详情
|
||||||
- 窗口 < 1024px: 自动折叠 LeftPanel
|
- **工作流页**: 列表 + 编辑器
|
||||||
- 窗口 < 768px: LeftPanel 改为抽屉
|
- **技能页**: 网格卡片布局(参考 WorkBuddy 技能页)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -204,22 +248,66 @@
|
|||||||
|
|
||||||
| 文件 | 操作 | 说明 |
|
| 文件 | 操作 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `components/TopNav.tsx` | 新建 | 顶部导航栏 |
|
| `components/Sidebar.tsx` | 重构 | 改为 WorkBuddy 风格导航 |
|
||||||
| `components/LeftPanel.tsx` | 新建/重构 | 可折叠左侧列表 |
|
| `components/DetailDrawer.tsx` | **新建** | 右侧滑出抽屉 |
|
||||||
| `components/Drawer.tsx` | 新建 | 通用抽屉组件 |
|
| `components/TopBar.tsx` | **新建** | 顶部工具栏 |
|
||||||
| `App.tsx` | 重构 | 新布局结构 |
|
| `App.tsx` | 重构 | 新布局结构 |
|
||||||
| `Sidebar.tsx` | 删除/重构 | 合并到 LeftPanel |
|
| `RightPanel.tsx` | 重构 | 内容移入 DetailDrawer |
|
||||||
| `RightPanel.tsx` | 重构 | 改为抽屉模式 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、验证方法
|
## 六、颜色和样式规范
|
||||||
|
|
||||||
|
### 主色调 (参考 WorkBuddy)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 强调色 */
|
||||||
|
--primary: #10b981; /* emerald-500 */
|
||||||
|
--primary-hover: #059669; /* emerald-600 */
|
||||||
|
|
||||||
|
/* 背景 */
|
||||||
|
--bg-main: #ffffff;
|
||||||
|
--bg-sidebar: #ffffff;
|
||||||
|
--bg-input: #f9fafb; /* gray-50 */
|
||||||
|
|
||||||
|
/* 边框 */
|
||||||
|
--border: #e5e7eb; /* gray-200 */
|
||||||
|
--border-light: #f3f4f6; /* gray-100 */
|
||||||
|
|
||||||
|
/* 文字 */
|
||||||
|
--text-primary: #111827; /* gray-900 */
|
||||||
|
--text-secondary: #6b7280; /* gray-500 */
|
||||||
|
--text-muted: #9ca3af; /* gray-400 */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 尺寸规范
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 导航栏 */
|
||||||
|
--sidebar-width: 256px;
|
||||||
|
--header-height: 56px;
|
||||||
|
|
||||||
|
/* 间距 */
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 12px;
|
||||||
|
--spacing-lg: 16px;
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、验证方法
|
||||||
|
|
||||||
1. **布局验证**: 检查各组件在正确位置
|
1. **布局验证**: 检查各组件在正确位置
|
||||||
2. **折叠验证**: LeftPanel 可正常折叠/展开
|
2. **导航验证**: 点击导航项切换视图
|
||||||
3. **抽屉验证**: RightPanel 抽屉正常滑出/关闭
|
3. **抽屉验证**: 点击详情按钮抽屉滑出
|
||||||
4. **响应式验证**: 不同窗口尺寸下布局自适应
|
4. **响应式验证**: 窗口缩放时布局正确
|
||||||
5. **功能验证**: 所有现有功能正常工作
|
5. **暗色模式验证**: 暗色模式下样式正确
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
pnpm dev
|
||||||
@@ -228,23 +316,51 @@ pnpm dev
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七、实施预估
|
## 八、视觉效果对比
|
||||||
|
|
||||||
| Phase | 工作量 | 优先级 |
|
### 改造前
|
||||||
|-------|--------|--------|
|
|
||||||
| Phase 1: TopNav | 2h | P0 |
|
|
||||||
| Phase 2: App 重构 | 2h | P0 |
|
|
||||||
| Phase 3: LeftPanel | 2h | P0 |
|
|
||||||
| Phase 4: RightPanel 抽屉 | 1.5h | P1 |
|
|
||||||
| Phase 5: 响应式 | 1h | P1 |
|
|
||||||
|
|
||||||
**总计**: 约 8.5 小时
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [Tab: 分身|Hands|工作流|技能|团队|协作] │
|
||||||
|
├────────────────────────────────────────────────┬───────────────┤
|
||||||
|
│ 主内容区 (~50%) │ RightPanel │
|
||||||
|
│ │ (320px) │
|
||||||
|
└────────────────────────────────────────────────┴───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 改造后 (WorkBuddy 风格)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────┬───────────────────────────────────────────────┐
|
||||||
|
│ 🔍 搜索 │ │
|
||||||
|
│ ✨ 新对话 │ │
|
||||||
|
│ 💬 聊天 │ │
|
||||||
|
│ 🤖 分身 │ 主内容区 │
|
||||||
|
│ 🖐️ Hands │ (100%) │
|
||||||
|
│ ⚡ 工作流 │ │
|
||||||
|
│ 📦 技能 │ [📋 详情] → 按需显示抽屉 │
|
||||||
|
│ 👥 团队 │ │
|
||||||
|
│ 🐝 Swarm │ │
|
||||||
|
│ ────────── │ │
|
||||||
|
│ 👤 用户 │ │
|
||||||
|
└────────────────┴───────────────────────────────────────────────┘
|
||||||
|
256px
|
||||||
|
```
|
||||||
|
|
||||||
|
**空间优化**:
|
||||||
|
- 移除常驻 RightPanel,主内容区从 ~50% 扩大到 ~100%
|
||||||
|
- 详情通过抽屉按需显示,不占用常驻空间
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 八、待确认问题
|
## 九、实施预估
|
||||||
|
|
||||||
1. **布局方案选择**: 你更倾向于哪个方案 (A/B/C/D)?
|
| Phase | 工作量 | 优先级 |
|
||||||
2. **RightPanel 处理**: 改为抽屉模式,还是保留常驻但可折叠?
|
|-------|--------|--------|
|
||||||
3. **快捷键**: 是否需要快捷键切换面板 (Cmd+[ / Cmd+])?
|
| Phase 1: 重构 Sidebar | 2h | P0 |
|
||||||
4. **过渡动画**: 是否需要平滑的折叠/展开动画?
|
| Phase 2: 移除 RightPanel,改为抽屉 | 1.5h | P0 |
|
||||||
|
| Phase 3: 创建 DetailDrawer | 1h | P0 |
|
||||||
|
| Phase 4: 主内容区适配 | 2h | P1 |
|
||||||
|
|
||||||
|
**总计**: 约 6.5 小时
|
||||||
|
|||||||