Akhil-Theerthala commited on
Commit
46a757e
·
verified ·
1 Parent(s): 57a85c6

Upload 32 files

Browse files
App.tsx CHANGED
@@ -4,9 +4,18 @@ import Background from './components/Background';
4
  import BentoCard from './components/BentoCard';
5
  import ChatBot from './components/ChatBot';
6
  import BlogView from './components/BlogView';
7
- import { BentoCardData, BlogSection, ChatMessage, AppSettings, ProcessingStatus, ViewMode, PaperStructure, GeminiModel } from './types';
8
  import { generateBentoCards, expandBentoCard, chatWithDocument, analyzePaperStructure, generateAndValidateSection, MODEL_INFO } from './services/aiService';
9
 
 
 
 
 
 
 
 
 
 
10
  const App: React.FC = () => {
11
  // Settings
12
  const [settings, setSettings] = useState<AppSettings>({
@@ -14,7 +23,8 @@ const App: React.FC = () => {
14
  model: 'gemini-2.5-flash',
15
  theme: 'light',
16
  layoutMode: 'auto',
17
- useThinking: false
 
18
  });
19
 
20
  // Inputs
@@ -735,6 +745,8 @@ const App: React.FC = () => {
735
  onRetrySection={handleRetrySection}
736
  retryingSectionIndex={retryingSectionIndex}
737
  contentRef={blogRef}
 
 
738
  />
739
  )}
740
 
 
4
  import BentoCard from './components/BentoCard';
5
  import ChatBot from './components/ChatBot';
6
  import BlogView from './components/BlogView';
7
+ import { BentoCardData, BlogSection, ChatMessage, AppSettings, ProcessingStatus, ViewMode, PaperStructure, GeminiModel, ReadingPreferences } from './types';
8
  import { generateBentoCards, expandBentoCard, chatWithDocument, analyzePaperStructure, generateAndValidateSection, MODEL_INFO } from './services/aiService';
9
 
10
+ // Default reading preferences for ADHD-friendly features
11
+ const defaultReadingPreferences: ReadingPreferences = {
12
+ bionicReading: false,
13
+ eli5Mode: false,
14
+ highlightKeyTerms: true,
15
+ ttsEnabled: false,
16
+ ttsSpeed: 1.0
17
+ };
18
+
19
  const App: React.FC = () => {
20
  // Settings
21
  const [settings, setSettings] = useState<AppSettings>({
 
23
  model: 'gemini-2.5-flash',
24
  theme: 'light',
25
  layoutMode: 'auto',
26
+ useThinking: false,
27
+ readingPreferences: defaultReadingPreferences
28
  });
29
 
30
  // Inputs
 
745
  onRetrySection={handleRetrySection}
746
  retryingSectionIndex={retryingSectionIndex}
747
  contentRef={blogRef}
748
+ readingPreferences={settings.readingPreferences}
749
+ onReadingPreferencesChange={(prefs) => setSettings(prev => ({ ...prev, readingPreferences: prefs }))}
750
  />
751
  )}
752
 
components/Background.tsx CHANGED
@@ -1,6 +1,26 @@
1
-
2
  import React, { useEffect, useRef } from 'react';
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => {
5
  const canvasRef = useRef<HTMLCanvasElement>(null);
6
 
@@ -11,7 +31,69 @@ const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => {
11
  if (!ctx) return;
12
 
13
  let animationFrameId: number;
14
- let time = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const resize = () => {
17
  canvas.width = window.innerWidth;
@@ -20,90 +102,183 @@ const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => {
20
  window.addEventListener('resize', resize);
21
  resize();
22
 
23
- const points: { x: number; y: number; z: number }[] = [];
24
- const numPoints = 50; // Reduced count slightly
25
- const size = 300;
 
 
26
 
27
- // Create a cube of dots
28
- for (let i = 0; i < numPoints; i++) {
29
- points.push({
30
- x: (Math.random() - 0.5) * size,
31
- y: (Math.random() - 0.5) * size,
32
- z: (Math.random() - 0.5) * size,
33
- });
34
- }
35
 
36
- const draw = () => {
37
- // Varied, slower speed
38
- time += 0.001 + Math.sin(Date.now() * 0.0005) * 0.0005;
 
39
 
40
- ctx.clearRect(0, 0, canvas.width, canvas.height);
 
 
 
 
41
 
42
- const cx = canvas.width / 2;
43
- const cy = canvas.height / 2;
44
-
45
- // Much lower opacity for better contrast with content
46
- const color = theme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)';
47
- const connectionColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(0, 0, 0, 0.02)';
48
-
49
- ctx.fillStyle = color;
50
- ctx.strokeStyle = connectionColor;
51
-
52
- // Rotate points
53
- const rotatedPoints = points.map(p => {
54
- // Rotate Y
55
- let x = p.x * Math.cos(time) - p.z * Math.sin(time);
56
- let z = p.x * Math.sin(time) + p.z * Math.cos(time);
57
- // Rotate X
58
- let y = p.y * Math.cos(time * 0.5) - z * Math.sin(time * 0.5);
59
- z = p.y * Math.sin(time * 0.5) + z * Math.cos(time * 0.5);
60
-
61
- // Perspective projection
62
- const scale = 800 / (800 + z);
63
- return {
64
- x: cx + x * scale,
65
- y: cy + y * scale,
66
- scale
67
- };
68
  });
 
 
 
 
 
 
 
 
 
 
69
 
70
- // Draw connections
71
- ctx.beginPath();
72
- for (let i = 0; i < rotatedPoints.length; i++) {
73
- for (let j = i + 1; j < rotatedPoints.length; j++) {
74
- const dx = rotatedPoints[i].x - rotatedPoints[j].x;
75
- const dy = rotatedPoints[i].y - rotatedPoints[j].y;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  const dist = Math.sqrt(dx * dx + dy * dy);
77
- if (dist < 120) {
78
- ctx.moveTo(rotatedPoints[i].x, rotatedPoints[i].y);
79
- ctx.lineTo(rotatedPoints[j].x, rotatedPoints[j].y);
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
 
 
 
 
 
81
  }
82
  }
83
- ctx.stroke();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- // Draw points
86
- rotatedPoints.forEach(p => {
87
- ctx.beginPath();
88
- ctx.arc(p.x, p.y, 2 * p.scale, 0, Math.PI * 2);
89
- ctx.fill();
 
90
  });
91
 
92
  animationFrameId = requestAnimationFrame(draw);
93
  };
94
 
95
- draw();
96
 
97
  return () => {
98
  window.removeEventListener('resize', resize);
 
99
  cancelAnimationFrame(animationFrameId);
100
  };
101
  }, [theme]);
102
 
103
  return (
104
- <canvas
105
- ref={canvasRef}
106
- className="fixed top-0 left-0 w-full h-full -z-10 pointer-events-none transition-opacity duration-1000 opacity-20"
107
  />
108
  );
109
  };
 
 
1
  import React, { useEffect, useRef } from 'react';
2
 
3
+ interface Point {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ interface ShapeDefinition {
9
+ points: Point[];
10
+ type: 'doc' | 'brain' | 'code' | 'graph';
11
+ }
12
+
13
+ interface DocumentEntity {
14
+ id: number;
15
+ gridX: number;
16
+ gridY: number;
17
+ life: number;
18
+ state: 'forming' | 'flipping' | 'fading';
19
+ flipProgress: number;
20
+ opacity: number;
21
+ shape: ShapeDefinition;
22
+ }
23
+
24
  const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => {
25
  const canvasRef = useRef<HTMLCanvasElement>(null);
26
 
 
31
  if (!ctx) return;
32
 
33
  let animationFrameId: number;
34
+ let mouseX = -1000;
35
+ let mouseY = -1000;
36
+
37
+ // Grid configuration
38
+ const DOT_SPACING = 25; // Denser grid
39
+ const DOT_RADIUS = 1.2;
40
+
41
+ // Shape Definitions
42
+ const DOC_SHAPE: ShapeDefinition = {
43
+ type: 'doc',
44
+ points: [
45
+ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, /* corner fold */ { x: 4, y: 1 },
46
+ { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 0, y: 3 }, { x: 0, y: 4 }, { x: 0, y: 5 },
47
+ { x: 4, y: 2 }, { x: 4, y: 3 }, { x: 4, y: 4 }, { x: 4, y: 5 },
48
+ { x: 1, y: 5 }, { x: 2, y: 5 }, { x: 3, y: 5 },
49
+ { x: 3, y: 0 }, { x: 4, y: 1 },
50
+ { x: 1, y: 2 }, { x: 2, y: 2 }, { x: 3, y: 2 },
51
+ { x: 1, y: 4 }, { x: 2, y: 4 }, { x: 3, y: 4 },
52
+ ]
53
+ };
54
+
55
+ const CODE_SHAPE: ShapeDefinition = {
56
+ type: 'code',
57
+ points: [
58
+ // <
59
+ { x: 2, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 3 }, { x: 2, y: 4 },
60
+ // /
61
+ { x: 3, y: 0 }, { x: 3, y: 4 }, // Slash approximation
62
+ // >
63
+ { x: 4, y: 0 }, { x: 5, y: 1 }, { x: 6, y: 2 }, { x: 5, y: 3 }, { x: 4, y: 4 },
64
+ ]
65
+ };
66
+
67
+ const BRAIN_SHAPE: ShapeDefinition = {
68
+ type: 'brain',
69
+ points: [
70
+ { x: 2, y: 0 }, { x: 3, y: 0 },
71
+ { x: 1, y: 1 }, { x: 4, y: 1 },
72
+ { x: 0, y: 2 }, { x: 5, y: 2 },
73
+ { x: 0, y: 3 }, { x: 5, y: 3 },
74
+ { x: 1, y: 4 }, { x: 4, y: 4 },
75
+ { x: 2, y: 5 }, { x: 3, y: 5 },
76
+ // Internal connections
77
+ { x: 2, y: 2 }, { x: 3, y: 2 },
78
+ { x: 2, y: 3 }, { x: 3, y: 3 },
79
+ ]
80
+ };
81
+
82
+ const GRAPH_SHAPE: ShapeDefinition = {
83
+ type: 'graph',
84
+ points: [
85
+ { x: 0, y: 4 }, { x: 1, y: 3 }, { x: 2, y: 4 }, { x: 3, y: 2 }, { x: 4, y: 3 }, { x: 5, y: 0 },
86
+ // Nodes
87
+ { x: 0, y: 4 }, { x: 2, y: 4 }, { x: 3, y: 2 }, { x: 5, y: 0 }
88
+ ]
89
+ };
90
+
91
+ const SHAPES = [DOC_SHAPE, CODE_SHAPE, BRAIN_SHAPE, GRAPH_SHAPE];
92
+
93
+ let documents: DocumentEntity[] = [];
94
+ let nextDocId = 0;
95
+ let lastSpawnTime = 0;
96
+ let pulseTime = 0;
97
 
98
  const resize = () => {
99
  canvas.width = window.innerWidth;
 
102
  window.addEventListener('resize', resize);
103
  resize();
104
 
105
+ const handleMouseMove = (e: MouseEvent) => {
106
+ mouseX = e.clientX;
107
+ mouseY = e.clientY;
108
+ };
109
+ window.addEventListener('mousemove', handleMouseMove);
110
 
111
+ const spawnDocument = () => {
112
+ const cols = Math.ceil(canvas.width / DOT_SPACING);
113
+ const rows = Math.ceil(canvas.height / DOT_SPACING);
 
 
 
 
 
114
 
115
+ // Spawn shapes only in the edge regions (left 15% or right 15%)
116
+ // to avoid cluttering the content area
117
+ const leftZoneCols = Math.floor(cols * 0.12);
118
+ const rightZoneStart = Math.floor(cols * 0.88);
119
 
120
+ // Randomly pick left or right edge zone
121
+ const useLeftZone = Math.random() < 0.5;
122
+ const gridX = useLeftZone
123
+ ? Math.floor(Math.random() * Math.max(1, leftZoneCols - 8))
124
+ : rightZoneStart + Math.floor(Math.random() * Math.max(1, cols - rightZoneStart - 8));
125
 
126
+ const gridY = Math.floor(Math.random() * (rows - 8));
127
+ const shape = SHAPES[Math.floor(Math.random() * SHAPES.length)];
128
+
129
+ documents.push({
130
+ id: nextDocId++,
131
+ gridX,
132
+ gridY,
133
+ life: 0,
134
+ state: 'forming',
135
+ flipProgress: 0,
136
+ opacity: 0,
137
+ shape
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  });
139
+ };
140
+
141
+ const draw = (timestamp: number) => {
142
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
143
+
144
+ const cols = Math.ceil(canvas.width / DOT_SPACING);
145
+ const rows = Math.ceil(canvas.height / DOT_SPACING);
146
+
147
+ const baseDotColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)';
148
+ const activeDotColor = theme === 'dark' ? 'rgba(99, 102, 241, 0.9)' : 'rgba(79, 70, 229, 0.9)';
149
 
150
+ // Pulse wave
151
+ pulseTime += 0.05;
152
+
153
+ // Define content-safe zone (center area where text is displayed)
154
+ // Dots will fade out as they approach this zone
155
+ const contentLeft = canvas.width * 0.15; // Left 15% is safe for dots
156
+ const contentRight = canvas.width * 0.85; // Right 15% is safe for dots
157
+ const contentTop = canvas.height * 0.08; // Top 8% is safe for dots
158
+ const fadeWidth = 100; // Pixels over which dots fade out
159
+
160
+ // Draw background grid with interaction
161
+ ctx.fillStyle = baseDotColor;
162
+ for (let i = 0; i < cols; i++) {
163
+ for (let j = 0; j < rows; j++) {
164
+ const x = i * DOT_SPACING + (DOT_SPACING / 2);
165
+ const y = j * DOT_SPACING + (DOT_SPACING / 2);
166
+
167
+ // Calculate opacity based on distance from content zone
168
+ let zoneOpacity = 1;
169
+
170
+ // Fade out dots as they enter the content area
171
+ if (x > contentLeft && x < contentRight && y > contentTop) {
172
+ // Inside content zone - calculate fade based on distance from edges
173
+ const distFromLeft = x - contentLeft;
174
+ const distFromRight = contentRight - x;
175
+ const distFromTop = y - contentTop;
176
+
177
+ const minDistX = Math.min(distFromLeft, distFromRight);
178
+ const minDist = Math.min(minDistX, distFromTop);
179
+
180
+ // Fade out completely within fadeWidth pixels of entering content zone
181
+ zoneOpacity = Math.max(0, 1 - (minDist / fadeWidth));
182
+
183
+ // Skip drawing dots that are fully transparent
184
+ if (zoneOpacity < 0.01) continue;
185
+ }
186
+
187
+ // Mouse interaction
188
+ const dx = x - mouseX;
189
+ const dy = y - mouseY;
190
  const dist = Math.sqrt(dx * dx + dy * dy);
191
+
192
+ let scale = 1;
193
+ let alpha = 1;
194
+
195
+ // Mouse ripple
196
+ if (dist < 150) {
197
+ scale = 1 + (150 - dist) / 150; // Scale up to 2x
198
+ alpha = 1 + (150 - dist) / 100;
199
+ }
200
+
201
+ // Global pulse wave
202
+ const wave = Math.sin(x * 0.01 + y * 0.01 + pulseTime) * 0.5 + 0.5;
203
+ if (wave > 0.8) {
204
+ scale += 0.2;
205
+ alpha += 0.2;
206
  }
207
+
208
+ ctx.globalAlpha = Math.min(alpha, 0.5) * zoneOpacity; // Apply zone fade
209
+ ctx.beginPath();
210
+ ctx.arc(x, y, DOT_RADIUS * scale, 0, Math.PI * 2);
211
+ ctx.fill();
212
  }
213
  }
214
+ ctx.globalAlpha = 1;
215
+
216
+ // Update and draw entities
217
+ if (timestamp - lastSpawnTime > 1500) { // Slightly faster spawn
218
+ spawnDocument();
219
+ lastSpawnTime = timestamp;
220
+ }
221
+
222
+ documents = documents.filter(doc => doc.state !== 'fading' || doc.opacity > 0.01);
223
+
224
+ documents.forEach(doc => {
225
+ if (doc.state === 'forming') {
226
+ doc.opacity += 0.03;
227
+ if (doc.opacity >= 1) {
228
+ doc.opacity = 1;
229
+ doc.state = 'flipping';
230
+ }
231
+ } else if (doc.state === 'flipping') {
232
+ doc.flipProgress += 0.03;
233
+ if (doc.flipProgress >= Math.PI) {
234
+ doc.state = 'fading';
235
+ }
236
+ } else if (doc.state === 'fading') {
237
+ doc.opacity -= 0.03;
238
+ }
239
+
240
+ const centerX = doc.gridX + 3; // Approx center
241
+
242
+ ctx.fillStyle = activeDotColor;
243
+
244
+ doc.shape.points.forEach(pt => {
245
+ const gx = doc.gridX + pt.x;
246
+ const gy = doc.gridY + pt.y;
247
+
248
+ let screenX = gx * DOT_SPACING + (DOT_SPACING / 2);
249
+ const screenY = gy * DOT_SPACING + (DOT_SPACING / 2);
250
+
251
+ if (doc.state === 'flipping' || doc.state === 'fading') {
252
+ const docCenterScreenX = centerX * DOT_SPACING + (DOT_SPACING / 2);
253
+ const relativeX = screenX - docCenterScreenX;
254
+ const scaleX = Math.cos(doc.flipProgress);
255
+ screenX = docCenterScreenX + (relativeX * scaleX);
256
+ }
257
 
258
+ ctx.globalAlpha = doc.opacity;
259
+ ctx.beginPath();
260
+ ctx.arc(screenX, screenY, DOT_RADIUS * 2.8, 0, Math.PI * 2);
261
+ ctx.fill();
262
+ ctx.globalAlpha = 1.0;
263
+ });
264
  });
265
 
266
  animationFrameId = requestAnimationFrame(draw);
267
  };
268
 
269
+ animationFrameId = requestAnimationFrame(draw);
270
 
271
  return () => {
272
  window.removeEventListener('resize', resize);
273
+ window.removeEventListener('mousemove', handleMouseMove);
274
  cancelAnimationFrame(animationFrameId);
275
  };
276
  }, [theme]);
277
 
278
  return (
279
+ <canvas
280
+ ref={canvasRef}
281
+ className="fixed top-0 left-0 w-full h-full -z-10 pointer-events-none"
282
  />
283
  );
284
  };
