Alleinzellgaenger commited on
Commit
f444dc0
·
1 Parent(s): 8066d07

Implement chunk loading tips, and autoscrolling to conversation regarding a specific chunk

Browse files
backend/app.py CHANGED
@@ -34,14 +34,21 @@ class ChatMessage(BaseModel):
34
 
35
  class ChatRequest(BaseModel):
36
  messages: List[ChatMessage]
37
- chunk: Optional[str] = None
 
 
 
38
  document: Optional[str] = None
39
 
40
  @app.post("/api/chat")
41
  async def chat_endpoint(request: ChatRequest):
42
- print(f"💬 Received chat with {len(request.messages)} messages")
43
- # Use provided chunk and document, or fallback to hardcoded content
44
- chunk = request.chunk or "No specific chunk provided"
 
 
 
 
45
  document = request.document or """
46
  # Auswertung Versuch F44: Zeeman Effekt
47
  Dominic Holst, Moritz Pfau
@@ -163,25 +170,54 @@ async def chat_endpoint(request: ChatRequest):
163
 
164
  *Figure 9: Cadmium rote Linie*
165
  """
166
- # Create system prompt for research paper tutor
167
- system_prompt = f"""
168
- You are PaperMentor, an expert academic tutor. Your purpose is to guide a user to a deep, phenomenological understanding of an academic paper.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- The user's primary goal is to: "phänomenologisch verstehen, was passiert, was beobachtet wurde und warum das so ist, mit wenig Fokus auf Formeln, sondern Fokus auf intuitivem Verständnis und dem experimentellen Ansatz." (phenomenologically understand what is happening, what was observed, and why, with little focus on formulas but a strong focus on intuitive understanding and the experimental approach).
171
 
172
- Your entire interaction must be guided by this goal.
173
 
174
- You will be given a specific chunk of the paper to discuss, as well as the full document for context.
175
 
176
- ---
177
- Current Chunk:
178
- {chunk}
179
- ---
180
- Full Document for Context:
181
- {document}
182
- ---
183
 
184
- Your interaction must follow this specific conversational flow:
185
 
186
  1. **Greeting and Contextualization:**
187
  * Begin with a friendly greeting.
@@ -226,8 +262,12 @@ async def chat_endpoint(request: ChatRequest):
226
  for msg in request.messages
227
  if msg.role in ["user", "assistant"]
228
  ]
 
229
  if not any(msg["role"] == "user" for msg in anthropic_messages):
230
- return {"role": "assistant", "content": "I didn't receive your message. Could you please ask again?"}
 
 
 
231
 
232
  print("🤖 Calling Claude for chat response...")
233
  response = client.messages.create(
@@ -235,13 +275,9 @@ async def chat_endpoint(request: ChatRequest):
235
  max_tokens=10000,
236
  system=system_prompt, # system prompt here
237
  messages=anthropic_messages,
238
- thinking={
239
- "type": "enabled",
240
- "budget_tokens": 5000
241
- },
242
  )
243
 
244
- response_text = response.content[1].text
245
  print(f"✅ Received response from Claude: {response_text[:100]}...")
246
  return {"role": "assistant", "content": response_text}
247
 
 
34
 
35
  class ChatRequest(BaseModel):
36
  messages: List[ChatMessage]
37
+ chunk: Optional[str] = None # Legacy support
38
+ currentChunk: Optional[str] = None
39
+ nextChunk: Optional[str] = None
40
+ action: Optional[str] = None # 'skip', 'understood', or None
41
  document: Optional[str] = None
42
 
43
  @app.post("/api/chat")
44
  async def chat_endpoint(request: ChatRequest):
45
+ print(f"💬 Received chat with {len(request.messages)} messages, action: {request.action}")
46
+
47
+ # Use new format if available, otherwise fall back to legacy
48
+ current_chunk = request.currentChunk or request.chunk or "No specific chunk provided"
49
+ next_chunk = request.nextChunk or ""
50
+ action = request.action
51
+
52
  document = request.document or """
53
  # Auswertung Versuch F44: Zeeman Effekt
54
  Dominic Holst, Moritz Pfau
 
170
 
171
  *Figure 9: Cadmium rote Linie*
172
  """
173
+ # Create system prompt for research paper tutor with transition support
174
+ is_transition = action in ['skip', 'understood']
175
+
176
+ if is_transition:
177
+ system_prompt = f"""
178
+ You are PaperMentor, an expert academic tutor guiding the user through a continuous learning journey of an academic paper.
179
+
180
+ The user has just {action} the previous section and is transitioning to a new topic. This is part of a continuous conversation where you maintain context and adapt based on the user's actions.
181
+
182
+ User's Action: {action}
183
+
184
+ Previous Section:
185
+ {current_chunk}
186
+
187
+ New Section to Introduce:
188
+ {next_chunk}
189
+
190
+ Full Document for Context:
191
+ {document}
192
+
193
+ Your response should:
194
+ 1. **Acknowledge the transition**: Briefly reference their choice to {action} the previous section
195
+ 2. **Provide smooth continuity**: Connect the previous section to this new one naturally
196
+ 3. **Introduce the new section**: Present the new topic with enthusiasm and context
197
+ 4. **Adapt your approach**: If they skipped, perhaps adjust to be more engaging. If they understood, acknowledge their progress
198
+ 5. **Begin new exploration**: Start the 3-question sequence for this new section
199
+
200
+ Maintain the same conversational style and focus on phenomenological understanding.
201
+ """
202
+ else:
203
+ system_prompt = f"""
204
+ You are PaperMentor, an expert academic tutor. Your purpose is to guide a user to a deep, phenomenological understanding of an academic paper.
205
 
206
+ The user's primary goal is to: "phänomenologisch verstehen, was passiert, was beobachtet wurde und warum das so ist, mit wenig Fokus auf Formeln, sondern Fokus auf intuitivem Verständnis und dem experimentellen Ansatz." (phenomenologically understand what is happening, what was observed, and why, with little focus on formulas but a strong focus on intuitive understanding and the experimental approach).
207
 
