Cool Calculator (multifunctional)

Always-on-top draggable scientific calculator with optional AI chatbot, reset button, and API connect button

目前為 2025-10-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Cool Calculator (multifunctional)
// @namespace    http://tampermonkey.net/
// @version      15.0
// @description  Always-on-top draggable scientific calculator with optional AI chatbot, reset button, and API connect button
// @author       Leon Luk
// @match        *://*/*
// @grant        none
// @license      Proprietary
// ==/UserScript==

(function(){
    'use strict';

    const DEFAULT_WIDTH = 350;
    const DEFAULT_HEIGHT = 600;
    const DEFAULT_LEFT = 70;
    const DEFAULT_TOP = window.innerHeight/2 - DEFAULT_HEIGHT/2;

    const STORAGE_POS = 'leonluk_calc_pos';
    const STORAGE_APIKEY = 'leonluk_openai_key';

    let apiKey = localStorage.getItem(STORAGE_APIKEY);

    function createCalculator(){
        const calc = document.createElement('div');
        Object.assign(calc.style,{
            position:'fixed', top: DEFAULT_TOP+'px', left: DEFAULT_LEFT+'px',
            width: DEFAULT_WIDTH+'px', height: DEFAULT_HEIGHT+'px',
            padding:'15px', background:'linear-gradient(135deg,#ec4899,#8b5cf6)',
            color:'white', borderRadius:'10px', boxShadow:'4px 0 15px rgba(0,0,0,0.6)',
            zIndex:'99999999', fontFamily:'monospace', display:'flex', flexDirection:'column', overflow:'hidden'
        });

        const savedPos = JSON.parse(localStorage.getItem(STORAGE_POS));
        if(savedPos){
            calc.style.left = savedPos.left+'px';
            calc.style.top = savedPos.top+'px';
            calc.style.width = savedPos.width+'px';
            calc.style.height = savedPos.height+'px';
        }

        calc.innerHTML = `
        <div id="calc-header" style="display:flex;justify-content:space-between;align-items:center;font-weight:bold;margin-bottom:5px;cursor:move;font-size:16px;">
            <span>Calculator</span>
            <div>
                <button id="refresh-btn" style="background:none;border:none;color:white;font-size:18px;cursor:pointer;line-height:1;padding:0;margin-right:5px;">⟳</button>
                <button id="minimize-btn" style="background:none;border:none;color:white;font-size:20px;cursor:pointer;line-height:1;padding:0;">—</button>
            </div>
        </div>
        <input type="text" id="calc-display" readonly style="width:100%;padding:10px;font-size:18px;border:none;border-radius:5px;text-align:right;background:rgba(0,0,0,0.3);color:white;margin-bottom:5px;flex-shrink:0;">
        <div id="calc-buttons" style="display:grid;grid-template-columns:repeat(6,1fr);gap:5px;flex-shrink:0;"></div>
        <div id="chat-container" style="margin-top:10px;flex-grow:1;"></div>
        <button id="connect-chat-api" style="margin-top:5px;padding:5px 10px;border:none;border-radius:5px;background:#7c3aed;color:white;cursor:pointer;">Connect Chat AI API Key</button>
        <div style="text-align:right;font-size:10px;opacity:0.8;">by Leon Luk</div>
        `;

        document.body.appendChild(calc);

        const display = calc.querySelector('#calc-display');
        const buttonsContainer = calc.querySelector('#calc-buttons');
        const minimizeBtn = calc.querySelector('#minimize-btn');
        const refreshBtn = calc.querySelector('#refresh-btn');
        const chatContainer = calc.querySelector('#chat-container');
        const connectChatBtn = calc.querySelector('#connect-chat-api');

        const buttons = ['7','8','9','/','C','(', '4','5','6','*','^',')', '1','2','3','-','√','x²','0','.','=','+','%','+/-','sin','cos','tan','asin','acos','atan','log','ln','pi','e'];

        buttons.forEach(key=>{
            const btn = document.createElement('button');
            btn.innerText = key;
            Object.assign(btn.style,{padding:'10px',fontSize:'14px',border:'none',borderRadius:'5px',background:'rgba(255,255,255,0.15)',color:'white',cursor:'pointer',transition:'background 0.2s ease, transform 0.1s ease'});
            btn.onmouseenter = ()=>btn.style.background='rgba(255,255,255,0.25)';
            btn.onmouseleave = ()=>btn.style.background='rgba(255,255,255,0.15)';
            btn.onmousedown = ()=>btn.style.transform='scale(0.95)';
            btn.onmouseup = ()=>btn.style.transform='scale(1)';
            btn.addEventListener('click',()=>{
                try{
                    if(key==='C'){display.value='';}
                    else if(key==='='){ 
                        let expr = display.value.replace(/\^/g,'**').replace(/√/g,'Math.sqrt')
                            .replace(/pi/g,'Math.PI').replace(/e/g,'Math.E')
                            .replace(/sin/g,'Math.sin').replace(/cos/g,'Math.cos').replace(/tan/g,'Math.tan')
                            .replace(/asin/g,'Math.asin').replace(/acos/g,'Math.acos').replace(/atan/g,'Math.atan')
                            .replace(/log/g,'Math.log10').replace(/ln/g,'Math.log');
                        display.value = eval(expr);
                    }
                    else if(key==='x²'){display.value+='**2';}
                    else if(key==='+/-'){display.value=display.value.startsWith('-')?display.value.slice(1):'-'+display.value;}
                    else{display.value += key;}
                }catch{display.value='Error';}
            });
            buttonsContainer.appendChild(btn);
        });

        function setupChat(){
            chatContainer.innerHTML=`
                <div id="chatbot" style="flex-grow:1;display:flex;flex-direction:column;background:rgba(0,0,0,0.2);border-radius:5px;padding:5px;overflow:auto;">
                    <div id="chat-messages" style="flex-grow:1;font-size:12px;color:white;overflow-y:auto;display:flex;flex-direction:column;gap:3px;"></div>
                    <input id="chat-input" type="text" placeholder="Chat with AI..." style="width:100%;padding:5px;font-size:12px;border:none;border-radius:3px;margin-top:5px;background:rgba(255,255,255,0.15);color:white;">
                </div>`;
            const chatMessages = chatContainer.querySelector('#chat-messages');
            const chatInput = chatContainer.querySelector('#chat-input');

            chatInput.addEventListener('keydown', async e=>{
                if(e.key==='Enter' && chatInput.value.trim()!=='' && apiKey){
                    const userMsg = chatInput.value.trim();
                    const msgDiv = document.createElement('div');
                    msgDiv.innerText='You: '+userMsg;
                    msgDiv.style.alignSelf='flex-end'; msgDiv.style.background='rgba(255,255,255,0.2)'; msgDiv.style.padding='3px 5px'; msgDiv.style.borderRadius='4px';
                    chatMessages.appendChild(msgDiv);
                    chatInput.value='';

                    const botDiv=document.createElement('div'); botDiv.innerText='AI: ...'; botDiv.style.alignSelf='flex-start'; botDiv.style.background='rgba(255,215,0,0.2)'; botDiv.style.padding='3px 5px'; botDiv.style.borderRadius='4px'; chatMessages.appendChild(botDiv);

                    try{
                        const response = await fetch('https://api.openai.com/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify({model:'gpt-3.5-turbo',messages:[{role:'user',content:userMsg}],max_tokens:150})});
                        const data = await response.json();
                        botDiv.innerText='AI: '+(data.choices?.[0]?.message?.content||'No response.');
                    }catch{botDiv.innerText='AI: Error connecting to API.';}
                    chatMessages.scrollTop = chatMessages.scrollHeight;
                }
            });
        }

        function showNoAPI(){
            chatContainer.innerHTML=`<div style="flex-grow:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:white;font-size:12px;gap:5px;">
                <div>No API key detected</div>
            </div>`;
        }

        connectChatBtn.addEventListener('click', ()=>{
            apiKey = prompt('Enter your OpenAI API key:');
            if(apiKey){
                localStorage.setItem(STORAGE_APIKEY, apiKey);
                setupChat();
            }
        });

        if(apiKey){setupChat();} else {showNoAPI();}

        minimizeBtn.addEventListener('click', ()=>{calc.style.display='none';});
        refreshBtn.addEventListener('click', ()=>{
            calc.style.width = DEFAULT_WIDTH+'px';
            calc.style.height = DEFAULT_HEIGHT+'px';
            calc.style.left = DEFAULT_LEFT+'px';
            calc.style.top = DEFAULT_TOP+'px';
            display.style.fontSize='18px';
            buttonsContainer.querySelectorAll('button').forEach(b=>{b.style.fontSize='14px'; b.style.padding='10px';});
        });

        // Draggable logic
        const header = calc.querySelector('#calc-header');
        let dragging = false, offsetX=0, offsetY=0;
        header.addEventListener('mousedown', e=>{dragging=true; offsetX=e.clientX-calc.getBoundingClientRect().left; offsetY=e.clientY-calc.getBoundingClientRect().top; calc.style.cursor='grabbing';});
        document.addEventListener('mousemove', e=>{if(!dragging) return; e.preventDefault(); calc.style.left=(e.clientX-offsetX)+'px'; calc.style.top=(e.clientY-offsetY)+'px';});
        document.addEventListener('mouseup', ()=>{if(dragging){dragging=false; calc.style.cursor='grab'; localStorage.setItem(STORAGE_POS, JSON.stringify({left:parseInt(calc.style.left), top:parseInt(calc.style.top), width:parseInt(calc.style.width), height:parseInt(calc.style.height)}));}});

        return calc;
    }

    window.addEventListener('load', ()=>{
        const calc = createCalculator();

        const toggleBtn = document.createElement('button');
        toggleBtn.innerText='🧮 Calculator';
        Object.assign(toggleBtn.style,{position:'fixed',top:'50%',left:'10px',transform:'translateY(-50%)',zIndex:'10000000',padding:'10px 15px',fontSize:'14px',background:'linear-gradient(135deg,#d946ef,#7c3aed)',color:'white',border:'none',borderRadius:'8px',cursor:'pointer',boxShadow:'0 2px 8px rgba(0,0,0,0.3)'});
        toggleBtn.onmouseenter=()=>toggleBtn.style.opacity='0.8';
        toggleBtn.onmouseleave=()=>toggleBtn.style.opacity='1';
        toggleBtn.addEventListener('click', ()=>{calc.style.display = (calc.style.display==='flex')?'none':'flex';});
        document.body.appendChild(toggleBtn);
    });
})();