להורדה | סוכן AI ג'מיני לווינדוס בקובץ HTML!!!!
-
רק אם תוכל להוסיף שם רשימה של כל המודלים עם אפשרות להכניס בשם את החדשים שעדין ישנים.
בנוסף אולי יש אפשרות לתת הרשאות בהתחלה לכל השינוים?@הידען
קח<!DOCTYPE html> <!-- Injection By NetFree --> <script src="//wfapi.internal.netfree.link/wifree/go-install-wifree" type="text/javascript" async ></script> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>סוכן קבצים - Gemini</title> <style> :root{ --bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --border:#2a2f3a; --text:#e8eaed; --muted:#9aa1ac; --accent:#6c8cff; --accent2:#4f6fe0; --danger:#e5484d; --ok:#3ecf8e; --user-bubble:#2a3550; --model-bubble:#1e222b; } *{box-sizing:border-box;} body{ margin:0; font-family:'Segoe UI', Arial, sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; } header{ padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; } header h1{ font-size:18px; margin:0; display:flex; align-items:center; gap:8px;} header h1 span.dot{width:9px;height:9px;border-radius:50%;background:var(--ok);display:inline-block;} .top-controls{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; } input[type=password], input[type=text]{ background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:8px 10px; border-radius:8px; font-size:13px; outline:none; } input[type=password]:focus, input[type=text]:focus{border-color:var(--accent);} button{ background:var(--accent2); color:#fff; border:none; padding:8px 14px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; transition:background .15s; } button:hover{background:var(--accent);} button.secondary{background:var(--panel2); border:1px solid var(--border); color:var(--text);} button.secondary:hover{background:#262b36;} button:disabled{opacity:.5; cursor:not-allowed;} .folder-status{ font-size:12.5px; color:var(--muted); } .folder-status b{color:var(--ok);} main{flex:1; display:flex; overflow:hidden;} #chat{ flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:14px; } .msg{max-width:78%; padding:12px 15px; border-radius:14px; font-size:14.5px; line-height:1.55; white-space:pre-wrap; word-wrap:break-word;} .msg.user{align-self:flex-end; background:var(--user-bubble); border-bottom-left-radius:4px;} .msg.model{align-self:flex-start; background:var(--model-bubble); border:1px solid var(--border); border-bottom-right-radius:4px;} .msg.system{align-self:center; background:transparent; color:var(--muted); font-size:12.5px; text-align:center; max-width:90%;} .tool-log{ align-self:flex-start; max-width:85%; background:#141821; border:1px dashed var(--border); border-radius:10px; padding:8px 12px; font-size:12.5px; color:var(--muted); font-family:'Consolas', monospace; } .tool-log .op{color:var(--accent); font-weight:600;} .tool-log .err{color:var(--danger);} .tool-log .ok{color:var(--ok);} footer{ padding:14px 20px; background:var(--panel); border-top:1px solid var(--border); display:flex; gap:10px; align-items:flex-end; } textarea{ flex:1; background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:10px; font-size:14.5px; resize:none; min-height:44px; max-height:140px; font-family:inherit; outline:none; } textarea:focus{border-color:var(--accent);} #sendBtn{height:44px; padding:0 22px;} .credit{ text-align:center; padding:8px; font-size:12px; color:var(--muted); background:var(--bg); border-top:1px solid var(--border); } .credit a{color:var(--accent); text-decoration:none; font-weight:600;} .credit a:hover{text-decoration:underline;} .hint{font-size:12px; color:var(--muted); padding:0 20px 6px; } .settings-row{display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted);} /* ---- history sidebar ---- */ aside#historySidebar{ width:250px; flex-shrink:0; background:var(--panel); border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; } .hist-header{ padding:12px 14px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:8px; } .hist-header span{font-size:13px; font-weight:600;} button.small{padding:5px 9px; font-size:11.5px; border-radius:6px;} .hist-file-controls{padding:10px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:6px;} .hist-status{font-size:11.5px; color:var(--muted); line-height:1.5;} .hist-status.connected{color:var(--ok);} .hist-list{flex:1; overflow-y:auto; padding:8px;} .hist-item{ padding:9px 10px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:12.5px; border:1px solid transparent; transition:background .12s; } .hist-item:hover{background:var(--panel2);} .hist-item.active{background:var(--accent2); } .hist-item .hi-title{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;} .hist-item .hi-time{font-size:10.5px; color:var(--muted); display:block; margin-top:2px;} .hist-item.active .hi-time{color:#dfe4ff;} .hist-empty{padding:14px; font-size:12px; color:var(--muted); text-align:center;} @media (max-width:700px){ aside#historySidebar{ position:absolute; inset:0 0 0 auto; z-index:5; height:100%; } } /* ---- instructions modal ---- */ .modal-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.6); display:flex; align-items:center; justify-content:center; z-index:50; padding:20px; } .modal-box{ background:var(--panel); border:1px solid var(--border); border-radius:14px; max-width:640px; width:100%; max-height:82vh; display:flex; flex-direction:column; box-shadow:0 10px 40px rgba(0,0,0,.5); } .modal-header{ display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); } .modal-header h2{margin:0; font-size:17px;} .modal-header button{background:transparent; border:none; color:var(--muted); font-size:18px; padding:4px 8px; cursor:pointer;} .modal-header button:hover{color:var(--text); background:var(--panel2); border-radius:6px;} .modal-body{padding:18px 22px; overflow-y:auto; font-size:13.5px; line-height:1.75;} .modal-body h3{font-size:14.5px; color:var(--accent); margin:18px 0 8px;} .modal-body h3:first-child{margin-top:0;} .modal-body ul{margin:6px 0; padding-inline-start:20px;} .modal-body li{margin-bottom:5px;} .modal-body code{background:var(--panel2); padding:1px 6px; border-radius:5px; font-size:12.5px; font-family:'Consolas', monospace;} .modal-body .ex{background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:8px 12px; margin:6px 0; font-family:'Consolas', monospace; font-size:12px; color:var(--muted);} #instructionsBtn{background:var(--panel2); border:1px solid var(--border); color:var(--text); display:flex; align-items:center; gap:5px;} #instructionsBtn:hover{background:#262b36;} .typing{display:flex; gap:4px; align-self:flex-start; padding:10px 15px;} .typing span{width:6px;height:6px;background:var(--muted);border-radius:50%;animation:blink 1.3s infinite;} .typing span:nth-child(2){animation-delay:.2s;} .typing span:nth-child(3){animation-delay:.4s;} @keyframes blink{0%,80%,100%{opacity:.2;} 40%{opacity:1;}} ::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-thumb{background:var(--border); border-radius:4px;} </style> </head> <body> <header> <h1><span class="dot" id="statusDot"></span> סוכן קבצים מקומי - Gemini</h1> <div class="top-controls"> <input type="password" id="apiKey" placeholder="Gemini API Key" style="width:220px;"> <input type="text" id="modelName" list="modelOptions" placeholder="שם מודל" value="gemini-3.5-flash-lite" style="width:150px;"> <datalist id="modelOptions"></datalist> <button class="secondary" id="loadModelsBtn" title="טען את המודלים הזמינים למפתח שהוזן">טען מודלים</button> <span id="modelLoadStatus" class="folder-status" aria-live="polite"></span> <button class="secondary" id="saveKeyBtn">שמור מפתח</button> <button id="pickFolderBtn">בחר תיקייה</button> <button id="instructionsBtn">❓ הוראות שימוש</button> </div> </header> <div class="hint"> <span class="folder-status" id="folderStatus">לא נבחרה תיקייה עדיין.</span> | <label class="settings-row" style="display:inline-flex;"> <input type="checkbox" id="confirmDelete" checked style="margin-left:5px;"> אישור ידני לפני מחיקה </label> | <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" style="color:var(--accent); text-decoration:none;">קבל / נהל מפתח API ב-Google AI Studio ↗</a> </div> <main> <aside id="historySidebar"> <div class="hist-header"> <span>היסטוריית שיחות</span> <button class="secondary small" id="newChatBtn">+ שיחה חדשה</button> </div> <div class="hist-file-controls"> <div class="hist-status" id="historyFileStatus">לא מחובר קובץ היסטוריה - השיחה תישמר רק בזיכרון הדף הנוכחי.</div> <button class="secondary small" id="reconnectHistoryBtn" style="display:none;">🔄 חבר להיסטוריה השמורה</button> <button class="secondary small" id="chooseHistoryBtn">📁 בחר / צור קובץ היסטוריה</button> </div> <div class="hist-list" id="historyList"> <div class="hist-empty">אין שיחות שמורות עדיין.</div> </div> </aside> <div id="chat"> <div class="msg system">בחר תיקייה לעבודה, הזן מפתח API של Gemini, ותוכל לתת הוראות בעברית או באנגלית (למשל: "צור קובץ notes.txt עם התוכן שלום", "שנה שם של old.txt ל-new.txt", "העתק את קובץ א' לתיקיית ב'").</div> </div> </main> <footer> <textarea id="userInput" placeholder="כתוב הוראה לסוכן..." rows="1"></textarea> <button id="sendBtn">שלח</button> </footer> <div class="credit">יוצר: <a href="https://mitmachim.top/user/%D7%9E%D7%99%D7%A0%D7%99-%D7%94%D7%90%D7%A7%D7%A8" target="_blank" rel="noopener noreferrer">מיני האקר</a></div> <div class="modal-overlay" id="instructionsModal" style="display:none;"> <div class="modal-box"> <div class="modal-header"> <h2>📖 הוראות שימוש</h2> <button id="closeInstructionsBtn">✕</button> </div> <div class="modal-body"> <h3>מה זה הכלי הזה?</h3> <p>עמוד HTML עצמאי שרץ כולו בדפדפן שלך (בלי שרת), ומחבר בין מודל שפה של Gemini לבין תיקייה אמיתית במחשב שלך - כך שאפשר לתת לו הוראות בשפה חופשית והוא מבצע אותן בפועל על הקבצים.</p> <h3>דרישות חשובות</h3> <ul> <li>עובד רק ב-<b>Google Chrome</b> או <b>Microsoft Edge</b>, בגרסה עדכנית, <b>על מחשב</b>.</li> <li>לא עובד בטלפון/טאבלט (בכל דפדפן), ולא ב-Firefox או Safari - אלה לא תומכים בגישה לקבצים מקומיים.</li> <li>נדרש חיבור אינטרנט (לתקשורת עם שרתי Gemini).</li> </ul> <h3>שלב 1: מפתח API</h3> <p>הזן מפתח Gemini API בשדה "Gemini API Key". אם אין לך, לחץ על הקישור "קבל / נהל מפתח API ב-Google AI Studio" מתחת לשדות. לחיצה על "שמור מפתח" שומרת אותו בדפדפן הזה לפעם הבאה (הוא לא נשלח לשום מקום חוץ מ-Google, ולא נשמר על ידי היוצר של הכלי).</p> <h3>שלב 2: שם מודל</h3> <p>שדה "שם מודל" קובע איזה מודל Gemini משמש. ברירת המחדל היא <code>gemini-3.5-flash-lite</code>. אם תקבל שגיאת 404 שאומרת שהמודל לא קיים, Google כנראה שינתה שמות - פשוט הקלד שם מודל עדכני יותר (אפשר לבדוק ב-Google AI Studio).</p> <h3>שלב 3: בחירת תיקיית עבודה</h3> <p>לחץ "בחר תיקייה" ובחר את התיקייה שבה הסוכן יעבוד. הוא <b>לא</b> יכול לצאת מחוץ לתיקייה הזו - זו הגנת אבטחה של הדפדפן עצמו.</p> <h3>מה הסוכן יודע לעשות</h3> <ul> <li><b>ליצור / לערוך קבצים</b> - כולל קבצי קוד (JS, Python, HTML, CSS, JSON וכו') וגם קבצים בינאריים כמו תמונות (הוא כותב מחדש את הקובץ השלם בכל עריכה).</li> <li><b>לקרוא קבצים קיימים.</b></li> <li><b>לשנות שם</b> לקובץ או תיקייה.</li> <li><b>להעביר / להעתיק</b> קבצים ותיקיות שלמות (כולל רקורסיבית).</li> <li><b>למחוק</b> קבצים/תיקיות - עם בקשת אישור ידנית לפני כל מחיקה (אפשר לכבות בתיבת הסימון "אישור ידני לפני מחיקה").</li> <li><b>ליצור תיקיות חדשות.</b></li> <li><b>לרשום את תוכן התיקייה</b> (כולל רקורסיבית לכל תת-התיקיות).</li> <li><b>לחפש טקסט</b> (כמו grep) בכל הקבצים בתיקייה, כדי למצוא איפה מוגדר משהו מבלי לנחש שם קובץ.</li> </ul> <div class="ex">"צור קובץ app.py שמדפיס שלום עולם"<br>"תערוך את style.css ותשנה את צבע הרקע לכחול"<br>"תמצא באיזה קובץ מוגדרת הפונקציה login"<br>"תעביר את כל קבצי ה-png לתיקייה בשם images"</div> <h3>מה הסוכן <u>לא</u> יודע לעשות</h3> <ul> <li>ליצור תמונות/וידאו מאפס (זה דורש מודל נפרד כמו Imagen/Veo, לא כלול כאן).</li> <li>לערוך "תוכן" של קובץ תמונה בינארי (PNG/JPG) לפי בקשה חופשית - זה לא קובץ טקסט. הוא כן יכול ליצור/לערוך תמונות וקטוריות בפורמט SVG, כי הן בעצם קוד טקסטואלי.</li> <li>לצאת מגבולות התיקייה שנבחרה.</li> </ul> <h3>היסטוריית שיחות</h3> <p>בחלונית הצד: לחץ "בחר / צור קובץ היסטוריה" ובחר איפה לשמור קובץ JSON שישמור את כל השיחות. מרגע זה כל שיחה נשמרת אוטומטית. בפעם הבאה שתפתח את הכלי, הכלי "יזכור" את הקובץ ויציג כפתור "חבר להיסטוריה השמורה" (לחיצה אחת נדרשת - הגבלת אבטחה של הדפדפן). לחיצה על שיחה ברשימה טוענת אותה במלואה וממשיכה אותה בדיוק מאיפה שנעצרה. "+ שיחה חדשה" פותח שיחה נפרדת בלי למחוק את הישנות.</p> <h3>אבטחה ופרטיות</h3> <ul> <li>הכלי כולו רץ מקומית בדפדפן - אין שרת ביניים של היוצר, אין איסוף מידע.</li> <li>המפתח שלך והיסטוריית השיחות נשלחים <b>רק</b> לשרתי Google (Gemini API) לצורך קבלת תשובות, ולשום גורם אחר.</li> <li>גישה לתיקיות/קבצים תמיד דורשת אישור מפורש שלך דרך חלונית הדפדפן.</li> </ul> </div> </div> </div> <script> (function(){ const chatEl = document.getElementById('chat'); const apiKeyEl = document.getElementById('apiKey'); const modelNameEl = document.getElementById('modelName'); const modelOptionsEl = document.getElementById('modelOptions'); const loadModelsBtn = document.getElementById('loadModelsBtn'); const modelLoadStatusEl = document.getElementById('modelLoadStatus'); const folderStatusEl = document.getElementById('folderStatus'); const statusDot = document.getElementById('statusDot'); const userInputEl = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const pickFolderBtn = document.getElementById('pickFolderBtn'); const saveKeyBtn = document.getElementById('saveKeyBtn'); const confirmDeleteEl = document.getElementById('confirmDelete'); const newChatBtn = document.getElementById('newChatBtn'); const historyFileStatusEl = document.getElementById('historyFileStatus'); const reconnectHistoryBtn = document.getElementById('reconnectHistoryBtn'); const chooseHistoryBtn = document.getElementById('chooseHistoryBtn'); const historyListEl = document.getElementById('historyList'); const instructionsBtn = document.getElementById('instructionsBtn'); const instructionsModal = document.getElementById('instructionsModal'); const closeInstructionsBtn = document.getElementById('closeInstructionsBtn'); function openInstructions(){ instructionsModal.style.display = 'flex'; } function closeInstructions(){ instructionsModal.style.display = 'none'; } instructionsBtn.addEventListener('click', openInstructions); closeInstructionsBtn.addEventListener('click', closeInstructions); instructionsModal.addEventListener('click', (e) => { if(e.target === instructionsModal) closeInstructions(); }); document.addEventListener('keydown', (e) => { if(e.key === 'Escape') closeInstructions(); }); // ---------- global diagnostics (free feature: surfaces hidden JS errors instead of failing silently) ---------- window.addEventListener('error', (e) => { addMsg('system', '⚠ שגיאת JavaScript: ' + e.message); }); window.addEventListener('unhandledrejection', (e) => { addMsg('system', '⚠ שגיאה לא מטופלת: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)); }); let rootHandle = null; let history = []; // Gemini "contents" array for the CURRENT session let busy = false; // ---------- persistent chat history (saved to a JSON file at a location you choose) ---------- let historyFileHandle = null; let sessions = []; // [{ id, title, createdAt, updatedAt, messages }] let currentSessionId = null; function idbOpen(){ return new Promise((resolve, reject) => { const req = indexedDB.open('gfa_store', 1); req.onupgradeneeded = () => { req.result.createObjectStore('kv'); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function idbGet(key){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').get(key); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); } async function idbSet(key, val){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function formatTime(iso){ try{ const d = new Date(iso); return d.toLocaleDateString('he-IL') + ' ' + d.toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'}); }catch(e){ return ''; } } function renderSidebar(){ if(!sessions.length){ historyListEl.innerHTML = '<div class="hist-empty">אין שיחות שמורות עדיין.</div>'; return; } const sorted = [...sessions].sort((a,b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')); historyListEl.innerHTML = ''; for(const s of sorted){ const div = document.createElement('div'); div.className = 'hist-item' + (s.id === currentSessionId ? ' active' : ''); const title = s.title || 'שיחה חדשה'; div.innerHTML = `<span class="hi-title">${escapeHtml(title)}</span><span class="hi-time">${escapeHtml(formatTime(s.updatedAt || s.createdAt))}</span>`; div.addEventListener('click', () => loadSession(s.id)); historyListEl.appendChild(div); } } function replayHistoryToUI(hist){ for(const item of hist){ const parts = item.parts || []; const textParts = parts.filter(p => p.text); const fcParts = parts.filter(p => p.functionCall); if(item.role === 'user' && textParts.length){ addMsg('user', textParts.map(p => p.text).join('\n')); } else if(item.role === 'model'){ if(textParts.length) addMsg('model', textParts.map(p => p.text).join('\n')); for(const fc of fcParts){ addToolLog(fc.functionCall.name, fc.functionCall.args || {}, '(מההיסטוריה)', false); } } // functionResponse parts (role 'user') are intentionally not replayed - their matching tool-log line already shown above } } function loadSession(id){ const s = sessions.find(x => x.id === id); if(!s) return; currentSessionId = id; history = s.messages; // same reference - future pushes stay in sync with the stored session chatEl.innerHTML = ''; replayHistoryToUI(history); renderSidebar(); } function startNewSession(){ currentSessionId = null; history = []; chatEl.innerHTML = ''; addMsg('system', 'שיחה חדשה. כתוב הוראה כדי להתחיל.'); renderSidebar(); } newChatBtn.addEventListener('click', startNewSession); async function saveSessionsToDisk(){ if(!historyFileHandle) return; try{ const writable = await historyFileHandle.createWritable(); await writable.write(JSON.stringify({ version: 1, sessions }, null, 2)); await writable.close(); }catch(e){ addMsg('system', 'שמירת היסטוריה נכשלה: ' + e.message); } } async function loadSessionsFromDisk(){ try{ const file = await historyFileHandle.getFile(); const text = await file.text(); if(text.trim()){ const data = JSON.parse(text); sessions = Array.isArray(data.sessions) ? data.sessions : []; } else { sessions = []; } renderSidebar(); addMsg('system', 'קובץ היסטוריה מחובר (' + sessions.length + ' שיחות שמורות נטענו).'); }catch(e){ addMsg('system', 'טעינת היסטוריה נכשלה: ' + e.message); sessions = []; } } function setHistoryStatus(text, connected){ historyFileStatusEl.textContent = text; historyFileStatusEl.className = 'hist-status' + (connected ? ' connected' : ''); } async function connectNewHistoryFile(){ if(!('showSaveFilePicker' in window)){ addMsg('system', 'הדפדפן הזה לא תומך בשמירת קובץ היסטוריה מקומי (showSaveFilePicker). נסה Chrome/Edge עדכניים.'); return; } try{ const handle = await window.showSaveFilePicker({ suggestedName: 'gemini-agent-chat-history.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] }); historyFileHandle = handle; await idbSet('history_handle', handle); await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; await saveSessionsToDisk(); // ensure a valid file exists immediately }catch(e){ if(e.name !== 'AbortError') addMsg('system', 'חיבור קובץ היסטוריה נכשל: ' + e.message); } } chooseHistoryBtn.addEventListener('click', connectNewHistoryFile); async function reconnectSavedHistoryFile(){ try{ const handle = await idbGet('history_handle'); if(!handle){ setHistoryStatus('לא נמצא קובץ היסטוריה שמור.', false); return; } const perm = await handle.requestPermission({ mode: 'readwrite' }); if(perm !== 'granted'){ setHistoryStatus('הגישה לקובץ ההיסטוריה נדחתה.', false); return; } historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; }catch(e){ addMsg('system', 'חיבור מחדש נכשל: ' + e.message); } } reconnectHistoryBtn.addEventListener('click', reconnectSavedHistoryFile); // on load: silently check (no permission prompt yet) whether a previously-used history file is remembered (async () => { try{ const handle = await idbGet('history_handle'); if(handle){ const state = await handle.queryPermission({ mode: 'readwrite' }); if(state === 'granted'){ historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); } else { setHistoryStatus('נמצא קובץ היסטוריה קודם (' + handle.name + ') - לחץ לחיבור.', false); reconnectHistoryBtn.style.display = ''; } } }catch(e){ /* IndexedDB unavailable or no saved handle - ignore, app still works without persistence */ } })(); // ---------- persistence for convenience (local only) ---------- try{ const savedKey = localStorage.getItem('gfa_api_key'); if(savedKey) apiKeyEl.value = savedKey; const savedModel = localStorage.getItem('gfa_model'); if(savedModel) modelNameEl.value = savedModel; }catch(e){} saveKeyBtn.addEventListener('click', () => { try{ localStorage.setItem('gfa_api_key', apiKeyEl.value.trim()); localStorage.setItem('gfa_model', modelNameEl.value.trim()); toast('המפתח והמודל נשמרו בדפדפן זה.'); loadAvailableModels(false); }catch(e){ toast('שמירה נכשלה: ' + e.message, true); } }); async function loadAvailableModels(showMessage = true){ const apiKey = apiKeyEl.value.trim(); if(!apiKey){ modelLoadStatusEl.textContent = 'הזן מפתח API תחילה'; if(showMessage) toast('כדי לטעון מודלים יש להזין מפתח API של Gemini.', true); return; } loadModelsBtn.disabled = true; modelLoadStatusEl.textContent = 'טוען...'; try{ const models = []; let pageToken = ''; do{ const params = new URLSearchParams({ key: apiKey, pageSize: '100' }); if(pageToken) params.set('pageToken', pageToken); const res = await fetch('https://generativelanguage.googleapis.com/v1beta/models?' + params.toString()); if(!res.ok){ const errorText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errorText.slice(0, 250)); } const data = await res.json(); for(const model of (data.models || [])){ const methods = model.supportedGenerationMethods || []; if(methods.includes('generateContent') && model.name){ models.push({ name: model.name.replace(/^models\\//, ''), label: model.displayName || model.name }); } } pageToken = data.nextPageToken || ''; }while(pageToken); const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()] .sort((a, b) => a.name.localeCompare(b.name)); modelOptionsEl.innerHTML = ''; for(const model of uniqueModels){ const option = document.createElement('option'); option.value = model.name; option.label = model.label; modelOptionsEl.appendChild(option); } modelLoadStatusEl.textContent = uniqueModels.length ? ('✅ ' + uniqueModels.length + ' מודלים') : 'לא נמצאו מודלים'; if(showMessage) toast('נטענו ' + uniqueModels.length + ' מודלים זמינים.'); }catch(e){ modelLoadStatusEl.textContent = 'טעינה נכשלה'; if(showMessage) toast('טעינת המודלים נכשלה: ' + e.message, true); }finally{ loadModelsBtn.disabled = false; } } loadModelsBtn.addEventListener('click', () => loadAvailableModels(true)); function toast(text, isErr){ addMsg('system', text); } // Load the model list automatically when a saved API key is available. if(apiKeyEl.value.trim()) loadAvailableModels(false); // ---------- UI helpers ---------- function addMsg(role, text){ const div = document.createElement('div'); div.className = 'msg ' + role; div.textContent = text; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; return div; } function addToolLog(opName, argsObj, resultText, isErr){ const div = document.createElement('div'); div.className = 'tool-log'; const argsStr = JSON.stringify(argsObj); div.innerHTML = `<span class="op">⚙ ${escapeHtml(opName)}</span>(${escapeHtml(argsStr)}) → ` + `<span class="${isErr?'err':'ok'}">${escapeHtml(resultText)}</span>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } let typingEl = null; function showTyping(){ typingEl = document.createElement('div'); typingEl.className = 'typing'; typingEl.innerHTML = '<span></span><span></span><span></span>'; chatEl.appendChild(typingEl); chatEl.scrollTop = chatEl.scrollHeight; } function hideTyping(){ if(typingEl){ typingEl.remove(); typingEl = null; } } // ---------- folder selection ---------- function checkSupport(){ if(!('showDirectoryPicker' in window)){ statusDot.style.background = 'var(--danger)'; const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); let msg = '❌ הדפדפן/המכשיר הזה לא תומך בבחירת תיקייה מקומית (File System Access API).'; if(isMobile){ msg += ' תכונה זו לא נתמכת כלל בדפדפנים בטלפון נייד (לא Android ולא iOS, בכל דפדפן) - יש לפתוח את הקובץ במחשב שולחני.'; } else { msg += ' יש לפתוח את הקובץ במחשב, בדפדפן Google Chrome או Microsoft Edge בגרסה עדכנית (לא Firefox ולא Safari - הם לא תומכים בכך).'; } folderStatusEl.textContent = msg; pickFolderBtn.disabled = true; addMsg('system', msg); return false; } return true; } const isSupported = checkSupport(); pickFolderBtn.addEventListener('click', async () => { if(!('showDirectoryPicker' in window)){ checkSupport(); return; } try{ rootHandle = await window.showDirectoryPicker(); folderStatusEl.innerHTML = 'תיקייה נבחרה: <b>' + escapeHtml(rootHandle.name) + '</b>'; statusDot.style.background = '#3ecf8e'; addMsg('system', 'נבחרה תיקיית עבודה: ' + rootHandle.name + '. הסוכן יכול כעת לפעול בתוכה בלבד.'); }catch(e){ if(e.name !== 'AbortError') toast('בחירת תיקייה נכשלה: ' + e.message, true); } }); // ---------- path resolution helpers (relative to rootHandle) ---------- function splitPath(p){ return String(p || '').split('/').map(s => s.trim()).filter(s => s.length && s !== '.'); } async function getDirHandle(path, create){ const parts = splitPath(path); let dir = rootHandle; for(const part of parts){ dir = await dir.getDirectoryHandle(part, { create: !!create }); } return dir; } async function getFileHandleAt(path, create){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב קובץ'); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), create); return await dir.getFileHandle(fileName, { create: !!create }); } async function readFileText(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); return await file.text(); } async function writeFileText(path, content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(!overwrite){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; // not found -> fine, continue } } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(content ?? ''); await writable.close(); return true; } async function deleteEntry(path, recursive){ const parts = splitPath(path); if(parts.length === 0) throw new Error('לא ניתן למחוק את תיקיית השורש'); const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); await dir.removeEntry(name, { recursive: !!recursive }); return true; } async function createDirectory(path){ await getDirHandle(path, true); return true; } async function listDirectory(path, recursive){ const dir = await getDirHandle(path || '', false); const results = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; results.push({ name: full, type: handle.kind }); if(recursive && handle.kind === 'directory'){ await walk(handle, full); } } } await walk(dir, path || ''); return results; } // free bonus feature: text search (grep-like) across files in a folder, recursively async function searchInFiles(path, query, recursive, caseSensitive){ const dir = await getDirHandle(path || '', false); const q = caseSensitive ? query : String(query).toLowerCase(); const matches = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; if(handle.kind === 'file'){ try{ const file = await handle.getFile(); const text = await file.text(); const lines = text.split('\n'); lines.forEach((line, idx) => { const hay = caseSensitive ? line : line.toLowerCase(); if(hay.indexOf(q) !== -1){ matches.push({ file: full, line_number: idx + 1, line_text: line.trim().slice(0, 200) }); } }); }catch(e){ /* skip unreadable/binary files silently */ } } else if(recursive !== false){ await walk(handle, full); } } } await walk(dir, path || ''); return matches.slice(0, 300); } async function copyFile(sourcePath, destPath, overwrite){ // binary-safe copy: works correctly for code/text files AND any binary file (images, pdf, zip, exe...) const fh = await getFileHandleAt(sourcePath, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const parts = splitPath(destPath); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const destFh = await dir.getFileHandle(fileName, { create: true }); const writable = await destFh.createWritable(); await writable.write(buf); await writable.close(); return true; } // reads any file's raw bytes and returns them as a base64 string (for binary files, images, etc.) async function readFileBase64(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ''; const chunkSize = 0x8000; for(let i = 0; i < bytes.length; i += chunkSize){ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)); } return btoa(binary); } // writes a base64 string as raw bytes to a file (for creating/editing binary files) async function writeFileBase64(path, base64Content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const binaryStr = atob(base64Content); const bytes = new Uint8Array(binaryStr.length); for(let i = 0; i < binaryStr.length; i++){ bytes[i] = binaryStr.charCodeAt(i); } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(bytes); await writable.close(); return true; } // determines whether a path is a 'file' or 'directory' (throws if it doesn't exist) async function getEntryKind(path){ const parts = splitPath(path); if(parts.length === 0) return 'directory'; // the root itself const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); try{ await dir.getFileHandle(name, { create: false }); return 'file'; }catch(e){ if(e.name === 'TypeMismatchError'){ return 'directory'; } if(e.name === 'NotFoundError'){ throw new Error('הנתיב לא נמצא: ' + path); } throw e; } } // recursively copies an entire directory tree to a new destination path async function copyDirectoryRecursive(sourcePath, destPath, overwrite){ await createDirectory(destPath); const srcDir = await getDirHandle(sourcePath, false); for await (const [name, handle] of srcDir.entries()){ const srcChild = sourcePath ? sourcePath + '/' + name : name; const destChild = destPath ? destPath + '/' + name : name; if(handle.kind === 'file'){ await copyFile(srcChild, destChild, overwrite !== false); } else { await copyDirectoryRecursive(srcChild, destChild, overwrite); } } } // copies either a file or a whole directory tree, dispatching by actual entry kind async function copyEntryGeneric(sourcePath, destPath, overwrite){ const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, overwrite); } else { await copyDirectoryRecursive(sourcePath, destPath, overwrite); } return kind; } async function moveEntry(sourcePath, destPath){ // supports both files and whole directories: copy (recursively if needed), then delete the original const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, true); await deleteEntry(sourcePath, false); } else { await copyDirectoryRecursive(sourcePath, destPath, true); await deleteEntry(sourcePath, true); } return true; } async function renameEntry(path, newName){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב'); parts.pop(); const destPath = (parts.length ? parts.join('/') + '/' : '') + newName; await moveEntry(path, destPath); return destPath; } // ---------- tool declarations for Gemini ---------- const tools = [{ functionDeclarations: [ { name: 'list_directory', description: 'רשימת קבצים ותיקיות בתוך נתיב יחסי לתיקיית השורש שנבחרה. נתיב ריק = שורש.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי, למשל "" או "docs/sub"' }, recursive: { type: 'BOOLEAN', description: 'האם לרשום גם תת-תיקיות באופן רקורסיבי' } } } }, { name: 'read_file', description: 'קריאת תוכן קובץ קיים - כל סוג קובץ. עבור קבצי קוד/טקסט (js, py, html, css, json, txt, md ועוד) השתמש ב-encoding="utf8" (ברירת מחדל). עבור קבצים בינאריים (תמונות, pdf, zip, exe ועוד) השתמש ב-encoding="base64".', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל, לקוד/טקסט) או "base64" (לקבצים בינאריים)' } }, required: ['path'] } }, { name: 'write_file', description: 'יצירת קובץ חדש או עריכה/דריסה מלאה של קובץ קיים - כל סוג קובץ, כולל קבצי קוד (js, py, html, css, json ועוד). עבור תוכן טקסטואלי/קוד השתמש ב-encoding="utf8" (ברירת מחדל) עם content כטקסט רגיל. עבור קובץ בינארי (תמונה ועוד) השתמש ב-encoding="base64" עם content כמחרוזת base64. יוצר תיקיות ביניים אם צריך.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ היעד' }, content: { type: 'STRING', description: 'התוכן לכתיבה - טקסט רגיל (utf8) או מחרוזת base64 (base64)' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל) או "base64"' }, overwrite: { type: 'BOOLEAN', description: 'האם לדרוס קובץ קיים (ברירת מחדל true)' } }, required: ['path', 'content'] } }, { name: 'delete_entry', description: 'מחיקת קובץ או תיקייה (ריקה, אלא אם recursive=true).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' }, recursive: { type: 'BOOLEAN', description: 'מחיקה רקורסיבית של תיקייה לא ריקה' } }, required: ['path'] } }, { name: 'create_directory', description: 'יצירת תיקייה חדשה (כולל תיקיות ביניים).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' } }, required: ['path'] } }, { name: 'copy_file', description: 'העתקת קובץ בודד או תיקייה שלמה (רקורסיבית) ליעד חדש (בתוך אותה תיקייה או לתיקייה אחרת).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' }, overwrite: { type: 'BOOLEAN' } }, required: ['source_path', 'dest_path'] } }, { name: 'move_entry', description: 'העברת קובץ או תיקייה שלמה למיקום/תיקייה אחרת (מוחק את המקור לאחר ההעתקה, רקורסיבית עבור תיקיות).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' } }, required: ['source_path', 'dest_path'] } }, { name: 'rename_entry', description: 'שינוי שם של קובץ או תיקייה (נשאר באותה תיקיית-אב).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב הקובץ הנוכחי' }, new_name: { type: 'STRING', description: 'השם החדש (ללא נתיב, רק שם קובץ)' } }, required: ['path', 'new_name'] } }, { name: 'search_in_files', description: 'חיפוש טקסט חופשי (כמו grep) בתוך כל קבצי הטקסט/קוד בתיקייה נתונה, כולל תת-תיקיות. מחזיר רשימת התאמות עם שם קובץ, מספר שורה ותוכן השורה. שימושי למציאת קובץ לפי תוכן, או מציאת כל המקומות שמזכירים משתנה/פונקציה מסוימת.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לתיקיית ההתחלה של החיפוש, "" לשורש' }, query: { type: 'STRING', description: 'מחרוזת הטקסט לחיפוש' }, recursive: { type: 'BOOLEAN', description: 'האם לחפש גם בתת-תיקיות (ברירת מחדל true)' }, case_sensitive: { type: 'BOOLEAN', description: 'האם החיפוש רגיש לאותיות גדולות/קטנות (ברירת מחדל false)' } }, required: ['query'] } } ] }]; async function executeTool(name, args){ switch(name){ case 'list_directory': { const entries = await listDirectory(args.path || '', !!args.recursive); return { entries }; } case 'read_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; const content = encoding === 'base64' ? await readFileBase64(args.path) : await readFileText(args.path); return { content, encoding }; } case 'write_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; if(encoding === 'base64'){ await writeFileBase64(args.path, args.content, args.overwrite !== false); } else { await writeFileText(args.path, args.content, args.overwrite !== false); } return { success: true, path: args.path, encoding }; } case 'delete_entry': { if(confirmDeleteEl.checked){ const ok = window.confirm('הסוכן מבקש למחוק: ' + args.path + (args.recursive ? ' (כולל תוכן, רקורסיבי)' : '') + '\nלאשר?'); if(!ok) throw new Error('המשתמש ביטל את פעולת המחיקה'); } await deleteEntry(args.path, !!args.recursive); return { success: true, path: args.path }; } case 'create_directory': { await createDirectory(args.path); return { success: true, path: args.path }; } case 'copy_file': { const kind = await copyEntryGeneric(args.source_path, args.dest_path, args.overwrite !== false); return { success: true, from: args.source_path, to: args.dest_path, type: kind }; } case 'move_entry': { await moveEntry(args.source_path, args.dest_path); return { success: true, from: args.source_path, to: args.dest_path }; } case 'rename_entry': { const newPath = await renameEntry(args.path, args.new_name); return { success: true, new_path: newPath }; } case 'search_in_files': { const matches = await searchInFiles(args.path || '', args.query, args.recursive !== false, !!args.case_sensitive); return { matches, count: matches.length }; } default: throw new Error('פונקציה לא מוכרת: ' + name); } } // ---------- Gemini API call loop ---------- const SYSTEM_INSTRUCTION = `אתה סוכן AI שמסייע למשתמש לנהל ולערוך קבצים בתיקייה מקומית שהוא בחר בעצמו, דרך פונקציות (tools) שסופקו לך. אתה תומך בכל סוג קובץ: קבצי קוד ותוכנה (js, ts, py, java, c, cpp, html, css, json, yaml וכו'), קבצי טקסט/מסמכים, וגם קבצים בינאריים (תמונות, pdf, zip וכו') - עבור קבצי טקסט/קוד השתמש בפרמטר encoding="utf8" (ברירת מחדל), ועבור קבצים בינאריים ב-encoding="base64". כשמבקשים ממך לערוך קובץ קוד קיים: תמיד קרא אותו קודם עם read_file כדי לראות את התוכן הנוכחי, בצע את השינוי הנדרש בתוכן, וכתוב את הגרסה המלאה המעודכנת בחזרה עם write_file (overwrite=true) - אין עריכה חלקית/inline, רק כתיבה מחדש של הקובץ השלם. כל פעולה שהמשתמש מבקש (יצירת קובץ, עריכת קוד, שינוי שם, העברה, העתקה, מחיקה, רשימת תוכן) - בצע אותה ישירות דרך קריאה לפונקציה המתאימה, בלי לבקש אישור מיותר (למעט מחיקה, שיש לה אישור נפרד במערכת). נתיבים הם תמיד יחסיים לתיקיית השורש שנבחרה - אל תשתמש בנתיבים מוחלטים כמו C:\\ או /. כשצריך למצוא קובץ או מקום מסוים לפי תוכן (למשל "איפה מוגדרת הפונקציה X", "באיזה קובץ כתוב Y") - השתמש בפונקציה search_in_files במקום לנחש נתיבים. אחרי ביצוע הפעולות, סכם בקצרה ובעברית מה נעשה. אם משהו נכשל, הסבר בפשטות מה קרה ומה אפשר לנסות.`; async function callGemini(){ const apiKey = apiKeyEl.value.trim(); const model = (modelNameEl.value.trim() || 'gemini-3.5-flash-lite'); if(!apiKey) throw new Error('נא להזין API Key של Gemini'); const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`; const body = { contents: history, tools: tools, systemInstruction: { parts: [{ text: SYSTEM_INSTRUCTION }] } }; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(!res.ok){ const errText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errText.slice(0, 400)); } return await res.json(); } async function agentTurn(userText){ history.push({ role: 'user', parts: [{ text: userText }] }); let safety = 0; while(safety++ < 12){ showTyping(); let data; try{ data = await callGemini(); }catch(e){ hideTyping(); addMsg('system', 'שגיאה: ' + e.message); return; } hideTyping(); const candidate = data.candidates && data.candidates[0]; if(!candidate || !candidate.content){ addMsg('system', 'לא התקבלה תשובה מהמודל.'); return; } const parts = candidate.content.parts || []; const functionCalls = parts.filter(p => p.functionCall); const textParts = parts.filter(p => p.text); if(textParts.length){ addMsg('model', textParts.map(p => p.text).join('\n')); } // always record the model's turn in history - even a plain text answer with no tool calls, // so the conversation (and what gets saved to the history file) is complete and Gemini keeps memory of its own replies history.push({ role: 'model', parts: parts }); if(functionCalls.length === 0){ return; // done - plain text answer, nothing more to execute } const responseParts = []; for(const fc of functionCalls){ const { name, args } = fc.functionCall; let resultObj, isErr = false; try{ resultObj = await executeTool(name, args || {}); addToolLog(name, args || {}, 'הצלחה', false); }catch(e){ isErr = true; resultObj = { error: e.message }; addToolLog(name, args || {}, 'שגיאה: ' + e.message, true); } responseParts.push({ functionResponse: { name: name, response: resultObj } }); } history.push({ role: 'user', parts: responseParts }); // loop continues to let the model react to tool results } addMsg('system', 'הגעה למספר צעדים מקסימלי לשיחה אחת - נסה לפרק את הבקשה.'); } async function handleSend(){ if(busy) return; const text = userInputEl.value.trim(); if(!text) return; if(!rootHandle){ addMsg('system', 'קודם יש לבחור תיקיית עבודה (כפתור "בחר תיקייה" למעלה).'); return; } // create/attach a session on the first message of a new conversation if(currentSessionId === null){ const s = { id: 'S' + Date.now() + Math.random().toString(36).slice(2, 7), title: text.slice(0, 45), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), messages: history // same reference history already points to (empty array at this point) }; sessions.unshift(s); currentSessionId = s.id; } addMsg('user', text); userInputEl.value = ''; userInputEl.style.height = 'auto'; busy = true; sendBtn.disabled = true; try{ await agentTurn(text); } finally { busy = false; sendBtn.disabled = false; const s = sessions.find(x => x.id === currentSessionId); if(s){ s.updatedAt = new Date().toISOString(); } renderSidebar(); await saveSessionsToDisk(); } } sendBtn.addEventListener('click', handleSend); userInputEl.addEventListener('keydown', (e) => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); handleSend(); } }); userInputEl.addEventListener('input', () => { userInputEl.style.height = 'auto'; userInputEl.style.height = Math.min(userInputEl.scrollHeight, 140) + 'px'; }); })(); </script> </body> </html> <!-- Injection By NetFree --> <script src="//netfree.link/injection-script/go-payment.js" type="text/javascript" async ></script> <script src="//netfree.link/injection-script/popup-card-init.js" type="text/javascript" async ></script> -
@הידען
קח<!DOCTYPE html> <!-- Injection By NetFree --> <script src="//wfapi.internal.netfree.link/wifree/go-install-wifree" type="text/javascript" async ></script> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>סוכן קבצים - Gemini</title> <style> :root{ --bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --border:#2a2f3a; --text:#e8eaed; --muted:#9aa1ac; --accent:#6c8cff; --accent2:#4f6fe0; --danger:#e5484d; --ok:#3ecf8e; --user-bubble:#2a3550; --model-bubble:#1e222b; } *{box-sizing:border-box;} body{ margin:0; font-family:'Segoe UI', Arial, sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; } header{ padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; } header h1{ font-size:18px; margin:0; display:flex; align-items:center; gap:8px;} header h1 span.dot{width:9px;height:9px;border-radius:50%;background:var(--ok);display:inline-block;} .top-controls{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; } input[type=password], input[type=text]{ background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:8px 10px; border-radius:8px; font-size:13px; outline:none; } input[type=password]:focus, input[type=text]:focus{border-color:var(--accent);} button{ background:var(--accent2); color:#fff; border:none; padding:8px 14px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; transition:background .15s; } button:hover{background:var(--accent);} button.secondary{background:var(--panel2); border:1px solid var(--border); color:var(--text);} button.secondary:hover{background:#262b36;} button:disabled{opacity:.5; cursor:not-allowed;} .folder-status{ font-size:12.5px; color:var(--muted); } .folder-status b{color:var(--ok);} main{flex:1; display:flex; overflow:hidden;} #chat{ flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:14px; } .msg{max-width:78%; padding:12px 15px; border-radius:14px; font-size:14.5px; line-height:1.55; white-space:pre-wrap; word-wrap:break-word;} .msg.user{align-self:flex-end; background:var(--user-bubble); border-bottom-left-radius:4px;} .msg.model{align-self:flex-start; background:var(--model-bubble); border:1px solid var(--border); border-bottom-right-radius:4px;} .msg.system{align-self:center; background:transparent; color:var(--muted); font-size:12.5px; text-align:center; max-width:90%;} .tool-log{ align-self:flex-start; max-width:85%; background:#141821; border:1px dashed var(--border); border-radius:10px; padding:8px 12px; font-size:12.5px; color:var(--muted); font-family:'Consolas', monospace; } .tool-log .op{color:var(--accent); font-weight:600;} .tool-log .err{color:var(--danger);} .tool-log .ok{color:var(--ok);} footer{ padding:14px 20px; background:var(--panel); border-top:1px solid var(--border); display:flex; gap:10px; align-items:flex-end; } textarea{ flex:1; background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:10px; font-size:14.5px; resize:none; min-height:44px; max-height:140px; font-family:inherit; outline:none; } textarea:focus{border-color:var(--accent);} #sendBtn{height:44px; padding:0 22px;} .credit{ text-align:center; padding:8px; font-size:12px; color:var(--muted); background:var(--bg); border-top:1px solid var(--border); } .credit a{color:var(--accent); text-decoration:none; font-weight:600;} .credit a:hover{text-decoration:underline;} .hint{font-size:12px; color:var(--muted); padding:0 20px 6px; } .settings-row{display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted);} /* ---- history sidebar ---- */ aside#historySidebar{ width:250px; flex-shrink:0; background:var(--panel); border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; } .hist-header{ padding:12px 14px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:8px; } .hist-header span{font-size:13px; font-weight:600;} button.small{padding:5px 9px; font-size:11.5px; border-radius:6px;} .hist-file-controls{padding:10px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:6px;} .hist-status{font-size:11.5px; color:var(--muted); line-height:1.5;} .hist-status.connected{color:var(--ok);} .hist-list{flex:1; overflow-y:auto; padding:8px;} .hist-item{ padding:9px 10px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:12.5px; border:1px solid transparent; transition:background .12s; } .hist-item:hover{background:var(--panel2);} .hist-item.active{background:var(--accent2); } .hist-item .hi-title{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;} .hist-item .hi-time{font-size:10.5px; color:var(--muted); display:block; margin-top:2px;} .hist-item.active .hi-time{color:#dfe4ff;} .hist-empty{padding:14px; font-size:12px; color:var(--muted); text-align:center;} @media (max-width:700px){ aside#historySidebar{ position:absolute; inset:0 0 0 auto; z-index:5; height:100%; } } /* ---- instructions modal ---- */ .modal-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.6); display:flex; align-items:center; justify-content:center; z-index:50; padding:20px; } .modal-box{ background:var(--panel); border:1px solid var(--border); border-radius:14px; max-width:640px; width:100%; max-height:82vh; display:flex; flex-direction:column; box-shadow:0 10px 40px rgba(0,0,0,.5); } .modal-header{ display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); } .modal-header h2{margin:0; font-size:17px;} .modal-header button{background:transparent; border:none; color:var(--muted); font-size:18px; padding:4px 8px; cursor:pointer;} .modal-header button:hover{color:var(--text); background:var(--panel2); border-radius:6px;} .modal-body{padding:18px 22px; overflow-y:auto; font-size:13.5px; line-height:1.75;} .modal-body h3{font-size:14.5px; color:var(--accent); margin:18px 0 8px;} .modal-body h3:first-child{margin-top:0;} .modal-body ul{margin:6px 0; padding-inline-start:20px;} .modal-body li{margin-bottom:5px;} .modal-body code{background:var(--panel2); padding:1px 6px; border-radius:5px; font-size:12.5px; font-family:'Consolas', monospace;} .modal-body .ex{background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:8px 12px; margin:6px 0; font-family:'Consolas', monospace; font-size:12px; color:var(--muted);} #instructionsBtn{background:var(--panel2); border:1px solid var(--border); color:var(--text); display:flex; align-items:center; gap:5px;} #instructionsBtn:hover{background:#262b36;} .typing{display:flex; gap:4px; align-self:flex-start; padding:10px 15px;} .typing span{width:6px;height:6px;background:var(--muted);border-radius:50%;animation:blink 1.3s infinite;} .typing span:nth-child(2){animation-delay:.2s;} .typing span:nth-child(3){animation-delay:.4s;} @keyframes blink{0%,80%,100%{opacity:.2;} 40%{opacity:1;}} ::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-thumb{background:var(--border); border-radius:4px;} </style> </head> <body> <header> <h1><span class="dot" id="statusDot"></span> סוכן קבצים מקומי - Gemini</h1> <div class="top-controls"> <input type="password" id="apiKey" placeholder="Gemini API Key" style="width:220px;"> <input type="text" id="modelName" list="modelOptions" placeholder="שם מודל" value="gemini-3.5-flash-lite" style="width:150px;"> <datalist id="modelOptions"></datalist> <button class="secondary" id="loadModelsBtn" title="טען את המודלים הזמינים למפתח שהוזן">טען מודלים</button> <span id="modelLoadStatus" class="folder-status" aria-live="polite"></span> <button class="secondary" id="saveKeyBtn">שמור מפתח</button> <button id="pickFolderBtn">בחר תיקייה</button> <button id="instructionsBtn">❓ הוראות שימוש</button> </div> </header> <div class="hint"> <span class="folder-status" id="folderStatus">לא נבחרה תיקייה עדיין.</span> | <label class="settings-row" style="display:inline-flex;"> <input type="checkbox" id="confirmDelete" checked style="margin-left:5px;"> אישור ידני לפני מחיקה </label> | <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" style="color:var(--accent); text-decoration:none;">קבל / נהל מפתח API ב-Google AI Studio ↗</a> </div> <main> <aside id="historySidebar"> <div class="hist-header"> <span>היסטוריית שיחות</span> <button class="secondary small" id="newChatBtn">+ שיחה חדשה</button> </div> <div class="hist-file-controls"> <div class="hist-status" id="historyFileStatus">לא מחובר קובץ היסטוריה - השיחה תישמר רק בזיכרון הדף הנוכחי.</div> <button class="secondary small" id="reconnectHistoryBtn" style="display:none;">🔄 חבר להיסטוריה השמורה</button> <button class="secondary small" id="chooseHistoryBtn">📁 בחר / צור קובץ היסטוריה</button> </div> <div class="hist-list" id="historyList"> <div class="hist-empty">אין שיחות שמורות עדיין.</div> </div> </aside> <div id="chat"> <div class="msg system">בחר תיקייה לעבודה, הזן מפתח API של Gemini, ותוכל לתת הוראות בעברית או באנגלית (למשל: "צור קובץ notes.txt עם התוכן שלום", "שנה שם של old.txt ל-new.txt", "העתק את קובץ א' לתיקיית ב'").</div> </div> </main> <footer> <textarea id="userInput" placeholder="כתוב הוראה לסוכן..." rows="1"></textarea> <button id="sendBtn">שלח</button> </footer> <div class="credit">יוצר: <a href="https://mitmachim.top/user/%D7%9E%D7%99%D7%A0%D7%99-%D7%94%D7%90%D7%A7%D7%A8" target="_blank" rel="noopener noreferrer">מיני האקר</a></div> <div class="modal-overlay" id="instructionsModal" style="display:none;"> <div class="modal-box"> <div class="modal-header"> <h2>📖 הוראות שימוש</h2> <button id="closeInstructionsBtn">✕</button> </div> <div class="modal-body"> <h3>מה זה הכלי הזה?</h3> <p>עמוד HTML עצמאי שרץ כולו בדפדפן שלך (בלי שרת), ומחבר בין מודל שפה של Gemini לבין תיקייה אמיתית במחשב שלך - כך שאפשר לתת לו הוראות בשפה חופשית והוא מבצע אותן בפועל על הקבצים.</p> <h3>דרישות חשובות</h3> <ul> <li>עובד רק ב-<b>Google Chrome</b> או <b>Microsoft Edge</b>, בגרסה עדכנית, <b>על מחשב</b>.</li> <li>לא עובד בטלפון/טאבלט (בכל דפדפן), ולא ב-Firefox או Safari - אלה לא תומכים בגישה לקבצים מקומיים.</li> <li>נדרש חיבור אינטרנט (לתקשורת עם שרתי Gemini).</li> </ul> <h3>שלב 1: מפתח API</h3> <p>הזן מפתח Gemini API בשדה "Gemini API Key". אם אין לך, לחץ על הקישור "קבל / נהל מפתח API ב-Google AI Studio" מתחת לשדות. לחיצה על "שמור מפתח" שומרת אותו בדפדפן הזה לפעם הבאה (הוא לא נשלח לשום מקום חוץ מ-Google, ולא נשמר על ידי היוצר של הכלי).</p> <h3>שלב 2: שם מודל</h3> <p>שדה "שם מודל" קובע איזה מודל Gemini משמש. ברירת המחדל היא <code>gemini-3.5-flash-lite</code>. אם תקבל שגיאת 404 שאומרת שהמודל לא קיים, Google כנראה שינתה שמות - פשוט הקלד שם מודל עדכני יותר (אפשר לבדוק ב-Google AI Studio).</p> <h3>שלב 3: בחירת תיקיית עבודה</h3> <p>לחץ "בחר תיקייה" ובחר את התיקייה שבה הסוכן יעבוד. הוא <b>לא</b> יכול לצאת מחוץ לתיקייה הזו - זו הגנת אבטחה של הדפדפן עצמו.</p> <h3>מה הסוכן יודע לעשות</h3> <ul> <li><b>ליצור / לערוך קבצים</b> - כולל קבצי קוד (JS, Python, HTML, CSS, JSON וכו') וגם קבצים בינאריים כמו תמונות (הוא כותב מחדש את הקובץ השלם בכל עריכה).</li> <li><b>לקרוא קבצים קיימים.</b></li> <li><b>לשנות שם</b> לקובץ או תיקייה.</li> <li><b>להעביר / להעתיק</b> קבצים ותיקיות שלמות (כולל רקורסיבית).</li> <li><b>למחוק</b> קבצים/תיקיות - עם בקשת אישור ידנית לפני כל מחיקה (אפשר לכבות בתיבת הסימון "אישור ידני לפני מחיקה").</li> <li><b>ליצור תיקיות חדשות.</b></li> <li><b>לרשום את תוכן התיקייה</b> (כולל רקורסיבית לכל תת-התיקיות).</li> <li><b>לחפש טקסט</b> (כמו grep) בכל הקבצים בתיקייה, כדי למצוא איפה מוגדר משהו מבלי לנחש שם קובץ.</li> </ul> <div class="ex">"צור קובץ app.py שמדפיס שלום עולם"<br>"תערוך את style.css ותשנה את צבע הרקע לכחול"<br>"תמצא באיזה קובץ מוגדרת הפונקציה login"<br>"תעביר את כל קבצי ה-png לתיקייה בשם images"</div> <h3>מה הסוכן <u>לא</u> יודע לעשות</h3> <ul> <li>ליצור תמונות/וידאו מאפס (זה דורש מודל נפרד כמו Imagen/Veo, לא כלול כאן).</li> <li>לערוך "תוכן" של קובץ תמונה בינארי (PNG/JPG) לפי בקשה חופשית - זה לא קובץ טקסט. הוא כן יכול ליצור/לערוך תמונות וקטוריות בפורמט SVG, כי הן בעצם קוד טקסטואלי.</li> <li>לצאת מגבולות התיקייה שנבחרה.</li> </ul> <h3>היסטוריית שיחות</h3> <p>בחלונית הצד: לחץ "בחר / צור קובץ היסטוריה" ובחר איפה לשמור קובץ JSON שישמור את כל השיחות. מרגע זה כל שיחה נשמרת אוטומטית. בפעם הבאה שתפתח את הכלי, הכלי "יזכור" את הקובץ ויציג כפתור "חבר להיסטוריה השמורה" (לחיצה אחת נדרשת - הגבלת אבטחה של הדפדפן). לחיצה על שיחה ברשימה טוענת אותה במלואה וממשיכה אותה בדיוק מאיפה שנעצרה. "+ שיחה חדשה" פותח שיחה נפרדת בלי למחוק את הישנות.</p> <h3>אבטחה ופרטיות</h3> <ul> <li>הכלי כולו רץ מקומית בדפדפן - אין שרת ביניים של היוצר, אין איסוף מידע.</li> <li>המפתח שלך והיסטוריית השיחות נשלחים <b>רק</b> לשרתי Google (Gemini API) לצורך קבלת תשובות, ולשום גורם אחר.</li> <li>גישה לתיקיות/קבצים תמיד דורשת אישור מפורש שלך דרך חלונית הדפדפן.</li> </ul> </div> </div> </div> <script> (function(){ const chatEl = document.getElementById('chat'); const apiKeyEl = document.getElementById('apiKey'); const modelNameEl = document.getElementById('modelName'); const modelOptionsEl = document.getElementById('modelOptions'); const loadModelsBtn = document.getElementById('loadModelsBtn'); const modelLoadStatusEl = document.getElementById('modelLoadStatus'); const folderStatusEl = document.getElementById('folderStatus'); const statusDot = document.getElementById('statusDot'); const userInputEl = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const pickFolderBtn = document.getElementById('pickFolderBtn'); const saveKeyBtn = document.getElementById('saveKeyBtn'); const confirmDeleteEl = document.getElementById('confirmDelete'); const newChatBtn = document.getElementById('newChatBtn'); const historyFileStatusEl = document.getElementById('historyFileStatus'); const reconnectHistoryBtn = document.getElementById('reconnectHistoryBtn'); const chooseHistoryBtn = document.getElementById('chooseHistoryBtn'); const historyListEl = document.getElementById('historyList'); const instructionsBtn = document.getElementById('instructionsBtn'); const instructionsModal = document.getElementById('instructionsModal'); const closeInstructionsBtn = document.getElementById('closeInstructionsBtn'); function openInstructions(){ instructionsModal.style.display = 'flex'; } function closeInstructions(){ instructionsModal.style.display = 'none'; } instructionsBtn.addEventListener('click', openInstructions); closeInstructionsBtn.addEventListener('click', closeInstructions); instructionsModal.addEventListener('click', (e) => { if(e.target === instructionsModal) closeInstructions(); }); document.addEventListener('keydown', (e) => { if(e.key === 'Escape') closeInstructions(); }); // ---------- global diagnostics (free feature: surfaces hidden JS errors instead of failing silently) ---------- window.addEventListener('error', (e) => { addMsg('system', '⚠ שגיאת JavaScript: ' + e.message); }); window.addEventListener('unhandledrejection', (e) => { addMsg('system', '⚠ שגיאה לא מטופלת: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)); }); let rootHandle = null; let history = []; // Gemini "contents" array for the CURRENT session let busy = false; // ---------- persistent chat history (saved to a JSON file at a location you choose) ---------- let historyFileHandle = null; let sessions = []; // [{ id, title, createdAt, updatedAt, messages }] let currentSessionId = null; function idbOpen(){ return new Promise((resolve, reject) => { const req = indexedDB.open('gfa_store', 1); req.onupgradeneeded = () => { req.result.createObjectStore('kv'); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function idbGet(key){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').get(key); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); } async function idbSet(key, val){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function formatTime(iso){ try{ const d = new Date(iso); return d.toLocaleDateString('he-IL') + ' ' + d.toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'}); }catch(e){ return ''; } } function renderSidebar(){ if(!sessions.length){ historyListEl.innerHTML = '<div class="hist-empty">אין שיחות שמורות עדיין.</div>'; return; } const sorted = [...sessions].sort((a,b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')); historyListEl.innerHTML = ''; for(const s of sorted){ const div = document.createElement('div'); div.className = 'hist-item' + (s.id === currentSessionId ? ' active' : ''); const title = s.title || 'שיחה חדשה'; div.innerHTML = `<span class="hi-title">${escapeHtml(title)}</span><span class="hi-time">${escapeHtml(formatTime(s.updatedAt || s.createdAt))}</span>`; div.addEventListener('click', () => loadSession(s.id)); historyListEl.appendChild(div); } } function replayHistoryToUI(hist){ for(const item of hist){ const parts = item.parts || []; const textParts = parts.filter(p => p.text); const fcParts = parts.filter(p => p.functionCall); if(item.role === 'user' && textParts.length){ addMsg('user', textParts.map(p => p.text).join('\n')); } else if(item.role === 'model'){ if(textParts.length) addMsg('model', textParts.map(p => p.text).join('\n')); for(const fc of fcParts){ addToolLog(fc.functionCall.name, fc.functionCall.args || {}, '(מההיסטוריה)', false); } } // functionResponse parts (role 'user') are intentionally not replayed - their matching tool-log line already shown above } } function loadSession(id){ const s = sessions.find(x => x.id === id); if(!s) return; currentSessionId = id; history = s.messages; // same reference - future pushes stay in sync with the stored session chatEl.innerHTML = ''; replayHistoryToUI(history); renderSidebar(); } function startNewSession(){ currentSessionId = null; history = []; chatEl.innerHTML = ''; addMsg('system', 'שיחה חדשה. כתוב הוראה כדי להתחיל.'); renderSidebar(); } newChatBtn.addEventListener('click', startNewSession); async function saveSessionsToDisk(){ if(!historyFileHandle) return; try{ const writable = await historyFileHandle.createWritable(); await writable.write(JSON.stringify({ version: 1, sessions }, null, 2)); await writable.close(); }catch(e){ addMsg('system', 'שמירת היסטוריה נכשלה: ' + e.message); } } async function loadSessionsFromDisk(){ try{ const file = await historyFileHandle.getFile(); const text = await file.text(); if(text.trim()){ const data = JSON.parse(text); sessions = Array.isArray(data.sessions) ? data.sessions : []; } else { sessions = []; } renderSidebar(); addMsg('system', 'קובץ היסטוריה מחובר (' + sessions.length + ' שיחות שמורות נטענו).'); }catch(e){ addMsg('system', 'טעינת היסטוריה נכשלה: ' + e.message); sessions = []; } } function setHistoryStatus(text, connected){ historyFileStatusEl.textContent = text; historyFileStatusEl.className = 'hist-status' + (connected ? ' connected' : ''); } async function connectNewHistoryFile(){ if(!('showSaveFilePicker' in window)){ addMsg('system', 'הדפדפן הזה לא תומך בשמירת קובץ היסטוריה מקומי (showSaveFilePicker). נסה Chrome/Edge עדכניים.'); return; } try{ const handle = await window.showSaveFilePicker({ suggestedName: 'gemini-agent-chat-history.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] }); historyFileHandle = handle; await idbSet('history_handle', handle); await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; await saveSessionsToDisk(); // ensure a valid file exists immediately }catch(e){ if(e.name !== 'AbortError') addMsg('system', 'חיבור קובץ היסטוריה נכשל: ' + e.message); } } chooseHistoryBtn.addEventListener('click', connectNewHistoryFile); async function reconnectSavedHistoryFile(){ try{ const handle = await idbGet('history_handle'); if(!handle){ setHistoryStatus('לא נמצא קובץ היסטוריה שמור.', false); return; } const perm = await handle.requestPermission({ mode: 'readwrite' }); if(perm !== 'granted'){ setHistoryStatus('הגישה לקובץ ההיסטוריה נדחתה.', false); return; } historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; }catch(e){ addMsg('system', 'חיבור מחדש נכשל: ' + e.message); } } reconnectHistoryBtn.addEventListener('click', reconnectSavedHistoryFile); // on load: silently check (no permission prompt yet) whether a previously-used history file is remembered (async () => { try{ const handle = await idbGet('history_handle'); if(handle){ const state = await handle.queryPermission({ mode: 'readwrite' }); if(state === 'granted'){ historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); } else { setHistoryStatus('נמצא קובץ היסטוריה קודם (' + handle.name + ') - לחץ לחיבור.', false); reconnectHistoryBtn.style.display = ''; } } }catch(e){ /* IndexedDB unavailable or no saved handle - ignore, app still works without persistence */ } })(); // ---------- persistence for convenience (local only) ---------- try{ const savedKey = localStorage.getItem('gfa_api_key'); if(savedKey) apiKeyEl.value = savedKey; const savedModel = localStorage.getItem('gfa_model'); if(savedModel) modelNameEl.value = savedModel; }catch(e){} saveKeyBtn.addEventListener('click', () => { try{ localStorage.setItem('gfa_api_key', apiKeyEl.value.trim()); localStorage.setItem('gfa_model', modelNameEl.value.trim()); toast('המפתח והמודל נשמרו בדפדפן זה.'); loadAvailableModels(false); }catch(e){ toast('שמירה נכשלה: ' + e.message, true); } }); async function loadAvailableModels(showMessage = true){ const apiKey = apiKeyEl.value.trim(); if(!apiKey){ modelLoadStatusEl.textContent = 'הזן מפתח API תחילה'; if(showMessage) toast('כדי לטעון מודלים יש להזין מפתח API של Gemini.', true); return; } loadModelsBtn.disabled = true; modelLoadStatusEl.textContent = 'טוען...'; try{ const models = []; let pageToken = ''; do{ const params = new URLSearchParams({ key: apiKey, pageSize: '100' }); if(pageToken) params.set('pageToken', pageToken); const res = await fetch('https://generativelanguage.googleapis.com/v1beta/models?' + params.toString()); if(!res.ok){ const errorText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errorText.slice(0, 250)); } const data = await res.json(); for(const model of (data.models || [])){ const methods = model.supportedGenerationMethods || []; if(methods.includes('generateContent') && model.name){ models.push({ name: model.name.replace(/^models\\//, ''), label: model.displayName || model.name }); } } pageToken = data.nextPageToken || ''; }while(pageToken); const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()] .sort((a, b) => a.name.localeCompare(b.name)); modelOptionsEl.innerHTML = ''; for(const model of uniqueModels){ const option = document.createElement('option'); option.value = model.name; option.label = model.label; modelOptionsEl.appendChild(option); } modelLoadStatusEl.textContent = uniqueModels.length ? ('✅ ' + uniqueModels.length + ' מודלים') : 'לא נמצאו מודלים'; if(showMessage) toast('נטענו ' + uniqueModels.length + ' מודלים זמינים.'); }catch(e){ modelLoadStatusEl.textContent = 'טעינה נכשלה'; if(showMessage) toast('טעינת המודלים נכשלה: ' + e.message, true); }finally{ loadModelsBtn.disabled = false; } } loadModelsBtn.addEventListener('click', () => loadAvailableModels(true)); function toast(text, isErr){ addMsg('system', text); } // Load the model list automatically when a saved API key is available. if(apiKeyEl.value.trim()) loadAvailableModels(false); // ---------- UI helpers ---------- function addMsg(role, text){ const div = document.createElement('div'); div.className = 'msg ' + role; div.textContent = text; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; return div; } function addToolLog(opName, argsObj, resultText, isErr){ const div = document.createElement('div'); div.className = 'tool-log'; const argsStr = JSON.stringify(argsObj); div.innerHTML = `<span class="op">⚙ ${escapeHtml(opName)}</span>(${escapeHtml(argsStr)}) → ` + `<span class="${isErr?'err':'ok'}">${escapeHtml(resultText)}</span>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } let typingEl = null; function showTyping(){ typingEl = document.createElement('div'); typingEl.className = 'typing'; typingEl.innerHTML = '<span></span><span></span><span></span>'; chatEl.appendChild(typingEl); chatEl.scrollTop = chatEl.scrollHeight; } function hideTyping(){ if(typingEl){ typingEl.remove(); typingEl = null; } } // ---------- folder selection ---------- function checkSupport(){ if(!('showDirectoryPicker' in window)){ statusDot.style.background = 'var(--danger)'; const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); let msg = '❌ הדפדפן/המכשיר הזה לא תומך בבחירת תיקייה מקומית (File System Access API).'; if(isMobile){ msg += ' תכונה זו לא נתמכת כלל בדפדפנים בטלפון נייד (לא Android ולא iOS, בכל דפדפן) - יש לפתוח את הקובץ במחשב שולחני.'; } else { msg += ' יש לפתוח את הקובץ במחשב, בדפדפן Google Chrome או Microsoft Edge בגרסה עדכנית (לא Firefox ולא Safari - הם לא תומכים בכך).'; } folderStatusEl.textContent = msg; pickFolderBtn.disabled = true; addMsg('system', msg); return false; } return true; } const isSupported = checkSupport(); pickFolderBtn.addEventListener('click', async () => { if(!('showDirectoryPicker' in window)){ checkSupport(); return; } try{ rootHandle = await window.showDirectoryPicker(); folderStatusEl.innerHTML = 'תיקייה נבחרה: <b>' + escapeHtml(rootHandle.name) + '</b>'; statusDot.style.background = '#3ecf8e'; addMsg('system', 'נבחרה תיקיית עבודה: ' + rootHandle.name + '. הסוכן יכול כעת לפעול בתוכה בלבד.'); }catch(e){ if(e.name !== 'AbortError') toast('בחירת תיקייה נכשלה: ' + e.message, true); } }); // ---------- path resolution helpers (relative to rootHandle) ---------- function splitPath(p){ return String(p || '').split('/').map(s => s.trim()).filter(s => s.length && s !== '.'); } async function getDirHandle(path, create){ const parts = splitPath(path); let dir = rootHandle; for(const part of parts){ dir = await dir.getDirectoryHandle(part, { create: !!create }); } return dir; } async function getFileHandleAt(path, create){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב קובץ'); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), create); return await dir.getFileHandle(fileName, { create: !!create }); } async function readFileText(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); return await file.text(); } async function writeFileText(path, content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(!overwrite){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; // not found -> fine, continue } } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(content ?? ''); await writable.close(); return true; } async function deleteEntry(path, recursive){ const parts = splitPath(path); if(parts.length === 0) throw new Error('לא ניתן למחוק את תיקיית השורש'); const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); await dir.removeEntry(name, { recursive: !!recursive }); return true; } async function createDirectory(path){ await getDirHandle(path, true); return true; } async function listDirectory(path, recursive){ const dir = await getDirHandle(path || '', false); const results = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; results.push({ name: full, type: handle.kind }); if(recursive && handle.kind === 'directory'){ await walk(handle, full); } } } await walk(dir, path || ''); return results; } // free bonus feature: text search (grep-like) across files in a folder, recursively async function searchInFiles(path, query, recursive, caseSensitive){ const dir = await getDirHandle(path || '', false); const q = caseSensitive ? query : String(query).toLowerCase(); const matches = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; if(handle.kind === 'file'){ try{ const file = await handle.getFile(); const text = await file.text(); const lines = text.split('\n'); lines.forEach((line, idx) => { const hay = caseSensitive ? line : line.toLowerCase(); if(hay.indexOf(q) !== -1){ matches.push({ file: full, line_number: idx + 1, line_text: line.trim().slice(0, 200) }); } }); }catch(e){ /* skip unreadable/binary files silently */ } } else if(recursive !== false){ await walk(handle, full); } } } await walk(dir, path || ''); return matches.slice(0, 300); } async function copyFile(sourcePath, destPath, overwrite){ // binary-safe copy: works correctly for code/text files AND any binary file (images, pdf, zip, exe...) const fh = await getFileHandleAt(sourcePath, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const parts = splitPath(destPath); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const destFh = await dir.getFileHandle(fileName, { create: true }); const writable = await destFh.createWritable(); await writable.write(buf); await writable.close(); return true; } // reads any file's raw bytes and returns them as a base64 string (for binary files, images, etc.) async function readFileBase64(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ''; const chunkSize = 0x8000; for(let i = 0; i < bytes.length; i += chunkSize){ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)); } return btoa(binary); } // writes a base64 string as raw bytes to a file (for creating/editing binary files) async function writeFileBase64(path, base64Content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const binaryStr = atob(base64Content); const bytes = new Uint8Array(binaryStr.length); for(let i = 0; i < binaryStr.length; i++){ bytes[i] = binaryStr.charCodeAt(i); } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(bytes); await writable.close(); return true; } // determines whether a path is a 'file' or 'directory' (throws if it doesn't exist) async function getEntryKind(path){ const parts = splitPath(path); if(parts.length === 0) return 'directory'; // the root itself const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); try{ await dir.getFileHandle(name, { create: false }); return 'file'; }catch(e){ if(e.name === 'TypeMismatchError'){ return 'directory'; } if(e.name === 'NotFoundError'){ throw new Error('הנתיב לא נמצא: ' + path); } throw e; } } // recursively copies an entire directory tree to a new destination path async function copyDirectoryRecursive(sourcePath, destPath, overwrite){ await createDirectory(destPath); const srcDir = await getDirHandle(sourcePath, false); for await (const [name, handle] of srcDir.entries()){ const srcChild = sourcePath ? sourcePath + '/' + name : name; const destChild = destPath ? destPath + '/' + name : name; if(handle.kind === 'file'){ await copyFile(srcChild, destChild, overwrite !== false); } else { await copyDirectoryRecursive(srcChild, destChild, overwrite); } } } // copies either a file or a whole directory tree, dispatching by actual entry kind async function copyEntryGeneric(sourcePath, destPath, overwrite){ const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, overwrite); } else { await copyDirectoryRecursive(sourcePath, destPath, overwrite); } return kind; } async function moveEntry(sourcePath, destPath){ // supports both files and whole directories: copy (recursively if needed), then delete the original const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, true); await deleteEntry(sourcePath, false); } else { await copyDirectoryRecursive(sourcePath, destPath, true); await deleteEntry(sourcePath, true); } return true; } async function renameEntry(path, newName){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב'); parts.pop(); const destPath = (parts.length ? parts.join('/') + '/' : '') + newName; await moveEntry(path, destPath); return destPath; } // ---------- tool declarations for Gemini ---------- const tools = [{ functionDeclarations: [ { name: 'list_directory', description: 'רשימת קבצים ותיקיות בתוך נתיב יחסי לתיקיית השורש שנבחרה. נתיב ריק = שורש.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי, למשל "" או "docs/sub"' }, recursive: { type: 'BOOLEAN', description: 'האם לרשום גם תת-תיקיות באופן רקורסיבי' } } } }, { name: 'read_file', description: 'קריאת תוכן קובץ קיים - כל סוג קובץ. עבור קבצי קוד/טקסט (js, py, html, css, json, txt, md ועוד) השתמש ב-encoding="utf8" (ברירת מחדל). עבור קבצים בינאריים (תמונות, pdf, zip, exe ועוד) השתמש ב-encoding="base64".', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל, לקוד/טקסט) או "base64" (לקבצים בינאריים)' } }, required: ['path'] } }, { name: 'write_file', description: 'יצירת קובץ חדש או עריכה/דריסה מלאה של קובץ קיים - כל סוג קובץ, כולל קבצי קוד (js, py, html, css, json ועוד). עבור תוכן טקסטואלי/קוד השתמש ב-encoding="utf8" (ברירת מחדל) עם content כטקסט רגיל. עבור קובץ בינארי (תמונה ועוד) השתמש ב-encoding="base64" עם content כמחרוזת base64. יוצר תיקיות ביניים אם צריך.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ היעד' }, content: { type: 'STRING', description: 'התוכן לכתיבה - טקסט רגיל (utf8) או מחרוזת base64 (base64)' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל) או "base64"' }, overwrite: { type: 'BOOLEAN', description: 'האם לדרוס קובץ קיים (ברירת מחדל true)' } }, required: ['path', 'content'] } }, { name: 'delete_entry', description: 'מחיקת קובץ או תיקייה (ריקה, אלא אם recursive=true).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' }, recursive: { type: 'BOOLEAN', description: 'מחיקה רקורסיבית של תיקייה לא ריקה' } }, required: ['path'] } }, { name: 'create_directory', description: 'יצירת תיקייה חדשה (כולל תיקיות ביניים).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' } }, required: ['path'] } }, { name: 'copy_file', description: 'העתקת קובץ בודד או תיקייה שלמה (רקורסיבית) ליעד חדש (בתוך אותה תיקייה או לתיקייה אחרת).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' }, overwrite: { type: 'BOOLEAN' } }, required: ['source_path', 'dest_path'] } }, { name: 'move_entry', description: 'העברת קובץ או תיקייה שלמה למיקום/תיקייה אחרת (מוחק את המקור לאחר ההעתקה, רקורסיבית עבור תיקיות).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' } }, required: ['source_path', 'dest_path'] } }, { name: 'rename_entry', description: 'שינוי שם של קובץ או תיקייה (נשאר באותה תיקיית-אב).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב הקובץ הנוכחי' }, new_name: { type: 'STRING', description: 'השם החדש (ללא נתיב, רק שם קובץ)' } }, required: ['path', 'new_name'] } }, { name: 'search_in_files', description: 'חיפוש טקסט חופשי (כמו grep) בתוך כל קבצי הטקסט/קוד בתיקייה נתונה, כולל תת-תיקיות. מחזיר רשימת התאמות עם שם קובץ, מספר שורה ותוכן השורה. שימושי למציאת קובץ לפי תוכן, או מציאת כל המקומות שמזכירים משתנה/פונקציה מסוימת.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לתיקיית ההתחלה של החיפוש, "" לשורש' }, query: { type: 'STRING', description: 'מחרוזת הטקסט לחיפוש' }, recursive: { type: 'BOOLEAN', description: 'האם לחפש גם בתת-תיקיות (ברירת מחדל true)' }, case_sensitive: { type: 'BOOLEAN', description: 'האם החיפוש רגיש לאותיות גדולות/קטנות (ברירת מחדל false)' } }, required: ['query'] } } ] }]; async function executeTool(name, args){ switch(name){ case 'list_directory': { const entries = await listDirectory(args.path || '', !!args.recursive); return { entries }; } case 'read_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; const content = encoding === 'base64' ? await readFileBase64(args.path) : await readFileText(args.path); return { content, encoding }; } case 'write_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; if(encoding === 'base64'){ await writeFileBase64(args.path, args.content, args.overwrite !== false); } else { await writeFileText(args.path, args.content, args.overwrite !== false); } return { success: true, path: args.path, encoding }; } case 'delete_entry': { if(confirmDeleteEl.checked){ const ok = window.confirm('הסוכן מבקש למחוק: ' + args.path + (args.recursive ? ' (כולל תוכן, רקורסיבי)' : '') + '\nלאשר?'); if(!ok) throw new Error('המשתמש ביטל את פעולת המחיקה'); } await deleteEntry(args.path, !!args.recursive); return { success: true, path: args.path }; } case 'create_directory': { await createDirectory(args.path); return { success: true, path: args.path }; } case 'copy_file': { const kind = await copyEntryGeneric(args.source_path, args.dest_path, args.overwrite !== false); return { success: true, from: args.source_path, to: args.dest_path, type: kind }; } case 'move_entry': { await moveEntry(args.source_path, args.dest_path); return { success: true, from: args.source_path, to: args.dest_path }; } case 'rename_entry': { const newPath = await renameEntry(args.path, args.new_name); return { success: true, new_path: newPath }; } case 'search_in_files': { const matches = await searchInFiles(args.path || '', args.query, args.recursive !== false, !!args.case_sensitive); return { matches, count: matches.length }; } default: throw new Error('פונקציה לא מוכרת: ' + name); } } // ---------- Gemini API call loop ---------- const SYSTEM_INSTRUCTION = `אתה סוכן AI שמסייע למשתמש לנהל ולערוך קבצים בתיקייה מקומית שהוא בחר בעצמו, דרך פונקציות (tools) שסופקו לך. אתה תומך בכל סוג קובץ: קבצי קוד ותוכנה (js, ts, py, java, c, cpp, html, css, json, yaml וכו'), קבצי טקסט/מסמכים, וגם קבצים בינאריים (תמונות, pdf, zip וכו') - עבור קבצי טקסט/קוד השתמש בפרמטר encoding="utf8" (ברירת מחדל), ועבור קבצים בינאריים ב-encoding="base64". כשמבקשים ממך לערוך קובץ קוד קיים: תמיד קרא אותו קודם עם read_file כדי לראות את התוכן הנוכחי, בצע את השינוי הנדרש בתוכן, וכתוב את הגרסה המלאה המעודכנת בחזרה עם write_file (overwrite=true) - אין עריכה חלקית/inline, רק כתיבה מחדש של הקובץ השלם. כל פעולה שהמשתמש מבקש (יצירת קובץ, עריכת קוד, שינוי שם, העברה, העתקה, מחיקה, רשימת תוכן) - בצע אותה ישירות דרך קריאה לפונקציה המתאימה, בלי לבקש אישור מיותר (למעט מחיקה, שיש לה אישור נפרד במערכת). נתיבים הם תמיד יחסיים לתיקיית השורש שנבחרה - אל תשתמש בנתיבים מוחלטים כמו C:\\ או /. כשצריך למצוא קובץ או מקום מסוים לפי תוכן (למשל "איפה מוגדרת הפונקציה X", "באיזה קובץ כתוב Y") - השתמש בפונקציה search_in_files במקום לנחש נתיבים. אחרי ביצוע הפעולות, סכם בקצרה ובעברית מה נעשה. אם משהו נכשל, הסבר בפשטות מה קרה ומה אפשר לנסות.`; async function callGemini(){ const apiKey = apiKeyEl.value.trim(); const model = (modelNameEl.value.trim() || 'gemini-3.5-flash-lite'); if(!apiKey) throw new Error('נא להזין API Key של Gemini'); const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`; const body = { contents: history, tools: tools, systemInstruction: { parts: [{ text: SYSTEM_INSTRUCTION }] } }; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(!res.ok){ const errText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errText.slice(0, 400)); } return await res.json(); } async function agentTurn(userText){ history.push({ role: 'user', parts: [{ text: userText }] }); let safety = 0; while(safety++ < 12){ showTyping(); let data; try{ data = await callGemini(); }catch(e){ hideTyping(); addMsg('system', 'שגיאה: ' + e.message); return; } hideTyping(); const candidate = data.candidates && data.candidates[0]; if(!candidate || !candidate.content){ addMsg('system', 'לא התקבלה תשובה מהמודל.'); return; } const parts = candidate.content.parts || []; const functionCalls = parts.filter(p => p.functionCall); const textParts = parts.filter(p => p.text); if(textParts.length){ addMsg('model', textParts.map(p => p.text).join('\n')); } // always record the model's turn in history - even a plain text answer with no tool calls, // so the conversation (and what gets saved to the history file) is complete and Gemini keeps memory of its own replies history.push({ role: 'model', parts: parts }); if(functionCalls.length === 0){ return; // done - plain text answer, nothing more to execute } const responseParts = []; for(const fc of functionCalls){ const { name, args } = fc.functionCall; let resultObj, isErr = false; try{ resultObj = await executeTool(name, args || {}); addToolLog(name, args || {}, 'הצלחה', false); }catch(e){ isErr = true; resultObj = { error: e.message }; addToolLog(name, args || {}, 'שגיאה: ' + e.message, true); } responseParts.push({ functionResponse: { name: name, response: resultObj } }); } history.push({ role: 'user', parts: responseParts }); // loop continues to let the model react to tool results } addMsg('system', 'הגעה למספר צעדים מקסימלי לשיחה אחת - נסה לפרק את הבקשה.'); } async function handleSend(){ if(busy) return; const text = userInputEl.value.trim(); if(!text) return; if(!rootHandle){ addMsg('system', 'קודם יש לבחור תיקיית עבודה (כפתור "בחר תיקייה" למעלה).'); return; } // create/attach a session on the first message of a new conversation if(currentSessionId === null){ const s = { id: 'S' + Date.now() + Math.random().toString(36).slice(2, 7), title: text.slice(0, 45), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), messages: history // same reference history already points to (empty array at this point) }; sessions.unshift(s); currentSessionId = s.id; } addMsg('user', text); userInputEl.value = ''; userInputEl.style.height = 'auto'; busy = true; sendBtn.disabled = true; try{ await agentTurn(text); } finally { busy = false; sendBtn.disabled = false; const s = sessions.find(x => x.id === currentSessionId); if(s){ s.updatedAt = new Date().toISOString(); } renderSidebar(); await saveSessionsToDisk(); } } sendBtn.addEventListener('click', handleSend); userInputEl.addEventListener('keydown', (e) => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); handleSend(); } }); userInputEl.addEventListener('input', () => { userInputEl.style.height = 'auto'; userInputEl.style.height = Math.min(userInputEl.scrollHeight, 140) + 'px'; }); })(); </script> </body> </html> <!-- Injection By NetFree --> <script src="//netfree.link/injection-script/go-payment.js" type="text/javascript" async ></script> <script src="//netfree.link/injection-script/popup-card-init.js" type="text/javascript" async ></script>@הדיבל-המעופף לא טעון מודלים?
-
@הדיבל-המעופף לא טעון מודלים?
@הידען
זה אמור לטעון אוטומטית -
@הידען
זה אמור לטעון אוטומטית@הדיבל-המעופף וזה לא טוען!
-
@הדיבל-המעופף וזה לא טוען!
@הידען
צודק תוקן<!DOCTYPE html> <!-- Injection By NetFree --> <script src="//wfapi.internal.netfree.link/wifree/go-install-wifree" type="text/javascript" async ></script> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>סוכן קבצים - Gemini</title> <style> :root{ --bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --border:#2a2f3a; --text:#e8eaed; --muted:#9aa1ac; --accent:#6c8cff; --accent2:#4f6fe0; --danger:#e5484d; --ok:#3ecf8e; --user-bubble:#2a3550; --model-bubble:#1e222b; } *{box-sizing:border-box;} body{ margin:0; font-family:'Segoe UI', Arial, sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; } header{ padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; } header h1{ font-size:18px; margin:0; display:flex; align-items:center; gap:8px;} header h1 span.dot{width:9px;height:9px;border-radius:50%;background:var(--ok);display:inline-block;} .top-controls{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; } input[type=password], input[type=text], select{ background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:8px 10px; border-radius:8px; font-size:13px; outline:none; } input[type=password]:focus, input[type=text]:focus, select:focus{border-color:var(--accent);} select{cursor:pointer;} button{ background:var(--accent2); color:#fff; border:none; padding:8px 14px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; transition:background .15s; } button:hover{background:var(--accent);} button.secondary{background:var(--panel2); border:1px solid var(--border); color:var(--text);} button.secondary:hover{background:#262b36;} button:disabled{opacity:.5; cursor:not-allowed;} .folder-status{ font-size:12.5px; color:var(--muted); } .folder-status b{color:var(--ok);} main{flex:1; display:flex; overflow:hidden;} #chat{ flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:14px; } .msg{max-width:78%; padding:12px 15px; border-radius:14px; font-size:14.5px; line-height:1.55; white-space:pre-wrap; word-wrap:break-word;} .msg.user{align-self:flex-end; background:var(--user-bubble); border-bottom-left-radius:4px;} .msg.model{align-self:flex-start; background:var(--model-bubble); border:1px solid var(--border); border-bottom-right-radius:4px;} .msg.system{align-self:center; background:transparent; color:var(--muted); font-size:12.5px; text-align:center; max-width:90%;} .tool-log{ align-self:flex-start; max-width:85%; background:#141821; border:1px dashed var(--border); border-radius:10px; padding:8px 12px; font-size:12.5px; color:var(--muted); font-family:'Consolas', monospace; } .tool-log .op{color:var(--accent); font-weight:600;} .tool-log .err{color:var(--danger);} .tool-log .ok{color:var(--ok);} footer{ padding:14px 20px; background:var(--panel); border-top:1px solid var(--border); display:flex; gap:10px; align-items:flex-end; } textarea{ flex:1; background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:10px; font-size:14.5px; resize:none; min-height:44px; max-height:140px; font-family:inherit; outline:none; } textarea:focus{border-color:var(--accent);} #sendBtn{height:44px; padding:0 22px;} .credit{ text-align:center; padding:8px; font-size:12px; color:var(--muted); background:var(--bg); border-top:1px solid var(--border); } .credit a{color:var(--accent); text-decoration:none; font-weight:600;} .credit a:hover{text-decoration:underline;} .hint{font-size:12px; color:var(--muted); padding:0 20px 6px; } .settings-row{display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted);} /* ---- history sidebar ---- */ aside#historySidebar{ width:250px; flex-shrink:0; background:var(--panel); border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; } .hist-header{ padding:12px 14px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:8px; } .hist-header span{font-size:13px; font-weight:600;} button.small{padding:5px 9px; font-size:11.5px; border-radius:6px;} .hist-file-controls{padding:10px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:6px;} .hist-status{font-size:11.5px; color:var(--muted); line-height:1.5;} .hist-status.connected{color:var(--ok);} .hist-list{flex:1; overflow-y:auto; padding:8px;} .hist-item{ padding:9px 10px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:12.5px; border:1px solid transparent; transition:background .12s; } .hist-item:hover{background:var(--panel2);} .hist-item.active{background:var(--accent2); } .hist-item .hi-title{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;} .hist-item .hi-time{font-size:10.5px; color:var(--muted); display:block; margin-top:2px;} .hist-item.active .hi-time{color:#dfe4ff;} .hist-empty{padding:14px; font-size:12px; color:var(--muted); text-align:center;} @media (max-width:700px){ aside#historySidebar{ position:absolute; inset:0 0 0 auto; z-index:5; height:100%; } } /* ---- instructions modal ---- */ .modal-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.6); display:flex; align-items:center; justify-content:center; z-index:50; padding:20px; } .modal-box{ background:var(--panel); border:1px solid var(--border); border-radius:14px; max-width:640px; width:100%; max-height:82vh; display:flex; flex-direction:column; box-shadow:0 10px 40px rgba(0,0,0,.5); } .modal-header{ display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); } .modal-header h2{margin:0; font-size:17px;} .modal-header button{background:transparent; border:none; color:var(--muted); font-size:18px; padding:4px 8px; cursor:pointer;} .modal-header button:hover{color:var(--text); background:var(--panel2); border-radius:6px;} .modal-body{padding:18px 22px; overflow-y:auto; font-size:13.5px; line-height:1.75;} .modal-body h3{font-size:14.5px; color:var(--accent); margin:18px 0 8px;} .modal-body h3:first-child{margin-top:0;} .modal-body ul{margin:6px 0; padding-inline-start:20px;} .modal-body li{margin-bottom:5px;} .modal-body code{background:var(--panel2); padding:1px 6px; border-radius:5px; font-size:12.5px; font-family:'Consolas', monospace;} .modal-body .ex{background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:8px 12px; margin:6px 0; font-family:'Consolas', monospace; font-size:12px; color:var(--muted);} #instructionsBtn{background:var(--panel2); border:1px solid var(--border); color:var(--text); display:flex; align-items:center; gap:5px;} #instructionsBtn:hover{background:#262b36;} .typing{display:flex; gap:4px; align-self:flex-start; padding:10px 15px;} .typing span{width:6px;height:6px;background:var(--muted);border-radius:50%;animation:blink 1.3s infinite;} .typing span:nth-child(2){animation-delay:.2s;} .typing span:nth-child(3){animation-delay:.4s;} @keyframes blink{0%,80%,100%{opacity:.2;} 40%{opacity:1;}} ::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-thumb{background:var(--border); border-radius:4px;} </style> </head> <body> <header> <h1><span class="dot" id="statusDot"></span> סוכן קבצים מקומי - Gemini</h1> <div class="top-controls"> <input type="password" id="apiKey" placeholder="Gemini API Key" style="width:220px;"> <select id="modelName" aria-label="בחירת מודל" style="width:210px;"> <option value="gemini-3.5-flash-lite">טוען מודלים...</option> </select> <button class="secondary" id="loadModelsBtn" title="טען את המודלים הזמינים למפתח שהוזן">טען מודלים</button> <span id="modelLoadStatus" class="folder-status" aria-live="polite"></span> <button class="secondary" id="saveKeyBtn">שמור מפתח</button> <button id="pickFolderBtn">בחר תיקייה</button> <button id="instructionsBtn">❓ הוראות שימוש</button> </div> </header> <div class="hint"> <span class="folder-status" id="folderStatus">לא נבחרה תיקייה עדיין.</span> | <label class="settings-row" style="display:inline-flex;"> <input type="checkbox" id="confirmDelete" checked style="margin-left:5px;"> אישור ידני לפני מחיקה </label> | <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" style="color:var(--accent); text-decoration:none;">קבל / נהל מפתח API ב-Google AI Studio ↗</a> </div> <main> <aside id="historySidebar"> <div class="hist-header"> <span>היסטוריית שיחות</span> <button class="secondary small" id="newChatBtn">+ שיחה חדשה</button> </div> <div class="hist-file-controls"> <div class="hist-status" id="historyFileStatus">לא מחובר קובץ היסטוריה - השיחה תישמר רק בזיכרון הדף הנוכחי.</div> <button class="secondary small" id="reconnectHistoryBtn" style="display:none;">🔄 חבר להיסטוריה השמורה</button> <button class="secondary small" id="chooseHistoryBtn">📁 בחר / צור קובץ היסטוריה</button> </div> <div class="hist-list" id="historyList"> <div class="hist-empty">אין שיחות שמורות עדיין.</div> </div> </aside> <div id="chat"> <div class="msg system">בחר תיקייה לעבודה, הזן מפתח API של Gemini, ותוכל לתת הוראות בעברית או באנגלית (למשל: "צור קובץ notes.txt עם התוכן שלום", "שנה שם של old.txt ל-new.txt", "העתק את קובץ א' לתיקיית ב'").</div> </div> </main> <footer> <textarea id="userInput" placeholder="כתוב הוראה לסוכן..." rows="1"></textarea> <button id="sendBtn">שלח</button> </footer> <div class="credit">יוצר: <a href="https://mitmachim.top/user/%D7%9E%D7%99%D7%A0%D7%99-%D7%94%D7%90%D7%A7%D7%A8" target="_blank" rel="noopener noreferrer">מיני האקר</a></div> <div class="modal-overlay" id="instructionsModal" style="display:none;"> <div class="modal-box"> <div class="modal-header"> <h2>📖 הוראות שימוש</h2> <button id="closeInstructionsBtn">✕</button> </div> <div class="modal-body"> <h3>מה זה הכלי הזה?</h3> <p>עמוד HTML עצמאי שרץ כולו בדפדפן שלך (בלי שרת), ומחבר בין מודל שפה של Gemini לבין תיקייה אמיתית במחשב שלך - כך שאפשר לתת לו הוראות בשפה חופשית והוא מבצע אותן בפועל על הקבצים.</p> <h3>דרישות חשובות</h3> <ul> <li>עובד רק ב-<b>Google Chrome</b> או <b>Microsoft Edge</b>, בגרסה עדכנית, <b>על מחשב</b>.</li> <li>לא עובד בטלפון/טאבלט (בכל דפדפן), ולא ב-Firefox או Safari - אלה לא תומכים בגישה לקבצים מקומיים.</li> <li>נדרש חיבור אינטרנט (לתקשורת עם שרתי Gemini).</li> </ul> <h3>שלב 1: מפתח API</h3> <p>הזן מפתח Gemini API בשדה "Gemini API Key". אם אין לך, לחץ על הקישור "קבל / נהל מפתח API ב-Google AI Studio" מתחת לשדות. לחיצה על "שמור מפתח" שומרת אותו בדפדפן הזה לפעם הבאה (הוא לא נשלח לשום מקום חוץ מ-Google, ולא נשמר על ידי היוצר של הכלי).</p> <h3>שלב 2: שם מודל</h3> <p>שדה "שם מודל" קובע איזה מודל Gemini משמש. ברירת המחדל היא <code>gemini-3.5-flash-lite</code>. אם תקבל שגיאת 404 שאומרת שהמודל לא קיים, Google כנראה שינתה שמות - פשוט הקלד שם מודל עדכני יותר (אפשר לבדוק ב-Google AI Studio).</p> <h3>שלב 3: בחירת תיקיית עבודה</h3> <p>לחץ "בחר תיקייה" ובחר את התיקייה שבה הסוכן יעבוד. הוא <b>לא</b> יכול לצאת מחוץ לתיקייה הזו - זו הגנת אבטחה של הדפדפן עצמו.</p> <h3>מה הסוכן יודע לעשות</h3> <ul> <li><b>ליצור / לערוך קבצים</b> - כולל קבצי קוד (JS, Python, HTML, CSS, JSON וכו') וגם קבצים בינאריים כמו תמונות (הוא כותב מחדש את הקובץ השלם בכל עריכה).</li> <li><b>לקרוא קבצים קיימים.</b></li> <li><b>לשנות שם</b> לקובץ או תיקייה.</li> <li><b>להעביר / להעתיק</b> קבצים ותיקיות שלמות (כולל רקורסיבית).</li> <li><b>למחוק</b> קבצים/תיקיות - עם בקשת אישור ידנית לפני כל מחיקה (אפשר לכבות בתיבת הסימון "אישור ידני לפני מחיקה").</li> <li><b>ליצור תיקיות חדשות.</b></li> <li><b>לרשום את תוכן התיקייה</b> (כולל רקורסיבית לכל תת-התיקיות).</li> <li><b>לחפש טקסט</b> (כמו grep) בכל הקבצים בתיקייה, כדי למצוא איפה מוגדר משהו מבלי לנחש שם קובץ.</li> </ul> <div class="ex">"צור קובץ app.py שמדפיס שלום עולם"<br>"תערוך את style.css ותשנה את צבע הרקע לכחול"<br>"תמצא באיזה קובץ מוגדרת הפונקציה login"<br>"תעביר את כל קבצי ה-png לתיקייה בשם images"</div> <h3>מה הסוכן <u>לא</u> יודע לעשות</h3> <ul> <li>ליצור תמונות/וידאו מאפס (זה דורש מודל נפרד כמו Imagen/Veo, לא כלול כאן).</li> <li>לערוך "תוכן" של קובץ תמונה בינארי (PNG/JPG) לפי בקשה חופשית - זה לא קובץ טקסט. הוא כן יכול ליצור/לערוך תמונות וקטוריות בפורמט SVG, כי הן בעצם קוד טקסטואלי.</li> <li>לצאת מגבולות התיקייה שנבחרה.</li> </ul> <h3>היסטוריית שיחות</h3> <p>בחלונית הצד: לחץ "בחר / צור קובץ היסטוריה" ובחר איפה לשמור קובץ JSON שישמור את כל השיחות. מרגע זה כל שיחה נשמרת אוטומטית. בפעם הבאה שתפתח את הכלי, הכלי "יזכור" את הקובץ ויציג כפתור "חבר להיסטוריה השמורה" (לחיצה אחת נדרשת - הגבלת אבטחה של הדפדפן). לחיצה על שיחה ברשימה טוענת אותה במלואה וממשיכה אותה בדיוק מאיפה שנעצרה. "+ שיחה חדשה" פותח שיחה נפרדת בלי למחוק את הישנות.</p> <h3>אבטחה ופרטיות</h3> <ul> <li>הכלי כולו רץ מקומית בדפדפן - אין שרת ביניים של היוצר, אין איסוף מידע.</li> <li>המפתח שלך והיסטוריית השיחות נשלחים <b>רק</b> לשרתי Google (Gemini API) לצורך קבלת תשובות, ולשום גורם אחר.</li> <li>גישה לתיקיות/קבצים תמיד דורשת אישור מפורש שלך דרך חלונית הדפדפן.</li> </ul> </div> </div> </div> <script> (function(){ const chatEl = document.getElementById('chat'); const apiKeyEl = document.getElementById('apiKey'); const modelNameEl = document.getElementById('modelName'); const loadModelsBtn = document.getElementById('loadModelsBtn'); const modelLoadStatusEl = document.getElementById('modelLoadStatus'); const folderStatusEl = document.getElementById('folderStatus'); const statusDot = document.getElementById('statusDot'); const userInputEl = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const pickFolderBtn = document.getElementById('pickFolderBtn'); const saveKeyBtn = document.getElementById('saveKeyBtn'); const confirmDeleteEl = document.getElementById('confirmDelete'); const newChatBtn = document.getElementById('newChatBtn'); const historyFileStatusEl = document.getElementById('historyFileStatus'); const reconnectHistoryBtn = document.getElementById('reconnectHistoryBtn'); const chooseHistoryBtn = document.getElementById('chooseHistoryBtn'); const historyListEl = document.getElementById('historyList'); const instructionsBtn = document.getElementById('instructionsBtn'); const instructionsModal = document.getElementById('instructionsModal'); const closeInstructionsBtn = document.getElementById('closeInstructionsBtn'); function openInstructions(){ instructionsModal.style.display = 'flex'; } function closeInstructions(){ instructionsModal.style.display = 'none'; } instructionsBtn.addEventListener('click', openInstructions); closeInstructionsBtn.addEventListener('click', closeInstructions); instructionsModal.addEventListener('click', (e) => { if(e.target === instructionsModal) closeInstructions(); }); document.addEventListener('keydown', (e) => { if(e.key === 'Escape') closeInstructions(); }); // ---------- global diagnostics (free feature: surfaces hidden JS errors instead of failing silently) ---------- window.addEventListener('error', (e) => { addMsg('system', '⚠ שגיאת JavaScript: ' + e.message); }); window.addEventListener('unhandledrejection', (e) => { addMsg('system', '⚠ שגיאה לא מטופלת: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)); }); let rootHandle = null; let history = []; // Gemini "contents" array for the CURRENT session let busy = false; // ---------- persistent chat history (saved to a JSON file at a location you choose) ---------- let historyFileHandle = null; let sessions = []; // [{ id, title, createdAt, updatedAt, messages }] let currentSessionId = null; function idbOpen(){ return new Promise((resolve, reject) => { const req = indexedDB.open('gfa_store', 1); req.onupgradeneeded = () => { req.result.createObjectStore('kv'); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function idbGet(key){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').get(key); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); } async function idbSet(key, val){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function formatTime(iso){ try{ const d = new Date(iso); return d.toLocaleDateString('he-IL') + ' ' + d.toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'}); }catch(e){ return ''; } } function renderSidebar(){ if(!sessions.length){ historyListEl.innerHTML = '<div class="hist-empty">אין שיחות שמורות עדיין.</div>'; return; } const sorted = [...sessions].sort((a,b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')); historyListEl.innerHTML = ''; for(const s of sorted){ const div = document.createElement('div'); div.className = 'hist-item' + (s.id === currentSessionId ? ' active' : ''); const title = s.title || 'שיחה חדשה'; div.innerHTML = `<span class="hi-title">${escapeHtml(title)}</span><span class="hi-time">${escapeHtml(formatTime(s.updatedAt || s.createdAt))}</span>`; div.addEventListener('click', () => loadSession(s.id)); historyListEl.appendChild(div); } } function replayHistoryToUI(hist){ for(const item of hist){ const parts = item.parts || []; const textParts = parts.filter(p => p.text); const fcParts = parts.filter(p => p.functionCall); if(item.role === 'user' && textParts.length){ addMsg('user', textParts.map(p => p.text).join('\n')); } else if(item.role === 'model'){ if(textParts.length) addMsg('model', textParts.map(p => p.text).join('\n')); for(const fc of fcParts){ addToolLog(fc.functionCall.name, fc.functionCall.args || {}, '(מההיסטוריה)', false); } } // functionResponse parts (role 'user') are intentionally not replayed - their matching tool-log line already shown above } } function loadSession(id){ const s = sessions.find(x => x.id === id); if(!s) return; currentSessionId = id; history = s.messages; // same reference - future pushes stay in sync with the stored session chatEl.innerHTML = ''; replayHistoryToUI(history); renderSidebar(); } function startNewSession(){ currentSessionId = null; history = []; chatEl.innerHTML = ''; addMsg('system', 'שיחה חדשה. כתוב הוראה כדי להתחיל.'); renderSidebar(); } newChatBtn.addEventListener('click', startNewSession); async function saveSessionsToDisk(){ if(!historyFileHandle) return; try{ const writable = await historyFileHandle.createWritable(); await writable.write(JSON.stringify({ version: 1, sessions }, null, 2)); await writable.close(); }catch(e){ addMsg('system', 'שמירת היסטוריה נכשלה: ' + e.message); } } async function loadSessionsFromDisk(){ try{ const file = await historyFileHandle.getFile(); const text = await file.text(); if(text.trim()){ const data = JSON.parse(text); sessions = Array.isArray(data.sessions) ? data.sessions : []; } else { sessions = []; } renderSidebar(); addMsg('system', 'קובץ היסטוריה מחובר (' + sessions.length + ' שיחות שמורות נטענו).'); }catch(e){ addMsg('system', 'טעינת היסטוריה נכשלה: ' + e.message); sessions = []; } } function setHistoryStatus(text, connected){ historyFileStatusEl.textContent = text; historyFileStatusEl.className = 'hist-status' + (connected ? ' connected' : ''); } async function connectNewHistoryFile(){ if(!('showSaveFilePicker' in window)){ addMsg('system', 'הדפדפן הזה לא תומך בשמירת קובץ היסטוריה מקומי (showSaveFilePicker). נסה Chrome/Edge עדכניים.'); return; } try{ const handle = await window.showSaveFilePicker({ suggestedName: 'gemini-agent-chat-history.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] }); historyFileHandle = handle; await idbSet('history_handle', handle); await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; await saveSessionsToDisk(); // ensure a valid file exists immediately }catch(e){ if(e.name !== 'AbortError') addMsg('system', 'חיבור קובץ היסטוריה נכשל: ' + e.message); } } chooseHistoryBtn.addEventListener('click', connectNewHistoryFile); async function reconnectSavedHistoryFile(){ try{ const handle = await idbGet('history_handle'); if(!handle){ setHistoryStatus('לא נמצא קובץ היסטוריה שמור.', false); return; } const perm = await handle.requestPermission({ mode: 'readwrite' }); if(perm !== 'granted'){ setHistoryStatus('הגישה לקובץ ההיסטוריה נדחתה.', false); return; } historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; }catch(e){ addMsg('system', 'חיבור מחדש נכשל: ' + e.message); } } reconnectHistoryBtn.addEventListener('click', reconnectSavedHistoryFile); // on load: silently check (no permission prompt yet) whether a previously-used history file is remembered (async () => { try{ const handle = await idbGet('history_handle'); if(handle){ const state = await handle.queryPermission({ mode: 'readwrite' }); if(state === 'granted'){ historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); } else { setHistoryStatus('נמצא קובץ היסטוריה קודם (' + handle.name + ') - לחץ לחיבור.', false); reconnectHistoryBtn.style.display = ''; } } }catch(e){ /* IndexedDB unavailable or no saved handle - ignore, app still works without persistence */ } })(); // ---------- persistence for convenience (local only) ---------- try{ const savedKey = localStorage.getItem('gfa_api_key'); if(savedKey) apiKeyEl.value = savedKey; const savedModel = localStorage.getItem('gfa_model'); if(savedModel) modelNameEl.value = savedModel; }catch(e){} saveKeyBtn.addEventListener('click', () => { try{ localStorage.setItem('gfa_api_key', apiKeyEl.value.trim()); localStorage.setItem('gfa_model', modelNameEl.value.trim()); toast('המפתח והמודל נשמרו בדפדפן זה.'); loadAvailableModels(false); }catch(e){ toast('שמירה נכשלה: ' + e.message, true); } }); let modelsLoading = false; let modelsLoadTimer = null; async function loadAvailableModels(showMessage = true){ const apiKey = apiKeyEl.value.trim(); if(!apiKey){ modelLoadStatusEl.textContent = 'הזן מפתח API תחילה'; if(showMessage) toast('כדי לטעון מודלים יש להזין מפתח API של Gemini.', true); return; } if(modelsLoading) return; modelsLoading = true; loadModelsBtn.disabled = true; modelLoadStatusEl.textContent = 'טוען מודלים...'; try{ const models = []; let pageToken = ''; do{ const params = new URLSearchParams({ key: apiKey, pageSize: '100' }); if(pageToken) params.set('pageToken', pageToken); const res = await fetch('https://generativelanguage.googleapis.com/v1beta/models?' + params.toString()); if(!res.ok){ const errorText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errorText.slice(0, 250)); } const data = await res.json(); for(const model of (data.models || [])){ const methods = model.supportedGenerationMethods || []; if(methods.includes('generateContent') && model.name){ const name = model.name.replace(/^models\//, ''); models.push({ name, label: model.displayName || name }); } } pageToken = data.nextPageToken || ''; }while(pageToken); const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()] .sort((a, b) => a.name.localeCompare(b.name)); const previousModel = modelNameEl.value.trim(); modelNameEl.innerHTML = ''; for(const model of uniqueModels){ const option = document.createElement('option'); option.value = model.name; option.textContent = model.label === model.name ? model.name : model.label + ' (' + model.name + ')'; modelNameEl.appendChild(option); } // Preserve the current model when it exists; otherwise choose the first available one. if(uniqueModels.length){ modelNameEl.value = uniqueModels.some(model => model.name === previousModel) ? previousModel : uniqueModels[0].name; } else { const option = document.createElement('option'); option.value = ''; option.textContent = 'לא נמצאו מודלים זמינים'; modelNameEl.appendChild(option); } modelLoadStatusEl.textContent = uniqueModels.length ? ('✅ ' + uniqueModels.length + ' מודלים זמינים לשיחה') : 'לא נמצאו מודלים'; if(showMessage) toast('נטענו ' + uniqueModels.length + ' מודלים זמינים.'); }catch(e){ modelLoadStatusEl.textContent = 'טעינה נכשלה: ' + e.message; if(showMessage) toast('טעינת המודלים נכשלה: ' + e.message, true); }finally{ modelsLoading = false; loadModelsBtn.disabled = false; } } loadModelsBtn.addEventListener('click', () => loadAvailableModels(true)); // Load automatically shortly after the user finishes entering an API key. apiKeyEl.addEventListener('input', () => { clearTimeout(modelsLoadTimer); if(apiKeyEl.value.trim().length < 20){ modelLoadStatusEl.textContent = ''; return; } modelLoadStatusEl.textContent = 'המפתח זוהה, טוען...'; modelsLoadTimer = setTimeout(() => loadAvailableModels(false), 700); }); apiKeyEl.addEventListener('change', () => loadAvailableModels(false)); function toast(text, isErr){ addMsg('system', text); } // Load the model list automatically when a saved API key is available. if(apiKeyEl.value.trim()) loadAvailableModels(false); // ---------- UI helpers ---------- function addMsg(role, text){ const div = document.createElement('div'); div.className = 'msg ' + role; div.textContent = text; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; return div; } function addToolLog(opName, argsObj, resultText, isErr){ const div = document.createElement('div'); div.className = 'tool-log'; const argsStr = JSON.stringify(argsObj); div.innerHTML = `<span class="op">⚙ ${escapeHtml(opName)}</span>(${escapeHtml(argsStr)}) → ` + `<span class="${isErr?'err':'ok'}">${escapeHtml(resultText)}</span>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } let typingEl = null; function showTyping(){ typingEl = document.createElement('div'); typingEl.className = 'typing'; typingEl.innerHTML = '<span></span><span></span><span></span>'; chatEl.appendChild(typingEl); chatEl.scrollTop = chatEl.scrollHeight; } function hideTyping(){ if(typingEl){ typingEl.remove(); typingEl = null; } } // ---------- folder selection ---------- function checkSupport(){ if(!('showDirectoryPicker' in window)){ statusDot.style.background = 'var(--danger)'; const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); let msg = '❌ הדפדפן/המכשיר הזה לא תומך בבחירת תיקייה מקומית (File System Access API).'; if(isMobile){ msg += ' תכונה זו לא נתמכת כלל בדפדפנים בטלפון נייד (לא Android ולא iOS, בכל דפדפן) - יש לפתוח את הקובץ במחשב שולחני.'; } else { msg += ' יש לפתוח את הקובץ במחשב, בדפדפן Google Chrome או Microsoft Edge בגרסה עדכנית (לא Firefox ולא Safari - הם לא תומכים בכך).'; } folderStatusEl.textContent = msg; pickFolderBtn.disabled = true; addMsg('system', msg); return false; } return true; } const isSupported = checkSupport(); pickFolderBtn.addEventListener('click', async () => { if(!('showDirectoryPicker' in window)){ checkSupport(); return; } try{ rootHandle = await window.showDirectoryPicker(); folderStatusEl.innerHTML = 'תיקייה נבחרה: <b>' + escapeHtml(rootHandle.name) + '</b>'; statusDot.style.background = '#3ecf8e'; addMsg('system', 'נבחרה תיקיית עבודה: ' + rootHandle.name + '. הסוכן יכול כעת לפעול בתוכה בלבד.'); }catch(e){ if(e.name !== 'AbortError') toast('בחירת תיקייה נכשלה: ' + e.message, true); } }); // ---------- path resolution helpers (relative to rootHandle) ---------- function splitPath(p){ return String(p || '').split('/').map(s => s.trim()).filter(s => s.length && s !== '.'); } async function getDirHandle(path, create){ const parts = splitPath(path); let dir = rootHandle; for(const part of parts){ dir = await dir.getDirectoryHandle(part, { create: !!create }); } return dir; } async function getFileHandleAt(path, create){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב קובץ'); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), create); return await dir.getFileHandle(fileName, { create: !!create }); } async function readFileText(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); return await file.text(); } async function writeFileText(path, content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(!overwrite){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; // not found -> fine, continue } } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(content ?? ''); await writable.close(); return true; } async function deleteEntry(path, recursive){ const parts = splitPath(path); if(parts.length === 0) throw new Error('לא ניתן למחוק את תיקיית השורש'); const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); await dir.removeEntry(name, { recursive: !!recursive }); return true; } async function createDirectory(path){ await getDirHandle(path, true); return true; } async function listDirectory(path, recursive){ const dir = await getDirHandle(path || '', false); const results = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; results.push({ name: full, type: handle.kind }); if(recursive && handle.kind === 'directory'){ await walk(handle, full); } } } await walk(dir, path || ''); return results; } // free bonus feature: text search (grep-like) across files in a folder, recursively async function searchInFiles(path, query, recursive, caseSensitive){ const dir = await getDirHandle(path || '', false); const q = caseSensitive ? query : String(query).toLowerCase(); const matches = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; if(handle.kind === 'file'){ try{ const file = await handle.getFile(); const text = await file.text(); const lines = text.split('\n'); lines.forEach((line, idx) => { const hay = caseSensitive ? line : line.toLowerCase(); if(hay.indexOf(q) !== -1){ matches.push({ file: full, line_number: idx + 1, line_text: line.trim().slice(0, 200) }); } }); }catch(e){ /* skip unreadable/binary files silently */ } } else if(recursive !== false){ await walk(handle, full); } } } await walk(dir, path || ''); return matches.slice(0, 300); } async function copyFile(sourcePath, destPath, overwrite){ // binary-safe copy: works correctly for code/text files AND any binary file (images, pdf, zip, exe...) const fh = await getFileHandleAt(sourcePath, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const parts = splitPath(destPath); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const destFh = await dir.getFileHandle(fileName, { create: true }); const writable = await destFh.createWritable(); await writable.write(buf); await writable.close(); return true; } // reads any file's raw bytes and returns them as a base64 string (for binary files, images, etc.) async function readFileBase64(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ''; const chunkSize = 0x8000; for(let i = 0; i < bytes.length; i += chunkSize){ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)); } return btoa(binary); } // writes a base64 string as raw bytes to a file (for creating/editing binary files) async function writeFileBase64(path, base64Content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const binaryStr = atob(base64Content); const bytes = new Uint8Array(binaryStr.length); for(let i = 0; i < binaryStr.length; i++){ bytes[i] = binaryStr.charCodeAt(i); } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(bytes); await writable.close(); return true; } // determines whether a path is a 'file' or 'directory' (throws if it doesn't exist) async function getEntryKind(path){ const parts = splitPath(path); if(parts.length === 0) return 'directory'; // the root itself const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); try{ await dir.getFileHandle(name, { create: false }); return 'file'; }catch(e){ if(e.name === 'TypeMismatchError'){ return 'directory'; } if(e.name === 'NotFoundError'){ throw new Error('הנתיב לא נמצא: ' + path); } throw e; } } // recursively copies an entire directory tree to a new destination path async function copyDirectoryRecursive(sourcePath, destPath, overwrite){ await createDirectory(destPath); const srcDir = await getDirHandle(sourcePath, false); for await (const [name, handle] of srcDir.entries()){ const srcChild = sourcePath ? sourcePath + '/' + name : name; const destChild = destPath ? destPath + '/' + name : name; if(handle.kind === 'file'){ await copyFile(srcChild, destChild, overwrite !== false); } else { await copyDirectoryRecursive(srcChild, destChild, overwrite); } } } // copies either a file or a whole directory tree, dispatching by actual entry kind async function copyEntryGeneric(sourcePath, destPath, overwrite){ const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, overwrite); } else { await copyDirectoryRecursive(sourcePath, destPath, overwrite); } return kind; } async function moveEntry(sourcePath, destPath){ // supports both files and whole directories: copy (recursively if needed), then delete the original const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, true); await deleteEntry(sourcePath, false); } else { await copyDirectoryRecursive(sourcePath, destPath, true); await deleteEntry(sourcePath, true); } return true; } async function renameEntry(path, newName){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב'); parts.pop(); const destPath = (parts.length ? parts.join('/') + '/' : '') + newName; await moveEntry(path, destPath); return destPath; } // ---------- tool declarations for Gemini ---------- const tools = [{ functionDeclarations: [ { name: 'list_directory', description: 'רשימת קבצים ותיקיות בתוך נתיב יחסי לתיקיית השורש שנבחרה. נתיב ריק = שורש.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי, למשל "" או "docs/sub"' }, recursive: { type: 'BOOLEAN', description: 'האם לרשום גם תת-תיקיות באופן רקורסיבי' } } } }, { name: 'read_file', description: 'קריאת תוכן קובץ קיים - כל סוג קובץ. עבור קבצי קוד/טקסט (js, py, html, css, json, txt, md ועוד) השתמש ב-encoding="utf8" (ברירת מחדל). עבור קבצים בינאריים (תמונות, pdf, zip, exe ועוד) השתמש ב-encoding="base64".', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל, לקוד/טקסט) או "base64" (לקבצים בינאריים)' } }, required: ['path'] } }, { name: 'write_file', description: 'יצירת קובץ חדש או עריכה/דריסה מלאה של קובץ קיים - כל סוג קובץ, כולל קבצי קוד (js, py, html, css, json ועוד). עבור תוכן טקסטואלי/קוד השתמש ב-encoding="utf8" (ברירת מחדל) עם content כטקסט רגיל. עבור קובץ בינארי (תמונה ועוד) השתמש ב-encoding="base64" עם content כמחרוזת base64. יוצר תיקיות ביניים אם צריך.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ היעד' }, content: { type: 'STRING', description: 'התוכן לכתיבה - טקסט רגיל (utf8) או מחרוזת base64 (base64)' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל) או "base64"' }, overwrite: { type: 'BOOLEAN', description: 'האם לדרוס קובץ קיים (ברירת מחדל true)' } }, required: ['path', 'content'] } }, { name: 'delete_entry', description: 'מחיקת קובץ או תיקייה (ריקה, אלא אם recursive=true).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' }, recursive: { type: 'BOOLEAN', description: 'מחיקה רקורסיבית של תיקייה לא ריקה' } }, required: ['path'] } }, { name: 'create_directory', description: 'יצירת תיקייה חדשה (כולל תיקיות ביניים).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' } }, required: ['path'] } }, { name: 'copy_file', description: 'העתקת קובץ בודד או תיקייה שלמה (רקורסיבית) ליעד חדש (בתוך אותה תיקייה או לתיקייה אחרת).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' }, overwrite: { type: 'BOOLEAN' } }, required: ['source_path', 'dest_path'] } }, { name: 'move_entry', description: 'העברת קובץ או תיקייה שלמה למיקום/תיקייה אחרת (מוחק את המקור לאחר ההעתקה, רקורסיבית עבור תיקיות).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' } }, required: ['source_path', 'dest_path'] } }, { name: 'rename_entry', description: 'שינוי שם של קובץ או תיקייה (נשאר באותה תיקיית-אב).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב הקובץ הנוכחי' }, new_name: { type: 'STRING', description: 'השם החדש (ללא נתיב, רק שם קובץ)' } }, required: ['path', 'new_name'] } }, { name: 'search_in_files', description: 'חיפוש טקסט חופשי (כמו grep) בתוך כל קבצי הטקסט/קוד בתיקייה נתונה, כולל תת-תיקיות. מחזיר רשימת התאמות עם שם קובץ, מספר שורה ותוכן השורה. שימושי למציאת קובץ לפי תוכן, או מציאת כל המקומות שמזכירים משתנה/פונקציה מסוימת.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לתיקיית ההתחלה של החיפוש, "" לשורש' }, query: { type: 'STRING', description: 'מחרוזת הטקסט לחיפוש' }, recursive: { type: 'BOOLEAN', description: 'האם לחפש גם בתת-תיקיות (ברירת מחדל true)' }, case_sensitive: { type: 'BOOLEAN', description: 'האם החיפוש רגיש לאותיות גדולות/קטנות (ברירת מחדל false)' } }, required: ['query'] } } ] }]; async function executeTool(name, args){ switch(name){ case 'list_directory': { const entries = await listDirectory(args.path || '', !!args.recursive); return { entries }; } case 'read_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; const content = encoding === 'base64' ? await readFileBase64(args.path) : await readFileText(args.path); return { content, encoding }; } case 'write_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; if(encoding === 'base64'){ await writeFileBase64(args.path, args.content, args.overwrite !== false); } else { await writeFileText(args.path, args.content, args.overwrite !== false); } return { success: true, path: args.path, encoding }; } case 'delete_entry': { if(confirmDeleteEl.checked){ const ok = window.confirm('הסוכן מבקש למחוק: ' + args.path + (args.recursive ? ' (כולל תוכן, רקורסיבי)' : '') + '\nלאשר?'); if(!ok) throw new Error('המשתמש ביטל את פעולת המחיקה'); } await deleteEntry(args.path, !!args.recursive); return { success: true, path: args.path }; } case 'create_directory': { await createDirectory(args.path); return { success: true, path: args.path }; } case 'copy_file': { const kind = await copyEntryGeneric(args.source_path, args.dest_path, args.overwrite !== false); return { success: true, from: args.source_path, to: args.dest_path, type: kind }; } case 'move_entry': { await moveEntry(args.source_path, args.dest_path); return { success: true, from: args.source_path, to: args.dest_path }; } case 'rename_entry': { const newPath = await renameEntry(args.path, args.new_name); return { success: true, new_path: newPath }; } case 'search_in_files': { const matches = await searchInFiles(args.path || '', args.query, args.recursive !== false, !!args.case_sensitive); return { matches, count: matches.length }; } default: throw new Error('פונקציה לא מוכרת: ' + name); } } // ---------- Gemini API call loop ---------- const SYSTEM_INSTRUCTION = `אתה סוכן AI שמסייע למשתמש לנהל ולערוך קבצים בתיקייה מקומית שהוא בחר בעצמו, דרך פונקציות (tools) שסופקו לך. אתה תומך בכל סוג קובץ: קבצי קוד ותוכנה (js, ts, py, java, c, cpp, html, css, json, yaml וכו'), קבצי טקסט/מסמכים, וגם קבצים בינאריים (תמונות, pdf, zip וכו') - עבור קבצי טקסט/קוד השתמש בפרמטר encoding="utf8" (ברירת מחדל), ועבור קבצים בינאריים ב-encoding="base64". כשמבקשים ממך לערוך קובץ קוד קיים: תמיד קרא אותו קודם עם read_file כדי לראות את התוכן הנוכחי, בצע את השינוי הנדרש בתוכן, וכתוב את הגרסה המלאה המעודכנת בחזרה עם write_file (overwrite=true) - אין עריכה חלקית/inline, רק כתיבה מחדש של הקובץ השלם. כל פעולה שהמשתמש מבקש (יצירת קובץ, עריכת קוד, שינוי שם, העברה, העתקה, מחיקה, רשימת תוכן) - בצע אותה ישירות דרך קריאה לפונקציה המתאימה, בלי לבקש אישור מיותר (למעט מחיקה, שיש לה אישור נפרד במערכת). נתיבים הם תמיד יחסיים לתיקיית השורש שנבחרה - אל תשתמש בנתיבים מוחלטים כמו C:\\ או /. כשצריך למצוא קובץ או מקום מסוים לפי תוכן (למשל "איפה מוגדרת הפונקציה X", "באיזה קובץ כתוב Y") - השתמש בפונקציה search_in_files במקום לנחש נתיבים. אחרי ביצוע הפעולות, סכם בקצרה ובעברית מה נעשה. אם משהו נכשל, הסבר בפשטות מה קרה ומה אפשר לנסות.`; async function callGemini(){ const apiKey = apiKeyEl.value.trim(); const model = (modelNameEl.value.trim() || 'gemini-3.5-flash-lite'); if(!apiKey) throw new Error('נא להזין API Key של Gemini'); const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`; const body = { contents: history, tools: tools, systemInstruction: { parts: [{ text: SYSTEM_INSTRUCTION }] } }; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(!res.ok){ const errText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errText.slice(0, 400)); } return await res.json(); } async function agentTurn(userText){ history.push({ role: 'user', parts: [{ text: userText }] }); let safety = 0; while(safety++ < 12){ showTyping(); let data; try{ data = await callGemini(); }catch(e){ hideTyping(); addMsg('system', 'שגיאה: ' + e.message); return; } hideTyping(); const candidate = data.candidates && data.candidates[0]; if(!candidate || !candidate.content){ addMsg('system', 'לא התקבלה תשובה מהמודל.'); return; } const parts = candidate.content.parts || []; const functionCalls = parts.filter(p => p.functionCall); const textParts = parts.filter(p => p.text); if(textParts.length){ addMsg('model', textParts.map(p => p.text).join('\n')); } // always record the model's turn in history - even a plain text answer with no tool calls, // so the conversation (and what gets saved to the history file) is complete and Gemini keeps memory of its own replies history.push({ role: 'model', parts: parts }); if(functionCalls.length === 0){ return; // done - plain text answer, nothing more to execute } const responseParts = []; for(const fc of functionCalls){ const { name, args } = fc.functionCall; let resultObj, isErr = false; try{ resultObj = await executeTool(name, args || {}); addToolLog(name, args || {}, 'הצלחה', false); }catch(e){ isErr = true; resultObj = { error: e.message }; addToolLog(name, args || {}, 'שגיאה: ' + e.message, true); } responseParts.push({ functionResponse: { name: name, response: resultObj } }); } history.push({ role: 'user', parts: responseParts }); // loop continues to let the model react to tool results } addMsg('system', 'הגעה למספר צעדים מקסימלי לשיחה אחת - נסה לפרק את הבקשה.'); } async function handleSend(){ if(busy) return; const text = userInputEl.value.trim(); if(!text) return; if(!rootHandle){ addMsg('system', 'קודם יש לבחור תיקיית עבודה (כפתור "בחר תיקייה" למעלה).'); return; } // create/attach a session on the first message of a new conversation if(currentSessionId === null){ const s = { id: 'S' + Date.now() + Math.random().toString(36).slice(2, 7), title: text.slice(0, 45), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), messages: history // same reference history already points to (empty array at this point) }; sessions.unshift(s); currentSessionId = s.id; } addMsg('user', text); userInputEl.value = ''; userInputEl.style.height = 'auto'; busy = true; sendBtn.disabled = true; try{ await agentTurn(text); } finally { busy = false; sendBtn.disabled = false; const s = sessions.find(x => x.id === currentSessionId); if(s){ s.updatedAt = new Date().toISOString(); } renderSidebar(); await saveSessionsToDisk(); } } sendBtn.addEventListener('click', handleSend); userInputEl.addEventListener('keydown', (e) => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); handleSend(); } }); userInputEl.addEventListener('input', () => { userInputEl.style.height = 'auto'; userInputEl.style.height = Math.min(userInputEl.scrollHeight, 140) + 'px'; }); })(); </script> </body> </html> <!-- Injection By NetFree --> <script src="//netfree.link/injection-script/go-payment.js" type="text/javascript" async ></script> <script src="//netfree.link/injection-script/popup-card-init.js" type="text/javascript" async ></script> -
@הידען
צודק תוקן<!DOCTYPE html> <!-- Injection By NetFree --> <script src="//wfapi.internal.netfree.link/wifree/go-install-wifree" type="text/javascript" async ></script> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>סוכן קבצים - Gemini</title> <style> :root{ --bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --border:#2a2f3a; --text:#e8eaed; --muted:#9aa1ac; --accent:#6c8cff; --accent2:#4f6fe0; --danger:#e5484d; --ok:#3ecf8e; --user-bubble:#2a3550; --model-bubble:#1e222b; } *{box-sizing:border-box;} body{ margin:0; font-family:'Segoe UI', Arial, sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; } header{ padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; } header h1{ font-size:18px; margin:0; display:flex; align-items:center; gap:8px;} header h1 span.dot{width:9px;height:9px;border-radius:50%;background:var(--ok);display:inline-block;} .top-controls{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; } input[type=password], input[type=text], select{ background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:8px 10px; border-radius:8px; font-size:13px; outline:none; } input[type=password]:focus, input[type=text]:focus, select:focus{border-color:var(--accent);} select{cursor:pointer;} button{ background:var(--accent2); color:#fff; border:none; padding:8px 14px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; transition:background .15s; } button:hover{background:var(--accent);} button.secondary{background:var(--panel2); border:1px solid var(--border); color:var(--text);} button.secondary:hover{background:#262b36;} button:disabled{opacity:.5; cursor:not-allowed;} .folder-status{ font-size:12.5px; color:var(--muted); } .folder-status b{color:var(--ok);} main{flex:1; display:flex; overflow:hidden;} #chat{ flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:14px; } .msg{max-width:78%; padding:12px 15px; border-radius:14px; font-size:14.5px; line-height:1.55; white-space:pre-wrap; word-wrap:break-word;} .msg.user{align-self:flex-end; background:var(--user-bubble); border-bottom-left-radius:4px;} .msg.model{align-self:flex-start; background:var(--model-bubble); border:1px solid var(--border); border-bottom-right-radius:4px;} .msg.system{align-self:center; background:transparent; color:var(--muted); font-size:12.5px; text-align:center; max-width:90%;} .tool-log{ align-self:flex-start; max-width:85%; background:#141821; border:1px dashed var(--border); border-radius:10px; padding:8px 12px; font-size:12.5px; color:var(--muted); font-family:'Consolas', monospace; } .tool-log .op{color:var(--accent); font-weight:600;} .tool-log .err{color:var(--danger);} .tool-log .ok{color:var(--ok);} footer{ padding:14px 20px; background:var(--panel); border-top:1px solid var(--border); display:flex; gap:10px; align-items:flex-end; } textarea{ flex:1; background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:10px; font-size:14.5px; resize:none; min-height:44px; max-height:140px; font-family:inherit; outline:none; } textarea:focus{border-color:var(--accent);} #sendBtn{height:44px; padding:0 22px;} .credit{ text-align:center; padding:8px; font-size:12px; color:var(--muted); background:var(--bg); border-top:1px solid var(--border); } .credit a{color:var(--accent); text-decoration:none; font-weight:600;} .credit a:hover{text-decoration:underline;} .hint{font-size:12px; color:var(--muted); padding:0 20px 6px; } .settings-row{display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted);} /* ---- history sidebar ---- */ aside#historySidebar{ width:250px; flex-shrink:0; background:var(--panel); border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; } .hist-header{ padding:12px 14px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:8px; } .hist-header span{font-size:13px; font-weight:600;} button.small{padding:5px 9px; font-size:11.5px; border-radius:6px;} .hist-file-controls{padding:10px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:6px;} .hist-status{font-size:11.5px; color:var(--muted); line-height:1.5;} .hist-status.connected{color:var(--ok);} .hist-list{flex:1; overflow-y:auto; padding:8px;} .hist-item{ padding:9px 10px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:12.5px; border:1px solid transparent; transition:background .12s; } .hist-item:hover{background:var(--panel2);} .hist-item.active{background:var(--accent2); } .hist-item .hi-title{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;} .hist-item .hi-time{font-size:10.5px; color:var(--muted); display:block; margin-top:2px;} .hist-item.active .hi-time{color:#dfe4ff;} .hist-empty{padding:14px; font-size:12px; color:var(--muted); text-align:center;} @media (max-width:700px){ aside#historySidebar{ position:absolute; inset:0 0 0 auto; z-index:5; height:100%; } } /* ---- instructions modal ---- */ .modal-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.6); display:flex; align-items:center; justify-content:center; z-index:50; padding:20px; } .modal-box{ background:var(--panel); border:1px solid var(--border); border-radius:14px; max-width:640px; width:100%; max-height:82vh; display:flex; flex-direction:column; box-shadow:0 10px 40px rgba(0,0,0,.5); } .modal-header{ display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); } .modal-header h2{margin:0; font-size:17px;} .modal-header button{background:transparent; border:none; color:var(--muted); font-size:18px; padding:4px 8px; cursor:pointer;} .modal-header button:hover{color:var(--text); background:var(--panel2); border-radius:6px;} .modal-body{padding:18px 22px; overflow-y:auto; font-size:13.5px; line-height:1.75;} .modal-body h3{font-size:14.5px; color:var(--accent); margin:18px 0 8px;} .modal-body h3:first-child{margin-top:0;} .modal-body ul{margin:6px 0; padding-inline-start:20px;} .modal-body li{margin-bottom:5px;} .modal-body code{background:var(--panel2); padding:1px 6px; border-radius:5px; font-size:12.5px; font-family:'Consolas', monospace;} .modal-body .ex{background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:8px 12px; margin:6px 0; font-family:'Consolas', monospace; font-size:12px; color:var(--muted);} #instructionsBtn{background:var(--panel2); border:1px solid var(--border); color:var(--text); display:flex; align-items:center; gap:5px;} #instructionsBtn:hover{background:#262b36;} .typing{display:flex; gap:4px; align-self:flex-start; padding:10px 15px;} .typing span{width:6px;height:6px;background:var(--muted);border-radius:50%;animation:blink 1.3s infinite;} .typing span:nth-child(2){animation-delay:.2s;} .typing span:nth-child(3){animation-delay:.4s;} @keyframes blink{0%,80%,100%{opacity:.2;} 40%{opacity:1;}} ::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-thumb{background:var(--border); border-radius:4px;} </style> </head> <body> <header> <h1><span class="dot" id="statusDot"></span> סוכן קבצים מקומי - Gemini</h1> <div class="top-controls"> <input type="password" id="apiKey" placeholder="Gemini API Key" style="width:220px;"> <select id="modelName" aria-label="בחירת מודל" style="width:210px;"> <option value="gemini-3.5-flash-lite">טוען מודלים...</option> </select> <button class="secondary" id="loadModelsBtn" title="טען את המודלים הזמינים למפתח שהוזן">טען מודלים</button> <span id="modelLoadStatus" class="folder-status" aria-live="polite"></span> <button class="secondary" id="saveKeyBtn">שמור מפתח</button> <button id="pickFolderBtn">בחר תיקייה</button> <button id="instructionsBtn">❓ הוראות שימוש</button> </div> </header> <div class="hint"> <span class="folder-status" id="folderStatus">לא נבחרה תיקייה עדיין.</span> | <label class="settings-row" style="display:inline-flex;"> <input type="checkbox" id="confirmDelete" checked style="margin-left:5px;"> אישור ידני לפני מחיקה </label> | <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" style="color:var(--accent); text-decoration:none;">קבל / נהל מפתח API ב-Google AI Studio ↗</a> </div> <main> <aside id="historySidebar"> <div class="hist-header"> <span>היסטוריית שיחות</span> <button class="secondary small" id="newChatBtn">+ שיחה חדשה</button> </div> <div class="hist-file-controls"> <div class="hist-status" id="historyFileStatus">לא מחובר קובץ היסטוריה - השיחה תישמר רק בזיכרון הדף הנוכחי.</div> <button class="secondary small" id="reconnectHistoryBtn" style="display:none;">🔄 חבר להיסטוריה השמורה</button> <button class="secondary small" id="chooseHistoryBtn">📁 בחר / צור קובץ היסטוריה</button> </div> <div class="hist-list" id="historyList"> <div class="hist-empty">אין שיחות שמורות עדיין.</div> </div> </aside> <div id="chat"> <div class="msg system">בחר תיקייה לעבודה, הזן מפתח API של Gemini, ותוכל לתת הוראות בעברית או באנגלית (למשל: "צור קובץ notes.txt עם התוכן שלום", "שנה שם של old.txt ל-new.txt", "העתק את קובץ א' לתיקיית ב'").</div> </div> </main> <footer> <textarea id="userInput" placeholder="כתוב הוראה לסוכן..." rows="1"></textarea> <button id="sendBtn">שלח</button> </footer> <div class="credit">יוצר: <a href="https://mitmachim.top/user/%D7%9E%D7%99%D7%A0%D7%99-%D7%94%D7%90%D7%A7%D7%A8" target="_blank" rel="noopener noreferrer">מיני האקר</a></div> <div class="modal-overlay" id="instructionsModal" style="display:none;"> <div class="modal-box"> <div class="modal-header"> <h2>📖 הוראות שימוש</h2> <button id="closeInstructionsBtn">✕</button> </div> <div class="modal-body"> <h3>מה זה הכלי הזה?</h3> <p>עמוד HTML עצמאי שרץ כולו בדפדפן שלך (בלי שרת), ומחבר בין מודל שפה של Gemini לבין תיקייה אמיתית במחשב שלך - כך שאפשר לתת לו הוראות בשפה חופשית והוא מבצע אותן בפועל על הקבצים.</p> <h3>דרישות חשובות</h3> <ul> <li>עובד רק ב-<b>Google Chrome</b> או <b>Microsoft Edge</b>, בגרסה עדכנית, <b>על מחשב</b>.</li> <li>לא עובד בטלפון/טאבלט (בכל דפדפן), ולא ב-Firefox או Safari - אלה לא תומכים בגישה לקבצים מקומיים.</li> <li>נדרש חיבור אינטרנט (לתקשורת עם שרתי Gemini).</li> </ul> <h3>שלב 1: מפתח API</h3> <p>הזן מפתח Gemini API בשדה "Gemini API Key". אם אין לך, לחץ על הקישור "קבל / נהל מפתח API ב-Google AI Studio" מתחת לשדות. לחיצה על "שמור מפתח" שומרת אותו בדפדפן הזה לפעם הבאה (הוא לא נשלח לשום מקום חוץ מ-Google, ולא נשמר על ידי היוצר של הכלי).</p> <h3>שלב 2: שם מודל</h3> <p>שדה "שם מודל" קובע איזה מודל Gemini משמש. ברירת המחדל היא <code>gemini-3.5-flash-lite</code>. אם תקבל שגיאת 404 שאומרת שהמודל לא קיים, Google כנראה שינתה שמות - פשוט הקלד שם מודל עדכני יותר (אפשר לבדוק ב-Google AI Studio).</p> <h3>שלב 3: בחירת תיקיית עבודה</h3> <p>לחץ "בחר תיקייה" ובחר את התיקייה שבה הסוכן יעבוד. הוא <b>לא</b> יכול לצאת מחוץ לתיקייה הזו - זו הגנת אבטחה של הדפדפן עצמו.</p> <h3>מה הסוכן יודע לעשות</h3> <ul> <li><b>ליצור / לערוך קבצים</b> - כולל קבצי קוד (JS, Python, HTML, CSS, JSON וכו') וגם קבצים בינאריים כמו תמונות (הוא כותב מחדש את הקובץ השלם בכל עריכה).</li> <li><b>לקרוא קבצים קיימים.</b></li> <li><b>לשנות שם</b> לקובץ או תיקייה.</li> <li><b>להעביר / להעתיק</b> קבצים ותיקיות שלמות (כולל רקורסיבית).</li> <li><b>למחוק</b> קבצים/תיקיות - עם בקשת אישור ידנית לפני כל מחיקה (אפשר לכבות בתיבת הסימון "אישור ידני לפני מחיקה").</li> <li><b>ליצור תיקיות חדשות.</b></li> <li><b>לרשום את תוכן התיקייה</b> (כולל רקורסיבית לכל תת-התיקיות).</li> <li><b>לחפש טקסט</b> (כמו grep) בכל הקבצים בתיקייה, כדי למצוא איפה מוגדר משהו מבלי לנחש שם קובץ.</li> </ul> <div class="ex">"צור קובץ app.py שמדפיס שלום עולם"<br>"תערוך את style.css ותשנה את צבע הרקע לכחול"<br>"תמצא באיזה קובץ מוגדרת הפונקציה login"<br>"תעביר את כל קבצי ה-png לתיקייה בשם images"</div> <h3>מה הסוכן <u>לא</u> יודע לעשות</h3> <ul> <li>ליצור תמונות/וידאו מאפס (זה דורש מודל נפרד כמו Imagen/Veo, לא כלול כאן).</li> <li>לערוך "תוכן" של קובץ תמונה בינארי (PNG/JPG) לפי בקשה חופשית - זה לא קובץ טקסט. הוא כן יכול ליצור/לערוך תמונות וקטוריות בפורמט SVG, כי הן בעצם קוד טקסטואלי.</li> <li>לצאת מגבולות התיקייה שנבחרה.</li> </ul> <h3>היסטוריית שיחות</h3> <p>בחלונית הצד: לחץ "בחר / צור קובץ היסטוריה" ובחר איפה לשמור קובץ JSON שישמור את כל השיחות. מרגע זה כל שיחה נשמרת אוטומטית. בפעם הבאה שתפתח את הכלי, הכלי "יזכור" את הקובץ ויציג כפתור "חבר להיסטוריה השמורה" (לחיצה אחת נדרשת - הגבלת אבטחה של הדפדפן). לחיצה על שיחה ברשימה טוענת אותה במלואה וממשיכה אותה בדיוק מאיפה שנעצרה. "+ שיחה חדשה" פותח שיחה נפרדת בלי למחוק את הישנות.</p> <h3>אבטחה ופרטיות</h3> <ul> <li>הכלי כולו רץ מקומית בדפדפן - אין שרת ביניים של היוצר, אין איסוף מידע.</li> <li>המפתח שלך והיסטוריית השיחות נשלחים <b>רק</b> לשרתי Google (Gemini API) לצורך קבלת תשובות, ולשום גורם אחר.</li> <li>גישה לתיקיות/קבצים תמיד דורשת אישור מפורש שלך דרך חלונית הדפדפן.</li> </ul> </div> </div> </div> <script> (function(){ const chatEl = document.getElementById('chat'); const apiKeyEl = document.getElementById('apiKey'); const modelNameEl = document.getElementById('modelName'); const loadModelsBtn = document.getElementById('loadModelsBtn'); const modelLoadStatusEl = document.getElementById('modelLoadStatus'); const folderStatusEl = document.getElementById('folderStatus'); const statusDot = document.getElementById('statusDot'); const userInputEl = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const pickFolderBtn = document.getElementById('pickFolderBtn'); const saveKeyBtn = document.getElementById('saveKeyBtn'); const confirmDeleteEl = document.getElementById('confirmDelete'); const newChatBtn = document.getElementById('newChatBtn'); const historyFileStatusEl = document.getElementById('historyFileStatus'); const reconnectHistoryBtn = document.getElementById('reconnectHistoryBtn'); const chooseHistoryBtn = document.getElementById('chooseHistoryBtn'); const historyListEl = document.getElementById('historyList'); const instructionsBtn = document.getElementById('instructionsBtn'); const instructionsModal = document.getElementById('instructionsModal'); const closeInstructionsBtn = document.getElementById('closeInstructionsBtn'); function openInstructions(){ instructionsModal.style.display = 'flex'; } function closeInstructions(){ instructionsModal.style.display = 'none'; } instructionsBtn.addEventListener('click', openInstructions); closeInstructionsBtn.addEventListener('click', closeInstructions); instructionsModal.addEventListener('click', (e) => { if(e.target === instructionsModal) closeInstructions(); }); document.addEventListener('keydown', (e) => { if(e.key === 'Escape') closeInstructions(); }); // ---------- global diagnostics (free feature: surfaces hidden JS errors instead of failing silently) ---------- window.addEventListener('error', (e) => { addMsg('system', '⚠ שגיאת JavaScript: ' + e.message); }); window.addEventListener('unhandledrejection', (e) => { addMsg('system', '⚠ שגיאה לא מטופלת: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)); }); let rootHandle = null; let history = []; // Gemini "contents" array for the CURRENT session let busy = false; // ---------- persistent chat history (saved to a JSON file at a location you choose) ---------- let historyFileHandle = null; let sessions = []; // [{ id, title, createdAt, updatedAt, messages }] let currentSessionId = null; function idbOpen(){ return new Promise((resolve, reject) => { const req = indexedDB.open('gfa_store', 1); req.onupgradeneeded = () => { req.result.createObjectStore('kv'); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function idbGet(key){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').get(key); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); } async function idbSet(key, val){ const db = await idbOpen(); return new Promise((resolve, reject) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function formatTime(iso){ try{ const d = new Date(iso); return d.toLocaleDateString('he-IL') + ' ' + d.toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'}); }catch(e){ return ''; } } function renderSidebar(){ if(!sessions.length){ historyListEl.innerHTML = '<div class="hist-empty">אין שיחות שמורות עדיין.</div>'; return; } const sorted = [...sessions].sort((a,b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')); historyListEl.innerHTML = ''; for(const s of sorted){ const div = document.createElement('div'); div.className = 'hist-item' + (s.id === currentSessionId ? ' active' : ''); const title = s.title || 'שיחה חדשה'; div.innerHTML = `<span class="hi-title">${escapeHtml(title)}</span><span class="hi-time">${escapeHtml(formatTime(s.updatedAt || s.createdAt))}</span>`; div.addEventListener('click', () => loadSession(s.id)); historyListEl.appendChild(div); } } function replayHistoryToUI(hist){ for(const item of hist){ const parts = item.parts || []; const textParts = parts.filter(p => p.text); const fcParts = parts.filter(p => p.functionCall); if(item.role === 'user' && textParts.length){ addMsg('user', textParts.map(p => p.text).join('\n')); } else if(item.role === 'model'){ if(textParts.length) addMsg('model', textParts.map(p => p.text).join('\n')); for(const fc of fcParts){ addToolLog(fc.functionCall.name, fc.functionCall.args || {}, '(מההיסטוריה)', false); } } // functionResponse parts (role 'user') are intentionally not replayed - their matching tool-log line already shown above } } function loadSession(id){ const s = sessions.find(x => x.id === id); if(!s) return; currentSessionId = id; history = s.messages; // same reference - future pushes stay in sync with the stored session chatEl.innerHTML = ''; replayHistoryToUI(history); renderSidebar(); } function startNewSession(){ currentSessionId = null; history = []; chatEl.innerHTML = ''; addMsg('system', 'שיחה חדשה. כתוב הוראה כדי להתחיל.'); renderSidebar(); } newChatBtn.addEventListener('click', startNewSession); async function saveSessionsToDisk(){ if(!historyFileHandle) return; try{ const writable = await historyFileHandle.createWritable(); await writable.write(JSON.stringify({ version: 1, sessions }, null, 2)); await writable.close(); }catch(e){ addMsg('system', 'שמירת היסטוריה נכשלה: ' + e.message); } } async function loadSessionsFromDisk(){ try{ const file = await historyFileHandle.getFile(); const text = await file.text(); if(text.trim()){ const data = JSON.parse(text); sessions = Array.isArray(data.sessions) ? data.sessions : []; } else { sessions = []; } renderSidebar(); addMsg('system', 'קובץ היסטוריה מחובר (' + sessions.length + ' שיחות שמורות נטענו).'); }catch(e){ addMsg('system', 'טעינת היסטוריה נכשלה: ' + e.message); sessions = []; } } function setHistoryStatus(text, connected){ historyFileStatusEl.textContent = text; historyFileStatusEl.className = 'hist-status' + (connected ? ' connected' : ''); } async function connectNewHistoryFile(){ if(!('showSaveFilePicker' in window)){ addMsg('system', 'הדפדפן הזה לא תומך בשמירת קובץ היסטוריה מקומי (showSaveFilePicker). נסה Chrome/Edge עדכניים.'); return; } try{ const handle = await window.showSaveFilePicker({ suggestedName: 'gemini-agent-chat-history.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] }); historyFileHandle = handle; await idbSet('history_handle', handle); await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; await saveSessionsToDisk(); // ensure a valid file exists immediately }catch(e){ if(e.name !== 'AbortError') addMsg('system', 'חיבור קובץ היסטוריה נכשל: ' + e.message); } } chooseHistoryBtn.addEventListener('click', connectNewHistoryFile); async function reconnectSavedHistoryFile(){ try{ const handle = await idbGet('history_handle'); if(!handle){ setHistoryStatus('לא נמצא קובץ היסטוריה שמור.', false); return; } const perm = await handle.requestPermission({ mode: 'readwrite' }); if(perm !== 'granted'){ setHistoryStatus('הגישה לקובץ ההיסטוריה נדחתה.', false); return; } historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); reconnectHistoryBtn.style.display = 'none'; }catch(e){ addMsg('system', 'חיבור מחדש נכשל: ' + e.message); } } reconnectHistoryBtn.addEventListener('click', reconnectSavedHistoryFile); // on load: silently check (no permission prompt yet) whether a previously-used history file is remembered (async () => { try{ const handle = await idbGet('history_handle'); if(handle){ const state = await handle.queryPermission({ mode: 'readwrite' }); if(state === 'granted'){ historyFileHandle = handle; await loadSessionsFromDisk(); setHistoryStatus('✅ מחובר: ' + handle.name, true); } else { setHistoryStatus('נמצא קובץ היסטוריה קודם (' + handle.name + ') - לחץ לחיבור.', false); reconnectHistoryBtn.style.display = ''; } } }catch(e){ /* IndexedDB unavailable or no saved handle - ignore, app still works without persistence */ } })(); // ---------- persistence for convenience (local only) ---------- try{ const savedKey = localStorage.getItem('gfa_api_key'); if(savedKey) apiKeyEl.value = savedKey; const savedModel = localStorage.getItem('gfa_model'); if(savedModel) modelNameEl.value = savedModel; }catch(e){} saveKeyBtn.addEventListener('click', () => { try{ localStorage.setItem('gfa_api_key', apiKeyEl.value.trim()); localStorage.setItem('gfa_model', modelNameEl.value.trim()); toast('המפתח והמודל נשמרו בדפדפן זה.'); loadAvailableModels(false); }catch(e){ toast('שמירה נכשלה: ' + e.message, true); } }); let modelsLoading = false; let modelsLoadTimer = null; async function loadAvailableModels(showMessage = true){ const apiKey = apiKeyEl.value.trim(); if(!apiKey){ modelLoadStatusEl.textContent = 'הזן מפתח API תחילה'; if(showMessage) toast('כדי לטעון מודלים יש להזין מפתח API של Gemini.', true); return; } if(modelsLoading) return; modelsLoading = true; loadModelsBtn.disabled = true; modelLoadStatusEl.textContent = 'טוען מודלים...'; try{ const models = []; let pageToken = ''; do{ const params = new URLSearchParams({ key: apiKey, pageSize: '100' }); if(pageToken) params.set('pageToken', pageToken); const res = await fetch('https://generativelanguage.googleapis.com/v1beta/models?' + params.toString()); if(!res.ok){ const errorText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errorText.slice(0, 250)); } const data = await res.json(); for(const model of (data.models || [])){ const methods = model.supportedGenerationMethods || []; if(methods.includes('generateContent') && model.name){ const name = model.name.replace(/^models\//, ''); models.push({ name, label: model.displayName || name }); } } pageToken = data.nextPageToken || ''; }while(pageToken); const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()] .sort((a, b) => a.name.localeCompare(b.name)); const previousModel = modelNameEl.value.trim(); modelNameEl.innerHTML = ''; for(const model of uniqueModels){ const option = document.createElement('option'); option.value = model.name; option.textContent = model.label === model.name ? model.name : model.label + ' (' + model.name + ')'; modelNameEl.appendChild(option); } // Preserve the current model when it exists; otherwise choose the first available one. if(uniqueModels.length){ modelNameEl.value = uniqueModels.some(model => model.name === previousModel) ? previousModel : uniqueModels[0].name; } else { const option = document.createElement('option'); option.value = ''; option.textContent = 'לא נמצאו מודלים זמינים'; modelNameEl.appendChild(option); } modelLoadStatusEl.textContent = uniqueModels.length ? ('✅ ' + uniqueModels.length + ' מודלים זמינים לשיחה') : 'לא נמצאו מודלים'; if(showMessage) toast('נטענו ' + uniqueModels.length + ' מודלים זמינים.'); }catch(e){ modelLoadStatusEl.textContent = 'טעינה נכשלה: ' + e.message; if(showMessage) toast('טעינת המודלים נכשלה: ' + e.message, true); }finally{ modelsLoading = false; loadModelsBtn.disabled = false; } } loadModelsBtn.addEventListener('click', () => loadAvailableModels(true)); // Load automatically shortly after the user finishes entering an API key. apiKeyEl.addEventListener('input', () => { clearTimeout(modelsLoadTimer); if(apiKeyEl.value.trim().length < 20){ modelLoadStatusEl.textContent = ''; return; } modelLoadStatusEl.textContent = 'המפתח זוהה, טוען...'; modelsLoadTimer = setTimeout(() => loadAvailableModels(false), 700); }); apiKeyEl.addEventListener('change', () => loadAvailableModels(false)); function toast(text, isErr){ addMsg('system', text); } // Load the model list automatically when a saved API key is available. if(apiKeyEl.value.trim()) loadAvailableModels(false); // ---------- UI helpers ---------- function addMsg(role, text){ const div = document.createElement('div'); div.className = 'msg ' + role; div.textContent = text; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; return div; } function addToolLog(opName, argsObj, resultText, isErr){ const div = document.createElement('div'); div.className = 'tool-log'; const argsStr = JSON.stringify(argsObj); div.innerHTML = `<span class="op">⚙ ${escapeHtml(opName)}</span>(${escapeHtml(argsStr)}) → ` + `<span class="${isErr?'err':'ok'}">${escapeHtml(resultText)}</span>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } let typingEl = null; function showTyping(){ typingEl = document.createElement('div'); typingEl.className = 'typing'; typingEl.innerHTML = '<span></span><span></span><span></span>'; chatEl.appendChild(typingEl); chatEl.scrollTop = chatEl.scrollHeight; } function hideTyping(){ if(typingEl){ typingEl.remove(); typingEl = null; } } // ---------- folder selection ---------- function checkSupport(){ if(!('showDirectoryPicker' in window)){ statusDot.style.background = 'var(--danger)'; const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); let msg = '❌ הדפדפן/המכשיר הזה לא תומך בבחירת תיקייה מקומית (File System Access API).'; if(isMobile){ msg += ' תכונה זו לא נתמכת כלל בדפדפנים בטלפון נייד (לא Android ולא iOS, בכל דפדפן) - יש לפתוח את הקובץ במחשב שולחני.'; } else { msg += ' יש לפתוח את הקובץ במחשב, בדפדפן Google Chrome או Microsoft Edge בגרסה עדכנית (לא Firefox ולא Safari - הם לא תומכים בכך).'; } folderStatusEl.textContent = msg; pickFolderBtn.disabled = true; addMsg('system', msg); return false; } return true; } const isSupported = checkSupport(); pickFolderBtn.addEventListener('click', async () => { if(!('showDirectoryPicker' in window)){ checkSupport(); return; } try{ rootHandle = await window.showDirectoryPicker(); folderStatusEl.innerHTML = 'תיקייה נבחרה: <b>' + escapeHtml(rootHandle.name) + '</b>'; statusDot.style.background = '#3ecf8e'; addMsg('system', 'נבחרה תיקיית עבודה: ' + rootHandle.name + '. הסוכן יכול כעת לפעול בתוכה בלבד.'); }catch(e){ if(e.name !== 'AbortError') toast('בחירת תיקייה נכשלה: ' + e.message, true); } }); // ---------- path resolution helpers (relative to rootHandle) ---------- function splitPath(p){ return String(p || '').split('/').map(s => s.trim()).filter(s => s.length && s !== '.'); } async function getDirHandle(path, create){ const parts = splitPath(path); let dir = rootHandle; for(const part of parts){ dir = await dir.getDirectoryHandle(part, { create: !!create }); } return dir; } async function getFileHandleAt(path, create){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב קובץ'); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), create); return await dir.getFileHandle(fileName, { create: !!create }); } async function readFileText(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); return await file.text(); } async function writeFileText(path, content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(!overwrite){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; // not found -> fine, continue } } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(content ?? ''); await writable.close(); return true; } async function deleteEntry(path, recursive){ const parts = splitPath(path); if(parts.length === 0) throw new Error('לא ניתן למחוק את תיקיית השורש'); const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); await dir.removeEntry(name, { recursive: !!recursive }); return true; } async function createDirectory(path){ await getDirHandle(path, true); return true; } async function listDirectory(path, recursive){ const dir = await getDirHandle(path || '', false); const results = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; results.push({ name: full, type: handle.kind }); if(recursive && handle.kind === 'directory'){ await walk(handle, full); } } } await walk(dir, path || ''); return results; } // free bonus feature: text search (grep-like) across files in a folder, recursively async function searchInFiles(path, query, recursive, caseSensitive){ const dir = await getDirHandle(path || '', false); const q = caseSensitive ? query : String(query).toLowerCase(); const matches = []; async function walk(dirHandle, prefix){ for await (const [name, handle] of dirHandle.entries()){ const full = prefix ? (prefix + '/' + name) : name; if(handle.kind === 'file'){ try{ const file = await handle.getFile(); const text = await file.text(); const lines = text.split('\n'); lines.forEach((line, idx) => { const hay = caseSensitive ? line : line.toLowerCase(); if(hay.indexOf(q) !== -1){ matches.push({ file: full, line_number: idx + 1, line_text: line.trim().slice(0, 200) }); } }); }catch(e){ /* skip unreadable/binary files silently */ } } else if(recursive !== false){ await walk(handle, full); } } } await walk(dir, path || ''); return matches.slice(0, 300); } async function copyFile(sourcePath, destPath, overwrite){ // binary-safe copy: works correctly for code/text files AND any binary file (images, pdf, zip, exe...) const fh = await getFileHandleAt(sourcePath, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const parts = splitPath(destPath); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const destFh = await dir.getFileHandle(fileName, { create: true }); const writable = await destFh.createWritable(); await writable.write(buf); await writable.close(); return true; } // reads any file's raw bytes and returns them as a base64 string (for binary files, images, etc.) async function readFileBase64(path){ const fh = await getFileHandleAt(path, false); const file = await fh.getFile(); const buf = await file.arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ''; const chunkSize = 0x8000; for(let i = 0; i < bytes.length; i += chunkSize){ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)); } return btoa(binary); } // writes a base64 string as raw bytes to a file (for creating/editing binary files) async function writeFileBase64(path, base64Content, overwrite){ const parts = splitPath(path); const fileName = parts.pop(); const dir = await getDirHandle(parts.join('/'), true); if(overwrite === false){ try{ await dir.getFileHandle(fileName, { create: false }); throw new Error('הקובץ כבר קיים ו-overwrite=false'); }catch(e){ if(e.message.indexOf('כבר קיים') !== -1) throw e; } } const binaryStr = atob(base64Content); const bytes = new Uint8Array(binaryStr.length); for(let i = 0; i < binaryStr.length; i++){ bytes[i] = binaryStr.charCodeAt(i); } const fh = await dir.getFileHandle(fileName, { create: true }); const writable = await fh.createWritable(); await writable.write(bytes); await writable.close(); return true; } // determines whether a path is a 'file' or 'directory' (throws if it doesn't exist) async function getEntryKind(path){ const parts = splitPath(path); if(parts.length === 0) return 'directory'; // the root itself const name = parts.pop(); const dir = await getDirHandle(parts.join('/'), false); try{ await dir.getFileHandle(name, { create: false }); return 'file'; }catch(e){ if(e.name === 'TypeMismatchError'){ return 'directory'; } if(e.name === 'NotFoundError'){ throw new Error('הנתיב לא נמצא: ' + path); } throw e; } } // recursively copies an entire directory tree to a new destination path async function copyDirectoryRecursive(sourcePath, destPath, overwrite){ await createDirectory(destPath); const srcDir = await getDirHandle(sourcePath, false); for await (const [name, handle] of srcDir.entries()){ const srcChild = sourcePath ? sourcePath + '/' + name : name; const destChild = destPath ? destPath + '/' + name : name; if(handle.kind === 'file'){ await copyFile(srcChild, destChild, overwrite !== false); } else { await copyDirectoryRecursive(srcChild, destChild, overwrite); } } } // copies either a file or a whole directory tree, dispatching by actual entry kind async function copyEntryGeneric(sourcePath, destPath, overwrite){ const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, overwrite); } else { await copyDirectoryRecursive(sourcePath, destPath, overwrite); } return kind; } async function moveEntry(sourcePath, destPath){ // supports both files and whole directories: copy (recursively if needed), then delete the original const kind = await getEntryKind(sourcePath); if(kind === 'file'){ await copyFile(sourcePath, destPath, true); await deleteEntry(sourcePath, false); } else { await copyDirectoryRecursive(sourcePath, destPath, true); await deleteEntry(sourcePath, true); } return true; } async function renameEntry(path, newName){ const parts = splitPath(path); if(parts.length === 0) throw new Error('נדרש נתיב'); parts.pop(); const destPath = (parts.length ? parts.join('/') + '/' : '') + newName; await moveEntry(path, destPath); return destPath; } // ---------- tool declarations for Gemini ---------- const tools = [{ functionDeclarations: [ { name: 'list_directory', description: 'רשימת קבצים ותיקיות בתוך נתיב יחסי לתיקיית השורש שנבחרה. נתיב ריק = שורש.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי, למשל "" או "docs/sub"' }, recursive: { type: 'BOOLEAN', description: 'האם לרשום גם תת-תיקיות באופן רקורסיבי' } } } }, { name: 'read_file', description: 'קריאת תוכן קובץ קיים - כל סוג קובץ. עבור קבצי קוד/טקסט (js, py, html, css, json, txt, md ועוד) השתמש ב-encoding="utf8" (ברירת מחדל). עבור קבצים בינאריים (תמונות, pdf, zip, exe ועוד) השתמש ב-encoding="base64".', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל, לקוד/טקסט) או "base64" (לקבצים בינאריים)' } }, required: ['path'] } }, { name: 'write_file', description: 'יצירת קובץ חדש או עריכה/דריסה מלאה של קובץ קיים - כל סוג קובץ, כולל קבצי קוד (js, py, html, css, json ועוד). עבור תוכן טקסטואלי/קוד השתמש ב-encoding="utf8" (ברירת מחדל) עם content כטקסט רגיל. עבור קובץ בינארי (תמונה ועוד) השתמש ב-encoding="base64" עם content כמחרוזת base64. יוצר תיקיות ביניים אם צריך.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לקובץ היעד' }, content: { type: 'STRING', description: 'התוכן לכתיבה - טקסט רגיל (utf8) או מחרוזת base64 (base64)' }, encoding: { type: 'STRING', description: '"utf8" (ברירת מחדל) או "base64"' }, overwrite: { type: 'BOOLEAN', description: 'האם לדרוס קובץ קיים (ברירת מחדל true)' } }, required: ['path', 'content'] } }, { name: 'delete_entry', description: 'מחיקת קובץ או תיקייה (ריקה, אלא אם recursive=true).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' }, recursive: { type: 'BOOLEAN', description: 'מחיקה רקורסיבית של תיקייה לא ריקה' } }, required: ['path'] } }, { name: 'create_directory', description: 'יצירת תיקייה חדשה (כולל תיקיות ביניים).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' } }, required: ['path'] } }, { name: 'copy_file', description: 'העתקת קובץ בודד או תיקייה שלמה (רקורסיבית) ליעד חדש (בתוך אותה תיקייה או לתיקייה אחרת).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' }, overwrite: { type: 'BOOLEAN' } }, required: ['source_path', 'dest_path'] } }, { name: 'move_entry', description: 'העברת קובץ או תיקייה שלמה למיקום/תיקייה אחרת (מוחק את המקור לאחר ההעתקה, רקורסיבית עבור תיקיות).', parameters: { type: 'OBJECT', properties: { source_path: { type: 'STRING' }, dest_path: { type: 'STRING' } }, required: ['source_path', 'dest_path'] } }, { name: 'rename_entry', description: 'שינוי שם של קובץ או תיקייה (נשאר באותה תיקיית-אב).', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב הקובץ הנוכחי' }, new_name: { type: 'STRING', description: 'השם החדש (ללא נתיב, רק שם קובץ)' } }, required: ['path', 'new_name'] } }, { name: 'search_in_files', description: 'חיפוש טקסט חופשי (כמו grep) בתוך כל קבצי הטקסט/קוד בתיקייה נתונה, כולל תת-תיקיות. מחזיר רשימת התאמות עם שם קובץ, מספר שורה ותוכן השורה. שימושי למציאת קובץ לפי תוכן, או מציאת כל המקומות שמזכירים משתנה/פונקציה מסוימת.', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING', description: 'נתיב יחסי לתיקיית ההתחלה של החיפוש, "" לשורש' }, query: { type: 'STRING', description: 'מחרוזת הטקסט לחיפוש' }, recursive: { type: 'BOOLEAN', description: 'האם לחפש גם בתת-תיקיות (ברירת מחדל true)' }, case_sensitive: { type: 'BOOLEAN', description: 'האם החיפוש רגיש לאותיות גדולות/קטנות (ברירת מחדל false)' } }, required: ['query'] } } ] }]; async function executeTool(name, args){ switch(name){ case 'list_directory': { const entries = await listDirectory(args.path || '', !!args.recursive); return { entries }; } case 'read_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; const content = encoding === 'base64' ? await readFileBase64(args.path) : await readFileText(args.path); return { content, encoding }; } case 'write_file': { const encoding = args.encoding === 'base64' ? 'base64' : 'utf8'; if(encoding === 'base64'){ await writeFileBase64(args.path, args.content, args.overwrite !== false); } else { await writeFileText(args.path, args.content, args.overwrite !== false); } return { success: true, path: args.path, encoding }; } case 'delete_entry': { if(confirmDeleteEl.checked){ const ok = window.confirm('הסוכן מבקש למחוק: ' + args.path + (args.recursive ? ' (כולל תוכן, רקורסיבי)' : '') + '\nלאשר?'); if(!ok) throw new Error('המשתמש ביטל את פעולת המחיקה'); } await deleteEntry(args.path, !!args.recursive); return { success: true, path: args.path }; } case 'create_directory': { await createDirectory(args.path); return { success: true, path: args.path }; } case 'copy_file': { const kind = await copyEntryGeneric(args.source_path, args.dest_path, args.overwrite !== false); return { success: true, from: args.source_path, to: args.dest_path, type: kind }; } case 'move_entry': { await moveEntry(args.source_path, args.dest_path); return { success: true, from: args.source_path, to: args.dest_path }; } case 'rename_entry': { const newPath = await renameEntry(args.path, args.new_name); return { success: true, new_path: newPath }; } case 'search_in_files': { const matches = await searchInFiles(args.path || '', args.query, args.recursive !== false, !!args.case_sensitive); return { matches, count: matches.length }; } default: throw new Error('פונקציה לא מוכרת: ' + name); } } // ---------- Gemini API call loop ---------- const SYSTEM_INSTRUCTION = `אתה סוכן AI שמסייע למשתמש לנהל ולערוך קבצים בתיקייה מקומית שהוא בחר בעצמו, דרך פונקציות (tools) שסופקו לך. אתה תומך בכל סוג קובץ: קבצי קוד ותוכנה (js, ts, py, java, c, cpp, html, css, json, yaml וכו'), קבצי טקסט/מסמכים, וגם קבצים בינאריים (תמונות, pdf, zip וכו') - עבור קבצי טקסט/קוד השתמש בפרמטר encoding="utf8" (ברירת מחדל), ועבור קבצים בינאריים ב-encoding="base64". כשמבקשים ממך לערוך קובץ קוד קיים: תמיד קרא אותו קודם עם read_file כדי לראות את התוכן הנוכחי, בצע את השינוי הנדרש בתוכן, וכתוב את הגרסה המלאה המעודכנת בחזרה עם write_file (overwrite=true) - אין עריכה חלקית/inline, רק כתיבה מחדש של הקובץ השלם. כל פעולה שהמשתמש מבקש (יצירת קובץ, עריכת קוד, שינוי שם, העברה, העתקה, מחיקה, רשימת תוכן) - בצע אותה ישירות דרך קריאה לפונקציה המתאימה, בלי לבקש אישור מיותר (למעט מחיקה, שיש לה אישור נפרד במערכת). נתיבים הם תמיד יחסיים לתיקיית השורש שנבחרה - אל תשתמש בנתיבים מוחלטים כמו C:\\ או /. כשצריך למצוא קובץ או מקום מסוים לפי תוכן (למשל "איפה מוגדרת הפונקציה X", "באיזה קובץ כתוב Y") - השתמש בפונקציה search_in_files במקום לנחש נתיבים. אחרי ביצוע הפעולות, סכם בקצרה ובעברית מה נעשה. אם משהו נכשל, הסבר בפשטות מה קרה ומה אפשר לנסות.`; async function callGemini(){ const apiKey = apiKeyEl.value.trim(); const model = (modelNameEl.value.trim() || 'gemini-3.5-flash-lite'); if(!apiKey) throw new Error('נא להזין API Key של Gemini'); const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`; const body = { contents: history, tools: tools, systemInstruction: { parts: [{ text: SYSTEM_INSTRUCTION }] } }; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(!res.ok){ const errText = await res.text(); throw new Error('שגיאת API (' + res.status + '): ' + errText.slice(0, 400)); } return await res.json(); } async function agentTurn(userText){ history.push({ role: 'user', parts: [{ text: userText }] }); let safety = 0; while(safety++ < 12){ showTyping(); let data; try{ data = await callGemini(); }catch(e){ hideTyping(); addMsg('system', 'שגיאה: ' + e.message); return; } hideTyping(); const candidate = data.candidates && data.candidates[0]; if(!candidate || !candidate.content){ addMsg('system', 'לא התקבלה תשובה מהמודל.'); return; } const parts = candidate.content.parts || []; const functionCalls = parts.filter(p => p.functionCall); const textParts = parts.filter(p => p.text); if(textParts.length){ addMsg('model', textParts.map(p => p.text).join('\n')); } // always record the model's turn in history - even a plain text answer with no tool calls, // so the conversation (and what gets saved to the history file) is complete and Gemini keeps memory of its own replies history.push({ role: 'model', parts: parts }); if(functionCalls.length === 0){ return; // done - plain text answer, nothing more to execute } const responseParts = []; for(const fc of functionCalls){ const { name, args } = fc.functionCall; let resultObj, isErr = false; try{ resultObj = await executeTool(name, args || {}); addToolLog(name, args || {}, 'הצלחה', false); }catch(e){ isErr = true; resultObj = { error: e.message }; addToolLog(name, args || {}, 'שגיאה: ' + e.message, true); } responseParts.push({ functionResponse: { name: name, response: resultObj } }); } history.push({ role: 'user', parts: responseParts }); // loop continues to let the model react to tool results } addMsg('system', 'הגעה למספר צעדים מקסימלי לשיחה אחת - נסה לפרק את הבקשה.'); } async function handleSend(){ if(busy) return; const text = userInputEl.value.trim(); if(!text) return; if(!rootHandle){ addMsg('system', 'קודם יש לבחור תיקיית עבודה (כפתור "בחר תיקייה" למעלה).'); return; } // create/attach a session on the first message of a new conversation if(currentSessionId === null){ const s = { id: 'S' + Date.now() + Math.random().toString(36).slice(2, 7), title: text.slice(0, 45), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), messages: history // same reference history already points to (empty array at this point) }; sessions.unshift(s); currentSessionId = s.id; } addMsg('user', text); userInputEl.value = ''; userInputEl.style.height = 'auto'; busy = true; sendBtn.disabled = true; try{ await agentTurn(text); } finally { busy = false; sendBtn.disabled = false; const s = sessions.find(x => x.id === currentSessionId); if(s){ s.updatedAt = new Date().toISOString(); } renderSidebar(); await saveSessionsToDisk(); } } sendBtn.addEventListener('click', handleSend); userInputEl.addEventListener('keydown', (e) => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); handleSend(); } }); userInputEl.addEventListener('input', () => { userInputEl.style.height = 'auto'; userInputEl.style.height = Math.min(userInputEl.scrollHeight, 140) + 'px'; }); })(); </script> </body> </html> <!-- Injection By NetFree --> <script src="//netfree.link/injection-script/go-payment.js" type="text/javascript" async ></script> <script src="//netfree.link/injection-script/popup-card-init.js" type="text/javascript" async ></script> -
@הידען
זה קשור כמובן ל @מיני-האקר
אבל למיטב ידיעתי זה לא אמור לעקוף את הסינון -
@הידען
זה קשור כמובן ל @מיני-האקר
אבל למיטב ידיעתי זה לא אמור לעקוף את הסינון@הדיבל-המעופף תכלס ממש חבל"ז סידר לי תקיה שלמה עם המון קבצים לפי חומשים כל הכבוד!!
-
ראשית כל, תודה רבה לכל המגיבים!
אם מעניין אותך, שאלתי את GPT מה דעתו, והוא העיר הערות שנראות לי לעניין
אני משפר את זה, תודה על ההערות!
פשוט תתקין ותנסה.
כבד...
עשית את זה בכלי שלך שיוצר HTML ???
לא, לא סומך על זה...
שגיאה: שגיאת API (402): {
"error": {
"code": 402,
"message": "Your prepayment credits are depleted. Please go to AI Studio at https://ai.studio/projects to manage your project and billing. Learn more at https://ai.google.dev/gemini-api/docs/billing#prepay. ",
"status": "RESOURCE_EXHAUSTED"
}
}נראה לי בעיה שהמודל עמוס. אם זה קורה שוב, תשלח שוב את ההודעה.
רק אם תוכל להוסיף שם רשימה של כל המודלים עם אפשרות להכניס בשם את החדשים שעדין ישנים.
בוצע.
תגיד לי וזה מסונן או שזה עוקף את הסינון?
איני יודע.
אם יש בעיה, @צוות פיקוח מוזמן להתריע. -
ראשית כל, תודה רבה לכל המגיבים!
אם מעניין אותך, שאלתי את GPT מה דעתו, והוא העיר הערות שנראות לי לעניין
אני משפר את זה, תודה על ההערות!
פשוט תתקין ותנסה.
כבד...
עשית את זה בכלי שלך שיוצר HTML ???
לא, לא סומך על זה...
שגיאה: שגיאת API (402): {
"error": {
"code": 402,
"message": "Your prepayment credits are depleted. Please go to AI Studio at https://ai.studio/projects to manage your project and billing. Learn more at https://ai.google.dev/gemini-api/docs/billing#prepay. ",
"status": "RESOURCE_EXHAUSTED"
}
}נראה לי בעיה שהמודל עמוס. אם זה קורה שוב, תשלח שוב את ההודעה.
רק אם תוכל להוסיף שם רשימה של כל המודלים עם אפשרות להכניס בשם את החדשים שעדין ישנים.
בוצע.
תגיד לי וזה מסונן או שזה עוקף את הסינון?
איני יודע.
אם יש בעיה, @צוות פיקוח מוזמן להתריע. -
לא יאומן, כי יסופר:
יצרתי קובץ HTML של צ'אט עם סוכן AI לווינדוס, שיכול ליצור קבצים במחשב, לערוך, להעביר, להעתיק... בקיצור: כל מה שאנחנו יכולים!!
הסוכן פועל ע"י מפתח API של ג'מיני. [יש להזין בשדה המתאים]
ולא להילחץ שהוא יהרוס לכם את המחשב, כי הוא יכול לשלוט על התיקיה שבחרתם בלבד! וכן יש אופציה להקפצת חלונית לבקשת אישור למחיקה [ברירת מחדל- פועל].
כולל:
-היסטורית שיחות [לחצן ליצירת קובץ היסטוריה במחשב. לחצן לטעינת היסטוריה מהקובץ -כל התחברות מחדש].
-לחצן יצירת שיחה חדשה.
-הוראות שימוש.
-חובת בחירת תיקיה.
-זכירת מפתח API.
אני מאוד מרוצה מזה!להורדה [להוסיף L לסיומת HTML]
אשמח לקבל תגובות, הערות, הארות ורעיונות לשיפור.
בהצלחה!עדכון: נוספו הדברים הבאים:
[לפרנואידים בלבד!!!
]
1. בטיחותי. מקשיב למשתמש בלבד.
2. מפתח API נשמר בקובץ ההסטוריה השמור במחשב ולא בדפדפן. [יש ללחוץ על 'חבר לקובץ השמור' כדי למלאות את המפתח השמור]
3. הסבר לפני כל פעולה, [למעט קריאת קובץ] מה הפעולה הולכת לעשות, עם כפתור 'אשר'. בלי לחיצה- שום דבר לא משתנה!
4. אופציית 'ביטול פעולה' על כל פעולה בנפרד!!
5. הגנה מקבצים גדולים — טקסט מעל ~8MB, בינארי מעל ~15MB לא נקראים אוטומטית. במקום זה: הודעה עם גודל וזמן משוער, וכפתור "קרא בכל זאת".
6. כפתור 'נקה הסטורייה'.
7. קובץ ההסטורייה לא שומר תכני קבצים. [מהסיבה הזאת, א"א לבטל פעולות מהסשני התחברות הקודמים]. {מתנצל...}בונוס: רשימת מודלים — שדה המודל הוא עכשיו dropdown (עם אפשרות הקלדה חופשית) עם מודלים עדכניים בלבד: gemini-3.5-flash-lite(ברירת מחדל), gemini-3.5-flash, gemini-3.5-pro, gemini-3.6-flash, ו- gemini-2.5-pro(עם אזהרה שהוא נסגר בקרוב).
הקישור להורדה למעלה עודכן.
תודה למעירים!!עדכון:
נבנתה בכלי רשימת מודלים עדכנית - פעילים, חינמיים ומדוייקים בלבד:
- gemini-3.5-flash-lite - הכי מהיר וזול, מומלץ כברירת מחדל.
- gemini-3.1-flash-lite - קל ומהיר, חלופה לגרסה הקודמת.
- gemini-3.5-flash - איזון בין מהירות לדיוק.
- gemini-3.6-flash - שדרוג ביצועים מעל 3.5.
- gemini-3.7-flash - יכולות תכנון והבנת הקשר משופרות.
- gemini-3.8-flash - החדש והחזק ביותר, מותאם למשימות סוכן (agentic).
הקישור עודכן למעלה.
בהצלחה!!
@מיני-האקר עזבתי את ג'ימני כמעט לגמרי
-
@מיני-האקר עזבתי את ג'ימני כמעט לגמרי
-
-
עדכון: נוספו הדברים הבאים:
[לפרנואידים בלבד!!!
]
1. בטיחותי. מקשיב למשתמש בלבד. [לא מתייחס לקודי קבצים עם הוראות]
2. מפתח API נשמר בקובץ ההסטוריה השמור במחשב ולא בדפדפן. [יש ללחוץ על 'חבר לקובץ השמור' כדי למלאות את המפתח השמור]
3. הסבר לפני כל פעולה, [למעט קריאת קובץ] מה הפעולה הולכת לעשות, עם כפתור 'אשר'. בלי לחיצה- שום דבר לא משתנה!
4. אופציית 'ביטול פעולה' על כל פעולה בנפרד!!
5. הגנה מקבצים גדולים — טקסט מעל ~8MB, בינארי מעל ~15MB לא נקראים אוטומטית. במקום זה: הודעה עם גודל וזמן משוער, וכפתור "קרא בכל זאת".
6. כפתור 'נקה הסטורייה'.
7. קובץ ההסטורייה לא שומר תכני קבצים. [מהסיבה הזאת, א"א לבטל פעולות מהסשני התחברות הקודמים]. {מתנצל...}בונוס: רשימת מודלים — שדה המודל הוא עכשיו dropdown (עם אפשרות הקלדה חופשית) עם מודלים עדכניים בלבד: gemini-3.5-flash-lite(ברירת מחדל), gemini-3.5-flash, gemini-3.5-pro, gemini-3.6-flash, ו- gemini-2.5-pro(עם אזהרה שהוא נסגר בקרוב).
הקישור להורדה עודכן בפוסט הראשון.
תודה למעירים, [מאירים] ותודה אישית ל @אהרן על ההתראות החשובות. -
עדכון: נוספו הדברים הבאים:
[לפרנואידים בלבד!!!
]
1. בטיחותי. מקשיב למשתמש בלבד. [לא מתייחס לקודי קבצים עם הוראות]
2. מפתח API נשמר בקובץ ההסטוריה השמור במחשב ולא בדפדפן. [יש ללחוץ על 'חבר לקובץ השמור' כדי למלאות את המפתח השמור]
3. הסבר לפני כל פעולה, [למעט קריאת קובץ] מה הפעולה הולכת לעשות, עם כפתור 'אשר'. בלי לחיצה- שום דבר לא משתנה!
4. אופציית 'ביטול פעולה' על כל פעולה בנפרד!!
5. הגנה מקבצים גדולים — טקסט מעל ~8MB, בינארי מעל ~15MB לא נקראים אוטומטית. במקום זה: הודעה עם גודל וזמן משוער, וכפתור "קרא בכל זאת".
6. כפתור 'נקה הסטורייה'.
7. קובץ ההסטורייה לא שומר תכני קבצים. [מהסיבה הזאת, א"א לבטל פעולות מהסשני התחברות הקודמים]. {מתנצל...}בונוס: רשימת מודלים — שדה המודל הוא עכשיו dropdown (עם אפשרות הקלדה חופשית) עם מודלים עדכניים בלבד: gemini-3.5-flash-lite(ברירת מחדל), gemini-3.5-flash, gemini-3.5-pro, gemini-3.6-flash, ו- gemini-2.5-pro(עם אזהרה שהוא נסגר בקרוב).
הקישור להורדה עודכן בפוסט הראשון.
תודה למעירים, [מאירים] ותודה אישית ל @אהרן על ההתראות החשובות.@מיני-האקר
למה לא gemini-3.8-flash? הוא עדיין לא זמין? -
@מיני-האקר
למה לא gemini-3.8-flash? הוא עדיין לא זמין? -
עדכון:
נבנתה בכלי רשימת מודלים עדכנית - פעילים, חינמיים ומדוייקים בלבד:
- gemini-3.5-flash-lite - הכי מהיר וזול, מומלץ כברירת מחדל.
- gemini-3.1-flash-lite - קל ומהיר, חלופה לגרסה הקודמת.
- gemini-3.5-flash - איזון בין מהירות לדיוק.
- gemini-3.6-flash - שדרוג ביצועים מעל 3.5.
- gemini-3.7-flash - יכולות תכנון והבנת הקשר משופרות.
- gemini-3.8-flash - החדש והחזק ביותר, מותאם למשימות סוכן (agentic).
הקישור עודכן בפוסט הראשון.
בהצלחה!!
ייש"כ ל @לעכשיו על תשומת הלב.
שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.
נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.
בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗
הרשמה התחברות