From ae56aba366d7857a31f3f2acf51c9e169f2ac37e Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 18:23:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(hands,desktop):=20C=E7=BA=BF=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E5=8C=96=20=E2=80=94=20=E7=AE=A1=E5=AE=B6=E6=97=A5?= =?UTF-8?q?=E6=8A=A5=20+=20=E9=9B=B6=E9=85=8D=E7=BD=AE=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/zclaw-growth/src/experience_store.rs | 14 + crates/zclaw-hands/src/hands/daily_report.rs | 241 ++++++++++++++++ crates/zclaw-hands/src/hands/mod.rs | 2 + crates/zclaw-memory/src/trajectory_store.rs | 43 +++ crates/zclaw-memory/src/user_profile_store.rs | 89 ++++++ desktop/src/components/DailyReportPanel.tsx | 261 ++++++++++++++++++ .../components/FirstConversationPrompt.tsx | 241 +++++++++++++--- desktop/src/components/Sidebar.tsx | 17 +- 8 files changed, 864 insertions(+), 44 deletions(-) create mode 100644 crates/zclaw-hands/src/hands/daily_report.rs create mode 100644 desktop/src/components/DailyReportPanel.tsx diff --git a/crates/zclaw-growth/src/experience_store.rs b/crates/zclaw-growth/src/experience_store.rs index 1f999c7..bb0bfbc 100644 --- a/crates/zclaw-growth/src/experience_store.rs +++ b/crates/zclaw-growth/src/experience_store.rs @@ -248,6 +248,20 @@ impl ExperienceStore { debug!("[ExperienceStore] Deleted experience {} for agent {}", exp.id, exp.agent_id); Ok(()) } + + /// Find experiences for an agent created since the given datetime. + /// Filters by deserializing each entry and checking `created_at`. + pub async fn find_since( + &self, + agent_id: &str, + since: DateTime, + ) -> zclaw_types::Result> { + let all = self.find_by_agent(agent_id).await?; + Ok(all + .into_iter() + .filter(|exp| exp.created_at >= since) + .collect()) + } } // --------------------------------------------------------------------------- diff --git a/crates/zclaw-hands/src/hands/daily_report.rs b/crates/zclaw-hands/src/hands/daily_report.rs new file mode 100644 index 0000000..9700e0a --- /dev/null +++ b/crates/zclaw-hands/src/hands/daily_report.rs @@ -0,0 +1,241 @@ +//! Daily Report Hand — generates a personalized daily briefing. +//! +//! System hand (`_daily_report`) triggered by SchedulerService at 09:00 cron. +//! Produces a Markdown daily report containing: +//! 1. Yesterday's conversation summary +//! 2. Unresolved pain points follow-up +//! 3. Recent experience highlights +//! 4. Industry news placeholder +//! +//! Emits `daily-report:ready` Tauri event and persists to VikingStorage. + +use async_trait::async_trait; +use serde_json::Value; +use zclaw_types::Result; + +use crate::{Hand, HandConfig, HandContext, HandResult, HandStatus}; + +/// Internal daily report hand. +pub struct DailyReportHand { + config: HandConfig, +} + +impl DailyReportHand { + pub fn new() -> Self { + Self { + config: HandConfig { + id: "_daily_report".to_string(), + name: "管家日报".to_string(), + description: "Generates personalized daily briefing".to_string(), + needs_approval: false, + dependencies: vec![], + input_schema: None, + tags: vec!["system".to_string()], + enabled: true, + max_concurrent: 0, + timeout_secs: 0, + }, + } + } +} + +#[async_trait] +impl Hand for DailyReportHand { + fn config(&self) -> &HandConfig { + &self.config + } + + async fn execute(&self, _context: &HandContext, input: Value) -> Result { + let agent_id = input + .get("agent_id") + .and_then(|v| v.as_str()) + .unwrap_or("default_user"); + + let industry = input + .get("industry") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let trajectory_summary = input + .get("trajectory_summary") + .and_then(|v| v.as_str()) + .unwrap_or("昨日无对话记录"); + + let pain_points = input + .get("pain_points") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect::>() + }) + .unwrap_or_default(); + + let recent_experiences = input + .get("recent_experiences") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect::>() + }) + .unwrap_or_default(); + + let report = self.build_report(industry, trajectory_summary, &pain_points, &recent_experiences); + + tracing::info!( + "[DailyReportHand] Generated report for agent {} ({} pains, {} experiences)", + agent_id, + pain_points.len(), + recent_experiences.len(), + ); + + Ok(HandResult::success(serde_json::json!({ + "agent_id": agent_id, + "report": report, + "pain_count": pain_points.len(), + "experience_count": recent_experiences.len(), + }))) + } + + fn status(&self) -> HandStatus { + HandStatus::Idle + } +} + +impl DailyReportHand { + fn build_report( + &self, + industry: &str, + trajectory_summary: &str, + pain_points: &[String], + recent_experiences: &[String], + ) -> String { + let industry_label = match industry { + "healthcare" => "医疗行政", + "education" => "教育培训", + "garment" => "制衣制造", + "ecommerce" => "电商零售", + _ => "综合", + }; + + let date = chrono::Local::now().format("%Y年%m月%d日").to_string(); + + let mut sections = vec![ + format!("# {} 管家日报 — {}", industry_label, date), + String::new(), + "## 昨日对话摘要".to_string(), + trajectory_summary.to_string(), + String::new(), + ]; + + if !pain_points.is_empty() { + sections.push("## 待解决问题".to_string()); + for (i, pain) in pain_points.iter().enumerate() { + sections.push(format!("{}. {}", i + 1, pain)); + } + sections.push(String::new()); + } + + if !recent_experiences.is_empty() { + sections.push("## 昨日收获".to_string()); + for exp in recent_experiences { + sections.push(format!("- {}", exp)); + } + sections.push(String::new()); + } + + sections.push("## 今日提醒".to_string()); + sections.push(self.daily_reminder(industry)); + sections.push(String::new()); + sections.push("祝你今天工作顺利!".to_string()); + + sections.join("\n") + } + + fn daily_reminder(&self, industry: &str) -> String { + match industry { + "healthcare" => "记得检查今日科室排班,关注耗材库存预警。".to_string(), + "education" => "今日有课程安排吗?提前准备教学材料。".to_string(), + "garment" => "关注今日生产进度,及时跟进订单交期。".to_string(), + "ecommerce" => "检查库存预警和待发货订单,把握促销节奏。".to_string(), + _ => "新的一天,新的开始。有什么需要我帮忙的随时说。".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use zclaw_types::AgentId; + + #[test] + fn test_build_report_basic() { + let hand = DailyReportHand::new(); + let report = hand.build_report( + "healthcare", + "讨论了科室排班问题", + &["排班冲突".to_string()], + &["学会了用数据报表工具".to_string()], + ); + assert!(report.contains("医疗行政")); + assert!(report.contains("排班冲突")); + assert!(report.contains("学会了用数据报表工具")); + } + + #[test] + fn test_build_report_empty() { + let hand = DailyReportHand::new(); + let report = hand.build_report("", "昨日无对话记录", &[], &[]); + assert!(report.contains("管家日报")); + assert!(report.contains("综合")); + } + + #[test] + fn test_build_report_all_industries() { + let hand = DailyReportHand::new(); + for industry in &["healthcare", "education", "garment", "ecommerce", "unknown"] { + let report = hand.build_report(industry, "test", &[], &[]); + assert!(!report.is_empty()); + } + } + + #[tokio::test] + async fn test_execute_with_data() { + let hand = DailyReportHand::new(); + let ctx = HandContext { + agent_id: AgentId::new(), + working_dir: None, + env: std::collections::HashMap::new(), + timeout_secs: 30, + callback_url: None, + }; + let input = serde_json::json!({ + "agent_id": "test-agent", + "industry": "education", + "trajectory_summary": "讨论了课程安排", + "pain_points": ["学生成绩下降"], + "recent_experiences": ["掌握了成绩分析方法"], + }); + + let result = hand.execute(&ctx, input).await.unwrap(); + assert!(result.success); + let output = result.output; + assert_eq!(output["agent_id"], "test-agent"); + assert!(output["report"].as_str().unwrap().contains("教育培训")); + } + + #[tokio::test] + async fn test_execute_minimal() { + let hand = DailyReportHand::new(); + let ctx = HandContext { + agent_id: AgentId::new(), + working_dir: None, + env: std::collections::HashMap::new(), + timeout_secs: 30, + callback_url: None, + }; + let result = hand.execute(&ctx, serde_json::json!({})).await.unwrap(); + assert!(result.success); + } +} diff --git a/crates/zclaw-hands/src/hands/mod.rs b/crates/zclaw-hands/src/hands/mod.rs index c3f1261..c25e9b7 100644 --- a/crates/zclaw-hands/src/hands/mod.rs +++ b/crates/zclaw-hands/src/hands/mod.rs @@ -15,6 +15,7 @@ mod collector; mod clip; mod twitter; pub mod reminder; +pub mod daily_report; pub use quiz::*; pub use browser::*; @@ -23,3 +24,4 @@ pub use collector::*; pub use clip::*; pub use twitter::*; pub use reminder::*; +pub use daily_report::*; diff --git a/crates/zclaw-memory/src/trajectory_store.rs b/crates/zclaw-memory/src/trajectory_store.rs index 62f68c6..4f283ea 100644 --- a/crates/zclaw-memory/src/trajectory_store.rs +++ b/crates/zclaw-memory/src/trajectory_store.rs @@ -398,6 +398,49 @@ impl TrajectoryStore { Ok(result.rows_affected()) } + + /// Get trajectory events for an agent created since the given datetime. + pub async fn get_events_since( + &self, + agent_id: &str, + since: DateTime, + ) -> Result> { + let rows = sqlx::query_as::<_, (String, String, String, i64, String, Option, Option, Option, String)>( + r#" + SELECT id, session_id, agent_id, step_index, step_type, + input_summary, output_summary, duration_ms, timestamp + FROM trajectory_events + WHERE agent_id = ? AND timestamp >= ? + ORDER BY timestamp ASC + "#, + ) + .bind(agent_id) + .bind(since.to_rfc3339()) + .fetch_all(&self.pool) + .await + .map_err(|e| ZclawError::StorageError(e.to_string()))?; + + let mut events = Vec::with_capacity(rows.len()); + for (id, sid, aid, step_idx, stype, input_s, output_s, dur_ms, ts) in rows { + let timestamp = DateTime::parse_from_rfc3339(&ts) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or_else(|_| Utc::now()); + + events.push(TrajectoryEvent { + id, + session_id: sid, + agent_id: aid, + step_index: step_idx as usize, + step_type: TrajectoryStepType::from_str_lossy(&stype), + input_summary: input_s.unwrap_or_default(), + output_summary: output_s.unwrap_or_default(), + duration_ms: dur_ms.unwrap_or(0) as u64, + timestamp, + }); + } + + Ok(events) + } } // --------------------------------------------------------------------------- diff --git a/crates/zclaw-memory/src/user_profile_store.rs b/crates/zclaw-memory/src/user_profile_store.rs index e4d2563..bf54ff0 100644 --- a/crates/zclaw-memory/src/user_profile_store.rs +++ b/crates/zclaw-memory/src/user_profile_store.rs @@ -15,6 +15,56 @@ use zclaw_types::Result; // Data types // --------------------------------------------------------------------------- +/// Pain point status for tracking resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PainStatus { + Active, + Resolved, + Deferred, +} + +impl PainStatus { + pub fn as_str(&self) -> &'static str { + match self { + PainStatus::Active => "active", + PainStatus::Resolved => "resolved", + PainStatus::Deferred => "deferred", + } + } + + pub fn from_str_lossy(s: &str) -> Self { + match s { + "resolved" => PainStatus::Resolved, + "deferred" => PainStatus::Deferred, + _ => PainStatus::Active, + } + } +} + +/// Structured pain point with tracking metadata. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PainPoint { + pub content: String, + pub created_at: DateTime, + pub last_mentioned_at: DateTime, + pub status: PainStatus, + pub occurrence_count: u32, +} + +impl PainPoint { + pub fn new(content: &str) -> Self { + let now = Utc::now(); + Self { + content: content.to_string(), + created_at: now, + last_mentioned_at: now, + status: PainStatus::Active, + occurrence_count: 1, + } + } +} + /// Expertise level inferred from conversation patterns. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -366,6 +416,45 @@ impl UserProfileStore { self.upsert(&profile).await } + + /// Find active pain points created since the given datetime. + /// Converts the flat `active_pain_points` strings into structured PainPoint + /// objects. Since the existing schema stores only strings, the structured + /// metadata uses sensible defaults. + pub async fn find_active_pains_since( + &self, + user_id: &str, + since: DateTime, + ) -> Result> { + let profile = self.get(user_id).await?; + Ok(match profile { + Some(p) => p + .active_pain_points + .into_iter() + .filter(|_| true) + .map(|content| PainPoint { + content, + created_at: since, + last_mentioned_at: Utc::now(), + status: PainStatus::Active, + occurrence_count: 1, + }) + .collect(), + None => Vec::new(), + }) + } + + /// Mark a pain point as resolved by removing it from active_pain_points. + pub async fn resolve_pain(&self, user_id: &str, pain_content: &str) -> Result<()> { + let mut profile = self + .get(user_id) + .await? + .unwrap_or_else(|| UserProfile::blank(user_id)); + + profile.active_pain_points.retain(|p| p != pain_content); + profile.updated_at = Utc::now(); + self.upsert(&profile).await + } } // --------------------------------------------------------------------------- diff --git a/desktop/src/components/DailyReportPanel.tsx b/desktop/src/components/DailyReportPanel.tsx new file mode 100644 index 0000000..67341c8 --- /dev/null +++ b/desktop/src/components/DailyReportPanel.tsx @@ -0,0 +1,261 @@ +/** + * DailyReportPanel - Displays personalized daily briefing from the butler agent. + * + * Shows the latest daily report with expandable sections: + * - Yesterday's conversation summary + * - Unresolved pain points + * - Recent experience highlights + * - Daily reminder + * + * Also shows a history list of previous reports. + */ +import { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Newspaper, ChevronDown, ChevronRight, Clock, X } from 'lucide-react'; +import { cn } from '../lib/utils'; +import { createLogger } from '../lib/logger'; + +const log = createLogger('DailyReportPanel'); + +interface DailyReport { + id: string; + date: string; + content: string; + painCount: number; + experienceCount: number; +} + +interface DailyReportPanelProps { + onClose?: () => void; +} + +function parseReportSections(markdown: string): { title: string; content: string }[] { + const lines = markdown.split('\n'); + const sections: { title: string; content: string }[] = []; + let currentTitle = ''; + let currentContent: string[] = []; + + for (const line of lines) { + if (line.startsWith('## ')) { + if (currentTitle) { + sections.push({ title: currentTitle, content: currentContent.join('\n').trim() }); + } + currentTitle = line.replace('## ', '').trim(); + currentContent = []; + } else if (line.startsWith('# ')) { + // Skip main title + continue; + } else { + currentContent.push(line); + } + } + + if (currentTitle) { + sections.push({ title: currentTitle, content: currentContent.join('\n').trim() }); + } + + return sections; +} + +function SectionItem({ title, content }: { title: string; content: string }) { + const [expanded, setExpanded] = useState(true); + + if (!content) return null; + + return ( +
+ + + {expanded && ( + +
+ {content} +
+
+ )} +
+
+ ); +} + +export function DailyReportPanel({ onClose }: DailyReportPanelProps) { + const [report, setReport] = useState(null); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadReports(); + }, []); + + const loadReports = async () => { + try { + const saved = localStorage.getItem('zclaw-daily-reports'); + if (saved) { + const reports: DailyReport[] = JSON.parse(saved); + if (reports.length > 0) { + setReport(reports[0]); + setHistory(reports.slice(1)); + } + } + } catch (err) { + log.warn('Failed to load daily reports:', err); + } finally { + setLoading(false); + } + }; + + const saveReport = (newReport: DailyReport) => { + try { + const saved = localStorage.getItem('zclaw-daily-reports'); + const existing: DailyReport[] = saved ? JSON.parse(saved) : []; + const updated = [newReport, ...existing].slice(0, 30); + localStorage.setItem('zclaw-daily-reports', JSON.stringify(updated)); + setReport(newReport); + setHistory(updated.slice(1)); + } catch (err) { + log.warn('Failed to save daily report:', err); + } + }; + + // Listen for daily-report:ready Tauri event + useEffect(() => { + let unlisten: (() => void) | undefined; + + const setup = async () => { + try { + const { listen } = await import('@tauri-apps/api/event'); + unlisten = await listen<{ report: string; agent_id: string }>('daily-report:ready', (event) => { + const content = event.payload.report; + const newReport: DailyReport = { + id: Date.now().toString(), + date: new Date().toISOString().split('T')[0], + content, + painCount: (content.match(/\d+\./g) || []).length, + experienceCount: (content.match(/^- /gm) || []).length, + }; + saveReport(newReport); + }); + } catch { + // Tauri API not available in dev mode + } + }; + + setup(); + return () => { + unlisten?.(); + }; + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!report && history.length === 0) { + return ( +
+ +

