Skip to content

Commit 5e43dd8

Browse files
committed
fix(misc): 杂项修复与文档更新
- PomodoroPage: 番茄钟页面优化 - PlannerPanel: 规划面板调整 - SocraticDialogue/hooks: 苏格拉底对话体验优化 - Notes: AnalysisPreview/NoteTagFilter 修复 - 3D: OrbitalStore/SpatialNav/AuroraModuleEntity/MobileNavGrid 微调 - useAIFlashcards: AI 闪卡优化 - captureManager/types: 视觉模式注入 + audioTextRaw 保真字段 - docs: README 更新索引,临时收入方案标记已过时 - 其他: social/onboarding 模块微调
1 parent 400e650 commit 5e43dd8

20 files changed

Lines changed: 294 additions & 34 deletions

File tree

‎client/src/features/feynman/components/SocraticDialogue.tsx‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ interface SocraticDialogueProps {
2424
loading: boolean;
2525
/** 当所有轮次完成时为 true */
2626
completed?: boolean;
27+
/** A 组流式接入:追问打字机渐进文本(生成中逐字展示,完成前 loading 波浪让位) */
28+
streamingQuestion?: string;
2729
}
2830

2931
function RoundBubble({ round }: { round: SocraticRound }) {
@@ -144,8 +146,32 @@ function AIThinkingWave() {
144146
);
145147
}
146148

149+
/** 追问打字机气泡:流式渐进文本 + 光标,替代思考波浪(首 chunk 到达后切换) */
150+
function StreamingQuestionBubble({ text }: { text: string }) {
151+
return (
152+
<div className="flex items-start gap-2.5 max-w-[88%]">
153+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center flex-shrink-0 mt-0.5 shadow-sm shadow-brand-500/20">
154+
<Sparkles className="w-4 h-4 text-white" strokeWidth={1.5} />
155+
</div>
156+
<div className={cn(
157+
'px-4 py-3 rounded-2xl rounded-tl-md',
158+
'bg-brand-50/80 dark:bg-brand-950/40 border border-brand-200/30 dark:border-brand-700/30',
159+
'text-b2 text-text-primary leading-relaxed',
160+
'shadow-sm',
161+
)}>
162+
{text}
163+
<motion.span
164+
className="inline-block w-[2px] h-[1.1em] bg-brand-500 align-text-bottom ml-0.5"
165+
animate={{ opacity: [1, 0, 1] }}
166+
transition={{ duration: 0.8, repeat: Infinity }}
167+
/>
168+
</div>
169+
</div>
170+
);
171+
}
172+
147173
export default function SocraticDialogue({
148-
rounds, currentRound, maxRounds, onSubmitAnswer, loading, completed,
174+
rounds, currentRound, maxRounds, onSubmitAnswer, loading, completed, streamingQuestion,
149175
}: SocraticDialogueProps) {
150176
const [answer, setAnswer] = useState('');
151177

@@ -187,8 +213,8 @@ export default function SocraticDialogue({
187213
<RoundBubble key={round.roundNumber} round={round} />
188214
))}
189215

190-
{/* AI 思考波浪动画 */}
191-
{loading && <AIThinkingWave />}
216+
{/* AI 思考波浪动画(首 chunk 到达前);之后切换为追问打字机气泡 */}
217+
{loading && (streamingQuestion ? <StreamingQuestionBubble text={streamingQuestion} /> : <AIThinkingWave />)}
192218
</div>
193219

194220
{/* 输入区(追问未完成时显示)— 底部固定风格 */}

‎client/src/features/feynman/hooks/useSocraticFlow.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ export function useSocraticFlow() {
179179
evaluateAI.evaluateAnswer(topic, currentQuestion, answer, history),
180180
currentRound >= MAX_ROUNDS
181181
? Promise.resolve(null)
182-
: questionAI.askQuestion(conversationIdRef.current, topic, history),
182+
// A 组流式接入:苏格拉底追问打字机(失败自动降级非流式 askQuestion)
183+
: questionAI.askQuestionStream(conversationIdRef.current, topic, history),
183184
]);
184185

