@@ -5,8 +5,11 @@ import { useEffect, useState, useCallback, useRef } from 'react';
55import { createPortal } from 'react-dom' ;
66import { useNavigate } from 'react-router-dom' ;
77import { AnimatePresence , motion } from 'framer-motion' ;
8- import { Clock } from 'lucide-react' ;
8+ import { Clock , Sparkles } from 'lucide-react' ;
99import { db } from '@/lib/storage' ;
10+ import { pomodoroSessionStore } from '@/lib/storage' ;
11+ import { aiPluginLoader } from '@/lib/ai/AIPluginLoader' ;
12+ import { useAIRecommendation } from '../hooks/useAIRecommendation' ;
1013import TimerRing from '../components/TimerRing' ;
1114import GoalInput from '../components/GoalInput' ;
1215import ImmersiveTimer from '../components/ImmersiveTimer' ;
@@ -45,6 +48,29 @@ export default function PomodoroPage() {
4548 const [ goalModalOpen , setGoalModalOpen ] = useState ( false ) ;
4649 const [ rememberGoal , setRememberGoal ] = useState ( false ) ;
4750 const [ presetEditorOpen , setPresetEditorOpen ] = useState ( false ) ;
51+
52+ // AI 时长推荐(T1:本地节律引擎即时结果,AI 增强可选且 3s 超时降级)
53+ const { recommendation, loading : recLoading , error : recError , getRecommendation } = useAIRecommendation ( ) ;
54+ const [ aiEnhance , setAiEnhance ] = useState ( false ) ;
55+ const handleGetRecommendation = useCallback ( async ( ) => {
56+ try {
57+ const since = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
58+ const sessions = await pomodoroSessionStore . getTable ( )
59+ . where ( 'completedAt' ) . aboveOrEqual ( since )
60+ . toArray ( ) ;
61+ const history = {
62+ sessions : sessions . map ( ( s ) => ( {
63+ duration : Math . round ( s . duration / 60 ) ,
64+ completed : ! s . interrupted && s . actualDuration >= s . duration * 0.8 ,
65+ date : new Date ( s . completedAt ) . toISOString ( ) ,
66+ subject : s . subject || '' ,
67+ } ) ) ,
68+ } ;
69+ await getRecommendation ( history , aiEnhance ? ( data ) => aiPluginLoader . recommendDuration ( data ) : undefined ) ;
70+ } catch {
71+ // 静默失败(本地推荐失败也无妨,不打扰主界面)
72+ }
73+ } , [ getRecommendation , aiEnhance ] ) ;
4874
4975 // 设备降级级别推导(与萤火海沟/沉浸模式同源:L0 全量 / L1 低端 / L2 减弱动效)
5076 const { shouldDisableHeavyAnimations, prefersReducedMotion } = useDeviceCapability ( ) ;
@@ -234,6 +260,53 @@ export default function PomodoroPage() {
234260 { ! isRunning && ! isPaused && (
235261 < div className = "mt-2 w-full max-w-md" >
236262 < EnergySuggestionBar />
263+ { /* AI 时长推荐:本地节律即时计算,AI 增强可选(3s 超时自动降级) */ }
264+ < div className = "mt-2 rounded-kb-lg border border-border/30 bg-bg-elevated/40 p-3" >
265+ < div className = "flex items-center justify-between" >
266+ < span className = "text-c1 text-text-secondary flex items-center gap-1.5" >
267+ < Sparkles className = "w-3.5 h-3.5 text-brand-500" strokeWidth = { 1.5 } />
268+ AI 时长推荐
269+ </ span >
270+ < label className = "flex items-center gap-1.5 text-c1 text-text-tertiary cursor-pointer" >
271+ < input
272+ type = "checkbox"
273+ checked = { aiEnhance }
274+ onChange = { ( e ) => setAiEnhance ( e . target . checked ) }
275+ className = "accent-brand-500"
276+ />
277+ AI 增强
278+ </ label >
279+ </ div >
280+ { recommendation ? (
281+ < div className = "mt-2 flex items-baseline gap-2" >
282+ < span className = "text-b2 font-semibold text-text-primary" >
283+ { recommendation . recommendedDuration } 分钟
284+ </ span >
285+ { recommendation . breakMinutes ? (
286+ < span className = "text-c1 text-text-tertiary" > · 休息 { recommendation . breakMinutes } 分钟</ span >
287+ ) : null }
288+ < span className = "ml-auto text-[10px] text-text-tertiary" >
289+ { recommendation . source === 'ai' ? 'AI 增强' : '本地节律' }
290+ { recommendation . confidence === 'high' ? ' · 高置信' : recommendation . confidence === 'medium' ? ' · 中置信' : '' }
291+ </ span >
292+ </ div >
293+ ) : null }
294+ { recError && ! recommendation ? (
295+ < p className = "mt-1 text-c1 text-semantic-error" > { recError } </ p >
296+ ) : null }
297+ { ! recommendation && (
298+ < p className = "mt-1 text-c1 text-text-tertiary" >
299+ 基于近 30 天专注节律推荐时长(数据不足时回退加权平均)
300+ </ p >
301+ ) }
302+ < button
303+ onClick = { ( ) => void handleGetRecommendation ( ) }
304+ disabled = { recLoading }
305+ className = "mt-2 px-3 py-1.5 rounded-kb-sm bg-brand-500/15 text-brand-600 dark:text-brand-400 text-c1 border border-brand-500/30 transition-all active:scale-95 disabled:opacity-50"
306+ >
307+ { recLoading ? '计算中…' : recommendation ? '重新推荐' : '获取推荐' }
308+ </ button >
309+ </ div >
237310 </ div >
238311 ) }
239312
0 commit comments