208
+ Your entire interaction must be guided by this goal.
209
 
210
+ You will be given a specific chunk of the paper to discuss, as well as the full document for context.
211
 
212
+ ---
213
+ Current Chunk:
214
+ {current_chunk}
215
+ ---
216
+ Full Document for Context:
217
+ {document}
218
+ ---
219
 
220
+ Your interaction must follow this specific conversational flow:
221
 
222
  1. **Greeting and Contextualization:**
223
  * Begin with a friendly greeting.
 
262
  for msg in request.messages
263
  if msg.role in ["user", "assistant"]
264
  ]
265
+ # For transitions, add a dummy user message to trigger Claude response
266
  if not any(msg["role"] == "user" for msg in anthropic_messages):
267
+ if is_transition:
268
+ anthropic_messages.append({"role": "user", "content": "Please continue to the next section."})
269
+ else:
270
+ return {"role": "assistant", "content": "I didn't receive your message. Could you please ask again?"}
271
 
272
  print("🤖 Calling Claude for chat response...")
273
  response = client.messages.create(
 
275
  max_tokens=10000,
276
  system=system_prompt, # system prompt here
277
  messages=anthropic_messages,
 
 
 
 
278
  )
279
 
280
+ response_text = response.content[0].text
281
  print(f"✅ Received response from Claude: {response_text[:100]}...")
282
  return {"role": "assistant", "content": response_text}
283
 