components/BionicText.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, ReactElement } from 'react';
2
+ import { TechnicalTerm } from '../types';
3
+
4
+ interface Props {
5
+ text: string;
6
+ bionicEnabled?: boolean;
7
+ highlightTerms?: TechnicalTerm[];
8
+ currentTTSWord?: number; // Index of word being spoken for TTS highlighting
9
+ }
10
+
11
+ // Color palette for consistent term highlighting
12
+ const TERM_COLORS = [
13
+ 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-200',
14
+ 'bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-200',
15
+ 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-200',
16
+ 'bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200',
17
+ 'bg-rose-100 dark:bg-rose-900/40 text-rose-800 dark:text-rose-200',
18
+ 'bg-cyan-100 dark:bg-cyan-900/40 text-cyan-800 dark:text-cyan-200',
19
+ 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-800 dark:text-indigo-200',
20
+ 'bg-teal-100 dark:bg-teal-900/40 text-teal-800 dark:text-teal-200',
21
+ ];
22
+
23
+ /**
24
+ * Apply Bionic Reading formatting to a word
25
+ * Bold the first portion of each word based on length
26
+ */
27
+ const applyBionicReading = (word: string): ReactElement => {
28
+ if (word.length <= 1) return <>{word}</>;
29
+
30
+ // Calculate how many letters to bold
31
+ // Short words (1-3): 1 letter
32
+ // Medium words (4-6): 2 letters
33
+ // Longer words (7+): ~40% of the word
34
+ let boldCount: number;
35
+ if (word.length <= 3) {
36
+ boldCount = 1;
37
+ } else if (word.length <= 6) {
38
+ boldCount = 2;
39
+ } else {
40
+ boldCount = Math.ceil(word.length * 0.4);
41
+ }
42
+
43
+ const boldPart = word.slice(0, boldCount);
44
+ const normalPart = word.slice(boldCount);
45
+
46
+ return (
47
+ <>
48
+ <span className="font-bold">{boldPart}</span>
49
+ <span className="font-normal opacity-80">{normalPart}</span>
50
+ </>
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Check if a word matches a technical term (case-insensitive)
56
+ */
57
+ const findMatchingTerm = (
58
+ word: string,
59
+ terms: TechnicalTerm[]
60
+ ): { term: TechnicalTerm; colorClass: string } | null => {
61
+ const cleanWord = word.toLowerCase().replace(/[^a-z0-9]/g, '');
62
+
63
+ for (let i = 0; i < terms.length; i++) {
64
+ const termLower = terms[i].term.toLowerCase();
65
+ if (cleanWord === termLower || cleanWord.includes(termLower)) {
66
+ return {
67
+ term: terms[i],
68
+ colorClass: terms[i].highlightColor || TERM_COLORS[i % TERM_COLORS.length]
69
+ };
70
+ }
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const BionicText: React.FC<Props> = ({
76
+ text,
77
+ bionicEnabled = false,
78
+ highlightTerms = [],
79
+ currentTTSWord
80
+ }) => {
81
+ const processedContent = useMemo(() => {
82
+ // Split text into words while preserving whitespace and punctuation
83
+ const words = text.split(/(\s+)/);
84
+ let wordIndex = 0;
85
+
86
+ return words.map((segment, idx) => {
87
+ // If it's whitespace, just return it
88
+ if (/^\s+$/.test(segment)) {
89
+ return <span key={idx}>{segment}</span>;
90
+ }
91
+
92
+ const currentWordIdx = wordIndex;
93
+ wordIndex++;
94
+
95
+ // Check if this word is currently being spoken (TTS)
96
+ const isTTSActive = currentTTSWord !== undefined && currentWordIdx === currentTTSWord;
97
+
98
+ // Check if this word matches a technical term
99
+ const matchingTerm = highlightTerms.length > 0
100
+ ? findMatchingTerm(segment, highlightTerms)
101
+ : null;
102
+
103
+ // Base styling
104
+ let className = '';
105
+ let content: ReactElement | string = segment;
106
+
107
+ // Apply TTS highlighting (karaoke style)
108
+ if (isTTSActive) {
109
+ className = 'bg-brand-200 dark:bg-brand-700 px-1 rounded transition-colors duration-150';
110
+ }
111
+
112
+ // Apply term highlighting
113
+ if (matchingTerm) {
114
+ className = `${matchingTerm.colorClass} px-1 py-0.5 rounded cursor-help transition-colors`;
115
+ }
116
+
117
+ // Apply bionic reading
118
+ if (bionicEnabled) {
119
+ content = applyBionicReading(segment);
120
+ }
121
+
122
+ // If it's a technical term, wrap with tooltip
123
+ if (matchingTerm) {
124
+ return (
125
+ <span
126
+ key={idx}
127
+ className={`inline-block ${className} group relative`}
128
+ title={matchingTerm.term.definition}
129
+ >
130
+ {content}
131
+ {/* Inline tooltip on hover */}
132
+ <span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap max-w-xs z-50 shadow-lg">
133
+ <span className="font-bold">{matchingTerm.term.term}:</span>{' '}
134
+ {matchingTerm.term.definition}
135
+ </span>
136
+ </span>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <span key={idx} className={className}>
142
+ {content}
143
+ </span>
144
+ );
145
+ });
146
+ }, [text, bionicEnabled, highlightTerms, currentTTSWord]);
147
+
148
+ return <span className="bionic-text">{processedContent}</span>;
149
+ };
150
+
151
+ /**
152
+ * Hook for processing markdown content with bionic reading
153
+ */
154
+ export const useBionicMarkdown = (content: string, enabled: boolean): string => {
155
+ return useMemo(() => {
156
+ if (!enabled) return content;
157
+
158
+ // Process markdown while preserving syntax
159
+ // We'll bold the first letters of words that aren't part of markdown syntax
160
+ return content.replace(
161
+ /(?<![#*_`\[\]])(\b\w{2,}\b)(?![#*_`\[\]])/g,
162
+ (match) => {
163
+ const boldCount = match.length <= 3 ? 1 : match.length <= 6 ? 2 : Math.ceil(match.length * 0.4);
164
+ return `**${match.slice(0, boldCount)}**${match.slice(boldCount)}`;
165
+ }
166
+ );
167
+ }, [content, enabled]);
168
+ };
169
+
170
+ export default BionicText;
components/BlogSection.tsx CHANGED
@@ -1,17 +1,21 @@
1
  import React, { useState } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
- import { BlogSection as BlogSectionType } from '../types';
4
  import MermaidDiagram from './MermaidDiagram';
5
- import InteractiveChart from './InteractiveChart';
6
  import EquationBlock from './EquationBlock';
7
  import Collapsible from './Collapsible';
8
  import Tooltip from './Tooltip';
9
- import { Info, Lightbulb, AlertTriangle, BookOpen, CheckCircle, XCircle, Shield, ChevronDown, Wrench } from 'lucide-react';
 
 
 
10
 
11
  interface Props {
12
  section: BlogSectionType;
13
  theme: 'light' | 'dark';
14
  index: number;
 
15
  }
16
 
17
  // Validation Badge Component
@@ -116,7 +120,10 @@ const ValidationBadge: React.FC<{ section: BlogSectionType }> = ({ section }) =>
116
  );
117
  };
118
 
119
- const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
 
 
 
120
  const getMarginNoteIcon = (icon?: 'info' | 'warning' | 'tip' | 'note') => {
121
  switch (icon) {
122
  case 'warning':
@@ -132,18 +139,73 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
132
 
133
  // Apply tooltips to content
134
  const renderContent = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  if (!section.technicalTerms || section.technicalTerms.length === 0) {
136
- return <ReactMarkdown>{section.content}</ReactMarkdown>;
137
  }
138
 
139
  // For complex tooltip integration, we'll render ReactMarkdown
140
  // and let users hover on specially marked terms
141
  return (
142
  <div className="relative">
143
- <ReactMarkdown>{section.content}</ReactMarkdown>
144
 
145
  {/* Technical Terms Legend */}
146
- {section.technicalTerms.length > 0 && (
147
  <div className="mt-8 p-6 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800/50 dark:to-gray-900/30 rounded-2xl border border-gray-200 dark:border-gray-700">
148
  <div className="flex items-center gap-2 mb-4">
149
  <div className="w-1 h-4 bg-brand-500 rounded-full"></div>
@@ -186,8 +248,36 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
186
  {section.title}
187
  </h2>
188
  <div className="h-1 w-24 bg-gradient-to-r from-brand-500 to-purple-500 rounded-full opacity-80" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  </div>
190
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
191
  </header>
192
 
193
  {/* Content */}
@@ -233,7 +323,7 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
233
  )}
234
 
235
  {section.visualizationType === 'chart' && section.chartData && (
236
- <InteractiveChart data={section.chartData} theme={theme} />
237
  )}
238
 
239
  {section.visualizationType === 'equation' && section.visualizationData && (
 
1
  import React, { useState } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
+ import { BlogSection as BlogSectionType, ReadingPreferences } from '../types';
4
  import MermaidDiagram from './MermaidDiagram';
5
+ import InteractiveChartPlayable from './InteractiveChartPlayable';
6
  import EquationBlock from './EquationBlock';
7
  import Collapsible from './Collapsible';
8
  import Tooltip from './Tooltip';
9
+ import ProgressiveContent from './ProgressiveContent';
10
+ import TextToSpeech from './TextToSpeech';
11
+ import BionicText from './BionicText';
12
+ import { Info, Lightbulb, AlertTriangle, BookOpen, CheckCircle, XCircle, Shield, ChevronDown, Wrench, Volume2, Zap, Layers } from 'lucide-react';
13
 
14
  interface Props {
15
  section: BlogSectionType;
16
  theme: 'light' | 'dark';
17
  index: number;
18
+ readingPreferences?: ReadingPreferences;
19
  }
20
 
21
  // Validation Badge Component
 
120
  );
121
  };
122
 
123
+ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index, readingPreferences }) => {
124
+ const [ttsWordIndex, setTtsWordIndex] = useState<number | undefined>(undefined);
125
+ const [isTtsPlaying, setIsTtsPlaying] = useState(false);
126
+
127
  const getMarginNoteIcon = (icon?: 'info' | 'warning' | 'tip' | 'note') => {
128
  switch (icon) {
129
  case 'warning':
 
139
 
140
  // Apply tooltips to content
141
  const renderContent = () => {
142
+ // Get the content to display based on ELI5 mode
143
+ const displayContent = readingPreferences?.eli5Mode && section.layers?.eli5Content
144
+ ? section.layers.eli5Content
145
+ : section.content;
146
+
147
+ // Apply bionic reading if enabled
148
+ const processText = (text: string) => {
149
+ if (!readingPreferences?.bionicReading) return text;
150
+
151
+ // Apply bionic reading: bold first portion of each word
152
+ return text.replace(/\b(\w{2,})\b/g, (match) => {
153
+ const boldCount = match.length <= 3 ? 1 : match.length <= 6 ? 2 : Math.ceil(match.length * 0.4);
154
+ return `**${match.slice(0, boldCount)}**${match.slice(boldCount)}`;
155
+ });
156
+ };
157
+
158
+ const processedContent = processText(displayContent);
159
+
160
+ // If we have progressive disclosure layers, show them first
161
+ if (section.layers) {
162
+ return (
163
+ <div className="space-y-8">
164
+ <ProgressiveContent
165
+ layers={section.layers}
166
+ eli5Mode={readingPreferences?.eli5Mode}
167
+ showConfidenceBadges={true}
168
+ />
169
+
170
+ {/* Technical Terms Legend */}
171
+ {section.technicalTerms && section.technicalTerms.length > 0 && readingPreferences?.highlightKeyTerms && (
172
+ <div className="mt-8 p-6 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800/50 dark:to-gray-900/30 rounded-2xl border border-gray-200 dark:border-gray-700">
173
+ <div className="flex items-center gap-2 mb-4">
174
+ <div className="w-1 h-4 bg-brand-500 rounded-full"></div>
175
+ <h5 className="text-sm font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">
176
+ Key Terms
177
+ </h5>
178
+ </div>
179
+ <div className="flex flex-wrap gap-2">
180
+ {section.technicalTerms.map((term, idx) => (
181
+ <Tooltip key={idx} term={term.term} definition={term.definition}>
182
+ <span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-sm font-medium cursor-help transition-colors ${
183
+ term.highlightColor || 'bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
184
+ }`}>
185
+ {term.term}
186
+ </span>
187
+ </Tooltip>
188
+ ))}
189
+ </div>
190
+ </div>
191
+ )}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ // Fallback to regular markdown rendering
197
  if (!section.technicalTerms || section.technicalTerms.length === 0) {
198
+ return <ReactMarkdown>{processedContent}</ReactMarkdown>;
199
  }
200
 
201
  // For complex tooltip integration, we'll render ReactMarkdown
202
  // and let users hover on specially marked terms
203
  return (
204
  <div className="relative">
205
+ <ReactMarkdown>{processedContent}</ReactMarkdown>
206
 
207
  {/* Technical Terms Legend */}
208
+ {section.technicalTerms.length > 0 && readingPreferences?.highlightKeyTerms && (
209
  <div className="mt-8 p-6 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800/50 dark:to-gray-900/30 rounded-2xl border border-gray-200 dark:border-gray-700">
210
  <div className="flex items-center gap-2 mb-4">
211
  <div className="w-1 h-4 bg-brand-500 rounded-full"></div>
 
248
  {section.title}
249
  </h2>
250
  <div className="h-1 w-24 bg-gradient-to-r from-brand-500 to-purple-500 rounded-full opacity-80" />
251
+
252
+ {/* Quick info badges */}
253
+ <div className="flex flex-wrap items-center gap-2 mt-4">
254
+ {section.layers && (
255
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 text-xs font-medium">
256
+ <Layers size={12} />
257
+ 3-Layer View
258
+ </span>
259
+ )}
260
+ {section.visualizationType && section.visualizationType !== 'none' && (
261
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium">
262
+ <Zap size={12} />
263
+ Interactive
264
+ </span>
265
+ )}
266
+ </div>
267
  </div>
268
  </div>
269
+
270
+ {/* Text-to-Speech Controls */}
271
+ {readingPreferences?.ttsEnabled && (
272
+ <div className="mt-6 pl-0 md:pl-[4.5rem]">
273
+ <TextToSpeech
274
+ text={section.content}
275
+ sectionId={section.id}
276
+ onWordChange={setTtsWordIndex}
277
+ onPlayingChange={setIsTtsPlaying}
278
+ />
279
+ </div>
280
+ )}
281
  </header>
282
 
283
  {/* Content */}
 
323
  )}
324
 
325
  {section.visualizationType === 'chart' && section.chartData && (
326
+ <InteractiveChartPlayable data={section.chartData} theme={theme} />
327
  )}
328
 
329
  {section.visualizationType === 'equation' && section.visualizationData && (
components/BlogView.tsx CHANGED
@@ -1,8 +1,9 @@
1
  import React, { useEffect, useState, useRef } from 'react';
2
- import { BlogSection as BlogSectionType, PaperStructure } from '../types';
3
  import BlogSectionComponent from './BlogSection';
4
  import Sidebar from './Sidebar';
5
- import { Clock, BookOpen, FileText, Share2, Download, Sparkles, CheckCircle2, Loader2, AlertCircle, RefreshCw, RotateCcw } from 'lucide-react';
 
6
 
7
  // Loading placeholder for sections being generated
8
  const SectionLoadingPlaceholder: React.FC<{
@@ -164,8 +165,19 @@ interface Props {
164
  onRetrySection?: (sectionIndex: number) => Promise<void>;
165
  retryingSectionIndex?: number;
166
  contentRef?: React.RefObject<HTMLDivElement>;
 
 
167
  }
168
 
 
 
 
 
 
 
 
 
 
169
  const BlogView: React.FC<Props> = ({
170
  sections,
171
  paperTitle,
@@ -179,13 +191,20 @@ const BlogView: React.FC<Props> = ({
179
  sectionStatus = '',
180
  onRetrySection,
181
  retryingSectionIndex = -1,
182
- contentRef
 
 
183
  }) => {
184
  const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
185
  const [readProgress, setReadProgress] = useState(0);
 
186
  const internalContentRef = useRef<HTMLDivElement>(null);
187
  const effectiveContentRef = contentRef || internalContentRef;
188
 
 
 
 
 
189
  // Calculate reading time (rough estimate: 200 words per minute)
190
  const completedSections = sections.filter(s => !s.isLoading && s.content);
191
  const totalWords = completedSections.reduce((acc, section) => {
@@ -399,6 +418,20 @@ const BlogView: React.FC<Props> = ({
399
  </button>
400
  </div>
401
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  </header>
403
  )}
404
 
@@ -446,6 +479,7 @@ const BlogView: React.FC<Props> = ({
446
  section={section}
447
  theme={theme}
448
  index={index}
 
449
  />
450
  )}
451
  </div>
 
1
  import React, { useEffect, useState, useRef } from 'react';
2
+ import { BlogSection as BlogSectionType, PaperStructure, ReadingPreferences } from '../types';
3
  import BlogSectionComponent from './BlogSection';
4
  import Sidebar from './Sidebar';
5
+ import ReadingToolbar from './ReadingToolbar';
6
+ import { Clock, BookOpen, FileText, Share2, Download, Sparkles, CheckCircle2, Loader2, AlertCircle, RefreshCw, RotateCcw, Zap, GraduationCap, Lightbulb } from 'lucide-react';
7
 
8
  // Loading placeholder for sections being generated
9
  const SectionLoadingPlaceholder: React.FC<{
 
165
  onRetrySection?: (sectionIndex: number) => Promise<void>;
166
  retryingSectionIndex?: number;
167
  contentRef?: React.RefObject<HTMLDivElement>;
168
+ readingPreferences?: ReadingPreferences;
169
+ onReadingPreferencesChange?: (prefs: ReadingPreferences) => void;
170
  }
171
 
172
+ // Default reading preferences
173
+ const defaultReadingPreferences: ReadingPreferences = {
174
+ bionicReading: false,
175
+ eli5Mode: false,
176
+ highlightKeyTerms: true,
177
+ ttsEnabled: false,
178
+ ttsSpeed: 1.0
179
+ };
180
+
181
  const BlogView: React.FC<Props> = ({
182
  sections,
183
  paperTitle,
 
191
  sectionStatus = '',
192
  onRetrySection,
193
  retryingSectionIndex = -1,
194
+ contentRef,
195
+ readingPreferences: externalPreferences,
196
+ onReadingPreferencesChange
197
  }) => {
198
  const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
199
  const [readProgress, setReadProgress] = useState(0);
200
+ const [internalPreferences, setInternalPreferences] = useState<ReadingPreferences>(defaultReadingPreferences);
201
  const internalContentRef = useRef<HTMLDivElement>(null);
202
  const effectiveContentRef = contentRef || internalContentRef;
203
 
204
+ // Use external or internal preferences
205
+ const readingPreferences = externalPreferences || internalPreferences;
206
+ const handlePreferencesChange = onReadingPreferencesChange || setInternalPreferences;
207
+
208
  // Calculate reading time (rough estimate: 200 words per minute)
209
  const completedSections = sections.filter(s => !s.isLoading && s.content);
210
  const totalWords = completedSections.reduce((acc, section) => {
 
418
  </button>
419
  </div>
420
  </div>
421
+
422
+ {/* ADHD-Friendly Reading Toolbar */}
423
+ <div className="mt-8 pt-6 border-t border-gray-100 dark:border-gray-800">
424
+ <div className="flex items-center gap-2 mb-3">
425
+ <Sparkles size={14} className="text-brand-500" />
426
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
427
+ Reading Mode
428
+ </span>
429
+ </div>
430
+ <ReadingToolbar
431
+ preferences={readingPreferences}
432
+ onPreferencesChange={handlePreferencesChange}
433
+ />
434
+ </div>
435
  </header>
436
  )}
437
 
 
479
  section={section}
480
  theme={theme}
481
  index={index}
482
+ readingPreferences={readingPreferences}
483
  />
484
  )}
485
  </div>
components/Collapsible.tsx CHANGED
@@ -86,3 +86,4 @@ const Collapsible: React.FC<Props> = ({
86
 
87
  export default Collapsible;
88
 
 
 
86
 
87
  export default Collapsible;
88
 
89
+
components/EquationBlock.tsx CHANGED
@@ -105,3 +105,4 @@ const EquationBlock: React.FC<Props> = ({ equation, label, inline = false }) =>
105
 
106
  export default EquationBlock;
107
 
 
 
105
 
106
  export default EquationBlock;
107
 
108
+
components/InteractiveChart.tsx CHANGED
@@ -333,3 +333,4 @@ const InteractiveChart: React.FC<Props> = ({ data, theme }) => {
333
 
334
  export default InteractiveChart;
335
 
 
 
333
 
334
  export default InteractiveChart;
335
 
336
+
components/InteractiveChartPlayable.tsx ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import { ChartData, SliderConfig } from '../types';
3
+ import { Play, Pause, RotateCcw, Eye, EyeOff, Sliders, Info } from 'lucide-react';
4
+
5
+ interface Props {
6
+ data: ChartData;
7
+ theme: 'light' | 'dark';
8
+ }
9
+
10
+ // Default colors palette
11
+ const DEFAULT_COLORS = [
12
+ '#0ea5e9', // brand blue
13
+ '#8b5cf6', // purple
14
+ '#10b981', // emerald
15
+ '#f59e0b', // amber
16
+ '#ef4444', // red
17
+ '#ec4899', // pink
18
+ '#06b6d4', // cyan
19
+ '#84cc16', // lime
20
+ ];
21
+
22
+ const InteractiveChartPlayable: React.FC<Props> = ({ data, theme }) => {
23
+ const isDark = theme === 'dark';
24
+ const colors = data.colors || DEFAULT_COLORS;
25
+
26
+ // State for interactive features
27
+ const [sliderValues, setSliderValues] = useState<Record<string, number>>(() => {
28
+ const initial: Record<string, number> = {};
29
+ data.interactive?.sliderConfig?.forEach(config => {
30
+ initial[config.id] = config.defaultValue;
31
+ });
32
+ return initial;
33
+ });
34
+
35
+ const [visibleDatasets, setVisibleDatasets] = useState<Set<string>>(() => {
36
+ return new Set(data.data.map(d => d.label));
37
+ });
38
+
39
+ const [hoveredItem, setHoveredItem] = useState<number | null>(null);
40
+ const [isAnimating, setIsAnimating] = useState(false);
41
+ const [showControls, setShowControls] = useState(true);
42
+
43
+ // Apply slider transformations to data
44
+ const transformedData = useMemo(() => {
45
+ if (!data.interactive?.enableSliders || !data.interactive.sliderConfig) {
46
+ return data.data;
47
+ }
48
+
49
+ return data.data.map(item => {
50
+ let newValue = item.value;
51
+
52
+ data.interactive?.sliderConfig?.forEach(config => {
53
+ const sliderValue = sliderValues[config.id] || config.defaultValue;
54
+
55
+ // Apply transformation based on the affected variable
56
+ if (config.affectsVariable === 'multiplier') {
57
+ newValue = item.value * sliderValue;
58
+ } else if (config.affectsVariable === 'offset') {
59
+ newValue = item.value + sliderValue;
60
+ } else if (config.affectsVariable === 'power') {
61
+ newValue = Math.pow(item.value, sliderValue);
62
+ }
63
+ });
64
+
65
+ return { ...item, value: newValue };
66
+ });
67
+ }, [data.data, sliderValues, data.interactive]);
68
+
69
+ // Filter by visible datasets
70
+ const visibleData = useMemo(() => {
71
+ return transformedData.filter(item => visibleDatasets.has(item.label));
72
+ }, [transformedData, visibleDatasets]);
73
+
74
+ const maxValue = useMemo(() => {
75
+ return Math.max(...visibleData.map(d => d.value), 1);
76
+ }, [visibleData]);
77
+
78
+ const handleSliderChange = useCallback((sliderId: string, value: number) => {
79
+ setSliderValues(prev => ({ ...prev, [sliderId]: value }));
80
+ }, []);
81
+
82
+ const handleToggleDataset = useCallback((label: string) => {
83
+ setVisibleDatasets(prev => {
84
+ const next = new Set(prev);
85
+ if (next.has(label)) {
86
+ if (next.size > 1) next.delete(label);
87
+ } else {
88
+ next.add(label);
89
+ }
90
+ return next;
91
+ });
92
+ }, []);
93
+
94
+ const handleReset = useCallback(() => {
95
+ setSliderValues(() => {
96
+ const initial: Record<string, number> = {};
97
+ data.interactive?.sliderConfig?.forEach(config => {
98
+ initial[config.id] = config.defaultValue;
99
+ });
100
+ return initial;
101
+ });
102
+ setVisibleDatasets(new Set(data.data.map(d => d.label)));
103
+ }, [data]);
104
+
105
+ const handleAnimate = useCallback(() => {
106
+ if (!data.interactive?.sliderConfig?.[0]) return;
107
+
108
+ setIsAnimating(true);
109
+ const config = data.interactive.sliderConfig[0];
110
+ const steps = 20;
111
+ const stepSize = (config.max - config.min) / steps;
112
+ let currentStep = 0;
113
+
114
+ const interval = setInterval(() => {
115
+ setSliderValues(prev => ({
116
+ ...prev,
117
+ [config.id]: config.min + (stepSize * currentStep)
118
+ }));
119
+ currentStep++;
120
+
121
+ if (currentStep > steps) {
122
+ clearInterval(interval);
123
+ setIsAnimating(false);
124
+ }
125
+ }, 100);
126
+ }, [data.interactive]);
127
+
128
+ // Chart dimensions
129
+ const chartWidth = 400;
130
+ const chartHeight = 200;
131
+ const barWidth = Math.min(50, (chartWidth - 80) / Math.max(visibleData.length, 1) - 10);
132
+
133
+ const renderBarChart = () => (
134
+ <svg viewBox={`0 0 ${chartWidth} ${chartHeight + 60}`} className="w-full h-auto">
135
+ {/* Grid lines */}
136
+ {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
137
+ <g key={i}>
138
+ <text
139
+ x="30"
140
+ y={chartHeight - (ratio * chartHeight) + 5}
141
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-500'}`}
142
+ textAnchor="end"
143
+ >
144
+ {Math.round(maxValue * ratio)}
145
+ </text>
146
+ <line
147
+ x1="40"
148
+ y1={chartHeight - (ratio * chartHeight)}
149
+ x2={chartWidth - 10}
150
+ y2={chartHeight - (ratio * chartHeight)}
151
+ className={isDark ? 'stroke-gray-700' : 'stroke-gray-200'}
152
+ strokeDasharray="4,4"
153
+ />
154
+ </g>
155
+ ))}
156
+
157
+ {/* Bars with animation */}
158
+ {visibleData.map((item, index) => {
159
+ const barHeight = (item.value / maxValue) * chartHeight;
160
+ const x = 50 + index * ((chartWidth - 60) / Math.max(visibleData.length, 1));
161
+ const y = chartHeight - barHeight;
162
+ const isHovered = hoveredItem === index;
163
+ const originalIndex = data.data.findIndex(d => d.label === item.label);
164
+
165
+ return (
166
+ <g
167
+ key={item.label}
168
+ className="cursor-pointer"
169
+ onMouseEnter={() => setHoveredItem(index)}
170
+ onMouseLeave={() => setHoveredItem(null)}
171
+ >
172
+ {/* Bar */}
173
+ <rect
174
+ x={x}
175
+ y={y}
176
+ width={barWidth}
177
+ height={barHeight}
178
+ fill={colors[originalIndex % colors.length]}
179
+ rx="4"
180
+ className={`
181
+ transition-all duration-300 ease-out
182
+ ${isHovered ? 'opacity-100' : 'opacity-85'}
183
+ `}
184
+ style={{
185
+ transform: isHovered ? 'scale(1.02)' : 'scale(1)',
186
+ transformOrigin: `${x + barWidth/2}px ${chartHeight}px`
187
+ }}
188
+ />
189
+
190
+ {/* Highlight glow on hover */}
191
+ {isHovered && (
192
+ <rect
193
+ x={x - 2}
194
+ y={y - 2}
195
+ width={barWidth + 4}
196
+ height={barHeight + 4}
197
+ fill="none"
198
+ stroke={colors[originalIndex % colors.length]}
199
+ strokeWidth="2"
200
+ rx="6"
201
+ opacity="0.3"
202
+ />
203
+ )}
204
+
205
+ {/* Value tooltip on hover */}
206
+ {isHovered && (
207
+ <g>
208
+ <rect
209
+ x={x + barWidth/2 - 25}
210
+ y={y - 35}
211
+ width="50"
212
+ height="24"
213
+ rx="4"
214
+ className={isDark ? 'fill-gray-800' : 'fill-gray-900'}
215
+ />
216
+ <text
217
+ x={x + barWidth / 2}
218
+ y={y - 18}
219
+ textAnchor="middle"
220
+ className="text-[12px] font-bold fill-white"
221
+ >
222
+ {item.value.toFixed(1)}
223
+ </text>
224
+ </g>
225
+ )}
226
+
227
+ {/* Label */}
228
+ <text
229
+ x={x + barWidth / 2}
230
+ y={chartHeight + 18}
231
+ textAnchor="middle"
232
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-600'} ${isHovered ? 'font-bold' : ''}`}
233
+ >
234
+ {item.label.length > 10 ? item.label.substring(0, 10) + '...' : item.label}
235
+ </text>
236
+ </g>
237
+ );
238
+ })}
239
+ </svg>
240
+ );
241
+
242
+ const renderLineChart = () => {
243
+ const points = visibleData.map((item, index) => {
244
+ const x = 50 + index * ((chartWidth - 80) / (Math.max(visibleData.length, 1) - 1 || 1));
245
+ const y = chartHeight - (item.value / maxValue) * chartHeight;
246
+ return { x, y, ...item };
247
+ });
248
+
249
+ const pathD = points
250
+ .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
251
+ .join(' ');
252
+
253
+ const areaD = `${pathD} L ${points[points.length - 1]?.x || 0} ${chartHeight} L ${points[0]?.x || 0} ${chartHeight} Z`;
254
+
255
+ return (
256
+ <svg viewBox={`0 0 ${chartWidth} ${chartHeight + 60}`} className="w-full h-auto">
257
+ {/* Grid */}
258
+ {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
259
+ <g key={i}>
260
+ <text
261
+ x="30"
262
+ y={chartHeight - (ratio * chartHeight) + 5}
263
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-500'}`}
264
+ textAnchor="end"
265
+ >
266
+ {Math.round(maxValue * ratio)}
267
+ </text>
268
+ <line
269
+ x1="40"
270
+ y1={chartHeight - (ratio * chartHeight)}
271
+ x2={chartWidth - 10}
272
+ y2={chartHeight - (ratio * chartHeight)}
273
+ className={isDark ? 'stroke-gray-700' : 'stroke-gray-200'}
274
+ strokeDasharray="4,4"
275
+ />
276
+ </g>
277
+ ))}
278
+
279
+ {/* Area fill */}
280
+ <path
281
+ d={areaD}
282
+ fill={colors[0]}
283
+ fillOpacity="0.15"
284
+ className="transition-all duration-300"
285
+ />
286
+
287
+ {/* Line */}
288
+ <path
289
+ d={pathD}
290
+ fill="none"
291
+ stroke={colors[0]}
292
+ strokeWidth="3"
293
+ strokeLinecap="round"
294
+ strokeLinejoin="round"
295
+ className="transition-all duration-300"
296
+ />
297
+
298
+ {/* Interactive points */}
299
+ {points.map((point, index) => (
300
+ <g
301
+ key={index}
302
+ className="cursor-pointer"
303
+ onMouseEnter={() => setHoveredItem(index)}
304
+ onMouseLeave={() => setHoveredItem(null)}
305
+ >
306
+ {/* Larger hit area */}
307
+ <circle
308
+ cx={point.x}
309
+ cy={point.y}
310
+ r="15"
311
+ fill="transparent"
312
+ />
313
+
314
+ {/* Visible point */}
315
+ <circle
316
+ cx={point.x}
317
+ cy={point.y}
318
+ r={hoveredItem === index ? 8 : 5}
319
+ fill={colors[0]}
320
+ className="transition-all duration-200"
321
+ />
322
+ <circle
323
+ cx={point.x}
324
+ cy={point.y}
325
+ r={hoveredItem === index ? 4 : 2}
326
+ fill={isDark ? '#1e293b' : 'white'}
327
+ />
328
+
329
+ {/* Tooltip */}
330
+ {hoveredItem === index && (
331
+ <g>
332
+ <rect
333
+ x={point.x - 30}
334
+ y={point.y - 40}
335
+ width="60"
336
+ height="28"
337
+ rx="4"
338
+ className={isDark ? 'fill-gray-800' : 'fill-gray-900'}
339
+ />
340
+ <text
341
+ x={point.x}
342
+ y={point.y - 22}
343
+ textAnchor="middle"
344
+ className="text-[11px] font-bold fill-white"
345
+ >
346
+ {point.value.toFixed(1)}
347
+ </text>
348
+ </g>
349
+ )}
350
+
351
+ {/* X-axis label */}
352
+ <text
353
+ x={point.x}
354
+ y={chartHeight + 18}
355
+ textAnchor="middle"
356
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-600'}`}
357
+ >
358
+ {point.label.length > 8 ? point.label.substring(0, 8) + '..' : point.label}
359
+ </text>
360
+ </g>
361
+ ))}
362
+ </svg>
363
+ );
364
+ };
365
+
366
+ return (
367
+ <div className="space-y-4">
368
+ {/* Interactive Controls Header */}
369
+ <div className="flex items-center justify-between">
370
+ <div className="flex items-center gap-2">
371
+ <Sliders size={16} className="text-brand-500" />
372
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
373
+ Interactive Chart
374
+ </span>
375
+ </div>
376
+
377
+ <div className="flex items-center gap-2">
378
+ {data.interactive?.enableSliders && (
379
+ <button
380
+ onClick={handleAnimate}
381
+ disabled={isAnimating}
382
+ className={`
383
+ flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-colors
384
+ ${isAnimating
385
+ ? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400'
386
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
387
+ }
388
+ `}
389
+ >
390
+ {isAnimating ? <Pause size={12} /> : <Play size={12} />}
391
+ {isAnimating ? 'Playing' : 'Animate'}
392
+ </button>
393
+ )}
394
+
395
+ <button
396
+ onClick={handleReset}
397
+ className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
398
+ >
399
+ <RotateCcw size={12} />
400
+ Reset
401
+ </button>
402
+
403
+ <button
404
+ onClick={() => setShowControls(!showControls)}
405
+ className={`
406
+ p-1.5 rounded-lg transition-colors
407
+ ${showControls
408
+ ? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400'
409
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500'
410
+ }
411
+ `}
412
+ >
413
+ {showControls ? <Eye size={14} /> : <EyeOff size={14} />}
414
+ </button>
415
+ </div>
416
+ </div>
417
+
418
+ {/* Title */}
419
+ {data.title && (
420
+ <h4 className="text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
421
+ {data.title}
422
+ </h4>
423
+ )}
424
+
425
+ {/* Chart */}
426
+ <div className="relative bg-white dark:bg-gray-900 rounded-xl p-4">
427
+ {data.type === 'line' || data.type === 'area' ? renderLineChart() : renderBarChart()}
428
+ </div>
429
+
430
+ {/* Interactive Controls */}
431
+ {showControls && (
432
+ <div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-gray-700">
433
+
434
+ {/* Sliders */}
435
+ {data.interactive?.enableSliders && data.interactive.sliderConfig?.map((config) => (
436
+ <div key={config.id} className="space-y-2">
437
+ <div className="flex items-center justify-between">
438
+ <label className="text-xs font-medium text-gray-700 dark:text-gray-300">
439
+ {config.label}
440
+ </label>
441
+ <span className="text-sm font-mono font-bold text-brand-600 dark:text-brand-400">
442
+ {sliderValues[config.id]?.toFixed(2) || config.defaultValue.toFixed(2)}
443
+ </span>
444
+ </div>
445
+ <input
446
+ type="range"
447
+ min={config.min}
448
+ max={config.max}
449
+ step={config.step}
450
+ value={sliderValues[config.id] || config.defaultValue}
451
+ onChange={(e) => handleSliderChange(config.id, parseFloat(e.target.value))}
452
+ className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-brand-500"
453
+ />
454
+ <div className="flex justify-between text-[10px] text-gray-400">
455
+ <span>{config.min}</span>
456
+ <span>{config.max}</span>
457
+ </div>
458
+ </div>
459
+ ))}
460
+
461
+ {/* Dataset Toggles */}
462
+ {data.interactive?.enableToggles && (
463
+ <div className="space-y-2">
464
+ <label className="text-xs font-medium text-gray-700 dark:text-gray-300">
465
+ Toggle Datasets
466
+ </label>
467
+ <div className="flex flex-wrap gap-2">
468
+ {data.data.map((item, idx) => (
469
+ <button
470
+ key={item.label}
471
+ onClick={() => handleToggleDataset(item.label)}
472
+ className={`
473
+ flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all
474
+ ${visibleDatasets.has(item.label)
475
+ ? 'bg-white dark:bg-gray-700 shadow-sm border border-gray-200 dark:border-gray-600'
476
+ : 'bg-gray-100 dark:bg-gray-800 opacity-50'
477
+ }
478
+ `}
479
+ >
480
+ <span
481
+ className="w-3 h-3 rounded-full"
482
+ style={{ backgroundColor: colors[idx % colors.length] }}
483
+ />
484
+ <span className={visibleDatasets.has(item.label) ? '' : 'line-through'}>
485
+ {item.label}
486
+ </span>
487
+ </button>
488
+ ))}
489
+ </div>
490
+ </div>
491
+ )}
492
+
493
+ {/* Info tooltip */}
494
+ <div className="flex items-start gap-2 p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
495
+ <Info size={14} className="flex-shrink-0 mt-0.5" />
496
+ <p className="text-xs">
497
+ <span className="font-medium">Tip:</span> Hover over data points for details.
498
+ {data.interactive?.enableSliders && ' Use the slider to see how changes affect the visualization.'}
499
+ {data.interactive?.enableToggles && ' Toggle datasets on/off to compare.'}
500
+ </p>
501
+ </div>
502
+ </div>
503
+ )}
504
+
505
+ {/* Axis Labels */}
506
+ {(data.xAxis || data.yAxis) && (
507
+ <div className="flex justify-between text-xs text-gray-500 dark:text-gray-400 italic">
508
+ {data.yAxis && <span>↑ {data.yAxis}</span>}
509
+ {data.xAxis && <span>{data.xAxis} →</span>}
510
+ </div>
511
+ )}
512
+ </div>
513
+ );
514
+ };
515
+
516
+ export default InteractiveChartPlayable;
components/ProgressiveContent.tsx ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { ChevronDown, ChevronUp, Layers, Zap, List, FileText, Quote, Sparkles } from 'lucide-react';
4
+ import { ContentLayers, ConfidenceLevel } from '../types';
5
+
6
+ interface Props {
7
+ layers: ContentLayers;
8
+ eli5Mode?: boolean;
9
+ showConfidenceBadges?: boolean;
10
+ }
11
+
12
+ type LayerLevel = 1 | 2 | 3;
13
+
14
+ const layerInfo = {
15
+ 1: {
16
+ name: 'TL;DR',
17
+ icon: Zap,
18
+ description: 'Core thesis in one sentence',
19
+ color: 'brand'
20
+ },
21
+ 2: {
22
+ name: 'Key Takeaways',
23
+ icon: List,
24
+ description: '3-5 essential points',
25
+ color: 'purple'
26
+ },
27
+ 3: {
28
+ name: 'Full Details',
29
+ icon: FileText,
30
+ description: 'Complete explanation',
31
+ color: 'emerald'
32
+ }
33
+ };
34
+
35
+ const ProgressiveContent: React.FC<Props> = ({
36
+ layers,
37
+ eli5Mode = false,
38
+ showConfidenceBadges = true
39
+ }) => {
40
+ const [expandedLayer, setExpandedLayer] = useState<LayerLevel>(1);
41
+ const [isAnimating, setIsAnimating] = useState(false);
42
+
43
+ const handleLayerChange = (level: LayerLevel) => {
44
+ if (level === expandedLayer) return;
45
+ setIsAnimating(true);
46
+ setTimeout(() => {
47
+ setExpandedLayer(level);
48
+ setIsAnimating(false);
49
+ }, 150);
50
+ };
51
+
52
+ const content = eli5Mode && layers.eli5Content ? layers.eli5Content : layers.detailed;
53
+
54
+ return (
55
+ <div className="space-y-4">
56
+ {/* Layer Selector Pills */}
57
+ <div className="flex flex-wrap items-center gap-2 mb-6">
58
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mr-2">
59
+ Depth:
60
+ </span>
61
+ {([1, 2, 3] as LayerLevel[]).map((level) => {
62
+ const info = layerInfo[level];
63
+ const Icon = info.icon;
64
+ const isActive = expandedLayer === level;
65
+
66
+ return (
67
+ <button
68
+ key={level}
69
+ onClick={() => handleLayerChange(level)}
70
+ className={`
71
+ flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-300
72
+ ${isActive
73
+ ? `bg-${info.color}-100 dark:bg-${info.color}-900/30 text-${info.color}-700 dark:text-${info.color}-300 ring-2 ring-${info.color}-500/30 shadow-sm`
74
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
75
+ }
76
+ `}
77
+ title={info.description}
78
+ >
79
+ <Icon size={14} />
80
+ <span>{info.name}</span>
81
+ {isActive && level < 3 && (
82
+ <span className="text-[10px] opacity-60 ml-1">
83
+ Click {level + 1} for more
84
+ </span>
85
+ )}
86
+ </button>
87
+ );
88
+ })}
89
+ </div>
90
+
91
+ {/* Content Display */}
92
+ <div className={`transition-all duration-300 ${isAnimating ? 'opacity-0 scale-98' : 'opacity-100 scale-100'}`}>
93
+
94
+ {/* Layer 1: Thesis */}
95
+ {expandedLayer >= 1 && (
96
+ <div className="mb-6">
97
+ <div className={`
98
+ relative p-6 rounded-2xl border-2 transition-all duration-500
99
+ ${expandedLayer === 1
100
+ ? 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 shadow-lg shadow-brand-500/10'
101
+ : 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
102
+ }
103
+ `}>
104
+ <div className="flex items-start gap-4">
105
+ <div className={`
106
+ flex-shrink-0 p-2 rounded-xl
107
+ ${expandedLayer === 1
108
+ ? 'bg-brand-500 text-white'
109
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-500'
110
+ }
111
+ `}>
112
+ <Zap size={20} />
113
+ </div>
114
+ <div className="flex-1">
115
+ <div className="flex items-center gap-2 mb-2">
116
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
117
+ Core Thesis
118
+ </span>
119
+ {showConfidenceBadges && (
120
+ <ConfidenceBadge level="synthesis" />
121
+ )}
122
+ </div>
123
+ <p className={`
124
+ text-lg md:text-xl font-medium leading-relaxed
125
+ ${expandedLayer === 1
126
+ ? 'text-gray-900 dark:text-white'
127
+ : 'text-gray-600 dark:text-gray-400'
128
+ }
129
+ `}>
130
+ {layers.thesis}
131
+ </p>
132
+ </div>
133
+ </div>
134
+
135
+ {expandedLayer === 1 && (
136
+ <button
137
+ onClick={() => handleLayerChange(2)}
138
+ className="absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1 px-3 py-1 rounded-full bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm"
139
+ >
140
+ <span>Want more detail?</span>
141
+ <ChevronDown size={14} />
142
+ </button>
143
+ )}
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ {/* Layer 2: Key Takeaways */}
149
+ {expandedLayer >= 2 && (
150
+ <div className={`mb-6 transition-all duration-500 ${expandedLayer >= 2 ? 'animate-in fade-in slide-in-from-top-4' : ''}`}>
151
+ <div className={`
152
+ relative p-6 rounded-2xl border-2 transition-all duration-500
153
+ ${expandedLayer === 2
154
+ ? 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800 shadow-lg shadow-purple-500/10'
155
+ : 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
156
+ }
157
+ `}>
158
+ <div className="flex items-start gap-4">
159
+ <div className={`
160
+ flex-shrink-0 p-2 rounded-xl
161
+ ${expandedLayer === 2
162
+ ? 'bg-purple-500 text-white'
163
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-500'
164
+ }
165
+ `}>
166
+ <List size={20} />
167
+ </div>
168
+ <div className="flex-1">
169
+ <div className="flex items-center gap-2 mb-3">
170
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
171
+ Key Takeaways
172
+ </span>
173
+ </div>
174
+ <ul className="space-y-3">
175
+ {layers.takeaways.map((takeaway, idx) => (
176
+ <li
177
+ key={idx}
178
+ className={`
179
+ flex items-start gap-3 animate-in fade-in slide-in-from-left-2
180
+ ${expandedLayer === 2
181
+ ? 'text-gray-800 dark:text-gray-200'
182
+ : 'text-gray-600 dark:text-gray-400'
183
+ }
184
+ `}
185
+ style={{ animationDelay: `${idx * 100}ms` }}
186
+ >
187
+ <span className={`
188
+ flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold mt-0.5
189
+ ${expandedLayer === 2
190
+ ? 'bg-purple-200 dark:bg-purple-800 text-purple-700 dark:text-purple-300'
191
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-500'
192
+ }
193
+ `}>
194
+ {idx + 1}
195
+ </span>
196
+ <span className="font-medium">{takeaway}</span>
197
+ </li>
198
+ ))}
199
+ </ul>
200
+ </div>
201
+ </div>
202
+
203
+ {expandedLayer === 2 && (
204
+ <button
205
+ onClick={() => handleLayerChange(3)}
206
+ className="absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1 px-3 py-1 rounded-full bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm"
207
+ >
208
+ <span>Read full details</span>
209
+ <ChevronDown size={14} />
210
+ </button>
211
+ )}
212
+ </div>
213
+ </div>
214
+ )}
215
+
216
+ {/* Layer 3: Full Details */}
217
+ {expandedLayer === 3 && (
218
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
219
+ <div className="relative p-6 rounded-2xl bg-emerald-50 dark:bg-emerald-900/20 border-2 border-emerald-200 dark:border-emerald-800">
220
+ <div className="flex items-center gap-2 mb-4">
221
+ <div className="p-2 rounded-xl bg-emerald-500 text-white">
222
+ <FileText size={20} />
223
+ </div>
224
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
225
+ Full Explanation
226
+ </span>
227
+ {eli5Mode && layers.eli5Content && (
228
+ <span className="ml-2 px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-xs font-medium">
229
+ Simple Mode
230
+ </span>
231
+ )}
232
+ </div>
233
+
234
+ <div className="prose prose-lg dark:prose-invert max-w-none">
235
+ <ReactMarkdown>{content}</ReactMarkdown>
236
+ </div>
237
+
238
+ <button
239
+ onClick={() => handleLayerChange(1)}
240
+ className="mt-6 flex items-center gap-1 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
241
+ >
242
+ <ChevronUp size={14} />
243
+ <span>Collapse to summary</span>
244
+ </button>
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+ </div>
250
+ );
251
+ };
252
+
253
+ // Confidence Badge Component
254
+ const ConfidenceBadge: React.FC<{ level: ConfidenceLevel }> = ({ level }) => {
255
+ const config = {
256
+ 'direct-quote': {
257
+ label: 'Direct Quote',
258
+ color: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800',
259
+ icon: Quote
260
+ },
261
+ 'paraphrase': {
262
+ label: 'Paraphrased',
263
+ color: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800',
264
+ icon: Sparkles
265
+ },
266
+ 'synthesis': {
267
+ label: 'Synthesized',
268
+ color: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800',
269
+ icon: Layers
270
+ },
271
+ 'interpretation': {
272
+ label: 'AI Interpretation',
273
+ color: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-800',
274
+ icon: Sparkles
275
+ }
276
+ };
277
+
278
+ const { label, color, icon: Icon } = config[level];
279
+
280
+ return (
281
+ <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border ${color}`}>
282
+ <Icon size={10} />
283
+ {label}
284
+ </span>
285
+ );
286
+ };
287
+
288
+ export { ConfidenceBadge };
289
+ export default ProgressiveContent;
components/ReadingToolbar.tsx ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import {
3
+ BookOpen,
4
+ Sparkles,
5
+ Type,
6
+ Volume2,
7
+ VolumeX,
8
+ Settings,
9
+ X,
10
+ Zap,
11
+ GraduationCap,
12
+ Eye,
13
+ Lightbulb
14
+ } from 'lucide-react';
15
+ import { ReadingPreferences } from '../types';
16
+
17
+ interface Props {
18
+ preferences: ReadingPreferences;
19
+ onPreferencesChange: (preferences: ReadingPreferences) => void;
20
+ compact?: boolean;
21
+ }
22
+
23
+ const ReadingToolbar: React.FC<Props> = ({
24
+ preferences,
25
+ onPreferencesChange,
26
+ compact = false
27
+ }) => {
28
+ const [showPanel, setShowPanel] = useState(false);
29
+
30
+ const togglePreference = (key: keyof ReadingPreferences) => {
31
+ if (typeof preferences[key] === 'boolean') {
32
+ onPreferencesChange({
33
+ ...preferences,
34
+ [key]: !preferences[key]
35
+ });
36
+ }
37
+ };
38
+
39
+ const handleSpeedChange = (speed: number) => {
40
+ onPreferencesChange({
41
+ ...preferences,
42
+ ttsSpeed: speed
43
+ });
44
+ };
45
+
46
+ if (compact) {
47
+ return (
48
+ <div className="flex items-center gap-1">
49
+ {/* ELI5 Toggle */}
50
+ <button
51
+ onClick={() => togglePreference('eli5Mode')}
52
+ className={`
53
+ p-2 rounded-lg transition-all duration-200
54
+ ${preferences.eli5Mode
55
+ ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 ring-2 ring-amber-500/30'
56
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
57
+ }
58
+ `}
59
+ title={preferences.eli5Mode ? 'Switch to Academic mode' : 'Switch to Simple mode'}
60
+ >
61
+ {preferences.eli5Mode ? <Lightbulb size={18} /> : <GraduationCap size={18} />}
62
+ </button>
63
+
64
+ {/* Bionic Reading Toggle */}
65
+ <button
66
+ onClick={() => togglePreference('bionicReading')}
67
+ className={`
68
+ p-2 rounded-lg transition-all duration-200
69
+ ${preferences.bionicReading
70
+ ? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 ring-2 ring-emerald-500/30'
71
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
72
+ }
73
+ `}
74
+ title={preferences.bionicReading ? 'Disable Bionic Reading' : 'Enable Bionic Reading'}
75
+ >
76
+ <Type size={18} />
77
+ </button>
78
+
79
+ {/* TTS Toggle */}
80
+ <button
81
+ onClick={() => togglePreference('ttsEnabled')}
82
+ className={`
83
+ p-2 rounded-lg transition-all duration-200
84
+ ${preferences.ttsEnabled
85
+ ? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 ring-2 ring-brand-500/30'
86
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
87
+ }
88
+ `}
89
+ title={preferences.ttsEnabled ? 'Hide audio controls' : 'Show audio controls'}
90
+ >
91
+ {preferences.ttsEnabled ? <Volume2 size={18} /> : <VolumeX size={18} />}
92
+ </button>
93
+
94
+ {/* Settings */}
95
+ <button
96
+ onClick={() => setShowPanel(!showPanel)}
97
+ className={`
98
+ p-2 rounded-lg transition-all duration-200
99
+ ${showPanel
100
+ ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400'
101
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
102
+ }
103
+ `}
104
+ >
105
+ <Settings size={18} />
106
+ </button>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <div className="relative">
113
+ {/* Main Toolbar */}
114
+ <div className="flex items-center gap-3 p-3 rounded-2xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg">
115
+ <div className="flex items-center gap-2 pr-3 border-r border-gray-200 dark:border-gray-700">
116
+ <Eye size={16} className="text-gray-400" />
117
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
118
+ Reading Mode
119
+ </span>
120
+ </div>
121
+
122
+ {/* ELI5 Toggle Button */}
123
+ <button
124
+ onClick={() => togglePreference('eli5Mode')}
125
+ className={`
126
+ flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300
127
+ ${preferences.eli5Mode
128
+ ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg shadow-amber-500/30'
129
+ : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
130
+ }
131
+ `}
132
+ >
133
+ {preferences.eli5Mode ? (
134
+ <>
135
+ <Lightbulb size={16} />
136
+ <span>Simple</span>
137
+ </>
138
+ ) : (
139
+ <>
140
+ <GraduationCap size={16} />
141
+ <span>Academic</span>
142
+ </>
143
+ )}
144
+ </button>
145
+
146
+ {/* Bionic Reading Toggle */}
147
+ <button
148
+ onClick={() => togglePreference('bionicReading')}
149
+ className={`
150
+ flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300
151
+ ${preferences.bionicReading
152
+ ? 'bg-gradient-to-r from-emerald-500 to-teal-500 text-white shadow-lg shadow-emerald-500/30'
153
+ : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
154
+ }
155
+ `}
156
+ >
157
+ <Type size={16} />
158
+ <span className={preferences.bionicReading ? '' : 'hidden sm:inline'}>
159
+ {preferences.bionicReading ? 'Bio**nic** On' : 'Bionic'}
160
+ </span>
161
+ </button>
162
+
163
+ {/* Key Terms Highlight Toggle */}
164
+ <button
165
+ onClick={() => togglePreference('highlightKeyTerms')}
166
+ className={`
167
+ flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300
168
+ ${preferences.highlightKeyTerms
169
+ ? 'bg-gradient-to-r from-purple-500 to-indigo-500 text-white shadow-lg shadow-purple-500/30'
170
+ : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
171
+ }
172
+ `}
173
+ >
174
+ <Sparkles size={16} />
175
+ <span className="hidden sm:inline">Highlight</span>
176
+ </button>
177
+
178
+ {/* TTS Toggle */}
179
+ <button
180
+ onClick={() => togglePreference('ttsEnabled')}
181
+ className={`
182
+ flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300
183
+ ${preferences.ttsEnabled
184
+ ? 'bg-gradient-to-r from-brand-500 to-blue-500 text-white shadow-lg shadow-brand-500/30'
185
+ : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
186
+ }
187
+ `}
188
+ >
189
+ {preferences.ttsEnabled ? <Volume2 size={16} /> : <VolumeX size={16} />}
190
+ <span className="hidden sm:inline">Audio</span>
191
+ </button>
192
+
193
+ {/* Settings */}
194
+ <button
195
+ onClick={() => setShowPanel(!showPanel)}
196
+ className={`
197
+ p-2 rounded-xl transition-all duration-200 ml-auto
198
+ ${showPanel
199
+ ? 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200'
200
+ : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
201
+ }
202
+ `}
203
+ >
204
+ <Settings size={18} />
205
+ </button>
206
+ </div>
207
+
208
+ {/* Settings Panel */}
209
+ {showPanel && (
210
+ <div className="absolute top-full left-0 right-0 mt-2 p-4 rounded-2xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-xl z-50 animate-in fade-in slide-in-from-top-2 duration-200">
211
+ <div className="flex items-center justify-between mb-4">
212
+ <h4 className="text-sm font-bold text-gray-900 dark:text-white">Reading Preferences</h4>
213
+ <button
214
+ onClick={() => setShowPanel(false)}
215
+ className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
216
+ >
217
+ <X size={14} />
218
+ </button>
219
+ </div>
220
+
221
+ <div className="space-y-6">
222
+ {/* Language Mode Section */}
223
+ <div>
224
+ <label className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 block">
225
+ Language Complexity
226
+ </label>
227
+ <div className="grid grid-cols-2 gap-2">
228
+ <button
229
+ onClick={() => onPreferencesChange({ ...preferences, eli5Mode: true })}
230
+ className={`
231
+ p-3 rounded-xl border-2 transition-all
232
+ ${preferences.eli5Mode
233
+ ? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
234
+ : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
235
+ }
236
+ `}
237
+ >
238
+ <div className="flex items-center gap-2 mb-1">
239
+ <Lightbulb size={16} className={preferences.eli5Mode ? 'text-amber-500' : 'text-gray-400'} />
240
+ <span className={`font-medium text-sm ${preferences.eli5Mode ? 'text-amber-700 dark:text-amber-300' : 'text-gray-600 dark:text-gray-400'}`}>
241
+ Simple
242
+ </span>
243
+ </div>
244
+ <p className="text-xs text-gray-500 dark:text-gray-400">
245
+ Everyday language with analogies
246
+ </p>
247
+ </button>
248
+ <button
249
+ onClick={() => onPreferencesChange({ ...preferences, eli5Mode: false })}
250
+ className={`
251
+ p-3 rounded-xl border-2 transition-all
252
+ ${!preferences.eli5Mode
253
+ ? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
254
+ : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
255
+ }
256
+ `}
257
+ >
258
+ <div className="flex items-center gap-2 mb-1">
259
+ <GraduationCap size={16} className={!preferences.eli5Mode ? 'text-brand-500' : 'text-gray-400'} />
260
+ <span className={`font-medium text-sm ${!preferences.eli5Mode ? 'text-brand-700 dark:text-brand-300' : 'text-gray-600 dark:text-gray-400'}`}>
261
+ Academic
262
+ </span>
263
+ </div>
264
+ <p className="text-xs text-gray-500 dark:text-gray-400">
265
+ Technical terminology
266
+ </p>
267
+ </button>
268
+ </div>
269
+ </div>
270
+
271
+ {/* Typography Section */}
272
+ <div>
273
+ <label className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 block">
274
+ Typography Aids
275
+ </label>
276
+ <div className="space-y-2">
277
+ <label className="flex items-center justify-between p-3 rounded-xl bg-gray-50 dark:bg-gray-700/50 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
278
+ <div className="flex items-center gap-3">
279
+ <Type size={18} className={preferences.bionicReading ? 'text-emerald-500' : 'text-gray-400'} />
280
+ <div>
281
+ <span className="font-medium text-sm text-gray-700 dark:text-gray-300">Bionic Reading</span>
282
+ <p className="text-xs text-gray-500">Bo**ld** fi**rst** le**tters**</p>
283
+ </div>
284
+ </div>
285
+ <input
286
+ type="checkbox"
287
+ checked={preferences.bionicReading}
288
+ onChange={() => togglePreference('bionicReading')}
289
+ className="w-5 h-5 rounded border-gray-300 text-emerald-500 focus:ring-emerald-500"
290
+ />
291
+ </label>
292
+
293
+ <label className="flex items-center justify-between p-3 rounded-xl bg-gray-50 dark:bg-gray-700/50 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
294
+ <div className="flex items-center gap-3">
295
+ <Sparkles size={18} className={preferences.highlightKeyTerms ? 'text-purple-500' : 'text-gray-400'} />
296
+ <div>
297
+ <span className="font-medium text-sm text-gray-700 dark:text-gray-300">Highlight Key Terms</span>
298
+ <p className="text-xs text-gray-500">Color-code technical terms</p>
299
+ </div>
300
+ </div>
301
+ <input
302
+ type="checkbox"
303
+ checked={preferences.highlightKeyTerms}
304
+ onChange={() => togglePreference('highlightKeyTerms')}
305
+ className="w-5 h-5 rounded border-gray-300 text-purple-500 focus:ring-purple-500"
306
+ />
307
+ </label>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Audio Section */}
312
+ <div>
313
+ <label className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 block">
314
+ Audio Playback
315
+ </label>
316
+ <div className="space-y-3">
317
+ <label className="flex items-center justify-between p-3 rounded-xl bg-gray-50 dark:bg-gray-700/50 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
318
+ <div className="flex items-center gap-3">
319
+ <Volume2 size={18} className={preferences.ttsEnabled ? 'text-brand-500' : 'text-gray-400'} />
320
+ <div>
321
+ <span className="font-medium text-sm text-gray-700 dark:text-gray-300">Text-to-Speech</span>
322
+ <p className="text-xs text-gray-500">Listen while reading</p>
323
+ </div>
324
+ </div>
325
+ <input
326
+ type="checkbox"
327
+ checked={preferences.ttsEnabled}
328
+ onChange={() => togglePreference('ttsEnabled')}
329
+ className="w-5 h-5 rounded border-gray-300 text-brand-500 focus:ring-brand-500"
330
+ />
331
+ </label>
332
+
333
+ {preferences.ttsEnabled && (
334
+ <div className="px-3">
335
+ <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
336
+ <span>Speech Speed</span>
337
+ <span className="font-mono">{preferences.ttsSpeed.toFixed(1)}x</span>
338
+ </div>
339
+ <input
340
+ type="range"
341
+ min="0.5"
342
+ max="2"
343
+ step="0.1"
344
+ value={preferences.ttsSpeed}
345
+ onChange={(e) => handleSpeedChange(parseFloat(e.target.value))}
346
+ className="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer accent-brand-500"
347
+ />
348
+ <div className="flex justify-between text-[10px] text-gray-400 mt-1">
349
+ <span>Slow</span>
350
+ <span>Normal</span>
351
+ <span>Fast</span>
352
+ </div>
353
+ </div>
354
+ )}
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ )}
360
+ </div>
361
+ );
362
+ };
363
+
364
+ export default ReadingToolbar;
components/Sidebar.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import React, { useState } from 'react';
2
- import { BookOpen, ChevronRight, Loader2, CheckCircle } from 'lucide-react';
3
  import { BlogSection } from '../types';
4
 
5
  interface Props {
@@ -10,6 +10,24 @@ interface Props {
10
 
11
  const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) => {
12
  const [isCollapsed, setIsCollapsed] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  const handleClick = (id: string) => {
15
  onSectionClick(id);
@@ -167,39 +185,110 @@ const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) =
167
  </span>
168
  </div>
169
  <div className="h-1.5 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden">
170
- <div
171
- className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500"
172
- style={{
173
- width: `${(sections.filter(s => !s.isLoading).length / sections.length) * 100}%`
174
- }}
175
- />
176
- </div>
177
- </div>
178
- )}
179
-
180
- {/* Reading Progress */}
181
- <div>
182
- <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
183
- <span>Reading</span>
184
- <span>
185
- {sections.findIndex(s => s.id === activeSection) + 1}/{sections.length}
186
- </span>
187
- </div>
188
- <div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
189
  <div
190
- className="h-full bg-gradient-to-r from-gray-400 to-gray-500 dark:from-gray-600 dark:to-gray-500 rounded-full transition-all duration-500"
191
  style={{
192
- width: `${((sections.findIndex(s => s.id === activeSection) + 1) / sections.length) * 100}%`
193
  }}
194
  />
195
  </div>
196
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  </div>
198
- )}
199
- </div>
200
- </nav>
201
- );
202
- };
203
 
204
- export default Sidebar;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
 
 
1
+ import React, { useState, useMemo } from 'react';
2
+ import { BookOpen, ChevronRight, Loader2, CheckCircle, Map, Clock, Eye, Target } from 'lucide-react';
3
  import { BlogSection } from '../types';
4
 
5
  interface Props {
 
10
 
11
  const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) => {
12
  const [isCollapsed, setIsCollapsed] = useState(false);
13
+ const [showMinimap, setShowMinimap] = useState(true);
14
+
15
+ // Calculate reading progress
16
+ const readingStats = useMemo(() => {
17
+ const activeIndex = sections.findIndex(s => s.id === activeSection);
18
+ const completedCount = sections.filter(s => !s.isLoading && !s.error && s.content).length;
19
+ const totalWords = sections.reduce((acc, s) => acc + (s.content?.split(/\s+/).length || 0), 0);
20
+ const readingTimeMin = Math.max(1, Math.ceil(totalWords / 200));
21
+ const progressPercent = activeIndex >= 0 ? ((activeIndex + 1) / sections.length) * 100 : 0;
22
+
23
+ return {
24
+ activeIndex,
25
+ completedCount,
26
+ totalWords,
27
+ readingTimeMin,
28
+ progressPercent
29
+ };
30
+ }, [sections, activeSection]);
31
 
32
  const handleClick = (id: string) => {
33
  onSectionClick(id);
 
185
  </span>
186
  </div>
187
  <div className="h-1.5 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  <div
189
+ className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500"
190
  style={{
191
+ width: `${(sections.filter(s => !s.isLoading).length / sections.length) * 100}%`
192
  }}
193
  />
194
  </div>
195
  </div>
196
+ )}
197
+
198
+ {/* Reading Progress */}
199
+ <div>
200
+ <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
201
+ <span className="flex items-center gap-1">
202
+ <Eye size={10} />
203
+ Reading
204
+ </span>
205
+ <span>
206
+ {sections.findIndex(s => s.id === activeSection) + 1}/{sections.length}
207
+ </span>
208
+ </div>
209
+ <div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
210
+ <div
211
+ className="h-full bg-gradient-to-r from-gray-400 to-gray-500 dark:from-gray-600 dark:to-gray-500 rounded-full transition-all duration-500"
212
+ style={{
213
+ width: `${((sections.findIndex(s => s.id === activeSection) + 1) / sections.length) * 100}%`
214
+ }}
215
+ />
216
+ </div>
217
  </div>
 
 
 
 
 
218
 
219
+ {/* Mini-map Toggle */}
220
+ <button
221
+ onClick={() => setShowMinimap(!showMinimap)}
222
+ className={`
223
+ w-full flex items-center justify-between px-2 py-1.5 rounded-lg text-xs font-medium transition-colors
224
+ ${showMinimap
225
+ ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400'
226
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'
227
+ }
228
+ `}
229
+ >
230
+ <span className="flex items-center gap-1.5">
231
+ <Map size={12} />
232
+ Mini-map
233
+ </span>
234
+ <span className="text-[10px] opacity-60">
235
+ {showMinimap ? 'On' : 'Off'}
236
+ </span>
237
+ </button>
238
+
239
+ {/* Visual Mini-map */}
240
+ {showMinimap && (
241
+ <div className="p-2 rounded-lg bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800">
242
+ <div className="flex gap-1 flex-wrap">
243
+ {sections.map((section, index) => {
244
+ const isActive = activeSection === section.id;
245
+ const isPast = sections.findIndex(s => s.id === activeSection) > index;
246
+ const isLoading = section.isLoading;
247
+ const hasError = section.error;
248
+
249
+ return (
250
+ <button
251
+ key={section.id}
252
+ onClick={() => handleClick(section.id)}
253
+ disabled={isLoading}
254
+ className={`
255
+ w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold transition-all
256
+ ${isActive
257
+ ? 'bg-brand-500 text-white ring-2 ring-brand-500/30 scale-110'
258
+ : isPast
259
+ ? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
260
+ : isLoading
261
+ ? 'bg-gray-100 dark:bg-gray-800 text-gray-400 animate-pulse'
262
+ : hasError
263
+ ? 'bg-red-100 dark:bg-red-900/30 text-red-500'
264
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
265
+ }
266
+ `}
267
+ title={section.title}
268
+ >
269
+ {isActive ? <Target size={10} /> : index + 1}
270
+ </button>
271
+ );
272
+ })}
273
+ </div>
274
+ <p className="text-[10px] text-gray-400 text-center mt-2">
275
+ You are here: Section {readingStats.activeIndex + 1}
276
+ </p>
277
+ </div>
278
+ )}
279
+
280
+ {/* Estimated Time Remaining */}
281
+ <div className="flex items-center justify-center gap-2 text-[10px] text-gray-400 py-1">
282
+ <Clock size={10} />
283
+ <span>
284
+ ~{Math.max(1, readingStats.readingTimeMin - Math.ceil(readingStats.readingTimeMin * (readingStats.progressPercent / 100)))} min remaining
285
+ </span>
286
+ </div>
287
+ </div>
288
+ )}
289
+ </div>
290
+ </nav>
291
+ );
292
+ };
293
 
294
+ export default Sidebar;
components/SourceAnchor.tsx ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { ExternalLink, Quote, X, CheckCircle, AlertTriangle, HelpCircle, FileText } from 'lucide-react';
3
+ import { SourceAnchor as SourceAnchorType, ConfidenceLevel } from '../types';
4
+
5
+ interface Props {
6
+ anchor: SourceAnchorType;
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ const confidenceConfig: Record<ConfidenceLevel, {
11
+ label: string;
12
+ color: string;
13
+ bgColor: string;
14
+ borderColor: string;
15
+ icon: React.ReactNode;
16
+ description: string;
17
+ }> = {
18
+ 'direct-quote': {
19
+ label: 'Direct Quote',
20
+ color: 'text-green-700 dark:text-green-300',
21
+ bgColor: 'bg-green-50 dark:bg-green-900/30',
22
+ borderColor: 'border-green-200 dark:border-green-800',
23
+ icon: <Quote size={12} />,
24
+ description: 'This is a direct quote from the paper'
25
+ },
26
+ 'paraphrase': {
27
+ label: 'Paraphrased',
28
+ color: 'text-blue-700 dark:text-blue-300',
29
+ bgColor: 'bg-blue-50 dark:bg-blue-900/30',
30
+ borderColor: 'border-blue-200 dark:border-blue-800',
31
+ icon: <FileText size={12} />,
32
+ description: 'Closely paraphrased from source text'
33
+ },
34
+ 'synthesis': {
35
+ label: 'Synthesized',
36
+ color: 'text-purple-700 dark:text-purple-300',
37
+ bgColor: 'bg-purple-50 dark:bg-purple-900/30',
38
+ borderColor: 'border-purple-200 dark:border-purple-800',
39
+ icon: <CheckCircle size={12} />,
40
+ description: 'Combined from multiple parts of the paper'
41
+ },
42
+ 'interpretation': {
43
+ label: 'AI Analysis',
44
+ color: 'text-amber-700 dark:text-amber-300',
45
+ bgColor: 'bg-amber-50 dark:bg-amber-900/30',
46
+ borderColor: 'border-amber-200 dark:border-amber-800',
47
+ icon: <AlertTriangle size={12} />,
48
+ description: 'AI interpretation - verify with source'
49
+ }
50
+ };
51
+
52
+ const SourceAnchorComponent: React.FC<Props> = ({ anchor, children }) => {
53
+ const [isOpen, setIsOpen] = useState(false);
54
+ const config = confidenceConfig[anchor.confidence];
55
+
56
+ return (
57
+ <span className="relative inline">
58
+ {/* Anchor trigger - the highlighted claim text */}
59
+ <span
60
+ onClick={() => setIsOpen(!isOpen)}
61
+ className={`
62
+ inline cursor-pointer border-b-2 border-dashed transition-all
63
+ hover:border-solid
64
+ ${config.borderColor}
65
+ `}
66
+ style={{
67
+ borderBottomColor: anchor.highlightColor || undefined,
68
+ backgroundColor: isOpen ? `${anchor.highlightColor}15` : undefined
69
+ }}
70
+ >
71
+ {children}
72
+ <sup
73
+ className={`
74
+ ml-0.5 text-[10px] font-bold px-1 py-0.5 rounded
75
+ ${config.bgColor} ${config.color}
76
+ `}
77
+ >
78
+ {anchor.pageNumber ? `p.${anchor.pageNumber}` : '↗'}
79
+ </sup>
80
+ </span>
81
+
82
+ {/* Popup with source text */}
83
+ {isOpen && (
84
+ <>
85
+ {/* Backdrop */}
86
+ <div
87
+ className="fixed inset-0 z-40"
88
+ onClick={() => setIsOpen(false)}
89
+ />
90
+
91
+ {/* Popup */}
92
+ <div className={`
93
+ absolute z-50 left-0 top-full mt-2
94
+ w-80 sm:w-96
95
+ rounded-xl shadow-2xl border-2
96
+ ${config.bgColor} ${config.borderColor}
97
+ animate-in fade-in zoom-in-95 slide-in-from-top-2 duration-200
98
+ `}>
99
+ {/* Header */}
100
+ <div className={`
101
+ flex items-center justify-between px-4 py-3 border-b ${config.borderColor}
102
+ `}>
103
+ <div className="flex items-center gap-2">
104
+ <span className={config.color}>{config.icon}</span>
105
+ <span className={`text-sm font-bold ${config.color}`}>
106
+ {config.label}
107
+ </span>
108
+ {anchor.pageNumber && (
109
+ <span className="text-xs text-gray-500 dark:text-gray-400">
110
+ Page {anchor.pageNumber}
111
+ </span>
112
+ )}
113
+ </div>
114
+ <button
115
+ onClick={() => setIsOpen(false)}
116
+ className="p-1 rounded-lg hover:bg-white/50 dark:hover:bg-black/20 transition-colors"
117
+ >
118
+ <X size={14} className="text-gray-500" />
119
+ </button>
120
+ </div>
121
+
122
+ {/* Content */}
123
+ <div className="p-4 space-y-3">
124
+ {/* Original source text */}
125
+ <div>
126
+ <label className="text-[10px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1 block">
127
+ Original Text
128
+ </label>
129
+ <blockquote className="text-sm text-gray-700 dark:text-gray-300 bg-white/50 dark:bg-black/20 p-3 rounded-lg border-l-4 border-gray-300 dark:border-gray-600 italic">
130
+ "{anchor.sourceText}"
131
+ </blockquote>
132
+ </div>
133
+
134
+ {/* Confidence explanation */}
135
+ <div className={`
136
+ flex items-start gap-2 p-2 rounded-lg
137
+ ${anchor.confidence === 'interpretation'
138
+ ? 'bg-amber-100/50 dark:bg-amber-900/20'
139
+ : 'bg-white/30 dark:bg-black/10'
140
+ }
141
+ `}>
142
+ <HelpCircle size={14} className="flex-shrink-0 mt-0.5 text-gray-400" />
143
+ <p className="text-xs text-gray-600 dark:text-gray-400">
144
+ {config.description}
145
+ </p>
146
+ </div>
147
+ </div>
148
+
149
+ {/* Footer */}
150
+ <div className="px-4 py-3 border-t border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between">
151
+ <span className="text-[10px] text-gray-400">
152
+ Click outside to close
153
+ </span>
154
+ <button
155
+ onClick={() => {
156
+ // In a real implementation, this would scroll to the PDF location
157
+ console.log('Navigate to source:', anchor);
158
+ }}
159
+ className="flex items-center gap-1 text-xs font-medium text-brand-600 dark:text-brand-400 hover:underline"
160
+ >
161
+ <ExternalLink size={12} />
162
+ View in PDF
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </>
167
+ )}
168
+ </span>
169
+ );
170
+ };
171
+
172
+ // Inline confidence badge (for use in content)
173
+ export const ConfidenceBadge: React.FC<{ level: ConfidenceLevel; compact?: boolean }> = ({
174
+ level,
175
+ compact = false
176
+ }) => {
177
+ const config = confidenceConfig[level];
178
+
179
+ if (compact) {
180
+ return (
181
+ <span
182
+ className={`inline-flex items-center justify-center w-4 h-4 rounded-full ${config.bgColor} ${config.color}`}
183
+ title={config.label}
184
+ >
185
+ {config.icon}
186
+ </span>
187
+ );
188
+ }
189
+
190
+ return (
191
+ <span className={`
192
+ inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium
193
+ ${config.bgColor} ${config.color}
194
+ `}>
195
+ {config.icon}
196
+ <span>{config.label}</span>
197
+ </span>
198
+ );
199
+ };
200
+
201
+ export default SourceAnchorComponent;
components/TextToSpeech.tsx ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Settings, X } from 'lucide-react';
3
+
4
+ interface Props {
5
+ text: string;
6
+ sectionId: string;
7
+ onWordChange?: (wordIndex: number) => void;
8
+ onPlayingChange?: (isPlaying: boolean) => void;
9
+ }
10
+
11
+ interface TTSSettings {
12
+ rate: number;
13
+ pitch: number;
14
+ voice: SpeechSynthesisVoice | null;
15
+ }
16
+
17
+ const TextToSpeech: React.FC<Props> = ({
18
+ text,
19
+ sectionId,
20
+ onWordChange,
21
+ onPlayingChange
22
+ }) => {
23
+ const [isPlaying, setIsPlaying] = useState(false);
24
+ const [isPaused, setIsPaused] = useState(false);
25
+ const [currentWordIndex, setCurrentWordIndex] = useState(0);
26
+ const [showSettings, setShowSettings] = useState(false);
27
+ const [settings, setSettings] = useState<TTSSettings>({
28
+ rate: 1.0,
29
+ pitch: 1.0,
30
+ voice: null
31
+ });
32
+ const [availableVoices, setAvailableVoices] = useState<SpeechSynthesisVoice[]>([]);
33
+ const [progress, setProgress] = useState(0);
34
+
35
+ const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
36
+ const wordsRef = useRef<string[]>([]);
37
+
38
+ // Parse text into words
39
+ useEffect(() => {
40
+ // Strip markdown and split into words
41
+ const cleanText = text
42
+ .replace(/[#*_`\[\]()]/g, '')
43
+ .replace(/\n+/g, ' ')
44
+ .trim();
45
+ wordsRef.current = cleanText.split(/\s+/).filter(w => w.length > 0);
46
+ }, [text]);
47
+
48
+ // Load available voices
49
+ useEffect(() => {
50
+ const loadVoices = () => {
51
+ const voices = window.speechSynthesis.getVoices();
52
+ const englishVoices = voices.filter(v => v.lang.startsWith('en'));
53
+ setAvailableVoices(englishVoices);
54
+
55
+ // Set default voice (prefer a natural-sounding one)
56
+ if (!settings.voice && englishVoices.length > 0) {
57
+ const preferredVoice = englishVoices.find(v =>
58
+ v.name.includes('Samantha') ||
59
+ v.name.includes('Alex') ||
60
+ v.name.includes('Google') ||
61
+ v.name.includes('Natural')
62
+ ) || englishVoices[0];
63
+ setSettings(prev => ({ ...prev, voice: preferredVoice }));
64
+ }
65
+ };
66
+
67
+ loadVoices();
68
+ window.speechSynthesis.onvoiceschanged = loadVoices;
69
+
70
+ return () => {
71
+ window.speechSynthesis.cancel();
72
+ };
73
+ }, []);
74
+
75
+ // Notify parent of word changes
76
+ useEffect(() => {
77
+ onWordChange?.(currentWordIndex);
78
+ }, [currentWordIndex, onWordChange]);
79
+
80
+ // Notify parent of playing state changes
81
+ useEffect(() => {
82
+ onPlayingChange?.(isPlaying);
83
+ }, [isPlaying, onPlayingChange]);
84
+
85
+ const handlePlay = useCallback(() => {
86
+ if (isPaused) {
87
+ window.speechSynthesis.resume();
88
+ setIsPaused(false);
89
+ setIsPlaying(true);
90
+ return;
91
+ }
92
+
93
+ // Cancel any existing speech
94
+ window.speechSynthesis.cancel();
95
+
96
+ // Clean text for speech
97
+ const cleanText = text
98
+ .replace(/[#*_`\[\]()]/g, '')
99
+ .replace(/\n+/g, '. ')
100
+ .trim();
101
+
102
+ const utterance = new SpeechSynthesisUtterance(cleanText);
103
+ utterance.rate = settings.rate;
104
+ utterance.pitch = settings.pitch;
105
+ if (settings.voice) {
106
+ utterance.voice = settings.voice;
107
+ }
108
+
109
+ // Track word boundaries
110
+ let charIndex = 0;
111
+ utterance.onboundary = (event) => {
112
+ if (event.name === 'word') {
113
+ charIndex = event.charIndex;
114
+ // Estimate word index based on character position
115
+ const spokenText = cleanText.substring(0, charIndex);
116
+ const wordIndex = spokenText.split(/\s+/).length - 1;
117
+ setCurrentWordIndex(Math.max(0, wordIndex));
118
+ setProgress((charIndex / cleanText.length) * 100);
119
+ }
120
+ };
121
+
122
+ utterance.onend = () => {
123
+ setIsPlaying(false);
124
+ setIsPaused(false);
125
+ setCurrentWordIndex(0);
126
+ setProgress(0);
127
+ };
128
+
129
+ utterance.onerror = () => {
130
+ setIsPlaying(false);
131
+ setIsPaused(false);
132
+ };
133
+
134
+ utteranceRef.current = utterance;
135
+ window.speechSynthesis.speak(utterance);
136
+ setIsPlaying(true);
137
+ setIsPaused(false);
138
+ }, [text, settings, isPaused]);
139
+
140
+ const handlePause = useCallback(() => {
141
+ if (isPlaying) {
142
+ window.speechSynthesis.pause();
143
+ setIsPaused(true);
144
+ setIsPlaying(false);
145
+ }
146
+ }, [isPlaying]);
147
+
148
+ const handleStop = useCallback(() => {
149
+ window.speechSynthesis.cancel();
150
+ setIsPlaying(false);
151
+ setIsPaused(false);
152
+ setCurrentWordIndex(0);
153
+ setProgress(0);
154
+ }, []);
155
+
156
+ const handleSkipBack = useCallback(() => {
157
+ // Restart from beginning
158
+ handleStop();
159
+ setTimeout(handlePlay, 100);
160
+ }, [handleStop, handlePlay]);
161
+
162
+ const handleSkipForward = useCallback(() => {
163
+ // Skip to next section (stop current)
164
+ handleStop();
165
+ }, [handleStop]);
166
+
167
+ return (
168
+ <div className="relative">
169
+ {/* Main TTS Control Bar */}
170
+ <div className="flex items-center gap-3 p-3 rounded-xl bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
171
+ {/* Play/Pause Button */}
172
+ <button
173
+ onClick={isPlaying ? handlePause : handlePlay}
174
+ className={`
175
+ p-3 rounded-full transition-all duration-200
176
+ ${isPlaying
177
+ ? 'bg-brand-500 text-white shadow-lg shadow-brand-500/30 hover:bg-brand-600'
178
+ : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 shadow-sm'
179
+ }
180
+ `}
181
+ title={isPlaying ? 'Pause' : 'Play'}
182
+ >
183
+ {isPlaying ? <Pause size={20} /> : <Play size={20} className="ml-0.5" />}
184
+ </button>
185
+
186
+ {/* Skip Controls */}
187
+ <div className="flex items-center gap-1">
188
+ <button
189
+ onClick={handleSkipBack}
190
+ className="p-2 rounded-lg text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
191
+ title="Restart"
192
+ >
193
+ <SkipBack size={16} />
194
+ </button>
195
+ <button
196
+ onClick={handleSkipForward}
197
+ className="p-2 rounded-lg text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
198
+ title="Stop"
199
+ >
200
+ <SkipForward size={16} />
201
+ </button>
202
+ </div>
203
+
204
+ {/* Progress Bar */}
205
+ <div className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
206
+ <div
207
+ className="h-full bg-gradient-to-r from-brand-500 to-purple-500 transition-all duration-150"
208
+ style={{ width: `${progress}%` }}
209
+ />
210
+ </div>
211
+
212
+ {/* Speed Indicator */}
213
+ <span className="text-xs font-mono text-gray-500 dark:text-gray-400 min-w-[3rem] text-center">
214
+ {settings.rate.toFixed(1)}x
215
+ </span>
216
+
217
+ {/* Settings Button */}
218
+ <button
219
+ onClick={() => setShowSettings(!showSettings)}
220
+ className={`
221
+ p-2 rounded-lg transition-colors
222
+ ${showSettings
223
+ ? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400'
224
+ : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
225
+ }
226
+ `}
227
+ title="Settings"
228
+ >
229
+ <Settings size={16} />
230
+ </button>
231
+ </div>
232
+
233
+ {/* Settings Panel */}
234
+ {showSettings && (
235
+ <div className="absolute top-full left-0 right-0 mt-2 p-4 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-xl z-50 animate-in fade-in slide-in-from-top-2 duration-200">
236
+ <div className="flex items-center justify-between mb-4">
237
+ <h4 className="text-sm font-bold text-gray-900 dark:text-white">Audio Settings</h4>
238
+ <button
239
+ onClick={() => setShowSettings(false)}
240
+ className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
241
+ >
242
+ <X size={14} />
243
+ </button>
244
+ </div>
245
+
246
+ <div className="space-y-4">
247
+ {/* Speed Control */}
248
+ <div>
249
+ <label className="flex items-center justify-between text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
250
+ <span>Speed</span>
251
+ <span className="font-mono">{settings.rate.toFixed(1)}x</span>
252
+ </label>
253
+ <input
254
+ type="range"
255
+ min="0.5"
256
+ max="2"
257
+ step="0.1"
258
+ value={settings.rate}
259
+ onChange={(e) => setSettings(prev => ({ ...prev, rate: parseFloat(e.target.value) }))}
260
+ className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-brand-500"
261
+ />
262
+ <div className="flex justify-between text-[10px] text-gray-400 mt-1">
263
+ <span>0.5x</span>
264
+ <span>1x</span>
265
+ <span>1.5x</span>
266
+ <span>2x</span>
267
+ </div>
268
+ </div>
269
+
270
+ {/* Pitch Control */}
271
+ <div>
272
+ <label className="flex items-center justify-between text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
273
+ <span>Pitch</span>
274
+ <span className="font-mono">{settings.pitch.toFixed(1)}</span>
275
+ </label>
276
+ <input
277
+ type="range"
278
+ min="0.5"
279
+ max="1.5"
280
+ step="0.1"
281
+ value={settings.pitch}
282
+ onChange={(e) => setSettings(prev => ({ ...prev, pitch: parseFloat(e.target.value) }))}
283
+ className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-brand-500"
284
+ />
285
+ </div>
286
+
287
+ {/* Voice Selection */}
288
+ {availableVoices.length > 0 && (
289
+ <div>
290
+ <label className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-2 block">
291
+ Voice
292
+ </label>
293
+ <select
294
+ value={settings.voice?.name || ''}
295
+ onChange={(e) => {
296
+ const voice = availableVoices.find(v => v.name === e.target.value) || null;
297
+ setSettings(prev => ({ ...prev, voice }));
298
+ }}
299
+ className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 outline-none"
300
+ >
301
+ {availableVoices.map((voice) => (
302
+ <option key={voice.name} value={voice.name}>
303
+ {voice.name} ({voice.lang})
304
+ </option>
305
+ ))}
306
+ </select>
307
+ </div>
308
+ )}
309
+ </div>
310
+ </div>
311
+ )}
312
+
313
+ {/* Current word indicator (for debugging, usually hidden) */}
314
+ {/* <div className="mt-2 text-xs text-gray-400">
315
+ Word {currentWordIndex + 1} of {wordsRef.current.length}
316
+ </div> */}
317
+ </div>
318
+ );
319
+ };
320
+
321
+ export default TextToSpeech;
services/geminiService.ts CHANGED
@@ -369,10 +369,34 @@ const PAPER_STRUCTURE_SCHEMA = {
369
  required: ['paperTitle', 'paperAbstract', 'mainContribution', 'sections', 'keyTerms']
370
  };
371
 
372
- // Single Blog Section Schema
373
  const SINGLE_SECTION_SCHEMA = {
374
  type: Type.OBJECT,
375
  properties: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  content: { type: Type.STRING, description: "Rich Markdown content (300-500 words). Use **bold**, *italics*, bullet points, numbered lists, and ### subheadings." },
377
  visualizationType: {
378
  type: Type.STRING,
@@ -608,6 +632,25 @@ export const generateSingleBlogSection = async (
608
  WRITING INSTRUCTIONS:
609
  ${sectionTypeGuide[sectionPlan.sectionType]}
610
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  CONTENT REQUIREMENTS:
612
  - Write in distill.pub style: accessible yet technically accurate
613
  - Use Markdown formatting: **bold** for emphasis, bullet points for lists, ### for subheadings
@@ -627,6 +670,7 @@ export const generateSingleBlogSection = async (
627
  - Use real numbers from tables/figures
628
  - Choose the right chart type (bar for comparisons, line for trends)
629
  - Include axis labels
 
630
  ` : ''}
631
  ${sectionPlan.suggestedVisualization === 'equation' ? `
632
  Write the key equation in LaTeX-style notation:
@@ -672,7 +716,13 @@ export const generateSingleBlogSection = async (
672
  return {
673
  id: sectionPlan.id,
674
  title: sectionPlan.title,
675
- content: parsed.content,
 
 
 
 
 
 
676
  visualizationType: parsed.visualizationType,
677
  visualizationData: parsed.visualizationData,
678
  chartData: parsed.chartData,
 
369
  required: ['paperTitle', 'paperAbstract', 'mainContribution', 'sections', 'keyTerms']
370
  };
371
 
372
+ // Single Blog Section Schema with Progressive Disclosure Layers
373
  const SINGLE_SECTION_SCHEMA = {
374
  type: Type.OBJECT,
375
  properties: {
376
+ // Progressive Disclosure Layers (ADHD-Friendly)
377
+ layers: {
378
+ type: Type.OBJECT,
379
+ properties: {
380
+ thesis: {
381
+ type: Type.STRING,
382
+ description: "Tweet-length core thesis of this section (< 280 characters). The one key insight."
383
+ },
384
+ takeaways: {
385
+ type: Type.ARRAY,
386
+ items: { type: Type.STRING },
387
+ description: "3-5 key bullet points that summarize this section"
388
+ },
389
+ detailed: {
390
+ type: Type.STRING,
391
+ description: "Full detailed content in rich Markdown (300-500 words)"
392
+ },
393
+ eli5Content: {
394
+ type: Type.STRING,
395
+ description: "Same content but explained using simple everyday language and analogies, avoiding jargon"
396
+ }
397
+ },
398
+ required: ['thesis', 'takeaways', 'detailed']
399
+ },
400
  content: { type: Type.STRING, description: "Rich Markdown content (300-500 words). Use **bold**, *italics*, bullet points, numbered lists, and ### subheadings." },
401
  visualizationType: {
402
  type: Type.STRING,
 
632
  WRITING INSTRUCTIONS:
633
  ${sectionTypeGuide[sectionPlan.sectionType]}
634
 
635
+ ===== CRITICAL: PROGRESSIVE DISCLOSURE LAYERS (ADHD-FRIENDLY) =====
636
+ You MUST generate content in THREE LAYERS for this section:
637
+
638
+ LAYER 1 - THESIS (Tweet-Length < 280 chars):
639
+ Write the ONE key insight of this section that a reader absolutely must know.
640
+ Example: "Attention mechanisms let models focus on relevant parts of input, like a spotlight on important words."
641
+
642
+ LAYER 2 - TAKEAWAYS (3-5 Bullet Points):
643
+ Write 3-5 key bullet points that expand on the thesis.
644
+ Each bullet should be one actionable/memorable piece of information.
645
+
646
+ LAYER 3 - DETAILED (Full Markdown Content):
647
+ The full explanation with all the context, examples, and depth.
648
+
649
+ BONUS - ELI5 CONTENT (Simple Language Version):
650
+ Rewrite the detailed content using everyday language, analogies, and no jargon.
651
+ Imagine explaining to a smart 12-year-old.
652
+ =================================================================
653
+
654
  CONTENT REQUIREMENTS:
655
  - Write in distill.pub style: accessible yet technically accurate
656
  - Use Markdown formatting: **bold** for emphasis, bullet points for lists, ### for subheadings
 
670
  - Use real numbers from tables/figures
671
  - Choose the right chart type (bar for comparisons, line for trends)
672
  - Include axis labels
673
+ - Make it interactive: if appropriate, add slider config for "what-if" exploration
674
  ` : ''}
675
  ${sectionPlan.suggestedVisualization === 'equation' ? `
676
  Write the key equation in LaTeX-style notation:
 
716
  return {
717
  id: sectionPlan.id,
718
  title: sectionPlan.title,
719
+ content: parsed.content || parsed.layers?.detailed || '',
720
+ layers: parsed.layers ? {
721
+ thesis: parsed.layers.thesis || '',
722
+ takeaways: parsed.layers.takeaways || [],
723
+ detailed: parsed.layers.detailed || parsed.content || '',
724
+ eli5Content: parsed.layers.eli5Content
725
+ } : undefined,
726
  visualizationType: parsed.visualizationType,
727
  visualizationData: parsed.visualizationData,
728
  chartData: parsed.chartData,
style.css CHANGED
@@ -20,3 +20,115 @@ body {
20
  border: 1px solid rgba(255, 255, 255, 0.1);
21
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
22
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  border: 1px solid rgba(255, 255, 255, 0.1);
21
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
22
  }
23
+
24
+ /* ADHD-Friendly Features */
25
+
26
+ /* Bionic Reading Support */
27
+ .bionic-text {
28
+ font-feature-settings: 'kern' 1;
29
+ letter-spacing: 0.01em;
30
+ word-spacing: 0.05em;
31
+ }
32
+
33
+ /* TTS Karaoke Highlight */
34
+ .tts-highlight {
35
+ background: linear-gradient(120deg, rgba(14, 165, 233, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
36
+ padding: 2px 4px;
37
+ border-radius: 4px;
38
+ transition: all 150ms ease;
39
+ }
40
+
41
+ /* Progressive Disclosure Animations */
42
+ @keyframes layer-expand {
43
+ from {
44
+ opacity: 0;
45
+ transform: translateY(-10px) scale(0.98);
46
+ }
47
+ to {
48
+ opacity: 1;
49
+ transform: translateY(0) scale(1);
50
+ }
51
+ }
52
+
53
+ .layer-content {
54
+ animation: layer-expand 0.3s ease-out forwards;
55
+ }
56
+
57
+ /* Focus Indicators for Accessibility */
58
+ button:focus-visible,
59
+ a:focus-visible {
60
+ outline: 2px solid #0ea5e9;
61
+ outline-offset: 2px;
62
+ }
63
+
64
+ /* Reduce motion for users who prefer it */
65
+ @media (prefers-reduced-motion: reduce) {
66
+ *,
67
+ *::before,
68
+ *::after {
69
+ animation-duration: 0.01ms !important;
70
+ animation-iteration-count: 1 !important;
71
+ transition-duration: 0.01ms !important;
72
+ }
73
+ }
74
+
75
+ /* Custom scrollbar for the sidebar */
76
+ .custom-scrollbar {
77
+ scrollbar-width: thin;
78
+ scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
79
+ }
80
+
81
+ .custom-scrollbar::-webkit-scrollbar {
82
+ width: 6px;
83
+ }
84
+
85
+ .custom-scrollbar::-webkit-scrollbar-track {
86
+ background: transparent;
87
+ }
88
+
89
+ .custom-scrollbar::-webkit-scrollbar-thumb {
90
+ background-color: rgba(156, 163, 175, 0.3);
91
+ border-radius: 3px;
92
+ }
93
+
94
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
95
+ background-color: rgba(156, 163, 175, 0.5);
96
+ }
97
+
98
+ /* Source anchor highlight */
99
+ .source-anchor-highlight {
100
+ background: linear-gradient(
101
+ to bottom,
102
+ transparent 60%,
103
+ rgba(14, 165, 233, 0.3) 60%
104
+ );
105
+ }
106
+
107
+ /* Interactive chart hover effects */
108
+ .chart-interactive:hover {
109
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
110
+ }
111
+
112
+ /* Reading progress gradient */
113
+ .reading-progress-bar {
114
+ background: linear-gradient(
115
+ 90deg,
116
+ #0ea5e9 0%,
117
+ #8b5cf6 50%,
118
+ #ec4899 100%
119
+ );
120
+ }
121
+
122
+ /* Mini-map active indicator pulse */
123
+ @keyframes minimap-pulse {
124
+ 0%, 100% {
125
+ box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.4);
126
+ }
127
+ 50% {
128
+ box-shadow: 0 0 0 4px rgba(14, 165, 233, 0);
129
+ }
130
+ }
131
+
132
+ .minimap-active {
133
+ animation: minimap-pulse 2s ease-in-out infinite;
134
+ }
types.ts CHANGED
@@ -24,12 +24,35 @@ export interface ChatMessage {
24
  // Model types
25
  export type GeminiModel = 'gemini-2.5-flash' | 'gemini-3-pro-preview';
26
 
 
 
 
 
 
 
 
 
 
27
  export interface AppSettings {
28
  apiKey: string;
29
  model: GeminiModel;
30
  theme: 'light' | 'dark';
31
  layoutMode: 'auto' | 'grid' | 'list';
32
  useThinking: boolean;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
35
  export interface ProcessingStatus {
@@ -37,11 +60,25 @@ export interface ProcessingStatus {
37
  message?: string;
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  // Blog View Types
41
  export interface BlogSection {
42
  id: string;
43
  title: string;
44
- content: string; // Markdown
 
 
45
  visualizationType?: 'mermaid' | 'chart' | 'equation' | 'none';
46
  visualizationData?: string;
47
  chartData?: ChartData;
@@ -52,6 +89,24 @@ export interface BlogSection {
52
  error?: string;
53
  // Validation status
54
  validationStatus?: ValidationStatus;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  export interface ValidationStatus {
@@ -98,6 +153,7 @@ export interface MarginNote {
98
  export interface TechnicalTerm {
99
  term: string;
100
  definition: string;
 
101
  }
102
 
103
  export interface CollapsibleContent {
@@ -106,7 +162,7 @@ export interface CollapsibleContent {
106
  content: string;
107
  }
108
 
109
- // Chart Types
110
  export interface ChartData {
111
  type: 'bar' | 'line' | 'pie' | 'scatter' | 'area';
112
  title: string;
@@ -114,6 +170,39 @@ export interface ChartData {
114
  xAxis?: string;
115
  yAxis?: string;
116
  colors?: string[];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
 
119
  export type ViewMode = 'grid' | 'blog';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  // Model types
25
  export type GeminiModel = 'gemini-2.5-flash' | 'gemini-3-pro-preview';
26
 
27
+ // ADHD-Friendly Reading Preferences
28
+ export interface ReadingPreferences {
29
+ bionicReading: boolean;
30
+ eli5Mode: boolean; // Simple language mode
31
+ highlightKeyTerms: boolean;
32
+ ttsEnabled: boolean;
33
+ ttsSpeed: number; // 0.5 to 2.0
34
+ }
35
+
36
  export interface AppSettings {
37
  apiKey: string;
38
  model: GeminiModel;
39
  theme: 'light' | 'dark';
40
  layoutMode: 'auto' | 'grid' | 'list';
41
  useThinking: boolean;
42
+ readingPreferences: ReadingPreferences;
43
+ }
44
+
45
+ // Confidence level for AI-generated content
46
+ export type ConfidenceLevel = 'direct-quote' | 'paraphrase' | 'synthesis' | 'interpretation';
47
+
48
+ // Source anchor for zero-latency verification
49
+ export interface SourceAnchor {
50
+ id: string;
51
+ claimText: string;
52
+ sourceText: string;
53
+ pageNumber?: number;
54
+ confidence: ConfidenceLevel;
55
+ highlightColor: string;
56
  }
57
 
58
  export interface ProcessingStatus {
 
60
  message?: string;
61
  }
62
 
63
+ // Progressive Disclosure Layers (ADHD-Friendly)
64
+ export interface ContentLayers {
65
+ // Layer 1: Tweet-length core thesis (< 280 chars)
66
+ thesis: string;
67
+ // Layer 2: Key takeaways (3-5 bullet points)
68
+ takeaways: string[];
69
+ // Layer 3: Full detailed content (markdown)
70
+ detailed: string;
71
+ // ELI5 version for simple language mode
72
+ eli5Content?: string;
73
+ }
74
+
75
  // Blog View Types
76
  export interface BlogSection {
77
  id: string;
78
  title: string;
79
+ content: string; // Markdown (Layer 3 - detailed)
80
+ // Progressive disclosure layers
81
+ layers?: ContentLayers;
82
  visualizationType?: 'mermaid' | 'chart' | 'equation' | 'none';
83
  visualizationData?: string;
84
  chartData?: ChartData;
 
89
  error?: string;
90
  // Validation status
91
  validationStatus?: ValidationStatus;
92
+ // Source anchors for verification
93
+ sourceAnchors?: SourceAnchor[];
94
+ }
95
+
96
+ export interface ValidationStatus {
97
+ isValidated: boolean;
98
+ contentRelevance: ValidationResult;
99
+ visualizationValidity: ValidationResult;
100
+ overallScore: number; // 0-100
101
+ wasRepaired?: boolean;
102
+ repairAttempts?: number;
103
+ }
104
+
105
+ export interface ValidationResult {
106
+ passed: boolean;
107
+ score: number; // 0-100
108
+ issues: string[];
109
+ suggestions?: string[];
110
  }
111
 
112
  export interface ValidationStatus {
 
153
  export interface TechnicalTerm {
154
  term: string;
155
  definition: string;
156
+ highlightColor?: string; // Consistent color for this term throughout
157
  }
158
 
159
  export interface CollapsibleContent {
 
162
  content: string;
163
  }
164
 
165
+ // Interactive Chart Types (Playable Visualizations)
166
  export interface ChartData {
167
  type: 'bar' | 'line' | 'pie' | 'scatter' | 'area';
168
  title: string;
 
170
  xAxis?: string;
171
  yAxis?: string;
172
  colors?: string[];
173
+ // Interactive features
174
+ interactive?: {
175
+ enableSliders?: boolean;
176
+ sliderConfig?: SliderConfig[];
177
+ enableToggles?: boolean;
178
+ toggleableDatasets?: string[];
179
+ };
180
+ }
181
+
182
+ // Slider configuration for interactive charts
183
+ export interface SliderConfig {
184
+ id: string;
185
+ label: string;
186
+ min: number;
187
+ max: number;
188
+ step: number;
189
+ defaultValue: number;
190
+ affectsVariable: string; // Which variable this slider modifies
191
  }
192
 
193
  export type ViewMode = 'grid' | 'blog';
194
+
195
+ // Text-to-Speech state
196
+ export interface TTSState {
197
+ isPlaying: boolean;
198
+ currentSectionId: string | null;
199
+ currentWordIndex: number;
200
+ speed: number;
201
+ }
202
+
203
+ // Wayfinding / Progress tracking
204
+ export interface ReadingProgress {
205
+ sectionId: string;
206
+ percentComplete: number;
207
+ timeSpent: number; // in seconds
208
+ }