| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SymCalc - Symbolic Calculator</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Source+Code+Pro:wght@400;600&display=swap'); |
| |
| :root { |
| --primary: #4f46e5; |
| --primary-light: #e0e7ff; |
| --text: #1f2937; |
| --text-light: #6b7280; |
| --border: #e5e7eb; |
| --bg: #f9fafb; |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| color: var(--text); |
| line-height: 1.5; |
| } |
| |
| .mono { |
| font-family: 'Source Code Pro', monospace; |
| } |
| |
| .document { |
| max-width: 800px; |
| margin: 0 auto; |
| background: white; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.05); |
| border-radius: 8px; |
| overflow: hidden; |
| } |
| .analysis-box { |
| background-color: #f8fafc; |
| border: 1px solid #e5e7eb; |
| padding: 16px; |
| margin-top: 12px; |
| border-radius: 8px; |
| font-family: 'Inter', sans-serif; |
| color: #334155; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.05); |
| } |
| .analysis-title { |
| font-weight: bold; |
| color: #4f46e5; |
| margin-bottom: 6px; |
| } |
| .analysis-item { |
| margin-left: 12px; |
| position: relative; |
| } |
| .analysis-item:before { |
| content: "•"; |
| position: absolute; |
| left: -12px; |
| color: #4f46e5; |
| } |
| .analysis-tip { |
| background-color: #e0e7ff; |
| padding: 8px; |
| border-radius: 4px; |
| margin-top: 8px; |
| font-style: italic; |
| } |
| .symbol-btn { |
| transition: all 0.2s ease; |
| } |
| .symbol-btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
| } |
| .history-item:hover { |
| background-color: #f3f4f6; |
| cursor: pointer; |
| } |
| .fade-in { |
| animation: fadeIn 0.3s ease-in-out; |
| } |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen p-6"> |
| <div class="document"> |
| <header class="border-b border-gray-200 px-6 py-4"> |
| <div class="flex items-center justify-between"> |
| <div class="flex items-center space-x-3"> |
| <i class="fas fa-infinity text-indigo-600 text-2xl"></i> |
| <h1 class="text-2xl font-semibold">SymCalc</h1> |
| <span class="text-sm text-gray-500">Symbolic Mathematics Calculator</span> |
| </div> |
| <div class="flex space-x-4"> |
| <button class="text-gray-500 hover:text-indigo-600 transition" title="History"> |
| <i class="fas fa-history"></i> |
| </button> |
| <button class="text-gray-500 hover:text-indigo-600 transition" title="Documentation"> |
| <i class="fas fa-question-circle"></i> |
| </button> |
| </div> |
| </div> |
| </header> |
| |
| <main class="p-6"> |
| <section class="mb-8"> |
| <div id="loading" class="hidden text-indigo-600 text-sm mb-2 font-medium">Calculating...</div> |
| <div class="mb-1 text-sm text-gray-500 font-medium">Input Expression</div> |
| <div class="relative"> |
| <textarea |
| id="formulaInput" |
| class="w-full bg-white text-gray-900 text-lg mono resize-none outline-none border border-gray-200 rounded p-3 mb-2 focus:ring-2 focus:ring-indigo-200 focus:border-indigo-500" |
| rows="2" |
| placeholder="Enter a mathematical expression (e.g. 2x + 3 = 7, sin(π/2), √(16))" |
| spellcheck="false" |
| ></textarea> |
| <div id="latexPreview" class="absolute right-3 top-3 text-gray-400 text-sm hidden"> |
| <span class="bg-white px-2 py-1 rounded">LaTeX Preview</span> |
| </div> |
| </div> |
| <div id="latexOutput" class="text-center p-2 bg-gray-50 border border-gray-200 rounded hidden"> |
| <div class="katex-display"></div> |
| </div> |
| |
| <div class="mt-6 mb-1 text-sm text-gray-500 font-medium">Result</div> |
| <div id="result" class="text-xl font-medium text-gray-900 mono p-3 bg-gray-50 rounded border border-gray-200 min-h-[60px]"></div> |
| <div id="steps" class="mt-3 hidden"> |
| <div class="text-sm text-gray-500 font-medium mb-1">Solution Steps</div> |
| <div id="stepsContent" class="text-sm mono p-3 bg-blue-50 rounded border border-blue-200"></div> |
| </div> |
| <div id="error" class="mt-2 text-sm text-red-500"></div> |
| <div id="sympyCode" class="hidden mt-3"> |
| <div class="text-sm text-gray-500 font-medium mb-1">SymPy Code</div> |
| <pre class="text-xs mono p-3 bg-gray-100 rounded overflow-x-auto"></pre> |
| </div> |
| </section> |
| |
| <section class="mb-8"> |
| <div class="mb-3 text-sm text-gray-500 font-medium">Common Symbols</div> |
| <div class="grid grid-cols-8 gap-2 mb-4"> |
| <button onclick="addSymbol('π')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">π</button> |
| <button onclick="addSymbol('e')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">e</button> |
| <button onclick="addSymbol('√(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">√</button> |
| <button onclick="addSymbol('^')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">^</button> |
| <button onclick="addSymbol('!')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">!</button> |
| <button onclick="addSymbol('(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">(</button> |
| <button onclick="addSymbol(')')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">)</button> |
| <button onclick="addSymbol('+')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">+</button> |
| </div> |
| |
| <div class="grid grid-cols-8 gap-2 mb-4"> |
| <button onclick="addSymbol('-')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">-</button> |
| <button onclick="addSymbol('*')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">×</button> |
| <button onclick="addSymbol('/')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">÷</button> |
| <button onclick="addSymbol('sin(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">sin</button> |
| <button onclick="addSymbol('cos(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">cos</button> |
| <button onclick="addSymbol('tan(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">tan</button> |
| <button onclick="addSymbol('ln(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">ln</button> |
| <button onclick="addSymbol('log(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">log</button> |
| </div> |
| |
| <div class="flex space-x-3"> |
| <button onclick="calculate()" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded text-sm font-medium flex items-center"> |
| <i class="fas fa-equals mr-2"></i> Calculate |
| </button> |
| <button onclick="analyzeExpression()" class="px-4 py-2 bg-white hover:bg-gray-50 border border-gray-200 rounded text-sm font-medium flex items-center"> |
| <i class="fas fa-search mr-2"></i> Analyze |
| </button> |
| <button onclick="clearAll()" class="px-4 py-2 bg-white hover:bg-gray-50 border border-gray-200 rounded text-sm font-medium flex items-center"> |
| <i class="fas fa-trash-alt mr-2"></i> Clear |
| </button> |
| </div> |
| </section> |
| |
| <section id="historyPanel" class="hidden mt-8 border-t border-gray-200 pt-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="font-medium text-gray-900">Calculation History</h3> |
| <button onclick="clearHistory()" class="text-gray-500 hover:text-red-500 text-sm flex items-center"> |
| <i class="fas fa-trash-alt mr-1"></i> Clear All |
| </button> |
| </div> |
| <div id="historyList" class="space-y-3"></div> |
| </section> |
| </div> |
| |
| <footer class="border-t border-gray-200 px-6 py-4 text-center text-gray-500 text-sm"> |
| <p>SymCalc v1.0 · Symbolic Mathematics Calculator</p> |
| </footer> |
| </div> |
|
|
| <script> |
| let currentExpression = ''; |
| let history = JSON.parse(localStorage.getItem('calcHistory')) || []; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| updateScreen(); |
| renderHistory(); |
| |
| |
| document.querySelector('.fa-history').parentElement.addEventListener('click', toggleHistoryPanel); |
| |
| |
| document.addEventListener('keydown', handleKeyPress); |
| }); |
| |
| function handleKeyPress(e) { |
| const input = document.getElementById('formulaInput'); |
| const cursorPos = input.selectionStart; |
| const textBeforeCursor = input.value.substring(0, cursorPos); |
| |
| |
| if (e.key === '(' && !e.shiftKey) { |
| const lastWord = textBeforeCursor.split(/[\s\+\-\*\/\^\(\)]/).pop(); |
| if (['sin', 'cos', 'tan', 'log', 'ln', 'sqrt'].includes(lastWord)) { |
| e.preventDefault(); |
| const newPos = cursorPos - lastWord.length; |
| input.value = input.value.substring(0, newPos) + |
| lastWord + '()' + |
| input.value.substring(cursorPos); |
| input.setSelectionRange(newPos + lastWord.length + 1, |
| newPos + lastWord.length + 1); |
| return; |
| } |
| } |
| |
| |
| if (e.key === 'Enter' && e.shiftKey) { |
| e.preventDefault(); |
| addSymbol('\n'); |
| return; |
| } |
| |
| |
| if (e.key === 'Tab') { |
| e.preventDefault(); |
| const lastWord = textBeforeCursor.split(/[\s\+\-\*\/\^\(\)]/).pop(); |
| const matches = ['pi', 'theta', 'alpha', 'beta', 'gamma'] |
| .filter(sym => sym.startsWith(lastWord.toLowerCase())); |
| if (matches.length === 1) { |
| const match = matches[0]; |
| const newPos = cursorPos - lastWord.length; |
| input.value = input.value.substring(0, newPos) + |
| match + |
| input.value.substring(cursorPos); |
| input.setSelectionRange(newPos + match.length, |
| newPos + match.length); |
| } |
| return; |
| } |
| |
| const keyMap = { |
| '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', |
| '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', |
| '+': '+', '-': '-', '*': '*', '/': '/', |
| '.': '.', '^': '^', '!': '!', |
| '(': '(', ')': ')', |
| 'Enter': '=', |
| 'Backspace': 'backspace', |
| 'Escape': 'clear', |
| 'ArrowUp': 'history-up', |
| 'ArrowDown': 'history-down' |
| }; |
| |
| const key = keyMap[e.key]; |
| if (key) { |
| e.preventDefault(); |
| if (key === '=') calculate(); |
| else if (key === 'backspace') backspace(); |
| else if (key === 'clear') clearAll(); |
| else if (key === 'history-up') navigateHistory(-1); |
| else if (key === 'history-down') navigateHistory(1); |
| else addSymbol(key); |
| } |
| } |
| |
| let historyNavigationIndex = -1; |
| let historyNavigationTemp = ''; |
| |
| function navigateHistory(direction) { |
| const input = document.getElementById('formulaInput'); |
| |
| if (historyNavigationIndex === -1) { |
| historyNavigationTemp = input.value; |
| } |
| |
| historyNavigationIndex += direction; |
| |
| if (historyNavigationIndex < -1) { |
| historyNavigationIndex = -1; |
| } else if (historyNavigationIndex >= history.length) { |
| historyNavigationIndex = history.length - 1; |
| } |
| |
| if (historyNavigationIndex === -1) { |
| input.value = historyNavigationTemp; |
| } else { |
| input.value = history[historyNavigationIndex].expression; |
| } |
| |
| input.focus(); |
| } |
| |
| function addSymbol(symbol) { |
| const input = document.getElementById('formulaInput'); |
| const startPos = input.selectionStart; |
| const endPos = input.selectionEnd; |
| const currentValue = input.value; |
| |
| input.value = currentValue.substring(0, startPos) + symbol + currentValue.substring(endPos); |
| input.focus(); |
| input.setSelectionRange(startPos + symbol.length, startPos + symbol.length); |
| } |
| |
| function clearAll() { |
| currentExpression = ''; |
| document.getElementById('error').textContent = ''; |
| updateScreen(); |
| } |
| |
| function backspace() { |
| const input = document.getElementById('formulaInput'); |
| const startPos = input.selectionStart; |
| const endPos = input.selectionEnd; |
| |
| if (startPos === endPos && startPos > 0) { |
| input.value = input.value.substring(0, startPos - 1) + input.value.substring(endPos); |
| input.setSelectionRange(startPos - 1, startPos - 1); |
| } else { |
| input.value = input.value.substring(0, startPos) + input.value.substring(endPos); |
| input.setSelectionRange(startPos, startPos); |
| } |
| input.focus(); |
| } |
| |
| function updateScreen() { |
| |
| } |
| |
| let pyodide; |
| async function initializePyodide() { |
| const loadingEl = document.getElementById('loading'); |
| loadingEl.classList.remove('hidden'); |
| document.getElementById('loadingText').textContent = "Loading SymPy..."; |
| |
| try { |
| pyodide = await loadPyodide({ |
| indexURL: "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/" |
| }); |
| await pyodide.loadPackage(['sympy']); |
| loadingEl.classList.add('hidden'); |
| return true; |
| } catch (error) { |
| document.getElementById('error').innerHTML = ` |
| <div class="bg-red-50 border border-red-200 p-3 rounded"> |
| <div class="font-medium text-red-600">Failed to initialize SymPy:</div> |
| <div class="text-sm">${error.message}</div> |
| </div> |
| `; |
| loadingEl.classList.add('hidden'); |
| return false; |
| } |
| } |
| |
| async function calculate() { |
| if (!pyodide) { |
| const initialized = await initializePyodide(); |
| if (!initialized) return; |
| } |
| |
| const input = document.getElementById('formulaInput'); |
| currentExpression = input.value.trim(); |
| if (!currentExpression) return; |
| |
| const loadingEl = document.getElementById('loading'); |
| loadingEl.classList.remove('hidden'); |
| document.getElementById('loadingText').textContent = "Calculating..."; |
| |
| try { |
| |
| let pythonCode = ` |
| from sympy import * |
| from sympy.parsing.sympy_parser import parse_expr |
| import traceback |
| |
| x, y, z = symbols('x y z') |
| result = None |
| error = None |
| |
| try: |
| if '=' in expr: |
| if expr.count('=') == 1: |
| # Single equation solving |
| lhs, rhs = expr.split('=', 1) |
| lhs_expr = parse_expr(lhs.strip()) |
| rhs_expr = parse_expr(rhs.strip()) |
| solution = solve(Eq(lhs_expr, rhs_expr)) |
| if isinstance(solution, dict): |
| result = "\\n".join([f"{k} = {v}" for k,v in solution.items()]) |
| elif isinstance(solution, list): |
| result = "\\n".join([f"Solution {i+1}: {s}" for i,s in enumerate(solution)]) |
| else: |
| result = str(solution) |
| else: |
| # System of equations |
| equations = [eq.strip() for eq in expr.split(';') if '=' in eq] |
| syms = set() |
| for eq in equations: |
| syms.update([str(s) for s in parse_expr(eq.split('=')[0].strip()).free_symbols]) |
| syms = sorted(syms, key=lambda s: str(s)) |
| eqs = [Eq(*map(parse_expr, eq.split('=', 1))) for eq in equations] |
| solution = solve(eqs, syms) |
| if solution: |
| if isinstance(solution, list): |
| result = "\\n".join([f"Solution {i+1}: {s}" for i,s in enumerate(solution)]) |
| else: |
| result = "\\n".join([f"{k} = {v}" for k,v in solution.items()]) |
| else: |
| result = "No solution found" |
| else: |
| # Expression evaluation |
| expr_parsed = parse_expr(expr) |
| result = str(expr_parsed.evalf(4)) |
| except Exception as e: |
| error = traceback.format_exc() |
| `; |
| |
| |
| setTimeout(() => { |
| document.getElementById('sympyCode').textContent = sympyCode; |
| document.getElementById('sympyCode').classList.remove('hidden'); |
| try { |
| |
| if (!currentExpression.trim()) throw "Please enter an expression"; |
| if (currentExpression.length > 200) throw "Expression too long (max 200 chars)"; |
| |
| |
| if (currentExpression.includes('=')) { |
| |
| const equations = currentExpression.split(/[\n,;]+/).map(eq => eq.trim()).filter(eq => eq.includes('=')); |
| if (equations.length > 1) { |
| |
| const vars = new Set(); |
| const equationsList = []; |
| |
| for (const eq of equations) { |
| const parts = eq.split('='); |
| if (parts.length !== 2) throw "Each equation must have exactly one '='"; |
| |
| const left = parts[0].trim().replace(/\s+/g, ''); |
| const right = parts[1].trim().replace(/\s+/g, ''); |
| |
| |
| const rearranged = `(${left}) - (${right})`; |
| equationsList.push(rearranged); |
| |
| |
| const varRegex = /([a-zA-Z])/g; |
| let match; |
| while ((match = varRegex.exec(left + right)) !== null) { |
| vars.add(match[1]); |
| } |
| } |
| |
| if (equationsList.length !== vars.size) { |
| throw `Need ${vars.size} independent equations to solve for ${[...vars].join(', ')}`; |
| } |
| |
| |
| sympyCode += `sol = solve([${equationsList.map(eq => `Eq(${eq}, 0)`).join(', ')}], [${[...vars].join(', ')}])\n`; |
| |
| try { |
| |
| const solution = {}; |
| [...vars].forEach(v => solution[v] = `solution_for_${v}`); |
| |
| let resultText = ''; |
| for (const [varName, value] of Object.entries(solution)) { |
| resultText += `${varName} = ${value}\n`; |
| } |
| |
| document.getElementById('result').textContent = resultText; |
| addToHistory(currentExpression, resultText); |
| return; |
| } catch (e) { |
| throw `Failed to solve system: ${e.message || e}`; |
| } |
| |
| throw `System with ${vars.size} variables requires ${vars.size} equations`; |
| } |
| |
| |
| const parts = currentExpression.split('='); |
| if (parts.length !== 2) throw "Each equation must have exactly one '='"; |
| |
| const left = parts[0].trim(); |
| const right = parts[1].trim(); |
| |
| |
| const variables = new Set(); |
| const varRegex = /([a-zA-Z])/g; |
| let match; |
| |
| while ((match = varRegex.exec(left + right)) !== null) { |
| variables.add(match[1]); |
| } |
| |
| if (variables.size === 0) { |
| |
| const leftValue = eval(left.replace(/×/g, '*').replace(/÷/g, '/').replace(/−/g, '-')); |
| const rightValue = eval(right.replace(/×/g, '*').replace(/÷/g, '/').replace(/−/g, '-')); |
| if (Math.abs(leftValue - rightValue) < 1e-10) { |
| document.getElementById('result').textContent = "Equation is true"; |
| } else { |
| document.getElementById('result').textContent = "Equation is false"; |
| } |
| addToHistory(currentExpression, leftValue - rightValue); |
| return; |
| } |
| else if (variables.size === 1) { |
| |
| const varName = [...variables][0]; |
| sympyCode += `sol = solve(Eq(${left}, ${right}), ${varName})\n`; |
| |
| try { |
| |
| const solution = `solution_for_${varName}`; |
| document.getElementById('result').textContent = `${varName} = ${solution}`; |
| addToHistory(currentExpression, solution); |
| return; |
| } catch (e) { |
| throw `Failed to solve equation: ${e.message || e}`; |
| } |
| } |
| else { |
| |
| const varList = [...variables].join(', '); |
| document.getElementById('result').textContent = `Multi-variable equation: ${currentExpression}`; |
| document.getElementById('error').textContent = `Variables: ${varList}\nTo solve for ${variables.size > 1 ? 'these variables' : 'this variable'}, you need ${variables.size} independent equation${variables.size > 1 ? 's' : ''}.`; |
| addToHistory(currentExpression, NaN); |
| return; |
| } |
| } |
| |
| |
| sympyCode += `expr = ${currentExpression.replace(/=/g, '==')}\n`; |
| |
| if (variables.length > 0) { |
| sympyCode += `result = expr.subs({${variables.map(v => `${v}: 1`).join(', ')}})\n`; |
| } else { |
| sympyCode += `result = N(expr)\n`; |
| } |
| |
| |
| const sanitized = expr.replace(/Math\./g, ''); |
| |
| const invalidVars = sanitized.match(/[^a-zA-Z\s\d\.\+\-\*\/\^\(\)\,]/g); |
| if (invalidVars) { |
| const uniqueVars = [...new Set(invalidVars)]; |
| throw `Invalid character${uniqueVars.length > 1 ? 's' : ''}: ${uniqueVars.join(', ')}`; |
| } |
| |
| |
| if (/[xyz]/.test(sanitized) && !currentExpression.includes('=')) { |
| const vars = [...new Set(sanitized.match(/[xyz]/g))]; |
| const varList = vars.join(', '); |
| |
| if (vars.length === 1 && /^[+-]?\d*[xyz]([+-]\d+)?$/.test(sanitized.replace(/\s/g, ''))) { |
| const match = sanitized.match(/^([+-]?\d*)([xyz])([+-]\d+)?$/); |
| const coeff = match[1] ? (match[1] === '+' ? 1 : match[1] === '-' ? -1 : parseFloat(match[1])) : 1; |
| const varName = match[2]; |
| const constant = match[3] ? parseFloat(match[3]) : 0; |
| |
| const analysis = `<div class="analysis-box"> |
| <div class="analysis-title">Linear Expression Analysis</div> |
| <div class="analysis-item">Form: <span class="font-mono">${coeff !== 1 ? coeff : ''}${varName} ${constant >= 0 ? '+' : ''}${constant !== 0 ? constant : ''}</span></div> |
| <div class="analysis-item">Slope (coefficient of ${varName}): <span class="font-bold">${coeff}</span></div> |
| <div class="analysis-item">Y-intercept: <span class="font-bold">${constant}</span></div> |
| |
| <div class="analysis-tip"> |
| <div class="font-bold mb-1">To evaluate:</div> |
| <div>1. Assign value to ${varName} (e.g. <span class="font-mono bg-gray-100 px-1">${varName}=5</span>)</div> |
| <div>2. Or make equation to solve (e.g. <span class="font-mono bg-gray-100 px-1">${currentExpression}=7</span>)</div> |
| </div> |
| </div>`; |
| throw analysis; |
| } else { |
| const analysis = `<div class="analysis-box"> |
| <div class="analysis-title">Expression Analysis</div> |
| <div class="analysis-item">Contains variable${vars.length > 1 ? 's' : ''}: <span class="font-bold">${varList}</span></div> |
| <div class="analysis-item">Not an equation (missing '=')</div> |
| |
| <div class="analysis-tip"> |
| <div class="font-bold mb-1">To evaluate this expression:</div> |
| <div>1. Assign values to variables (e.g. <span class="font-mono bg-gray-100 px-1">${vars[0]}=5</span>)</div> |
| <div>2. Or make it an equation (e.g. <span class="font-mono bg-gray-100 px-1">${currentExpression}=7</span>)</div> |
| </div> |
| </div>`; |
| throw analysis; |
| } |
| throw analysis; |
| } |
| |
| |
| if (expr.includes('**') && expr.split('**')[1].match(/\d{3,}/)) { |
| console.warn("Warning: Very large exponent may cause performance issues"); |
| } |
| |
| let result; |
| try { |
| result = eval(expr); |
| if (typeof result !== 'number' || isNaN(result)) { |
| throw "Calculation did not produce a valid number"; |
| } |
| |
| |
| if (Math.abs(result) > 1e100) { |
| console.warn("Warning: Result is extremely large - may be inaccurate"); |
| } |
| } catch (e) { |
| throw `Calculation error: ${e.message || e}`; |
| } |
| document.getElementById('result').textContent = formatResult(result); |
| document.getElementById('error').textContent = ''; |
| |
| |
| addToHistory(currentExpression, result); |
| |
| } catch (error) { |
| if (typeof error === 'string' && error.startsWith('<div')) { |
| document.getElementById('error').innerHTML = error; |
| } else { |
| document.getElementById('error').innerHTML = ` |
| <div class="analysis-box"> |
| <div class="flex items-center text-red-500 mb-2"> |
| <i class="fas fa-exclamation-circle mr-2"></i> |
| <div class="font-medium">SymPy Calculation Error</div> |
| </div> |
| <div class="text-sm mb-3">${error}</div> |
| <div class="text-xs mono bg-gray-100 p-2 rounded mb-3">${sympyCode}</div> |
| <div class="analysis-tip bg-red-50 p-3 rounded-lg"> |
| <div class="flex items-center text-red-600 font-medium mb-1"> |
| <i class="fas fa-lightbulb mr-2"></i> |
| <div>SymPy Suggestions</div> |
| </div> |
| <ul class="list-disc pl-5 space-y-1 text-sm"> |
| ${error.includes('variables') ? '<li>Use symbols() to define variables first</li>' : ''} |
| ${error.includes('solve') ? '<li>Ensure equations are properly formatted</li>' : ''} |
| <li>Use simplify() for complex expressions</li> |
| <li>Check SymPy documentation for function syntax</li> |
| </ul> |
| </div> |
| </div> |
| `; |
| } |
| document.getElementById('result').textContent = ''; |
| console.error("Calculation error:", error); |
| } finally { |
| loadingEl.classList.add('hidden'); |
| } |
| }, 10); |
| } |
| |
| function formatResult(num) { |
| if (num === undefined || num === null) return ''; |
| if (!Number.isFinite(num)) { |
| return num > 0 ? "Infinity" : "-Infinity"; |
| } |
| if (Number.isInteger(num)) return num; |
| |
| |
| if (Math.abs(num) > 1e12 || (Math.abs(num) < 1e-4 && num !== 0)) { |
| return num.toExponential(6).replace(/(\.\d*?[1-9])0+e/, '$1e'); |
| } |
| |
| |
| const str = num.toFixed(8); |
| if (str.indexOf('.') !== -1) { |
| return str.replace(/\.?0+$/, ''); |
| } |
| return str; |
| } |
| |
| function addToHistory(expression, result, steps = null, sympyCode = null) { |
| const historyItem = { |
| expression, |
| result, |
| steps, |
| sympyCode, |
| timestamp: new Date().toISOString(), |
| id: Date.now().toString() |
| }; |
| |
| history.unshift(historyItem); |
| |
| |
| if (history.length > 20) history.pop(); |
| |
| localStorage.setItem('calcHistory', JSON.stringify(history)); |
| renderHistory(); |
| return historyItem; |
| } |
| |
| function renderHistory() { |
| const historyList = document.getElementById('historyList'); |
| historyList.innerHTML = ''; |
| |
| history.forEach((item, index) => { |
| const historyItem = document.createElement('div'); |
| historyItem.className = 'p-3 history-item fade-in'; |
| historyItem.innerHTML = ` |
| <div class="flex justify-between items-start"> |
| <div> |
| <div class="text-xs text-gray-400">${new Date(item.timestamp).toLocaleString()}</div> |
| <div class="font-mono text-gray-700">${item.expression}</div> |
| </div> |
| <div class="font-medium text-indigo-600">= ${formatResult(item.result)}</div> |
| </div> |
| `; |
| |
| historyItem.addEventListener('click', () => { |
| currentExpression = item.expression; |
| updateScreen(); |
| document.getElementById('result').textContent = formatResult(item.result); |
| }); |
| |
| historyList.appendChild(historyItem); |
| }); |
| } |
| |
| function clearHistory() { |
| history = []; |
| localStorage.setItem('calcHistory', JSON.stringify(history)); |
| renderHistory(); |
| } |
| |
| function toggleHistoryPanel() { |
| const panel = document.getElementById('historyPanel'); |
| panel.classList.toggle('hidden'); |
| } |
| |
| function analyzeExpression() { |
| const input = document.getElementById('formulaInput'); |
| const expr = input.value.trim(); |
| if (!expr) return; |
| |
| let analysis = ''; |
| |
| |
| analysis += `<div class="analysis-box"> |
| <div class="flex items-center mb-3"> |
| <i class="fas fa-search text-indigo-500 mr-2"></i> |
| <div class="analysis-title text-lg font-semibold text-gray-800">Expression Analysis</div> |
| </div> |
| <div class="grid grid-cols-2 gap-3 mb-3"> |
| <div class="bg-white p-2 rounded border border-gray-100"> |
| <div class="text-xs text-gray-500">Type</div> |
| <div class="font-medium">${expr.includes('=') ? 'Equation' : 'Expression'}</div> |
| </div> |
| <div class="bg-white p-2 rounded border border-gray-100"> |
| <div class="text-xs text-gray-500">Length</div> |
| <div class="font-medium">${expr.length} chars</div> |
| </div> |
| </div>`; |
| |
| |
| const variables = [...new Set(expr.match(/[a-zA-Z]/g) || [])]; |
| const operators = [...new Set(expr.match(/[\+\-\*\/\^]/g) || [])]; |
| |
| if (variables.length > 0 || operators.length > 0) { |
| analysis += `<div class="grid grid-cols-2 gap-3 mb-3">`; |
| if (variables.length > 0) { |
| analysis += `<div class="bg-white p-2 rounded border border-gray-100"> |
| <div class="text-xs text-gray-500">Variables</div> |
| <div class="font-mono text-indigo-600">${variables.join(', ')}</div> |
| </div>`; |
| } |
| if (operators.length > 0) { |
| analysis += `<div class="bg-white p-2 rounded border border-gray-100"> |
| <div class="text-xs text-gray-500">Operators</div> |
| <div class="font-mono text-indigo-600">${operators.map(op => { |
| if (op === '*') return '×'; |
| if (op === '/') return '÷'; |
| return op; |
| }).join(', ')}</div> |
| </div>`; |
| } |
| analysis += `</div>`; |
| } |
| |
| |
| const functions = [...new Set(expr.match(/(sin|cos|tan|ln|log|sqrt)\(/g) || [])]; |
| if (functions.length > 0) { |
| analysis += `<div class="analysis-item"> |
| <span class="font-medium">Functions:</span> |
| <span class="font-mono bg-gray-100 px-1 rounded">${functions.join('</span>, <span class="font-mono bg-gray-100 px-1 rounded">')}</span> |
| </div>`; |
| } |
| |
| |
| const constants = [...new Set(expr.match(/(π|e)/g) || [])]; |
| if (constants.length > 0) { |
| analysis += `<div class="analysis-item"> |
| <span class="font-medium">Constants:</span> |
| <span class="font-mono bg-gray-100 px-1 rounded">${constants.join('</span>, <span class="font-mono bg-gray-100 px-1 rounded">')}</span> |
| </div>`; |
| } |
| |
| |
| const openParens = (expr.match(/\(/g) || []).length; |
| const closeParens = (expr.match(/\)/g) || []).length; |
| if (openParens !== closeParens) { |
| analysis += `<div class="analysis-item text-red-500"> |
| <i class="fas fa-exclamation-triangle mr-1"></i> |
| Unbalanced parentheses (${openParens} open, ${closeParens} close) |
| </div>`; |
| } |
| |
| |
| if (expr.match(/\/\s*0/g)) { |
| analysis += `<div class="analysis-item text-red-500"> |
| <i class="fas fa-exclamation-triangle mr-1"></i> |
| Potential division by zero detected |
| </div>`; |
| } |
| if (expr.match(/\^\-?\d+/g)) { |
| analysis += `<div class="analysis-item"> |
| <i class="fas fa-info-circle text-blue-500 mr-1"></i> |
| Contains exponentiation operations |
| </div>`; |
| } |
| if (expr.match(/\!/g)) { |
| analysis += `<div class="analysis-item"> |
| <i class="fas fa-info-circle text-blue-500 mr-1"></i> |
| Contains factorial operations |
| </div>`; |
| } |
| |
| |
| if (expr.includes('=')) { |
| const parts = expr.split('='); |
| if (parts.length === 2) { |
| const left = parts[0].trim(); |
| const right = parts[1].trim(); |
| |
| analysis += `<div class="analysis-item mt-3"> |
| <div class="font-medium">Equation Analysis:</div> |
| <div class="grid grid-cols-2 gap-2 mt-2"> |
| <div class="bg-gray-50 p-2 rounded"> |
| <div class="text-xs text-gray-500">Left Side</div> |
| <div class="font-mono">${left}</div> |
| </div> |
| <div class="bg-gray-50 p-2 rounded"> |
| <div class="text-xs text-gray-500">Right Side</div> |
| <div class="font-mono">${right}</div> |
| </div> |
| </div> |
| </div>`; |
| |
| if (variables.length > 0) { |
| analysis += `<div class="analysis-item"> |
| <div class="font-medium">Solution Strategy:</div> |
| <div class="mt-1 text-sm"> |
| ${variables.length === 1 ? |
| `To solve for ${variables[0]}, isolate the variable on one side` : |
| `Need ${variables.length} independent equations to solve for ${variables.join(', ')}`} |
| </div> |
| </div>`; |
| } |
| } |
| } |
| |
| analysis += `<div class="analysis-tip bg-indigo-50 p-3 rounded-lg mt-4"> |
| <div class="flex items-center text-indigo-600 font-medium mb-2"> |
| <i class="fas fa-lightbulb mr-2"></i> |
| <div>Quick Tips</div> |
| </div> |
| <ul class="list-disc pl-5 space-y-1 text-sm"> |
| <li>Use '=' to create equations (e.g. 2x+3=7)</li> |
| <li>Assign values to variables (e.g. x=5) before evaluation</li> |
| <li>Press 'Calculate' or Enter to evaluate</li> |
| ${variables.length > 0 ? `<li>For ${variables.length > 1 ? 'systems of equations' : 'equations'}, provide ${variables.length} equation${variables.length > 1 ? 's' : ''}</li>` : ''} |
| ${functions.length > 0 ? `<li>Trigonometric functions use radians by default</li>` : ''} |
| <li>Use parentheses to control operation order</li> |
| </ul> |
| </div></div>`; |
| |
| document.getElementById('error').innerHTML = analysis; |
| document.getElementById('result').textContent = ''; |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=shism/symcalc" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |