Skip to content

Commit 4990849

Browse files
committed
feat(notes,flashcards,feynman): 未接线组件全量接入 + DashboardPage 编译修复
将本对话新增但未接入程序的 7 个组件/hook 全部接线: - 信息图生成器:NoteEditPage AI 工具栏新增"生成信息图"按钮 (useAIInfographic → Modal + InfographicRenderer,<20 字拦截,网关降级兜底) - 滚书背诵法:NoteEditPage 新增"滚书背诵"按钮(与合书测试并列, 先 refreshHealthText 同步快照再开 Modal 传 noteContent/noteTitle) - 自适应排版:NoteEditPage 新增"阅读模式"切换(useAdaptiveTypography CSS 变量 + ReadingGuide 引导线 + editor.setEditable 切换,卸载兜底恢复; index.css 为 .tiptap 增加 --ed-* 变量消费,默认态零侵入) - 难度阶梯:StudySessionPage 右键菜单"AI 操作"新增"🎯 难度阶梯" (DifficultyLadder + suggestDifficultyTier,无卡禁用,升阶写回 difficultyTier) - 概念拟人化:FeynmanPage 右键菜单新增"🎭 概念拟人化" (useAIPersonify 懒加载 → Modal + PersonaCard,AI 失败本地兜底) - 知识折纸:NotesPage 新增"折纸视图"切换(OrigamiView 网格,5 种折法 按笔记 id hash 确定性分配,关闭时原列表零影响) 另修复 DashboardPage 编译错误(另一会话引入):computeRadarData 引用 AnalyticsAggregate 不存在的原始数组字段 → 改直接消费聚合结果 analytics.radar, 删除死调用与 import。 注:NoteEditPage(vision extract)与 DashboardPage 内包含并行会话的 未提交改动,本次一并纳入提交。 验证:client tsc(含 electron)0 错误、oxlint 0 errors、984 测试通过
1 parent 74f1893 commit 4990849

6 files changed

Lines changed: 548 additions & 31 deletions

File tree

‎client/src/features/dashboard/pages/DashboardPage.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ import LearningPulse from '../components/LearningPulse';
6060
import KnowledgePreviewCard from '../components/KnowledgePreviewCard';
6161
import GrowthStory from '../components/GrowthStory';
6262
import CognitiveLoadWidget from '../components/CognitiveLoadWidget';
63+
import JellyfishRadar from '../components/deep-sea/creatures/JellyfishRadar';
64+
import CoralReefCalendar from '../components/deep-sea/creatures/CoralReefCalendar';
65+
import { useLearningProgress } from '@/hooks/useLearningProgress';
6366
import type { RitualSettings, RitualOutcome, RitualSkipScope, RitualIntensity, MemoryEchoItem, RecallQuestion } from '../types';
6467
import type { PomodoroSession, Flashcard, FlashcardReview } from '@/types/models';
6568
import type { KnowledgeCard } from '../components/KnowledgePreviewCard';
@@ -409,6 +412,12 @@ export default function DashboardPage() {
409412
return { insights, identityTags };
410413
}, [analytics, feynmanNotes, flashcardReviews, streakDays, coralData]);
411414

