Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions app/learn/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ const Chat: React.FC = () => {
const savedResponse = localStorage.getItem("chatResponse");
if (savedResponse) {
const parsedResponse = JSON.parse(savedResponse);
const moduleContent =
parsedResponse.data[0]?.module || "No content available";
const moduleContent = parsedResponse.explanation ||
parsedResponse.response ||
parsedResponse.summary ||
"No content available";
setMessages([{ id: Date.now(), content: moduleContent, sender: "ai" }]);
localStorage.removeItem("chatResponse");
}
Expand Down Expand Up @@ -88,7 +90,11 @@ const Chat: React.FC = () => {
files: [] // Future enhancement: add file upload support
});

const aiContent = response.data.data[0]?.module || "Sorry, I couldn't generate a response";
const aiContent = response.data.explanation ||
response.data.response ||
response.data.summary ||
response.data.learning_plan ||
"Sorry, I couldn't generate a response";

const aiMessage: Message = {
id: Date.now() + 1,
Expand Down
134 changes: 116 additions & 18 deletions app/learn/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,30 @@ export default function UploadModule() {
{ name: string; url: string }[]
>([]);

const validateFiles = (files: File[]): { validFiles: File[], invalidFiles: File[] } => {
return files.reduce((acc, file) => {
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
acc.validFiles.push(file);
} else {
acc.invalidFiles.push(file);
}
return acc;
}, { validFiles: [] as File[], invalidFiles: [] as File[] });
};

const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
// Only show the drag effect if at least one file is a PDF
const hasValidFile = Array.from(e.dataTransfer.items).some(item =>
item.type === 'application/pdf' ||
(item.kind === 'file' && item.type.includes('pdf'))
);
setIsDragging(hasValidFile);

// Add visual feedback for invalid files
if (!hasValidFile) {
e.dataTransfer.dropEffect = 'none';
}
};

const onDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
Expand All @@ -64,22 +85,92 @@ export default function UploadModule() {
e.preventDefault();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
await handleFiles(files);

const { validFiles, invalidFiles } = validateFiles(files);

if (invalidFiles.length > 0) {
toast.error(`${invalidFiles.length} file(s) were rejected. Only PDF files are allowed.`);
}

if (validFiles.length > 0) {
await handleFiles(validFiles);
}
};

const onFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
await handleFiles(files);
const files = Array.from(e.target.files || []);

// Validate file types - only allow PDFs
const invalidFiles = files.filter(file =>
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
);

if (invalidFiles.length > 0) {
toast.error('Only PDF files are allowed');
e.target.value = ''; // Clear the file input
return;
}

setUploading(true);
setProgress(0);

try {
const uploadedFiles = await Promise.all(
files.map(async (file) => {
const formData = new FormData();
formData.append("file", file);

const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});

if (!response.ok) throw new Error("Upload failed");

const data = await response.json();
return {
name: file.name,
url: data.url,
type: file.type,
};
})
);

setUploadedFiles((prev) => [...prev, ...uploadedFiles]);
toast.success("Files uploaded successfully");
}

catch (error) {
console.error("Upload error:", error);
toast.error("Error uploading files");
}

finally {
setUploading(false);
setProgress(0);
e.target.value = ''; // Clear the file input
}
};

