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
406 lines
15 KiB
TypeScript
406 lines
15 KiB
TypeScript
/**
|
||
* FirstConversationPrompt - Conversation-driven cold start UI
|
||
*
|
||
* 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';
|
||
import {
|
||
Sparkles,
|
||
PenLine,
|
||
Microscope,
|
||
Layers,
|
||
GraduationCap,
|
||
MessageSquare,
|
||
} from 'lucide-react';
|
||
import { cn } from '../lib/utils';
|
||
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';
|
||
|
||
// 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' },
|
||
{ key: 'research', label: '研究', icon: Microscope, color: 'text-purple-500', handId: 'researcher' },
|
||
{ key: 'collect', label: '收集', icon: Layers, color: 'text-green-500', handId: 'collector' },
|
||
{ key: 'learn', label: '学习', icon: GraduationCap, color: 'text-indigo-500' },
|
||
];
|
||
|
||
const QUICK_ACTION_PROMPTS: Record<string, string> = {
|
||
surprise: '给我一个小惊喜吧!来点创意的',
|
||
write: '帮我写一份关于"远程医疗行政管理优化方案"的提案大纲',
|
||
research: '帮我深度研究"2026年教育数字化转型趋势",包括政策、技术和实践三个维度',
|
||
collect: '帮我采集 5 个主流 AI 教育工具的产品信息,对比功能和价格',
|
||
learn: '我想了解汕头玩具产业 2026 年出口趋势,能帮我分析一下吗?',
|
||
};
|
||
|
||
interface FirstConversationPromptProps {
|
||
clone: Clone;
|
||
onSelectSuggestion?: (text: string) => void;
|
||
onDismiss?: () => void;
|
||
}
|
||
|
||
export function FirstConversationPrompt({
|
||
clone,
|
||
onSelectSuggestion,
|
||
}: FirstConversationPromptProps) {
|
||
const chatMode = useChatStore((s) => s.chatMode);
|
||
const {
|
||
isColdStart,
|
||
phase,
|
||
config,
|
||
greetingSent,
|
||
markGreetingSent,
|
||
advanceTo,
|
||
updateConfig,
|
||
markCompleted,
|
||
getGreetingMessage,
|
||
} = useColdStart();
|
||
|
||
// 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, advanceTo, getGreetingMessage]);
|
||
|
||
const modeGreeting: Record<string, string> = {
|
||
flash: '快速回答,即时响应',
|
||
thinking: '深度分析,逐步推理',
|
||
pro: '专业规划,系统思考',
|
||
ultra: '多代理协作,全能力调度',
|
||
};
|
||
|
||
const isNewUser = !localStorage.getItem('zclaw-onboarding-completed');
|
||
const welcomeTitle = isNewUser ? '你好,欢迎开始!' : '你好,欢迎回来!';
|
||
|
||
// === 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') {
|
||
const classroomStore = useClassroomStore.getState();
|
||
const prompt = QUICK_ACTION_PROMPTS[key] || '';
|
||
const topic = prompt
|
||
.replace(/^[你我].*?(想了解|想学|了解|学习|分析|研究|探索)\s*/g, '')
|
||
.replace(/[,。?!].*$/g, '')
|
||
.replace(/^(能|帮|请|可不可以).*/g, '')
|
||
.trim() || '互动课堂';
|
||
classroomStore.startGeneration({
|
||
topic,
|
||
style: 'lecture',
|
||
level: 'intermediate',
|
||
language: 'zh-CN',
|
||
}).catch(() => {});
|
||
return;
|
||
}
|
||
|
||
const actionDef = QUICK_ACTIONS.find((a) => a.key === key);
|
||
if (actionDef?.handId) {
|
||
const handStore = useHandStore.getState();
|
||
handStore.triggerHand(actionDef.handId, {
|
||
action: key === 'research' ? 'report' : 'collect',
|
||
query: { query: QUICK_ACTION_PROMPTS[key] || '' },
|
||
}).catch(() => {
|
||
onSelectSuggestion?.(QUICK_ACTION_PROMPTS[key] || '你好!');
|
||
});
|
||
return;
|
||
}
|
||
|
||
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 }}
|
||
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"
|
||
>
|
||
{welcomeTitle}
|
||
</motion.h1>
|
||
|
||
<motion.p
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ delay: 0.2, duration: 0.4 }}
|
||
className="text-sm text-orange-500 dark:text-orange-400 font-medium mb-4 flex items-center gap-1.5"
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
{modeGreeting[chatMode] || '智能对话,随时待命'}
|
||
</motion.p>
|
||
|
||
<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>
|
||
|
||
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||
{clone.quickCommands && clone.quickCommands.length > 0
|
||
? clone.quickCommands.map((cmd, index) => (
|
||
<motion.button
|
||
key={cmd.label}
|
||
initial={{ opacity: 0, y: 8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.3 + index * 0.05, duration: 0.2 }}
|
||
onClick={() => onSelectSuggestion?.(cmd.command)}
|
||
className={cn(
|
||
'flex items-center gap-2 px-4 py-2',
|
||
'bg-white dark:bg-gray-800',
|
||
'border border-gray-200 dark:border-gray-700',
|
||
'rounded-full text-sm text-gray-600 dark:text-gray-300',
|
||
'hover:border-gray-300 dark:hover:border-gray-600',
|
||
'hover:bg-gray-50 dark:hover:bg-gray-750',
|
||
'transition-all duration-150'
|
||
)}
|
||
>
|
||
<MessageSquare className="w-4 h-4 text-primary" />
|
||
<span>{cmd.label}</span>
|
||
</motion.button>
|
||
))
|
||
: QUICK_ACTIONS.map((action, index) => {
|
||
const ActionIcon = action.icon;
|
||
return (
|
||
<motion.button
|
||
key={action.key}
|
||
initial={{ opacity: 0, y: 8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.3 + index * 0.05, duration: 0.2 }}
|
||
onClick={() => handleQuickAction(action.key)}
|
||
className={cn(
|
||
'flex items-center gap-2 px-4 py-2',
|
||
'bg-white dark:bg-gray-800',
|
||
'border border-gray-200 dark:border-gray-700',
|
||
'rounded-full text-sm text-gray-600 dark:text-gray-300',
|
||
'hover:border-gray-300 dark:hover:border-gray-600',
|
||
'hover:bg-gray-50 dark:hover:bg-gray-750',
|
||
'transition-all duration-150'
|
||
)}
|
||
>
|
||
<ActionIcon className={`w-4 h-4 ${action.color}`} />
|
||
<span>{action.label}</span>
|
||
</motion.button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{clone.scenarios && clone.scenarios.length > 0 && (
|
||
<div className="mt-8 flex flex-wrap gap-2 justify-center">
|
||
{clone.scenarios.map((scenarioId) => {
|
||
const scenario = getScenarioById(scenarioId);
|
||
if (!scenario) return null;
|
||
return (
|
||
<span
|
||
key={scenarioId}
|
||
className={cn(
|
||
'px-3 py-1 rounded-full text-xs font-medium',
|
||
'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
||
)}
|
||
>
|
||
{scenario.label}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<p className="mt-8 text-xs text-gray-400 dark:text-gray-500">
|
||
发送消息开始对话,或点击上方建议
|
||
</p>
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
export default FirstConversationPrompt;
|