| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>AI Alpha Chat</title> |
| <style> |
| |
| * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } |
| body, html { |
| margin: 0; padding: 0; |
| font-family: IRANSans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| background-color: #ffffff; |
| height: 100dvh; |
| overflow: hidden; |
| color: #333; |
| transition: background-color 0.3s, color 0.3s; |
| } |
| |
| ::-webkit-scrollbar { width: 5px; height: 5px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: #dcdcdc; border-radius: 10px; } |
| ::-webkit-scrollbar-thumb:hover { background: #b0b0b0; } |
| |
| |
| .app-container { |
| display: flex; flex-direction: column; height: 100%; position: relative; |
| } |
| |
| |
| .header { |
| display: flex; justify-content: space-between; align-items: center; |
| padding: 12px 14px; background-color: rgba(255, 255, 255, 0.95); |
| backdrop-filter: blur(10px); border-bottom: 1px solid #f0f0f0; z-index: 20; |
| transition: all 0.3s; |
| } |
| .header-left, .header-right { |
| display: flex; align-items: center; gap: 4px; flex-shrink: 0; |
| } |
| .header-center { |
| display: flex; align-items: center; justify-content: center; flex-grow: 1; padding: 0 8px; |
| } |
| |
| |
| .model-switch { |
| position: relative; |
| display: flex; |
| background: #f1f5f9; |
| border-radius: 20px; |
| padding: 3px; |
| width: 170px; |
| max-width: 100%; |
| transition: background 0.3s; |
| } |
| .switch-btn { |
| width: 50%; |
| border: none; |
| background: none; |
| padding: 6px 0; |
| font-size: 11px; |
| font-weight: 700; |
| color: #64748b; |
| cursor: pointer; |
| z-index: 2; |
| transition: color 0.3s; |
| font-family: inherit; |
| white-space: nowrap; |
| } |
| .switch-btn.active { color: white; } |
| .switch-glider { |
| position: absolute; |
| top: 3px; |
| right: 3px; |
| width: calc(50% - 3px); |
| height: calc(100% - 6px); |
| background: #5a67d8; |
| border-radius: 18px; |
| transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); |
| z-index: 1; |
| } |
| .model-switch[data-active="audio"] .switch-glider { |
| transform: translateX(-100%); |
| } |
| .model-switch[data-active="gpt5"] .switch-glider { |
| transform: translateX(0); |
| } |
| |
| .icon-btn { |
| background: none; border: none; padding: 8px; border-radius: 50%; |
| cursor: pointer; display: flex; align-items: center; justify-content: center; |
| color: #555; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| .icon-btn:hover { background-color: #f5f5f5; color: #111; } |
| .icon-btn:active { transform: scale(0.85); background-color: #e0e0e0; } |
| .icon-btn svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; } |
| |
| |
| .sidebar { |
| position: fixed; top: 0; right: -300px; width: 280px; height: 100%; |
| background-color: #f8f9fa; box-shadow: -4px 0 15px rgba(0,0,0,0.08); |
| transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); z-index: 100; |
| display: flex; flex-direction: column; |
| } |
| .sidebar.open { right: 0; } |
| .sidebar-header { |
| padding: 20px 16px; font-weight: bold; border-bottom: 1px solid #eee; |
| display: flex; justify-content: space-between; align-items: center; |
| color: #5a67d8; transition: border-color 0.3s; |
| } |
| .session-list { list-style: none; padding: 12px; margin: 0; overflow-y: auto; flex-grow: 1; } |
| |
| .session-item { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 12px 14px; margin-bottom: 8px; border-radius: 12px; |
| cursor: pointer; font-size: 14px; color: #333; |
| transition: all 0.2s ease; position: relative; |
| } |
| .session-item.active { background-color: #eef2ff; font-weight: bold; } |
| .session-item:hover:not(.active) { background-color: #f0f0f0; } |
| |
| .session-item-content { |
| display: flex; align-items: center; gap: 10px; flex-grow: 1; overflow: hidden; |
| } |
| .session-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1; } |
| |
| .dots-btn { |
| background: none; border: none; padding: 4px; border-radius: 50%; |
| cursor: pointer; color: #666; display: flex; align-items: center; justify-content: center; |
| } |
| .dots-btn:hover { background: #ddd; } |
| .dots-btn svg { width: 18px; height: 18px; fill: currentColor; stroke: none; } |
| |
| .popover-menu { |
| position: absolute; left: 10px; top: 40px; background: white; |
| border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); |
| display: none; flex-direction: column; z-index: 10; min-width: 120px; |
| overflow: hidden; transition: background 0.3s; |
| } |
| .popover-menu.show { display: flex; } |
| .popover-btn { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 12px 16px; border: none; background: none; width: 100%; |
| cursor: pointer; font-family: inherit; font-size: 14px; color: #ef4444; font-weight: bold; |
| } |
| .popover-btn:hover { background-color: #fff1f2; } |
| .popover-btn svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; fill: none; } |
| |
| .overlay { |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; |
| background: rgba(0,0,0,0.4); backdrop-filter: blur(2px); z-index: 90; |
| display: none; opacity: 0; transition: opacity 0.3s ease; |
| } |
| .overlay.visible { display: block; opacity: 1; } |
| |
| |
| .bottom-modal { |
| position: fixed; bottom: -100%; left: 0; width: 100%; |
| background-color: #fff; border-radius: 24px 24px 0 0; |
| padding: 24px 24px 30px 24px; box-shadow: 0 -5px 25px rgba(0,0,0,0.1); |
| z-index: 200; transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); |
| display: flex; flex-direction: column; align-items: center; |
| } |
| .bottom-modal.open { bottom: 0; } |
| .modal-handle { |
| width: 40px; height: 4px; background-color: #ddd; |
| border-radius: 2px; margin-bottom: 20px; |
| } |
| .modal-title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 15px; text-align: center;} |
| .modal-desc { font-size: 15px; color: #666; margin-bottom: 30px; text-align: center; line-height: 1.6;} |
| .modal-buttons { display: flex; gap: 15px; width: 100%; } |
| .modal-btn { |
| flex: 1; padding: 14px 0; border-radius: 12px; font-size: 16px; font-weight: bold; |
| cursor: pointer; border: none; transition: all 0.2s; font-family: inherit; |
| } |
| .btn-cancel { background-color: transparent; border: 1.5px solid #5a67d8; color: #5a67d8; } |
| .btn-cancel:active { background-color: #eef2ff; } |
| .btn-delete { background-color: #f43f5e; color: white; } |
| .btn-delete:active { background-color: #e11d48; } |
| |
| .btn-upgrade { background: linear-gradient(135deg, #8b5cf6, #6d28d9); color: white; box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2); } |
| .btn-upgrade:active { background: linear-gradient(135deg, #7c3aed, #5b21b6); } |
| |
| .modal-overlay { |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; |
| background: rgba(0,0,0,0.5); z-index: 190; display: none; opacity: 0; transition: opacity 0.3s; |
| } |
| .modal-overlay.open { display: block; opacity: 1; } |
| |
| |
| .chat-area { |
| flex-grow: 1; overflow-y: auto; |
| padding: 20px 16px 120px 16px; |
| position: relative; |
| display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; |
| } |
| |
| .empty-state { |
| position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); |
| font-size: 24px; font-weight: 900; text-align: center; |
| background: linear-gradient(to right, #ff8a80, #8c9eff); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; |
| color: transparent; text-shadow: none; -webkit-font-smoothing: antialiased; |
| display: none; width: 100%; |
| } |
| |
| @keyframes messageSlideUp { |
| 0% { opacity: 0; transform: translateY(20px); } |
| 100% { opacity: 1; transform: translateY(0); } |
| } |
| |
| .message-row { |
| display: flex; flex-direction: column; width: 100%; |
| animation: messageSlideUp 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; |
| } |
| .message-row.user { align-items: flex-start; } |
| .message-row.bot { align-items: flex-start; } |
| |
| .message-bubble { |
| max-width: 88%; font-size: 15px; line-height: 1.7; |
| word-wrap: break-word; white-space: pre-wrap; letter-spacing: 0.2px; |
| transition: background 0.3s, color 0.3s; |
| } |
| |
| .message-row.user .message-bubble { |
| padding: 14px 18px; |
| background-color: #e6ecff; |
| color: #15151c; |
| border-radius: 20px 20px 4px 20px; |
| box-shadow: 0 2px 8px rgba(0,0,0,0.04); |
| } |
| |
| .message-row.bot .message-bubble { |
| padding: 5px 0; |
| background-color: transparent; |
| color: #333; |
| border: none; |
| border-radius: 0; |
| text-align: right; |
| box-shadow: none; |
| max-width: 100%; |
| } |
| |
| .action-buttons { display: flex; gap: 8px; margin-top: 8px; margin-right: 4px; } |
| .action-btn { |
| background: #f8f9fa; border: 1px solid #eee; border-radius: 50%; |
| width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; |
| cursor: pointer; color: #666; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| .action-btn:hover { background: #fff; border-color: #ddd; color: #333; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } |
| .action-btn:active { transform: scale(0.85); } |
| .action-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } |
| .action-btn.play-btn svg { fill: currentColor; stroke: none; } |
| |
| @keyframes checkmarkPop { |
| 0% { transform: scale(0.5) rotate(-45deg); opacity: 0; } |
| 50% { transform: scale(1.2) rotate(0); } |
| 100% { transform: scale(1) rotate(0); opacity: 1; } |
| } |
| .icon-check { animation: checkmarkPop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; stroke: currentColor; } |
| |
| .status-bar { text-align: center; font-size: 11px; font-weight: bold; color: #999; margin-bottom: 8px; letter-spacing: 0.5px; transition: 0.3s; } |
| |
| |
| .typing-indicator { |
| display: flex; align-items: center; gap: 5px; padding: 10px 8px; |
| } |
| .typing-indicator .dot { |
| width: 8px; height: 8px; border-radius: 50%; background-color: #5a67d8; |
| animation: bounce 1.4s infinite ease-in-out both; |
| } |
| .typing-indicator .dot:nth-child(1) { animation-delay: -0.32s; } |
| .typing-indicator .dot:nth-child(2) { animation-delay: -0.16s; } |
| |
| @keyframes bounce { |
| 0%, 80%, 100% { transform: scale(0); opacity: 0.4; } |
| 40% { transform: scale(1); opacity: 1; } |
| } |
| |
| |
| .document-card { |
| background: #ebebeb; |
| border-radius: 16px; |
| padding: 16px; |
| width: 100%; |
| max-width: 320px; |
| box-shadow: 0 4px 12px rgba(0,0,0,0.04); |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| font-family: inherit; |
| } |
| .doc-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| border-bottom: 1px solid #d1d5db; |
| padding-bottom: 12px; |
| } |
| .doc-title-container { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-weight: 900; |
| font-size: 14px; |
| color: #111; |
| } |
| .doc-download-actions { |
| display: flex; |
| gap: 8px; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity 0.3s ease; |
| } |
| .doc-download-actions.visible { |
| opacity: 1; |
| pointer-events: auto; |
| } |
| .doc-dl-btn { |
| background: none; |
| border: none; |
| color: #4b5563; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: 4px; |
| transition: color 0.2s; |
| } |
| .doc-dl-btn:hover { color: #111; } |
| |
| .doc-content-scroll { |
| max-height: 250px; |
| overflow-y: auto; |
| padding-left: 8px; |
| font-size: 14px; |
| line-height: 1.8; |
| color: #111; |
| text-align: right; |
| } |
| |
| .doc-content-scroll::-webkit-scrollbar { width: 6px; } |
| .doc-content-scroll::-webkit-scrollbar-track { background: transparent; } |
| .doc-content-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } |
| |
| |
| |
| .input-container { |
| position: absolute; bottom: 0; left: 0; width: 100%; |
| padding: 10px 16px 20px 16px; |
| background: linear-gradient(to top, rgba(255,255,255,1) 85%, rgba(255,255,255,0)); |
| z-index: 10; transition: background 0.3s; |
| } |
| .image-preview-box { |
| display: none; padding: 6px; background: #fff; border-radius: 12px; |
| border: 1px solid #ddd; margin-bottom: 8px; position: relative; max-width: 120px; |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); transition: background 0.3s, border-color 0.3s; |
| } |
| .image-preview-box img { width: 100%; border-radius: 8px; display: block; } |
| .image-preview-box .close-btn { |
| position: absolute; top: -5px; right: -5px; background: #ff4d4f; color: white; |
| border: none; border-radius: 50%; width: 22px; height: 22px; cursor: pointer; |
| display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; |
| } |
| |
| |
| .attach-menu { |
| position: absolute; bottom: 75px; right: 16px; background: white; |
| border: 1px solid #ddd; border-radius: 14px; box-shadow: 0 6px 20px rgba(0,0,0,0.1); |
| display: flex; flex-direction: column; overflow: hidden; z-index: 100; |
| min-width: 160px; transition: background 0.3s, border-color 0.3s; |
| transform-origin: bottom right; animation: menuPopUp 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); |
| } |
| @keyframes menuPopUp { |
| 0% { transform: scale(0.9) translateY(10px); opacity: 0; } |
| 100% { transform: scale(1) translateY(0); opacity: 1; } |
| } |
| .attach-menu button { |
| padding: 14px 16px; border: none; background: transparent; text-align: right; |
| font-family: inherit; font-size: 14px; cursor: pointer; transition: background 0.2s; |
| color: #333; display: flex; align-items: center; gap: 10px; font-weight: bold; |
| } |
| .attach-menu button:hover { background: #f4f4f5; } |
| .attach-menu button:not(:last-child) { border-bottom: 1px solid #f0f0f0; } |
| |
| .input-wrapper { |
| display: flex; align-items: flex-end; background-color: #ffffff; |
| border: 1.5px solid #e8e8e8; border-radius: 24px; padding: 6px 6px 6px 16px; |
| box-shadow: 0 8px 25px rgba(0,0,0,0.06); transition: border-color 0.3s ease, background 0.3s; |
| } |
| .input-wrapper:focus-within { border-color: #c0caff; } |
| |
| .attach-btn { |
| background: none; border: none; padding: 6px; color: #666; cursor: pointer; |
| display: flex; align-items: center; justify-content: center; transition: all 0.2s; |
| margin-bottom: 2px; |
| } |
| .attach-btn:hover { color: #5a67d8; } |
| |
| |
| .input-wrapper textarea { |
| flex-grow: 1; border: none; background: transparent; outline: none; |
| font-size: 15px; padding: 7px 5px; font-family: inherit; color: #333; transition: color 0.3s; |
| resize: none; |
| height: 38px; |
| max-height: 140px; |
| line-height: 1.5; |
| overflow-y: auto; |
| box-sizing: border-box; |
| } |
| .input-wrapper textarea::placeholder { color: #bbb; } |
| |
| .send-btn { |
| background-color: #333; color: white; border: none; |
| width: 40px; height: 40px; border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
| margin-bottom: 2px; |
| flex-shrink: 0; |
| } |
| .send-btn:hover { background-color: #111; } |
| .send-btn:active { transform: scale(0.85); } |
| .send-btn:disabled { background-color: #e0e0e0; cursor: not-allowed; transform: none; box-shadow: none; } |
| .send-btn svg { width: 20px; height: 20px; stroke: white; stroke-width: 2.5; fill: none; stroke-linecap: round; stroke-linejoin: round; } |
| |
| |
| .sidebar-footer { |
| padding: 15px; border-top: 1px solid #eee; background: #f8f9fa; z-index: 10; |
| transition: background 0.3s, border-color 0.3s; |
| } |
| .settings-btn { |
| display: flex; align-items: center; gap: 10px; width: 100%; padding: 12px; |
| background: #fff; border: 1px solid #ddd; border-radius: 12px; |
| color: #333; font-weight: bold; font-family: inherit; cursor: pointer; |
| transition: all 0.2s; font-size: 14px; justify-content: center; |
| } |
| .settings-btn:hover { background: #f0f0f0; } |
| .settings-btn svg { width: 18px; height: 18px; } |
| |
| .settings-menu { |
| display: none; background: #fff; border: 1px solid #ddd; border-radius: 12px; |
| padding: 12px; margin-bottom: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); |
| animation: messageSlideUp 0.3s forwards; transition: background 0.3s, border-color 0.3s; |
| } |
| .settings-menu.open { display: block; } |
| |
| |
| .user-status-row { text-align: center; margin-bottom: 15px; } |
| .status-badge { display: inline-block; padding: 6px 12px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-bottom: 8px;} |
| .status-badge.free { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; } |
| .status-badge.paid { background: #ede9fe; color: #5b21b6; border: 1px solid #ddd6fe; } |
| .upgrade-btn { display: none; width: 100%; margin-top: 10px; padding: 10px; background: linear-gradient(135deg, #8b5cf6, #6d28d9); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; font-family: inherit; font-size: 13px; box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);} |
| .upgrade-btn:active { background: linear-gradient(135deg, #7c3aed, #5b21b6); } |
| |
| .theme-toggle-row { |
| display: flex; justify-content: space-between; align-items: center; font-size: 14px; font-weight: bold; color: #333; |
| transition: color 0.3s; |
| } |
| .toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; } |
| .toggle-switch input { opacity: 0; width: 0; height: 0; } |
| .toggle-slider { |
| position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; |
| background-color: #ccc; transition: .4s; border-radius: 24px; |
| } |
| .toggle-slider:before { |
| position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; |
| background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); |
| } |
| input:checked + .toggle-slider { background-color: #5a67d8; } |
| input:checked + .toggle-slider:before { transform: translateX(20px); } |
| |
| |
| body.dark-theme { background-color: #121212; color: #e0e0e0; } |
| body.dark-theme .header { background-color: rgba(30, 30, 30, 0.95); border-bottom-color: #333; } |
| body.dark-theme .sidebar { background-color: #1a1a1a; border-left: 1px solid #333; box-shadow: -4px 0 15px rgba(0,0,0,0.5); } |
| body.dark-theme .sidebar-header { border-bottom-color: #333; color: #818cf8; } |
| body.dark-theme .session-item { color: #e0e0e0; } |
| body.dark-theme .session-item:hover:not(.active) { background-color: #2a2a2a; } |
| body.dark-theme .session-item.active { background-color: #2d3748; color: #fff; } |
| body.dark-theme .sidebar-footer { background-color: #1a1a1a; border-top-color: #333; } |
| body.dark-theme .settings-btn { background-color: #2a2a2a; border-color: #444; color: #e0e0e0; } |
| body.dark-theme .settings-btn:hover { background-color: #333; } |
| body.dark-theme .settings-menu { background-color: #2a2a2a; border-color: #444; } |
| body.dark-theme .theme-toggle-row { color: #e0e0e0; } |
| body.dark-theme .input-container { background: linear-gradient(to top, rgba(18,18,18,1) 85%, rgba(18,18,18,0)); } |
| body.dark-theme .input-wrapper { background-color: #1e1e1e; border-color: #444; } |
| body.dark-theme .input-wrapper textarea { color: #e0e0e0; } |
| body.dark-theme .message-row.user .message-bubble { background-color: #2d3748; color: #e0e0e0; } |
| body.dark-theme .message-row.bot .message-bubble { color: #e0e0e0; } |
| body.dark-theme .action-btn { background: #2a2a2a; border-color: #444; color: #aaa; } |
| body.dark-theme .action-btn:hover { background: #333; border-color: #555; color: #fff; } |
| body.dark-theme .icon-btn { color: #ccc; } |
| body.dark-theme .icon-btn:hover { background-color: #333; color: #fff; } |
| body.dark-theme .model-switch { background: #2a2a2a; } |
| body.dark-theme .switch-btn { color: #aaa; } |
| body.dark-theme .switch-btn.active { color: white; } |
| body.dark-theme .bottom-modal { background-color: #1e1e1e; } |
| body.dark-theme .modal-title { color: #fff; } |
| body.dark-theme .modal-desc { color: #aaa; } |
| body.dark-theme .modal-handle { background-color: #555; } |
| body.dark-theme .popover-menu { background: #2a2a2a; } |
| body.dark-theme .popover-btn { color: #f87171; } |
| body.dark-theme .popover-btn:hover { background-color: #3f1d1d; } |
| body.dark-theme .dots-btn { color: #aaa; } |
| body.dark-theme .dots-btn:hover { background: #444; } |
| body.dark-theme .image-preview-box { background: #1e1e1e; border-color: #444; } |
| body.dark-theme .send-btn:disabled { background-color: #444; } |
| body.dark-theme .status-badge.free { background: #334155; color: #cbd5e1; border-color: #475569; } |
| body.dark-theme .status-badge.paid { background: #2e1065; color: #ddd6fe; border-color: #4c1d95; } |
| body.dark-theme .typing-indicator .dot { background-color: #818cf8; } |
| body.dark-theme .attach-menu { background: #2a2a2a; border-color: #444; } |
| body.dark-theme .attach-menu button { color: #eee; } |
| body.dark-theme .attach-menu button:hover { background: #333; } |
| body.dark-theme .attach-menu button:not(:last-child) { border-bottom-color: #444; } |
| body.dark-theme .document-card { background: #2d3748; color: #e0e0e0; } |
| body.dark-theme .doc-header { border-bottom-color: #4a5568; } |
| body.dark-theme .doc-title-container { color: #f9fafb; } |
| body.dark-theme .doc-dl-btn { color: #9ca3af; } |
| body.dark-theme .doc-dl-btn:hover { color: #f9fafb; } |
| body.dark-theme .doc-content-scroll { color: #d1d5db; } |
| body.dark-theme .doc-content-scroll::-webkit-scrollbar-thumb { background: #4b5563; } |
| </style> |
| </head> |
| <body> |
|
|
| <input type="file" id="imageInput" accept="image/*" style="position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; opacity: 0;" onchange="handleImageSelection(event)"> |
|
|
| <div class="app-container"> |
| <div class="header"> |
| <div class="header-right"> |
| <button class="icon-btn" onclick="toggleSidebar()" aria-label="منو"> |
| <svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg> |
| </button> |
| </div> |
| <div class="header-center"> |
| <div class="model-switch" id="modelSwitch" data-active="gpt5"> |
| <div class="switch-glider"></div> |
| <button class="switch-btn active" id="btn-gpt5" onclick="setModel('gpt5')">آلفا GPT5</button> |
| <button class="switch-btn" id="btn-audio" onclick="setModel('audio')">آلفا صوتی</button> |
| </div> |
| </div> |
| <div class="header-left"> |
| <button class="icon-btn" onclick="startNewSession()" aria-label="چت جدید"> |
| <svg viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><path d="M9 12h6M12 9v6"/></svg> |
| </button> |
| <button class="icon-btn" id="muteToggleBtn" onclick="toggleMute()" aria-label="قطع و وصل صدا"> |
| <svg id="icon-sound-on" viewBox="0 0 24 24"><path d="M11 5L6 9H2v6h4l5 4V5zM15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14"/></svg> |
| <svg id="icon-sound-off" viewBox="0 0 24 24" style="display:none;"><path d="M11 5L6 9H2v6h4l5 4V5zM22 9l-6 6M16 9l6 6"/></svg> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="overlay" id="overlay" onclick="toggleSidebar()"></div> |
| <div class="sidebar" id="sidebar"> |
| <div class="sidebar-header"> |
| <span>تاریخچه گفتگوها</span> |
| <button class="icon-btn" onclick="toggleSidebar()"> |
| <svg viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg> |
| </button> |
| </div> |
| <ul class="session-list" id="sessionList"></ul> |
|
|
| |
| <div class="sidebar-footer"> |
| <div class="settings-menu" id="settingsMenu"> |
| |
| |
| <div class="user-status-row"> |
| <span id="userStatusBadge" class="status-badge free">در حال بررسی...</span> |
| <button id="upgradeBtnChat" class="upgrade-btn" onclick="goToPremium()">ارتقا به نسخه نامحدود ⭐️</button> |
| <hr style="border:0; border-top:1px solid #ddd; margin:15px 0;"> |
| </div> |
|
|
| <div class="theme-toggle-row"> |
| <span>حالت تاریک</span> |
| <label class="toggle-switch"> |
| <input type="checkbox" id="themeCheckbox" onchange="toggleTheme()"> |
| <span class="toggle-slider"></span> |
| </label> |
| </div> |
| </div> |
| <button class="settings-btn" onclick="toggleSettingsMenu()"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> |
| تنظیمات کاربری |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalOverlay" onclick="closeAllModals()"></div> |
|
|
| |
| <div class="bottom-modal" id="deleteModal"> |
| <div class="modal-handle"></div> |
| <div class="modal-title">حذف گفتگو</div> |
| <div class="modal-desc">آیا از حذف گفتوگوی خود مطمئن هستی؟</div> |
| <div class="modal-buttons"> |
| <button class="modal-btn btn-cancel" onclick="closeAllModals()">انصراف</button> |
| <button class="modal-btn btn-delete" onclick="confirmDeleteSession()">حذف</button> |
| </div> |
| </div> |
|
|
| |
| <div class="bottom-modal" id="premiumModal"> |
| <div class="modal-handle"></div> |
| <div class="modal-title" style="color: #8b5cf6;">محدودیت نسخه رایگان</div> |
| <div class="modal-desc" id="premiumModalDesc">محدودیت پیامهای روزانه شما به پایان رسیده است. برای ادامه چت بهصورت نامحدود، حسابتان را ارتقا دهید.</div> |
| <div class="modal-buttons"> |
| <button class="modal-btn btn-cancel" onclick="closeAllModals()">متوجه شدم</button> |
| <button class="modal-btn btn-upgrade" onclick="goToPremium()">ارتقا حساب ⭐️</button> |
| </div> |
| </div> |
|
|
| <div class="chat-area" id="chatArea"> |
| <div class="empty-state" id="emptyState">چطور میتونم کمکت کنم؟</div> |
| <div id="dynamicSpacer" style="height: 0px; flex-shrink: 0;"></div> |
| </div> |
|
|
| <div class="input-container"> |
| <div class="status-bar" id="statusBar"></div> |
| |
| <div class="image-preview-box" id="imagePreviewBox"> |
| <button class="close-btn" onclick="clearSelectedImage()">✕</button> |
| <img id="previewImage" src="" alt="پیشنمایش"> |
| </div> |
|
|
| |
| <div id="attachMenu" class="attach-menu" style="display: none;"> |
| <button onclick="selectUploadImage()">🖼️ آپلود عکس</button> |
| <button onclick="selectCreateFile()">📄 ساخت فایل (مقاله)</button> |
| </div> |
|
|
| <div class="input-wrapper"> |
| <button type="button" class="attach-btn" id="attachBtn" onclick="toggleAttachMenu(event)" aria-label="ارسال تصویر یا ساخت فایل"> |
| <svg viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="12" y1="5" x2="12" y2="19"></line> |
| <line x1="5" y1="12" x2="19" y2="12"></line> |
| </svg> |
| </button> |
| |
| |
| <textarea id="textInput" placeholder="پیام خودتو اینجا بنویس..." autocomplete="off" rows="1"></textarea> |
| |
| <button type="button" class="send-btn" id="sendBtn"> |
| <svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let currentAbortController = null; |
| let isGenerating = false; |
| let isFileCreationMode = false; |
| |
| |
| let userSubscriptionStatus = 'free'; |
| const PREMIUM_PAGE_ID = '1149636'; |
| const DAILY_MSG_LIMIT = 20; |
| const DAILY_IMG_LIMIT = 2; |
| const DAILY_FILE_LIMIT = 1; |
| |
| function isUserPaid(userObject) { |
| return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID); |
| } |
| |
| |
| window.addEventListener('message', (event) => { |
| if (event.data?.type === 'USER_DATA_RESPONSE') { |
| if (event.data.error || !event.data.payload) { |
| updateSubscriptionUI('free'); |
| return; |
| } |
| try { |
| const userObject = JSON.parse(event.data.payload); |
| updateSubscriptionUI(isUserPaid(userObject) ? 'paid' : 'free'); |
| } catch (e) { |
| updateSubscriptionUI('free'); |
| } |
| } |
| }); |
| |
| function goToPremium() { |
| parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); |
| } |
| |
| function triggerDownload(url) { |
| if (!url) return; |
| parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: url }, '*'); |
| } |
| |
| function getTodayString() { |
| const date = new Date(); |
| return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; |
| } |
| |
| function getUsage() { |
| const today = getTodayString(); |
| let usage = JSON.parse(localStorage.getItem('alpha_chat_usage') || '{}'); |
| if (usage.date !== today) { |
| |
| usage = { date: today, messages: 0, images: 0, files: 0 }; |
| localStorage.setItem('alpha_chat_usage', JSON.stringify(usage)); |
| } |
| if (usage.files === undefined) { |
| usage.files = 0; |
| } |
| return usage; |
| } |
| |
| function updateSubscriptionUI(status) { |
| userSubscriptionStatus = status; |
| const badge = document.getElementById('userStatusBadge'); |
| const upgradeBtn = document.getElementById('upgradeBtnChat'); |
| |
| if (status === 'paid') { |
| badge.textContent = 'نسخه نامحدود ⭐️'; |
| badge.className = 'status-badge paid'; |
| upgradeBtn.style.display = 'none'; |
| } else { |
| badge.textContent = 'کاربر نسخه رایگان'; |
| badge.className = 'status-badge free'; |
| upgradeBtn.style.display = 'block'; |
| } |
| } |
| |
| function checkCanSend(hasImage, isFile = false) { |
| if (userSubscriptionStatus === 'paid') return true; |
| const usage = getUsage(); |
| |
| if (isFile) { |
| if (usage.files >= DAILY_FILE_LIMIT) { |
| document.getElementById('premiumModalDesc').innerText = "محدودیت ساخت ۱ فایل در روز شما به پایان رسیده است. برای استفاده نامحدود حساب خود را ارتقا دهید."; |
| openPremiumModal(); |
| return false; |
| } |
| return true; |
| } |
| |
| if (usage.messages >= DAILY_MSG_LIMIT) { |
| document.getElementById('premiumModalDesc').innerText = "محدودیت ۲۰ پیام روزانه شما به پایان رسیده است. برای چت نامحدود حساب خود را ارتقا دهید."; |
| openPremiumModal(); |
| return false; |
| } |
| if (hasImage && usage.images >= DAILY_IMG_LIMIT) { |
| document.getElementById('premiumModalDesc').innerText = "محدودیت تحلیل ۲ تصویر در روز به پایان رسیده است. برای استفاده نامحدود حساب خود را ارتقا دهید."; |
| openPremiumModal(); |
| return false; |
| } |
| return true; |
| } |
| |
| function incrementUsage(hasImage, isFile = false) { |
| if (userSubscriptionStatus === 'paid') return; |
| let usage = getUsage(); |
| if (isFile) { |
| usage.files = (usage.files || 0) + 1; |
| } else { |
| usage.messages += 1; |
| if (hasImage) usage.images += 1; |
| } |
| localStorage.setItem('alpha_chat_usage', JSON.stringify(usage)); |
| } |
| |
| |
| let isMuted = false; |
| let globalAudioPlayer = new Audio(); |
| let currentPlayingButton = null; |
| let currentModel = 'gpt5'; |
| let modelActiveSession = { gpt5: null, audio: null }; |
| let currentAttachedImageBase64 = null; |
| let forceNewSession = false; |
| let activeUserRow = null; |
| |
| const PLAY_ICON_SVG = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`; |
| const PAUSE_ICON_SVG = `<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`; |
| const COPY_ICON_SVG = `<svg viewBox="0 0 24 24"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>`; |
| const EDIT_ICON_SVG = `<svg viewBox="0 0 24 24"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>`; |
| const CHECK_ICON_SVG = `<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`; |
| |
| |
| function toggleAttachMenu(event) { |
| event.stopPropagation(); |
| const menu = document.getElementById('attachMenu'); |
| menu.style.display = menu.style.display === 'none' ? 'flex' : 'none'; |
| } |
| |
| function selectUploadImage() { |
| document.getElementById('attachMenu').style.display = 'none'; |
| isFileCreationMode = false; |
| openFilePicker(); |
| } |
| |
| function selectCreateFile() { |
| document.getElementById('attachMenu').style.display = 'none'; |
| isFileCreationMode = true; |
| |
| setTimeout(() => { |
| appendBotMessageUI("📄 شما وارد بخش **ساخت فایل** شدید.\n\nلطفاً موضوع مقالهای که میخواهید ساخته شود را کامل بفرستید.\nمثال: راهنمای جامع مدیریت زمان"); |
| textInput.focus(); |
| }, 100); |
| } |
| |
| |
| document.addEventListener('click', (e) => { |
| const menu = document.getElementById('attachMenu'); |
| const attachBtn = document.getElementById('attachBtn'); |
| if (menu && menu.style.display !== 'none' && !menu.contains(e.target) && !attachBtn.contains(e.target)) { |
| menu.style.display = 'none'; |
| } |
| }); |
| |
| |
| |
| function toggleSettingsMenu() { |
| const menu = document.getElementById('settingsMenu'); |
| menu.classList.toggle('open'); |
| } |
| |
| function toggleTheme() { |
| const isDark = document.getElementById('themeCheckbox').checked; |
| if (isDark) { |
| document.body.classList.add('dark-theme'); |
| localStorage.setItem('appTheme', 'dark'); |
| } else { |
| document.body.classList.remove('dark-theme'); |
| localStorage.setItem('appTheme', 'light'); |
| } |
| } |
| |
| |
| |
| function renderMarkdownBasic(txt) { |
| let html = txt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| |
| |
| html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"); |
| |
| |
| html = html.replace(/^### (.*?)$/gm, '<h3 style="font-size: 15px; margin: 12px 0 6px 0; color: #5a67d8; font-weight: bold;">$1</h3>'); |
| html = html.replace(/^## (.*?)$/gm, '<h2 style="font-size: 17px; margin: 16px 0 8px 0; border-bottom: 1px solid #ccc; padding-bottom: 4px; color: #111; font-weight: bold;">$1</h2>'); |
| html = html.replace(/^# (.*?)$/gm, '<h1 style="font-size: 19px; margin: 20px 0 10px 0; color: #000; font-weight: bold;">$1</h1>'); |
| |
| |
| html = html.replace(/^\s*[\-\*]\s+(.*?)$/gm, '<li style="margin-right: 15px; margin-bottom: 4px; list-style-type: square;">$1</li>'); |
| |
| return html; |
| } |
| |
| function formatMessageText(text) { |
| if (!text) return ""; |
| const escapeHTML = (str) => str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| const codeBlockRegex = /```(.*?)?\n([\s\S]*?)```/g; |
| let lastIndex = 0; |
| let result = ""; |
| let match; |
| |
| while ((match = codeBlockRegex.exec(text)) !== null) { |
| const beforeText = text.substring(lastIndex, match.index); |
| result += renderMarkdownBasic(beforeText); |
| |
| const lang = escapeHTML(match[1].trim()); |
| const code = escapeHTML(match[2]); |
| const uniqueId = 'code-' + Math.random().toString(36).substr(2, 9); |
| |
| result += ` |
| <div class="code-container" style="background: #1e1e1e; border-radius: 8px; margin: 10px 0; direction: ltr; text-align: left; white-space: normal; font-family: sans-serif; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
| <div style="background: #2d2d2d; padding: 8px 12px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444;"> |
| <span style="color: #9cdcfe; font-size: 12px; font-family: monospace;">${lang || 'Code'}</span> |
| <button onclick="copyCodeBlock('${uniqueId}', this)" style="background: transparent; border: none; color: #ccc; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; font-family: inherit; padding: 0;"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> |
| <span class="copy-text">کپی</span> |
| </button> |
| </div> |
| <div style="padding: 12px; overflow-x: auto; color: #d4d4d4; font-size: 13px; line-height: 1.5; white-space: pre;"> |
| <pre style="margin: 0; font-family: monospace;" id="${uniqueId}">${code}</pre> |
| </div> |
| </div>`; |
| |
| lastIndex = codeBlockRegex.lastIndex; |
| } |
| |
| let remainingText = text.substring(lastIndex); |
| |
| const unclosedMatch = /```(.*?)?\n([\s\S]*)$/.exec(remainingText); |
| if (unclosedMatch) { |
| const beforeUnclosed = remainingText.substring(0, unclosedMatch.index); |
| result += renderMarkdownBasic(beforeUnclosed); |
| |
| const lang = escapeHTML(unclosedMatch[1].trim()); |
| const code = escapeHTML(unclosedMatch[2]); |
| const uniqueId = 'code-' + Math.random().toString(36).substr(2, 9); |
| |
| result += ` |
| <div class="code-container" style="background: #1e1e1e; border-radius: 8px; margin: 10px 0; direction: ltr; text-align: left; white-space: normal; font-family: sans-serif; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
| <div style="background: #2d2d2d; padding: 8px 12px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444;"> |
| <span style="color: #9cdcfe; font-size: 12px; font-family: monospace;">${lang || 'Code'}</span> |
| <button style="background: transparent; border: none; color: #888; font-size: 12px; display: flex; align-items: center; gap: 4px; font-family: inherit; padding: 0;"> |
| <span class="copy-text">در حال تایپ...</span> |
| </button> |
| </div> |
| <div style="padding: 12px; overflow-x: auto; color: #d4d4d4; font-size: 13px; line-height: 1.5; white-space: pre;"> |
| <pre style="margin: 0; font-family: monospace;" id="${uniqueId}">${code}</pre> |
| </div> |
| </div>`; |
| } else { |
| result += renderMarkdownBasic(remainingText); |
| } |
| |
| return result; |
| } |
| |
| function copyCodeBlock(id, btn) { |
| const codeElement = document.getElementById(id); |
| if (!codeElement) return; |
| |
| let textToCopy = codeElement.innerText; |
| |
| if (navigator.clipboard && window.isSecureContext) { |
| navigator.clipboard.writeText(textToCopy).then(showSuccess).catch(fallbackCopy); |
| } else { |
| fallbackCopy(); |
| } |
| |
| function showSuccess() { |
| const span = btn.querySelector('.copy-text'); |
| const originalText = span.innerText; |
| span.innerText = "کپی شد ✔"; |
| btn.style.color = "#10b981"; |
| setTimeout(() => { |
| span.innerText = originalText; |
| btn.style.color = "#ccc"; |
| }, 2000); |
| } |
| |
| function fallbackCopy() { |
| const textarea = document.createElement("textarea"); |
| textarea.value = textToCopy; |
| document.body.appendChild(textarea); |
| textarea.select(); |
| try { document.execCommand('copy'); showSuccess(); } catch(e) {} |
| textarea.remove(); |
| } |
| } |
| |
| |
| function updateModelUI(modelName) { |
| currentModel = modelName; |
| const switchContainer = document.getElementById('modelSwitch'); |
| switchContainer.setAttribute('data-active', modelName); |
| document.getElementById('btn-audio').classList.remove('active'); |
| document.getElementById('btn-gpt5').classList.remove('active'); |
| document.getElementById(`btn-${modelName}`).classList.add('active'); |
| const attachBtn = document.getElementById('attachBtn'); |
| if (modelName === 'audio') { |
| attachBtn.style.display = 'none'; |
| document.getElementById('attachMenu').style.display = 'none'; |
| isFileCreationMode = false; |
| clearSelectedImage(); |
| } else { |
| attachBtn.style.display = 'flex'; |
| } |
| } |
| |
| function setModel(modelName) { |
| if (currentModel === modelName) return; |
| |
| modelActiveSession[currentModel] = currentSessionId; |
| |
| updateModelUI(modelName); |
| |
| const targetSessionId = modelActiveSession[currentModel]; |
| if (targetSessionId) { |
| loadSession(targetSessionId, false); |
| } else { |
| startNewSession(); |
| } |
| } |
| |
| function toggleMute() { |
| isMuted = !isMuted; |
| document.getElementById('icon-sound-on').style.display = isMuted ? 'none' : 'block'; |
| document.getElementById('icon-sound-off').style.display = isMuted ? 'block' : 'none'; |
| |
| |
| globalAudioPlayer.muted = isMuted; |
| |
| |
| if (audioContext) { |
| if (isMuted) { |
| audioContext.suspend(); |
| } else { |
| audioContext.resume(); |
| } |
| } |
| } |
| |
| function toggleAudioReplay(audioUrl, btnElement) { |
| if (!audioUrl) return; |
| if (globalAudioPlayer.src.endsWith(audioUrl) && !globalAudioPlayer.paused) { |
| globalAudioPlayer.pause(); |
| btnElement.innerHTML = PLAY_ICON_SVG; |
| return; |
| } |
| if (currentPlayingButton && currentPlayingButton !== btnElement) { |
| currentPlayingButton.innerHTML = PLAY_ICON_SVG; |
| } |
| globalAudioPlayer.src = audioUrl; |
| globalAudioPlayer.play(); |
| btnElement.innerHTML = PAUSE_ICON_SVG; |
| currentPlayingButton = btnElement; |
| globalAudioPlayer.onended = () => { |
| btnElement.innerHTML = PLAY_ICON_SVG; |
| currentPlayingButton = null; |
| }; |
| } |
| |
| function copyMessageText(btnElement) { |
| const messageRow = btnElement.closest('.message-row'); |
| let textToCopy = ""; |
| const bubble = messageRow.querySelector('.message-bubble') || messageRow.querySelector('.doc-content-scroll'); |
| if (bubble) { |
| const clone = bubble.cloneNode(true); |
| const imgs = clone.querySelectorAll('img'); |
| imgs.forEach(img => img.remove()); |
| textToCopy = clone.innerText.trim(); |
| } |
| function showCopiedSuccess() { |
| btnElement.innerHTML = CHECK_ICON_SVG; |
| const originalColor = btnElement.style.color; |
| btnElement.style.color = '#10b981'; |
| statusBar.textContent = "متن کپی شد ✔️"; |
| setTimeout(() => { |
| btnElement.innerHTML = COPY_ICON_SVG; |
| btnElement.style.color = originalColor; |
| statusBar.textContent = ""; |
| }, 2000); |
| } |
| if (navigator.clipboard && window.isSecureContext) { |
| navigator.clipboard.writeText(textToCopy).then(showCopiedSuccess).catch(fallbackCopy); |
| } else { |
| fallbackCopy(); |
| } |
| function fallbackCopy() { |
| const textArea = document.createElement("textarea"); |
| textArea.value = textToCopy; |
| textArea.style.position = "fixed"; |
| textArea.style.left = "-999999px"; |
| textArea.style.top = "-999999px"; |
| document.body.appendChild(textArea); |
| textArea.focus(); |
| textArea.select(); |
| try { |
| document.execCommand('copy'); |
| showCopiedSuccess(); |
| } catch (err) { |
| console.error('Copy failed', err); |
| } |
| textArea.remove(); |
| } |
| } |
| |
| |
| function editUserMessage(btn) { |
| if (isGenerating) return; |
| const row = btn.closest('.message-row'); |
| const bubble = row.querySelector('.message-bubble'); |
| const originalHTML = row.innerHTML; |
| |
| const clone = bubble.cloneNode(true); |
| const imgs = clone.querySelectorAll('img'); |
| imgs.forEach(img => img.remove()); |
| const textToEdit = clone.innerText.trim(); |
| |
| const editContainer = document.createElement('div'); |
| editContainer.style.width = "100%"; |
| editContainer.style.maxWidth = "88%"; |
| editContainer.style.background = "var(--bg-color, #f8f9fa)"; |
| editContainer.style.padding = "10px"; |
| editContainer.style.borderRadius = "12px"; |
| editContainer.style.boxShadow = "0 2px 8px rgba(0,0,0,0.05)"; |
| if(document.body.classList.contains('dark-theme')) { |
| editContainer.style.background = "#2a2a2a"; |
| } |
| |
| const textarea = document.createElement('textarea'); |
| textarea.value = textToEdit; |
| textarea.style.width = "100%"; |
| textarea.style.minHeight = "60px"; |
| textarea.style.borderRadius = "8px"; |
| textarea.style.border = "1px solid #ccc"; |
| textarea.style.padding = "10px"; |
| textarea.style.fontFamily = "inherit"; |
| textarea.style.fontSize = "14px"; |
| textarea.style.resize = "vertical"; |
| textarea.style.marginBottom = "8px"; |
| textarea.style.outline = "none"; |
| if(document.body.classList.contains('dark-theme')) { |
| textarea.style.background = "#1e1e1e"; |
| textarea.style.color = "#eee"; |
| textarea.style.borderColor = "#444"; |
| } |
| |
| const btnContainer = document.createElement('div'); |
| btnContainer.style.display = "flex"; |
| btnContainer.style.gap = "8px"; |
| btnContainer.style.justifyContent = "flex-end"; |
| |
| const cancelBtn = document.createElement('button'); |
| cancelBtn.innerText = "انصراف"; |
| cancelBtn.style.padding = "6px 14px"; |
| cancelBtn.style.borderRadius = "20px"; |
| cancelBtn.style.border = "none"; |
| cancelBtn.style.background = "#e2e8f0"; |
| cancelBtn.style.color = "#475569"; |
| cancelBtn.style.cursor = "pointer"; |
| cancelBtn.style.fontFamily = "inherit"; |
| cancelBtn.style.fontSize = "12px"; |
| cancelBtn.style.fontWeight = "bold"; |
| cancelBtn.onclick = () => { row.innerHTML = originalHTML; }; |
| if(document.body.classList.contains('dark-theme')) { |
| cancelBtn.style.background = "#444"; |
| cancelBtn.style.color = "#ccc"; |
| } |
| |
| const submitBtn = document.createElement('button'); |
| submitBtn.innerText = "تایید و ارسال"; |
| submitBtn.style.padding = "6px 14px"; |
| submitBtn.style.borderRadius = "20px"; |
| submitBtn.style.border = "none"; |
| submitBtn.style.background = "#5a67d8"; |
| submitBtn.style.color = "white"; |
| submitBtn.style.cursor = "pointer"; |
| submitBtn.style.fontFamily = "inherit"; |
| submitBtn.style.fontSize = "12px"; |
| submitBtn.style.fontWeight = "bold"; |
| submitBtn.onclick = () => { |
| const newText = textarea.value.trim(); |
| if(!newText && !row.querySelector('img')) return; |
| |
| const hasImg = !!row.querySelector('img'); |
| if (!checkCanSend(hasImg)) return; |
| |
| incrementUsage(hasImg); |
| submitEditedMessage(row, newText); |
| }; |
| |
| btnContainer.appendChild(cancelBtn); |
| btnContainer.appendChild(submitBtn); |
| editContainer.appendChild(textarea); |
| editContainer.appendChild(btnContainer); |
| |
| row.innerHTML = ''; |
| row.appendChild(editContainer); |
| } |
| |
| function submitEditedMessage(row, newText) { |
| const timestamp = parseInt(row.getAttribute('data-timestamp'), 10); |
| |
| const tx = db.transaction('messages', 'readwrite'); |
| const store = tx.objectStore('messages'); |
| const index = store.index('sessionId'); |
| const request = index.getAll(currentSessionId); |
| |
| request.onsuccess = () => { |
| const allMessages = request.result.sort((a, b) => a.timestamp - b.timestamp); |
| const editedMsgIndex = allMessages.findIndex(m => m.timestamp === timestamp); |
| |
| if (editedMsgIndex === -1) { |
| row.remove(); |
| proceedToSend([], newText, null); |
| return; |
| } |
| |
| const editedMsg = allMessages[editedMsgIndex]; |
| const imageToKeep = editedMsg.imageBase64; |
| |
| const historyMessages = allMessages.slice(0, editedMsgIndex); |
| const messagesToDelete = allMessages.slice(editedMsgIndex); |
| |
| messagesToDelete.forEach(m => store.delete(m.id)); |
| |
| const allRows = Array.from(chatArea.querySelectorAll('.message-row')); |
| const rowIndex = allRows.indexOf(row); |
| if(rowIndex !== -1) { |
| for(let i = rowIndex; i < allRows.length; i++) { |
| allRows[i].remove(); |
| } |
| } |
| |
| proceedToSend(historyMessages, newText, imageToKeep); |
| }; |
| } |
| |
| |
| function openFilePicker() { |
| document.getElementById('textInput').blur(); |
| const fileInput = document.getElementById('imageInput'); |
| fileInput.value = ''; |
| setTimeout(() => { |
| fileInput.click(); |
| }, 50); |
| } |
| |
| function handleImageSelection(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| currentAttachedImageBase64 = e.target.result; |
| document.getElementById('previewImage').src = currentAttachedImageBase64; |
| document.getElementById('imagePreviewBox').style.display = 'block'; |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| function clearSelectedImage() { |
| currentAttachedImageBase64 = null; |
| document.getElementById('imageInput').value = ''; |
| document.getElementById('imagePreviewBox').style.display = 'none'; |
| document.getElementById('previewImage').src = ''; |
| } |
| |
| const DB_NAME = 'AlphaChatDB_V2'; |
| const DB_VERSION = 1; |
| let db; |
| let currentSessionId = null; |
| let sessionToDelete = null; |
| |
| function initDB() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(DB_NAME, DB_VERSION); |
| request.onupgradeneeded = (event) => { |
| const database = event.target.result; |
| if (!database.objectStoreNames.contains('sessions')) database.createObjectStore('sessions', { keyPath: 'id' }); |
| if (!database.objectStoreNames.contains('messages')) { |
| const msgStore = database.createObjectStore('messages', { keyPath: 'id', autoIncrement: true }); |
| msgStore.createIndex('sessionId', 'sessionId', { unique: false }); |
| } |
| }; |
| request.onsuccess = (event) => { db = event.target.result; resolve(); }; |
| request.onerror = (event) => reject(); |
| }); |
| } |
| |
| async function createNewSession(title = "چت جدید") { |
| currentSessionId = Date.now().toString(); |
| modelActiveSession[currentModel] = currentSessionId; |
| const session = { id: currentSessionId, title: title, timestamp: Date.now(), model: currentModel }; |
| return new Promise((resolve) => { |
| const tx = db.transaction('sessions', 'readwrite'); |
| tx.objectStore('sessions').add(session); |
| tx.oncomplete = () => { loadSessionsToMenu(); resolve(currentSessionId); }; |
| }); |
| } |
| |
| async function saveMessage(role, text, audioUrl = null, imageBase64 = null, timestamp = null, pdfUrl = null, docxUrl = null, isDocument = false) { |
| let displayTitle = text ? text.substring(0, 20) + "..." : "تصویر ارسال شد"; |
| if (isDocument) { |
| displayTitle = "📄 مقاله: " + displayTitle; |
| } |
| if (!currentSessionId) { |
| await createNewSession(displayTitle); |
| } |
| const msgTime = timestamp || Date.now(); |
| const message = { |
| sessionId: currentSessionId, |
| role: role, |
| text: text, |
| audioUrl: audioUrl, |
| imageBase64: imageBase64, |
| timestamp: msgTime, |
| pdfUrl: pdfUrl, |
| docxUrl: docxUrl, |
| isDocument: isDocument |
| }; |
| const tx = db.transaction('messages', 'readwrite'); |
| tx.objectStore('messages').add(message); |
| if (role === 'user') { |
| const sessionTx = db.transaction('sessions', 'readwrite'); |
| const store = sessionTx.objectStore('sessions'); |
| const getReq = store.get(currentSessionId); |
| getReq.onsuccess = () => { |
| if (getReq.result && getReq.result.title === "چت جدید") { |
| getReq.result.title = displayTitle; |
| store.put(getReq.result); |
| loadSessionsToMenu(); |
| } |
| }; |
| } |
| } |
| |
| function loadSessionsToMenu() { |
| const tx = db.transaction('sessions', 'readonly'); |
| const request = tx.objectStore('sessions').getAll(); |
| request.onsuccess = () => { |
| const sessions = request.result.sort((a, b) => b.timestamp - a.timestamp); |
| const list = document.getElementById('sessionList'); |
| list.innerHTML = ''; |
| sessions.forEach(s => { |
| const li = document.createElement('li'); |
| li.className = `session-item ${s.id === currentSessionId ? 'active' : ''}`; |
| li.onclick = (e) => { |
| if (!e.target.closest('.dots-btn') && !e.target.closest('.popover-menu')) { |
| const targetModel = s.model || 'gpt5'; |
| if (currentModel !== targetModel) { |
| modelActiveSession[currentModel] = currentSessionId; |
| updateModelUI(targetModel); |
| } |
| loadSession(s.id, true); |
| } |
| }; |
| |
| const isAudio = (s.model === 'audio'); |
| const modelBadge = isAudio ? '🎤 ' : '🤖 '; |
| |
| li.innerHTML = ` |
| <div class="session-item-content"> |
| <img src="https://uploadkon.ir/uploads/d90813_26IMG-20260513-155052-256.jpg" style="width: 20px; height: 20px; flex-shrink: 0; border-radius: 4px; object-fit: cover;" alt="logo"> |
| <span class="session-title" style="direction:rtl; text-align:right;">${modelBadge}${s.title || 'بدون عنوان'}</span> |
| </div> |
| <button class="dots-btn" onclick="toggleDeleteMenu(event, '${s.id}')"> |
| <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg> |
| </button> |
| <div class="popover-menu" id="popover-${s.id}"> |
| <button class="popover-btn" onclick="openDeleteModal(event, '${s.id}')"> |
| <span>حذف</span> |
| <svg viewBox="0 0 24 24"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> |
| </button> |
| </div> |
| `; |
| list.appendChild(li); |
| }); |
| }; |
| } |
| |
| function loadSession(sessionId, fromSidebar = true) { |
| currentSessionId = sessionId; |
| modelActiveSession[currentModel] = sessionId; |
| clearChatUI(); |
| if (fromSidebar) toggleSidebar(); |
| loadSessionsToMenu(); |
| const tx = db.transaction('messages', 'readonly'); |
| const request = tx.objectStore('messages').index('sessionId').getAll(sessionId); |
| request.onsuccess = () => { |
| const messages = request.result.sort((a, b) => a.timestamp - b.timestamp); |
| if(messages.length === 0) { |
| document.getElementById('emptyState').style.display = 'block'; |
| } else { |
| document.getElementById('emptyState').style.display = 'none'; |
| messages.forEach(msg => { |
| if(msg.role === 'user') { |
| appendUserMessageUI(msg.text, true, msg.imageBase64, msg.timestamp); |
| } else { |
| if (msg.isDocument) { |
| appendBotDocumentUI(msg.text, msg.pdfUrl, msg.docxUrl, true); |
| } else { |
| appendBotMessageUI(msg.text, msg.audioUrl, true); |
| } |
| } |
| }); |
| setTimeout(() => { |
| const rows = chatArea.querySelectorAll('.message-row'); |
| if (rows.length > 0) { |
| const lastRow = rows[rows.length - 1]; |
| document.getElementById('dynamicSpacer').style.height = '0px'; |
| let targetPos = lastRow.offsetTop + lastRow.clientHeight - chatArea.clientHeight + 140; |
| chatArea.scrollTo({ top: Math.max(0, targetPos), behavior: 'auto' }); |
| } |
| }, 50); |
| } |
| }; |
| } |
| |
| function startNewSession() { |
| currentSessionId = null; |
| modelActiveSession[currentModel] = null; |
| clearChatUI(); |
| document.getElementById('emptyState').style.display = 'block'; |
| loadSessionsToMenu(); |
| } |
| |
| function clearChatUI() { |
| document.querySelectorAll('.message-row').forEach(m => m.remove()); |
| document.getElementById('emptyState').style.display = 'none'; |
| document.getElementById('dynamicSpacer').style.height = '0px'; |
| activeUserRow = null; |
| isFileCreationMode = false; |
| if(!globalAudioPlayer.paused) globalAudioPlayer.pause(); |
| |
| activeSources.forEach(source => { |
| try { source.stop(); } catch(e) {} |
| }); |
| activeSources = []; |
| nextAudioTime = 0; |
| } |
| |
| function toggleDeleteMenu(event, sessionId) { |
| event.stopPropagation(); |
| document.querySelectorAll('.popover-menu').forEach(menu => { |
| if (menu.id !== `popover-${sessionId}`) menu.classList.remove('show'); |
| }); |
| const popover = document.getElementById(`popover-${sessionId}`); |
| popover.classList.toggle('show'); |
| } |
| |
| |
| function closeAllModals() { |
| sessionToDelete = null; |
| document.getElementById('modalOverlay').classList.remove('open'); |
| document.getElementById('deleteModal').classList.remove('open'); |
| document.getElementById('premiumModal').classList.remove('open'); |
| } |
| |
| function openDeleteModal(event, sessionId) { |
| event.stopPropagation(); |
| sessionToDelete = sessionId; |
| document.getElementById('modalOverlay').classList.add('open'); |
| document.getElementById('deleteModal').classList.add('open'); |
| document.querySelectorAll('.popover-menu').forEach(m => m.classList.remove('show')); |
| } |
| |
| |
| function confirmDeleteSession() { |
| if (!sessionToDelete) return; |
| |
| const tx = db.transaction(['sessions', 'messages'], 'readwrite'); |
| const sessionStore = tx.objectStore('sessions'); |
| const msgStore = tx.objectStore('messages'); |
| |
| |
| sessionStore.delete(sessionToDelete); |
| |
| |
| const index = msgStore.index('sessionId'); |
| const request = index.getAll(sessionToDelete); |
| |
| request.onsuccess = () => { |
| request.result.forEach(msg => { |
| msgStore.delete(msg.id); |
| }); |
| }; |
| |
| tx.oncomplete = () => { |
| |
| if (currentSessionId === sessionToDelete) { |
| startNewSession(); |
| } |
| closeAllModals(); |
| loadSessionsToMenu(); |
| }; |
| } |
| |
| |
| function openPremiumModal() { |
| document.getElementById('modalOverlay').classList.add('open'); |
| document.getElementById('premiumModal').classList.add('open'); |
| } |
| |
| |
| const chatArea = document.getElementById('chatArea'); |
| const textInput = document.getElementById('textInput'); |
| const sendBtn = document.getElementById('sendBtn'); |
| const statusBar = document.getElementById('statusBar'); |
| |
| |
| function adjustTextInputHeight() { |
| textInput.style.height = 'auto'; |
| textInput.style.height = textInput.scrollHeight + 'px'; |
| } |
| textInput.addEventListener('input', adjustTextInputHeight); |
| |
| |
| function toggleSidebar() { |
| document.getElementById('sidebar').classList.toggle('open'); |
| document.getElementById('overlay').classList.toggle('visible'); |
| } |
| |
| function appendUserMessageUI(text, isRestore = false, imageBase64 = null, timestamp = null) { |
| document.getElementById('emptyState').style.display = 'none'; |
| const row = document.createElement('div'); |
| row.className = 'message-row user'; |
| if (timestamp) row.setAttribute('data-timestamp', timestamp); |
| |
| let html = '<div class="message-bubble">'; |
| if (imageBase64) { |
| let src = imageBase64; |
| if (!src.startsWith('data:')) src = 'data:image/jpeg;base64,' + src; |
| html += `<img src="${src}" style="max-width: 100%; border-radius: 8px; margin-bottom: 8px; display: block;">`; |
| } |
| if (text) { |
| html += text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| } |
| html += '</div>'; |
| |
| html += `<div class="action-buttons">`; |
| html += `<button class="action-btn" title="ویرایش" onclick="editUserMessage(this)">${EDIT_ICON_SVG}</button>`; |
| html += `<button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button>`; |
| html += `</div>`; |
| |
| row.innerHTML = html; |
| chatArea.insertBefore(row, document.getElementById('dynamicSpacer')); |
| if (!isRestore) { |
| activeUserRow = row; |
| setTimeout(() => { |
| const visibleSpace = chatArea.clientHeight - 130; |
| let requiredSpacer = visibleSpace - row.offsetHeight - 20; |
| if (requiredSpacer < 0) requiredSpacer = 0; |
| |
| let targetScrollPosition = row.offsetTop - 20; |
| |
| if (row.offsetHeight > 150) { |
| requiredSpacer = chatArea.clientHeight - 100; |
| targetScrollPosition = (row.offsetTop + row.offsetHeight) - 130; |
| } |
| |
| document.getElementById('dynamicSpacer').style.height = requiredSpacer + 'px'; |
| chatArea.scrollTo({ top: targetScrollPosition, behavior: 'smooth' }); |
| }, 150); |
| } |
| } |
| |
| function appendBotMessageUI(text, audioUrl = null, isRestore = false) { |
| document.getElementById('emptyState').style.display = 'none'; |
| const row = document.createElement('div'); |
| row.className = 'message-row bot'; |
| let html = `<div class="message-bubble">${formatMessageText(text)}</div><div class="action-buttons">`; |
| html += `<button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button>`; |
| if (audioUrl) { |
| html += `<button class="action-btn play-btn" title="پخش صدا" onclick="toggleAudioReplay('${audioUrl}', this)">${PLAY_ICON_SVG}</button>`; |
| } |
| html += `</div>`; |
| row.innerHTML = html; |
| chatArea.insertBefore(row, document.getElementById('dynamicSpacer')); |
| } |
| |
| |
| function appendBotDocumentUI(text, pdfUrl, docxUrl, isRestore = false) { |
| document.getElementById('emptyState').style.display = 'none'; |
| const row = document.createElement('div'); |
| row.className = 'message-row bot'; |
| |
| const cardId = 'doc-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5); |
| const contentId = 'content-' + cardId; |
| |
| let pdfHtml = ''; |
| if (pdfUrl) { |
| pdfHtml = ` |
| <div class="document-card" style="width:100%; border-right: 4px solid #ef4444;"> |
| <div class="doc-header" style="border-bottom:none; padding-bottom:0;"> |
| <div class="doc-download-actions visible"> |
| <button type="button" onclick="triggerDownload('${pdfUrl}')" class="doc-dl-btn" title="دانلود فایل PDF"> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </svg> |
| </button> |
| </div> |
| <div class="doc-title-container"> |
| <span>فایل PDF مقاله</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
| |
| let docxHtml = ''; |
| if (docxUrl) { |
| docxHtml = ` |
| <div class="document-card" style="width:100%; border-right: 4px solid #2563eb;"> |
| <div class="doc-header" style="border-bottom:none; padding-bottom:0;"> |
| <div class="doc-download-actions visible"> |
| <button type="button" onclick="triggerDownload('${docxUrl}')" class="doc-dl-btn" title="دانلود فایل Word"> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </svg> |
| </button> |
| </div> |
| <div class="doc-title-container"> |
| <span>فایل Word مقاله</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
| |
| row.innerHTML = ` |
| <div style="display:flex; flex-direction:column; gap:12px; width:100%; max-width:320px;"> |
| <div class="document-card" style="width:100%;"> |
| <div class="doc-header"> |
| <div class="doc-title-container" style="width: 100%; justify-content: flex-start;"> |
| <span>متن مقاله ساخته شده</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| <div class="doc-content-scroll" id="${contentId}"> |
| ${formatMessageText(text)} |
| </div> |
| </div> |
| ${pdfHtml} |
| ${docxHtml} |
| </div> |
| <div class="action-buttons"> |
| <button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button> |
| </div> |
| `; |
| |
| chatArea.insertBefore(row, document.getElementById('dynamicSpacer')); |
| } |
| |
| let currentBotRow = null; |
| let currentBotBubble = null; |
| let currentBotText = ""; |
| let audioContext; |
| let nextAudioTime = 0; |
| let activeSources = []; |
| |
| function initAudioContext() { |
| if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); |
| if (audioContext.state === 'suspended') audioContext.resume(); |
| } |
| |
| function playStreamingAudio(base64Data) { |
| if (!base64Data || isMuted) return; |
| const binaryString = atob(base64Data); |
| const len = binaryString.length; |
| const bytes = new Uint8Array(len); |
| for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i); |
| const int16Array = new Int16Array(bytes.buffer); |
| const float32Array = new Float32Array(int16Array.length); |
| for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768.0; |
| const audioBuffer = audioContext.createBuffer(1, float32Array.length, 24000); |
| audioBuffer.getChannelData(0).set(float32Array); |
| const source = audioContext.createBufferSource(); |
| source.buffer = audioBuffer; |
| source.connect(audioContext.destination); |
| |
| source.onended = () => { |
| const index = activeSources.indexOf(source); |
| if (index > -1) activeSources.splice(index, 1); |
| }; |
| activeSources.push(source); |
| |
| |
| if (nextAudioTime < audioContext.currentTime) { |
| nextAudioTime = audioContext.currentTime; |
| } |
| |
| source.start(nextAudioTime); |
| nextAudioTime += audioBuffer.duration; |
| } |
| |
| function updateDynamicSpacer() { |
| if (!activeUserRow || !currentBotRow) return; |
| const spacer = document.getElementById('dynamicSpacer'); |
| |
| |
| if (activeUserRow.offsetHeight > 150) { |
| const requiredSpacer = chatArea.clientHeight - 100 - currentBotRow.offsetHeight; |
| spacer.style.height = Math.max(0, requiredSpacer) + 'px'; |
| return; |
| } |
| |
| const turnHeight = activeUserRow.offsetHeight + 20 + currentBotRow.offsetHeight; |
| const visibleSpace = chatArea.clientHeight - 130; |
| if (turnHeight < visibleSpace) { |
| spacer.style.height = (visibleSpace - turnHeight) + 'px'; |
| } else { |
| spacer.style.height = '0px'; |
| } |
| } |
| |
| |
| async function proceedToCreateFile(topic) { |
| const msgTimestamp = Date.now(); |
| appendUserMessageUI(topic, false, null, msgTimestamp); |
| await saveMessage('user', topic, null, null, msgTimestamp); |
| |
| isGenerating = true; |
| sendBtn.innerHTML = `<svg viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" fill="white" stroke="none" rx="2" ry="2"/></svg>`; |
| sendBtn.style.backgroundColor = "#5a67d8"; |
| statusBar.textContent = "در حال ایجاد مقاله..."; |
| |
| currentBotRow = document.createElement('div'); |
| currentBotRow.className = 'message-row bot'; |
| |
| const cardId = 'doc-' + Date.now(); |
| const contentId = 'content-' + cardId; |
| const pdfCardContainerId = 'pdf-container-' + cardId; |
| const wordCardContainerId = 'word-container-' + cardId; |
| |
| |
| currentBotRow.innerHTML = ` |
| <div style="display:flex; flex-direction:column; gap:12px; width:100%; max-width:320px;"> |
| <!-- کادر اول: فقط متن مقاله تایپ شده بدون دکمههای دانلود --> |
| <div class="document-card" style="width:100%;"> |
| <div class="doc-header"> |
| <div class="doc-title-container" style="width: 100%; justify-content: flex-start;"> |
| <span>متن مقاله: ${topic.substring(0, 18)}${topic.length > 18 ? '...' : ''}</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| <div class="doc-content-scroll" id="${contentId}"> |
| <div class="typing-indicator" style="padding:0; justify-content:center;"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div> |
| </div> |
| </div> |
| |
| <!-- کادر دوم: فایل PDF که پس از اتمام ساخت ظاهر میشود --> |
| <div id="${pdfCardContainerId}"></div> |
| |
| <!-- کادر سوم: فایل Word که پس از اتمام ساخت ظاهر میشود --> |
| <div id="${wordCardContainerId}"></div> |
| </div> |
| <div class="action-buttons"> |
| <button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button> |
| </div> |
| `; |
| |
| chatArea.insertBefore(currentBotRow, document.getElementById('dynamicSpacer')); |
| updateDynamicSpacer(); |
| chatArea.scrollTo({ top: chatArea.scrollHeight, behavior: 'smooth' }); |
| |
| const contentBox = document.getElementById(contentId); |
| let fullText = ""; |
| |
| try { |
| const response = await fetch('/api/create_file', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ topic: topic }) |
| }); |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder("utf-8"); |
| let isFirstChunk = true; |
| let buffer = ""; |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| |
| buffer += decoder.decode(value, { stream: true }); |
| let lines = buffer.split('\n'); |
| buffer = lines.pop(); |
| |
| for (let line of lines) { |
| line = line.trim(); |
| if (!line) continue; |
| if (line.startsWith("data: ")) { |
| try { |
| const data = JSON.parse(line.substring(6)); |
| |
| if (data.type === 'text') { |
| if (isFirstChunk) { |
| contentBox.innerHTML = ''; |
| isFirstChunk = false; |
| } |
| |
| const wasAtBottom = (chatArea.scrollHeight - chatArea.scrollTop - chatArea.clientHeight) < 20; |
| |
| fullText += data.content; |
| contentBox.innerHTML = formatMessageText(fullText); |
| updateDynamicSpacer(); |
| |
| |
| if (wasAtBottom) { |
| chatArea.scrollTop = chatArea.scrollHeight; |
| } |
| } |
| else if (data.type === 'status') { |
| statusBar.textContent = data.content; |
| } |
| else if (data.type === 'done') { |
| if(data.pdf_url) { |
| const pdfBox = document.getElementById(pdfCardContainerId); |
| pdfBox.innerHTML = ` |
| <div class="document-card" style="width:100%; border-right: 4px solid #ef4444; animation: messageSlideUp 0.3s forwards;"> |
| <div class="doc-header" style="border-bottom:none; padding-bottom:0;"> |
| <div class="doc-download-actions visible"> |
| <button type="button" onclick="triggerDownload('${data.pdf_url}')" class="doc-dl-btn" title="دانلود فایل PDF"> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </svg> |
| </button> |
| </div> |
| <div class="doc-title-container"> |
| <span>فایل PDF: ${topic.substring(0, 18)}${topic.length > 18 ? '...' : ''}</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
| |
| if(data.docx_url) { |
| const wordBox = document.getElementById(wordCardContainerId); |
| wordBox.innerHTML = ` |
| <div class="document-card" style="width:100%; border-right: 4px solid #2563eb; animation: messageSlideUp 0.3s forwards;"> |
| <div class="doc-header" style="border-bottom:none; padding-bottom:0;"> |
| <div class="doc-download-actions visible"> |
| <button type="button" onclick="triggerDownload('${data.docx_url}')" class="doc-dl-btn" title="دانلود فایل Word"> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </svg> |
| </button> |
| </div> |
| <div class="doc-title-container"> |
| <span>فایل Word: ${topic.substring(0, 18)}${topic.length > 18 ? '...' : ''}</span> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
| |
| saveMessage('bot', fullText, null, null, Date.now(), data.pdf_url, data.docx_url, true); |
| } |
| else if (data.type === 'error') { |
| contentBox.innerHTML += `<br><span style="color:red;">خطا: ${data.message}</span>`; |
| } |
| } catch(e) {} |
| } |
| } |
| } |
| } catch (err) { |
| contentBox.innerHTML = `<span style="color:red;">خطا در ارتباط با سرور.</span>`; |
| } finally { |
| isGenerating = false; |
| sendBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg>`; |
| sendBtn.style.backgroundColor = ""; |
| statusBar.textContent = ""; |
| document.getElementById('dynamicSpacer').style.height = '0px'; |
| currentBotRow = null; |
| } |
| } |
| |
| |
| async function proceedToSend(messages, text, base64Image) { |
| let textHistoryStr = ""; |
| messages.sort((a, b) => a.timestamp - b.timestamp).forEach(msg => { |
| if (msg.role === 'user' && msg.text) { |
| textHistoryStr += "کاربر: " + msg.text + "\n"; |
| } else if (msg.role === 'bot' && msg.text) { |
| textHistoryStr += "هوش مصنوعی: " + msg.text + "\n"; |
| } |
| }); |
| |
| const msgTimestamp = Date.now(); |
| appendUserMessageUI(text, false, base64Image, msgTimestamp); |
| await saveMessage('user', text, null, base64Image, msgTimestamp); |
| |
| isGenerating = true; |
| currentAbortController = new AbortController(); |
| |
| sendBtn.innerHTML = `<svg viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" fill="white" stroke="none" rx="2" ry="2"/></svg>`; |
| sendBtn.style.backgroundColor = "#5a67d8"; |
| |
| statusBar.textContent = "در حال پردازش..."; |
| currentBotRow = document.createElement('div'); |
| currentBotRow.className = 'message-row bot'; |
| currentBotBubble = document.createElement('div'); |
| currentBotBubble.className = 'message-bubble'; |
| |
| currentBotBubble.innerHTML = `<div class="typing-indicator" id="typingIndicator"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>`; |
| |
| currentBotText = ""; |
| currentBotRow.appendChild(currentBotBubble); |
| chatArea.insertBefore(currentBotRow, document.getElementById('dynamicSpacer')); |
| |
| try { |
| const pureBase64 = base64Image ? base64Image.split(',')[1] : null; |
| const response = await fetch('/api/chat_proxy', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| signal: currentAbortController.signal, |
| body: JSON.stringify({ |
| type: 'text', content: text, model: currentModel, |
| text_history: textHistoryStr, image_base64: pureBase64, |
| session_id: currentSessionId |
| }) |
| }); |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder("utf-8"); |
| let streamBuffer = ""; |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) { |
| if (streamBuffer.trim()) { |
| try { |
| const data = JSON.parse(streamBuffer); |
| if (data.status === "streaming") { |
| if (data.text) { |
| currentBotText += data.text; |
| currentBotBubble.innerHTML = formatMessageText(currentBotText); |
| } |
| if (data.audio) playStreamingAudio(data.audio); |
| } |
| } catch(e) {} |
| } |
| break; |
| } |
| |
| streamBuffer += decoder.decode(value, { stream: true }); |
| const lines = streamBuffer.split('\n'); |
| streamBuffer = lines.pop(); |
| |
| for (let line of lines) { |
| if (!line.trim()) continue; |
| try { |
| const data = JSON.parse(line); |
| if (data.status === "streaming") { |
| if (data.text) { |
| |
| const wasAtBottom = (chatArea.scrollHeight - chatArea.scrollTop - chatArea.clientHeight) < 20; |
| |
| currentBotText += data.text; |
| currentBotBubble.innerHTML = formatMessageText(currentBotText); |
| updateDynamicSpacer(); |
| |
| |
| if (wasAtBottom) { |
| chatArea.scrollTop = chatArea.scrollHeight; |
| } |
| } |
| if (data.audio) playStreamingAudio(data.audio); |
| } |
| else if (data.status === "success" || data.status === "error") { |
| statusBar.textContent = ""; |
| if (data.status === "error") { |
| currentBotText = data.message || "خطا در برقراری ارتباط"; |
| |
| } |
| let finalHtml = `<div class="message-bubble">${formatMessageText(currentBotText)}</div><div class="action-buttons">`; |
| finalHtml += `<button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button>`; |
| if (data.audio_url) { |
| finalHtml += `<button class="action-btn play-btn" title="پخش صدا" onclick="toggleAudioReplay('${data.audio_url}', this)">${PLAY_ICON_SVG}</button>`; |
| } |
| finalHtml += `</div>`; |
| currentBotRow.innerHTML = finalHtml; |
| updateDynamicSpacer(); |
| if (data.status === "success") { |
| saveMessage('bot', currentBotText, data.audio_url, null, Date.now()); |
| } |
| |
| currentBotRow = null; currentBotBubble = null; currentBotText = ""; |
| } |
| } catch(e) {} |
| } |
| } |
| } catch (error) { |
| |
| if (error.name === 'AbortError') { |
| statusBar.textContent = "تولید پیام متوقف شد"; |
| |
| |
| if (!globalAudioPlayer.paused) globalAudioPlayer.pause(); |
| activeSources.forEach(source => { try { source.stop(); } catch(e) {} }); |
| activeSources = []; |
| nextAudioTime = 0; |
| } else { |
| statusBar.textContent = "اتصال قطع شد یا زمان پاسخ به پایان رسید"; |
| |
| } |
| |
| |
| if (currentBotRow) { |
| let finalHtml = `<div class="message-bubble">${formatMessageText(currentBotText)}</div><div class="action-buttons">`; |
| finalHtml += `<button class="action-btn" title="کپی" onclick="copyMessageText(this)">${COPY_ICON_SVG}</button>`; |
| |
| finalHtml += `</div>`; |
| currentBotRow.innerHTML = finalHtml; |
| updateDynamicSpacer(); |
| |
| |
| if (currentBotText.trim() !== "") { |
| saveMessage('bot', currentBotText, null, null, Date.now()); |
| } |
| currentBotRow = null; currentBotBubble = null; currentBotText = ""; |
| } |
| } finally { |
| isGenerating = false; |
| sendBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg>`; |
| sendBtn.style.backgroundColor = ""; |
| statusBar.textContent = ""; |
| currentBotRow = null; |
| } |
| } |
| |
| sendBtn.addEventListener('click', () => { |
| if (isGenerating) { |
| if (currentAbortController) { |
| currentAbortController.abort(); |
| } |
| return; |
| } |
| |
| initAudioContext(); |
| if (forceNewSession) { |
| startNewSession(); |
| forceNewSession = false; |
| } |
| |
| const text = textInput.value.trim(); |
| |
| |
| if (isFileCreationMode && text) { |
| if (!checkCanSend(false, true)) { |
| return; |
| } |
| incrementUsage(false, true); |
| isFileCreationMode = false; |
| textInput.value = ''; |
| textInput.style.height = 'auto'; |
| proceedToCreateFile(text); |
| return; |
| } |
| |
| const imageToSend = currentAttachedImageBase64; |
| const hasImage = !!imageToSend; |
| |
| if (!text && !imageToSend) return; |
| |
| if (!checkCanSend(hasImage)) { |
| return; |
| } |
| |
| incrementUsage(hasImage); |
| |
| textInput.value = ''; |
| textInput.style.height = 'auto'; |
| clearSelectedImage(); |
| |
| if (!currentSessionId) { |
| proceedToSend([], text, imageToSend); |
| } else { |
| const tx = db.transaction('messages', 'readonly'); |
| const request = tx.objectStore('messages').index('sessionId').getAll(currentSessionId); |
| request.onsuccess = () => { |
| proceedToSend(request.result || [], text, imageToSend); |
| }; |
| } |
| }); |
| |
| textInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') sendBtn.click(); |
| }); |
| |
| window.onload = async () => { |
| document.getElementById('emptyState').style.display = 'block'; |
| |
| |
| const savedTheme = localStorage.getItem('appTheme'); |
| if (savedTheme === 'dark') { |
| document.getElementById('themeCheckbox').checked = true; |
| document.body.classList.add('dark-theme'); |
| } |
| |
| |
| parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*'); |
| |
| await initDB(); |
| loadSessionsToMenu(); |
| }; |
| </script> |
|
|
| </body> |
| </html> |