const handleFiles = async (files: File[]) => {
// Double-check validation before uploading
const { validFiles, invalidFiles } = validateFiles(files);

if (invalidFiles.length > 0) {
toast.error('Only PDF files are allowed');
return;
}

if (validFiles.length === 0) {
return;
}

setUploading(true);
setProgress(0);
const uploadedData: { name: string; url: string }[] = [];

for (const file of files) {
for (const file of validFiles) {
try {
const { cdnUrl } = await client.uploadFile(file);
uploadedData.push({ name: file.name, url: cdnUrl });
Expand Down Expand Up @@ -160,6 +251,9 @@ export default function UploadModule() {
files: uploadedFiles.map((file) => file.url),
};

// Debug log
console.log("Sending payload:", payload);

const response = await fetch("http://127.0.0.1:5000/process-content", {
method: "POST",
headers: {
Expand All @@ -168,9 +262,17 @@ export default function UploadModule() {
body: JSON.stringify(payload),
});

if (!response.ok) throw new Error("Failed to process content");

const data = await response.json();
// Debug log
console.log("Response data:", data);

if (!response.ok) {
// Get the error message from the backend response
const errorMessage = data.error || "Failed to process content";
console.error("Backend error:", errorMessage); // Debug log
throw new Error(errorMessage);
}

console.log("Processed Data:", data);
localStorage.setItem("chatResponse", JSON.stringify(data));

Expand All @@ -181,8 +283,8 @@ export default function UploadModule() {
setNotes("");
router.push("/learn/chat");
} catch (error) {
console.error(error);
toast.error("There was an error processing your content");
console.error("Error details:", error); // Debug log
toast.error(error instanceof Error ? error.message : "There was an error processing your content");
}

setUploading(false);
Expand Down Expand Up @@ -266,31 +368,27 @@ export default function UploadModule() {
Drop your files here
</p>
<p className="text-gray-600 mt-2">
or click to select files
or click to select PDFs
</p>
</div>
</motion.div>

<div className="flex flex-wrap justify-center gap-3 text-sm text-gray-500">
<div className="flex items-center gap-1.5">
<FileText className="w-4 h-4" />
<span>PDFs</span>
<span>Lecture Slides</span>
</div>
<div className="flex items-center gap-1.5">
<Book className="w-4 h-4" />
<span>Presentations</span>
</div>
<div className="flex items-center gap-1.5">
<Video className="w-4 h-4" />
<span>Videos</span>
<span>Research Papers</span>
</div>
</div>

<div>
<input
type="file"
onChange={onFileSelect}
accept=".pdf,.ppt,.pptx,.mp4"
accept=".pdf"
multiple
className="hidden"
id="file-upload"
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function Home() {
</div>
<h3 className="text-2xl font-semibold">Drop Any Content</h3>
<p className="text-gray-600 text-lg">
PDFs, slides, videos, notes - we&apos;ll make them interactive.
Lecture slides, research papers, notes - we&apos;ll make them interactive.
</p>
</motion.div>

Expand Down
109 changes: 89 additions & 20 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ def process_interaction():
current_topic = data.get('current_topic')
active_subtopic = data.get('active_subtopic')
session_history = data.get('session_history')

# Process the interaction through the agent service
response = agent_service.start_new_topic(user_input, current_topic=current_topic, active_subtopic=active_subtopic, session_history=session_history)

# Convert the response to a dictionary
response_dict = response.to_dict()

return jsonify(response_dict)

except Exception as e:
print(f"Error processing interaction: {e}")
return jsonify({
'error': str(e)
}), 500

def generate_audio(text):
generator = pipeline(
text, voice='af_heart', # <= change voice here
Expand Down Expand Up @@ -150,8 +165,6 @@ def process_text2speech():
if not text:
return jsonify({"error": "No text provided"}), 400

if not text:
return jsonify({"error": "No text provided"}), 400
audio = generate_audio(text)

wav_file = io.BytesIO()
Expand All @@ -160,42 +173,98 @@ def process_text2speech():
return send_file(wav_file, mimetype='audio/wav', as_attachment=False)


# Process the interaction through the agent service
response = agent_service.start_new_topic(user_input, current_topic=current_topic, active_subtopic=active_subtopic, session_history=session_history)

# Convert the response to a dictionary
response_dict = response.to_dict()

return jsonify(response_dict)

def is_valid_pdf(file_url):
"""Check if the file is a valid PDF."""
try:
# For Uploadcare URLs, we can trust the file extension
if 'ucarecdn.com' in file_url:
return True

# For other URLs, check the content
response = requests.get(file_url, stream=True)
response.raise_for_status()

# Check content type header first
content_type = response.headers.get('content-type', '').lower()
if 'application/pdf' in content_type:
return True

# If no content type header, check magic numbers
magic_numbers = response.raw.read(4)
return magic_numbers.startswith(b'%PDF')
except Exception as e:
print(f"Error processing interaction: {e}")
return jsonify({
'error': str(e)
}), 500
print(f"Error validating PDF: {e}")
return False

@app.route('/process-content', methods=['POST'])
def process_content():
"""Process uploaded content."""
try:
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400

print("Received data:", data) # Debug log

notes = data.get('notes', '')
files = data.get('files', [])

# Process files if any
processed_files = []
all_text = []

# Add notes if provided
if notes.strip():
all_text.append(notes)

# Process each file
for file_url in files:
print(f"Processing file URL: {file_url}") # Debug log

# Skip empty URLs
if not file_url:
continue

# Validate PDF
if not is_valid_pdf(file_url):
print(f"Invalid PDF URL: {file_url}") # Debug log
return jsonify({
'error': f'Invalid or unsupported file format. Only PDF files are allowed.'
}), 400

local_file = download_file(file_url)
if local_file:
processed_files.append(local_file)
try:
text = extract_text_from_pdf(local_file)
if text:
all_text.append(text)
except Exception as e:
print(f"Error extracting text from PDF: {e}")
return jsonify({
'error': 'Could not extract text from PDF. Please ensure it is a valid PDF file with extractable text.'
}), 400

# If no content was processed, return error
if not all_text:
return jsonify({
'error': 'No content could be processed'
}), 400

# Combine all text and process with Gemini
combined_text = "\n\n".join(all_text)
processed_content = process_with_gemini(combined_text)

# TODO: Process the content and generate learning plan
# For now, return a mock response
response = [{
'learning_plan': f"Generated learning plan from {len(processed_files)} files and notes: {notes[:100]}..."
}]
if not processed_content:
return jsonify({
'error': 'Failed to process content with AI'
}), 500

return jsonify(response)
# Return the processed content
return jsonify({
'response': processed_content,
'status': 'success'
})

except Exception as e:
print(f"Error processing content: {e}")
Expand Down
File renamed without changes.
Loading