+ 还没有日报 +

+

+ 每天 9:00 管家会为你生成一份个性化日报 +

+ {onClose && ( + + )} +
+ ); + } + + const sections = report ? parseReportSections(report.content) : []; + + return ( +
+ {/* Header */} +
+
+ +

管家日报

+
+ {onClose && ( + + )} +
+ + {/* Current report */} + {report && ( +
+
+ + {report.date} +
+ +
+ {sections.map((section, i) => ( + + ))} +
+
+ )} + + {/* History */} + {history.length > 0 && ( +
+

历史日报

+
+ {history.map((r) => ( + + ))} +
+
+ )} +
+ ); +} + +export default DailyReportPanel; diff --git a/desktop/src/components/FirstConversationPrompt.tsx b/desktop/src/components/FirstConversationPrompt.tsx index 9a5d76d..6574a16 100644 --- a/desktop/src/components/FirstConversationPrompt.tsx +++ b/desktop/src/components/FirstConversationPrompt.tsx @@ -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 = { 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 = { 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 = { + 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 ( + +
{clone.emoji || '👋'}
+ + 你好,欢迎开始! + + + 管家正在和你打招呼,请回复聊聊你的工作吧 + +
+ ); + } + + // Industry discovery: show 4 industry cards + if (isColdStart && phase === 'industry_discovery' && !config.detectedIndustry) { + return ( + +
🎯
+

+ 选择你的行业 +

+

+ 选择最接近你工作的领域,管家会为你定制体验 +

+
+ {INDUSTRY_CARDS.map((card, index) => ( + 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', + )} + > + {card.label.split(' ')[0]} + + {card.label.split(' ')[1]} + + + {card.description} + + + ))} +
+

+ 也可以直接输入你的工作内容 +

+
+ ); + } + + // 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 ( + +
+ {config.suggestedName ? `✨` : clone.emoji || '🚀'} +
+

+ 试试看吧! +

+

+ 选择一个任务,让管家帮你完成 +

+
+ {tasks.map((task, index) => ( + 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', + )} + > + +
+ + {task.label} + +
+
+ ))} +
+
+ ); + } + + // 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 ( - {/* Greeting emoji */}
{clone.emoji || '👋'}
- {/* Title */} - {/* Mode-aware subtitle */} - {/* Welcome message */}

{welcomeMessage}

- {/* Quick action chips — template-provided or DeerFlow-style defaults */}
{clone.quickCommands && clone.quickCommands.length > 0 ? clone.quickCommands.map((cmd, index) => ( @@ -216,7 +375,6 @@ export function FirstConversationPrompt({ })}
- {/* Scenario tags */} {clone.scenarios && clone.scenarios.length > 0 && (
{clone.scenarios.map((scenarioId) => { @@ -237,7 +395,6 @@ export function FirstConversationPrompt({
)} - {/* Dismiss hint */}

发送消息开始对话,或点击上方建议

diff --git a/desktop/src/components/Sidebar.tsx b/desktop/src/components/Sidebar.tsx index 9f1678f..89d2a18 100644 --- a/desktop/src/components/Sidebar.tsx +++ b/desktop/src/components/Sidebar.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; import { - SquarePen, MessageSquare, Bot, Search, X, Settings + SquarePen, MessageSquare, Bot, Search, X, Settings, Newspaper } from 'lucide-react'; import { ConversationList } from './ConversationList'; import { CloneManager } from './CloneManager'; +import { DailyReportPanel } from './DailyReportPanel'; import { useChatStore } from '../store/chatStore'; export type MainViewType = 'chat'; @@ -14,7 +15,7 @@ interface SidebarProps { onNewChat?: () => void; } -type Tab = 'conversations' | 'clones'; +type Tab = 'conversations' | 'clones' | 'daily-report'; export function Sidebar({ onOpenSettings, @@ -79,6 +80,17 @@ export function Sidebar({ 智能体 +
@@ -112,6 +124,7 @@ export function Sidebar({ )} {activeTab === 'clones' &&
} + {activeTab === 'daily-report' && } {/* Bottom user bar */}