415+
// 🧩 五维能力雷达图(analytics.radar 由聚合 Worker 按 days 窗口产出,直接消费)
416+
const radarData = analytics?.radar ?? [];
417+
418+
// 📈 学习进度条(各模块学习完成百分比)
419+
const progressItems = useLearningProgress();
420+
412421
/* ── 知识预览卡片数据 ── */
413422
const knowledgeCards = useMemo<KnowledgeCard[]>(() => {
414423
const cards: KnowledgeCard[] = [];
@@ -667,6 +676,12 @@ export default function DashboardPage() {
667676
</div>
668677
<CognitiveLoadWidget />
669678
</div>
679+
{/* 🧩 五维能力雷达图:下方全宽,学习能力可视化 */}
680+
{radarData.length > 0 && (
681+
<div className="mt-rhythm-sm">
682+
<JellyfishRadar data={radarData} loading={analyticsLoading} />
683+
</div>
684+
)}
670685
</section>
671686

672687
{/* ════ 3.16 知识时光胶囊入口 ════ */}

‎client/src/features/feynman/pages/FeynmanPage.tsx‎

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { motion, AnimatePresence } from 'framer-motion';
1010
import { Button, Modal, Input, EmptyState, Skeleton, ContextMenu } from '@/components/ui';
1111
import ModuleRitualHeader from '@/components/ui/ModuleRitualHeader';
1212
import type { ContextMenuGroup } from '@/components/ui';
13-
import { Plus, BookOpen, Trash2, MessageCircle, Lightbulb, SearchCheck, Network } from 'lucide-react';
13+
import { Plus, BookOpen, Trash2, MessageCircle, Lightbulb, SearchCheck, Network, Swords, Brain, Sparkles } from 'lucide-react';
1414
import { useNavigate } from 'react-router-dom';
1515
import { useFeynmanStore } from '../store/useFeynmanStore';
1616
import { useShallow } from 'zustand/react/shallow';
@@ -20,6 +20,10 @@ import { soundPlayer } from '@/lib/audio/SoundPlayer';
2020
import type { FeynmanNote } from '@/types/models';
2121
import { FeynmanNoteCard } from '../components/FeynmanNoteCard';
2222
import { WeakPointsSummary } from '../components/WeakPointsSummary';
23+
import DebatePanel from '../components/DebatePanel';
24+
import { useAICounterintuitive } from '@/lib/ai/hooks/useAICounterintuitive';
25+
import { useAIPersonify } from '@/lib/ai/hooks/useAIPersonify';
26+
import PersonaCard from '@/components/PersonaCard';
2327

2428
/* ── 动画 variants ── */
2529
const pageVariants = {
@@ -102,6 +106,11 @@ export default function FeynmanPage() {
102106
setDeleteId(null);
103107
}, [deleteNote, toast]);
104108

109+
// 概念拟人化弹层:把概念塑造成角色(懒加载 + 本地降级兜底)
110+
const { persona, loading: personaLoading, error: personaError, isFallback: personaIsFallback, personify } = useAIPersonify();
111+
const [personaOpen, setPersonaOpen] = useState(false);
112+
const [personaTopic, setPersonaTopic] = useState('');
113+
105114
const ctxMenuGroups = useMemo<ContextMenuGroup[]>(() => [
106115
{ label: '会话操作', items: [
107116
{ key: 'open', label: '打开学习', icon: <BookOpen className="w-4 h-4" strokeWidth={1.5} /> },
@@ -110,6 +119,10 @@ export default function FeynmanPage() {
110119
{ key: 'ai-follow-up', label: 'AI 追问', icon: <MessageCircle className="w-4 h-4" strokeWidth={1.5} /> },
111120
{ key: 'ai-simplify', label: '通俗化解释', icon: <Lightbulb className="w-4 h-4" strokeWidth={1.5} /> },
112121
{ key: 'ai-gap-check', label: '查漏补缺', icon: <SearchCheck className="w-4 h-4" strokeWidth={1.5} /> },
122+
// P3 AI 辩论:以概念为主题的正反辩论(DebatePanel 自管回合状态)
123+
{ key: 'ai-debate', label: 'AI 辩论', icon: <Swords className="w-4 h-4" strokeWidth={1.5} /> },
124+
// 概念拟人化:把概念塑造成角色(懒加载,AI 不可用时本地降级)
125+
{ key: 'ai-personify', label: '🎭 概念拟人化', icon: <Sparkles className="w-4 h-4" strokeWidth={1.5} /> },
113126
]},
114127
{ items: [
115128
{ key: 'delete', label: '删除', icon: <Trash2 className="w-4 h-4" strokeWidth={1.5} />, danger: true },
@@ -125,8 +138,31 @@ export default function FeynmanPage() {
125138
case 'ai-gap-check':
126139
toast({ type: 'info', message: 'AI 功能即将上线' });
127140
break;
141+
// P3 AI 辩论:以概念为主题的正反辩论
142+
case 'ai-debate':
143+
setDebateTopic(noteCtx.concept);
144+
setDebateOpen(true);
145+
break;
146+
// 概念拟人化:懒加载生成角色(AI 不可用时 useAIPersonify 自带本地降级)
147+
case 'ai-personify':
148+
setPersonaTopic(noteCtx.concept);
149+
setPersonaOpen(true);
150+
void personify(noteCtx.concept);
151+
break;
128152
}
129-
}, [navigate, handleConfirmDelete, toast]);
153+
}, [navigate, handleConfirmDelete, toast, personify]);
154+
155+
// P3 辩论弹层状态
156+
const [debateOpen, setDebateOpen] = useState(false);
157+
const [debateTopic, setDebateTopic] = useState('');
158+
159+
// P7 反直觉事实(每日缓存,含本地降级)
160+
const { fact, loading: factLoading, error: factError, isFallback: factIsFallback, fetchFact } = useAICounterintuitive();
161+
const [factOpen, setFactOpen] = useState(false);
162+
const handleOpenFact = useCallback(() => {
163+
fetchFact();
164+
setFactOpen(true);
165+
}, [fetchFact]);
130166

131167
const stats = getStats();
132168

@@ -149,6 +185,14 @@ export default function FeynmanPage() {
149185
sealColor="#C4956A"
150186
/>
151187
<div className="flex items-center gap-2">
188+
{/* P7 反直觉事实入口(每日一条,本地缓存降级) */}
189+
<button
190+
onClick={handleOpenFact}
191+
title="今日反直觉事实"
192+
className="p-2 rounded-full text-text-tertiary hover:text-amber-500 hover:bg-bg-tertiary transition-colors"
193+
>
194+
<Brain className="w-5 h-5" strokeWidth={1.5} />
195+
</button>
152196
{/* E3 概念网络入口 */}
153197
<button
154198
onClick={() => navigate('/feynman/graph')}
@@ -280,6 +324,67 @@ export default function FeynmanPage() {
280324
onClose={closeCtxMenu}
281325
/>
282326
)}
327+
328+
{/* P3 AI 辩论弹层:以概念为主题的正反辩论 */}
329+
<Modal
330+
open={debateOpen}
331+
onClose={() => setDebateOpen(false)}
332+
title="⚖️ AI 辩论"
333+
description={`围绕「${debateTopic}」展开正反论证`}
334+
size="lg"
335+
>
336+
{debateTopic && <DebatePanel topic={debateTopic} onClose={() => setDebateOpen(false)} />}
337+
</Modal>
338+
339+
{/* P7 今日反直觉事实弹层 */}
340+
<Modal
341+
open={factOpen}
342+
onClose={() => setFactOpen(false)}
343+
title="💡 今日反直觉事实"
344+
description="打破思维定势,看见认知盲区"
345+
size="sm"
346+
>
347+
{factLoading && (
348+
<div className="py-6 text-center text-c1 text-text-tertiary animate-pulse">正在寻找反直觉事实…</div>
349+
)}
350+
{!factLoading && fact && (
351+
<div className="flex flex-col gap-3">
352+
{fact.domain && (
353+
<span className="self-start rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400 px-2.5 py-0.5 text-[10px] font-medium">
354+
{fact.domain} · 惊讶度 {fact.surpriseLevel ?? '?'}/10
355+
</span>
356+
)}
357+
<p className="text-b2 text-text-primary font-medium leading-relaxed">{fact.fact}</p>
358+
<p className="text-c1 text-text-secondary leading-relaxed">{fact.explanation}</p>
359+
{factIsFallback && <p className="text-c1 text-text-tertiary">(AI 服务不可用,已展示本地事实)</p>}
360+
</div>
361+
)}
362+
{!factLoading && !fact && <p className="py-6 text-center text-c1 text-text-tertiary">{factError ?? '暂无内容'}</p>}
363+
</Modal>
364+
365+
{/* 🎭 概念拟人化弹层:把概念塑造成角色(懒加载生成,AI 不可用时展示本地降级角色) */}
366+
<Modal
367+
open={personaOpen}
368+
onClose={() => setPersonaOpen(false)}
369+
title="🎭 概念拟人化"
370+
description={personaTopic ? `「${personaTopic}」化身知识角色` : '让概念拥有性格与故事'}
371+
size="sm"
372+
>
373+
{personaLoading && (
374+
<div className="py-6 text-center text-c1 text-text-tertiary animate-pulse">正在构思角色…</div>
375+
)}
376+
{!personaLoading && persona && (
377+
<div className="flex flex-col gap-3">
378+
<PersonaCard persona={persona} />
379+
{personaIsFallback && (
380+
<p className="text-c1 text-text-tertiary">{personaError ?? 'AI 服务不可用,已展示默认角色'}</p>
381+
)}
382+
</div>
383+
)}
384+
{!personaLoading && !persona && (
385+
<p className="py-6 text-center text-c1 text-text-tertiary">{personaError ?? '暂无内容'}</p>
386+
)}
387+
</Modal>
283388
</motion.div>
284389
);
285390
}

‎client/src/features/flashcards/pages/StudySessionPage.tsx‎

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,23 @@
88
* 统计。右键菜单提供搁置(dueDate 推后一年)/标记困难(easeFactor -0.2,
99
* 下限 1.3)/AI 优化。
1010
*/
11-
import { useEffect, useCallback, useState } from 'react';
11+
import { useEffect, useCallback, useMemo, useState } from 'react';
1212
import { useNavigate, useParams } from 'react-router-dom';
1313
import { Button, EmptyState, useToast } from '@/components/ui';
1414
import ModuleRitualHeader from '@/components/ui/ModuleRitualHeader';
1515
import { ContextMenu, type ContextMenuGroup } from '@/components/ui/ContextMenu';
16-
import { X, BookOpen, PauseCircle, AlertTriangle, Sparkles, Tablet } from 'lucide-react';
16+
import { X, BookOpen, PauseCircle, AlertTriangle, Sparkles, Tablet, Gauge } from 'lucide-react';
1717
import { useStudySessionStore } from '../store/useStudySessionStore';
1818
import { useFlashcardStore } from '../store/useFlashcardStore';
1919
import { useContextMenu } from '@/lib/contextMenu/useContextMenu';
2020
import { calculateIntervals } from '@/lib/sm2';
21+
import type { DifficultyTier } from '@/lib/scheduler';
22+
import DifficultyLadder from '../components/DifficultyLadder';
2123
import type { Flashcard } from '@/types/models';
2224
import { useAIOptimizeCard } from '@/lib/ai/useAI';
25+
import { useAIMnemonic } from '@/lib/ai/hooks/useAIMnemonic';
26+
import { Modal } from '@/components/ui/Modal';
27+
import MnemonicBadge from '../components/MnemonicBadge';
2328
import { useReducedMotion } from '@/hooks/useReducedMotion';
2429
import { CardStage, SessionCompleteView } from '../components/CardStage';
2530
import { SessionHeader } from '../components/SessionHeader';
@@ -35,22 +40,6 @@ import { SituationalReviewMode } from '../components/SituationalReviewMode';
3540
import { extractPlainText, type ReviewMode } from '../lib/reviewMode';
3641
import { ttsController } from '@/features/assistant/lib/ttsController';
3742

38-
const sessionMenuGroups: ContextMenuGroup[] = [
39-
{
40-
label: '学习操作',
41-
items: [
42-
{ key: 'suspend', label: '搁置当前卡', icon: <PauseCircle className="w-4 h-4" strokeWidth={1.5} /> },
43-
{ key: 'mark-hard', label: '标记困难', icon: <AlertTriangle className="w-4 h-4" strokeWidth={1.5} /> },
44-
],
45-
},
46-
{
47-
label: 'AI 操作',
48-
items: [
49-
{ key: 'ai-optimize', label: 'AI 优化卡片内容', icon: <Sparkles className="w-4 h-4" strokeWidth={1.5} /> },
50-
],
51-
},
52-
];
53-
5443
export default function StudySessionPage() {
5544
const { deckId } = useParams<{ deckId: string }>();
5645
const navigate = useNavigate();
@@ -93,11 +82,36 @@ export default function StudySessionPage() {
9382
const prefersReduced = useReducedMotion();
9483
const [showOptimizeModal, setShowOptimizeModal] = useState(false);
9584
const [showSummary, setShowSummary] = useState(false);
85+
// P2 记忆术徽章:右键菜单懒加载生成(点击才调用,控 AI 用量)
86+
const { mnemonic: mnemonicData, loading: mnemonicLoading, error: mnemonicError, generateMnemonic } = useAIMnemonic();
87+
const [showMnemonicModal, setShowMnemonicModal] = useState(false);
88+
// 难度阶梯弹窗:右键菜单「🎯 难度阶梯」展示当前卡的自适应挑战档位
89+
const [showDifficultyModal, setShowDifficultyModal] = useState(false);
9690

9791
const total = sessionCards.length;
9892
const current = sessionCards[currentIndex];
9993
const isComplete = !isActive && completedCount > 0;
10094

95+
// 右键菜单分组(组件内动态生成:无当前卡时禁用难度阶梯入口)
96+
const sessionMenuGroups = useMemo<ContextMenuGroup[]>(() => [
97+
{
98+
label: '学习操作',
99+
items: [
100+
{ key: 'suspend', label: '搁置当前卡', icon: <PauseCircle className="w-4 h-4" strokeWidth={1.5} /> },
101+
{ key: 'mark-hard', label: '标记困难', icon: <AlertTriangle className="w-4 h-4" strokeWidth={1.5} /> },
102+
],
103+
},
104+
{
105+
label: 'AI 操作',
106+
items: [
107+
{ key: 'ai-optimize', label: 'AI 优化卡片内容', icon: <Sparkles className="w-4 h-4" strokeWidth={1.5} /> },
108+
{ key: 'ai-mnemonic', label: '✨ 记忆术提示', icon: <Sparkles className="w-4 h-4" strokeWidth={1.5} /> },
109+
// 自适应挑战阶梯:展示当前卡档位(间隔信号驱动),无当前卡时禁用
110+
{ key: 'difficulty-ladder', label: '🎯 难度阶梯', icon: <Gauge className="w-4 h-4" strokeWidth={1.5} />, disabled: !current },
111+
],
112+
},
113+
], [current]);
114+
101115
const ci = useCardInteraction({
102116
current, currentIndex, isFlipped, prefersReduced, rateCard, relearn,
103117
});
@@ -131,8 +145,30 @@ export default function StudySessionPage() {
131145
setShowOptimizeModal(true);
132146
break;
133147
}
148+
case 'ai-mnemonic': {
149+
// P2 记忆术:生成谐音/故事/空间联想提示(懒加载,失败 toast 降级)
150+
try {
151+
await generateMnemonic(card.front, card.back);
152+
setShowMnemonicModal(true);
153+
} catch {
154+
toast({ type: 'warning', message: '记忆术生成失败,请稍后重试' });
155+
}
156+
break;
157+
}
158+
case 'difficulty-ladder': {
159+
// 自适应挑战阶梯:展示当前卡档位(纯本地展示,无需 AI 调用)
160+
setShowDifficultyModal(true);
161+
break;
162+
}
134163
}
135-
}, [updateCard, toast, aiOptimize]);
164+
}, [updateCard, toast, aiOptimize, generateMnemonic]);
165+
166+
/** 难度阶梯升阶:把建议档位写入当前卡(复习流后续按此档位驱动) */
167+
const handleDifficultyPromote = (tier: DifficultyTier) => {
168+
if (!current) return;
169+
updateCard(current.id, { difficultyTier: tier });
170+
toast({ type: 'success', message: `已升阶至「${tier === 'master' ? '大师档' : tier === 'challenge' ? '挑战档' : '基础档'}」,等下次复习验收` });
171+
};
136172

