feat(hands,desktop): C线差异化 — 管家日报 + 零配置引导优化
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

C1 管家日报:
- 新增 _daily_report Hand (daily_report.rs) — 5个测试
- 增强 user_profile_store — PainPoint 结构体 + find_active_pains_since + resolve_pain
- experience_store 新增 find_since 日期范围查询
- trajectory_store 新增 get_events_since 日期范围查询
- 新增 DailyReportPanel.tsx 前端日报面板
- Sidebar 新增"日报"导航入口

C3 零配置引导:
- 修复行业卡点击后阶段推进 bug (industry_discovery → identity_setup)

验证: 940 tests PASS, 0 failures
This commit is contained in:
iven
2026-04-21 18:23:36 +08:00
parent a43806ccc2
commit ae56aba366
8 changed files with 864 additions and 44 deletions

View File

@@ -1,11 +1,12 @@
/**
* FirstConversationPrompt - Welcome prompt for new conversations
* FirstConversationPrompt - Conversation-driven cold start UI
*
* DeerFlow-inspired design:
* - Centered layout with emoji greeting
* - Input bar embedded in welcome screen
* - Horizontal quick-action chips (colored pills)
* - Clean, minimal aesthetic
* Dynamically adapts based on cold start phase:
* idle/agent_greeting → Welcome + auto-greeting
* industry_discovery → 4 industry cards
* identity_setup → Name confirmation prompt
* first_task → Industry-specific task suggestions
* completed → General quick actions (original DeerFlow-style)
*/
import { useEffect } from 'react';
import { motion } from 'framer-motion';
@@ -18,18 +19,14 @@ import {
MessageSquare,
} from 'lucide-react';
import { cn } from '../lib/utils';
import {
generateWelcomeMessage,
getScenarioById,
} from '../lib/personality-presets';
import { useColdStart } from '../lib/use-cold-start';
import { generateWelcomeMessage, getScenarioById } from '../lib/personality-presets';
import { useColdStart, INDUSTRY_CARDS, INDUSTRY_FIRST_TASKS } from '../lib/use-cold-start';
import type { Clone } from '../store/agentStore';
import { useChatStore } from '../store/chatStore';
import { useClassroomStore } from '../store/classroomStore';
import { useHandStore } from '../store/handStore';
// Quick action chip definitions — DeerFlow-style colored pills
// handId maps to actual Hand names in the runtime
// Original quick actions for completed state
const QUICK_ACTIONS = [
{ key: 'surprise', label: '小惊喜', icon: Sparkles, color: 'text-orange-500' },
{ key: 'write', label: '写作', icon: PenLine, color: 'text-blue-500' },
@@ -38,7 +35,6 @@ const QUICK_ACTIONS = [
{ key: 'learn', label: '学习', icon: GraduationCap, color: 'text-indigo-500' },
];
// Pre-filled prompts for each quick action — tailored for target industries
const QUICK_ACTION_PROMPTS: Record<string, string> = {
surprise: '给我一个小惊喜吧!来点创意的',
write: '帮我写一份关于"远程医疗行政管理优化方案"的提案大纲',
@@ -58,16 +54,27 @@ export function FirstConversationPrompt({
onSelectSuggestion,
}: FirstConversationPromptProps) {
const chatMode = useChatStore((s) => s.chatMode);
const { isColdStart, phase, greetingSent, markGreetingSent, getGreetingMessage } = useColdStart();
const {
isColdStart,
phase,
config,
greetingSent,
markGreetingSent,
advanceTo,
updateConfig,
markCompleted,
getGreetingMessage,
} = useColdStart();
// Cold start: auto-trigger greeting for first-time users
// Auto-trigger greeting for new users
useEffect(() => {
if (isColdStart && phase === 'idle' && !greetingSent) {
const greeting = getGreetingMessage(clone.nickname || clone.name, clone.emoji);
onSelectSuggestion?.(greeting);
markGreetingSent();
advanceTo('agent_greeting');
}
}, [isColdStart, phase, greetingSent, clone.nickname, clone.name, clone.emoji, onSelectSuggestion, markGreetingSent, getGreetingMessage]);
}, [isColdStart, phase, greetingSent, clone.nickname, clone.name, clone.emoji, onSelectSuggestion, markGreetingSent, advanceTo, getGreetingMessage]);
const modeGreeting: Record<string, string> = {
flash: '快速回答,即时响应',
@@ -76,23 +83,40 @@ export function FirstConversationPrompt({
ultra: '多代理协作,全能力调度',
};
// Use template-provided welcome message if available, otherwise generate dynamically
const isNewUser = !localStorage.getItem('zclaw-onboarding-completed');
const welcomeTitle = isNewUser ? '你好,欢迎开始!' : '你好,欢迎回来!';
const welcomeMessage = clone.welcomeMessage
|| generateWelcomeMessage({
userName: clone.userName,
agentName: clone.nickname || clone.name,
emoji: clone.emoji,
personality: clone.personality,
scenarios: clone.scenarios,
});
// === Industry card click handler ===
const handleIndustrySelect = (industryKey: string) => {
const industryNames: Record<string, string> = {
healthcare: '医疗行政',
education: '教育培训',
garment: '制衣制造',
ecommerce: '电商零售',
};
const prompt = `我是做${industryNames[industryKey] ?? industryKey}`;
onSelectSuggestion?.(prompt);
updateConfig({
detectedIndustry: industryKey,
personality: {
tone: industryKey === 'healthcare' ? 'professional' : industryKey === 'ecommerce' ? 'energetic' : 'friendly',
formality: 'semi-formal',
proactiveness: 'moderate',
},
});
advanceTo('identity_setup');
};
// === First task click handler ===
const handleFirstTask = (prompt: string) => {
onSelectSuggestion?.(prompt);
markCompleted();
};
// === Original quick action handler (completed state) ===
const handleQuickAction = (key: string) => {
if (key === 'learn') {
// Trigger classroom generation flow
const classroomStore = useClassroomStore.getState();
// Extract a clean topic from the prompt
const prompt = QUICK_ACTION_PROMPTS[key] || '';
const topic = prompt
.replace(/^[你我].*?(想了解|想学|了解|学习|分析|研究|探索)\s*/g, '')
@@ -104,13 +128,10 @@ export function FirstConversationPrompt({
style: 'lecture',
level: 'intermediate',
language: 'zh-CN',
}).catch(() => {
// Error is already stored in classroomStore.error and displayed in ChatArea
});
}).catch(() => {});
return;
}
// Check if this action maps to a Hand
const actionDef = QUICK_ACTIONS.find((a) => a.key === key);
if (actionDef?.handId) {
const handStore = useHandStore.getState();
@@ -118,16 +139,159 @@ export function FirstConversationPrompt({
action: key === 'research' ? 'report' : 'collect',
query: { query: QUICK_ACTION_PROMPTS[key] || '' },
}).catch(() => {
// Fallback: fill prompt into input bar
onSelectSuggestion?.(QUICK_ACTION_PROMPTS[key] || '你好!');
});
return;
}
const prompt = QUICK_ACTION_PROMPTS[key] || '你好!';
onSelectSuggestion?.(prompt);
onSelectSuggestion?.(QUICK_ACTION_PROMPTS[key] || '你好!');
};
// === Render based on phase ===
// During active cold start, show contextual UI
if (isColdStart && phase === 'agent_greeting') {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex flex-col items-center justify-center py-12 px-4"
>
<div className="text-5xl mb-4">{clone.emoji || '👋'}</div>
<motion.h1
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.5 }}
className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2"
>
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="text-sm text-gray-500 dark:text-gray-400 text-center max-w-md"
>
</motion.p>
</motion.div>
);
}
// Industry discovery: show 4 industry cards
if (isColdStart && phase === 'industry_discovery' && !config.detectedIndustry) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex flex-col items-center justify-center py-12 px-4"
>
<div className="text-4xl mb-4">🎯</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6 text-center max-w-sm">
</p>
<div className="grid grid-cols-2 gap-3 max-w-sm w-full">
{INDUSTRY_CARDS.map((card, index) => (
<motion.button
key={card.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + index * 0.05, duration: 0.2 }}
onClick={() => handleIndustrySelect(card.key)}
className={cn(
'flex flex-col items-center gap-1 px-4 py-4',
'bg-white dark:bg-gray-800',
'border border-gray-200 dark:border-gray-700',
'rounded-xl text-center',
'hover:border-primary/50 dark:hover:border-primary/50',
'hover:bg-primary/5 dark:hover:bg-primary/5',
'transition-all duration-150',
)}
>
<span className="text-lg">{card.label.split(' ')[0]}</span>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{card.label.split(' ')[1]}
</span>
<span className="text-xs text-gray-400 dark:text-gray-500 mt-1">
{card.description}
</span>
</motion.button>
))}
</div>
<p className="mt-4 text-xs text-gray-400 dark:text-gray-500">
</p>
</motion.div>
);
}
// First task: show industry-specific task suggestions
if (isColdStart && (phase === 'first_task' || (phase === 'identity_setup' && config.detectedIndustry))) {
const industry = config.detectedIndustry ?? '_default';
const tasks = INDUSTRY_FIRST_TASKS[industry] ?? INDUSTRY_FIRST_TASKS._default;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex flex-col items-center justify-center py-12 px-4"
>
<div className="text-4xl mb-4">
{config.suggestedName ? `` : clone.emoji || '🚀'}
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6 text-center max-w-sm">
</p>
<div className="flex flex-col gap-2 max-w-sm w-full">
{tasks.map((task, index) => (
<motion.button
key={task.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.05 + index * 0.04, duration: 0.2 }}
onClick={() => handleFirstTask(task.prompt)}
className={cn(
'flex items-center gap-3 px-4 py-3',
'bg-white dark:bg-gray-800',
'border border-gray-200 dark:border-gray-700',
'rounded-lg text-left',
'hover:border-primary/50 dark:hover:border-primary/50',
'hover:bg-primary/5 dark:hover:bg-primary/5',
'transition-all duration-150',
)}
>
<Sparkles className="w-4 h-4 text-primary shrink-0" />
<div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{task.label}
</span>
</div>
</motion.button>
))}
</div>
</motion.div>
);
}
// Default / completed state: original DeerFlow-style quick actions
const welcomeMessage = clone.welcomeMessage
|| generateWelcomeMessage({
userName: clone.userName,
agentName: clone.nickname || clone.name,
emoji: clone.emoji,
personality: clone.personality,
scenarios: clone.scenarios,
});
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -135,10 +299,8 @@ export function FirstConversationPrompt({
exit={{ opacity: 0, y: -10 }}
className="flex flex-col items-center justify-center py-12 px-4"
>
{/* Greeting emoji */}
<div className="text-5xl mb-4">{clone.emoji || '👋'}</div>
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
@@ -148,7 +310,6 @@ export function FirstConversationPrompt({
{welcomeTitle}
</motion.h1>
{/* Mode-aware subtitle */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -159,14 +320,12 @@ export function FirstConversationPrompt({
{modeGreeting[chatMode] || '智能对话,随时待命'}
</motion.p>
{/* Welcome message */}
<div className="text-center max-w-md mb-8">
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
{welcomeMessage}
</p>
</div>
{/* Quick action chips — template-provided or DeerFlow-style defaults */}
<div className="flex items-center justify-center gap-2 flex-wrap">
{clone.quickCommands && clone.quickCommands.length > 0
? clone.quickCommands.map((cmd, index) => (
@@ -216,7 +375,6 @@ export function FirstConversationPrompt({
})}
</div>
{/* Scenario tags */}
{clone.scenarios && clone.scenarios.length > 0 && (
<div className="mt-8 flex flex-wrap gap-2 justify-center">
{clone.scenarios.map((scenarioId) => {
@@ -237,7 +395,6 @@ export function FirstConversationPrompt({
</div>
)}
{/* Dismiss hint */}
<p className="mt-8 text-xs text-gray-400 dark:text-gray-500">
</p>