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
- FirstConversationPrompt: 新用户显示"欢迎开始!",老用户"欢迎回来!" - use-cold-start: 冷启动问候语改为通用语言,去掉政务场景特定文案 - LoginPage: 添加"忘记密码?请联系管理员重置"提示 - connectionStore: 错误提示改为用户友好的"暂时没有可用的 AI 模型"
207 lines
5.8 KiB
TypeScript
207 lines
5.8 KiB
TypeScript
/**
|
||
* useColdStart - Cold start state management hook
|
||
*
|
||
* Detects first-time users and manages the cold start greeting flow.
|
||
* Reuses the onboarding completion key to determine if user is new.
|
||
*
|
||
* Flow: idle -> greeting_sent -> waiting_response -> completed
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { createLogger } from './logger';
|
||
|
||
const log = createLogger('useColdStart');
|
||
|
||
// Reuse the same key from use-onboarding.ts
|
||
const ONBOARDING_COMPLETED_KEY = 'zclaw-onboarding-completed';
|
||
|
||
// Cold start state persisted to localStorage
|
||
const COLD_START_STATE_KEY = 'zclaw-cold-start-state';
|
||
|
||
// Re-export UserProfile for consumers that need it
|
||
export type { UserProfile } from './use-onboarding';
|
||
|
||
// === Types ===
|
||
|
||
export type ColdStartPhase = 'idle' | 'greeting_sent' | 'waiting_response' | 'completed';
|
||
|
||
export interface ColdStartState {
|
||
isColdStart: boolean;
|
||
phase: ColdStartPhase;
|
||
greetingSent: boolean;
|
||
markGreetingSent: () => void;
|
||
markWaitingResponse: () => void;
|
||
markCompleted: () => void;
|
||
getGreetingMessage: (agentName?: string, agentEmoji?: string) => string;
|
||
}
|
||
|
||
// === Default Greeting ===
|
||
|
||
const DEFAULT_GREETING_BODY =
|
||
'我可以帮您处理写作、研究、数据分析、内容生成等各类任务。\n\n请告诉我您需要什么帮助?';
|
||
|
||
const FALLBACK_GREETING =
|
||
'您好!我是您的工作助手。我可以帮您处理写作、研究、数据分析、内容生成等各类任务。请告诉我您需要什么帮助?';
|
||
|
||
// === Persistence Helpers ===
|
||
|
||
interface PersistedColdStart {
|
||
phase: ColdStartPhase;
|
||
}
|
||
|
||
function loadPersistedPhase(): ColdStartPhase {
|
||
try {
|
||
const raw = localStorage.getItem(COLD_START_STATE_KEY);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw) as PersistedColdStart;
|
||
if (parsed && typeof parsed.phase === 'string') {
|
||
return parsed.phase;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
log.warn('Failed to read cold start state:', err);
|
||
}
|
||
return 'idle';
|
||
}
|
||
|
||
function persistPhase(phase: ColdStartPhase): void {
|
||
try {
|
||
const data: PersistedColdStart = { phase };
|
||
localStorage.setItem(COLD_START_STATE_KEY, JSON.stringify(data));
|
||
} catch (err) {
|
||
log.warn('Failed to persist cold start state:', err);
|
||
}
|
||
}
|
||
|
||
// === Greeting Builder ===
|
||
|
||
function buildGreeting(agentName?: string, agentEmoji?: string): string {
|
||
if (!agentName) {
|
||
return FALLBACK_GREETING;
|
||
}
|
||
|
||
const emoji = agentEmoji ? ` ${agentEmoji}` : '';
|
||
return `您好!我是${agentName}${emoji}\n\n${DEFAULT_GREETING_BODY}`;
|
||
}
|
||
|
||
// === Hook ===
|
||
|
||
/**
|
||
* Hook to manage cold start state for first-time users.
|
||
*
|
||
* A user is considered "cold start" when they have not completed onboarding
|
||
* AND have not yet gone through the greeting flow.
|
||
*
|
||
* Usage:
|
||
* ```tsx
|
||
* const { isColdStart, phase, markGreetingSent, getGreetingMessage } = useColdStart();
|
||
*
|
||
* if (isColdStart && phase === 'idle') {
|
||
* const msg = getGreetingMessage(agent.name, agent.emoji);
|
||
* sendMessage(msg);
|
||
* markGreetingSent();
|
||
* }
|
||
* ```
|
||
*/
|
||
export function useColdStart(): ColdStartState {
|
||
const [phase, setPhase] = useState<ColdStartPhase>(loadPersistedPhase);
|
||
const [isColdStart, setIsColdStart] = useState(false);
|
||
|
||
// Determine cold start status on mount
|
||
useEffect(() => {
|
||
try {
|
||
const onboardingCompleted = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
|
||
const isNewUser = onboardingCompleted !== 'true';
|
||
|
||
if (isNewUser && phase !== 'completed') {
|
||
setIsColdStart(true);
|
||
} else {
|
||
setIsColdStart(false);
|
||
// If onboarding is completed but phase is not completed,
|
||
// force phase to completed to avoid stuck states
|
||
if (phase !== 'completed') {
|
||
setPhase('completed');
|
||
persistPhase('completed');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
log.warn('Failed to check cold start status:', err);
|
||
setIsColdStart(false);
|
||
}
|
||
}, [phase]);
|
||
|
||
const markGreetingSent = useCallback(() => {
|
||
const nextPhase: ColdStartPhase = 'greeting_sent';
|
||
setPhase(nextPhase);
|
||
persistPhase(nextPhase);
|
||
log.debug('Cold start: greeting sent');
|
||
}, []);
|
||
|
||
const markWaitingResponse = useCallback(() => {
|
||
const nextPhase: ColdStartPhase = 'waiting_response';
|
||
setPhase(nextPhase);
|
||
persistPhase(nextPhase);
|
||
log.debug('Cold start: waiting for user response');
|
||
}, []);
|
||
|
||
const markCompleted = useCallback(() => {
|
||
const nextPhase: ColdStartPhase = 'completed';
|
||
setPhase(nextPhase);
|
||
persistPhase(nextPhase);
|
||
setIsColdStart(false);
|
||
log.debug('Cold start: completed');
|
||
}, []);
|
||
|
||
const getGreetingMessage = useCallback(
|
||
(agentName?: string, agentEmoji?: string): string => {
|
||
return buildGreeting(agentName, agentEmoji);
|
||
},
|
||
[],
|
||
);
|
||
|
||
return {
|
||
isColdStart,
|
||
phase,
|
||
greetingSent: phase === 'greeting_sent' || phase === 'waiting_response' || phase === 'completed',
|
||
markGreetingSent,
|
||
markWaitingResponse,
|
||
markCompleted,
|
||
getGreetingMessage,
|
||
};
|
||
}
|
||
|
||
// === Non-hook Accessor ===
|
||
|
||
/**
|
||
* Get cold start state without React hook (for use outside components).
|
||
*/
|
||
export function getColdStartState(): { isColdStart: boolean; phase: ColdStartPhase } {
|
||
try {
|
||
const onboardingCompleted = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
|
||
const isNewUser = onboardingCompleted !== 'true';
|
||
const phase = loadPersistedPhase();
|
||
|
||
return {
|
||
isColdStart: isNewUser && phase !== 'completed',
|
||
phase,
|
||
};
|
||
} catch (err) {
|
||
log.warn('Failed to get cold start state:', err);
|
||
return { isColdStart: false, phase: 'completed' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Reset cold start state (for testing or debugging).
|
||
*/
|
||
export function resetColdStartState(): void {
|
||
try {
|
||
localStorage.removeItem(COLD_START_STATE_KEY);
|
||
log.debug('Cold start state reset');
|
||
} catch (err) {
|
||
log.warn('Failed to reset cold start state:', err);
|
||
}
|
||
}
|
||
|
||
export default useColdStart;
|