185186
if (evalResult?.dimensions) {
@@ -305,6 +306,9 @@ export function useSocraticFlow() {
305306
brainstormLoading: brainstormAI.loading,
306307
brainstormError: brainstormAI.error,
307308
questionLoading: questionAI.loading,
309+
// A 组流式接入:追问打字机渐进文本与状态(SocraticDialogue 消费)
310+
streamingQuestion: questionAI.streamingQuestion,
311+
isQuestionStreaming: questionAI.isQuestionStreaming,
308312
// handlers
309313
handleStartBrainstorm, handleSelectIdea, handleToDialogue,
310314
handleSubmitAnswer, handleToDeepening, handleDeepeningSubmit, handleGoBack,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function SocraticSessionPage() {
3232
dialogueCompleted, savingNote, exiting,
3333
deepeningAngles, deepeningFallbackMsg, deepeningLoading,
3434
brainstormLoading, brainstormError, questionLoading,
35+
streamingQuestion,
3536
handleStartBrainstorm, handleSelectIdea, handleToDialogue,
3637
handleSubmitAnswer, handleToDeepening, handleDeepeningSubmit, handleGoBack,
3738
maxRounds,
@@ -181,6 +182,7 @@ export default function SocraticSessionPage() {
181182
onSubmitAnswer={handleSubmitAnswer}
182183
loading={questionLoading}
183184
completed={dialogueCompleted}
185+
streamingQuestion={streamingQuestion}
184186
/>
185187
</motion.div>
186188
)}

‎client/src/features/notes/components/AnalysisPreview.tsx‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ interface AnalysisPreviewProps {
3232
onInsert: (content: string) => void;
3333
onDismiss: () => void;
3434
onRetry?: () => void;
35-
/** 可选:从笔记生成闪卡 */
36-
onGenerateCards?: (content: string) => void;
35+
/** 可选:从笔记生成闪卡(P2-3:支持全量/仅重点模式) */
36+
onGenerateCards?: (content: string, mode?: 'full' | 'bookmarks') => void;
3737
/** 可选:错误态「打开设置」(P0-1;既有调用方不传则行为零变化) */
3838
onGoSettings?: () => void;
3939
}
@@ -253,16 +253,29 @@ export function AnalysisPreview({
253253
{/* 底部操作栏 */}
254254
<div className="flex items-center justify-end gap-2 px-4 py-2.5 border-t border-border/20 bg-bg-secondary/50">
255255
{onGenerateCards && (
256-
<button
257-
onClick={() => onGenerateCards(result.content)}
258-
className={cn(
259-
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-kb-md text-b3 font-medium mr-auto',
260-
'bg-flashcard-50 text-flashcard-600 hover:bg-flashcard-100 active:scale-95 transition-all duration-kb-fast',
261-
)}
262-
>
263-
<Layers className="w-3.5 h-3.5" strokeWidth={1.5} />
264-
生成闪卡
265-
</button>
256+
<>
257+
<button
258+
onClick={() => onGenerateCards(result.content)}
259+
className={cn(
260+
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-kb-md text-b3 font-medium mr-auto',
261+
'bg-flashcard-50 text-flashcard-600 hover:bg-flashcard-100 active:scale-95 transition-all duration-kb-fast',
262+
)}
263+
>
264+
<Layers className="w-3.5 h-3.5" strokeWidth={1.5} />
265+
生成闪卡
266+
</button>
267+
<button
268+
onClick={() => onGenerateCards(result.content, 'bookmarks')}
269+
className={cn(
270+
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-kb-md text-b3 font-medium',
271+
'bg-flashcard-50/50 text-flashcard-600 hover:bg-flashcard-100 active:scale-95 transition-all duration-kb-fast',
272+
)}
273+
title="仅从课中标记的重点生成闪卡"
274+
>
275+
<Layers className="w-3.5 h-3.5" strokeWidth={1.5} />
276+
仅重点
277+
</button>
278+
</>
266279
)}
267280
<button
268281
onClick={onDismiss}

‎client/src/features/notes/components/NoteTagFilter.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { motion, AnimatePresence } from 'framer-motion';
55
import { X } from 'lucide-react';
66
import { cn } from '@/lib/utils';
7+
import { useShallow } from 'zustand/react/shallow';
78
import { useNoteStore } from '../store/useNoteStore';
89
import { useReducedMotion } from '@/hooks/useReducedMotion';
910

@@ -12,7 +13,7 @@ import { useReducedMotion } from '@/hooks/useReducedMotion';
1213
* v0.9.0: chip 样式多选标签筛选
1314
*/
1415
export function NoteTagFilter() {
15-
const allTags = useNoteStore((s) => s.getAllTags());
16+
const allTags = useNoteStore(useShallow((s) => s.getAllTags()));
1617
const selectedTags = useNoteStore((s) => s.selectedTags);
1718
const toggleTag = useNoteStore((s) => s.toggleTag);
1819
const clearTagFilter = useNoteStore((s) => s.clearTagFilter);

‎client/src/features/onboarding/firstDive/moduleSubtitles.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const MODULE_SUBTITLES: Record<string, string> = {
1212
feynman: '费曼讲解',
1313
inspiration: '灵感收集',
1414
classroom: '课堂采集',
15+
constellation: '知识可视化',
1516
};
1617

1718
/** 取模块副标题;无副标题(如首页)返回 undefined */

‎client/src/features/planner/components/PlannerPanel.tsx‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,26 @@
1010
* differently from local-rule fallback plans.
1111
*/
1212
import { useNavigate } from 'react-router-dom';
13+
import { useState } from 'react';
1314
import { motion } from 'framer-motion';
14-
import { RefreshCw, Sparkles, Compass, Check } from 'lucide-react';
15+
import { RefreshCw, Sparkles, Compass, Check, CalendarRange } from 'lucide-react';
1516
import { cn } from '@/lib/utils';
1617
import { useLearningPlan } from '../hooks/useLearningPlan';
1718
import { PLAN_MODULE_META } from '../types';
19+
import { Modal, Button } from '@/components/ui';
20+
import CoachPlanView from './CoachPlanView';
21+
import { useAILearningCoach } from '@/lib/ai/hooks/useAILearningCoach';
1822

1923
export default function PlannerPanel() {
2024
const navigate = useNavigate();
2125
const { plan, loading, regenerate, toggleDone } = useLearningPlan();
26+
// P6 学习教练:AI 生成周学习计划(懒加载,失败 isFallback 展示降级文案)
27+
const { plan: coachPlan, loading: coachLoading, isFallback: coachFallback, generatePlan } = useAILearningCoach();
28+
const [coachOpen, setCoachOpen] = useState(false);
29+
const handleOpenCoach = () => {
30+
setCoachOpen(true);
31+
if (!coachPlan) void generatePlan();
32+
};
2233

2334
// 加载中:骨架占位
2435
if (loading) {
@@ -73,6 +84,15 @@ export default function PlannerPanel() {
7384
<RefreshCw className="w-3.5 h-3.5" strokeWidth={1.5} />
7485
重排
7586
</button>
87+
{/* P6 学习教练:AI 周计划入口 */}
88+
<button
89+
onClick={handleOpenCoach}
90+
className="flex items-center gap-1 rounded-lg px-2 py-1 text-[11px] text-text-tertiary transition-colors hover:text-brand-500 hover:bg-brand-500/10"
91+
aria-label="AI 教练周计划"
92+
>
93+
<CalendarRange className="w-3.5 h-3.5" strokeWidth={1.5} />
94+
AI 教练
95+
</button>
7696
</div>
7797

7898
<ul className="space-y-2">
@@ -132,6 +152,31 @@ export default function PlannerPanel() {
132152
{plan.note}
133153
</p>
134154
)}
155+
156+
{/* P6 AI 教练周计划弹层 */}
157+
<Modal
158+
open={coachOpen}
159+
onClose={() => setCoachOpen(false)}
160+
title="🧭 AI 教练周计划"
161+
description="基于本周学习数据生成下周安排"
162+
size="lg"
163+
>
164+
{coachPlan ? (
165+
<CoachPlanView plan={coachPlan} loading={coachLoading} isFallback={coachFallback} />
166+
) : (
167+
<div className="py-10 text-center text-c1 text-text-tertiary animate-pulse">
168+
{coachLoading ? 'AI 教练正在规划你的学习周…' : '暂无周计划,点击下方按钮生成'}
169+
</div>
170+
)}
171+
{coachPlan && (
172+
<div className="mt-4 flex justify-end">
173+
<Button size="sm" variant="secondary" onClick={() => void generatePlan()}>
174+
<RefreshCw className="w-3 h-3 mr-1" strokeWidth={1.5} />
175+
重新生成
176+
</Button>
177+
</div>
178+
)}
179+
</Modal>
135180
</motion.div>
136181
);
137182
}

‎client/src/features/pomodoro/pages/PomodoroPage.tsx‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import { useEffect, useState, useCallback, useRef } from 'react';
55
import { createPortal } from 'react-dom';
66
import { useNavigate } from 'react-router-dom';
77
import { AnimatePresence, motion } from 'framer-motion';
8-
import { Clock } from 'lucide-react';
8+
import { Clock, Sparkles } from 'lucide-react';
99
import { db } from '@/lib/storage';
10+
import { pomodoroSessionStore } from '@/lib/storage';
11+
import { aiPluginLoader } from '@/lib/ai/AIPluginLoader';
12+
import { useAIRecommendation } from '../hooks/useAIRecommendation';
1013
import TimerRing from '../components/TimerRing';
1114
import GoalInput from '../components/GoalInput';
1215
import 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

‎client/src/features/social/components/DeepDiveRoomView.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { DoorOpen, Timer } from 'lucide-react';
1515
import { Card, CardContent, Button } from '@/components/ui';
1616
import { cn } from '@/lib/utils';
1717
import { useAuth } from '@/lib/auth/AuthContext';
18+
import { useShallow } from 'zustand/react/shallow';
1819
import { usePomodoroStore } from '@/features/pomodoro/store/usePomodoroStore';
1920
import { CHEER_EMOJIS, type RoomStateResult } from '../hooks/useRoomState';
2021
import type { PresenceStatus, RoomPresence } from '../types';
@@ -41,12 +42,12 @@ const STATUS_LABELS: Record<PresenceStatus, string> = {
4142
export default function DeepDiveRoomView({ roomId, state, onLeave }: DeepDiveRoomViewProps) {
4243
const { room, cheerFor } = state;
4344
const { user } = useAuth();
44-
const pomodoro = usePomodoroStore((s) => ({
45+
const pomodoro = usePomodoroStore(useShallow((s) => ({
4546
phase: s.phase,
4647
isRunning: s.isRunning,
4748
remainingSeconds: s.remainingSeconds,
4849
totalSeconds: s.totalSeconds,
49-
}));
50+
})));
5051

5152
// 自己的番茄进度百分比(休息阶段显示 0,避免误导)
5253
const selfProgress = useMemo(() => {

‎client/src/features/social/components/RelayPanel.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
1313
import { Handshake, Zap, Target } from 'lucide-react';
1414
import { Card, CardContent, Button, Input, useToast } from '@/components/ui';
1515
import { cn } from '@/lib/utils';
16+
import { useShallow } from 'zustand/react/shallow';
1617
import { usePomodoroStore } from '@/features/pomodoro/store/usePomodoroStore';
1718
import {
1819
acceptPair, getCachedPair, getIncomingPairs, getRelayState, getRelayStats,
@@ -47,11 +48,11 @@ export default function RelayPanel({ offlineReason }: RelayPanelProps) {
4748
// M5: accept/reject 防双击——ref 同步守卫(state 闭包在快速双击时可能未刷新)
4849
const actionRef = useRef<string | null>(null);
4950

50-
const pomodoro = usePomodoroStore((s) => ({
51+
const pomodoro = usePomodoroStore(useShallow((s) => ({
5152
phase: s.phase,
5253
isRunning: s.isRunning,
5354
remainingSeconds: s.remainingSeconds,
54-
}));
55+
})));
5556

5657
const refresh = useCallback(async () => {
5758
const [incomingPairs, relayStats, cached] = await Promise.all([

0 commit comments

Comments
 (0)