עוזר AI מדויק עבור ימות המשיח! בדף html! גירסה 2
-
עוזר AI מדויק עבור ימות המשיח! גירסה 2 !!ממשק משופר ואפשרות לשליחת רק חלק מההגדרות
עדכון: ניתן לבחור שלוחות מסוימות שישלחו לגמיניי, וכך גם חוסכים טוקנים, וגם גמיניי הרבה יותר ממוקד!!!
קרדיט ענק ל @perfect על קובץ הזיפ המסודר!!!
הדף עובד על מפתח API גמיני שבכל שיחה שולח קובץ זיפ עם כל הגדרות המערכת!
ניתן להזין מספר מפתחות API וברגע שמפתח אחד נכשל, הדף עובר בצורה אוטמטית למפתח הבא!
(שימו לב חובה להעלאות קובץ זיפ באופן חד פעמי, כמו כן אין אחריות על אופצית העלאה לימות המשיח האופציה טרם נבדקה כדי הצורך, הקריינות כרגע עדיין לא עובדת)
יש להוריד את קובץ הזיפ מפה קובץ אזור התעשייה.zi ולהוסיף P בסוף הסיומתקוד דף הhtml המעודכן !! בספוילר
[4aa0e732-fa68-443f-9c42-b120b1dd5273-מומחה לימות.html](/assets/uploads/files/1786649972515-4aa0e732-fa68-443f-9c42-b120b1dd5273-מומחה-לימות.html) <!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>מומחה AI ימות המשיח - סביבת פיתוח מתקדמת</title> <!-- Fonts & Icons --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> <!-- JSZip for dynamic zip extraction and folder repackaging --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <!-- Marked.js for Markdown --> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Highlight.js for Syntax Highlighting --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/tokyo-night-dark.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/ini.min.js"></script> <style> /* Theme Palettes */ :root, body[data-theme="cyberpunk"] { --bg-dark: #070913; --bg-gradient: radial-gradient(circle at 50% -20%, #1e1b4b 0%, #0c1022 50%, #070913 100%); --panel-bg: rgba(18, 24, 43, 0.7); --panel-border: rgba(99, 102, 241, 0.18); --panel-border-hover: rgba(99, 102, 241, 0.4); --user-msg: linear-gradient(135deg, #2563eb, #1d4ed8); --ai-msg: rgba(26, 34, 56, 0.75); --accent-glow: #38bdf8; --accent-blue: #3b82f6; --accent-purple: #818cf8; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.2); --text-main: #f8fafc; --text-muted: #94a3b8; --code-bg: #0d1322; --analysis-bg: rgba(28, 25, 68, 0.7); --analysis-border: #6366f1; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.35); } body[data-theme="obsidian"] { --bg-dark: #000000; --bg-gradient: radial-gradient(circle at 50% 0%, #141414 0%, #000000 80%); --panel-bg: rgba(18, 18, 18, 0.85); --panel-border: #262626; --panel-border-hover: #404040; --user-msg: linear-gradient(135deg, #27272a, #18181b); --ai-msg: rgba(20, 20, 20, 0.85); --accent-glow: #10b981; --accent-blue: #10b981; --accent-purple: #059669; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.15); --text-main: #ffffff; --text-muted: #a3a3a3; --code-bg: #080808; --analysis-bg: #121212; --analysis-border: #2e2e2e; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.7); } body[data-theme="espresso"] { --bg-dark: #140d08; --bg-gradient: radial-gradient(circle at 50% -10%, #382417 0%, #1c120b 60%, #140d08 100%); --panel-bg: rgba(38, 25, 17, 0.8); --panel-border: rgba(217, 119, 6, 0.2); --panel-border-hover: rgba(217, 119, 6, 0.45); --user-msg: linear-gradient(135deg, #b45309, #78350f); --ai-msg: rgba(48, 31, 22, 0.8); --accent-glow: #fcd34d; --accent-blue: #d97706; --accent-purple: #b45309; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.2); --text-main: #fffbeb; --text-muted: #d6b89e; --code-bg: #1b1008; --analysis-bg: rgba(69, 42, 28, 0.7); --analysis-border: #d97706; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.5); } body[data-theme="daylight"] { --bg-dark: #f8fafc; --bg-gradient: radial-gradient(circle at 50% -20%, #e0e7ff 0%, #f1f5f9 60%, #f8fafc 100%); --panel-bg: rgba(255, 255, 255, 0.85); --panel-border: #cbd5e1; --panel-border-hover: #94a3b8; --user-msg: linear-gradient(135deg, #2563eb, #1d4ed8); --ai-msg: rgba(241, 245, 249, 0.9); --accent-glow: #2563eb; --accent-blue: #2563eb; --accent-purple: #4f46e5; --success: #059669; --success-glow: rgba(5, 150, 105, 0.15); --text-main: #0f172a; --text-muted: #64748b; --code-bg: #ffffff; --analysis-bg: #eef2ff; --analysis-border: #818cf8; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.06); } /* Font Sizes */ body[data-font="small"] { font-size: 14px; } body[data-font="medium"] { font-size: 15.5px; } body[data-font="large"] { font-size: 17.5px; } body[data-font="huge"] { font-size: 19.5px; } * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Rubik', sans-serif; } body { background-color: var(--bg-dark); color: var(--text-main); height: 100vh; display: flex; flex-direction: column; overflow: hidden; background-image: var(--bg-gradient); transition: background 0.3s ease, color 0.3s ease; } /* Custom Scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); } /* Navigation Header */ header { background: var(--panel-bg); backdrop-filter: blur(20px); border-bottom: 1px solid var(--panel-border); padding: 10px 24px; display: flex; justify-content: space-between; align-items: center; z-index: 20; box-shadow: var(--shadow-subtle); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .logo-area { display: flex; align-items: center; gap: 16px; } .logo { font-size: 1.35rem; font-weight: 800; background: linear-gradient(135deg, var(--accent-glow), var(--accent-purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: flex; align-items: center; gap: 10px; letter-spacing: -0.5px; } /* Topbar Status Badge */ .topbar-status-badge { display: inline-flex; align-items: center; gap: 7px; padding: 4px 12px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; background: rgba(255,255,255,0.04); border: 1px solid var(--panel-border); color: var(--text-muted); } .topbar-status-badge.active { background: var(--success-glow); border-color: rgba(16, 185, 129, 0.4); color: var(--success); } .status-dot { width: 7px; height: 7px; border-radius: 50%; background: #64748b; } .topbar-status-badge.active .status-dot { background: var(--success); box-shadow: 0 0 8px var(--success); animation: pulse-dot 2s infinite; } @keyframes pulse-dot { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.7; } } .header-actions { display: flex; align-items: center; gap: 10px; } .tabs { display: flex; gap: 8px; } .tab-btn { background: rgba(255,255,255,0.03); border: 1px solid var(--panel-border); color: var(--text-main); padding: 7px 16px; border-radius: 10px; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 600; display: flex; align-items: center; gap: 8px; } .tab-btn.active { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border-color: transparent; color: #fff; box-shadow: 0 4px 15px rgba(59, 130, 246, 0.35); } .tab-btn:hover:not(.active) { background: rgba(255,255,255,0.08); border-color: var(--panel-border-hover); } /* Workspace Grid */ main { flex: 1; overflow: hidden; position: relative; } .tab-content { display: none; height: 100%; padding: 16px 20px; } .tab-content.active { display: flex; gap: 16px; } /* Sidebar Sessions */ .history-sidebar { width: 280px; background: var(--panel-bg); backdrop-filter: blur(20px); border: 1px solid var(--panel-border); border-radius: 18px; padding: 16px; display: flex; flex-direction: column; gap: 12px; box-shadow: var(--shadow-subtle); transition: all 0.3s ease; } .new-chat-btn { width: 100%; background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border: none; color: white; padding: 11px 16px; border-radius: 12px; cursor: pointer; font-weight: 700; font-size: 0.92rem; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s ease; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); } .new-chat-btn:hover { opacity: 0.95; transform: translateY(-1.5px); box-shadow: 0 6px 18px rgba(59, 130, 246, 0.45); } .history-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-left: 4px; } .history-item { background: rgba(255,255,255,0.02); border: 1px solid transparent; padding: 9px 12px; border-radius: 10px; cursor: pointer; transition: all 0.2s; display: flex; justify-content: space-between; align-items: center; font-size: 0.86rem; color: var(--text-muted); } .history-item:hover, .history-item.active { background: rgba(99, 102, 241, 0.12); border-color: rgba(99, 102, 241, 0.3); color: var(--text-main); } .history-item .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } .history-item .delete-btn { opacity: 0; transition: opacity 0.2s; color: #ef4444; padding: 2px 6px; } .history-item:hover .delete-btn { opacity: 1; } /* Main Chat Window */ .chat-main { flex: 1; display: flex; flex-direction: column; height: 100%; position: relative; } .chat-container { flex: 1; overflow-y: auto; padding: 10px 10px 30px 10px; display: flex; flex-direction: column; gap: 18px; scroll-behavior: smooth; } .message { max-width: 85%; padding: 16px 22px; border-radius: 18px; line-height: 1.7; word-wrap: break-word; box-shadow: var(--shadow-subtle); backdrop-filter: blur(14px); animation: messageIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); } @keyframes messageIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .message.user { align-self: flex-start; background: var(--user-msg); color: white; border-bottom-right-radius: 3px; } .message.model { align-self: flex-end; background: var(--ai-msg); border-bottom-left-radius: 3px; border: 1px solid var(--panel-border); } .message-attachments { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; } .msg-att-chip { background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.15); padding: 3px 10px; border-radius: 6px; font-size: 0.78rem; display: flex; align-items: center; gap: 6px; } /* Loading UI Animation */ .loading-shimmer { display: inline-flex; align-items: center; gap: 10px; font-weight: 500; color: var(--accent-glow); } .loading-shimmer i { font-size: 1.1rem; animation: spin 1s linear infinite; } @keyframes spin { 100% { transform: rotate(360deg); } } /* Analysis Component */ .analysis-box { background: var(--analysis-bg); border: 1px solid var(--analysis-border); border-radius: 14px; margin-bottom: 14px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15); } .analysis-header { padding: 11px 16px; background: rgba(99, 102, 241, 0.22); cursor: pointer; font-weight: 700; font-size: 0.9rem; color: var(--accent-glow); display: flex; justify-content: space-between; align-items: center; user-select: none; } .analysis-content { padding: 16px; font-size: 0.9rem; color: var(--text-main); display: none; border-top: 1px solid var(--analysis-border); white-space: pre-wrap; line-height: 1.6; } .analysis-box.open .analysis-content { display: block; } /* Attachments Bar */ .attachments-preview { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 4px; } .att-chip { background: rgba(56, 189, 248, 0.12); border: 1px solid var(--accent-glow); padding: 5px 12px; border-radius: 8px; font-size: 0.82rem; display: flex; align-items: center; gap: 8px; color: var(--text-main); } .att-chip .remove-att { cursor: pointer; color: #ef4444; font-weight: bold; } /* Input Controls Area */ .input-area { background: var(--panel-bg); backdrop-filter: blur(20px); padding: 14px 18px; border: 1px solid var(--panel-border); border-radius: 18px; display: flex; flex-direction: column; gap: 10px; margin-top: 8px; box-shadow: var(--shadow-subtle); } .controls-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: var(--text-muted); flex-wrap: wrap; gap: 10px; } .input-wrapper { display: flex; gap: 8px; align-items: center; } textarea { flex: 1; background: rgba(0, 0, 0, 0.25); border: 1px solid var(--panel-border); color: var(--text-main); padding: 12px 16px; border-radius: 14px; resize: none; height: 60px; outline: none; font-size: 0.98rem; transition: all 0.2s ease; } textarea:focus { border-color: var(--accent-glow); box-shadow: 0 0 12px rgba(56, 189, 248, 0.2); } .btn-action { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border: none; color: white; padding: 0 22px; height: 60px; border-radius: 14px; cursor: pointer; font-weight: 700; font-size: 1rem; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); } .btn-action:hover { opacity: 0.95; transform: translateY(-1.5px); box-shadow: 0 6px 20px rgba(59, 130, 246, 0.45); } .btn-action:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; } .btn-icon-ctl { background: rgba(255,255,255,0.03); border: 1px solid var(--panel-border); color: var(--text-main); width: 60px; height: 60px; border-radius: 14px; cursor: pointer; font-size: 1.2rem; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .btn-icon-ctl:hover { background: rgba(255,255,255,0.08); border-color: var(--accent-glow); transform: translateY(-1px); } .btn-icon-ctl.recording { background: #ef4444; color: white; animation: pulse 1.2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 12px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } /* Code Block Styling */ .code-container { position: relative; margin: 14px 0; border-radius: 12px; overflow: hidden; border: 1px solid var(--panel-border); background: var(--code-bg); box-shadow: 0 6px 20px rgba(0,0,0,0.3); } .code-header { background: rgba(255,255,255,0.03); padding: 9px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--panel-border); font-size: 0.82rem; color: var(--text-muted); } .code-actions { display: flex; gap: 6px; } .code-btn { background: rgba(255,255,255,0.05); border: 1px solid var(--panel-border); color: var(--text-main); padding: 4px 10px; border-radius: 6px; font-size: 0.78rem; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 5px; font-weight: 500; } .code-btn:hover { background: var(--accent-blue); color: white; border-color: transparent; } pre { margin: 0 !important; } pre code { font-family: 'JetBrains Mono', monospace !important; font-size: 0.92rem; padding: 16px !important; line-height: 1.55; } /* Settings Panels */ .settings-card { background: var(--panel-bg); backdrop-filter: blur(20px); padding: 24px; border-radius: 18px; border: 1px solid var(--panel-border); margin-bottom: 18px; width: 100%; max-width: 820px; box-shadow: var(--shadow-subtle); } .settings-card h3 { margin-bottom: 14px; color: var(--accent-glow); display: flex; align-items: center; gap: 10px; font-size: 1.15rem; font-weight: 700; } .input-field { width: 100%; padding: 11px 16px; background: rgba(0, 0, 0, 0.25); border: 1px solid var(--panel-border); color: var(--text-main); border-radius: 10px; margin-bottom: 12px; font-size: 0.95rem; outline: none; transition: 0.2s; } .input-field:focus { border-color: var(--accent-glow); } textarea.keys-input { height: 100px; font-family: 'JetBrains Mono', monospace; direction: ltr; } select { background: var(--panel-bg); color: var(--text-main); border: 1px solid var(--panel-border); padding: 6px 12px; border-radius: 8px; outline: none; font-weight: 500; } /* ZIP Management & Dynamic Folder Selector */ .zip-status-container { border: 1px dashed var(--panel-border); border-radius: 14px; padding: 20px; background: rgba(0,0,0,0.15); display: flex; flex-direction: column; gap: 14px; transition: all 0.2s ease; } .zip-status-container.has-file { border-style: solid; border-color: rgba(16, 185, 129, 0.4); background: var(--success-glow); } .zip-info-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } .zip-info-details { display: flex; align-items: center; gap: 12px; } .zip-icon { font-size: 2rem; color: var(--success); } .zip-info-text .title { font-weight: 700; font-size: 0.98rem; } .zip-info-text .sub { font-size: 0.82rem; color: var(--text-muted); } .zip-actions { display: flex; gap: 8px; } /* Folder Picker Grid in Settings */ .folder-picker-section { border-top: 1px solid var(--panel-border); padding-top: 14px; } .folder-picker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .folder-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; max-height: 180px; overflow-y: auto; padding: 4px; } .folder-checkbox-item { background: rgba(0,0,0,0.25); border: 1px solid var(--panel-border); padding: 7px 12px; border-radius: 8px; display: flex; align-items: center; gap: 8px; font-size: 0.86rem; cursor: pointer; user-select: none; transition: 0.2s; } .folder-checkbox-item:hover { border-color: var(--accent-glow); background: rgba(56, 189, 248, 0.08); } .folder-checkbox-item input { cursor: pointer; } /* Zen Mode */ body.zen-mode header { transform: translateY(-100%); height: 0; padding: 0; overflow: hidden; } body.zen-mode .history-sidebar { display: none; } body.zen-mode .tab-content { padding: 10px 14%; } .exit-zen-btn { display: none; position: fixed; top: 16px; left: 16px; background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; padding: 9px 16px; border-radius: 10px; cursor: pointer; z-index: 1000; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.5); } body.zen-mode .exit-zen-btn { display: flex; align-items: center; gap: 6px; } /* Jump Button */ .jump-bottom-btn { position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%) translateY(20px); background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; padding: 8px 18px; border-radius: 20px; cursor: pointer; font-size: 0.84rem; font-weight: bold; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4); display: flex; align-items: center; gap: 8px; opacity: 0; pointer-events: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); z-index: 50; } .jump-bottom-btn.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); } .jump-bottom-btn:hover { transform: translateX(-50%) scale(1.05); } </style> </head> <body data-theme="cyberpunk" data-font="medium"> <!-- Exit Zen Mode Button --> <button class="exit-zen-btn" onclick="toggleZenMode()"><i class="fa-solid fa-compress"></i> צא ממצב פוקוס</button> <header> <div class="logo-area"> <div class="logo"> <i class="fa-solid fa-headset"></i> <span>מומחה AI ימות המשיח</span> </div> <!-- Live Topbar Context Indicator --> <div class="topbar-status-badge" id="topbarZipBadge" title="סטטוס קובץ תיעוד ימות המשיח"> <div class="status-dot"></div> <span id="topbarZipText">בודק תיעוד...</span> </div> </div> <div class="header-actions"> <button class="tab-btn" onclick="toggleZenMode()" title="מצב פוקוס נקי (Zen Mode)"><i class="fa-solid fa-expand"></i> פוקוס</button> <div class="tabs"> <button class="tab-btn active" onclick="switchTab('chatTab', event)"><i class="fa-solid fa-comments"></i> צ'אט וסביבת עבודה</button> <button class="tab-btn" onclick="switchTab('settingsTab', event)"><i class="fa-solid fa-sliders"></i> הגדרות ותיעוד</button> </div> </div> </header> <main> <!-- Chat Tab with History Sidebar --> <div id="chatTab" class="tab-content active"> <div class="history-sidebar"> <button class="new-chat-btn" onclick="createNewSession()"><i class="fa-solid fa-plus"></i> שיחה חדשה</button> <div style="font-size: 0.78rem; color: var(--text-muted); font-weight: 700; margin-top: 6px; letter-spacing: 0.5px;">שיחות אחרונות</div> <div class="history-list" id="historyList"></div> </div> <div class="chat-main"> <div class="chat-container" id="chatContainer" onscroll="handleChatScroll()"> <div class="message model"> שלום! אני עוזר ה-AI הבכיר שלך לבניית מרכזיות, שלוחות ו-Webhooks ב'ימות המשיח'.<br> כעת ניתן <b>לבחור תיקיות ספציפיות</b> מתוך קובץ ה-ZIP של התיעוד לשליחה ממוקדת ומהירה! </div> </div> <button class="jump-bottom-btn" id="jumpBottomBtn" onclick="scrollToBottom(true)"> <i class="fa-solid fa-arrow-down"></i> קפוץ לקוד האחרון / למטה </button> <div class="input-area"> <div class="attachments-preview" id="attachmentsPreview"></div> <div class="controls-row"> <div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> <div> <i class="fa-solid fa-robot"></i> מודל: <select id="modelSelect"></select> </div> <!-- Fast In-Chat Folder Scope Selector --> <div id="chatFolderScopeContainer" style="display: none;"> <i class="fa-solid fa-folder-tree"></i> תיקיית תיעוד: <select id="chatFolderSelect" onchange="syncSelectedFolderFromChat(this.value)"></select> </div> </div> <div> <label style="cursor: pointer;"> <input type="checkbox" id="deepAnalysisToggle" checked> דרוש ניתוח מעמיק (<analysis>) </label> </div> </div> <div class="input-wrapper"> <input type="file" id="genericFileInput" multiple style="display: none;" onchange="handleGenericFileUpload(event)"> <button class="btn-icon-ctl" onclick="document.getElementById('genericFileInput').click()" title="צרף קבצים (קוד, תמונות, שמע, PDF, ZIP...)"> <i class="fa-solid fa-paperclip"></i> </button> <button class="btn-icon-ctl" id="micBtn" onclick="toggleVoiceInput()" title="הקלטה קולית בעברית"> <i class="fa-solid fa-microphone"></i> </button> <textarea id="userInput" placeholder="תאר את השלוחה או שאל שאלה (ניתן לגרור קבצים לכאן)..." onkeydown="handleKeyDown(event)"></textarea> <button class="btn-action" id="sendBtn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i> שלח</button> </div> </div> </div> </div> <!-- Settings & UI Customization Tab --> <div id="settingsTab" class="tab-content" style="flex-direction: column; overflow-y: auto;"> <!-- Dynamic ZIP & Folder Management Card --> <div class="settings-card"> <h3><i class="fa-solid fa-file-zipper"></i> תיעוד ימות המשיח ובחירת תיקיות (Folder Filtering)</h3> <p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 14px;">קובץ ה-ZIP של התיעוד מנותח אוטומטית. תוכל לבחור לשלוח את כל התיעוד או רק תיקיות ספציפיות לקבלת מענה ממוקד ומהיר.</p> <input type="file" id="zipFileInput" accept=".zip" style="display: none;" onchange="handleZipUpload(event)"> <div class="zip-status-container" id="zipStatusBox"></div> </div> <div class="settings-card"> <h3><i class="fa-solid fa-key"></i> שרשור מפתחות API (Gemini Key Rotation)</h3> <textarea id="apiKeysInput" class="input-field keys-input" placeholder="הכנס מפתחות API (מפתח בכל שורה)..."></textarea> <button class="tab-btn active" onclick="saveKeys()"><i class="fa-solid fa-floppy-disk"></i> שמור מפתחות וטען מודלים</button> </div> <div class="settings-card"> <h3><i class="fa-solid fa-palette"></i> התאמת מראה וסביבת עבודה (Themes & UI)</h3> <div style="display: flex; gap: 16px; flex-wrap: wrap;"> <div style="flex: 1;"> <label style="display: block; margin-bottom: 6px; color: var(--text-muted); font-size: 0.88rem;">ערכת נושא (Theme):</label> <select id="themeSelect" class="input-field" onchange="changeTheme(this.value)"> <option value="cyberpunk">🌌 Midnight Cyberpunk (ניאון כהה)</option> <option value="obsidian">🖤 Obsidian Pure Black (שחור מוחלט)</option> <option value="espresso">☕ Warm Espresso (חום קפה חמים)</option> <option value="daylight">☀️ Clean Daylight (מצב יום בהיר)</option> </select> </div> <div style="flex: 1;"> <label style="display: block; margin-bottom: 6px; color: var(--text-muted); font-size: 0.88rem;">גודל גופן (Font Size):</label> <select id="fontSelect" class="input-field" onchange="changeFontSize(this.value)"> <option value="small">קטן (14px)</option> <option value="medium" selected>בינוני (15.5px)</option> <option value="large">גדול (17.5px)</option> <option value="huge">ענק (19.5px)</option> </select> </div> </div> </div> <div class="settings-card"> <h3><i class="fa-solid fa-cloud-arrow-up"></i> חיבור ישיר ל-API ימות המשיח</h3> <input type="text" id="ymUsername" class="input-field" placeholder="מספר מערכת (System Username)"> <input type="password" id="ymPassword" class="input-field" placeholder="סיסמת מערכת (System Password)"> <button class="tab-btn active" onclick="saveYemotCredentials()"><i class="fa-solid fa-link"></i> שמור פרטי התחברות למערכת</button> </div> </div> </main> <script> const SYSTEM_INSTRUCTION = `אתה מתכנת בכיר ומומחה לשלוחות, מרכזיות ופיתוח ב'ימות המשיח'. תפקידך לבנות קובצי ext.ini מדויקים, תפריטי IVR, ושרתי Webhook. לרשותך מצורף קובץ ZIP המכיל את התיעוד וההגדרות של ימות המשיח (בהתאם לתיקיות שנבחרו), לצד כל קובץ שהמשתמש מעלה ומצרף. עליך להתבסס על התיעוד באופן מוחלט בכל תשובה. הנחיות חובה: 1. אם המשתמש ביקש ניתוח מעמיק (או כברירת מחדל), עליך לפתוח את תגובתך בניתוח מקיף ומעמיק של הדרישה, הפורמט, הלוגיקה, והפרמטרים הנדרשים עטוף כולו בתוך תגיות <analysis> ו-</analysis>. 2. רק לאחר התגית הסוגרת </analysis> תציג את התשובה והקוד המלאים והסופיים. 3. הקפד על כתיבת קוד ext.ini תקני, כולל הערות והסברים ברורים בעברית.`; const LOCAL_STORAGE_KEYS = { API_KEYS: 'yemot_v8_gemini_api_keys', ZIP_BASE64: 'yemot_v8_zip_base64', ZIP_META: 'yemot_v8_zip_meta', ZIP_FOLDERS: 'yemot_v8_zip_folders', SELECTED_FOLDERS: 'yemot_v8_selected_folders', SESSIONS: 'yemot_v8_chat_sessions', YM_USER: 'yemot_v8_ym_username', YM_PASS: 'yemot_v8_ym_password', THEME: 'yemot_v8_theme', FONT_SIZE: 'yemot_v8_font_size' }; // App State let apiKeys = []; let zipBase64 = localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_BASE64) || null; let zipMeta = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_META) || 'null'); let zipFolders = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS) || '[]'); let selectedFolders = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS) || '[]'); let sessions = []; let currentSessionId = null; let isRecording = false; let recognition = null; let pendingAttachments = []; window.onload = async () => { loadThemePreferences(); loadKeys(); loadYemotCredentials(); initSpeechRecognition(); initDragAndDrop(); loadSessions(); updateZipStatusUI(); if (apiKeys.length > 0) await fetchModels(); }; function switchTab(tabId, event) { document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); document.getElementById(tabId).classList.add('active'); if(event) event.currentTarget.classList.add('active'); } // --- Dynamic ZIP & Folder Analysis --- async function analyzeZipFolders(base64Data) { try { const zip = new JSZip(); await zip.loadAsync(base64Data, { base64: true }); const folderSet = new Set(); zip.forEach((relativePath, file) => { const parts = relativePath.split('/'); if (parts.length > 1 && parts[0].trim().length > 0) { folderSet.add(parts[0]); } }); zipFolders = Array.from(folderSet).sort(); selectedFolders = [...zipFolders]; // Default: select all localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS, JSON.stringify(zipFolders)); localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); } catch (err) { console.error("Error analyzing ZIP folders:", err); zipFolders = []; selectedFolders = []; } } function updateZipStatusUI() { const statusBox = document.getElementById('zipStatusBox'); const topbarBadge = document.getElementById('topbarZipBadge'); const topbarText = document.getElementById('topbarZipText'); const chatFolderContainer = document.getElementById('chatFolderScopeContainer'); const chatFolderSelect = document.getElementById('chatFolderSelect'); if (zipBase64 && zipMeta) { let folderCheckboxesHTML = ''; if (zipFolders.length > 0) { folderCheckboxesHTML = ` <div class="folder-picker-section"> <div class="folder-picker-header"> <span style="font-weight: 700; font-size: 0.9rem;"><i class="fa-solid fa-folder-tree"></i> בחר תיקיות לשליחה כהקשר (${selectedFolders.length}/${zipFolders.length}):</span> <div style="display: flex; gap: 8px;"> <button class="tab-btn" style="padding: 3px 10px; font-size: 0.78rem;" onclick="selectAllFolders(true)">בחר הכל</button> <button class="tab-btn" style="padding: 3px 10px; font-size: 0.78rem;" onclick="selectAllFolders(false)">נקה הכל</button> </div> </div> <div class="folder-picker-grid"> ${zipFolders.map(folder => ` <label class="folder-checkbox-item"> <input type="checkbox" value="${escapeHtml(folder)}" ${selectedFolders.includes(folder) ? 'checked' : ''} onchange="toggleFolderSelection('${escapeHtml(folder)}')"> <i class="fa-regular fa-folder" style="color: var(--accent-glow);"></i> <span>${escapeHtml(folder)}</span> </label> `).join('')} </div> </div> `; } statusBox.className = 'zip-status-container has-file'; statusBox.innerHTML = ` <div class="zip-info-row"> <div class="zip-info-details"> <i class="fa-solid fa-circle-check zip-icon"></i> <div class="zip-info-text"> <div class="title">קובץ תיעוד פעיל: ${escapeHtml(zipMeta.name)}</div> <div class="sub">גודל: ${zipMeta.size} | סה"כ ${zipFolders.length} תיקיות זוהו</div> </div> </div> <div class="zip-actions"> <button class="tab-btn active" onclick="document.getElementById('zipFileInput').click()"> <i class="fa-solid fa-arrows-rotate"></i> החלף ZIP </button> <button class="tab-btn" style="color: #ef4444; border-color: rgba(239, 68, 68, 0.3);" onclick="removeZipFile()"> <i class="fa-solid fa-trash"></i> הסר </button> </div> </div> ${folderCheckboxesHTML} `; topbarBadge.className = 'topbar-status-badge active'; const folderCountText = selectedFolders.length === zipFolders.length ? 'כל התיקיות' : `${selectedFolders.length} תיקיות`; topbarText.innerText = `תיעוד פעיל (${folderCountText})`; // Update In-Chat Quick Selector if (zipFolders.length > 0) { chatFolderContainer.style.display = 'block'; chatFolderSelect.innerHTML = `<option value="__ALL__">כל התיקיות (${zipFolders.length})</option>` + zipFolders.map(f => `<option value="${escapeHtml(f)}" ${selectedFolders.length === 1 && selectedFolders[0] === f ? 'selected' : ''}>📁 ${escapeHtml(f)}</option>`).join(''); } else { chatFolderContainer.style.display = 'none'; } } else { statusBox.className = 'zip-status-container'; statusBox.innerHTML = ` <div class="zip-info-row" style="justify-content: center; text-align: center; padding: 10px 0;"> <div> <i class="fa-solid fa-cloud-arrow-up" style="font-size: 2.2rem; color: var(--text-muted); margin-bottom: 8px;"></i> <div style="font-weight: 700; margin-bottom: 4px;">טרם נטען קובץ ZIP של תיעוד ימות המשיח</div> <div style="font-size: 0.84rem; color: var(--text-muted); margin-bottom: 12px;">העלה את קובץ ההגדרות (~688 KB) כדי לאפשר בחירת תיקיות ממוקדת</div> <button class="tab-btn active" onclick="document.getElementById('zipFileInput').click()" style="margin: 0 auto;"> <i class="fa-solid fa-upload"></i> בחר קובץ ZIP להעלאה </button> </div> </div> `; topbarBadge.className = 'topbar-status-badge'; topbarText.innerText = 'אין תיעוד ZIP טעון'; chatFolderContainer.style.display = 'none'; } } async function handleZipUpload(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async function(e) { zipBase64 = e.target.result.split(',')[1]; zipMeta = { name: file.name, size: (file.size / 1024).toFixed(1) + ' KB' }; await analyzeZipFolders(zipBase64); try { localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_BASE64, zipBase64); localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_META, JSON.stringify(zipMeta)); } catch (err) { console.warn('Zip stored in runtime memory.'); } updateZipStatusUI(); }; reader.readAsDataURL(file); event.target.value = ''; } function toggleFolderSelection(folder) { if (selectedFolders.includes(folder)) { selectedFolders = selectedFolders.filter(f => f !== folder); } else { selectedFolders.push(folder); } localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); updateZipStatusUI(); } function selectAllFolders(selectAll) { selectedFolders = selectAll ? [...zipFolders] : []; localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); updateZipStatusUI(); } function syncSelectedFolderFromChat(folderVal) { if (folderVal === '__ALL__') { selectedFolders = [...zipFolders]; } else { selectedFolders = [folderVal]; } localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); updateZipStatusUI(); } function removeZipFile() { if (!confirm('האם להסיר את קובץ ה-ZIP של התיעוד?')) return; zipBase64 = null; zipMeta = null; zipFolders = []; selectedFolders = []; localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_BASE64); localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_META); localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS); localStorage.removeItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS); updateZipStatusUI(); } // --- Generate Filtered ZIP Payload On-The-Fly --- async function getFilteredZipBase64() { if (!zipBase64) return null; if (zipFolders.length === 0 || selectedFolders.length === zipFolders.length) { return zipBase64; // Send full ZIP } if (selectedFolders.length === 0) { return null; // No folders selected } try { const sourceZip = new JSZip(); await sourceZip.loadAsync(zipBase64, { base64: true }); const filteredZip = new JSZip(); const promises = []; sourceZip.forEach((relativePath, file) => { const topFolder = relativePath.split('/')[0]; if (selectedFolders.includes(topFolder) && !file.dir) { const p = file.async('uint8array').then(content => { filteredZip.file(relativePath, content); }); promises.push(p); } }); await Promise.all(promises); return await filteredZip.generateAsync({ type: 'base64' }); } catch (err) { console.error("Error generating filtered ZIP:", err); return zipBase64; } } // --- File Uploads & Drag-and-Drop --- function handleGenericFileUpload(event) { const files = Array.from(event.target.files); processFiles(files); event.target.value = ''; } function initDragAndDrop() { const textarea = document.getElementById('userInput'); ['dragenter', 'dragover'].forEach(eventName => { textarea.addEventListener(eventName, (e) => { e.preventDefault(); textarea.style.borderColor = 'var(--accent-glow)'; }, false); }); ['dragleave', 'drop'].forEach(eventName => { textarea.addEventListener(eventName, (e) => { e.preventDefault(); textarea.style.borderColor = 'var(--panel-border)'; }, false); }); textarea.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = Array.from(dt.files); if (files.length > 0) processFiles(files); }); } function processFiles(files) { files.forEach(file => { const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; let mimeType = file.type || getFallbackMime(file.name); pendingAttachments.push({ name: file.name, mimeType: mimeType, data: base64Data, size: (file.size / 1024).toFixed(1) + ' KB' }); renderAttachmentsPreview(); }; reader.readAsDataURL(file); }); } function getFallbackMime(filename) { const ext = filename.split('.').pop().toLowerCase(); const map = { 'ini': 'text/plain', 'txt': 'text/plain', 'wav': 'audio/wav', 'mp3': 'audio/mp3', 'pdf': 'application/pdf', 'csv': 'text/csv', 'json': 'application/json', 'zip': 'application/zip', 'js': 'text/javascript', 'php': 'text/x-php', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg' }; return map[ext] || 'application/octet-stream'; } function removeAttachment(index) { pendingAttachments.splice(index, 1); renderAttachmentsPreview(); } function renderAttachmentsPreview() { const previewEl = document.getElementById('attachmentsPreview'); previewEl.innerHTML = ''; pendingAttachments.forEach((att, idx) => { const chip = document.createElement('div'); chip.className = 'att-chip'; chip.innerHTML = ` <i class="fa-solid fa-file"></i> <span>${escapeHtml(att.name)} (${att.size})</span> <span class="remove-att" onclick="removeAttachment(${idx})">×</span> `; previewEl.appendChild(chip); }); } // --- Zen & Themes --- function toggleZenMode() { document.body.classList.toggle('zen-mode'); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.body.classList.contains('zen-mode')) { toggleZenMode(); } }); function changeTheme(theme) { document.body.setAttribute('data-theme', theme); localStorage.setItem(LOCAL_STORAGE_KEYS.THEME, theme); } function changeFontSize(size) { document.body.setAttribute('data-font', size); localStorage.setItem(LOCAL_STORAGE_KEYS.FONT_SIZE, size); } function loadThemePreferences() { const savedTheme = localStorage.getItem(LOCAL_STORAGE_KEYS.THEME) || 'cyberpunk'; const savedFont = localStorage.getItem(LOCAL_STORAGE_KEYS.FONT_SIZE) || 'medium'; changeTheme(savedTheme); changeFontSize(savedFont); document.getElementById('themeSelect').value = savedTheme; document.getElementById('fontSelect').value = savedFont; } function handleChatScroll() { const container = document.getElementById('chatContainer'); const jumpBtn = document.getElementById('jumpBottomBtn'); const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; if (distanceFromBottom > 200) { jumpBtn.classList.add('show'); } else { jumpBtn.classList.remove('show'); } } function scrollToBottom(smooth = true) { const container = document.getElementById('chatContainer'); container.scrollTo({ top: container.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); } // --- Session Engine --- function loadSessions() { try { const saved = localStorage.getItem(LOCAL_STORAGE_KEYS.SESSIONS); sessions = saved ? JSON.parse(saved) : []; } catch (e) { sessions = []; } if (sessions.length === 0) { createNewSession(); } else { currentSessionId = sessions[0].id; renderSessionsList(); renderCurrentSessionChat(); } } function saveSessions() { try { localStorage.setItem(LOCAL_STORAGE_KEYS.SESSIONS, JSON.stringify(sessions)); } catch (e) { console.warn('LocalStorage limit reached.'); } renderSessionsList(); } function createNewSession() { const newSession = { id: Date.now().toString(), title: 'שיחה חדשה', timestamp: new Date().toLocaleTimeString('he-IL', {hour: '2-digit', minute:'2-digit'}), history: [] }; sessions.unshift(newSession); currentSessionId = newSession.id; saveSessions(); renderCurrentSessionChat(); } function switchSession(id) { currentSessionId = id; renderSessionsList(); renderCurrentSessionChat(); } function deleteSession(id, event) { event.stopPropagation(); sessions = sessions.filter(s => s.id !== id); if (sessions.length === 0) { createNewSession(); } else { if (currentSessionId === id) currentSessionId = sessions[0].id; saveSessions(); renderCurrentSessionChat(); } } function renderSessionsList() { const listEl = document.getElementById('historyList'); listEl.innerHTML = ''; sessions.forEach(s => { const item = document.createElement('div'); item.className = `history-item ${s.id === currentSessionId ? 'active' : ''}`; item.onclick = () => switchSession(s.id); item.innerHTML = ` <span class="title"><i class="fa-regular fa-message"></i> ${escapeHtml(s.title)}</span> <i class="fa-solid fa-trash delete-btn" onclick="deleteSession('${s.id}', event)" title="מחק שיחה"></i> `; listEl.appendChild(item); }); } function getCurrentSession() { return sessions.find(s => s.id === currentSessionId); } function renderCurrentSessionChat() { const container = document.getElementById('chatContainer'); container.innerHTML = ` <div class="message model"> שלום! אני עוזר ה-AI הבכיר שלך לבניית מרכזיות, שלוחות ו-Webhooks ב'ימות המשיח'.<br> העלה קבצים, שאל שאלות, או סנן תיקיות תיעוד ספציפיות לקבלת קובצי <code>ext.ini</code> מדויקים! </div>`; const session = getCurrentSession(); if (!session) return; session.history.forEach(msg => { if (msg.role === 'user') { appendMessageUI('user', msg.text, msg.attachedNames || [], false); } else if (msg.role === 'model') { renderModelResponseUI(msg.text); } }); scrollToBottom(false); } // --- Keys & Storage --- function loadKeys() { const saved = localStorage.getItem(LOCAL_STORAGE_KEYS.API_KEYS); if (saved) { apiKeys = JSON.parse(saved); document.getElementById('apiKeysInput').value = apiKeys.join('\n'); } } async function saveKeys() { const lines = document.getElementById('apiKeysInput').value.split('\n').map(k => k.trim()).filter(k => k.length > 0); apiKeys = lines; localStorage.setItem(LOCAL_STORAGE_KEYS.API_KEYS, JSON.stringify(apiKeys)); alert('מפתחות ה-API נשמרו בהצלחה!'); await fetchModels(); } function saveYemotCredentials() { localStorage.setItem(LOCAL_STORAGE_KEYS.YM_USER, document.getElementById('ymUsername').value); localStorage.setItem(LOCAL_STORAGE_KEYS.YM_PASS, document.getElementById('ymPassword').value); alert('פרטי החיבור נשמרו!'); } function loadYemotCredentials() { document.getElementById('ymUsername').value = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_USER) || ''; document.getElementById('ymPassword').value = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_PASS) || ''; } // --- API Rotation & Models --- async function makeGeminiApiCall(fetchFunction) { if (apiKeys.length === 0) { alert('אנא הוסף לפחות מפתח API אחד בלשונית ההגדרות.'); switchTab('settingsTab'); throw new Error('No API keys configured'); } let lastError = null; for (let i = 0; i < apiKeys.length; i++) { try { return await fetchFunction(apiKeys[i]); } catch (err) { console.warn(`Key ${i + 1} failed, trying next...`, err); lastError = err; } } throw new Error(`כל המפתחות נכשלו: ${lastError?.message}`); } async function fetchModels() { try { const data = await makeGeminiApiCall(async (key) => { const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`); return await res.json(); }); let models = (data.models || []) .filter(m => m.supportedGenerationMethods?.includes('generateContent')) .map(m => m.name.replace('models/', '')); if (!models.includes('gemini-3.6-flash')) { models.unshift('gemini-3.6-flash'); } const select = document.getElementById('modelSelect'); select.innerHTML = ''; models.forEach(m => { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; select.appendChild(opt); }); select.value = 'gemini-3.6-flash'; } catch(e) { console.error(e); const select = document.getElementById('modelSelect'); select.innerHTML = '<option value="gemini-3.6-flash">gemini-3.6-flash</option><option value="gemini-1.5-flash">gemini-1.5-flash</option>'; select.value = 'gemini-3.6-flash'; } } // --- Speech Input --- function initSpeechRecognition() { if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; recognition = new SpeechRecognition(); recognition.lang = 'he-IL'; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; const inputEl = document.getElementById('userInput'); inputEl.value += (inputEl.value ? ' ' : '') + transcript; toggleVoiceInput(); }; recognition.onerror = () => toggleVoiceInput(); } else { document.getElementById('micBtn').style.display = 'none'; } } function toggleVoiceInput() { if (!recognition) return; const micBtn = document.getElementById('micBtn'); if (!isRecording) { recognition.start(); isRecording = true; micBtn.classList.add('recording'); } else { recognition.stop(); isRecording = false; micBtn.classList.remove('recording'); } } // --- Message Dispatcher with Dynamic ZIP Folder Filtering --- async function sendMessage() { const inputEl = document.getElementById('userInput'); const text = inputEl.value.trim(); if (!text && pendingAttachments.length === 0) return; const sendBtn = document.getElementById('sendBtn'); const deepAnalysis = document.getElementById('deepAnalysisToggle').checked; const selectedModel = document.getElementById('modelSelect').value || 'gemini-3.6-flash'; const session = getCurrentSession(); if (session.history.length === 0) { session.title = text ? (text.substring(0, 25) + '...') : (pendingAttachments[0]?.name || 'קובץ מצורף'); } const currentTurnAttachments = [...pendingAttachments]; const attachedNames = currentTurnAttachments.map(a => a.name); appendMessageUI('user', text || '(קבצים מצורפים)', attachedNames, false); inputEl.value = ''; pendingAttachments = []; renderAttachmentsPreview(); sendBtn.disabled = true; scrollToBottom(); session.history.push({ role: 'user', text: text || '(קבצים מצורפים)', attachedNames: attachedNames }); saveSessions(); const loadingDiv = appendMessageUI('model', '<div class="loading-shimmer"><i class="fa-solid fa-spinner"></i> <span>אורז תיקיות נבחרות ומנתח תיעוד...</span></div>', [], true); scrollToBottom(); try { // Dynamically prepare filtered ZIP payload based on selected folders const effectiveZipBase64 = await getFilteredZipBase64(); let contentsPayload = []; session.history.forEach((msg, idx) => { let parts = []; // Inject filtered ZIP documentation context on the first user turn if (idx === 0 && msg.role === 'user' && effectiveZipBase64) { parts.push({ inline_data: { mime_type: "application/zip", data: effectiveZipBase64 } }); } if (idx === session.history.length - 1 && msg.role === 'user' && currentTurnAttachments.length > 0) { currentTurnAttachments.forEach(att => { parts.push({ inline_data: { mime_type: att.mimeType, data: att.data } }); }); } let promptText = msg.text; if (idx === session.history.length - 1 && msg.role === 'user' && deepAnalysis) { promptText = `[הוראת חובה לניתוח מעמיק של הפורמט]: לפני מתן התשובה והקוד הסופיים, עליך לבצע ניתוח מקיף ומעמיק של הדרישה, הפורמט המבוקש (כגון ext.ini, HTML, Webhook, תפריטי IVR), לוגיקת הרכיבים, בדיקת קצה ומבנה הנתונים. חובה לעטוף את *כל* הניתוח המעמיק בתוך תגיות <analysis> ו-</analysis>. רק לאחר התגית הסוגרת </analysis> תציג את התשובה והקוד המלאים והסופיים. בקשת המשתמש: ${msg.text}`; } parts.push({ text: promptText }); contentsPayload.push({ role: msg.role, parts: parts }); }); const responseData = await makeGeminiApiCall(async (key) => { const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_instruction: { parts: [{ text: SYSTEM_INSTRUCTION }] }, contents: contentsPayload }) }); if (!res.ok) { const errBody = await res.text(); throw new Error(`HTTP ${res.status}: ${errBody}`); } return await res.json(); }); const replyText = responseData?.candidates?.[0]?.content?.parts?.[0]?.text; if (!replyText) throw new Error('התקבלה תשובה ריקה מהמודל.'); session.history.push({ role: 'model', text: replyText }); saveSessions(); loadingDiv.remove(); renderModelResponseUI(replyText); scrollToBottom(); } catch (err) { loadingDiv.innerHTML = `<span style="color:#ef4444;"><i class="fa-solid fa-circle-exclamation"></i> שגיאה: ${err.message}</span>`; } finally { sendBtn.disabled = false; } } // Render AI Model Response function renderModelResponseUI(text) { const container = document.getElementById('chatContainer'); const msgDiv = document.createElement('div'); msgDiv.className = 'message model'; let analysisHTML = ''; let mainContent = text; const analysisMatch = text.match(/<analysis>([\s\S]*?)<\/analysis>/); if (analysisMatch) { const analysisText = analysisMatch[1].trim(); mainContent = text.replace(/<analysis>[\s\S]*?<\/analysis>/, '').trim(); analysisHTML = ` <div class="analysis-box"> <div class="analysis-header" onclick="this.parentElement.classList.toggle('open')"> <span><i class="fa-solid fa-brain"></i> ניתוח מעמיק של הפורמט והלוגיקה (לחץ לפתיחה)</span> <i class="fa-solid fa-chevron-down"></i> </div> <div class="analysis-content">${escapeHtml(analysisText)}</div> </div> `; } msgDiv.innerHTML = analysisHTML + marked.parse(mainContent); container.appendChild(msgDiv); // Enhance Code Blocks msgDiv.querySelectorAll('pre code').forEach((codeBlock) => { const rawCode = codeBlock.innerText; const parentPre = codeBlock.parentNode; const wrapper = document.createElement('div'); wrapper.className = 'code-container'; const header = document.createElement('div'); header.className = 'code-header'; header.innerHTML = `<span><i class="fa-solid fa-code"></i> קוד תצורה (ext.ini)</span>`; const actions = document.createElement('div'); actions.className = 'code-actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'code-btn'; copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> העתק'; copyBtn.onclick = () => { navigator.clipboard.writeText(rawCode); copyBtn.innerHTML = '<i class="fa-solid fa-check"></i> הועתק!'; setTimeout(() => copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> העתק', 2000); }; const dlBtn = document.createElement('button'); dlBtn.className = 'code-btn'; dlBtn.innerHTML = '<i class="fa-solid fa-download"></i> הורד ext.ini'; dlBtn.onclick = () => downloadCodeFile(rawCode, 'ext.ini'); const uploadBtn = document.createElement('button'); uploadBtn.className = 'code-btn'; uploadBtn.style.color = '#10b981'; uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> העלה למרכזיה'; uploadBtn.onclick = () => uploadDirectToYemot(rawCode); const geminiTtsBtn = document.createElement('button'); geminiTtsBtn.className = 'code-btn'; geminiTtsBtn.style.color = '#38bdf8'; geminiTtsBtn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> שמע Gemini AI'; geminiTtsBtn.onclick = () => generateGeminiAudio(rawCode); actions.appendChild(copyBtn); actions.appendChild(dlBtn); actions.appendChild(geminiTtsBtn); actions.appendChild(uploadBtn); header.appendChild(actions); wrapper.appendChild(header); parentPre.parentNode.insertBefore(wrapper, parentPre); wrapper.appendChild(parentPre); hljs.highlightElement(codeBlock); }); } // Audio TTS async function generateGeminiAudio(code) { let textToRead = prompt('הכנס את הטקסט שברצונך שה-Gemini AI יכין ויקריא עבור המרכזייה:', 'ברוכים הבאים למערכת שלנו'); if (!textToRead) return; const selectedModel = document.getElementById('modelSelect').value || 'gemini-3.6-flash'; try { const responseData = await makeGeminiApiCall(async (key) => { const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `עבד והכשר את הניסוח הבא לקריאה קולית נקייה ומדויקת בלבד במרכזיה (ללא סימני פיסוק או הערות נוספות):\n"${textToRead}"` }] }] }) }); return await res.json(); }); const speechText = responseData?.candidates?.[0]?.content?.parts?.[0]?.text || textToRead; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(speechText); utterance.lang = 'he-IL'; utterance.rate = 0.95; window.speechSynthesis.speak(utterance); } catch (err) { window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(textToRead); utterance.lang = 'he-IL'; window.speechSynthesis.speak(utterance); } } // Direct Yemot Push async function uploadDirectToYemot(code) { const username = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_USER); const password = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_PASS); if (!username || !password) { alert('אנא הגדר פרטי התחברות לימות המשיח בלשונית ההגדרות.'); switchTab('settingsTab'); return; } const path = prompt('הכנס את נתיב השלוחה להעלאה (לדוגמה: 1/2 או 0 לראשית):', '1'); if (!path) return; try { const loginRes = await fetch(`https://www.call2all.co.il/ym/api/Login?username=${username}&password=${password}`); const loginData = await loginRes.json(); if (loginData.responseStatus !== 'OK') { throw new Error('התחברות למערכת ימות המשיח נכשלה.'); } const token = loginData.token; const formData = new FormData(); const blob = new Blob([code], { type: 'text/plain' }); formData.append('file', blob, 'ext.ini'); const uploadRes = await fetch(`https://www.call2all.co.il/ym/api/UploadFile?token=${token}&path=ivr2:${path}/ext.ini`, { method: 'POST', body: formData }); const uploadData = await uploadRes.json(); if (uploadData.responseStatus === 'OK') { alert(`הקוד הועלה בהצלחה לשלוחה ${path}!`); } else { alert(`שגיאה: ${uploadData.message || 'תקלה לא ידועה'}`); } } catch (err) { alert(`שגיאה: ${err.message}`); } } function downloadCodeFile(code, filename) { const blob = new Blob([code], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); } function appendMessageUI(role, content, attachedNames = [], isHTML = false) { const container = document.getElementById('chatContainer'); const msgDiv = document.createElement('div'); msgDiv.className = `message ${role}`; let attachmentsHTML = ''; if (attachedNames && attachedNames.length > 0) { attachmentsHTML = '<div class="message-attachments">' + attachedNames.map(n => `<span class="msg-att-chip"><i class="fa-solid fa-paperclip"></i> ${escapeHtml(n)}</span>`).join('') + '</div>'; } const bodyContent = isHTML ? content : `<div>${escapeHtml(content)}</div>`; msgDiv.innerHTML = attachmentsHTML + bodyContent; container.appendChild(msgDiv); return msgDiv; } function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } } function escapeHtml(text) { return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } </script> </body> </html>כמו כן ניתן להוריד את הקובץ יש להוסיף את האות l בסוף הסיומת
מומחה לימות.htmכרגע לא עובד זה בטיפול גירסא מתקדמת שאמורה לעבוד גם לחסומים !!!
בגירסא זו כל התקשורת מתבצעת דרך גוגל סקריפט שיש לפתוח עפ"י ההוראות שתראו בדף עצמו,
את הסקירפט יש לפרוס באופן חד פעמי במחשב שאינו נטפריקוד הדף בספוילר
<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>מומחה AI ימות המשיח - סביבת פיתוח מתקדמת (GAS Proxy)</title> <!-- Fonts & Icons --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> <!-- JSZip for dynamic zip extraction and folder repackaging --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <!-- Marked.js for Markdown --> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Highlight.js for Syntax Highlighting --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/tokyo-night-dark.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/ini.min.js"></script> <style> /* Theme Palettes */ :root, body[data-theme="cyberpunk"] { --bg-dark: #070913; --bg-gradient: radial-gradient(circle at 50% -20%, #1e1b4b 0%, #0c1022 50%, #070913 100%); --panel-bg: rgba(18, 24, 43, 0.7); --panel-border: rgba(99, 102, 241, 0.18); --panel-border-hover: rgba(99, 102, 241, 0.4); --user-msg: linear-gradient(135deg, #2563eb, #1d4ed8); --ai-msg: rgba(26, 34, 56, 0.75); --accent-glow: #38bdf8; --accent-blue: #3b82f6; --accent-purple: #818cf8; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.2); --text-main: #f8fafc; --text-muted: #94a3b8; --code-bg: #0d1322; --analysis-bg: rgba(28, 25, 68, 0.7); --analysis-border: #6366f1; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.35); } body[data-theme="obsidian"] { --bg-dark: #000000; --bg-gradient: radial-gradient(circle at 50% 0%, #141414 0%, #000000 80%); --panel-bg: rgba(18, 18, 18, 0.85); --panel-border: #262626; --panel-border-hover: #404040; --user-msg: linear-gradient(135deg, #27272a, #18181b); --ai-msg: rgba(20, 20, 20, 0.85); --accent-glow: #10b981; --accent-blue: #10b981; --accent-purple: #059669; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.15); --text-main: #ffffff; --text-muted: #a3a3a3; --code-bg: #080808; --analysis-bg: #121212; --analysis-border: #2e2e2e; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.7); } body[data-theme="espresso"] { --bg-dark: #140d08; --bg-gradient: radial-gradient(circle at 50% -10%, #382417 0%, #1c120b 60%, #140d08 100%); --panel-bg: rgba(38, 25, 17, 0.8); --panel-border: rgba(217, 119, 6, 0.2); --panel-border-hover: rgba(217, 119, 6, 0.45); --user-msg: linear-gradient(135deg, #b45309, #78350f); --ai-msg: rgba(48, 31, 22, 0.8); --accent-glow: #fcd34d; --accent-blue: #d97706; --accent-purple: #b45309; --success: #10b981; --success-glow: rgba(16, 185, 129, 0.2); --text-main: #fffbeb; --text-muted: #d6b89e; --code-bg: #1b1008; --analysis-bg: rgba(69, 42, 28, 0.7); --analysis-border: #d97706; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.5); } body[data-theme="daylight"] { --bg-dark: #f8fafc; --bg-gradient: radial-gradient(circle at 50% -20%, #e0e7ff 0%, #f1f5f9 60%, #f8fafc 100%); --panel-bg: rgba(255, 255, 255, 0.85); --panel-border: #cbd5e1; --panel-border-hover: #94a3b8; --user-msg: linear-gradient(135deg, #2563eb, #1d4ed8); --ai-msg: rgba(241, 245, 249, 0.9); --accent-glow: #2563eb; --accent-blue: #2563eb; --accent-purple: #4f46e5; --success: #059669; --success-glow: rgba(5, 150, 105, 0.15); --text-main: #0f172a; --text-muted: #64748b; --code-bg: #ffffff; --analysis-bg: #eef2ff; --analysis-border: #818cf8; --shadow-subtle: 0 10px 30px rgba(0, 0, 0, 0.06); } /* Font Sizes */ body[data-font="small"] { font-size: 14px; } body[data-font="medium"] { font-size: 15.5px; } body[data-font="large"] { font-size: 17.5px; } body[data-font="huge"] { font-size: 19.5px; } * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Rubik', sans-serif; } body { background-color: var(--bg-dark); color: var(--text-main); height: 100vh; display: flex; flex-direction: column; overflow: hidden; background-image: var(--bg-gradient); transition: background 0.3s ease, color 0.3s ease; } /* Custom Scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); } /* Navigation Header */ header { background: var(--panel-bg); backdrop-filter: blur(20px); border-bottom: 1px solid var(--panel-border); padding: 10px 24px; display: flex; justify-content: space-between; align-items: center; z-index: 20; box-shadow: var(--shadow-subtle); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .logo-area { display: flex; align-items: center; gap: 16px; } .logo { font-size: 1.35rem; font-weight: 800; background: linear-gradient(135deg, var(--accent-glow), var(--accent-purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: flex; align-items: center; gap: 10px; letter-spacing: -0.5px; } /* Topbar Status Badge */ .topbar-status-badge { display: inline-flex; align-items: center; gap: 7px; padding: 4px 12px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; background: rgba(255,255,255,0.04); border: 1px solid var(--panel-border); color: var(--text-muted); } .topbar-status-badge.active { background: var(--success-glow); border-color: rgba(16, 185, 129, 0.4); color: var(--success); } .status-dot { width: 7px; height: 7px; border-radius: 50%; background: #64748b; } .topbar-status-badge.active .status-dot { background: var(--success); box-shadow: 0 0 8px var(--success); animation: pulse-dot 2s infinite; } @keyframes pulse-dot { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.7; } } .header-actions { display: flex; align-items: center; gap: 10px; } .tabs { display: flex; gap: 8px; } .tab-btn { background: rgba(255,255,255,0.03); border: 1px solid var(--panel-border); color: var(--text-main); padding: 7px 16px; border-radius: 10px; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 600; display: flex; align-items: center; gap: 8px; } .tab-btn.active { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border-color: transparent; color: #fff; box-shadow: 0 4px 15px rgba(59, 130, 246, 0.35); } .tab-btn:hover:not(.active) { background: rgba(255,255,255,0.08); border-color: var(--panel-border-hover); } /* Workspace Grid */ main { flex: 1; overflow: hidden; position: relative; } .tab-content { display: none; height: 100%; padding: 16px 20px; } .tab-content.active { display: flex; gap: 16px; } /* Sidebar Sessions */ .history-sidebar { width: 280px; background: var(--panel-bg); backdrop-filter: blur(20px); border: 1px solid var(--panel-border); border-radius: 18px; padding: 16px; display: flex; flex-direction: column; gap: 12px; box-shadow: var(--shadow-subtle); transition: all 0.3s ease; } .new-chat-btn { width: 100%; background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border: none; color: white; padding: 11px 16px; border-radius: 12px; cursor: pointer; font-weight: 700; font-size: 0.92rem; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s ease; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); } .new-chat-btn:hover { opacity: 0.95; transform: translateY(-1.5px); box-shadow: 0 6px 18px rgba(59, 130, 246, 0.45); } .history-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-left: 4px; } .history-item { background: rgba(255,255,255,0.02); border: 1px solid transparent; padding: 9px 12px; border-radius: 10px; cursor: pointer; transition: all 0.2s; display: flex; justify-content: space-between; align-items: center; font-size: 0.86rem; color: var(--text-muted); } .history-item:hover, .history-item.active { background: rgba(99, 102, 241, 0.12); border-color: rgba(99, 102, 241, 0.3); color: var(--text-main); } .history-item .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } .history-item .delete-btn { opacity: 0; transition: opacity 0.2s; color: #ef4444; padding: 2px 6px; } .history-item:hover .delete-btn { opacity: 1; } /* Main Chat Window */ .chat-main { flex: 1; display: flex; flex-direction: column; height: 100%; position: relative; } .chat-container { flex: 1; overflow-y: auto; padding: 10px 10px 30px 10px; display: flex; flex-direction: column; gap: 18px; scroll-behavior: smooth; } .message { max-width: 85%; padding: 16px 22px; border-radius: 18px; line-height: 1.7; word-wrap: break-word; box-shadow: var(--shadow-subtle); backdrop-filter: blur(14px); animation: messageIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); } @keyframes messageIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .message.user { align-self: flex-start; background: var(--user-msg); color: white; border-bottom-right-radius: 3px; } .message.model { align-self: flex-end; background: var(--ai-msg); border-bottom-left-radius: 3px; border: 1px solid var(--panel-border); } .message-attachments { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; } .msg-att-chip { background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.15); padding: 3px 10px; border-radius: 6px; font-size: 0.78rem; display: flex; align-items: center; gap: 6px; } /* Loading UI Animation */ .loading-shimmer { display: inline-flex; align-items: center; gap: 10px; font-weight: 500; color: var(--accent-glow); } .loading-shimmer i { font-size: 1.1rem; animation: spin 1s linear infinite; } @keyframes spin { 100% { transform: rotate(360deg); } } /* Analysis Component */ .analysis-box { background: var(--analysis-bg); border: 1px solid var(--analysis-border); border-radius: 14px; margin-bottom: 14px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15); } .analysis-header { padding: 11px 16px; background: rgba(99, 102, 241, 0.22); cursor: pointer; font-weight: 700; font-size: 0.9rem; color: var(--accent-glow); display: flex; justify-content: space-between; align-items: center; user-select: none; } .analysis-content { padding: 16px; font-size: 0.9rem; color: var(--text-main); display: none; border-top: 1px solid var(--analysis-border); white-space: pre-wrap; line-height: 1.6; } .analysis-box.open .analysis-content { display: block; } /* Attachments Bar */ .attachments-preview { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 4px; } .att-chip { background: rgba(56, 189, 248, 0.12); border: 1px solid var(--accent-glow); padding: 5px 12px; border-radius: 8px; font-size: 0.82rem; display: flex; align-items: center; gap: 8px; color: var(--text-main); } .att-chip .remove-att { cursor: pointer; color: #ef4444; font-weight: bold; } /* Input Controls Area */ .input-area { background: var(--panel-bg); backdrop-filter: blur(20px); padding: 14px 18px; border: 1px solid var(--panel-border); border-radius: 18px; display: flex; flex-direction: column; gap: 10px; margin-top: 8px; box-shadow: var(--shadow-subtle); position: relative; } .controls-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: var(--text-muted); flex-wrap: wrap; gap: 10px; } .input-wrapper { display: flex; gap: 8px; align-items: center; } textarea { flex: 1; background: rgba(0, 0, 0, 0.25); border: 1px solid var(--panel-border); color: var(--text-main); padding: 12px 16px; border-radius: 14px; resize: none; height: 60px; outline: none; font-size: 0.98rem; transition: all 0.2s ease; } textarea:focus { border-color: var(--accent-glow); box-shadow: 0 0 12px rgba(56, 189, 248, 0.2); } .btn-action { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); border: none; color: white; padding: 0 22px; height: 60px; border-radius: 14px; cursor: pointer; font-weight: 700; font-size: 1rem; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); } .btn-action:hover { opacity: 0.95; transform: translateY(-1.5px); box-shadow: 0 6px 20px rgba(59, 130, 246, 0.45); } .btn-action:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; } .btn-icon-ctl { background: rgba(255,255,255,0.03); border: 1px solid var(--panel-border); color: var(--text-main); width: 60px; height: 60px; border-radius: 14px; cursor: pointer; font-size: 1.2rem; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .btn-icon-ctl:hover { background: rgba(255,255,255,0.08); border-color: var(--accent-glow); transform: translateY(-1px); } .btn-icon-ctl.recording { background: #ef4444; color: white; animation: pulse 1.2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 12px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } /* Fast In-Chat Folder Selector Popover & Checkboxes */ .chat-folder-picker-wrapper { position: relative; display: inline-block; } .chat-folder-btn { background: rgba(255, 255, 255, 0.04); border: 1px solid var(--panel-border); color: var(--text-main); padding: 5px 12px; border-radius: 8px; cursor: pointer; font-size: 0.82rem; font-weight: 600; display: flex; align-items: center; gap: 7px; transition: all 0.2s ease; } .chat-folder-btn:hover { border-color: var(--accent-glow); background: rgba(56, 189, 248, 0.1); } .chat-folder-popover { position: absolute; bottom: calc(100% + 8px); right: 0; width: 320px; max-height: 280px; background: var(--panel-bg); backdrop-filter: blur(25px); border: 1px solid var(--panel-border); border-radius: 14px; padding: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 100; display: none; flex-direction: column; gap: 10px; } .chat-folder-popover.open { display: flex; } .chat-folder-popover-header { display: flex; justify-content: space-between; align-items: center; font-size: 0.82rem; font-weight: 700; color: var(--accent-glow); border-bottom: 1px dashed var(--panel-border); padding-bottom: 6px; } .chat-folder-grid { display: flex; flex-direction: column; gap: 6px; max-height: 200px; overflow-y: auto; padding-left: 4px; } /* Code Block Styling */ .code-container { position: relative; margin: 14px 0; border-radius: 12px; overflow: hidden; border: 1px solid var(--panel-border); background: var(--code-bg); box-shadow: 0 6px 20px rgba(0,0,0,0.3); } .code-header { background: rgba(255,255,255,0.03); padding: 9px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--panel-border); font-size: 0.82rem; color: var(--text-muted); } .code-actions { display: flex; gap: 6px; } .code-btn { background: rgba(255,255,255,0.05); border: 1px solid var(--panel-border); color: var(--text-main); padding: 4px 10px; border-radius: 6px; font-size: 0.78rem; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 5px; font-weight: 500; } .code-btn:hover { background: var(--accent-blue); color: white; border-color: transparent; } pre { margin: 0 !important; } pre code { font-family: 'JetBrains Mono', monospace !important; font-size: 0.92rem; padding: 16px !important; line-height: 1.55; } /* Settings Panels */ .settings-card { background: var(--panel-bg); backdrop-filter: blur(20px); padding: 24px; border-radius: 18px; border: 1px solid var(--panel-border); margin-bottom: 18px; width: 100%; max-width: 820px; box-shadow: var(--shadow-subtle); } .settings-card h3 { margin-bottom: 14px; color: var(--accent-glow); display: flex; align-items: center; gap: 10px; font-size: 1.15rem; font-weight: 700; } .input-field { width: 100%; padding: 11px 16px; background: rgba(0, 0, 0, 0.25); border: 1px solid var(--panel-border); color: var(--text-main); border-radius: 10px; margin-bottom: 12px; font-size: 0.95rem; outline: none; transition: 0.2s; } .input-field:focus { border-color: var(--accent-glow); } textarea.keys-input { height: 100px; font-family: 'JetBrains Mono', monospace; direction: ltr; } select { background: var(--panel-bg); color: var(--text-main); border: 1px solid var(--panel-border); padding: 6px 12px; border-radius: 8px; outline: none; font-weight: 500; } /* ZIP Management & Dynamic Folder Selector */ .zip-status-container { border: 1px dashed var(--panel-border); border-radius: 14px; padding: 20px; background: rgba(0,0,0,0.15); display: flex; flex-direction: column; gap: 14px; transition: all 0.2s ease; } .zip-status-container.has-file { border-style: solid; border-color: rgba(16, 185, 129, 0.4); background: var(--success-glow); } .zip-info-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } .zip-info-details { display: flex; align-items: center; gap: 12px; } .zip-icon { font-size: 2rem; color: var(--success); } .zip-info-text .title { font-weight: 700; font-size: 0.98rem; } .zip-info-text .sub { font-size: 0.82rem; color: var(--text-muted); } .zip-actions { display: flex; gap: 8px; } /* Folder Picker Grid in Settings */ .folder-picker-section { border-top: 1px solid var(--panel-border); padding-top: 14px; } .folder-picker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .folder-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; max-height: 180px; overflow-y: auto; padding: 4px; } .folder-checkbox-item { background: rgba(0,0,0,0.25); border: 1px solid var(--panel-border); padding: 7px 12px; border-radius: 8px; display: flex; align-items: center; gap: 8px; font-size: 0.86rem; cursor: pointer; user-select: none; transition: 0.2s; } .folder-checkbox-item:hover { border-color: var(--accent-glow); background: rgba(56, 189, 248, 0.08); } .folder-checkbox-item input { cursor: pointer; } /* Zen Mode */ body.zen-mode header { transform: translateY(-100%); height: 0; padding: 0; overflow: hidden; } body.zen-mode .history-sidebar { display: none; } body.zen-mode .tab-content { padding: 10px 14%; } .exit-zen-btn { display: none; position: fixed; top: 16px; left: 16px; background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; padding: 9px 16px; border-radius: 10px; cursor: pointer; z-index: 1000; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.5); } body.zen-mode .exit-zen-btn { display: flex; align-items: center; gap: 6px; } /* Jump Button */ .jump-bottom-btn { position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%) translateY(20px); background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; padding: 8px 18px; border-radius: 20px; cursor: pointer; font-size: 0.84rem; font-weight: bold; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4); display: flex; align-items: center; gap: 8px; opacity: 0; pointer-events: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); z-index: 50; } .jump-bottom-btn.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); } .jump-bottom-btn:hover { transform: translateX(-50%) scale(1.05); } .code-snippet-box { background: #080808; border: 1px solid var(--panel-border); border-radius: 10px; padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; color: #38bdf8; white-space: pre; overflow-x: auto; direction: ltr; margin: 10px 0; } </style> </head> <body data-theme="cyberpunk" data-font="medium"> <!-- Exit Zen Mode Button --> <button class="exit-zen-btn" onclick="toggleZenMode()"><i class="fa-solid fa-compress"></i> צא ממצב פוקוס</button> <header> <div class="logo-area"> <div class="logo"> <i class="fa-solid fa-headset"></i> <span>מומחה AI ימות המשיח</span> </div> <!-- Live Topbar Context Indicator --> <div class="topbar-status-badge" id="topbarZipBadge" title="סטטוס קובץ תיעוד ימות המשיח"> <div class="status-dot"></div> <span id="topbarZipText">בודק תיעוד...</span> </div> </div> <div class="header-actions"> <button class="tab-btn" onclick="toggleZenMode()" title="מצב פוקוס נקי (Zen Mode)"><i class="fa-solid fa-expand"></i> פוקוס</button> <div class="tabs"> <button class="tab-btn active" onclick="switchTab('chatTab', event)"><i class="fa-solid fa-comments"></i> צ'אט וסביבת עבודה</button> <button class="tab-btn" id="settingsTabBtn" onclick="switchTab('settingsTab', event)"><i class="fa-solid fa-sliders"></i> הגדרות וגוגל סקריפט</button> </div> </div> </header> <main> <!-- Chat Tab with History Sidebar --> <div id="chatTab" class="tab-content active"> <div class="history-sidebar"> <button class="new-chat-btn" onclick="createNewSession()"><i class="fa-solid fa-plus"></i> שיחה חדשה</button> <div style="font-size: 0.78rem; color: var(--text-muted); font-weight: 700; margin-top: 6px; letter-spacing: 0.5px;">שיחות אחרונות</div> <div class="history-list" id="historyList"></div> </div> <div class="chat-main"> <div class="chat-container" id="chatContainer" onscroll="handleChatScroll()"> <div class="message model"> שלום! אני עוזר ה-AI הבכיר שלך לבניית מרכזיות, שלוחות ו-Webhooks ב'ימות המשיח'.<br> התקשורת מנותבת בבטחה דרך **Google Apps Script Proxy** כאשר <b>כל ההגדרות, המפתחות והתיעוד נשמרים בדפדפן שלך בלבד</b>! </div> </div> <button class="jump-bottom-btn" id="jumpBottomBtn" onclick="scrollToBottom(true)"> <i class="fa-solid fa-arrow-down"></i> קפוץ לקוד האחרון / למטה </button> <div class="input-area"> <div class="attachments-preview" id="attachmentsPreview"></div> <div class="controls-row"> <div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> <div> <i class="fa-solid fa-robot"></i> מודל: <select id="modelSelect"></select> </div> <!-- Fast In-Chat Folder Scope Selector with Checkboxes Popover --> <div id="chatFolderScopeContainer" class="chat-folder-picker-wrapper" style="display: none;"> <button class="chat-folder-btn" onclick="toggleChatFolderPopover(event)"> <i class="fa-solid fa-folder-tree" style="color: var(--accent-glow);"></i> <span>תיקיות לשליחה (<b id="chatFolderCountBadge">0</b>)</span> <i class="fa-solid fa-chevron-up" style="font-size: 0.75rem;"></i> </button> <div class="chat-folder-popover" id="chatFolderPopover" onclick="event.stopPropagation()"> <div class="chat-folder-popover-header"> <span><i class="fa-solid fa-check-double"></i> בחירת תיקיות בודדות לשליחה</span> <div style="display: flex; gap: 6px;"> <button class="tab-btn" style="padding: 2px 6px; font-size: 0.72rem;" onclick="selectAllFolders(true)">הכל</button> <button class="tab-btn" style="padding: 2px 6px; font-size: 0.72rem;" onclick="selectAllFolders(false)">נקה</button> </div> </div> <div class="chat-folder-grid" id="chatFolderCheckboxesGrid"></div> </div> </div> </div> <div> <label style="cursor: pointer;"> <input type="checkbox" id="deepAnalysisToggle" checked> דרוש ניתוח מעמיק (<analysis>) </label> </div> </div> <div class="input-wrapper"> <input type="file" id="genericFileInput" multiple style="display: none;" onchange="handleGenericFileUpload(event)"> <button class="btn-icon-ctl" onclick="document.getElementById('genericFileInput').click()" title="צרף קבצים (קוד, תמונות, שמע, PDF, ZIP...)"> <i class="fa-solid fa-paperclip"></i> </button> <button class="btn-icon-ctl" id="micBtn" onclick="toggleVoiceInput()" title="הקלטה קולית בעברית"> <i class="fa-solid fa-microphone"></i> </button> <textarea id="userInput" placeholder="תאר את השלוחה או שאל שאלה (ניתן לגרור קבצים לכאן)..." onkeydown="handleKeyDown(event)"></textarea> <button class="btn-action" id="sendBtn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i> שלח</button> </div> </div> </div> </div> <!-- Settings Tab --> <div id="settingsTab" class="tab-content" style="flex-direction: column; overflow-y: auto;"> <!-- Google Apps Script Proxy Setting --> <div class="settings-card"> <h3><i class="fa-brands fa-google"></i> הזרמת בקשות דרך Google Apps Script Proxy</h3> <p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 12px;">הכנס את כתובת ה-Web App של Google Script שמשמש כצינור תיווך בלבד (כל ההגדרות והמפתחות נשארים בדפדפן):</p> <input type="url" id="gasUrlInput" class="input-field" placeholder="https://script.google.com/macros/s/AKfycb.../exec" dir="ltr"> <button class="tab-btn active" onclick="saveGasUrl()"><i class="fa-solid fa-floppy-disk"></i> שמור כתובת סקריפט גוגל</button> <div style="margin-top: 16px; border-top: 1px dashed var(--panel-border); padding-top: 12px;"> <div style="font-weight: 700; font-size: 0.9rem; margin-bottom: 6px; color: var(--accent-glow);"> <i class="fa-solid fa-code"></i> קוד ה-Google Apps Script (העתק והדבק ב-Script Editor): </div> <p style="font-size: 0.82rem; color: var(--text-muted);">הקוד הבא הינו תיווך נקי. צור פרויקט ב-Google Apps Script, הדבק את הקוד, ובצע <b>Deploy -> New deployment -> Web App</b> (תחת Access שנה ל-<b>Anyone</b>):</p> <div class="code-snippet-box" id="gasScriptSnippet">function doPost(e) { try { var data = JSON.parse(e.postData.contents); var options = { method: data.method || 'post', contentType: 'application/json', muteHttpExceptions: true }; if (data.payload) { options.payload = typeof data.payload === 'string' ? data.payload : JSON.stringify(data.payload); } var response = UrlFetchApp.fetch(data.url, options); return ContentService.createTextOutput(response.getContentText()) .setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService.createTextOutput(JSON.stringify({ error: err.toString() })) .setMimeType(ContentService.MimeType.JSON); } } function doGet(e) { if (e.parameter && e.parameter.url) { var response = UrlFetchApp.fetch(e.parameter.url, { muteHttpExceptions: true }); return ContentService.createTextOutput(response.getContentText()) .setMimeType(ContentService.MimeType.JSON); } return ContentService.createTextOutput(JSON.stringify({ status: "active", message: "Yemot GAS Proxy Active" })) .setMimeType(ContentService.MimeType.JSON); }</div> <button class="code-btn" onclick="copyGasScriptCode()"><i class="fa-solid fa-copy"></i> העתק קוד סקריפט גוגל</button> </div> </div> <div class="settings-card"> <h3><i class="fa-solid fa-key"></i> שרשור מפתחות API בדפדפן (Gemini Key Rotation)</h3> <p style="font-size: 0.84rem; color: var(--text-muted); margin-bottom: 8px;">המפתחות נשמרים ב-LocalStorage של הדפדפן שלך ונשלחים דרך הסקריפט המתווך בלבד:</p> <textarea id="apiKeysInput" class="input-field keys-input" placeholder="הכנס מפתחות API (מפתח בכל שורה)..."></textarea> <button class="tab-btn active" onclick="saveKeys()"><i class="fa-solid fa-floppy-disk"></i> שמור מפתחות בדפדפן</button> </div> <!-- Dynamic ZIP & Folder Management Card --> <div class="settings-card"> <h3><i class="fa-solid fa-file-zipper"></i> תיעוד ימות המשיח ובחירת תיקיות בדפדפן</h3> <p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 14px;">קובץ ה-ZIP של התיעוד מנותח אוטומטית בדפדפן. תוכל לבחור לשלוח את כל התיעוד או רק תיקיות ספציפיות לקבלת מענה ממוקד ומהיר.</p> <input type="file" id="zipFileInput" accept=".zip" style="display: none;" onchange="handleZipUpload(event)"> <div class="zip-status-container" id="zipStatusBox"></div> </div> <div class="settings-card"> <h3><i class="fa-solid fa-palette"></i> התאמת מראה וסביבת עבודה (Themes & UI)</h3> <div style="display: flex; gap: 16px; flex-wrap: wrap;"> <div style="flex: 1;"> <label style="display: block; margin-bottom: 6px; color: var(--text-muted); font-size: 0.88rem;">ערכת נושא (Theme):</label> <select id="themeSelect" class="input-field" onchange="changeTheme(this.value)"> <option value="cyberpunk">🌌 Midnight Cyberpunk (ניאון כהה)</option> <option value="obsidian">🖤 Obsidian Pure Black (שחור מוחלט)</option> <option value="espresso">☕ Warm Espresso (חום קפה חמים)</option> <option value="daylight">☀️ Clean Daylight (מצב יום בהיר)</option> </select> </div> <div style="flex: 1;"> <label style="display: block; margin-bottom: 6px; color: var(--text-muted); font-size: 0.88rem;">גודל גופן (Font Size):</label> <select id="fontSelect" class="input-field" onchange="changeFontSize(this.value)"> <option value="small">קטן (14px)</option> <option value="medium" selected>בינוני (15.5px)</option> <option value="large">גדול (17.5px)</option> <option value="huge">ענק (19.5px)</option> </select> </div> </div> </div> <div class="settings-card"> <h3><i class="fa-solid fa-cloud-arrow-up"></i> חיבור ישיר ל-API ימות המשיח (בדפדפן)</h3> <input type="text" id="ymUsername" class="input-field" placeholder="מספר מערכת (System Username)"> <input type="password" id="ymPassword" class="input-field" placeholder="סיסמת מערכת (System Password)"> <button class="tab-btn active" onclick="saveYemotCredentials()"><i class="fa-solid fa-link"></i> שמור פרטי התחברות למערכת</button> </div> </div> </main> <script> const SYSTEM_INSTRUCTION = `אתה מתכנת בכיר ומומחה לשלוחות, מרכזיות ופיתוח ב'ימות המשיח'. תפקידך לבנות קובצי ext.ini מדויקים, תפריטי IVR, ושרתי Webhook. לרשותך מצורף קובץ ZIP המכיל את התיעוד וההגדרות של ימות המשיח (בהתאם לתיקיות שנבחרו), לצד כל קובץ שהמשתמש מעלה ומצרף. עליך להתבסס על התיעוד באופן מוחלט בכל תשובה. הנחיות חובה: 1. אם המשתמש ביקש ניתוח מעמיק (או כברירת מחדל), עליך לפתוח את תגובתך בניתוח מקיף ומעמיק של הדרישה, הפורמט, הלוגיקה, והפרמטרים הנדרשים עטוף כולו בתוך תגיות <analysis> ו-</analysis>. 2. רק לאחר התגית הסוגרת </analysis> תציג את התשובה והקוד המלאים והסופיים. 3. הקפד על כתיבת קוד ext.ini תקני, כולל הערות והסברים ברורים בעברית.`; const LOCAL_STORAGE_KEYS = { GAS_URL: 'yemot_v8_gas_script_url', API_KEYS: 'yemot_v8_gemini_api_keys', ZIP_BASE64: 'yemot_v8_zip_base64', ZIP_META: 'yemot_v8_zip_meta', ZIP_FOLDERS: 'yemot_v8_zip_folders', SELECTED_FOLDERS: 'yemot_v8_selected_folders', SESSIONS: 'yemot_v8_chat_sessions', YM_USER: 'yemot_v8_ym_username', YM_PASS: 'yemot_v8_ym_password', THEME: 'yemot_v8_theme', FONT_SIZE: 'yemot_v8_font_size' }; // App State (All maintained in browser) let gasProxyUrl = localStorage.getItem(LOCAL_STORAGE_KEYS.GAS_URL) || ''; let apiKeys = []; let zipBase64 = localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_BASE64) || null; let zipMeta = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_META) || 'null'); let zipFolders = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS) || '[]'); let selectedFolders = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS) || '[]'); let sessions = []; let currentSessionId = null; let isRecording = false; let recognition = null; let pendingAttachments = []; window.onload = async () => { loadThemePreferences(); loadGasUrl(); loadKeys(); loadYemotCredentials(); initSpeechRecognition(); initDragAndDrop(); loadSessions(); updateZipStatusUI(); populateFallbackModels(); if (gasProxyUrl && apiKeys.length > 0) { await fetchModels(); } // Close folder popover when clicking outside document.addEventListener('click', () => { const popover = document.getElementById('chatFolderPopover'); if (popover) popover.classList.remove('open'); }); }; function switchTab(tabId, event) { document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); document.getElementById(tabId).classList.add('active'); if(event) event.currentTarget.classList.add('active'); } // --- GAS Proxy & Storage --- function loadGasUrl() { gasProxyUrl = localStorage.getItem(LOCAL_STORAGE_KEYS.GAS_URL) || ''; document.getElementById('gasUrlInput').value = gasProxyUrl; } function saveGasUrl() { const url = document.getElementById('gasUrlInput').value.trim(); gasProxyUrl = url; localStorage.setItem(LOCAL_STORAGE_KEYS.GAS_URL, url); alert('כתובת ה-Google Script נשמרה בדפדפן בהצלחה!'); if (gasProxyUrl && apiKeys.length > 0) fetchModels(); } function copyGasScriptCode() { const code = document.getElementById('gasScriptSnippet').innerText; navigator.clipboard.writeText(code); alert('קוד הסקריפט הועתק ללוח!'); } // --- Generic Relay Call via GAS Proxy with Robust Error Parsing --- async function fetchViaGasProxy(targetUrl, method = 'POST', payloadData = null) { if (!gasProxyUrl) { throw new Error('MISSING_GAS_URL'); } const proxyBody = { url: targetUrl, method: method, payload: payloadData }; const res = await fetch(gasProxyUrl, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify(proxyBody) }); if (!res.ok) { throw new Error(`Google Script Proxy returned status ${res.status}`); } const textResponse = await res.text(); let parsed; try { parsed = JSON.parse(textResponse); } catch(e) { throw new Error(`תגובה לא חוקית מ-Google Script. וודא שביצעת Deploy כ-Web App עם הרשאת Anyone.\nתוכן התגובה: ${textResponse.substring(0, 150)}...`); } return parsed; } // --- Key Rotation Engine in Client --- async function makeGeminiApiCall(targetUrlFactory, method = 'POST', payloadFactory = null) { if (!gasProxyUrl) { throw new Error('MISSING_GAS_URL'); } if (apiKeys.length === 0) { throw new Error('MISSING_KEYS'); } let lastError = null; for (let i = 0; i < apiKeys.length; i++) { const key = apiKeys[i]; const targetUrl = targetUrlFactory(key); const payload = payloadFactory ? payloadFactory(key) : null; try { const data = await fetchViaGasProxy(targetUrl, method, payload); if (data.error) { throw new Error(data.error.message || JSON.stringify(data.error)); } return data; } catch (err) { if (err.message === 'MISSING_GAS_URL') throw err; console.warn(`Key ${i + 1} failed via proxy, trying next key...`, err); lastError = err; } } throw new Error(`כל מפתחות ה-API נכשלו: ${lastError?.message || lastError}`); } function populateFallbackModels() { const select = document.getElementById('modelSelect'); if (select.children.length === 0) { select.innerHTML = ` <option value="gemini-3.6-flash" selected>gemini-3.6-flash</option> <option value="gemini-2.0-flash">gemini-2.0-flash</option> <option value="gemini-1.5-flash">gemini-1.5-flash</option> <option value="gemini-1.5-pro">gemini-1.5-pro</option> `; } } // --- Fetch Models via GAS Proxy Safely --- async function fetchModels() { populateFallbackModels(); if (!gasProxyUrl || apiKeys.length === 0) return; try { const data = await makeGeminiApiCall( (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`, 'GET' ); let models = (data.models || []) .filter(m => m.supportedGenerationMethods?.includes('generateContent')) .map(m => m.name.replace('models/', '')); if (models.length > 0) { const select = document.getElementById('modelSelect'); const currentValue = select.value || 'gemini-3.6-flash'; select.innerHTML = ''; // Always guarantee gemini-3.6-flash is at the top if (!models.includes('gemini-3.6-flash')) { models.unshift('gemini-3.6-flash'); } models.forEach(m => { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; select.appendChild(opt); }); select.value = models.includes(currentValue) ? currentValue : 'gemini-3.6-flash'; } } catch(e) { console.warn("Could not fetch models dynamically, using default list with gemini-3.6-flash.", e); } } // --- Dynamic ZIP & Folder Analysis (Client Side) --- async function analyzeZipFolders(base64Data) { try { const zip = new JSZip(); await zip.loadAsync(base64Data, { base64: true }); const folderSet = new Set(); zip.forEach((relativePath, file) => { const parts = relativePath.split('/'); if (parts.length > 1 && parts[0].trim().length > 0) { folderSet.add(parts[0]); } }); zipFolders = Array.from(folderSet).sort(); selectedFolders = [...zipFolders]; localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS, JSON.stringify(zipFolders)); localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); } catch (err) { console.error("Error analyzing ZIP folders:", err); zipFolders = []; selectedFolders = []; } } function toggleChatFolderPopover(event) { event.stopPropagation(); const popover = document.getElementById('chatFolderPopover'); if (popover) popover.classList.toggle('open'); } function updateZipStatusUI() { const statusBox = document.getElementById('zipStatusBox'); const topbarBadge = document.getElementById('topbarZipBadge'); const topbarText = document.getElementById('topbarZipText'); const chatFolderContainer = document.getElementById('chatFolderScopeContainer'); const chatFolderGrid = document.getElementById('chatFolderCheckboxesGrid'); const chatFolderCountBadge = document.getElementById('chatFolderCountBadge'); if (zipBase64 && zipMeta) { let folderCheckboxesHTML = ''; if (zipFolders.length > 0) { folderCheckboxesHTML = ` <div class="folder-picker-section"> <div class="folder-picker-header"> <span style="font-weight: 700; font-size: 0.9rem;"><i class="fa-solid fa-folder-tree"></i> בחר תיקיות לשליחה כהקשר (${selectedFolders.length}/${zipFolders.length}):</span> <div style="display: flex; gap: 8px;"> <button class="tab-btn" style="padding: 3px 10px; font-size: 0.78rem;" onclick="selectAllFolders(true)">בחר הכל</button> <button class="tab-btn" style="padding: 3px 10px; font-size: 0.78rem;" onclick="selectAllFolders(false)">נקה הכל</button> </div> </div> <div class="folder-picker-grid"> ${zipFolders.map(folder => ` <label class="folder-checkbox-item"> <input type="checkbox" value="${escapeHtml(folder)}" ${selectedFolders.includes(folder) ? 'checked' : ''} onchange="toggleFolderSelection('${escapeHtml(folder)}')"> <i class="fa-regular fa-folder" style="color: var(--accent-glow);"></i> <span>${escapeHtml(folder)}</span> </label> `).join('')} </div> </div> `; } statusBox.className = 'zip-status-container has-file'; statusBox.innerHTML = ` <div class="zip-info-row"> <div class="zip-info-details"> <i class="fa-solid fa-circle-check zip-icon"></i> <div class="zip-info-text"> <div class="title">קובץ תיעוד פעיל בדפדפן: ${escapeHtml(zipMeta.name)}</div> <div class="sub">גודל: ${zipMeta.size} | סה"כ ${zipFolders.length} תיקיות זוהו</div> </div> </div> <div class="zip-actions"> <button class="tab-btn active" onclick="document.getElementById('zipFileInput').click()"> <i class="fa-solid fa-arrows-rotate"></i> החלף ZIP </button> <button class="tab-btn" style="color: #ef4444; border-color: rgba(239, 68, 68, 0.3);" onclick="removeZipFile()"> <i class="fa-solid fa-trash"></i> הסר </button> </div> </div> ${folderCheckboxesHTML} `; topbarBadge.className = 'topbar-status-badge active'; const folderCountText = selectedFolders.length === zipFolders.length ? 'כל התיקיות' : `${selectedFolders.length}/${zipFolders.length} תיקיות`; topbarText.innerText = `תיעוד פעיל (${folderCountText})`; if (zipFolders.length > 0) { chatFolderContainer.style.display = 'inline-block'; chatFolderCountBadge.innerText = `${selectedFolders.length}/${zipFolders.length}`; chatFolderGrid.innerHTML = zipFolders.map(folder => ` <label class="folder-checkbox-item" style="padding: 5px 8px; font-size: 0.8rem;"> <input type="checkbox" value="${escapeHtml(folder)}" ${selectedFolders.includes(folder) ? 'checked' : ''} onchange="toggleFolderSelection('${escapeHtml(folder)}')"> <i class="fa-regular fa-folder" style="color: var(--accent-glow);"></i> <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(folder)}</span> </label> `).join(''); } else { chatFolderContainer.style.display = 'none'; } } else { statusBox.className = 'zip-status-container'; statusBox.innerHTML = ` <div class="zip-info-row" style="justify-content: center; text-align: center; padding: 10px 0;"> <div> <i class="fa-solid fa-cloud-arrow-up" style="font-size: 2.2rem; color: var(--text-muted); margin-bottom: 8px;"></i> <div style="font-weight: 700; margin-bottom: 4px;">טרם נטען קובץ ZIP של תיעוד ימות המשיח</div> <div style="font-size: 0.84rem; color: var(--text-muted); margin-bottom: 12px;">העלה את קובץ התיעוד כדי לאפשר סינון תיקיות ממוקד בדפדפן</div> <button class="tab-btn active" onclick="document.getElementById('zipFileInput').click()" style="margin: 0 auto;"> <i class="fa-solid fa-upload"></i> בחר קובץ ZIP להעלאה </button> </div> </div> `; topbarBadge.className = 'topbar-status-badge'; topbarText.innerText = 'אין תיעוד ZIP טעון'; chatFolderContainer.style.display = 'none'; } } async function handleZipUpload(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async function(e) { zipBase64 = e.target.result.split(',')[1]; zipMeta = { name: file.name, size: (file.size / 1024).toFixed(1) + ' KB' }; await analyzeZipFolders(zipBase64); try { localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_BASE64, zipBase64); localStorage.setItem(LOCAL_STORAGE_KEYS.ZIP_META, JSON.stringify(zipMeta)); } catch (err) { console.warn('Zip file size exceeds LocalStorage quota; keeping in active RAM context.'); } updateZipStatusUI(); }; reader.readAsDataURL(file); event.target.value = ''; } function toggleFolderSelection(folder) { if (selectedFolders.includes(folder)) { selectedFolders = selectedFolders.filter(f => f !== folder); } else { selectedFolders.push(folder); } localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); updateZipStatusUI(); } function selectAllFolders(selectAll) { selectedFolders = selectAll ? [...zipFolders] : []; localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS, JSON.stringify(selectedFolders)); updateZipStatusUI(); } function removeZipFile() { if (!confirm('האם להסיר את קובץ ה-ZIP של התיעוד?')) return; zipBase64 = null; zipMeta = null; zipFolders = []; selectedFolders = []; localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_BASE64); localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_META); localStorage.removeItem(LOCAL_STORAGE_KEYS.ZIP_FOLDERS); localStorage.removeItem(LOCAL_STORAGE_KEYS.SELECTED_FOLDERS); updateZipStatusUI(); } // --- Generate Filtered ZIP Payload On-The-Fly --- async function getFilteredZipBase64() { if (!zipBase64) return null; if (zipFolders.length === 0 || selectedFolders.length === zipFolders.length) { return zipBase64; } if (selectedFolders.length === 0) { return null; } try { const sourceZip = new JSZip(); await sourceZip.loadAsync(zipBase64, { base64: true }); const filteredZip = new JSZip(); const promises = []; sourceZip.forEach((relativePath, file) => { const topFolder = relativePath.split('/')[0]; if (selectedFolders.includes(topFolder) && !file.dir) { const p = file.async('uint8array').then(content => { filteredZip.file(relativePath, content); }); promises.push(p); } }); await Promise.all(promises); return await filteredZip.generateAsync({ type: 'base64' }); } catch (err) { console.error("Error generating filtered ZIP:", err); return zipBase64; } } // --- File Uploads & Drag-and-Drop --- function handleGenericFileUpload(event) { const files = Array.from(event.target.files); processFiles(files); event.target.value = ''; } function initDragAndDrop() { const textarea = document.getElementById('userInput'); ['dragenter', 'dragover'].forEach(eventName => { textarea.addEventListener(eventName, (e) => { e.preventDefault(); textarea.style.borderColor = 'var(--accent-glow)'; }, false); }); ['dragleave', 'drop'].forEach(eventName => { textarea.addEventListener(eventName, (e) => { e.preventDefault(); textarea.style.borderColor = 'var(--panel-border)'; }, false); }); textarea.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = Array.from(dt.files); if (files.length > 0) processFiles(files); }); } function processFiles(files) { files.forEach(file => { const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; let mimeType = file.type || getFallbackMime(file.name); pendingAttachments.push({ name: file.name, mimeType: mimeType, data: base64Data, size: (file.size / 1024).toFixed(1) + ' KB' }); renderAttachmentsPreview(); }; reader.readAsDataURL(file); }); } function getFallbackMime(filename) { const ext = filename.split('.').pop().toLowerCase(); const map = { 'ini': 'text/plain', 'txt': 'text/plain', 'wav': 'audio/wav', 'mp3': 'audio/mp3', 'pdf': 'application/pdf', 'csv': 'text/csv', 'json': 'application/json', 'zip': 'application/zip', 'js': 'text/javascript', 'php': 'text/x-php', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg' }; return map[ext] || 'application/octet-stream'; } function removeAttachment(index) { pendingAttachments.splice(index, 1); renderAttachmentsPreview(); } function renderAttachmentsPreview() { const previewEl = document.getElementById('attachmentsPreview'); previewEl.innerHTML = ''; pendingAttachments.forEach((att, idx) => { const chip = document.createElement('div'); chip.className = 'att-chip'; chip.innerHTML = ` <i class="fa-solid fa-file"></i> <span>${escapeHtml(att.name)} (${att.size})</span> <span class="remove-att" onclick="removeAttachment(${idx})">×</span> `; previewEl.appendChild(chip); }); } // --- Zen & Themes --- function toggleZenMode() { document.body.classList.toggle('zen-mode'); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.body.classList.contains('zen-mode')) { toggleZenMode(); } }); function changeTheme(theme) { document.body.setAttribute('data-theme', theme); localStorage.setItem(LOCAL_STORAGE_KEYS.THEME, theme); } function changeFontSize(size) { document.body.setAttribute('data-font', size); localStorage.setItem(LOCAL_STORAGE_KEYS.FONT_SIZE, size); } function loadThemePreferences() { const savedTheme = localStorage.getItem(LOCAL_STORAGE_KEYS.THEME) || 'cyberpunk'; const savedFont = localStorage.getItem(LOCAL_STORAGE_KEYS.FONT_SIZE) || 'medium'; changeTheme(savedTheme); changeFontSize(savedFont); document.getElementById('themeSelect').value = savedTheme; document.getElementById('fontSelect').value = savedFont; } function handleChatScroll() { const container = document.getElementById('chatContainer'); const jumpBtn = document.getElementById('jumpBottomBtn'); const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; if (distanceFromBottom > 200) { jumpBtn.classList.add('show'); } else { jumpBtn.classList.remove('show'); } } function scrollToBottom(smooth = true) { const container = document.getElementById('chatContainer'); container.scrollTo({ top: container.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); } // --- Session Engine --- function loadSessions() { try { const saved = localStorage.getItem(LOCAL_STORAGE_KEYS.SESSIONS); sessions = saved ? JSON.parse(saved) : []; } catch (e) { sessions = []; } if (sessions.length === 0) { createNewSession(); } else { currentSessionId = sessions[0].id; renderSessionsList(); renderCurrentSessionChat(); } } function saveSessions() { try { localStorage.setItem(LOCAL_STORAGE_KEYS.SESSIONS, JSON.stringify(sessions)); } catch (e) { console.warn('LocalStorage limit reached.'); } renderSessionsList(); } function createNewSession() { const newSession = { id: Date.now().toString(), title: 'שיחה חדשה', timestamp: new Date().toLocaleTimeString('he-IL', {hour: '2-digit', minute:'2-digit'}), history: [] }; sessions.unshift(newSession); currentSessionId = newSession.id; saveSessions(); renderCurrentSessionChat(); } function switchSession(id) { currentSessionId = id; renderSessionsList(); renderCurrentSessionChat(); } function deleteSession(id, event) { event.stopPropagation(); sessions = sessions.filter(s => s.id !== id); if (sessions.length === 0) { createNewSession(); } else { if (currentSessionId === id) currentSessionId = sessions[0].id; saveSessions(); renderCurrentSessionChat(); } } function renderSessionsList() { const listEl = document.getElementById('historyList'); listEl.innerHTML = ''; sessions.forEach(s => { const item = document.createElement('div'); item.className = `history-item ${s.id === currentSessionId ? 'active' : ''}`; item.onclick = () => switchSession(s.id); item.innerHTML = ` <span class="title"><i class="fa-regular fa-message"></i> ${escapeHtml(s.title)}</span> <i class="fa-solid fa-trash delete-btn" onclick="deleteSession('${s.id}', event)" title="מחק שיחה"></i> `; listEl.appendChild(item); }); } function getCurrentSession() { return sessions.find(s => s.id === currentSessionId); } function renderCurrentSessionChat() { const container = document.getElementById('chatContainer'); container.innerHTML = ` <div class="message model"> שלום! אני עוזר ה-AI הבכיר שלך לבניית מרכזיות, שלוחות ו-Webhooks ב'ימות המשיח'.<br> העלה קבצים, שאל שאלות, או סנן תיקיות תיעוד ספציפיות לקבלת קובצי <code>ext.ini</code> מדויקים! </div>`; const session = getCurrentSession(); if (!session) return; session.history.forEach(msg => { if (msg.role === 'user') { appendMessageUI('user', msg.text, msg.attachedNames || [], false); } else if (msg.role === 'model') { renderModelResponseUI(msg.text); } }); scrollToBottom(false); } // --- Keys & Storage --- function loadKeys() { const saved = localStorage.getItem(LOCAL_STORAGE_KEYS.API_KEYS); if (saved) { apiKeys = JSON.parse(saved); document.getElementById('apiKeysInput').value = apiKeys.join('\n'); } } async function saveKeys() { const lines = document.getElementById('apiKeysInput').value.split('\n').map(k => k.trim()).filter(k => k.length > 0); apiKeys = lines; localStorage.setItem(LOCAL_STORAGE_KEYS.API_KEYS, JSON.stringify(apiKeys)); alert('מפתחות ה-API נשמרו בדפדפן בהצלחה!'); if (gasProxyUrl && apiKeys.length > 0) fetchModels(); } function saveYemotCredentials() { localStorage.setItem(LOCAL_STORAGE_KEYS.YM_USER, document.getElementById('ymUsername').value); localStorage.setItem(LOCAL_STORAGE_KEYS.YM_PASS, document.getElementById('ymPassword').value); alert('פרטי החיבור נשמרו בדפדפן!'); } function loadYemotCredentials() { document.getElementById('ymUsername').value = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_USER) || ''; document.getElementById('ymPassword').value = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_PASS) || ''; } // --- Speech Input with Complete Event Handlers --- function initSpeechRecognition() { if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; recognition = new SpeechRecognition(); recognition.lang = 'he-IL'; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; const inputEl = document.getElementById('userInput'); inputEl.value += (inputEl.value ? ' ' : '') + transcript; }; recognition.onend = () => { isRecording = false; const micBtn = document.getElementById('micBtn'); if (micBtn) micBtn.classList.remove('recording'); }; recognition.onerror = () => { isRecording = false; const micBtn = document.getElementById('micBtn'); if (micBtn) micBtn.classList.remove('recording'); }; } else { document.getElementById('micBtn').style.display = 'none'; } } function toggleVoiceInput() { if (!recognition) return; const micBtn = document.getElementById('micBtn'); if (!isRecording) { try { recognition.start(); isRecording = true; micBtn.classList.add('recording'); } catch(e) { console.warn("Recognition start failed", e); } } else { recognition.stop(); isRecording = false; micBtn.classList.remove('recording'); } } // --- Message Dispatcher via GAS Proxy --- async function sendMessage() { const inputEl = document.getElementById('userInput'); const text = inputEl.value.trim(); if (!text && pendingAttachments.length === 0) return; // Soft-check settings without blocking loops if (!gasProxyUrl) { appendMessageUI('model', ` <div style="color: #f59e0b; padding: 6px 0;"> <i class="fa-solid fa-triangle-exclamation" style="font-size: 1.2rem; margin-left: 6px;"></i> <b>טרם הוגדרה כתובת Google Script Proxy:</b><br> עבור בלשונית <a href="javascript:void(0)" onclick="switchTab('settingsTab')" style="color:#38bdf8; font-weight:bold;">הגדרות וגוגל סקריפט</a>, הדבק את כתובת ה-Web App ולחץ על "שמור". </div> `, [], true); scrollToBottom(); return; } if (apiKeys.length === 0) { appendMessageUI('model', ` <div style="color: #f59e0b; padding: 6px 0;"> <i class="fa-solid fa-key" style="font-size: 1.2rem; margin-left: 6px;"></i> <b>חסר מפתח API:</b><br> אנא הכנס לפחות מפתח Gemini API אחד בלשונית <a href="javascript:void(0)" onclick="switchTab('settingsTab')" style="color:#38bdf8; font-weight:bold;">הגדרות וגוגל סקריפט</a>. </div> `, [], true); scrollToBottom(); return; } const sendBtn = document.getElementById('sendBtn'); const deepAnalysis = document.getElementById('deepAnalysisToggle').checked; const selectedModel = document.getElementById('modelSelect').value || 'gemini-3.6-flash'; const session = getCurrentSession(); if (session.history.length === 0) { session.title = text ? (text.substring(0, 25) + '...') : (pendingAttachments[0]?.name || 'קובץ מצורף'); } const currentTurnAttachments = [...pendingAttachments]; const attachedNames = currentTurnAttachments.map(a => a.name); appendMessageUI('user', text || '(קבצים מצורפים)', attachedNames, false); inputEl.value = ''; pendingAttachments = []; renderAttachmentsPreview(); sendBtn.disabled = true; scrollToBottom(); session.history.push({ role: 'user', text: text || '(קבצים מצורפים)', attachedNames: attachedNames }); saveSessions(); const loadingDiv = appendMessageUI('model', '<div class="loading-shimmer"><i class="fa-solid fa-spinner"></i> <span>שולח דרך Google Script Proxy ומנתח תיעוד...</span></div>', [], true); scrollToBottom(); try { const effectiveZipBase64 = await getFilteredZipBase64(); let contentsPayload = []; session.history.forEach((msg, idx) => { let parts = []; if (idx === 0 && msg.role === 'user' && effectiveZipBase64) { parts.push({ inline_data: { mime_type: "application/zip", data: effectiveZipBase64 } }); } if (idx === session.history.length - 1 && msg.role === 'user' && currentTurnAttachments.length > 0) { currentTurnAttachments.forEach(att => { parts.push({ inline_data: { mime_type: att.mimeType, data: att.data } }); }); } let promptText = msg.text; if (idx === session.history.length - 1 && msg.role === 'user' && deepAnalysis) { promptText = `[הוראת חובה לניתוח מעמיק של הפורמט]: לפני מתן התשובה והקוד הסופיים, עליך לבצע ניתוח מקיף ומעמיק של הדרישה, הפורמט המבוקש (כגון ext.ini, HTML, Webhook, תפריטי IVR), לוגיקת הרכיבים, בדיקת קצה ומבנה הנתונים. חובה לעטוף את *כל* הניתוח המעמיק בתוך תגיות <analysis> ו-</analysis>. רק לאחר התגית הסוגרת </analysis> תציג את התשובה והקוד המלאים והסופיים. בקשת המשתמש: ${msg.text}`; } parts.push({ text: promptText }); contentsPayload.push({ role: msg.role, parts: parts }); }); const payloadBody = { system_instruction: { parts: [{ text: SYSTEM_INSTRUCTION }] }, contents: contentsPayload }; const responseData = await makeGeminiApiCall( (key) => `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`, 'POST', () => payloadBody ); const replyText = responseData?.candidates?.[0]?.content?.parts?.[0]?.text; if (!replyText) throw new Error('התקבלה תשובה ריקה משרת ה-Proxy.'); session.history.push({ role: 'model', text: replyText }); saveSessions(); loadingDiv.remove(); renderModelResponseUI(replyText); scrollToBottom(); } catch (err) { let errDesc = err.message; if (err.message === 'MISSING_GAS_URL') { errDesc = 'כתובת הסקריפט חסרה. אנא הגדר אותה בלשונית ההגדרות.'; } else if (err.message === 'MISSING_KEYS') { errDesc = 'מפתח API חסר. אנא הכנס מפתח בלשונית ההגדרות.'; } loadingDiv.innerHTML = `<span style="color:#ef4444;"><i class="fa-solid fa-circle-exclamation"></i> שגיאה: ${escapeHtml(errDesc)}</span>`; } finally { sendBtn.disabled = false; } } // Render AI Model Response function renderModelResponseUI(text) { const container = document.getElementById('chatContainer'); const msgDiv = document.createElement('div'); msgDiv.className = 'message model'; let analysisHTML = ''; let mainContent = text; const analysisMatch = text.match(/<analysis>([\s\S]*?)<\/analysis>/); if (analysisMatch) { const analysisText = analysisMatch[1].trim(); mainContent = text.replace(/<analysis>[\s\S]*?<\/analysis>/, '').trim(); analysisHTML = ` <div class="analysis-box"> <div class="analysis-header" onclick="this.parentElement.classList.toggle('open')"> <span><i class="fa-solid fa-brain"></i> ניתוח מעמיק של הפורמט והלוגיקה (לחץ לפתיחה)</span> <i class="fa-solid fa-chevron-down"></i> </div> <div class="analysis-content">${escapeHtml(analysisText)}</div> </div> `; } msgDiv.innerHTML = analysisHTML + marked.parse(mainContent); container.appendChild(msgDiv); // Enhance Code Blocks msgDiv.querySelectorAll('pre code').forEach((codeBlock) => { const rawCode = codeBlock.innerText; const parentPre = codeBlock.parentNode; const wrapper = document.createElement('div'); wrapper.className = 'code-container'; const header = document.createElement('div'); header.className = 'code-header'; header.innerHTML = `<span><i class="fa-solid fa-code"></i> קוד תצורה (ext.ini)</span>`; const actions = document.createElement('div'); actions.className = 'code-actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'code-btn'; copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> העתק'; copyBtn.onclick = () => { navigator.clipboard.writeText(rawCode); copyBtn.innerHTML = '<i class="fa-solid fa-check"></i> הועתק!'; setTimeout(() => copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> העתק', 2000); }; const dlBtn = document.createElement('button'); dlBtn.className = 'code-btn'; dlBtn.innerHTML = '<i class="fa-solid fa-download"></i> הורד ext.ini'; dlBtn.onclick = () => downloadCodeFile(rawCode, 'ext.ini'); const uploadBtn = document.createElement('button'); uploadBtn.className = 'code-btn'; uploadBtn.style.color = '#10b981'; uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> העלה למרכזיה'; uploadBtn.onclick = () => uploadDirectToYemot(rawCode); const geminiTtsBtn = document.createElement('button'); geminiTtsBtn.className = 'code-btn'; geminiTtsBtn.style.color = '#38bdf8'; geminiTtsBtn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> שמע Gemini AI'; geminiTtsBtn.onclick = () => generateGeminiAudio(rawCode); actions.appendChild(copyBtn); actions.appendChild(dlBtn); actions.appendChild(geminiTtsBtn); actions.appendChild(uploadBtn); header.appendChild(actions); wrapper.appendChild(header); parentPre.parentNode.insertBefore(wrapper, parentPre); wrapper.appendChild(parentPre); hljs.highlightElement(codeBlock); }); } // Audio TTS via Client Synthesis async function generateGeminiAudio(code) { let textToRead = prompt('הכנס את הטקסט שברצונך שה-Gemini AI יכין ויקריא עבור המרכזייה:', 'ברוכים הבאים למערכת שלנו'); if (!textToRead) return; const selectedModel = document.getElementById('modelSelect').value || 'gemini-3.6-flash'; try { const responseData = await makeGeminiApiCall( (key) => `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`, 'POST', () => ({ contents: [{ parts: [{ text: `עבד והכשר את הניסוח הבא לקריאה קולית נקייה ומדויקת בלבד במרכזיה (ללא סימני פיסוק או הערות נוספות):\n"${textToRead}"` }] }] }) ); const speechText = responseData?.candidates?.[0]?.content?.parts?.[0]?.text || textToRead; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(speechText); utterance.lang = 'he-IL'; utterance.rate = 0.95; window.speechSynthesis.speak(utterance); } catch (err) { window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(textToRead); utterance.lang = 'he-IL'; window.speechSynthesis.speak(utterance); } } // Direct Yemot Push (Client Side) async function uploadDirectToYemot(code) { const username = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_USER); const password = localStorage.getItem(LOCAL_STORAGE_KEYS.YM_PASS); if (!username || !password) { alert('אנא הגדר פרטי התחברות לימות המשיח בלשונית ההגדרות.'); switchTab('settingsTab'); return; } const path = prompt('הכנס את נתיב השלוחה להעלאה (לדוגמה: 1/2 או 0 לראשית):', '1'); if (!path) return; try { const loginRes = await fetch(`https://www.call2all.co.il/ym/api/Login?username=${username}&password=${password}`); const loginData = await loginRes.json(); if (loginData.responseStatus !== 'OK') { throw new Error('התחברות למערכת ימות המשיח נכשלה.'); } const token = loginData.token; const formData = new FormData(); const blob = new Blob([code], { type: 'text/plain' }); formData.append('file', blob, 'ext.ini'); const uploadRes = await fetch(`https://www.call2all.co.il/ym/api/UploadFile?token=${token}&path=ivr2:${path}/ext.ini`, { method: 'POST', body: formData }); const uploadData = await uploadRes.json(); if (uploadData.responseStatus === 'OK') { alert(`הקוד הועלה בהצלחה לשלוחה ${path}!`); } else { alert(`שגיאה: ${uploadData.message || 'תקלה לא ידועה'}`); } } catch (err) { alert(`שגיאה: ${err.message}`); } } function downloadCodeFile(code, filename) { const blob = new Blob([code], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); } function appendMessageUI(role, content, attachedNames = [], isHTML = false) { const container = document.getElementById('chatContainer'); const msgDiv = document.createElement('div'); msgDiv.className = `message ${role}`; let attachmentsHTML = ''; if (attachedNames && attachedNames.length > 0) { attachmentsHTML = '<div class="message-attachments">' + attachedNames.map(n => `<span class="msg-att-chip"><i class="fa-solid fa-paperclip"></i> ${escapeHtml(n)}</span>`).join('') + '</div>'; } const bodyContent = isHTML ? content : `<div>${escapeHtml(content)}</div>`; msgDiv.innerHTML = attachmentsHTML + bodyContent; container.appendChild(msgDiv); return msgDiv; } function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage(); } } function escapeHtml(text) { return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } </script> </body> </html>להורדה יש למחוק מהסיומת את "מיותר"
|-
-| -
L lavitoren הטי"ל התייחס לנושא זה
שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.
נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.
בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗
הרשמה התחברות