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
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user