import React, { useEffect, useRef, useState, useCallback } from 'react';
import { saveAs } from 'file-saver';
// We load PDF-LIB via CDN and access it globally as window.PDFLib
// Firebase Imports
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, getDoc, setDoc, getDocs, collection, query, where } from 'firebase/firestore';
// Gemini API Configuration
const API_KEY = ""; // If you want to use models other than gemini-2.5-flash-preview-09-2025 or imagen-3.0-generate-002, provide an API key here. Otherwise, leave this as-is.
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${API_KEY}`;
// Helper function to convert canvas to Blob
function toBlobAsync(canvas, type = 'image/png', quality = 0.92) {
return new Promise(res => canvas.toBlob(b => res(b), type, quality));
}
// --- Age Calculation Logic ---
function calculateAgeAndMeta(dob, tob) {
if (!dob) return null;
const dobDate = new Date(dob + (tob ? 'T' + tob : 'T00:00'));
const now = new Date();
if (isNaN(dobDate) || dobDate > now) {
console.error('Invalid or future date provided.');
return null;
}
let y = now.getFullYear() - dobDate.getFullYear();
let m = now.getMonth() - dobDate.getMonth();
let d = now.getDate() - dobDate.getDate();
if (d < 0) { m--; const prev = new Date(now.getFullYear(), now.getMonth(), 0); d += prev.getDate(); }
if (m < 0) { y--; m += 12; }
const totalDays = Math.floor((now - dobDate) / (24 * 60 * 60 * 1000));
const weekday = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dobDate.getDay()];
const zodiac = (d) => {
const day = d.getDate(); const month = d.getMonth() + 1;
if ((month == 1 && day >= 20) || (month == 2 && day <= 18)) return 'Aquarius';
if ((month == 2 && day >= 19) || (month == 3 && day <= 20)) return 'Pisces';
if ((month == 3 && day >= 21) || (month == 4 && day <= 19)) return 'Aries';
if ((month == 4 && day >= 20) || (month == 5 && day <= 20)) return 'Taurus';
if ((month == 5 && day >= 21) || (month == 6 && day <= 20)) return 'Gemini';
if ((month == 6 && day >= 21) || (month == 7 && day <= 22)) return 'Cancer';
if ((month == 7 && day >= 23) || (month == 8 && day <= 22)) return 'Leo';
if ((month == 8 && day >= 23) || (month == 9 && day <= 22)) return 'Virgo';
if ((month == 9 && day >= 23) || (month == 10 && day <= 22)) return 'Libra';
if ((month == 10 && day >= 23) || (month == 11 && day <= 21)) return 'Scorpio';
if ((month == 11 && day >= 22) || (month == 12 && day <= 21)) return 'Sagittarius';
return 'Capricorn';
};
return {
summary: `${y} yrs, ${m} mo, ${d} days (${totalDays} total days)`,
meta: `Born on ${weekday}, Zodiac: ${zodiac(dobDate)}`,
dobDate,
};
}
export default function App() {
const [tab, setTab] = useState('pdf-resizer'); // Default to PDF Resizer
// --- Firebase/Auth State ---
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
// --- State for PDF Library Loading (tracks both pdf.js and pdf-lib) ---
const [isPdfLibsLoaded, setIsPdfLibsLoaded] = useState(false);
// --- State for Age Calculator ---
const [dob, setDob] = useState('');
const [tob, setTob] = useState('');
const [ageSummary, setAgeSummary] = useState('—');
const [birthMeta, setBirthMeta] = useState('—');
const [countdown, setCountdown] = useState('—');
const progressRef = useRef(null);
const countdownTimerRef = useRef(null);
// --- State for Age Calculator LLM Feature (NEW) ---
const [birthdayInsight, setBirthdayInsight] = useState('');
const [isInsightLoading, setIsInsightLoading] = useState(false);
// --- State for Text Refiner LLM Feature (NEW) ---
const [refinerText, setRefinerText] = useState('');
const [refinerAction, setRefinerAction] = useState('Summarize');
const [refinedOutput, setRefinedOutput] = useState('');
const [isRefining, setIsRefining] = useState(false);
// --- State/Refs for Image Resizer ---
const [selectedImageFile, setSelectedImageFile] = useState(null);
const canvasRef = useRef(null);
const originalDimensionsRef = useRef({ width: 0, height: 0 });
const [imgParams, setImgParams] = useState({ width: '', height: '', percent: 100, kb: '' });
const [targetKb, setTargetKb] = useState(''); // State for Image KB reduction
// --- State for PDF Reader/Converter ---
const [pdfData, setPdfData] = useState(null);
const [pdfFileName, setPdfFileName] = useState('');
const pdfCanvasRef = useRef(null);
const [loadingPdf, setLoadingPdf] = useState(false);
const [pdfError, setPdfError] = useState(null);
// --- State for PDF Converter ---
const [converterFile, setConverterFile] = useState(null);
const [conversionType, setConversionType] = useState('PDF to JPG (Working)');
const [isConverting, setIsConverting] = useState(false);
// --- State for PDF Resizer/Optimizer ---
const [resizerFile, setResizerFile] = useState(null);
const [isResizing, setIsResizing] = useState(false);
const [resizerMessage, setResizerMessage] = useState('');
const [originalResizerSize, setOriginalResizerSize] = useState(null);
const [newResizerSize, setNewResizerSize] = useState(null);
const [targetPdfKb, setTargetPdfKb] = useState(''); // NEW: State for PDF Target KB
// --- Utility for robust API calls with exponential backoff ---
const exponentialBackoffFetch = async (url, options, maxRetries = 3) => {
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
console.warn(`Attempt ${i + 1}: Received status ${response.status}. Retrying...`);
} catch (error) {
console.error(`Attempt ${i + 1} failed with network error: ${error.message}. Retrying...`);
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
throw new Error("Failed to fetch from API after multiple retries.");
};
// --- Firebase Initialization and Auth ---
useEffect(() => {
try {
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
if (Object.keys(firebaseConfig).length === 0) {
console.error("Firebase config is missing. Cannot initialize Firebase.");
setIsAuthReady(true);
return;
}
const app = initializeApp(firebaseConfig);
const firestoreDb = getFirestore(app);
const firebaseAuth = getAuth(app);
setDb(firestoreDb);
setAuth(firebaseAuth);
onAuthStateChanged(firebaseAuth, async (user) => {
if (user) {
setUserId(user.uid);
setIsAuthReady(true);
} else {
if (initialAuthToken) {
try {
await signInWithCustomToken(firebaseAuth, initialAuthToken);
} catch (e) {
console.error("Error signing in with custom token:", e);
await signInAnonymously(firebaseAuth);
setUserId(firebaseAuth.currentUser?.uid || crypto.randomUUID());
setIsAuthReady(true);
}
} else {
await signInAnonymously(firebaseAuth);
setUserId(firebaseAuth.currentUser?.uid || crypto.randomUUID());
setIsAuthReady(true);
}
}
});
} catch (error) {
console.error("Firebase setup failed:", error);
setIsAuthReady(true);
}
return () => clearInterval(countdownTimerRef.current);
}, []);
// --- PDF.js and PDF-LIB Dynamic Loading (Fix for import error) ---
useEffect(() => {
const loadScript = (src, onLoadCallback) => {
if (document.querySelector(`script[src="${src}"]`)) {
return true; // Already exists
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = onLoadCallback;
document.head.appendChild(script);
return false;
};
const checkReadiness = () => {
// Check if both global objects are present
const isPdfJsReady = typeof window.pdfjsLib !== 'undefined';
const isPdfLibReady = typeof window.PDFLib !== 'undefined';
if (isPdfJsReady) {
// Set the worker source for pdf.js
window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.14.305/pdf.worker.min.js';
}
// Set the state true only if BOTH are loaded
setIsPdfLibsLoaded(isPdfJsReady && isPdfLibReady);
};
// 1. Load PDF.js (needed for Reader/Converter)
loadScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.14.305/pdf.js', checkReadiness);
// 2. Load PDF-LIB (needed for Resizer/Optimizer)
loadScript('https://unpkg.com/pdf-lib/dist/pdf-lib.js', checkReadiness);
// Initial check if they were already loaded (or after a script loaded)
checkReadiness();
}, []);
// --- Age Calculator Functions ---
const startCountdown = useCallback((dobDate, ref) => {
clearInterval(countdownTimerRef.current);
const now = new Date();
let next = new Date(now.getFullYear(), dobDate.getMonth(), dobDate.getDate(), dobDate.getHours() || 0, dobDate.getMinutes() || 0);
if (next <= now) next.setFullYear(next.getFullYear() + 1);
const updateCountdown = () => {
const current = new Date(); const diff = next - current;
if (diff <= 0) {
clearInterval(countdownTimerRef.current);
setCountdown('🎂 Happy Birthday!');
if (ref.current) ref.current.style.width = '100%';
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const secs = Math.floor((diff % (1000 * 60)) / 1000);
setCountdown(`${days}d ${hours}h ${mins}m ${secs}s`);
const last = new Date(next); last.setFullYear(next.getFullYear() - 1);
const total = next - last;
const passed = current - last;
const pct = Math.min(100, Math.round((passed / total) * 100));
if (ref.current) ref.current.style.width = pct + '%';
};
updateCountdown();
countdownTimerRef.current = setInterval(updateCountdown, 1000);
}, []);
const onComputeAge = () => {
// Clear previous insight when recalculating
setBirthdayInsight('');
const result = calculateAgeAndMeta(dob, tob);
if (result) {
setAgeSummary(result.summary);
setBirthMeta(result.meta);
startCountdown(result.dobDate, progressRef);
} else {
setAgeSummary('—');
setBirthMeta('—');
setCountdown('—');
}
};
// --- LLM Feature 1: Birthday Insight Generator ---
const generateBirthdayInsight = async () => {
if (!dob || !birthMeta || birthMeta === '—') {
setBirthdayInsight('Please compute your age first.');
return;
}
setIsInsightLoading(true);
setBirthdayInsight('Generating insight...');
// Extract weekday and zodiac from birthMeta
const match = birthMeta.match(/Born on (\w+), Zodiac: (\w+)/);
if (!match) {
setBirthdayInsight('Could not parse birth details.');
setIsInsightLoading(false);
return;
}
const weekday = match[1];
const zodiac = match[2];
const userQuery = `Generate a fun, concise, single-paragraph personality insight based on being born on a ${weekday} under the ${zodiac} sign. Focus on positive traits and destiny.`;
const payload = {
contents: [{ parts: [{ text: userQuery }] }],
systemInstruction: {
parts: [{ text: "Act as a friendly, imaginative astrologer providing a concise, single-paragraph personality summary. Do not include any title or introductory phrase, just the paragraph." }]
},
};
try {
const response = await exponentialBackoffFetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const text = result.candidates?.[0]?.content?.parts?.[0]?.text || 'Failed to generate insight.';
setBirthdayInsight(text);
} catch (error) {
console.error('Gemini API Insight Failed:', error);
setBirthdayInsight('Error: Failed to fetch personality insight.');
} finally {
setIsInsightLoading(false);
}
};
// --- End LLM Feature 1 ---
// --- LLM Feature 2: Text Refiner ---
const refineText = async () => {
if (!refinerText.trim()) {
setRefinedOutput('Please enter some text to refine.');
return;
}
setIsRefining(true);
setRefinedOutput('Refining text...');
let systemPrompt = "You are a professional text editor. Execute the user's request precisely. Do not add any conversational text, preamble, or conclusion, just the processed text.";
let userQuery = refinerText;
switch (refinerAction) {
case 'Summarize':
userQuery = `Summarize the following text concisely:\n\n${refinerText}`;
break;
case 'Paraphrase':
userQuery = `Paraphrase the following text, keeping the meaning the same but changing the sentence structure and vocabulary:\n\n${refinerText}`;
break;
case 'Change Tone to Formal':
userQuery = `Rewrite the following text in a highly formal and professional tone:\n\n${refinerText}`;
break;
case 'Change Tone to Casual':
userQuery = `Rewrite the following text in a casual and friendly tone:\n\n${refinerText}`;
break;
default:
break;
}
const payload = {
contents: [{ parts: [{ text: userQuery }] }],
systemInstruction: { parts: [{ text: systemPrompt }] },
};
try {
const response = await exponentialBackoffFetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const text = result.candidates?.[0]?.content?.parts?.[0]?.text || 'Failed to refine text.';
setRefinedOutput(text);
} catch (error) {
console.error('Gemini API Refiner Failed:', error);
setRefinedOutput('Error: Failed to communicate with the refinement service.');
} finally {
setIsRefining(false);
}
};
// --- End LLM Feature 2 ---
// --- Image Resizer Functions (Includes KB Reduction Logic) ---
const loadImageToCanvas = useCallback(async (file) => {
if (!file) return;
try {
const img = await createImageBitmap(file);
originalDimensionsRef.current = { width: img.width, height: img.height };
setImgParams({
width: img.width,
height: img.height,
percent: 100,
kb: (file.size / 1024).toFixed(2)
});
setSelectedImageFile(file);
setTargetKb(''); // Reset KB target
} catch (error) {
console.error("Error loading image:", error);
setSelectedImageFile(null);
}
}, []);
useEffect(() => {
const drawInitialImage = async () => {
const canvas = canvasRef.current;
if (!selectedImageFile || !canvas || tab !== 'image-resizer') return;
try {
const { width, height } = originalDimensionsRef.current;
if (!width || !height) { return; }
const img = await createImageBitmap(selectedImageFile);
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
} catch (error) {
console.error("Error drawing image to canvas:", error);
setSelectedImageFile(null);
}
};
drawInitialImage();
}, [selectedImageFile, tab]);
const onImageFileSelected = (e) => {
const file = e.target.files[0];
if (file) {
loadImageToCanvas(file);
}
};
const handleDimensionChange = (e) => {
const { name, value } = e.target;
const inputVal = value === '' ? '' : parseInt(value);
let newParams = { ...imgParams, [name]: inputVal };
const { width: originalW, height: originalH } = originalDimensionsRef.current;
if (!originalW || !originalH || isNaN(inputVal) || inputVal <= 0) {
setImgParams(newParams);
setTargetKb('');
return;
}
const aspectRatio = originalW / originalH;
if (name === 'width') {
const newHeight = Math.round(inputVal / aspectRatio);
const newPercent = Math.round((inputVal / originalW) * 100);
newParams = { ...newParams, height: newHeight, percent: newPercent };
} else if (name === 'height') {
const newWidth = Math.round(inputVal * aspectRatio);
const newPercent = Math.round((inputVal / originalH) * 100);
newParams = { ...newParams, width: newWidth, height: newHeight };
} else if (name === 'percent') {
const percentage = inputVal / 100;
const newWidth = Math.round(originalW * percentage);
const newHeight = Math.round(originalH * percentage);
newParams = { ...newParams, width: newWidth, height: newHeight };
}
setImgParams(newParams);
// Note: Target KB is separate from dimension changes
};
const resizeImage = async () => {
const file = selectedImageFile;
const canvas = canvasRef.current;
const { width: currentWidth, height: currentHeight } = imgParams;
const targetKbVal = parseInt(targetKb, 10);
if (!canvas || !file || (!currentWidth || !currentHeight)) {
console.error('Please load an image and set valid dimensions.');
return;
}
const mimeType = file.type || 'image/png';
const isJpeg = mimeType.startsWith('image/jpeg');
let finalBlob;
let finalWidth = currentWidth;
let finalHeight = currentHeight;
let img = await createImageBitmap(file);
// 1. Create an offscreen canvas to handle the new pixel dimensions
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = currentWidth;
offscreenCanvas.height = currentHeight;
offscreenCanvas.getContext('2d').drawImage(img, 0, 0, currentWidth, currentHeight);
// 2. KB REDUCTION LOGIC
if (targetKbVal > 0 && isJpeg) {
const targetBytes = targetKbVal * 1024;
let quality = 0.9;
let blob;
let attempts = 0;
const MAX_ATTEMPTS = 20;
console.log(`Attempting to compress JPEG image to approximately ${targetKbVal} KB...`);
while (quality > 0.1 && attempts < MAX_ATTEMPTS) {
attempts++;
// Generate Blob with current quality setting
blob = await toBlobAsync(offscreenCanvas, mimeType, quality);
if (blob.size <= targetBytes) {
finalBlob = blob;
console.log(`Compression successful at quality ${quality.toFixed(2)}: ${Math.round(blob.size / 1024)} KB.`);
break;
}
// If size is too big, estimate a better quality for the next iteration
const currentRatio = blob.size / targetBytes;
quality = Math.max(0.1, quality / currentRatio * 0.95); // Adjust quality
}
if (!finalBlob) {
finalBlob = blob; // Use the last, lowest quality blob
console.warn(`Could not reach target size ${targetKbVal} KB. Downloaded image size: ${Math.round(finalBlob.size / 1024)} KB at minimum quality.`);
}
} else if (targetKbVal > 0 && !isJpeg) {
console.warn('KB reduction is most effective on JPEG files. Applying pixel dimensions only.');
finalBlob = await toBlobAsync(offscreenCanvas, mimeType, 0.92);
} else {
// 3. Simple Resize/Default Quality
finalBlob = await toBlobAsync(offscreenCanvas, mimeType, 0.92);
}
// 4. Download and Update UI
saveAs(finalBlob, `processed_${file.name}`);
const finalImg = await createImageBitmap(finalBlob);
finalWidth = finalImg.width;
finalHeight = finalImg.height;
// Update the visible canvas preview
canvas.width = finalWidth;
canvas.height = finalHeight;
canvas.getContext('2d').clearRect(0, 0, finalWidth, finalHeight);
canvas.getContext('2d').drawImage(finalImg, 0, 0);
setImgParams(p => ({
...p,
width: finalWidth,
height: finalHeight,
kb: (finalBlob.size / 1024).toFixed(2), // Final size
}));
setTargetKb('');
};
// --- PDF Reader Functions ---
const onPdfSelected = (e, targetStateSetter, nameSetter) => {
const file = e.target.files[0];
if (file && file.type === 'application/pdf') {
setPdfError(null);
setPdfData(null);
if (nameSetter) nameSetter(file.name);
const reader = new FileReader();
reader.onload = (event) => {
targetStateSetter(event.target.result);
};
reader.readAsArrayBuffer(file);
} else {
targetStateSetter(null);
setPdfError('Please select a valid PDF file.');
if (nameSetter) nameSetter('');
}
};
const renderPdf = useCallback(async () => {
setPdfError(null);
if (typeof window.pdfjsLib === 'undefined') {
setPdfError('PDF.js library is not globally loaded. Cannot render PDF.');
setLoadingPdf(false);
return;
}
if (!pdfData) return;
setLoadingPdf(true);
try {
const loadingTask = window.pdfjsLib.getDocument({ data: pdfData });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const canvas = pdfCanvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
const viewportForScale = page.getViewport({ scale: 1 });
const containerWidth = canvas.parentNode.offsetWidth;
const scale = containerWidth / viewportForScale.width;
const viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height;
canvas.width = containerWidth;
const renderContext = { canvasContext: context, viewport: viewport };
await page.render(renderContext).promise;
} catch (error) {
console.error('Error rendering PDF:', error);
setPdfError('Failed to render PDF. Check console for details (e.g., corrupted file or missing worker).');
} finally {
setLoadingPdf(false);
}
}, [pdfData]);
useEffect(() => {
// Only trigger rendering if PDF libraries are loaded
if (pdfData && tab === 'pdf-reader' && isPdfLibsLoaded) {
renderPdf();
}
}, [pdfData, tab, renderPdf, isPdfLibsLoaded]);
// --- PDF Converter Implementation (Functional PDF to JPG) ---
const startConversion = async () => {
if (!converterFile || !conversionType.startsWith('PDF to JPG')) {
console.log('Conversion type not supported or file not selected.');
return;
}
setIsConverting(true);
setPdfError(null);
try {
if (typeof window.pdfjsLib === 'undefined') {
throw new Error('PDF.js library is required for this conversion.');
}
const loadingTask = window.pdfjsLib.getDocument({ data: converterFile });
const pdf = await loadingTask.promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = { canvasContext: context, viewport: viewport };
await page.render(renderContext).promise;
const blob = await toBlobAsync(canvas, 'image/jpeg', 0.90);
const baseName = pdfFileName.replace(/\.pdf$/i, '');
saveAs(blob, `${baseName}_page_${i}.jpg`);
}
console.log('PDF to JPG conversion complete. Files downloaded.');
} catch (error) {
console.error('Conversion failed:', error);
setPdfError(`Conversion failed: ${error.message}. Please check console.`);
} finally {
setIsConverting(false);
}
};
// --- PDF Resizer/Optimizer Logic ---
const handlePdfResizerFileSelect = (e) => {
const file = e.target.files[0];
if (file && file.type === 'application/pdf') {
setResizerMessage('');
setResizerFile(null);
setOriginalResizerSize((file.size / 1024).toFixed(2));
setNewResizerSize(null);
setTargetPdfKb(''); // Reset target KB on new file load
const reader = new FileReader();
reader.onload = (event) => {
setResizerFile({ arrayBuffer: event.target.result, name: file.name });
};
reader.readAsArrayBuffer(file);
} else {
setResizerFile(null);
setResizerMessage('Please select a valid PDF file.');
setOriginalResizerSize(null);
setNewResizerSize(null);
}
};
const optimizeAndSavePdf = async () => {
if (!resizerFile) {
setResizerMessage('Please load a PDF file first.');
return;
}
if (typeof window.PDFLib === 'undefined' || !window.PDFLib.PDFDocument) {
setResizerMessage('PDF-LIB is not fully loaded. Please wait a moment and try again.');
return;
}
const targetKbVal = parseInt(targetPdfKb, 10);
setIsResizing(true);
setNewResizerSize(null);
if (targetKbVal > 0) {
setResizerMessage(`Attempting aggressive structural optimization to reduce size towards ${targetKbVal} KB...`);
} else {
setResizerMessage('Processing PDF for structural optimization...');
}
try {
const { PDFDocument } = window.PDFLib; // Accessing the class globally
const existingPdfBytes = resizerFile.arrayBuffer;
// 1. Load the existing document from the ArrayBuffer
const existingPdfDoc = await PDFDocument.load(existingPdfBytes, {
ignoreEncryption: true,
});
// 2. Create a new, clean document
const newPdfDoc = await PDFDocument.create();
// 3. Copy all pages from the old to the new document (The core optimization step)
const pageIndices = existingPdfDoc.getPageIndices();
const copiedPages = await newPdfDoc.copyPages(existingPdfDoc, pageIndices);
copiedPages.forEach((page) => newPdfDoc.addPage(page));
// 4. Save the new, optimized PDF (using default compression)
const pdfBytes = await newPdfDoc.save();
const newBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const newSizeKb = (newBlob.size / 1024).toFixed(2);
saveAs(newBlob, `optimized_${resizerFile.name}`);
setNewResizerSize(newSizeKb);
let finalMessage = `Optimization complete! Compare sizes below.`;
if (targetKbVal > 0) {
finalMessage = `Optimization complete. Final Size: ${newSizeKb} KB. Note: Due to PDF complexity, a specific KB target is often not possible on the client-side, but this is the maximum structural reduction possible.`;
}
setResizerMessage(finalMessage);
} catch (error) {
console.error('PDF Optimization failed:', error);
setResizerMessage(`Optimization failed: ${error.message}. This may happen with heavily protected or corrupted PDFs.`);
} finally {
setIsResizing(false);
}
};
// --- Tab Configuration ---
const tabs = [
{ id: 'age', name: '👶 Age Calculator' },
{ id: 'text-refiner', name: '✍️ Text Refiner' },
{ id: 'image-resizer', name: '🖼️ Image Resizer' },
{ id: 'pdf-reader', name: '📖 PDF Reader' },
{ id: 'pdf-converter', name: '🔄 PDF Converter' },
{ id: 'pdf-resizer', name: '🗜️ PDF Resizer/Optimizer' },
];
const tabStyles = (id) => `
px-6 py-3 font-semibold cursor-pointer transition-all duration-200 ease-in-out rounded-t-lg text-sm sm:text-base whitespace-nowrap
${tab === id
? 'bg-white border-b-2 border-sky-600 text-sky-700 shadow-t-md'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
}
`;
if (!isAuthReady || !isPdfLibsLoaded) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-xl font-semibold text-sky-600">
{isAuthReady ? 'Loading PDF Dependencies...' : 'Initializing Service...'}
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 p-4 sm:p-8 font-sans">
<style>{`
/* Ensure canvas is fluid inside its container */
canvas { max-width: 100%; height: auto; display: block; margin: 0 auto; }
`}</style>
<div className="max-w-4xl mx-auto bg-white shadow-xl rounded-xl overflow-hidden">
<header className="p-6 bg-sky-600 text-white">
<h1 className="text-3xl font-extrabold tracking-tight">Utility Toolkit Dashboard</h1>
<p className="text-sky-100 mt-1">Client-side file utilities with Gemini LLM integration.</p>
</header>
{userId && (
<div className="p-2 bg-gray-50 border-b text-xs text-gray-600 font-mono text-center overflow-x-auto">
User ID: {userId}
</div>
)}
<div className="flex border-b border-gray-200 overflow-x-auto">
{tabs.map((t) => (
<div key={t.id} className={tabStyles(t.id)} onClick={() => setTab(t.id)}>
{t.name}
</div>
))}
</div>
<div className="p-6">
{/* --- AGE CALCULATOR --- */}
{tab === 'age' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">Calculate Your Age Precisely</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Date of Birth</label>
<input
type="date"
value={dob}
onChange={(e) => setDob(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-sky-500 focus:border-sky-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Time of Birth (Optional)</label>
<input
type="time"
value={tob}
onChange={(e) => setTob(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-sky-500 focus:border-sky-500"
/>
</div>
</div>
<button
onClick={onComputeAge}
className="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-3 rounded-lg transition duration-150"
>
Compute Age & Birthday Countdown
</button>
<div className="bg-sky-50 border-l-4 border-sky-500 p-4 space-y-3 rounded-lg">
<p className="text-lg font-bold text-sky-800">Age Summary: <span className="font-mono text-gray-900">{ageSummary}</span></p>
<p className="text-sm text-gray-700">Birth Details: <span className="font-medium">{birthMeta}</span></p>
<div className="pt-2">
<p className="text-sm font-medium text-gray-700 mb-1">Next Birthday Countdown: <span className="text-xl font-extrabold text-red-600 ml-2">{countdown}</span></p>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
ref={progressRef}
className="bg-sky-600 h-2.5 rounded-full transition-all duration-1000"
style={{ width: '0%' }}
></div>
</div>
</div>
</div>
{/* --- LLM Feature 1: Birthday Insight Generator UI --- */}
{birthMeta !== '—' && (
<div className="pt-4 border-t border-gray-200 space-y-3">
<h3 className="text-xl font-bold text-indigo-700">✨ Birthday Insight Generator</h3>
<button
onClick={generateBirthdayInsight}
disabled={isInsightLoading}
className={`w-full font-bold py-3 rounded-lg transition duration-150 ${
isInsightLoading ? 'bg-indigo-300 text-indigo-700 cursor-not-allowed' : 'bg-indigo-500 hover:bg-indigo-600 text-white'
}`}
>
{isInsightLoading ? 'Consulting the Stars...' : 'Get Birthday Insight ✨'}
</button>
{birthdayInsight && (
<div className="bg-indigo-100 p-4 rounded-lg border-l-4 border-indigo-500 text-indigo-800">
<p className="italic">{birthdayInsight}</p>
</div>
)}
</div>
)}
{/* --- End LLM Feature 1 UI --- */}
</div>
)}
{/* --- LLM Feature 2: TEXT REFINER --- */}
{tab === 'text-refiner' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">✍️ AI Text Refiner</h2>
{/* Input Area */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Text to Refine</label>
<textarea
value={refinerText}
onChange={(e) => {
setRefinerText(e.target.value);
setRefinedOutput(''); // Clear output on new input
}}
rows="8"
placeholder="Paste your text here to summarize, paraphrase, or change the tone..."
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-sky-500 focus:border-sky-500 resize-y"
></textarea>
</div>
{/* Action Selector */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Select Action</label>
<select
value={refinerAction}
onChange={(e) => setRefinerAction(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-sky-500 focus:border-sky-500"
>
<option>Summarize</option>
<option>Paraphrase</option>
<option>Change Tone to Formal</option>
<option>Change Tone to Casual</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={refineText}
disabled={!refinerText.trim() || isRefining}
className={`w-full font-bold py-3 rounded-lg transition duration-150 ${
refinerText.trim() && !isRefining ? 'bg-sky-500 hover:bg-sky-600 text-white' : 'bg-gray-400 text-gray-600 cursor-not-allowed'
}`}
>
{isRefining ? 'Processing...' : 'Refine Text ✨'}
</button>
</div>
</div>
{/* Output Area */}
{refinedOutput && (
<div className="pt-4 border-t border-gray-200">
<h3 className="text-lg font-bold text-gray-700 mb-2">Refined Output</h3>
<div className="bg-gray-50 p-4 border rounded-lg whitespace-pre-wrap text-gray-800 shadow-inner">
{refinedOutput}
</div>
</div>
)}
</div>
)}
{/* --- END LLM Feature 2 --- */}
{/* --- IMAGE RESIZER --- */}
{tab === 'image-resizer' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">Image Resizer (Client-Side)</h2>
<input
type="file"
accept="image/*"
onChange={onImageFileSelected}
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100"
/>
{selectedImageFile && (
<>
<div className="bg-gray-50 p-4 rounded-lg border">
<p className="text-sm font-medium text-gray-700">
Current Dimensions: <span className="font-bold">{imgParams.width}x{imgParams.height}</span> |
Size: <span className="font-bold">{imgParams.kb} KB</span>
<span className="text-xs text-gray-500 ml-2">(Original: {originalDimensionsRef.current.width}x{originalDimensionsRef.current.height})</span>
</p>
<p className="text-xs text-red-500 mt-1">Note: Dimensions and Scale are linked to preserve aspect ratio.</p>
</div>
{/* Dimension and Scale Controls */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">New Width (px)</label>
<input
type="number"
name="width"
value={imgParams.width}
onChange={handleDimensionChange}
className="w-full p-2 border border-gray-300 rounded-lg"
min="1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">New Height (px)</label>
<input
type="number"
name="height"
value={imgParams.height}
onChange={handleDimensionChange}
className="w-full p-2 border border-gray-300 rounded-lg"
min="1"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Scale (%)</label>
<input
type="number"
name="percent"
value={imgParams.percent}
onChange={handleDimensionChange}
className="w-full p-2 border border-gray-300 rounded-lg"
min="1" max="500"
/>
</div>
</div>
{/* KB REDUCTION OPTION (Target Size) */}
<div className="md:col-span-4 space-y-2 pt-4 border-t pt-6 mt-6 border-blue-200">
<label className="block text-lg font-bold text-gray-700">File Size Compression (Target KB)</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Target Size (KB)</label>
<input
type="number"
name="targetKb"
value={targetKb}
onChange={(e) => {
const val = e.target.value === '' ? '' : parseInt(e.target.value);
setTargetKb(val);
}}
className="w-full p-2 border border-blue-400 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="e.g., 100"
min="1"
/>
<p className="text-xs text-blue-600 bg-blue-50 p-2 rounded-md">
**Note:** If a **Target Size (KB)** is set, this will attempt to reach that file size by adjusting **JPEG quality**. Pixel dimensions will be maintained. This option is ignored for PNG files.
</p>
</div>
{/* END KB REDUCTION OPTION */}
<button
onClick={resizeImage}
className="w-full bg-sky-500 hover:bg-sky-600 text-white font-bold py-3 rounded-lg transition duration-150"
>
{targetKb ? `Compress to ${targetKb} KB & Download` : 'Resize & Download Image'}
</button>
<div className="mt-4 border border-dashed border-gray-400 p-4 rounded-lg bg-gray-50">
<h3 className="text-center font-semibold mb-2">Image Preview (Current Size)</h3>
<canvas ref={canvasRef} className="shadow-lg border border-gray-300 rounded-md"></canvas>
</div>
</>
)}
</div>
)}
{/* --- PDF READER --- */}
{tab === 'pdf-reader' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">PDF Reader (First Page Preview)</h2>
<input
type="file"
accept=".pdf"
onChange={(e) => onPdfSelected(e, setPdfData, setPdfFileName)}
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100"
/>
{loadingPdf && <div className="text-center py-4 text-indigo-500 font-semibold">Loading PDF...</div>}
{pdfError && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded" role="alert">
<p className="font-bold">Error Rendering PDF:</p>
<p>{pdfError}</p>
</div>
)}
<div className="mt-4 border border-dashed border-gray-400 p-2 rounded-lg bg-gray-50">
<h3 className="text-center font-semibold mb-2">PDF Render Output</h3>
<div className="w-full mx-auto" style={{ maxWidth: '600px' }}>
{pdfData && !loadingPdf && !pdfError && (
<p className="text-sm text-center text-gray-500 mb-2">Displaying first page of: <span className="font-semibold">{pdfFileName}</span></p>
)}
<canvas ref={pdfCanvasRef} className="shadow-lg border border-gray-300 rounded-md"></canvas>
</div>
</div>
</div>
)}
{/* --- PDF CONVERTER --- */}
{tab === 'pdf-converter' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">PDF Converter</h2>
<input
type="file"
accept=".pdf"
onChange={(e) => onPdfSelected(e, setConverterFile, setPdfFileName)}
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100"
/>
<select
value={conversionType}
onChange={(e) => setConversionType(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg"
>
<option>PDF to JPG (Working)</option>
<option disabled>DOCX to PDF (Server Required)</option>
<option disabled>PDF to DOCX (Server Required)</option>
</select>
<div className={`p-4 rounded-lg ${conversionType.includes('Working') ? 'bg-green-100 border-l-4 border-green-500 text-green-800' : 'bg-yellow-100 border-l-4 border-yellow-500 text-yellow-800'}`}>
<p className="font-bold">Conversion Status:</p>
<p>
{conversionType.includes('Working') ?
'Client-side conversion is ready: Converts every page of the PDF into a separate JPG file.' :
'This complex format conversion requires dedicated server-side software to process proprietary file structures.'
}
</p>
</div>
<button
onClick={startConversion}
disabled={!converterFile || !conversionType.includes('Working') || isConverting}
className={`w-full font-bold py-3 rounded-lg transition duration-150 ${
converterFile && conversionType.includes('Working') && !isConverting ? 'bg-indigo-500 hover:bg-indigo-600 text-white' : 'bg-gray-400 text-gray-600 cursor-not-allowed'
}`}
>
{isConverting ? 'Converting Pages...' : `Start Conversion: ${conversionType}`}
</button>
{pdfError && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded" role="alert">
<p className="font-bold">Conversion Error:</p>
<p>{pdfError}</p>
</div>
)}
</div>
)}
{/* --- PDF RESIZER/OPTIMIZER --- */}
{tab === 'pdf-resizer' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-700">PDF Resizer/Optimizer (Client-Side Cleanup)</h2>
<input
type="file"
accept=".pdf"
onChange={handlePdfResizerFileSelect}
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100"
/>
{/* KB TARGET INPUT FOR PDF */}
<div className="md:col-span-4 space-y-2 pt-4 border-t pt-6 mt-6 border-blue-200">
<label className="block text-lg font-bold text-gray-700">Target Size (KB) for Optimization</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Desired Final Size (KB) - Optional</label>
<input
type="number"
name="targetPdfKb"
value={targetPdfKb}
onChange={(e) => {
const val = e.target.value === '' ? '' : parseInt(e.target.value);
setTargetPdfKb(val);
}}
className="w-full p-2 border border-blue-400 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="e.g., 500"
min="1"
/>
<div className="bg-sky-50 border-l-4 border-sky-500 p-4 text-sky-800 rounded-lg">
<p className="font-bold">💡 How it works:</p>
<p className="text-sm">The tool performs a **structural cleanup** (stripping metadata, unused objects) by copying pages to a new PDF. This is the most reliable client-side method to reduce size and will get you *closer* to your desired target, but **cannot guarantee an exact file size** due to the complex nature of PDF content (fonts, images, vectors, etc.).</p>
</div>
</div>
<button
onClick={optimizeAndSavePdf}
disabled={!resizerFile || isResizing || !isPdfLibsLoaded}
className={`w-full font-bold py-3 rounded-lg transition duration-150 ${
resizerFile && !isResizing && isPdfLibsLoaded ? 'bg-green-500 hover:bg-green-600 text-white' : 'bg-gray-400 text-gray-600 cursor-not-allowed'
}`}
>
{isResizing ? 'Optimizing PDF...' : 'Optimize & Download New PDF'}
</button>
{resizerMessage && (
<div className={`p-4 rounded-lg ${resizerMessage.includes('failed') ? 'bg-red-100 border-l-4 border-red-500 text-red-700' : 'bg-green-100 border-l-4 border-green-500 text-green-800'}`} role="alert">
<p className="font-bold">Status:</p>
<p>{resizerMessage}</p>
</div>
)}
{(originalResizerSize && newResizerSize) && (
<div className="bg-gray-50 p-4 rounded-lg border border-gray-300">
<h3 className="text-lg font-bold text-gray-700 mb-2">Optimization Results:</h3>
<div className="grid grid-cols-2 gap-4 text-center">
<div className="p-3 bg-red-50 rounded-lg">
<p className="text-sm font-medium text-gray-600">Original Size (KB)</p>
<p className="text-2xl font-extrabold text-red-700">{originalResizerSize}</p>
</div>
<div className="p-3 bg-green-50 rounded-lg">
<p className="text-sm font-medium text-gray-600">Optimized Size (KB)</p>
<p className="text-2xl font-extrabold text-green-700">{newResizerSize}</p>
</div>
</div>
{parseFloat(newResizerSize) < parseFloat(originalResizerSize) && (
<p className="mt-3 text-center text-sm font-semibold text-green-600">
🎉 Size Reduced by: {((1 - (parseFloat(newResizerSize) / parseFloat(originalResizerSize))) * 100).toFixed(2)}%
</p>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}