frontend/src/App.css CHANGED
@@ -40,3 +40,18 @@
40
  .read-the-docs {
41
  color: #888;
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  .read-the-docs {
41
  color: #888;
42
  }
43
+
44
+ @keyframes fade-in {
45
+ from {
46
+ opacity: 0;
47
+ transform: translateY(5px);
48
+ }
49
+ to {
50
+ opacity: 1;
51
+ transform: translateY(0);
52
+ }
53
+ }
54
+
55
+ .animate-fade-in {
56
+ animation: fade-in 0.3s ease-out;
57
+ }
frontend/src/components/ChunkLoadingTips.jsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ const tips = [
4
+ "You can always skip a chunk if it's not relevant to your learning goals.",
5
+ "Be sure to mark a chunk as understood when you've grasped the concept.",
6
+ "Ask the mentor to confirm your understanding before moving forward.",
7
+ "Don't hesitate to ask follow-up questions about complex topics.",
8
+ "Use the previous chunk button to review earlier concepts.",
9
+ "The mentor adapts explanations based on your questions and responses.",
10
+ "Take your time - there's no rush to complete chunks quickly.",
11
+ "Ask for examples when abstract concepts seem unclear.",
12
+ "Request connections between current and previous chunks when helpful.",
13
+ "The mentor can explain mathematical formulas step by step."
14
+ ];
15
+
16
+ const ChunkLoadingTips = ({ message = "Preparing your document..." }) => {
17
+ const [currentTipIndex, setCurrentTipIndex] = useState(0);
18
+
19
+ useEffect(() => {
20
+ const interval = setInterval(() => {
21
+ setCurrentTipIndex((prev) => (prev + 1) % tips.length);
22
+ }, 4000); // Change tip every 4 seconds
23
+
24
+ return () => clearInterval(interval);
25
+ }, []);
26
+
27
+ return (
28
+ <div className="flex flex-col items-center justify-center h-full p-8 text-center">
29
+ {/* Loading spinner */}
30
+ <div className="relative mb-8">
31
+ <div className="w-12 h-12 border-4 border-blue-200 rounded-full animate-spin border-t-blue-600"></div>
32
+ </div>
33
+
34
+ {/* Main message */}
35
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
36
+ {message}
37
+ </h3>
38
+ <span key={currentTipIndex} className="inline-block animate-fade-in text-gray-500">
39
+ {tips[currentTipIndex]}
40
+ </span>
41
+ </div>
42
+ );
43
+ };
44
+
45
+ export default ChunkLoadingTips;
frontend/src/components/ChunkPanel.jsx CHANGED
@@ -2,32 +2,40 @@ import ReactMarkdown from 'react-markdown';
2
  import remarkMath from 'remark-math';
3
  import rehypeKatex from 'rehype-katex';
4
  import rehypeRaw from 'rehype-raw';
5
- import { getChatMarkdownComponents } from '../utils/markdownComponents.jsx';
6
  import SimpleChat from './SimpleChat.jsx';
 
7
  import React, { useState, useEffect } from 'react';
8
 
9
  const ChunkPanel = ({
10
  documentData,
11
  currentChunkIndex,
12
  showChat,
13
- updateChunkChatHistory,
14
- getCurrentChunkChatHistory,
 
 
 
 
 
 
15
  setWaitingForFirstResponse,
16
  markChunkUnderstood,
17
- skipChunk
 
18
  }) => {
19
 
20
  const chatMarkdownComponents = getChatMarkdownComponents();
 
21
  const [isLoading, setIsLoading] = useState(false);
22
 
23
- // Generate greeting only when navigating to a chunk that has no chat history
 
24
  useEffect(() => {
25
- const hasNoMessages = (getCurrentChunkChatHistory()?.length ?? 0) === 0;
26
-
27
- if (documentData && showChat && hasNoMessages) {
28
  generateGreeting();
29
  }
30
- }, [currentChunkIndex]); // Only trigger on chunk navigation
31
 
32
  const generateGreeting = async () => {
33
  setIsLoading(true);
@@ -40,27 +48,29 @@ const ChunkPanel = ({
40
  headers: { 'Content-Type': 'application/json' },
41
  body: JSON.stringify({
42
  messages: [],
43
- chunk: documentData?.chunks?.[currentChunkIndex]?.text || '',
44
  document: documentData ? JSON.stringify(documentData) : ''
45
  })
46
  });
47
 
48
  const data = await response.json();
49
 
50
- updateChunkChatHistory([
51
  {
52
  role: 'assistant',
53
- content: data.content || 'Hi! Ask me anything about this section.'
54
- }
55
- ]);
 
56
  } catch (error) {
57
  console.error('Error generating greeting:', error);
58
- updateChunkChatHistory([
59
  {
60
  role: 'assistant',
61
- content: 'Hi! Ask me anything about this section.'
62
- }
63
- ]);
 
64
  } finally {
65
  setIsLoading(false);
66
  if (setWaitingForFirstResponse) {
@@ -70,9 +80,8 @@ const ChunkPanel = ({
70
  };
71
 
72
  const handleSend = async (text) => {
73
- const userMessage = { role: 'user', content: text };
74
- const newMessages = [...getCurrentChunkChatHistory(), userMessage];
75
- updateChunkChatHistory(newMessages);
76
  setIsLoading(true);
77
 
78
  try {
@@ -80,23 +89,23 @@ const ChunkPanel = ({
80
  method: 'POST',
81
  headers: { 'Content-Type': 'application/json' },
82
  body: JSON.stringify({
83
- messages: newMessages,
84
- chunk: documentData?.chunks?.[currentChunkIndex]?.text || '',
85
  document: documentData ? JSON.stringify(documentData) : ''
86
  })
87
  });
88
 
89
  const data = await response.json();
90
- updateChunkChatHistory([
91
- ...newMessages,
92
- { role: 'assistant', content: data.content || 'Sorry, no response received.' }
93
- ]);
94
  } catch (error) {
95
  console.error('Error:', error);
96
- updateChunkChatHistory([
97
- ...newMessages,
98
- { role: 'assistant', content: 'Sorry, something went wrong. Please try again.' }
99
- ]);
100
  } finally {
101
  setIsLoading(false);
102
  }
@@ -106,24 +115,44 @@ const ChunkPanel = ({
106
  <>
107
  {/* Chunk Header */}
108
  <div className="px-6 py-4 flex-shrink-0 bg-white rounded-t-lg border-b border-gray-200 z-10 flex items-center justify-between">
109
- <div className="font-semibold text-gray-900 text-left flex-1">
110
- <ReactMarkdown
111
- remarkPlugins={[remarkMath]}
112
- rehypePlugins={[rehypeRaw, rehypeKatex]}
113
- components={{
114
- p: ({ children }) => <span>{children}</span>, // Render as inline span
115
- ...chatMarkdownComponents
116
- }}
117
  >
118
- {documentData?.chunks?.[currentChunkIndex]?.topic || "Loading..."}
119
- </ReactMarkdown>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
 
122
  <div className="flex items-center gap-2">
123
  {/* Skip Button */}
124
  <button
125
  onClick={skipChunk}
126
- className="p-2 rounded-full bg-red-100 hover:bg-red-200 text-red-600 transition-colors duration-200"
127
  title="Skip this chunk"
128
  >
129
  <svg
@@ -166,14 +195,26 @@ const ChunkPanel = ({
166
 
167
  </div>
168
 
169
- {/* Chat Interface - Only shown when showChat is true */}
170
- {showChat && (
171
  <SimpleChat
172
- messages={getCurrentChunkChatHistory()}
 
 
173
  onSend={handleSend}
174
- isLoading={isLoading}
175
  />
176
  )}
 
 
 
 
 
 
 
 
 
 
177
  </>
178
  );
179
  };
 
2
  import remarkMath from 'remark-math';
3
  import rehypeKatex from 'rehype-katex';
4
  import rehypeRaw from 'rehype-raw';
5
+ import { getChatMarkdownComponents, getTitleMarkdownComponents } from '../utils/markdownComponents.jsx';
6
  import SimpleChat from './SimpleChat.jsx';
7
+ import ChunkLoadingTips from './ChunkLoadingTips.jsx';
8
  import React, { useState, useEffect } from 'react';
9
 
10
  const ChunkPanel = ({
11
  documentData,
12
  currentChunkIndex,
13
  showChat,
14
+ isTransitioning,
15
+ updateGlobalChatHistory,
16
+ getGlobalChatHistory,
17
+ addMessageToChunk,
18
+ getCurrentChunkMessages,
19
+ hasChunkMessages,
20
+ isChunkCompleted,
21
+ canEditChunk,
22
  setWaitingForFirstResponse,
23
  markChunkUnderstood,
24
+ skipChunk,
25
+ goToPrevChunk
26
  }) => {
27
 
28
  const chatMarkdownComponents = getChatMarkdownComponents();
29
+ const titleMarkdownComponents = getTitleMarkdownComponents();
30
  const [isLoading, setIsLoading] = useState(false);
31
 
32
+ // Generate greeting for chunks that don't have messages yet
33
+ // Only for initial chunk (0) and when not transitioning
34
  useEffect(() => {
35
+ if (documentData && showChat && !hasChunkMessages(currentChunkIndex) && currentChunkIndex === 0 && !isTransitioning) {
 
 
36
  generateGreeting();
37
  }
38
+ }, [currentChunkIndex, documentData, showChat, isTransitioning]);
39
 
40
  const generateGreeting = async () => {
41
  setIsLoading(true);
 
48
  headers: { 'Content-Type': 'application/json' },
49
  body: JSON.stringify({
50
  messages: [],
51
+ currentChunk: documentData?.chunks?.[currentChunkIndex]?.text || '',
52
  document: documentData ? JSON.stringify(documentData) : ''
53
  })
54
  });
55
 
56
  const data = await response.json();
57
 
58
+ addMessageToChunk(
59
  {
60
  role: 'assistant',
61
+ content: data.content || 'Hi! Welcome to your learning session. Let\'s explore this document together!'
62
+ },
63
+ currentChunkIndex
64
+ );
65
  } catch (error) {
66
  console.error('Error generating greeting:', error);
67
+ addMessageToChunk(
68
  {
69
  role: 'assistant',
70
+ content: 'Hi! Welcome to your learning session. Let\'s explore this document together!'
71
+ },
72
+ currentChunkIndex
73
+ );
74
  } finally {
75
  setIsLoading(false);
76
  if (setWaitingForFirstResponse) {
 
80
  };
81
 
82
  const handleSend = async (text) => {
83
+ const userMessage = { role: 'user', content: text, chunkIndex: currentChunkIndex };
84
+ addMessageToChunk(userMessage, currentChunkIndex);
 
85
  setIsLoading(true);
86
 
87
  try {
 
89
  method: 'POST',
90
  headers: { 'Content-Type': 'application/json' },
91
  body: JSON.stringify({
92
+ messages: getGlobalChatHistory(),
93
+ currentChunk: documentData?.chunks?.[currentChunkIndex]?.text || '',
94
  document: documentData ? JSON.stringify(documentData) : ''
95
  })
96
  });
97
 
98
  const data = await response.json();
99
+ addMessageToChunk(
100
+ { role: 'assistant', content: data.content || 'Sorry, no response received.' },
101
+ currentChunkIndex
102
+ );
103
  } catch (error) {
104
  console.error('Error:', error);
105
+ addMessageToChunk(
106
+ { role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
107
+ currentChunkIndex
108
+ );
109
  } finally {
110
  setIsLoading(false);
111
  }
 
115
  <>
116
  {/* Chunk Header */}
117
  <div className="px-6 py-4 flex-shrink-0 bg-white rounded-t-lg border-b border-gray-200 z-10 flex items-center justify-between">
118
+ <div className="flex items-center flex-1">
119
+ {/* Previous Chunk Button */}
120
+ <button
121
+ onClick={goToPrevChunk}
122
+ disabled={currentChunkIndex === 0}
123
+ className="mr-3 p-2 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
124
+ title="Go to previous chunk"
 
125
  >
126
+ <svg
127
+ className="w-5 h-5"
128
+ fill="currentColor"
129
+ viewBox="0 0 20 20"
130
+ >
131
+ <path
132
+ fillRule="evenodd"
133
+ d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
134
+ clipRule="evenodd"
135
+ />
136
+ </svg>
137
+ </button>
138
+
139
+ {/* Chunk Title */}
140
+ <div className="font-semibold text-xl text-gray-900 text-left">
141
+ <ReactMarkdown
142
+ remarkPlugins={[remarkMath]}
143
+ rehypePlugins={[rehypeRaw, rehypeKatex]}
144
+ components={titleMarkdownComponents}
145
+ >
146
+ {documentData?.chunks?.[currentChunkIndex]?.topic || "Loading..."}
147
+ </ReactMarkdown>
148
+ </div>
149
  </div>
150
 
151
  <div className="flex items-center gap-2">
152
  {/* Skip Button */}
153
  <button
154
  onClick={skipChunk}
155
+ className="p-2 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 transition-colors duration-200"
156
  title="Skip this chunk"
157
  >
158
  <svg
 
195
 
196
  </div>
197
 
198
+ {/* Chat Interface - Only shown when showChat is true and not transitioning */}
199
+ {showChat && !isLoading && !isTransitioning && (
200
  <SimpleChat
201
+ messages={getGlobalChatHistory()}
202
+ currentChunkIndex={currentChunkIndex}
203
+ canEdit={canEditChunk(currentChunkIndex)}
204
  onSend={handleSend}
205
+ isLoading={isLoading || isTransitioning}
206
  />
207
  )}
208
+
209
+ {/* Loading Tips - Shown when generating greeting */}
210
+ {showChat && isLoading && !hasChunkMessages(currentChunkIndex) && (
211
+ <ChunkLoadingTips message="Preparing your lesson..." />
212
+ )}
213
+
214
+ {/* Transition Loading - Shown when moving between chunks */}
215
+ {showChat && isTransitioning && (
216
+ <ChunkLoadingTips message="Transitioning to next topic..." />
217
+ )}
218
  </>
219
  );
220
  };
frontend/src/components/DocumentProcessor.jsx CHANGED
@@ -9,8 +9,8 @@ import { usePanelResize } from '../hooks/usePanelResize';
9
  // Import components
10
  import LoadingAnimation from './LoadingAnimation';
11
  import DocumentViewer from './DocumentViewer';
12
- import ChunkNavigation from './ChunkNavigation';
13
  import ChunkPanel from './ChunkPanel';
 
14
 
15
  function DocumentProcessor() {
16
  // State for PDF navigation
@@ -35,6 +35,7 @@ function DocumentProcessor() {
35
  currentChunkIndex,
36
  chunkExpanded,
37
  showChat,
 
38
  goToNextChunk,
39
  goToPrevChunk,
40
  skipChunk,
@@ -43,8 +44,13 @@ function DocumentProcessor() {
43
  setChunkExpanded,
44
  setShowChat,
45
  setChunkAsInteractive,
46
- updateChunkChatHistory,
47
- getCurrentChunkChatHistory
 
 
 
 
 
48
  } = useChunkNavigation(documentData, null);
49
 
50
  const {
@@ -90,10 +96,6 @@ function DocumentProcessor() {
90
  );
91
  }
92
 
93
- if (processing || waitingForFirstResponse) {
94
- return <LoadingAnimation uploadProgress={uploadProgress} />;
95
- }
96
-
97
  if (!documentData) {
98
  return (
99
  <div className="h-screen bg-gray-50 flex items-center justify-center">
@@ -123,12 +125,17 @@ function DocumentProcessor() {
123
  style={{ cursor: isDragging ? 'col-resize' : 'default' }}
124
  >
125
  {/* Left Panel - Document */}
126
- <div style={{ width: `${leftPanelWidth}%`, height: '100%' }}>
127
- <DocumentViewer
128
- selectedFile={selectedFile}
129
- documentData={documentData}
130
- onPageChange={setPdfNavigation}
131
- />
 
 
 
 
 
132
  </div>
133
 
134
  {/* Resizable Divider */}
@@ -150,27 +157,33 @@ function DocumentProcessor() {
150
  <div
151
  className="flex flex-col"
152
  style={{ width: `${100 - leftPanelWidth}%` }}
153
- >
154
- {/* Navigation Bar */}
155
- <ChunkNavigation
156
- currentChunkIndex={currentChunkIndex}
157
- documentData={documentData}
158
- chunkStates={chunkStates}
159
- goToPrevChunk={goToPrevChunk}
160
- goToNextChunk={goToNextChunk}
161
- />
162
-
163
  {/* Chunk Panel */}
164
  <div className="flex-1 flex flex-col min-h-0 bg-white rounded-lg shadow-sm">
165
  <ChunkPanel
166
  documentData={documentData}
167
  currentChunkIndex={currentChunkIndex}
168
  showChat={showChat}
169
- updateChunkChatHistory={updateChunkChatHistory}
170
- getCurrentChunkChatHistory={getCurrentChunkChatHistory}
 
 
 
 
 
 
171
  setWaitingForFirstResponse={setWaitingForFirstResponse}
172
  markChunkUnderstood={markChunkUnderstood}
173
  skipChunk={skipChunk}
 
174
  />
175
  </div>
176
  </div>
 
9
  // Import components
10
  import LoadingAnimation from './LoadingAnimation';
11
  import DocumentViewer from './DocumentViewer';
 
12
  import ChunkPanel from './ChunkPanel';
13
+ import ProgressBar from './ProgressBar';
14
 
15
  function DocumentProcessor() {
16
  // State for PDF navigation
 
35
  currentChunkIndex,
36
  chunkExpanded,
37
  showChat,
38
+ isTransitioning,
39
  goToNextChunk,
40
  goToPrevChunk,
41
  skipChunk,
 
44
  setChunkExpanded,
45
  setShowChat,
46
  setChunkAsInteractive,
47
+ updateGlobalChatHistory,
48
+ getGlobalChatHistory,
49
+ addMessageToChunk,
50
+ getCurrentChunkMessages,
51
+ hasChunkMessages,
52
+ isChunkCompleted,
53
+ canEditChunk
54
  } = useChunkNavigation(documentData, null);
55
 
56
  const {
 
96
  );
97
  }
98
 
 
 
 
 
99
  if (!documentData) {
100
  return (
101
  <div className="h-screen bg-gray-50 flex items-center justify-center">
 
125
  style={{ cursor: isDragging ? 'col-resize' : 'default' }}
126
  >
127
  {/* Left Panel - Document */}
128
+ <div style={{ width: `${leftPanelWidth}%`, height: '100%' }} className="flex flex-col">
129
+
130
+
131
+ {/* Document Viewer */}
132
+ <div className="flex-1 min-h-0">
133
+ <DocumentViewer
134
+ selectedFile={selectedFile}
135
+ documentData={documentData}
136
+ onPageChange={setPdfNavigation}
137
+ />
138
+ </div>
139
  </div>
140
 
141
  {/* Resizable Divider */}
 
157
  <div
158
  className="flex flex-col"
159
  style={{ width: `${100 - leftPanelWidth}%` }}
160
+ > {/* Progress Bar */}
161
+ <div className="mb-4 flex-shrink-0">
162
+ <ProgressBar
163
+ currentChunkIndex={currentChunkIndex}
164
+ totalChunks={documentData?.chunks?.length || 0}
165
+ onChunkClick={null} // Start with linear progression only
166
+ />
167
+ </div>
168
+
 
169
  {/* Chunk Panel */}
170
  <div className="flex-1 flex flex-col min-h-0 bg-white rounded-lg shadow-sm">
171
  <ChunkPanel
172
  documentData={documentData}
173
  currentChunkIndex={currentChunkIndex}
174
  showChat={showChat}
175
+ isTransitioning={isTransitioning}
176
+ updateGlobalChatHistory={updateGlobalChatHistory}
177
+ getGlobalChatHistory={getGlobalChatHistory}
178
+ addMessageToChunk={addMessageToChunk}
179
+ getCurrentChunkMessages={getCurrentChunkMessages}
180
+ hasChunkMessages={hasChunkMessages}
181
+ isChunkCompleted={isChunkCompleted}
182
+ canEditChunk={canEditChunk}
183
  setWaitingForFirstResponse={setWaitingForFirstResponse}
184
  markChunkUnderstood={markChunkUnderstood}
185
  skipChunk={skipChunk}
186
+ goToPrevChunk={goToPrevChunk}
187
  />
188
  </div>
189
  </div>
frontend/src/components/ProgressBar.jsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const ProgressBar = ({
4
+ currentChunkIndex,
5
+ totalChunks,
6
+ onChunkClick = null // Optional: allow clicking on progress bar segments
7
+ }) => {
8
+ const progressPercentage = totalChunks > 0 ? ((currentChunkIndex) / totalChunks) * 100 : 0;
9
+
10
+ return (
11
+ <div className="w-full">
12
+ {/* Progress Label */}
13
+ <div className="flex justify-between items-center mb-2">
14
+ <span className="text-sm font-medium text-gray-700">
15
+ Progress: {Math.round(progressPercentage)}%
16
+ </span>
17
+ <span className="text-sm text-gray-500">
18
+ {currentChunkIndex + 1} of {totalChunks} sections
19
+ </span>
20
+ </div>
21
+
22
+ {/* Progress Bar */}
23
+ <div className="w-full bg-gray-200 rounded-lg h-3 overflow-hidden shadow-sm relative">
24
+ {/* Progress fill */}
25
+ <div
26
+ className="h-full bg-green-500 transition-all duration-500 ease-out rounded-lg"
27
+ style={{ width: `${progressPercentage}%` }}
28
+ />
29
+
30
+ {/* Optional: Clickable segments overlay */}
31
+ {onChunkClick && (
32
+ <div className="absolute inset-0 flex">
33
+ {Array.from({ length: totalChunks }, (_, index) => (
34
+ <button
35
+ key={index}
36
+ onClick={() => onChunkClick(index)}
37
+ className="flex-1 hover:bg-white hover:bg-opacity-20 transition-colors duration-200"
38
+ title={`Go to chunk ${index + 1}`}
39
+ />
40
+ ))}
41
+ </div>
42
+ )}
43
+ </div>
44
+ </div>
45
+ );
46
+ };
47
+
48
+ export default ProgressBar;
frontend/src/components/SimpleChat.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
  import remarkMath from 'remark-math';
4
  import rehypeKatex from 'rehype-katex';
@@ -6,42 +6,60 @@ import rehypeRaw from 'rehype-raw';
6
  import { getChatMarkdownComponents } from '../utils/markdownComponents.jsx';
7
 
8
 
9
- const SimpleChat = ({ messages, onSend, isLoading }) => {
10
  const [input, setInput] = useState('');
 
 
11
 
12
  const handleSubmit = (e) => {
13
  e.preventDefault();
14
- if (!input.trim() || isLoading) return;
15
  onSend(input.trim());
16
  setInput('');
17
  };
18
 
 
 
 
 
 
 
 
 
 
 
19
  return (
20
  <div className="flex flex-col h-full">
21
  {/* Messages */}
22
  <div className="flex-1 overflow-y-auto p-4 space-y-3">
23
- {messages.map((message, idx) => (
24
- <div
25
- key={idx}
26
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
27
- >
28
  <div
29
- className={`max-w-[90%] p-3 rounded-lg ${
30
- message.role === 'user'
31
- ? 'bg-gray-100 text-white'
32
- : 'bg-white text-gray-900'
33
- }`}
34
  >
35
- <ReactMarkdown
36
- remarkPlugins={[remarkMath]}
37
- rehypePlugins={[rehypeRaw, rehypeKatex]}
38
- components={getChatMarkdownComponents()}
39
- >
40
- {message.content}
41
- </ReactMarkdown>
 
 
 
 
 
 
 
 
42
  </div>
43
- </div>
44
- ))}
45
 
46
  {isLoading && (
47
  <div className="flex justify-start">
@@ -69,13 +87,13 @@ const SimpleChat = ({ messages, onSend, isLoading }) => {
69
  type="text"
70
  value={input}
71
  onChange={(e) => setInput(e.target.value)}
72
- placeholder="Type your message..."
73
- disabled={isLoading}
74
- className="flex-1 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
75
  />
76
  <button
77
  type="submit"
78
- disabled={!input.trim() || isLoading}
79
  className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
80
  >
81
  {isLoading ? '...' : 'Send'}
 
1
+ import { useState, useEffect, useRef } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
  import remarkMath from 'remark-math';
4
  import rehypeKatex from 'rehype-katex';
 
6
  import { getChatMarkdownComponents } from '../utils/markdownComponents.jsx';
7
 
8
 
9
+ const SimpleChat = ({ messages, currentChunkIndex, canEdit, onSend, isLoading }) => {
10
  const [input, setInput] = useState('');
11
+ const messagesEndRef = useRef(null);
12
+ const currentChunkStartRef = useRef(null);
13
 
14
  const handleSubmit = (e) => {
15
  e.preventDefault();
16
+ if (!input.trim() || isLoading || !canEdit) return;
17
  onSend(input.trim());
18
  setInput('');
19
  };
20
 
21
+ // Scroll to current chunk's first message when chunk changes
22
+ useEffect(() => {
23
+ if (currentChunkStartRef.current) {
24
+ currentChunkStartRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
25
+ }
26
+ }, [currentChunkIndex]);
27
+
28
+ // Find the first message of the current chunk
29
+ const currentChunkFirstMessageIndex = messages.findIndex(msg => msg.chunkIndex === currentChunkIndex);
30
+
31
  return (
32
  <div className="flex flex-col h-full">
33
  {/* Messages */}
34
  <div className="flex-1 overflow-y-auto p-4 space-y-3">
35
+ {messages.map((message, idx) => {
36
+ const isCurrentChunk = message.chunkIndex === currentChunkIndex;
37
+ const isFirstOfCurrentChunk = idx === currentChunkFirstMessageIndex;
38
+
39
+ return (
40
  <div
41
+ key={idx}
42
+ ref={isFirstOfCurrentChunk ? currentChunkStartRef : null}
43
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
 
 
44
  >
45
+ <div
46
+ className={`max-w-[90%] p-3 rounded-lg transition-opacity ${
47
+ message.role === 'user'
48
+ ? `bg-gray-100 text-white ${isCurrentChunk ? 'opacity-100' : 'opacity-40'}`
49
+ : `bg-white text-gray-900 ${isCurrentChunk ? 'opacity-100' : 'opacity-40'}`
50
+ }`}
51
+ >
52
+ <ReactMarkdown
53
+ remarkPlugins={[remarkMath]}
54
+ rehypePlugins={[rehypeRaw, rehypeKatex]}
55
+ components={getChatMarkdownComponents()}
56
+ >
57
+ {message.content}
58
+ </ReactMarkdown>
59
+ </div>
60
  </div>
61
+ );
62
+ })}
63
 
64
  {isLoading && (
65
  <div className="flex justify-start">
 
87
  type="text"
88
  value={input}
89
  onChange={(e) => setInput(e.target.value)}
90
+ placeholder={canEdit ? "Type your message..." : "This chunk is completed - navigation only"}
91
+ disabled={isLoading || !canEdit}
92
+ className="flex-1 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:text-gray-500"
93
  />
94
  <button
95
  type="submit"
96
+ disabled={!input.trim() || isLoading || !canEdit}
97
  className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
98
  >
99
  {isLoading ? '...' : 'Send'}
frontend/src/hooks/useChunkNavigation.js CHANGED
@@ -4,8 +4,10 @@ export const useChunkNavigation = (documentData, clearTypingAnimation) => {
4
  const [chunkStates, setChunkStates] = useState({});
5
  const [currentChunkIndex, setCurrentChunkIndex] = useState(0);
6
  const [chunkExpanded, setChunkExpanded] = useState(true);
7
- const [chunkChatHistories, setChunkChatHistories] = useState({});
8
  const [showChat, setShowChat] = useState(true);
 
 
9
 
10
  const goToNextChunk = () => {
11
  if (documentData && currentChunkIndex < documentData.chunks.length - 1) {
@@ -27,50 +29,67 @@ export const useChunkNavigation = (documentData, clearTypingAnimation) => {
27
  }
28
  };
29
 
30
- const skipChunk = () => {
31
- setChunkStates(prev => {
32
- const currentState = prev[currentChunkIndex];
33
- const newState = currentState === 'skipped' ? undefined : 'skipped';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- // Auto-advance to next chunk if setting to skipped (not toggling off)
36
- if (newState === 'skipped' && documentData && currentChunkIndex < documentData.chunks.length - 1) {
37
- setTimeout(() => {
38
- if (clearTypingAnimation) {
39
- clearTypingAnimation();
40
- }
41
- setCurrentChunkIndex(currentChunkIndex + 1);
42
- setChunkExpanded(true);
43
- }, 100); // Small delay to allow state update to complete
44
  }
45
-
46
- return {
47
- ...prev,
48
- [currentChunkIndex]: newState
49
- };
50
- });
 
 
 
 
 
 
 
51
  };
52
 
53
  const markChunkUnderstood = () => {
54
- setChunkStates(prev => {
55
- const currentState = prev[currentChunkIndex];
56
- const newState = currentState === 'understood' ? undefined : 'understood';
57
-
58
- // Auto-advance to next chunk if setting to understood (not toggling off)
59
- if (newState === 'understood' && documentData && currentChunkIndex < documentData.chunks.length - 1) {
60
- setTimeout(() => {
61
- if (clearTypingAnimation) {
62
- clearTypingAnimation();
63
- }
64
- setCurrentChunkIndex(currentChunkIndex + 1);
65
- setChunkExpanded(true);
66
- }, 100); // Small delay to allow state update to complete
67
- }
68
-
69
- return {
70
- ...prev,
71
- [currentChunkIndex]: newState
72
- };
73
- });
74
  };
75
 
76
  const startInteractiveLesson = (startChunkLessonFn) => {
@@ -82,21 +101,36 @@ export const useChunkNavigation = (documentData, clearTypingAnimation) => {
82
  };
83
 
84
  const setChunkAsInteractive = () => {
85
- setChunkStates(prev => ({
86
- ...prev,
87
- [currentChunkIndex]: 'interactive'
88
- }));
89
  };
90
 
91
- const updateChunkChatHistory = (messages) => {
92
- setChunkChatHistories(prev => ({
93
- ...prev,
94
- [currentChunkIndex]: messages
95
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  };
97
 
98
- const getCurrentChunkChatHistory = () => {
99
- return chunkChatHistories[currentChunkIndex] || [];
100
  };
101
 
102
  return {
@@ -104,6 +138,7 @@ export const useChunkNavigation = (documentData, clearTypingAnimation) => {
104
  currentChunkIndex,
105
  chunkExpanded,
106
  showChat,
 
107
  goToNextChunk,
108
  goToPrevChunk,
109
  skipChunk,
@@ -112,7 +147,12 @@ export const useChunkNavigation = (documentData, clearTypingAnimation) => {
112
  setChunkExpanded,
113
  setShowChat,
114
  setChunkAsInteractive,
115
- updateChunkChatHistory,
116
- getCurrentChunkChatHistory
 
 
 
 
 
117
  };
118
  };
 
4
  const [chunkStates, setChunkStates] = useState({});
5
  const [currentChunkIndex, setCurrentChunkIndex] = useState(0);
6
  const [chunkExpanded, setChunkExpanded] = useState(true);
7
+ const [globalChatHistory, setGlobalChatHistory] = useState([]);
8
  const [showChat, setShowChat] = useState(true);
9
+ const [isTransitioning, setIsTransitioning] = useState(false);
10
+ const [completedChunks, setCompletedChunks] = useState(new Set());
11
 
12
  const goToNextChunk = () => {
13
  if (documentData && currentChunkIndex < documentData.chunks.length - 1) {
 
29
  }
30
  };
31
 
32
+ const sendAutomatedMessage = async (action) => {
33
+ if (!documentData || currentChunkIndex >= documentData.chunks.length - 1) return;
34
+
35
+ setIsTransitioning(true);
36
+ const nextChunkIndex = currentChunkIndex + 1;
37
+ const nextChunk = documentData.chunks[nextChunkIndex];
38
+
39
+ // Mark current chunk as completed
40
+ setCompletedChunks(prev => new Set(prev).add(currentChunkIndex));
41
+
42
+ // Update chunk index immediately for UI feedback
43
+ setCurrentChunkIndex(nextChunkIndex);
44
+ setChunkExpanded(true);
45
+
46
+ // Check if we already have messages for this chunk
47
+ if (hasChunkMessages(nextChunkIndex)) {
48
+ // Don't generate new response, just navigate
49
+ setIsTransitioning(false);
50
+ return;
51
+ }
52
+
53
+ try {
54
+ const response = await fetch('/api/chat', {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({
58
+ messages: globalChatHistory,
59
+ currentChunk: documentData.chunks[currentChunkIndex]?.text || '',
60
+ nextChunk: nextChunk.text,
61
+ action: action,
62
+ document: documentData ? JSON.stringify(documentData) : ''
63
+ })
64
+ });
65
+
66
+ const data = await response.json();
67
+ addMessageToChunk(
68
+ { role: 'assistant', content: data.content || 'Let\'s continue to the next section.' },
69
+ nextChunkIndex
70
+ );
71
 
72
+ // Clear any animations after successful response
73
+ if (clearTypingAnimation) {
74
+ clearTypingAnimation();
 
 
 
 
 
 
75
  }
76
+ } catch (error) {
77
+ console.error('Error in automated transition:', error);
78
+ // Clear animations on error too
79
+ if (clearTypingAnimation) {
80
+ clearTypingAnimation();
81
+ }
82
+ } finally {
83
+ setIsTransitioning(false);
84
+ }
85
+ };
86
+
87
+ const skipChunk = () => {
88
+ return sendAutomatedMessage('skip');
89
  };
90
 
91
  const markChunkUnderstood = () => {
92
+ return sendAutomatedMessage('understood');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  };
94
 
95
  const startInteractiveLesson = (startChunkLessonFn) => {
 
101
  };
102
 
103
  const setChunkAsInteractive = () => {
104
+ // No longer tracking status - this is just for compatibility
 
 
 
105
  };
106
 
107
+ const updateGlobalChatHistory = (messages) => {
108
+ setGlobalChatHistory(messages);
109
+ };
110
+
111
+ const getGlobalChatHistory = () => {
112
+ return globalChatHistory;
113
+ };
114
+
115
+ const addMessageToChunk = (message, chunkIndex) => {
116
+ const messageWithChunk = { ...message, chunkIndex };
117
+ setGlobalChatHistory(prev => [...prev, messageWithChunk]);
118
+ };
119
+
120
+ const getCurrentChunkMessages = () => {
121
+ return globalChatHistory.filter(msg => msg.chunkIndex === currentChunkIndex);
122
+ };
123
+
124
+ const hasChunkMessages = (chunkIndex) => {
125
+ return globalChatHistory.some(msg => msg.chunkIndex === chunkIndex);
126
+ };
127
+
128
+ const isChunkCompleted = (chunkIndex) => {
129
+ return completedChunks.has(chunkIndex);
130
  };
131
 
132
+ const canEditChunk = (chunkIndex) => {
133
+ return chunkIndex === currentChunkIndex && !isChunkCompleted(chunkIndex);
134
  };
135
 
136
  return {
 
138
  currentChunkIndex,
139
  chunkExpanded,
140
  showChat,
141
+ isTransitioning,
142
  goToNextChunk,
143
  goToPrevChunk,
144
  skipChunk,
 
147
  setChunkExpanded,
148
  setShowChat,
149
  setChunkAsInteractive,
150
+ updateGlobalChatHistory,
151
+ getGlobalChatHistory,
152
+ addMessageToChunk,
153
+ getCurrentChunkMessages,
154
+ hasChunkMessages,
155
+ isChunkCompleted,
156
+ canEditChunk
157
  };
158
  };
frontend/src/utils/markdownComponents.jsx CHANGED
@@ -1,11 +1,13 @@
 
1
  export const getChatMarkdownComponents = () => ({
2
- p: ({ children }) => <p className="mb-2 text-gray-800 leading-relaxed">{children}</p>,
3
  h1: ({ children }) => <h1 className="text-xl font-bold mb-3 text-gray-900">{children}</h1>,
4
  h2: ({ children }) => <h2 className="text-lg font-bold mb-2 text-gray-900">{children}</h2>,
5
  h3: ({ children }) => <h3 className="text-base font-bold mb-2 text-gray-900">{children}</h3>,
6
  ul: ({ children }) => <ul className="mb-2 ml-4 list-disc">{children}</ul>,
7
  ol: ({ children }) => <ol className="mb-2 ml-4 list-decimal">{children}</ol>,
8
  li: ({ children }) => <li className="mb-1 text-gray-800">{children}</li>,
 
9
  strong: ({ children }) => <strong className="font-semibold text-gray-900">{children}</strong>,
10
  em: ({ children }) => <em className="italic">{children}</em>,
11
  code: ({ inline, children }) =>
@@ -19,4 +21,20 @@ export const getChatMarkdownComponents = () => ({
19
  {children}
20
  </blockquote>
21
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  });
 
1
+ // Default markdown components for chat content
2
  export const getChatMarkdownComponents = () => ({
3
+ p: ({ children }) => <p className="mb-3 text-gray-800 leading-relaxed">{children}</p>,
4
  h1: ({ children }) => <h1 className="text-xl font-bold mb-3 text-gray-900">{children}</h1>,
5
  h2: ({ children }) => <h2 className="text-lg font-bold mb-2 text-gray-900">{children}</h2>,
6
  h3: ({ children }) => <h3 className="text-base font-bold mb-2 text-gray-900">{children}</h3>,
7
  ul: ({ children }) => <ul className="mb-2 ml-4 list-disc">{children}</ul>,
8
  ol: ({ children }) => <ol className="mb-2 ml-4 list-decimal">{children}</ol>,
9
  li: ({ children }) => <li className="mb-1 text-gray-800">{children}</li>,
10
+ hr: () => <hr className="my-4 border-gray-300" />,
11
  strong: ({ children }) => <strong className="font-semibold text-gray-900">{children}</strong>,
12
  em: ({ children }) => <em className="italic">{children}</em>,
13
  code: ({ inline, children }) =>
 
21
  {children}
22
  </blockquote>
23
  )
24
+ });
25
+
26
+ // Title-specific markdown components with no bottom margins
27
+ export const getTitleMarkdownComponents = () => ({
28
+ p: ({ children }) => <span className="text-gray-900">{children}</span>,
29
+ h1: ({ children }) => <span className="text-xl font-bold text-gray-900">{children}</span>,
30
+ h2: ({ children }) => <span className="text-lg font-bold text-gray-900">{children}</span>,
31
+ h3: ({ children }) => <span className="text-base font-bold text-gray-900">{children}</span>,
32
+ strong: ({ children }) => <strong className="font-semibold text-gray-900">{children}</strong>,
33
+ em: ({ children }) => <em className="italic">{children}</em>,
34
+ code: ({ children }) => <code className="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">{children}</code>,
35
+ // Convert block elements to inline for titles
36
+ ul: ({ children }) => <span>{children}</span>,
37
+ ol: ({ children }) => <span>{children}</span>,
38
+ li: ({ children }) => <span>{children} </span>,
39
+ blockquote: ({ children }) => <span className="italic text-gray-700">{children}</span>
40
  });