66 * (Ctrl+Shift+H 快捷键 + 停滞计时)及讲解文本右键 AI 操作菜单。
77 * @ai -context: 接收 note 作为参数(由 useFeynmanSession 提供);反问/评估
88 * 均要求 concept+explanation 已填写,否则 toast 提示。
9+ * @ai -context: v0.30 — AI 结果持久化:评估/追问/回答评估成功后写入
10+ * feynmanAIResults 表,重新进入会话时自动恢复;“重置 AI 反馈”删除记录。
911 */
1012import { useEffect , useState , useCallback , useMemo } from 'react' ;
1113import { MessageCircle , Lightbulb , SearchCheck } from 'lucide-react' ;
@@ -14,7 +16,9 @@ import { useContextMenu } from '@/lib/contextMenu';
1416import { useAIEvaluate , useAIFeynmanQuestion , useAIFeynmanEvaluateAnswers } from '@/lib/ai/useAI' ;
1517import { useAIErrorHandler } from '@/lib/ai/hooks/useAIErrorHandler' ;
1618import { useStuckTimer } from '@/hooks/useStuckTimer' ;
17- import type { FeynmanNote } from '@/types/models' ;
19+ import { feynmanAIResultStore } from '@/lib/storage' ;
20+ import { createWithLog , updateWithLog } from '@/lib/storage/writeWithLog' ;
21+ import type { FeynmanNote , FeynmanAIResult } from '@/types/models' ;
1822
1923export function useFeynmanAI ( note : FeynmanNote | null ) {
2024 const { toast } = useToast ( ) ;
@@ -26,6 +30,7 @@ export function useFeynmanAI(note: FeynmanNote | null) {
2630 error : aiEvalError ,
2731 needsConfig : aiEvalNeedsConfig ,
2832 evaluate : aiEvaluate ,
33+ clear : clearEval ,
2934 } = useAIEvaluate ( ) ;
3035
3136 // AI 反问
@@ -35,6 +40,7 @@ export function useFeynmanAI(note: FeynmanNote | null) {
3540 error : aiQuestionError ,
3641 needsConfig : aiQuestionNeedsConfig ,
3742 generateQuestions,
43+ clear : clearQuestion ,
3844 } = useAIFeynmanQuestion ( ) ;
3945
4046 // AI 回答评估
@@ -44,6 +50,7 @@ export function useFeynmanAI(note: FeynmanNote | null) {
4450 error : aiAnswerEvalError ,
4551 needsConfig : aiAnswerEvalNeedsConfig ,
4652 evaluateAnswers : aiEvaluateAnswers ,
53+ clear : clearAnswerEval ,
4754 } = useAIFeynmanEvaluateAnswers ( ) ;
4855
4956 const handleQuestionError = useAIErrorHandler ( 'AI 追问生成失败' ) ;
@@ -54,6 +61,66 @@ export function useFeynmanAI(note: FeynmanNote | null) {
5461 const [ showQuestionPanel , setShowQuestionPanel ] = useState ( false ) ;
5562 const [ localAnswers , setLocalAnswers ] = useState < string [ ] > ( [ ] ) ;
5663
64+ // ── v0.30: AI 结果持久化 ────────────────────────────────
65+ const [ persisted , setPersisted ] = useState < FeynmanAIResult | null > ( null ) ;
66+
67+ /** 会话加载时恢复持久化的 AI 结果 */
68+ useEffect ( ( ) => {
69+ if ( ! note ?. id ) { setPersisted ( null ) ; return ; }
70+ let cancelled = false ;
71+ feynmanAIResultStore . where ( 'noteId' , note . id ) . then ( ( records ) => {
72+ if ( cancelled ) return ;
73+ const record = records [ 0 ] ?? null ;
74+ setPersisted ( record ) ;
75+ if ( record ) {
76+ if ( record . evalResult ) setShowAIEval ( true ) ;
77+ if ( record . questionData ) setShowQuestionPanel ( true ) ;
78+ if ( record . answers ?. length ) setLocalAnswers ( record . answers ) ;
79+ }
80+ } ) . catch ( ( ) => { /* 恢复失败不阻塞主流程 */ } ) ;
81+ return ( ) => { cancelled = true ; } ;
82+ } , [ note ?. id ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
83+
84+ /** 将 AI 结果增量写入持久层(upsert by noteId) */
85+ const persistAIResult = useCallback ( async ( patch : Partial < FeynmanAIResult > ) => {
86+ if ( ! note ?. id ) return ;
87+ const noteId = note . id ;
88+ try {
89+ const existing = ( await feynmanAIResultStore . where ( 'noteId' , noteId ) ) [ 0 ] ;
90+ if ( existing ) {
91+ const updated = { ...existing , ...patch , updatedAt : new Date ( ) } ;
92+ await updateWithLog ( feynmanAIResultStore , 'feynmanAIResults' , existing . id , updated ) ;
93+ setPersisted ( updated ) ;
94+ } else {
95+ const now = new Date ( ) ;
96+ const data = { noteId, ...patch , createdAt : now , updatedAt : now } ;
97+ const id = await createWithLog ( feynmanAIResultStore , 'feynmanAIResults' , data ) ;
98+ setPersisted ( { id, ...data } as FeynmanAIResult ) ;
99+ }
100+ } catch { /* 持久化失败不阻塞主流程 */ }
101+ } , [ note ?. id ] ) ;
102+
103+ /** 重置 AI 反馈:删除持久化记录 + 清空内存状态 */
104+ const resetAIResults = useCallback ( async ( ) => {
105+ if ( ! note ?. id ) return ;
106+ try {
107+ const records = await feynmanAIResultStore . where ( 'noteId' , note . id ) ;
108+ for ( const r of records ) await feynmanAIResultStore . delete ( r . id ) ;
109+ } catch { /* 忽略 */ }
110+ setPersisted ( null ) ;
111+ setShowAIEval ( false ) ;
112+ setShowQuestionPanel ( false ) ;
113+ setLocalAnswers ( [ ] ) ;
114+ clearEval ( ) ;
115+ clearQuestion ( ) ;
116+ clearAnswerEval ( ) ;
117+ toast ( { type : 'success' , message : 'AI 反馈已重置' } ) ;
118+ } , [ note ?. id , clearEval , clearQuestion , clearAnswerEval , toast ] ) ;
119+
120+ /** 是否存在 AI 结果(内存或持久层)——用于展示重置按钮 */
121+ const hasAIResults = ! ! ( aiEvalData || aiQuestionData || aiAnswerEvalData
122+ || persisted ?. evalResult || persisted ?. questionData || persisted ?. answerEvalData ) ;
123+
57124 // 卡壳救援
58125 const [ rescueOpen , setRescueOpen ] = useState ( false ) ;
59126 const stuckTimer = useStuckTimer ( {
@@ -81,8 +148,10 @@ export function useFeynmanAI(note: FeynmanNote | null) {
81148 return ;
82149 }
83150 setShowAIEval ( true ) ;
84- aiEvaluate ( note . concept , note . explanation ) . catch ( handleEvalError ) ;
85- } , [ note , toast , aiEvaluate , handleEvalError ] ) ;
151+ aiEvaluate ( note . concept , note . explanation )
152+ . then ( ( result ) => { persistAIResult ( { evalResult : result } ) ; } )
153+ . catch ( handleEvalError ) ;
154+ } , [ note , toast , aiEvaluate , handleEvalError , persistAIResult ] ) ;
86155
87156 const handleGenerateQuestions = useCallback ( ( ) => {
88157 if ( ! note ?. concept || ! note ?. explanation ) {
@@ -91,8 +160,10 @@ export function useFeynmanAI(note: FeynmanNote | null) {
91160 }
92161 setShowQuestionPanel ( true ) ;
93162 setLocalAnswers ( [ ] ) ;
94- generateQuestions ( note . concept , note . explanation ) . catch ( handleQuestionError ) ;
95- } , [ note , toast , generateQuestions , handleQuestionError ] ) ;
163+ generateQuestions ( note . concept , note . explanation )
164+ . then ( ( result ) => { persistAIResult ( { questionData : result , answers : undefined , answerEvalData : undefined } ) ; } )
165+ . catch ( handleQuestionError ) ;
166+ } , [ note , toast , generateQuestions , handleQuestionError , persistAIResult ] ) ;
96167
97168 const handleSubmitAnswers = useCallback ( async ( ) => {
98169 if ( ! note ?. concept || ! aiQuestionData ) return ;
@@ -102,8 +173,11 @@ export function useFeynmanAI(note: FeynmanNote | null) {
102173 toast ( { type : 'warning' , message : '请至少回答一个追问' } ) ;
103174 return ;
104175 }
105- await aiEvaluateAnswers ( note . concept , questions , answers ) . catch ( handleEvalError ) ;
106- } , [ note , aiQuestionData , localAnswers , toast , aiEvaluateAnswers , handleEvalError ] ) ;
176+ const evalResult = await aiEvaluateAnswers ( note . concept , questions , answers ) . catch ( handleEvalError ) ;
177+ if ( evalResult ) {
178+ persistAIResult ( { answers, answerEvalData : evalResult } ) ;
179+ }
180+ } , [ note , aiQuestionData , localAnswers , toast , aiEvaluateAnswers , handleEvalError , persistAIResult ] ) ;
107181
108182 // ── 右键 AI 操作菜单 ──
109183
@@ -171,13 +245,21 @@ export function useFeynmanAI(note: FeynmanNote | null) {
171245 return {
172246 rescueOpen, setRescueOpen, stuckTimer,
173247 showAIEval, setShowAIEval,
174- aiEvalLoading, aiEvalData, aiEvalError, aiEvalNeedsConfig, handleAIEval,
248+ aiEvalLoading,
249+ // v0.30: 内存数据优先,回退持久化记录(返回列表后重新进入可恢复)
250+ aiEvalData : aiEvalData ?? persisted ?. evalResult ?? null ,
251+ aiEvalError, aiEvalNeedsConfig, handleAIEval,
175252 showQuestionPanel, setShowQuestionPanel,
176253 localAnswers, setLocalAnswers,
177- aiQuestionLoading, aiQuestionData, aiQuestionError, aiQuestionNeedsConfig,
254+ aiQuestionLoading,
255+ aiQuestionData : aiQuestionData ?? persisted ?. questionData ?? null ,
256+ aiQuestionError, aiQuestionNeedsConfig,
178257 handleGenerateQuestions,
179- aiAnswerEvalLoading, aiAnswerEvalData, aiAnswerEvalError, aiAnswerEvalNeedsConfig,
258+ aiAnswerEvalLoading,
259+ aiAnswerEvalData : aiAnswerEvalData ?? persisted ?. answerEvalData ?? null ,
260+ aiAnswerEvalError, aiAnswerEvalNeedsConfig,
180261 handleSubmitAnswers,
262+ resetAIResults, hasAIResults,
181263 menuOpen, menuPosition, menuContext, closeMenu,
182264 aiMenuGroups, handleMenuSelect, handleNoteContextMenu,
183265 } ;
0 commit comments