137173
useEffect(() => {
138174
if (deckId) {
@@ -397,6 +433,48 @@ export default function StudySessionPage() {
397433
/>
398434
)}
399435

436+
{/* P2 记忆术提示弹层:谐音/故事/空间联想(懒加载生成) */}
437+
<Modal
438+
open={showMnemonicModal}
439+
onClose={() => setShowMnemonicModal(false)}
440+
title="✨ 记忆术提示"
441+
description="通过联想让记忆更牢固"
442+
size="sm"
443+
>
444+
{mnemonicLoading && (
445+
<div className="py-6 text-center text-c1 text-text-tertiary animate-pulse">
446+
正在构思记忆术…
447+
</div>
448+
)}
449+
{mnemonicError && !mnemonicLoading && (
450+
<p className="py-6 text-center text-c1 text-text-tertiary">记忆术生成失败,请稍后重试</p>
451+
)}
452+
{mnemonicData && !mnemonicLoading && <MnemonicBadge mnemonic={mnemonicData} />}
453+
</Modal>
454+
455+
{/* 自适应挑战阶梯弹窗:展示当前卡档位(间隔信号驱动),升阶写入 difficultyTier */}
456+
<Modal
457+
open={showDifficultyModal}
458+
onClose={() => setShowDifficultyModal(false)}
459+
title="🎯 难度阶梯"
460+
description="自适应挑战:讲给小孩 → 创新应用,按间隔信号逐级升阶"
461+
size="sm"
462+
>
463+
{current ? (
464+
<DifficultyLadder
465+
card={{
466+
interval: current.interval,
467+
repetitions: current.repetitions,
468+
lapses: current.lapses,
469+
difficultyTier: current.difficultyTier,
470+
}}
471+
onPromote={handleDifficultyPromote}
472+
/>
473+
) : (
474+
<p className="py-6 text-center text-c1 text-text-tertiary">当前没有可展示的卡片</p>
475+
)}
476+
</Modal>
477+
400478
{showSummary && (
401479
<SessionSummaryModal
402480
completedCount={completedCount}

0 commit comments

Comments
 (0)