montana/Русский/Логистика/index_v1_backup.html

3048 lines
124 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>SeaFare Montana — Maritime Logistics</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://accounts.google.com/gsi/client" async defer></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-dark: #0a1628;
--bg-panel: #111d35;
--bg-chat: #0c1522;
--bg-input: #162240;
--bg-hover: rgba(255, 255, 255, 0.04);
--accent: #00b4d8;
--accent2: #0077b6;
--text: #e0e6ed;
--text-dim: #7b8da3;
--user-msg: #1a3a5c;
--bot-msg: rgba(22, 34, 64, 0.45);
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.14);
--success: #00c853;
--warn: #ffa726;
}
body {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-dark);
color: var(--text);
height: 100vh;
height: 100dvh;
display: flex;
overflow: hidden;
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Sidebar */
.sidebar {
width: 280px;
background: var(--bg-panel);
border-right: 1px solid var(--border-strong);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.logo-icon {
width: 42px;
height: 42px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.logo-text h1 {
font-size: 16px;
font-weight: 600;
color: var(--text);
}
.logo-text span {
font-size: 11px;
color: var(--text-dim);
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: rgba(0, 200, 83, 0.1);
border: 1px solid rgba(0, 200, 83, 0.3);
border-radius: 20px;
font-size: 11px;
color: var(--success);
margin-top: 12px;
}
.status-dot {
width: 6px;
height: 6px;
background: var(--success);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Services */
.services {
padding: 16px 20px;
flex: 1;
overflow-y: auto;
}
.services h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
margin-bottom: 12px;
}
.service-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
margin-bottom: 4px;
cursor: pointer;
transition: background 0.2s;
}
.service-item:hover {
background: rgba(0, 180, 216, 0.1);
}
.service-icon {
font-size: 18px;
width: 32px;
text-align: center;
}
.service-info {
flex: 1;
}
.service-info .name {
font-size: 13px;
font-weight: 500;
}
.service-info .price {
font-size: 11px;
color: var(--text-dim);
}
.price-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: rgba(0, 180, 216, 0.15);
color: var(--accent);
font-weight: 600;
}
.price-free {
background: rgba(0, 200, 83, 0.15);
color: var(--success);
}
/* Sidebar footer */
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-dim);
text-align: center;
}
.sidebar-footer a {
color: var(--accent);
text-decoration: none;
}
/* Main chat area */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
/* Chat header */
.chat-header {
padding: 16px 24px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border-strong);
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.chat-header-left h2 {
font-size: 15px;
font-weight: 600;
}
.chat-header-left span {
font-size: 12px;
color: var(--text-dim);
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.header-btn {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.header-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Language switcher */
.lang-switcher {
display: flex;
gap: 2px;
background: var(--bg-input);
border-radius: 6px;
padding: 2px;
border: 1px solid var(--border);
}
.lang-btn {
padding: 4px 10px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.lang-btn:hover {
color: var(--text);
}
.lang-btn.active {
background: var(--accent2);
color: white;
}
/* Messages */
.messages {
flex: 1;
overflow-y: auto;
padding: 28px 24px 100px;
display: flex;
flex-direction: column;
gap: 24px;
background: var(--bg-chat);
scroll-behavior: smooth;
}
.message {
display: flex;
gap: 12px;
max-width: 85%;
animation: messageIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes messageIn {
from { opacity: 0; transform: translateY(10px) scale(0.98); filter: blur(3px); }
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.msg-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.message.bot .msg-avatar {
background: linear-gradient(135deg, var(--accent), var(--accent2));
}
.message.user .msg-avatar {
background: var(--user-msg);
}
.msg-content {
padding: 16px 20px;
border-radius: 18px;
font-size: 15px;
line-height: 1.65;
letter-spacing: -0.01em;
}
.message.bot .msg-content {
background: var(--bot-msg);
border-radius: 18px;
}
.message.user .msg-content {
background: linear-gradient(135deg, var(--accent2), #005a99);
color: white;
border-radius: 20px;
padding: 12px 18px;
}
.msg-content pre {
background: rgba(0, 0, 0, 0.35);
padding: 14px 18px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.06);
margin: 10px 0;
font-size: 13px;
overflow-x: auto;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
line-height: 1.6;
}
.msg-content code {
background: rgba(0, 0, 0, 0.3);
padding: 2px 7px;
border-radius: 5px;
font-size: 13px;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
}
.msg-content .vessel-card {
background: rgba(0, 180, 216, 0.06);
border: 1px solid rgba(0, 180, 216, 0.15);
border-radius: 14px;
padding: 16px;
margin: 10px 0;
backdrop-filter: blur(8px);
}
.vessel-card .vessel-name {
font-size: 15px;
font-weight: 600;
color: var(--accent);
margin-bottom: 10px;
}
.vessel-card .vessel-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 13px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.vessel-card .vessel-row:last-child { border-bottom: none; }
.vessel-card .vessel-label {
color: var(--text-dim);
}
.msg-time {
font-size: 10px;
color: var(--text-dim);
margin-top: 6px;
padding: 0 4px;
opacity: 0.7;
}
.message.user .msg-time {
text-align: right;
}
/* Typing indicator */
.typing {
display: flex;
gap: 5px;
padding: 8px 4px;
}
.typing span {
width: 6px;
height: 6px;
background: var(--accent);
border-radius: 50%;
animation: typingPulse 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
opacity: 0.4;
}
.typing span:nth-child(2) { animation-delay: 0.15s; }
.typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes typingPulse {
0%, 60%, 100% { transform: translateY(0) scale(1); opacity: 0.4; }
30% { transform: translateY(-5px) scale(1.2); opacity: 1; }
}
/* Input area */
.input-area {
padding: 0 24px 20px;
background: linear-gradient(to top, var(--bg-panel) 60%, transparent);
position: relative;
margin-top: -80px;
pointer-events: none;
padding-top: 80px;
}
.input-area > * { pointer-events: auto; }
.input-glass {
display: flex;
gap: 6px;
align-items: flex-end;
background: rgba(22, 34, 64, 0.65);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 24px;
padding: 6px 6px 6px 18px;
transition: border-color 0.25s, box-shadow 0.25s;
}
.input-glass:focus-within {
border-color: rgba(0, 180, 216, 0.4);
box-shadow: 0 0 0 3px rgba(0, 180, 216, 0.08), 0 4px 24px rgba(0, 0, 0, 0.2);
}
.input-wrapper {
flex: 1;
position: relative;
}
.input-wrapper textarea {
width: 100%;
padding: 10px 0;
background: transparent;
border: none;
color: var(--text);
font-size: 15px;
font-family: inherit;
resize: none;
outline: none;
min-height: 40px;
max-height: 120px;
line-height: 1.5;
}
.input-wrapper textarea::placeholder {
color: var(--text-dim);
}
.send-btn {
width: 38px;
height: 38px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none;
border-radius: 12px;
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s, opacity 0.2s;
flex-shrink: 0;
}
.send-btn:hover { transform: scale(1.08); }
.send-btn:active { transform: scale(0.93); }
.send-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; }
/* Voice input button */
.voice-btn {
width: 38px;
height: 38px;
background: transparent;
border: none;
border-radius: 12px;
color: var(--text-dim);
font-size: 18px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.voice-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); }
.voice-btn.recording {
background: rgba(255, 59, 48, 0.15);
color: #ff3b30;
animation: pulse-rec 1.5s ease infinite;
}
@keyframes pulse-rec {
0%, 100% { box-shadow: 0 0 0 0 rgba(255,59,48,0.4); }
50% { box-shadow: 0 0 0 8px rgba(255,59,48,0); }
}
.voice-btn.recording:hover { color: #ff3b30; }
.input-hint {
font-size: 11px;
color: var(--text-dim);
margin-top: 10px;
padding: 0 8px;
opacity: 0.7;
}
/* Quick actions */
.quick-actions {
display: flex;
gap: 8px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.quick-btn {
padding: 8px 16px;
background: rgba(22, 34, 64, 0.5);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.quick-btn:hover {
border-color: rgba(0, 180, 216, 0.3);
color: var(--accent);
background: rgba(0, 180, 216, 0.08);
}
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.16); }
.messages { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.08) transparent; }
/* Auth modal */
.auth-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(10, 22, 40, 0.92);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.auth-overlay.hidden { display: none; }
.auth-modal {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 36px;
width: 370px;
max-width: 90vw;
}
.auth-logo {
display: flex; align-items: center; gap: 12px;
justify-content: center; margin-bottom: 20px;
}
.auth-logo .logo-icon { width: 38px; height: 38px; font-size: 20px; }
.auth-logo h2 { font-size: 17px; font-weight: 600; }
.auth-modal h3 {
text-align: center; font-size: 15px;
margin-bottom: 18px; color: var(--text-dim);
}
.auth-field { margin-bottom: 12px; }
.auth-field input {
width: 100%; padding: 11px 14px;
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 8px; color: var(--text);
font-size: 14px; font-family: inherit; outline: none;
transition: border-color 0.2s;
}
.auth-field input:focus { border-color: var(--accent); }
.auth-submit {
width: 100%; padding: 11px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none; border-radius: 8px; color: white;
font-size: 14px; font-weight: 600; cursor: pointer;
transition: opacity 0.2s; margin-top: 4px;
}
.auth-submit:hover { opacity: 0.9; }
.auth-submit:disabled { opacity: 0.5; cursor: not-allowed; }
.auth-divider {
display: flex; align-items: center; gap: 12px;
margin: 16px 0; color: var(--text-dim); font-size: 12px;
}
.auth-divider::before, .auth-divider::after {
content: ''; flex: 1; height: 1px; background: var(--border);
}
.google-btn-wrap {
display: flex; justify-content: center; margin-bottom: 4px;
}
.auth-switch { text-align: center; margin-top: 14px; }
.auth-switch a { color: var(--accent); text-decoration: none; font-size: 13px; }
.auth-close { text-align: center; margin-top: 10px; }
.auth-close a { color: var(--text-dim); text-decoration: none; font-size: 12px; }
.auth-close a:hover { color: var(--text); }
.auth-error {
background: rgba(255, 82, 82, 0.1);
border: 1px solid rgba(255, 82, 82, 0.3);
color: #ff5252; padding: 9px 14px; border-radius: 8px;
font-size: 13px; margin-bottom: 12px; text-align: center;
}
/* User info in header */
.user-info {
font-size: 13px; color: var(--text-dim);
display: flex; align-items: center; gap: 8px;
}
.admin-badge {
padding: 2px 8px; border-radius: 10px;
background: rgba(255, 107, 53, 0.2);
color: #ff6b35; font-size: 10px; font-weight: 700;
letter-spacing: 0.5px;
}
.admin-revenue-btn {
background: rgba(255, 107, 53, 0.15) !important;
color: #ff6b35 !important;
border: 1px solid rgba(255, 107, 53, 0.3) !important;
font-size: 12px !important;
padding: 3px 8px !important;
}
/* Revenue Modal */
.revenue-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); z-index: 1001;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px);
}
.revenue-overlay.hidden { display: none; }
.revenue-modal {
background: var(--bg-panel); border-radius: 16px;
padding: 32px; width: 520px; max-width: 95vw;
max-height: 90vh; max-height: 90dvh; overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--border);
}
.revenue-modal h2 {
color: var(--text); font-size: 20px; margin-bottom: 20px;
display: flex; align-items: center; gap: 10px;
}
.revenue-cards {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
margin-bottom: 20px;
}
.revenue-card {
background: rgba(22, 34, 64, 0.5); border-radius: 12px;
padding: 16px; text-align: center;
border: 1px solid var(--border);
}
.revenue-card .label {
font-size: 11px; color: var(--text-dim);
text-transform: uppercase; letter-spacing: 0.5px;
margin-bottom: 6px;
}
.revenue-card .value {
font-size: 22px; font-weight: 700; color: var(--text);
}
.revenue-card .value.green { color: var(--success); }
.revenue-card .value.orange { color: #ff6b35; }
.revenue-table {
width: 100%; border-collapse: collapse; margin-top: 16px;
}
.revenue-table th {
text-align: left; padding: 8px 10px; font-size: 11px;
color: var(--text-dim); text-transform: uppercase;
border-bottom: 1px solid var(--border);
}
.revenue-table td {
padding: 8px 10px; font-size: 13px; color: var(--text);
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.revenue-close {
position: absolute; top: 12px; right: 16px;
background: none; border: none; color: var(--text-dim);
font-size: 24px; cursor: pointer;
}
.revenue-close:hover { color: var(--text); }
.balance-badge {
padding: 2px 8px; border-radius: 10px;
background: rgba(0, 200, 83, 0.15);
color: var(--success); font-size: 11px; font-weight: 600;
}
.topup-btn {
background: rgba(0, 200, 83, 0.15) !important;
color: var(--success) !important;
border: 1px solid rgba(0, 200, 83, 0.3) !important;
font-size: 12px !important;
padding: 3px 8px !important;
}
.topup-btn:hover {
background: rgba(0, 200, 83, 0.25) !important;
}
/* Deposit Modal */
.deposit-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); z-index: 1001;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px);
}
.deposit-overlay.hidden { display: none; }
.deposit-modal {
background: var(--bg-panel); border-radius: 16px;
padding: 32px; width: 440px; max-width: 95vw;
max-height: 90vh; max-height: 90dvh; overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--border);
text-align: center;
}
.deposit-modal h2 {
color: var(--text); font-size: 20px; margin-bottom: 16px;
display: flex; align-items: center; justify-content: center; gap: 10px;
}
.deposit-network {
display: inline-block;
padding: 4px 12px; border-radius: 8px;
background: rgba(0, 180, 216, 0.15); color: var(--accent);
font-size: 13px; font-weight: 600; margin-bottom: 16px;
}
.deposit-qr {
margin: 16px auto;
width: 200px; height: 200px;
background: #fff; border-radius: 12px;
padding: 8px;
display: flex; align-items: center; justify-content: center;
}
.deposit-qr img { width: 100%; height: 100%; }
.deposit-address-wrap {
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 10px; padding: 12px; margin: 12px 0;
display: flex; align-items: center; gap: 8px;
}
.deposit-address {
flex: 1; font-family: monospace; font-size: 12px;
color: var(--text); word-break: break-all; text-align: left;
}
.deposit-copy-btn {
background: var(--accent); border: none; border-radius: 6px;
color: #fff; padding: 6px 12px; font-size: 12px;
cursor: pointer; flex-shrink: 0; transition: all 0.2s;
}
.deposit-copy-btn:hover { opacity: 0.85; }
.deposit-check-btn {
background: linear-gradient(135deg, var(--success), #00a844);
border: none; border-radius: 10px; color: #fff;
padding: 12px 24px; font-size: 14px; font-weight: 600;
cursor: pointer; width: 100%; margin-top: 16px;
transition: all 0.2s;
}
.deposit-check-btn:hover { opacity: 0.9; transform: scale(1.02); }
.deposit-check-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.deposit-result {
margin-top: 12px; padding: 10px; border-radius: 8px;
font-size: 13px;
}
.deposit-result.success {
background: rgba(0, 200, 83, 0.1); color: var(--success);
border: 1px solid rgba(0, 200, 83, 0.3);
}
.deposit-result.empty {
background: rgba(255, 167, 38, 0.1); color: var(--warn);
border: 1px solid rgba(255, 167, 38, 0.3);
}
.deposit-result.error {
background: rgba(255, 82, 82, 0.1); color: #ff5252;
border: 1px solid rgba(255, 82, 82, 0.3);
}
.deposit-close {
display: inline-block; margin-top: 16px;
color: var(--text-dim); cursor: pointer; font-size: 13px;
}
.deposit-close:hover { color: var(--text); }
.deposit-history { margin-top: 16px; text-align: left; }
.deposit-history h4 { font-size: 13px; color: var(--text-dim); margin-bottom: 8px; }
.deposit-tx {
font-size: 11px; color: var(--text-dim); padding: 4px 0;
border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between;
}
.deposit-tx .amount { color: var(--success); font-weight: 600; }
.deposit-tx .amount.negative { color: #ff5252; }
/* Deposit/Withdraw Tabs */
.wallet-tabs {
display: flex; gap: 0; margin-bottom: 20px;
border-radius: 10px; overflow: hidden;
border: 1px solid var(--border);
}
.wallet-tab {
flex: 1; padding: 10px 16px; font-size: 13px; font-weight: 600;
cursor: pointer; border: none; transition: all 0.2s;
background: var(--bg-input); color: var(--text-dim);
}
.wallet-tab.active {
background: var(--accent); color: #fff;
}
.wallet-tab:hover:not(.active) { background: var(--bg-hover); }
.wallet-tab-content { display: none; }
.wallet-tab-content.active { display: block; }
/* Withdraw form */
.withdraw-form { text-align: left; }
.withdraw-field { margin-bottom: 14px; }
.withdraw-field label {
display: block; font-size: 12px; color: var(--text-dim);
margin-bottom: 4px; font-weight: 600;
}
.withdraw-field input {
width: 100%; padding: 10px 12px; font-size: 14px;
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); outline: none;
box-sizing: border-box;
}
.withdraw-field input:focus { border-color: var(--accent); }
.withdraw-balance-info {
font-size: 12px; color: var(--text-dim); margin-bottom: 16px;
text-align: center;
}
.withdraw-balance-info strong { color: var(--success); }
.withdraw-submit-btn {
background: linear-gradient(135deg, #ff6b35, #ff4444);
border: none; border-radius: 10px; color: #fff;
padding: 12px 24px; font-size: 14px; font-weight: 600;
cursor: pointer; width: 100%; transition: all 0.2s;
}
.withdraw-submit-btn:hover { opacity: 0.9; transform: scale(1.02); }
.withdraw-submit-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.withdraw-result {
margin-top: 12px; padding: 10px; border-radius: 8px; font-size: 13px;
}
.withdraw-result.success {
background: rgba(0, 200, 83, 0.1); color: var(--success);
border: 1px solid rgba(0, 200, 83, 0.3);
}
.withdraw-result.error {
background: rgba(255, 82, 82, 0.1); color: #ff5252;
border: 1px solid rgba(255, 82, 82, 0.3);
}
.withdraw-history { margin-top: 16px; text-align: left; }
.withdraw-history h4 { font-size: 13px; color: var(--text-dim); margin-bottom: 8px; }
.withdraw-tx {
font-size: 11px; color: var(--text-dim); padding: 4px 0;
border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
}
.withdraw-tx .amount { color: #ff5252; font-weight: 600; }
.withdraw-status {
font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600;
}
.withdraw-status.pending { background: rgba(255,167,38,0.15); color: var(--warn); }
.withdraw-status.completed { background: rgba(0,200,83,0.15); color: var(--success); }
.withdraw-status.rejected { background: rgba(255,82,82,0.15); color: #ff5252; }
/* Profile Cabinet Modal */
.profile-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); z-index: 1001;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px);
}
.profile-overlay.hidden { display: none; }
.profile-modal {
background: var(--bg-panel); border-radius: 16px;
padding: 32px; width: 560px; max-width: 95vw;
max-height: 90vh; max-height: 90dvh; overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--border);
}
.profile-modal h2 {
color: var(--text); font-size: 20px; margin-bottom: 8px;
display: flex; align-items: center; gap: 10px;
}
.profile-modal .profile-subtitle {
color: var(--text-dim); font-size: 13px; margin-bottom: 20px;
}
.profile-section {
margin-bottom: 20px;
}
.profile-section h3 {
font-size: 14px; color: var(--accent); margin-bottom: 10px;
text-transform: uppercase; letter-spacing: 0.5px;
}
.profile-field {
margin-bottom: 12px;
}
.profile-field label {
display: block; font-size: 12px; color: var(--text-dim);
margin-bottom: 4px; font-weight: 600;
}
.profile-field input, .profile-field select, .profile-field textarea {
width: 100%; padding: 9px 12px;
background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); font-size: 14px;
font-family: inherit; box-sizing: border-box;
}
.profile-field input:focus, .profile-field select:focus, .profile-field textarea:focus {
border-color: var(--accent); outline: none;
}
.profile-field textarea { resize: vertical; min-height: 60px; }
.profile-field select { cursor: pointer; }
.chip-group {
display: flex; flex-wrap: wrap; gap: 6px;
}
.chip {
padding: 5px 12px; border-radius: 16px; font-size: 12px;
border: 1px solid var(--border); color: var(--text-dim);
cursor: pointer; transition: all 0.2s; user-select: none;
background: transparent;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active {
background: rgba(0,122,255,0.15); border-color: var(--accent);
color: var(--accent); font-weight: 600;
}
.profile-vessels-list {
display: flex; flex-direction: column; gap: 6px; margin-top: 6px;
}
.profile-vessel-row {
display: flex; gap: 8px; align-items: center;
}
.profile-vessel-row input { flex: 1; }
.profile-vessel-row button {
background: transparent; border: 1px solid var(--border);
border-radius: 6px; color: var(--text-dim); cursor: pointer;
width: 30px; height: 30px; font-size: 16px;
display: flex; align-items: center; justify-content: center;
}
.profile-vessel-row button:hover { border-color: #ff5252; color: #ff5252; }
.add-vessel-btn {
background: transparent; border: 1px dashed var(--border);
border-radius: 8px; color: var(--text-dim); cursor: pointer;
padding: 8px; font-size: 13px; width: 100%; margin-top: 6px;
}
.add-vessel-btn:hover { border-color: var(--accent); color: var(--accent); }
.profile-actions {
display: flex; gap: 12px; margin-top: 24px;
}
.profile-save-btn {
flex: 1; padding: 11px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none; border-radius: 8px; color: white;
font-size: 14px; font-weight: 600; cursor: pointer;
}
.profile-save-btn:hover { opacity: 0.9; }
.profile-cancel-btn {
padding: 11px 24px;
background: transparent; border: 1px solid var(--border);
border-radius: 8px; color: var(--text-dim);
font-size: 14px; cursor: pointer;
}
.profile-cancel-btn:hover { border-color: var(--text); color: var(--text); }
.profile-saved-msg {
color: var(--success); font-size: 13px; text-align: center;
margin-top: 12px; display: none;
}
.purchased-contact-card {
background: var(--bg); border: 1px solid var(--border);
border-radius: 10px; padding: 12px 14px; margin-bottom: 8px;
}
.purchased-contact-card .pcc-company {
font-weight: 600; font-size: 14px; color: var(--text);
}
.purchased-contact-card .pcc-type {
display: inline-block; font-size: 11px; color: var(--accent);
background: rgba(0,122,255,0.1); border-radius: 10px;
padding: 2px 8px; margin-left: 6px; vertical-align: middle;
}
.purchased-contact-card .pcc-detail {
font-size: 12px; color: var(--text-dim); margin-top: 4px;
}
.purchased-contact-card .pcc-detail a {
color: var(--accent); text-decoration: none;
}
.purchased-contact-card .pcc-date {
font-size: 11px; color: var(--text-dim); opacity: 0.6; margin-top: 6px;
}
.purchased-empty {
color: var(--text-dim); font-size: 13px; text-align: center; padding: 16px 0;
}
/* ============ Responsive ============ */
/* Tablet (≤ 1024px) */
@media (max-width: 1024px) {
.sidebar { width: 240px; }
.chat-header { padding: 12px 16px; }
.messages { padding: 20px 16px 90px; gap: 20px; }
.input-area { padding: 0 16px 16px; margin-top: -70px; padding-top: 70px; }
}
/* Menu toggle button (hidden on desktop) */
.menu-toggle {
display: none;
width: 40px; height: 40px;
background: var(--accent);
border: none;
border-radius: 8px;
color: #fff;
font-size: 22px;
cursor: pointer;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0,122,255,0.3);
}
.menu-toggle:hover { background: #0066d6; }
/* Mobile (≤ 768px) */
@media (max-width: 768px) {
body { flex-direction: column; }
/* Sidebar → slide-in drawer from left */
.sidebar {
position: fixed;
top: 0; left: 0; bottom: 0;
width: 300px;
max-width: 85vw;
z-index: 900;
overflow-y: auto;
transform: translateX(-100%);
transition: transform 0.3s ease;
box-shadow: none;
}
.sidebar.open {
transform: translateX(0);
box-shadow: 4px 0 20px rgba(0,0,0,0.3);
}
.main { width: 100%; height: var(--app-height, 100vh); }
/* Header: stack actions below title */
.chat-header {
flex-wrap: wrap;
gap: 8px;
padding: 10px 12px;
}
.chat-header-left h2 { font-size: 14px; }
.chat-header-left span { display: none; }
.header-actions {
flex-wrap: wrap;
gap: 6px;
width: 100%;
justify-content: flex-end;
}
.header-btn { padding: 5px 10px; font-size: 11px; }
.lang-btn { padding: 3px 8px; font-size: 11px; }
/* User info compact */
.user-info { font-size: 11px; gap: 4px; }
.balance-badge { font-size: 10px; padding: 1px 6px; }
/* Messages */
.messages { padding: 16px 10px 80px; gap: 16px; }
.message { max-width: 92%; }
.msg-content { padding: 12px 14px; font-size: 14px; border-radius: 16px; }
.msg-avatar { width: 26px; height: 26px; font-size: 12px; }
/* Input area */
.input-area { padding: 0 10px 12px; margin-top: -60px; padding-top: 60px; }
.input-glass {
border-radius: 20px;
padding: 4px 4px 4px 14px;
}
.input-wrapper textarea {
font-size: 14px;
min-height: 38px;
}
.send-btn { width: 36px; height: 36px; font-size: 14px; border-radius: 10px; }
.voice-btn { width: 36px; height: 36px; font-size: 16px; border-radius: 10px; }
.input-hint { font-size: 10px; margin-top: 6px; }
/* Quick actions scroll horizontally */
.quick-actions {
flex-wrap: nowrap;
overflow-x: auto;
margin-bottom: 8px;
gap: 6px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.quick-actions::-webkit-scrollbar { display: none; }
.quick-btn { font-size: 11px; padding: 5px 12px; flex-shrink: 0; }
/* Auth modal */
.auth-modal { padding: 28px 20px; width: 95vw; }
.profile-modal { padding: 24px 16px; width: 95vw; }
.deposit-modal { padding: 24px 16px; width: 95vw; }
.chip { font-size: 11px; padding: 4px 10px; }
/* Menu toggle button visible */
.menu-toggle { display: flex; }
}
/* Small phones (≤ 480px) */
@media (max-width: 480px) {
.chat-header-left h2 { font-size: 13px; }
.header-actions { gap: 4px; }
.header-btn { padding: 4px 8px; font-size: 10px; }
.lang-btn { padding: 2px 6px; font-size: 10px; }
.message { max-width: 96%; }
.msg-content { font-size: 13px; padding: 10px 12px; }
.msg-avatar { width: 22px; height: 22px; font-size: 10px; }
}
/* Sidebar backdrop overlay */
.sidebar-backdrop {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 899;
}
.sidebar-backdrop.open { display: block; }
/* Sidebar close button (only on mobile) */
.sidebar-close {
display: none;
position: absolute;
top: 16px; right: 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-dim);
font-size: 18px;
width: 32px; height: 32px;
cursor: pointer;
align-items: center;
justify-content: center;
}
.sidebar-close:hover { color: var(--text); border-color: var(--accent); }
@media (max-width: 768px) {
.sidebar-close { display: flex; }
}
</style>
<script>
/* Fix 100vh for mobile browsers (Yandex, Samsung, old Chrome) that don't support dvh */
function setVH() {
var vh = window.innerHeight;
document.documentElement.style.setProperty('--app-height', vh + 'px');
document.body.style.height = vh + 'px';
}
setVH();
window.addEventListener('resize', setVH);
window.addEventListener('orientationchange', function() { setTimeout(setVH, 100); });
</script>
</head>
<body>
<!-- Auth Modal -->
<div class="auth-overlay hidden" id="authOverlay">
<div class="auth-modal">
<div class="auth-logo">
<div class="logo-icon">&#9875;</div>
<h2>SeaFare Montana</h2>
</div>
<h3 id="authTitle"></h3>
<div id="authError" class="auth-error" style="display:none;"></div>
<form id="authForm" onsubmit="handleAuth(event)">
<div id="nameField" class="auth-field" style="display:none;">
<input type="text" id="authName" autocomplete="name">
</div>
<div class="auth-field">
<input type="email" id="authEmail" required autocomplete="email">
</div>
<div class="auth-field">
<input type="password" id="authPassword" required minlength="6" autocomplete="current-password">
</div>
<button type="submit" class="auth-submit" id="authSubmitBtn"></button>
</form>
<div class="auth-divider" id="authDivider" style="display:none;"><span>or</span></div>
<div class="google-btn-wrap" id="googleBtnWrap" style="display:none;">
<div id="googleSignInBtn"></div>
</div>
<div class="auth-switch">
<a href="#" id="authSwitchLink" onclick="toggleAuthMode(event)"></a>
</div>
<div class="auth-close">
<a href="#" onclick="hideAuthModal(event)">&times; <span id="authCloseText"></span></a>
</div>
</div>
</div>
<!-- Profile Cabinet Modal -->
<!-- Deposit Modal -->
<div class="deposit-overlay hidden" id="depositOverlay">
<div class="deposit-modal">
<h2>&#128176; <span data-i18n="topup_title">Top Up Balance</span></h2>
<!-- Tabs -->
<div class="wallet-tabs">
<button class="wallet-tab active" onclick="switchWalletTab('deposit')" id="walletTabDeposit" data-i18n="wallet_tab_deposit">Deposit</button>
<button class="wallet-tab" onclick="switchWalletTab('withdraw')" id="walletTabWithdraw" data-i18n="wallet_tab_withdraw">Withdraw</button>
</div>
<!-- Deposit Tab -->
<div class="wallet-tab-content active" id="walletContentDeposit">
<div class="deposit-network">USDT — TRC20 (Tron)</div>
<p style="font-size:13px; color:var(--text-dim); margin-bottom:8px;" data-i18n="topup_instruction">
Send USDT (TRC20) to the address below. Your balance will be credited after confirmation.
</p>
<div class="fee-info" id="depositFeeInfo" style="font-size:12px; color:var(--accent); background:rgba(0,200,200,0.06); border-radius:10px; padding:8px 12px; margin-bottom:12px;">
<span data-i18n="deposit_fee_info">Service fee: 2%. Example: send $100 — receive $98.</span>
</div>
<div id="depositQR" class="deposit-qr" style="display:none;"></div>
<div id="depositAddressWrap" class="deposit-address-wrap" style="display:none;">
<span id="depositAddress" class="deposit-address"></span>
<button class="deposit-copy-btn" onclick="copyDepositAddress()" data-i18n="topup_copy">Copy</button>
</div>
<div id="depositLoading" style="padding:20px; color:var(--text-dim);">Loading...</div>
<button class="deposit-check-btn" id="depositCheckBtn" onclick="checkDeposits()" data-i18n="topup_check">
Check for deposits
</button>
<div id="depositResult" class="deposit-result" style="display:none;"></div>
<div id="depositHistory" class="deposit-history" style="display:none;"></div>
</div>
<!-- Withdraw Tab -->
<div class="wallet-tab-content" id="walletContentWithdraw">
<div class="deposit-network">USDT — TRC20 (Tron)</div>
<div class="withdraw-balance-info">
<span data-i18n="withdraw_your_balance">Your balance:</span>
<strong id="withdrawBalanceDisplay">$0.00</strong> USDT
</div>
<div class="withdraw-form">
<div class="withdraw-field">
<label data-i18n="withdraw_address_label">TRC20 Wallet Address</label>
<input type="text" id="withdrawAddress" placeholder="T..." maxlength="34">
</div>
<div class="withdraw-field">
<label data-i18n="withdraw_amount_label">Amount (USDT)</label>
<input type="number" id="withdrawAmount" placeholder="0.00" min="2" step="0.01" oninput="updateWithdrawTotal()">
</div>
<div class="withdraw-fee-breakdown" id="withdrawFeeBreakdown" style="font-size:12px; color:var(--text-dim); background:rgba(0,200,200,0.06); border-radius:10px; padding:8px 12px; margin-bottom:10px;">
<div><span data-i18n="withdraw_fee_label">Network fee:</span> <strong>1.00 USDT</strong></div>
<div id="withdrawTotalLine" style="display:none;"><span data-i18n="withdraw_total_label">Total deducted:</span> <strong id="withdrawTotalDisplay"></strong></div>
</div>
<p style="font-size:11px; color:var(--text-dim); margin-bottom:14px;" data-i18n="withdraw_note">
Minimum withdrawal: $2 USDT. Withdrawals are processed manually within 24 hours.
</p>
<button class="withdraw-submit-btn" id="withdrawSubmitBtn" onclick="submitWithdrawal()" data-i18n="withdraw_submit">
Request Withdrawal
</button>
<div id="withdrawResult" class="withdraw-result" style="display:none;"></div>
</div>
<div id="withdrawHistory" class="withdraw-history" style="display:none;"></div>
</div>
<span class="deposit-close" onclick="hideDeposit()" data-i18n="topup_close">Close</span>
</div>
</div>
<div class="profile-overlay hidden" id="profileOverlay">
<div class="profile-modal">
<h2>&#128188; <span data-i18n="profile_title">My Cabinet</span></h2>
<div class="profile-subtitle" data-i18n="profile_subtitle">Fill out your profile so SeaFare Montana can give you more accurate, personalized answers.</div>
<div class="profile-section" id="purchasedContactsSection" style="display:none">
<h3 data-i18n="purchased_contacts_title">Purchased Contacts</h3>
<div id="purchasedContactsList"></div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_company_section">Company</h3>
<div class="profile-field">
<label data-i18n="profile_company">Company Name</label>
<input type="text" id="profCompany" placeholder="e.g. Pacific Shipping Ltd.">
</div>
<div class="profile-field">
<label data-i18n="profile_role">Your Role</label>
<select id="profRole">
<option value="">— Select —</option>
<option value="shipowner">Shipowner</option>
<option value="operator">Operator</option>
<option value="charterer">Charterer</option>
<option value="broker">Broker</option>
<option value="freight_forwarder">Freight Forwarder</option>
<option value="port_agent">Port Agent</option>
<option value="surveyor">Surveyor</option>
<option value="other">Other</option>
</select>
</div>
<div class="profile-field">
<label data-i18n="profile_experience">Experience (years)</label>
<input type="number" id="profExperience" min="0" max="60" placeholder="0">
</div>
<div class="profile-field">
<label data-i18n="profile_fleet_size">Fleet Size (vessels)</label>
<input type="number" id="profFleetSize" min="0" placeholder="0">
</div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_vessel_types_title">Vessel Types</h3>
<div class="chip-group" id="profVesselTypes">
<span class="chip" data-val="bulk_carrier">Bulk Carrier</span>
<span class="chip" data-val="tanker">Tanker</span>
<span class="chip" data-val="container">Container</span>
<span class="chip" data-val="general_cargo">General Cargo</span>
<span class="chip" data-val="ro_ro">Ro-Ro</span>
<span class="chip" data-val="lng_carrier">LNG Carrier</span>
<span class="chip" data-val="chemical_tanker">Chemical Tanker</span>
<span class="chip" data-val="offshore">Offshore</span>
<span class="chip" data-val="tug">Tug</span>
<span class="chip" data-val="barge">Barge</span>
<span class="chip" data-val="passenger">Passenger</span>
</div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_trade_routes_title">Trade Routes</h3>
<div class="chip-group" id="profTradeRoutes">
<span class="chip" data-val="Mediterranean">Mediterranean</span>
<span class="chip" data-val="Baltic">Baltic</span>
<span class="chip" data-val="Black Sea">Black Sea</span>
<span class="chip" data-val="North Sea">North Sea</span>
<span class="chip" data-val="Atlantic">Atlantic</span>
<span class="chip" data-val="Pacific">Pacific</span>
<span class="chip" data-val="Indian Ocean">Indian Ocean</span>
<span class="chip" data-val="Southeast Asia">Southeast Asia</span>
<span class="chip" data-val="Middle East / Persian Gulf">Middle East / Gulf</span>
<span class="chip" data-val="West Africa">West Africa</span>
<span class="chip" data-val="East Africa">East Africa</span>
<span class="chip" data-val="South America">South America</span>
<span class="chip" data-val="Caribbean">Caribbean</span>
<span class="chip" data-val="North America East Coast">N. America East</span>
<span class="chip" data-val="North America West Coast">N. America West</span>
<span class="chip" data-val="Australia / Oceania">Australia / Oceania</span>
<span class="chip" data-val="Arctic">Arctic</span>
</div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_cargo_types_title">Cargo Types</h3>
<div class="chip-group" id="profCargoTypes">
<span class="chip" data-val="dry_bulk">Dry Bulk</span>
<span class="chip" data-val="liquid_bulk">Liquid Bulk</span>
<span class="chip" data-val="containerized">Containerized</span>
<span class="chip" data-val="breakbulk">Breakbulk</span>
<span class="chip" data-val="project_cargo">Project Cargo</span>
<span class="chip" data-val="reefer">Reefer</span>
<span class="chip" data-val="lng">LNG</span>
<span class="chip" data-val="lpg">LPG</span>
<span class="chip" data-val="chemicals">Chemicals</span>
<span class="chip" data-val="crude_oil">Crude Oil</span>
<span class="chip" data-val="refined_products">Refined Products</span>
<span class="chip" data-val="grain">Grain</span>
<span class="chip" data-val="coal">Coal</span>
<span class="chip" data-val="iron_ore">Iron Ore</span>
<span class="chip" data-val="fertilizer">Fertilizer</span>
</div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_vessels_title">Vessels of Interest</h3>
<div class="profile-subtitle" data-i18n="profile_vessels_hint">Add vessel names or IMO numbers you want to track</div>
<div class="profile-vessels-list" id="profVesselsList"></div>
<button class="add-vessel-btn" onclick="addVesselRow()">+ <span data-i18n="profile_add_vessel">Add vessel</span></button>
</div>
<div class="profile-section">
<h3 data-i18n="profile_contact_section">Contact</h3>
<div class="profile-field">
<label data-i18n="profile_phone">Phone</label>
<input type="tel" id="profPhone" placeholder="+1 555 123 4567">
</div>
<div class="profile-field">
<label>Telegram</label>
<input type="text" id="profTelegram" placeholder="@username">
</div>
</div>
<div class="profile-section">
<h3 data-i18n="profile_notes_title">Notes</h3>
<div class="profile-field">
<textarea id="profNotes" rows="3" data-i18n-placeholder="profile_notes_placeholder" placeholder="Any additional info for the AI agent..."></textarea>
</div>
</div>
<div class="profile-actions">
<button class="profile-cancel-btn" onclick="hideProfile()" data-i18n="profile_cancel">Cancel</button>
<button class="profile-save-btn" onclick="saveProfile()" data-i18n="profile_save">Save Profile</button>
</div>
<div class="profile-saved-msg" id="profileSavedMsg" data-i18n="profile_saved">Profile saved successfully!</div>
</div>
</div>
<!-- Revenue Modal (admin only) -->
<div class="revenue-overlay hidden" id="revenueOverlay">
<div class="revenue-modal" style="position:relative;">
<button class="revenue-close" onclick="hideRevenue()">&times;</button>
<h2>&#128200; Platform Revenue</h2>
<div id="revenueContent">
<div style="text-align:center; padding:40px; color:var(--text-dim);">Loading...</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar-backdrop" id="sidebarBackdrop" onclick="toggleSidebar()"></div>
<aside class="sidebar" id="sidebar">
<button class="sidebar-close" onclick="toggleSidebar()">&times;</button>
<div class="sidebar-header">
<div class="logo">
<div class="logo-icon">&#9875;</div>
<div class="logo-text">
<h1>SeaFare Montana</h1>
<span data-i18n="subtitle">Maritime Logistics Agent</span>
</div>
</div>
<div class="status-badge">
<span class="status-dot"></span>
<span data-i18n="status">Online — Ready</span>
</div>
</div>
<div class="services">
<h3 data-i18n="services_title">Services</h3>
<div class="service-item" data-quick="search_vessel">
<div class="service-icon">&#128674;</div>
<div class="service-info">
<div class="name" data-i18n="svc_search">Vessel Search</div>
<div class="price" data-i18n="svc_search_desc">Name, IMO, MMSI lookup</div>
</div>
<span class="price-tag price-free" data-i18n="free">Free</span>
</div>
<div class="service-item" data-quick="vessel_position">
<div class="service-icon">&#128205;</div>
<div class="service-info">
<div class="name" data-i18n="svc_position">Vessel Position</div>
<div class="price" data-i18n="svc_position_desc">AIS real-time tracking</div>
</div>
<span class="price-tag price-free" data-i18n="free">Free</span>
</div>
<div class="service-item" data-quick="owner_info">
<div class="service-icon">&#127970;</div>
<div class="service-info">
<div class="name" data-i18n="svc_owner">Owner / Operator</div>
<div class="price" data-i18n="svc_owner_desc">Company information</div>
</div>
<span class="price-tag price-free" data-i18n="free">Free</span>
</div>
<div class="service-item" data-quick="demurrage_calc">
<div class="service-icon">&#128178;</div>
<div class="service-info">
<div class="name" data-i18n="svc_demurrage">Demurrage Calculator</div>
<div class="price" data-i18n="svc_demurrage_desc">Delay cost estimation</div>
</div>
<span class="price-tag price-free" data-i18n="free">Free</span>
</div>
<div class="service-item" data-quick="contacts">
<div class="service-icon">&#128587;</div>
<div class="service-info">
<div class="name" data-i18n="svc_contacts">Contact Introduction</div>
<div class="price" data-i18n="svc_contacts_desc">Shipper &#8596; Operator</div>
</div>
<span class="price-tag">$10</span>
</div>
<div class="service-item" data-quick="consultation">
<div class="service-icon">&#128220;</div>
<div class="service-info">
<div class="name" data-i18n="svc_consult">Freight Consultation</div>
<div class="price" data-i18n="svc_consult_desc">Expert market advice</div>
</div>
<span class="price-tag">$10</span>
</div>
</div>
<div class="sidebar-footer">
Powered by <a href="https://moltbook.com/u/SeaFare_Montana">Moltbook</a><br>
<span data-i18n="footer">Montana Protocol &bull; 25 years expertise</span><br>
<span id="appVersion" style="font-size:10px;opacity:0.5;"></span>
</div>
</aside>
<!-- Main Chat -->
<main class="main">
<div class="chat-header">
<div class="chat-header-left">
<button class="menu-toggle" onclick="toggleSidebar()" title="Menu">&#9776;</button>
<h2>&#9875; SeaFare Montana</h2>
<span data-i18n="header_sub">Maritime Logistics Consultant</span>
</div>
<div class="header-actions">
<span id="userInfo" class="user-info" style="display:none;">
<span id="adminBadge" class="admin-badge" style="display:none;">ADMIN</span>
<button id="revenueBtn" class="header-btn admin-revenue-btn" onclick="showRevenue()" style="display:none;">&#128200;</button>
<span id="userName"></span>
<span id="userBalance" class="balance-badge" onclick="showDeposit()" style="cursor:pointer;" title="Top Up"></span>
<button class="header-btn topup-btn" onclick="showDeposit()" data-i18n="topup_btn">&#128176;</button>
<button class="header-btn" onclick="showProfile()" data-i18n="profile_btn" title="My Cabinet">&#128188;</button>
</span>
<div class="lang-switcher">
<button class="lang-btn active" onclick="setLang('en')">EN</button>
<button class="lang-btn" onclick="setLang('ru')">RU</button>
<button class="lang-btn" onclick="setLang('es')">ES</button>
</div>
<button class="header-btn" onclick="clearChat()" data-i18n="clear_chat">Clear Chat</button>
<button class="header-btn" id="loginBtn" onclick="showAuthModal()" data-i18n="auth_login">Login</button>
<button class="header-btn" id="logoutBtn" onclick="doLogout()" style="display:none;" data-i18n="auth_logout">Logout</button>
</div>
</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<div class="quick-actions" id="quickActions"></div>
<div class="input-glass">
<div class="input-wrapper">
<textarea id="userInput" rows="1" onkeydown="handleKey(event)"></textarea>
</div>
<button class="voice-btn" id="voiceBtn" onclick="toggleVoice()" title="Voice input">&#127908;</button>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">&#10148;</button>
</div>
<div class="input-hint" data-i18n="input_hint">Press Enter to send &bull; Shift+Enter for new line</div>
</div>
</main>
<script>
// =============================================================================
// i18n — translations
// =============================================================================
const i18n = {
en: {
subtitle: 'Maritime Logistics Agent',
status: 'Online — Ready',
services_title: 'Services',
header_sub: 'Maritime Logistics Consultant',
clear_chat: 'Clear Chat',
input_hint: 'Press Enter to send \u2022 Shift+Enter for new line',
placeholder: 'Ask about vessels, positions, owners, demurrage...',
free: 'Free',
svc_search: 'Vessel Search',
svc_search_desc: 'Name, IMO, MMSI lookup',
svc_position: 'Vessel Position',
svc_position_desc: 'AIS real-time tracking',
svc_owner: 'Owner / Operator',
svc_owner_desc: 'Company information',
svc_demurrage: 'Demurrage Calculator',
svc_demurrage_desc: 'Delay cost estimation',
svc_contacts: 'Contact Introduction',
svc_contacts_desc: 'Shipper \u2194 Operator',
svc_consult: 'Freight Consultation',
svc_consult_desc: 'Expert market advice',
footer: 'Montana Protocol \u2022 25 years expertise',
welcome: '<strong>Welcome to SeaFare Montana!</strong><br><br>' +
"I'm your maritime logistics consultant with 25 years of industry expertise. I can help you with:<br><br>" +
'&#128674; <strong>Vessel search</strong> — find any ship by name, IMO, or MMSI<br>' +
'&#128205; <strong>Live position</strong> — real-time AIS tracking<br>' +
'&#127970; <strong>Owner & operator</strong> — company information<br>' +
'&#128178; <strong>Demurrage</strong> — calculate delay costs<br>' +
'&#128587; <strong>Introductions</strong> — connect with operators ($10 USDT)<br><br>' +
'Try the quick actions below or just ask me anything!',
chat_cleared: '<strong>Chat cleared.</strong><br>How can I help you today?',
confirm_clear: 'Are you sure you want to clear the chat history?',
error_generic: 'Sorry, something went wrong. Please try again.',
error_connection: 'Connection error. Make sure the server is running on port 5050.',
// Quick action buttons (labels)
qb_vessels_port: 'Vessels near port',
qb_route: 'Route A to B',
qb_cargo: 'Ship my cargo',
qb_search: 'Search vessel',
qb_demurrage: 'Demurrage calc',
qb_contacts: 'Find contacts',
// Quick action messages sent to bot
qm_btn_vessels_port: 'What vessels are near Rotterdam right now?',
qm_btn_route: 'Calculate route from Shanghai to Rotterdam for a container ship',
qm_btn_cargo: 'I need to ship 50000 tons of grain from Santos to Rotterdam',
qm_btn_search: 'Search vessel EVER GIVEN',
qm_btn_demurrage: 'Calculate demurrage: 10 agreed days, 14 actual, $25000/day',
qm_btn_contacts: 'Find contacts for bulk carrier operators',
// Auth
auth_login: 'Login',
auth_logout: 'Logout',
auth_title_login: 'Log In',
auth_title_register: 'Create Account',
auth_email: 'Email',
auth_password: 'Password',
auth_name: 'Name (optional)',
auth_login_btn: 'Log In',
auth_register_btn: 'Register',
auth_switch_to_register: "Don't have an account? Register",
auth_switch_to_login: 'Already have an account? Log In',
auth_close: 'Continue as guest',
auth_or: 'or',
auth_error_generic: 'Something went wrong. Try again.',
user_balance: 'Balance',
profile_btn: 'Cabinet',
profile_title: 'My Cabinet',
profile_subtitle: 'Fill out your profile so SeaFare Montana can give you more accurate, personalized answers.',
purchased_contacts_title: 'Purchased Contacts',
purchased_contacts_empty: 'No purchased contacts yet. Unlock contacts through the chat.',
profile_company_section: 'Company',
profile_company: 'Company Name',
profile_role: 'Your Role',
profile_experience: 'Experience (years)',
profile_fleet_size: 'Fleet Size (vessels)',
profile_vessel_types_title: 'Vessel Types',
profile_trade_routes_title: 'Trade Routes',
profile_cargo_types_title: 'Cargo Types',
profile_vessels_title: 'Vessels of Interest',
profile_vessels_hint: 'Add vessel names or IMO numbers you want to track',
profile_add_vessel: 'Add vessel',
profile_contact_section: 'Contact',
profile_phone: 'Phone',
profile_notes_title: 'Notes',
profile_notes_placeholder: 'Any additional info for the AI agent...',
profile_cancel: 'Cancel',
profile_save: 'Save Profile',
profile_saved: 'Profile saved successfully!',
voice_permission: 'Please allow microphone access to use voice input.',
voice_not_supported: 'Voice input is not supported in this browser. Try Chrome or Yandex Browser.',
voice_listening: 'Listening...',
voice_tooltip: 'Voice input',
topup_btn: 'Top Up',
topup_title: 'Top Up Balance',
topup_instruction: 'Send USDT (TRC20) to the address below. Your balance will be credited after confirmation.',
topup_copy: 'Copy',
topup_copied: 'Copied!',
topup_check: 'Check for deposits',
topup_checking: 'Checking...',
topup_found: 'Deposited: ${amount} USDT (fee 2%). Credited: ${credited} USDT. Balance: ${balance} USDT',
topup_none: 'No new deposits found. If you already sent USDT, wait a few minutes and try again.',
topup_error: 'Error checking deposits. Try again later.',
topup_close: 'Close',
topup_history: 'Deposit History',
deposit_fee_info: 'Service fee: 2%. Example: send $100 — receive $98.',
wallet_tab_deposit: 'Deposit',
wallet_tab_withdraw: 'Withdraw',
withdraw_your_balance: 'Your balance:',
withdraw_address_label: 'TRC20 Wallet Address',
withdraw_amount_label: 'Amount (USDT)',
withdraw_fee_label: 'Network fee:',
withdraw_total_label: 'Total deducted:',
withdraw_note: 'Minimum withdrawal: $2 USDT. Withdrawals are processed manually within 24 hours.',
withdraw_submit: 'Request Withdrawal',
withdraw_success: 'Withdrawal submitted! Amount: ${amount} USDT (fee: $1). New balance: ${balance} USDT',
withdraw_history: 'Withdrawal History',
withdraw_status_pending: 'Pending',
withdraw_status_completed: 'Completed',
withdraw_status_rejected: 'Rejected',
},
ru: {
subtitle: 'Агент морской логистики',
status: 'Онлайн — Готов',
services_title: 'Услуги',
header_sub: 'Консультант по морской логистике',
clear_chat: 'Очистить чат',
input_hint: 'Enter — отправить \u2022 Shift+Enter — новая строка',
placeholder: 'Спросите о судах, позициях, владельцах, демередже...',
free: 'Бесплатно',
svc_search: 'Поиск судна',
svc_search_desc: 'По названию, IMO, MMSI',
svc_position: 'Позиция судна',
svc_position_desc: 'AIS отслеживание',
svc_owner: 'Владелец / Оператор',
svc_owner_desc: 'Информация о компании',
svc_demurrage: 'Калькулятор демереджа',
svc_demurrage_desc: 'Расчёт стоимости простоя',
svc_contacts: 'Предоставление контактов',
svc_contacts_desc: 'Грузоотправитель \u2194 Оператор',
svc_consult: 'Фрахтовая консультация',
svc_consult_desc: 'Экспертный совет по рынку',
footer: 'Montana Protocol \u2022 25 лет опыта',
welcome: '<strong>Добро пожаловать в SeaFare Montana!</strong><br><br>' +
'Я ваш консультант по морской логистике с 25-летним опытом в индустрии. Я помогу вам с:<br><br>' +
'&#128674; <strong>Поиск судна</strong> — найти любое судно по названию, IMO или MMSI<br>' +
'&#128205; <strong>Позиция</strong> — отслеживание в реальном времени через AIS<br>' +
'&#127970; <strong>Владелец и оператор</strong> — информация о компаниях<br>' +
'&#128178; <strong>Демередж</strong> — расчёт стоимости простоя<br>' +
'&#128587; <strong>Контакты</strong> — связь с операторами ($10 USDT)<br><br>' +
'Попробуйте быстрые кнопки ниже или задайте вопрос!',
chat_cleared: '<strong>Чат очищен.</strong><br>Чем могу помочь?',
confirm_clear: 'Вы уверены, что хотите очистить историю чата?',
error_generic: 'Произошла ошибка. Попробуйте ещё раз.',
error_connection: 'Ошибка соединения. Убедитесь, что сервер запущен на порту 5050.',
qb_vessels_port: 'Суда у порта',
qb_route: 'Маршрут A-B',
qb_cargo: 'Отправить груз',
qb_search: 'Найти судно',
qb_demurrage: 'Демередж',
qb_contacts: 'Контакты',
qm_btn_vessels_port: 'Какие суда сейчас рядом с Роттердамом?',
qm_btn_route: 'Рассчитай маршрут из Шанхая в Роттердам для контейнеровоза',
qm_btn_cargo: 'Мне нужно отправить 50000 тонн зерна из Сантоса в Роттердам',
qm_btn_search: 'Найти судно EVER GIVEN',
qm_btn_demurrage: 'Рассчитать демередж: 10 согласованных дней, 14 фактических, $25000/день',
qm_btn_contacts: 'Найти контакты операторов балкеров',
// Auth
auth_login: 'Войти',
auth_logout: 'Выход',
auth_title_login: 'Вход',
auth_title_register: 'Регистрация',
auth_email: 'Email',
auth_password: 'Пароль',
auth_name: 'Имя (необязательно)',
auth_login_btn: 'Войти',
auth_register_btn: 'Зарегистрироваться',
auth_switch_to_register: 'Нет аккаунта? Зарегистрироваться',
auth_switch_to_login: 'Уже есть аккаунт? Войти',
auth_close: 'Продолжить как гость',
auth_or: 'или',
auth_error_generic: 'Произошла ошибка. Попробуйте ещё раз.',
user_balance: 'Баланс',
profile_btn: 'Кабинет',
profile_title: 'Мой кабинет',
profile_subtitle: 'Заполните профиль, чтобы SeaFare Montana давал более точные и персонализированные ответы.',
purchased_contacts_title: 'Купленные контакты',
purchased_contacts_empty: 'Пока нет купленных контактов. Разблокируйте контакты через чат.',
profile_company_section: 'Компания',
profile_company: 'Название компании',
profile_role: 'Ваша роль',
profile_experience: 'Опыт (лет)',
profile_fleet_size: 'Размер флота (судов)',
profile_vessel_types_title: 'Типы судов',
profile_trade_routes_title: 'Торговые маршруты',
profile_cargo_types_title: 'Типы грузов',
profile_vessels_title: 'Интересующие суда',
profile_vessels_hint: 'Добавьте названия судов или номера IMO для отслеживания',
profile_add_vessel: 'Добавить судно',
profile_contact_section: 'Контакт',
profile_phone: 'Телефон',
profile_notes_title: 'Заметки',
profile_notes_placeholder: 'Дополнительная информация для AI-агента...',
profile_cancel: 'Отмена',
profile_save: 'Сохранить профиль',
profile_saved: 'Профиль успешно сохранён!',
voice_permission: 'Разрешите доступ к микрофону для голосового ввода.',
voice_not_supported: 'Голосовой ввод не поддерживается в этом браузере. Попробуйте Chrome или Яндекс Браузер.',
voice_listening: 'Слушаю...',
voice_tooltip: 'Голосовой ввод',
topup_btn: 'Пополнить',
topup_title: 'Пополнение баланса',
topup_instruction: 'Отправьте USDT (TRC20) на адрес ниже. Баланс будет зачислен после подтверждения.',
topup_copy: 'Копировать',
topup_copied: 'Скопировано!',
topup_check: 'Проверить поступления',
topup_checking: 'Проверяю...',
topup_found: 'Получено: ${amount} USDT (комиссия 2%). Зачислено: ${credited} USDT. Баланс: ${balance} USDT',
topup_none: 'Новых поступлений нет. Если вы уже отправили USDT, подождите пару минут и попробуйте снова.',
topup_error: 'Ошибка проверки. Попробуйте позже.',
topup_close: 'Закрыть',
topup_history: 'История пополнений',
deposit_fee_info: 'Комиссия сервиса: 2%. Пример: отправляете $100 — получаете $98.',
wallet_tab_deposit: 'Пополнение',
wallet_tab_withdraw: 'Вывод',
withdraw_your_balance: 'Ваш баланс:',
withdraw_address_label: 'Адрес TRC20 кошелька',
withdraw_amount_label: 'Сумма (USDT)',
withdraw_fee_label: 'Комиссия сети:',
withdraw_total_label: 'Итого к списанию:',
withdraw_note: 'Минимальный вывод: $2 USDT. Выводы обрабатываются вручную в течение 24 часов.',
withdraw_submit: 'Запросить вывод',
withdraw_success: 'Заявка на вывод создана! Сумма: ${amount} USDT (комиссия: $1). Новый баланс: ${balance} USDT',
withdraw_history: 'История выводов',
withdraw_status_pending: 'В обработке',
withdraw_status_completed: 'Выполнен',
withdraw_status_rejected: 'Отклонён',
},
es: {
subtitle: 'Agente de Logística Marítima',
status: 'En línea — Listo',
services_title: 'Servicios',
header_sub: 'Consultor de Logística Marítima',
clear_chat: 'Limpiar chat',
input_hint: 'Enter para enviar \u2022 Shift+Enter nueva línea',
placeholder: 'Pregunte sobre buques, posiciones, propietarios, demora...',
free: 'Gratis',
svc_search: 'Búsqueda de buques',
svc_search_desc: 'Por nombre, IMO, MMSI',
svc_position: 'Posición del buque',
svc_position_desc: 'Seguimiento AIS en tiempo real',
svc_owner: 'Propietario / Operador',
svc_owner_desc: 'Información de la empresa',
svc_demurrage: 'Calculadora de demoras',
svc_demurrage_desc: 'Estimación de costos por demora',
svc_contacts: 'Contactos',
svc_contacts_desc: 'Cargador \u2194 Operador',
svc_consult: 'Consulta de flete',
svc_consult_desc: 'Asesoramiento experto',
footer: 'Montana Protocol \u2022 25 años de experiencia',
welcome: '<strong>¡Bienvenido a SeaFare Montana!</strong><br><br>' +
'Soy su consultor de logística marítima con 25 años de experiencia. Puedo ayudarle con:<br><br>' +
'&#128674; <strong>Búsqueda de buques</strong> — encontrar cualquier buque por nombre, IMO o MMSI<br>' +
'&#128205; <strong>Posición en vivo</strong> — seguimiento AIS en tiempo real<br>' +
'&#127970; <strong>Propietario y operador</strong> — información de empresas<br>' +
'&#128178; <strong>Demoras</strong> — calcular costos por demora<br>' +
'&#128587; <strong>Contactos</strong> — conexión con operadores ($10 USDT)<br><br>' +
'¡Pruebe las acciones rápidas o haga cualquier pregunta!',
chat_cleared: '<strong>Chat limpiado.</strong><br>¿En qué puedo ayudarle hoy?',
confirm_clear: '¿Está seguro de que desea borrar el historial del chat?',
error_generic: 'Lo siento, algo salió mal. Inténtelo de nuevo.',
error_connection: 'Error de conexión. Asegúrese de que el servidor esté en el puerto 5050.',
qb_vessels_port: 'Buques en puerto',
qb_route: 'Ruta A-B',
qb_cargo: 'Enviar carga',
qb_search: 'Buscar buque',
qb_demurrage: 'Demora',
qb_contacts: 'Contactos',
qm_btn_vessels_port: '¿Qué buques hay cerca de Róterdam ahora?',
qm_btn_route: 'Calcula la ruta de Shanghái a Róterdam para un portacontenedores',
qm_btn_cargo: 'Necesito enviar 50000 toneladas de grano de Santos a Róterdam',
qm_btn_search: 'Buscar buque EVER GIVEN',
qm_btn_demurrage: 'Calcular demora: 10 días acordados, 14 reales, $25000/día',
qm_btn_contacts: 'Buscar contactos de operadores de graneleros',
// Auth
auth_login: 'Entrar',
auth_logout: 'Salir',
auth_title_login: 'Iniciar sesión',
auth_title_register: 'Crear cuenta',
auth_email: 'Correo electrónico',
auth_password: 'Contraseña',
auth_name: 'Nombre (opcional)',
auth_login_btn: 'Iniciar sesión',
auth_register_btn: 'Registrarse',
auth_switch_to_register: '¿No tiene cuenta? Regístrese',
auth_switch_to_login: '¿Ya tiene cuenta? Inicie sesión',
auth_close: 'Continuar como invitado',
auth_or: 'o',
auth_error_generic: 'Algo salió mal. Inténtelo de nuevo.',
user_balance: 'Saldo',
profile_btn: 'Gabinete',
profile_title: 'Mi Gabinete',
profile_subtitle: 'Complete su perfil para que SeaFare Montana le dé respuestas más precisas y personalizadas.',
purchased_contacts_title: 'Contactos comprados',
purchased_contacts_empty: 'Aún no tiene contactos comprados. Desbloquee contactos a través del chat.',
profile_company_section: 'Empresa',
profile_company: 'Nombre de la empresa',
profile_role: 'Su rol',
profile_experience: 'Experiencia (años)',
profile_fleet_size: 'Tamaño de la flota (buques)',
profile_vessel_types_title: 'Tipos de buques',
profile_trade_routes_title: 'Rutas comerciales',
profile_cargo_types_title: 'Tipos de carga',
profile_vessels_title: 'Buques de interés',
profile_vessels_hint: 'Agregue nombres de buques o números IMO para seguimiento',
profile_add_vessel: 'Agregar buque',
profile_contact_section: 'Contacto',
profile_phone: 'Teléfono',
profile_notes_title: 'Notas',
profile_notes_placeholder: 'Información adicional para el agente AI...',
profile_cancel: 'Cancelar',
profile_save: 'Guardar perfil',
profile_saved: '¡Perfil guardado exitosamente!',
voice_permission: 'Permita el acceso al micrófono para la entrada de voz.',
voice_not_supported: 'La entrada de voz no es compatible con este navegador. Pruebe Chrome o Yandex Browser.',
voice_listening: 'Escuchando...',
voice_tooltip: 'Entrada de voz',
topup_btn: 'Recargar',
topup_title: 'Recargar saldo',
topup_instruction: 'Envíe USDT (TRC20) a la dirección de abajo. Su saldo se acreditará después de la confirmación.',
topup_copy: 'Copiar',
topup_copied: '¡Copiado!',
topup_check: 'Verificar depósitos',
topup_checking: 'Verificando...',
topup_found: 'Depositado: ${amount} USDT (comisión 2%). Acreditado: ${credited} USDT. Saldo: ${balance} USDT',
topup_none: 'No se encontraron nuevos depósitos. Si ya envió USDT, espere unos minutos e intente de nuevo.',
topup_error: 'Error al verificar. Intente más tarde.',
topup_close: 'Cerrar',
topup_history: 'Historial de depósitos',
deposit_fee_info: 'Comisión del servicio: 2%. Ejemplo: envía $100 — recibe $98.',
wallet_tab_deposit: 'Depósito',
wallet_tab_withdraw: 'Retiro',
withdraw_your_balance: 'Su saldo:',
withdraw_address_label: 'Dirección de billetera TRC20',
withdraw_amount_label: 'Monto (USDT)',
withdraw_fee_label: 'Comisión de red:',
withdraw_total_label: 'Total a deducir:',
withdraw_note: 'Retiro mínimo: $2 USDT. Los retiros se procesan manualmente en 24 horas.',
withdraw_submit: 'Solicitar retiro',
withdraw_success: '¡Retiro solicitado! Monto: ${amount} USDT (comisión: $1). Nuevo saldo: ${balance} USDT',
withdraw_history: 'Historial de retiros',
withdraw_status_pending: 'Pendiente',
withdraw_status_completed: 'Completado',
withdraw_status_rejected: 'Rechazado',
}
};
// =============================================================================
// State
// =============================================================================
let currentLang = localStorage.getItem('seafare_lang') || 'en';
let userHasSentMessage = false;
let sessionChecked = false;
let authToken = localStorage.getItem('seafare_token') || null;
let currentUser = null;
let authMode = 'login';
let googleClientId = null;
const API_BASE = '';
const messagesDiv = document.getElementById('messages');
const input = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key]) || i18n.en[key] || key;
}
// =============================================================================
// Auth
// =============================================================================
function showAuthModal() {
document.getElementById('authOverlay').classList.remove('hidden');
updateAuthForm();
// Re-init Google button after modal is visible (GSI needs visible container)
if (googleClientId) {
setTimeout(() => initGoogleButton(), 100);
}
}
function hideAuthModal(e) {
if (e) e.preventDefault();
document.getElementById('authOverlay').classList.add('hidden');
}
function toggleAuthMode(e) {
e.preventDefault();
authMode = authMode === 'login' ? 'register' : 'login';
updateAuthForm();
}
function updateAuthForm() {
const isLogin = authMode === 'login';
document.getElementById('authTitle').textContent = t(isLogin ? 'auth_title_login' : 'auth_title_register');
document.getElementById('authSubmitBtn').textContent = t(isLogin ? 'auth_login_btn' : 'auth_register_btn');
document.getElementById('authSwitchLink').textContent = t(isLogin ? 'auth_switch_to_register' : 'auth_switch_to_login');
document.getElementById('authCloseText').textContent = t('auth_close');
document.getElementById('nameField').style.display = isLogin ? 'none' : 'block';
document.getElementById('authError').style.display = 'none';
document.getElementById('authEmail').placeholder = t('auth_email');
document.getElementById('authPassword').placeholder = t('auth_password');
document.getElementById('authName').placeholder = t('auth_name');
// Google Sign-In
const hasGoogle = !!googleClientId;
document.getElementById('authDivider').style.display = hasGoogle ? 'flex' : 'none';
document.getElementById('googleBtnWrap').style.display = hasGoogle ? 'flex' : 'none';
if (hasGoogle) {
document.querySelector('#authDivider span').textContent = t('auth_or');
initGoogleButton();
}
}
let googleBtnRendered = false;
function initGoogleButton() {
if (!googleClientId || !window.google) return;
try {
if (!googleBtnRendered) {
google.accounts.id.initialize({
client_id: googleClientId,
callback: handleGoogleResponse,
ux_mode: 'popup',
});
}
// Re-render button each time modal opens (GSI needs visible container)
const container = document.getElementById('googleSignInBtn');
if (container) {
container.innerHTML = '';
const btnWidth = Math.min(300, window.innerWidth - 80);
google.accounts.id.renderButton(container, {
theme: 'filled_blue', size: 'large',
width: btnWidth, text: 'signin_with'
});
}
googleBtnRendered = true;
} catch (e) {
console.error('GSI init error:', e);
}
}
async function handleGoogleResponse(response) {
document.getElementById('authError').style.display = 'none';
if (!response || !response.credential) {
showAuthError('Google sign-in failed. Please try again.');
return;
}
try {
const resp = await fetch(API_BASE + '/api/v1/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: response.credential, lang: currentLang })
});
const data = await resp.json();
if (data.success) {
authToken = data.token;
currentUser = data.user;
localStorage.setItem('seafare_token', authToken);
hideAuthModal();
onLoginSuccess();
} else {
showAuthError(data.error || t('auth_error_generic'));
}
} catch (err) {
console.error('Google auth error:', err);
showAuthError(t('auth_error_generic'));
}
}
async function handleAuth(e) {
e.preventDefault();
const btn = document.getElementById('authSubmitBtn');
btn.disabled = true;
document.getElementById('authError').style.display = 'none';
const email = document.getElementById('authEmail').value.trim();
const password = document.getElementById('authPassword').value;
const name = document.getElementById('authName').value.trim();
const endpoint = authMode === 'login' ? '/api/v1/auth/login' : '/api/v1/auth/register';
const body = { email, password, lang: currentLang };
if (authMode === 'register') body.name = name;
try {
const resp = await fetch(API_BASE + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await resp.json();
if (data.success) {
authToken = data.token;
currentUser = data.user;
localStorage.setItem('seafare_token', authToken);
hideAuthModal();
onLoginSuccess();
} else {
showAuthError(data.error || t('auth_error_generic'));
}
} catch (err) {
showAuthError(t('auth_error_generic'));
}
btn.disabled = false;
}
function showAuthError(msg) {
const el = document.getElementById('authError');
el.textContent = msg;
el.style.display = 'block';
}
async function checkSession() {
if (!authToken) return;
try {
const resp = await fetch(API_BASE + '/api/v1/auth/me', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
if (resp.ok) {
const data = await resp.json();
currentUser = data.user;
onLoginSuccess();
} else {
localStorage.removeItem('seafare_token');
authToken = null;
}
} catch (e) {
// server unreachable — keep guest mode
}
}
async function onLoginSuccess() {
document.getElementById('userInfo').style.display = 'flex';
document.getElementById('userName').textContent = currentUser.name || currentUser.email;
document.getElementById('userBalance').textContent = '$' + (currentUser.balance || 0).toFixed(2);
document.getElementById('adminBadge').style.display = currentUser.is_admin ? '' : 'none';
document.getElementById('revenueBtn').style.display = currentUser.is_admin ? '' : 'none';
document.getElementById('logoutBtn').style.display = '';
document.getElementById('loginBtn').style.display = 'none';
// Load saved chat history
try {
const resp = await fetch(API_BASE + '/api/v1/chat/history', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
if (resp.ok) {
const data = await resp.json();
if (data.messages && data.messages.length > 0) {
messagesDiv.innerHTML = '';
userHasSentMessage = true;
for (const msg of data.messages) {
addMessage(
msg.role === 'user' ? msg.message : formatResponse(msg.message),
msg.role === 'user'
);
}
}
}
} catch (e) { /* ignore */ }
}
async function doLogout() {
try {
await fetch(API_BASE + '/api/v1/auth/logout', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + authToken }
});
} catch (e) { /* ignore */ }
authToken = null;
currentUser = null;
localStorage.removeItem('seafare_token');
document.getElementById('userInfo').style.display = 'none';
document.getElementById('adminBadge').style.display = 'none';
document.getElementById('revenueBtn').style.display = 'none';
document.getElementById('logoutBtn').style.display = 'none';
document.getElementById('loginBtn').style.display = '';
messagesDiv.innerHTML = '';
userHasSentMessage = false;
showWelcome();
}
// =============================================================================
// Revenue Dashboard (admin)
// =============================================================================
async function showRevenue() {
if (!authToken || !currentUser || !currentUser.is_admin) return;
document.getElementById('revenueOverlay').classList.remove('hidden');
document.getElementById('revenueContent').innerHTML =
'<div style="text-align:center;padding:40px;color:var(--text-dim);">Loading...</div>';
try {
const resp = await fetch(API_BASE + '/api/v1/admin/revenue', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
const data = await resp.json();
if (!data.success) {
document.getElementById('revenueContent').innerHTML =
'<div style="text-align:center;padding:40px;color:#ff6b6b;">Error: ' + (data.error || 'Unknown') + '</div>';
return;
}
let html = '<div class="revenue-cards">';
html += '<div class="revenue-card"><div class="label">Total Revenue</div><div class="value green">$' + data.total_revenue.toFixed(2) + '</div></div>';
html += '<div class="revenue-card"><div class="label">Today</div><div class="value green">$' + data.today_revenue.toFixed(2) + '</div></div>';
html += '<div class="revenue-card"><div class="label">Platform Profit</div><div class="value orange">$' + data.platform_profit.toFixed(2) + '</div></div>';
html += '<div class="revenue-card"><div class="label">User Balances</div><div class="value">$' + data.total_user_balances.toFixed(2) + '</div></div>';
html += '</div>';
// By service
if (data.by_service && data.by_service.length > 0) {
html += '<h3 style="color:var(--text);font-size:14px;margin:16px 0 8px;">By Service</h3>';
html += '<table class="revenue-table"><tr><th>Service</th><th>Count</th><th>Total</th></tr>';
for (const s of data.by_service) {
html += '<tr><td>' + s.service + '</td><td>' + s.count + '</td><td>$' + s.total.toFixed(2) + '</td></tr>';
}
html += '</table>';
}
// Recent charges
if (data.recent_charges && data.recent_charges.length > 0) {
html += '<h3 style="color:var(--text);font-size:14px;margin:20px 0 8px;">Recent Charges</h3>';
html += '<table class="revenue-table"><tr><th>User</th><th>Service</th><th>Amount</th><th>Date</th></tr>';
for (const c of data.recent_charges) {
const date = c.date ? c.date.split(' ')[0] : '-';
html += '<tr><td>' + c.user + '</td><td>' + c.service + '</td><td>$' + c.amount.toFixed(2) + '</td><td>' + date + '</td></tr>';
}
html += '</table>';
}
// Summary row
html += '<div style="margin-top:20px;padding:12px;background:rgba(22,34,64,0.5);border-radius:10px;font-size:12px;color:var(--text-dim);">';
html += 'Total deposited: $' + data.total_deposited.toFixed(2) +
' &nbsp;|&nbsp; Withdrawn: $' + data.total_withdrawn.toFixed(2) +
' &nbsp;|&nbsp; User balances: $' + data.total_user_balances.toFixed(2);
html += '</div>';
document.getElementById('revenueContent').innerHTML = html;
} catch (e) {
document.getElementById('revenueContent').innerHTML =
'<div style="text-align:center;padding:40px;color:#ff6b6b;">Connection error</div>';
}
}
function hideRevenue() {
document.getElementById('revenueOverlay').classList.add('hidden');
}
document.getElementById('revenueOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) hideRevenue();
});
// =============================================================================
// Profile Cabinet
// =============================================================================
let profileData = null;
function showProfile() {
document.getElementById('profileOverlay').classList.remove('hidden');
loadProfile();
}
function hideProfile() {
document.getElementById('profileOverlay').classList.add('hidden');
document.getElementById('profileSavedMsg').style.display = 'none';
}
async function loadProfile() {
if (!authToken) return;
try {
const resp = await fetch(API_BASE + '/api/v1/profile', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
const data = await resp.json();
if (data.success && data.profile) {
profileData = data.profile;
fillProfileForm(data.profile);
} else {
profileData = null;
clearProfileForm();
}
} catch (e) {
console.error('Load profile error:', e);
}
loadPurchasedContacts();
}
async function loadPurchasedContacts() {
const section = document.getElementById('purchasedContactsSection');
const list = document.getElementById('purchasedContactsList');
if (!authToken) { section.style.display = 'none'; return; }
try {
const resp = await fetch(API_BASE + '/api/v1/purchased-contacts', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
const data = await resp.json();
if (data.success && data.contacts && data.contacts.length > 0) {
section.style.display = '';
list.innerHTML = data.contacts.map(pc => {
const c = pc.contact || {};
const name = c.company_name || c.contact_person || pc.query || '—';
const typ = c.type || pc.contact_type || '';
const email = c.email ? `<div class="pcc-detail">&#9993; <a href="mailto:${c.email}">${c.email}</a></div>` : '';
const phone = c.phone ? `<div class="pcc-detail">&#128222; <a href="tel:${c.phone}">${c.phone}</a></div>` : '';
const addr = c.address ? `<div class="pcc-detail">&#128205; ${c.address}${c.country ? ', ' + c.country : ''}</div>` : '';
const website = c.website ? `<div class="pcc-detail">&#127760; <a href="${c.website}" target="_blank">${c.website}</a></div>` : '';
const person = c.contact_person && c.company_name ? `<div class="pcc-detail">&#128100; ${c.contact_person}</div>` : '';
const date = pc.purchased_at ? new Date(pc.purchased_at).toLocaleDateString() : '';
return `<div class="purchased-contact-card">
<span class="pcc-company">${name}</span>${typ ? `<span class="pcc-type">${typ}</span>` : ''}
${person}${email}${phone}${addr}${website}
${date ? `<div class="pcc-date">${date}</div>` : ''}
</div>`;
}).join('');
} else {
section.style.display = '';
list.innerHTML = `<div class="purchased-empty">${t('purchased_contacts_empty')}</div>`;
}
} catch (e) {
section.style.display = 'none';
}
}
function fillProfileForm(p) {
document.getElementById('profCompany').value = p.company_name || '';
document.getElementById('profRole').value = p.role || '';
document.getElementById('profExperience').value = p.experience_years || '';
document.getElementById('profFleetSize').value = p.fleet_size || '';
document.getElementById('profPhone').value = p.phone || '';
document.getElementById('profTelegram').value = p.telegram || '';
document.getElementById('profNotes').value = p.notes || '';
// Chips
setChips('profVesselTypes', p.vessel_types || []);
setChips('profTradeRoutes', p.trade_routes || []);
setChips('profCargoTypes', p.cargo_types || []);
// Vessels of interest
const list = document.getElementById('profVesselsList');
list.innerHTML = '';
(p.vessels_of_interest || []).forEach(v => addVesselRow(v));
}
function clearProfileForm() {
document.getElementById('profCompany').value = '';
document.getElementById('profRole').value = '';
document.getElementById('profExperience').value = '';
document.getElementById('profFleetSize').value = '';
document.getElementById('profPhone').value = '';
document.getElementById('profTelegram').value = '';
document.getElementById('profNotes').value = '';
setChips('profVesselTypes', []);
setChips('profTradeRoutes', []);
setChips('profCargoTypes', []);
document.getElementById('profVesselsList').innerHTML = '';
}
function setChips(groupId, selected) {
document.querySelectorAll('#' + groupId + ' .chip').forEach(chip => {
chip.classList.toggle('active', selected.includes(chip.dataset.val));
});
}
function getChips(groupId) {
return Array.from(document.querySelectorAll('#' + groupId + ' .chip.active'))
.map(c => c.dataset.val);
}
function addVesselRow(value) {
const list = document.getElementById('profVesselsList');
const row = document.createElement('div');
row.className = 'profile-vessel-row';
row.innerHTML = '<input type="text" placeholder="e.g. EVER GIVEN or IMO 9811000" value="' + (value || '') + '">' +
'<button onclick="this.parentElement.remove()">&times;</button>';
list.appendChild(row);
}
function getVessels() {
return Array.from(document.querySelectorAll('#profVesselsList input'))
.map(i => i.value.trim())
.filter(v => v.length > 0);
}
async function saveProfile() {
if (!authToken) return;
const body = {
company_name: document.getElementById('profCompany').value.trim(),
role: document.getElementById('profRole').value,
experience_years: parseInt(document.getElementById('profExperience').value) || 0,
fleet_size: parseInt(document.getElementById('profFleetSize').value) || 0,
vessel_types: getChips('profVesselTypes'),
trade_routes: getChips('profTradeRoutes'),
cargo_types: getChips('profCargoTypes'),
vessels_of_interest: getVessels(),
phone: document.getElementById('profPhone').value.trim(),
telegram: document.getElementById('profTelegram').value.trim(),
notes: document.getElementById('profNotes').value.trim(),
};
try {
const resp = await fetch(API_BASE + '/api/v1/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + authToken
},
body: JSON.stringify(body)
});
const data = await resp.json();
if (data.success) {
profileData = data.profile;
const msg = document.getElementById('profileSavedMsg');
msg.style.display = 'block';
setTimeout(() => { msg.style.display = 'none'; }, 3000);
}
} catch (e) {
console.error('Save profile error:', e);
}
}
// Chip click handlers
document.querySelectorAll('.chip-group .chip').forEach(chip => {
chip.addEventListener('click', () => chip.classList.toggle('active'));
});
// Close profile on overlay click
document.getElementById('profileOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) hideProfile();
});
// =============================================================================
// Deposit / Top Up
// =============================================================================
let depositAddress = '';
async function showDeposit() {
if (!authToken) { showAuthModal(); return; }
document.getElementById('depositOverlay').classList.remove('hidden');
switchWalletTab('deposit');
document.getElementById('depositLoading').style.display = 'block';
document.getElementById('depositQR').style.display = 'none';
document.getElementById('depositAddressWrap').style.display = 'none';
document.getElementById('depositResult').style.display = 'none';
document.getElementById('depositHistory').style.display = 'none';
try {
const resp = await fetch(API_BASE + '/api/v1/wallet', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
const data = await resp.json();
if (data.success) {
depositAddress = data.address;
document.getElementById('depositLoading').style.display = 'none';
document.getElementById('depositAddress').textContent = data.address;
document.getElementById('depositAddressWrap').style.display = 'flex';
// QR code via free API
const qrEl = document.getElementById('depositQR');
qrEl.innerHTML = '<img src="https://api.qrserver.com/v1/create-qr-code/?size=184x184&data=' +
encodeURIComponent(data.address) + '" alt="QR">';
qrEl.style.display = 'flex';
// Show deposit history
if (data.deposits && data.deposits.length > 0) {
renderDepositHistory(data.deposits);
}
} else {
document.getElementById('depositLoading').textContent = data.error || 'Error';
}
} catch (e) {
document.getElementById('depositLoading').textContent = 'Connection error';
}
}
function hideDeposit() {
document.getElementById('depositOverlay').classList.add('hidden');
}
function copyDepositAddress() {
if (!depositAddress) return;
navigator.clipboard.writeText(depositAddress).then(() => {
const btn = document.querySelector('.deposit-copy-btn');
const orig = btn.textContent;
btn.textContent = t('topup_copied');
setTimeout(() => { btn.textContent = orig; }, 2000);
}).catch(() => {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = depositAddress;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
async function checkDeposits() {
if (!authToken) return;
const btn = document.getElementById('depositCheckBtn');
const resultEl = document.getElementById('depositResult');
btn.disabled = true;
btn.textContent = t('topup_checking');
resultEl.style.display = 'none';
try {
const resp = await fetch(API_BASE + '/api/v1/wallet/check', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + authToken,
'Content-Type': 'application/json'
}
});
const data = await resp.json();
if (data.success) {
if (data.new_deposits > 0) {
resultEl.className = 'deposit-result success';
resultEl.textContent = t('topup_found')
.replace('${amount}', data.new_amount.toFixed(2))
.replace('${credited}', (data.credited_amount || data.new_amount).toFixed(2))
.replace('${balance}', data.balance.toFixed(2));
// Update header balance
if (currentUser) {
currentUser.balance = data.balance;
document.getElementById('userBalance').textContent = '$' + data.balance.toFixed(2);
}
} else {
resultEl.className = 'deposit-result empty';
resultEl.textContent = t('topup_none');
}
} else {
resultEl.className = 'deposit-result error';
resultEl.textContent = data.error || t('topup_error');
}
resultEl.style.display = 'block';
} catch (e) {
resultEl.className = 'deposit-result error';
resultEl.textContent = t('topup_error');
resultEl.style.display = 'block';
}
btn.disabled = false;
btn.textContent = t('topup_check');
}
function renderDepositHistory(deposits) {
const container = document.getElementById('depositHistory');
let html = '<h4>' + t('topup_history') + '</h4>';
deposits.forEach(d => {
const date = new Date(d.confirmed_at).toLocaleDateString();
const txShort = d.tx_id ? d.tx_id.substring(0, 12) + '...' : '';
html += '<div class="deposit-tx"><span>' + date + ' ' + txShort + '</span><span class="amount">+' + d.amount.toFixed(2) + ' USDT</span></div>';
});
container.innerHTML = html;
container.style.display = 'block';
}
// =============================================================================
// Wallet Tabs
// =============================================================================
function switchWalletTab(tab) {
document.getElementById('walletTabDeposit').classList.toggle('active', tab === 'deposit');
document.getElementById('walletTabWithdraw').classList.toggle('active', tab === 'withdraw');
document.getElementById('walletContentDeposit').classList.toggle('active', tab === 'deposit');
document.getElementById('walletContentWithdraw').classList.toggle('active', tab === 'withdraw');
if (tab === 'withdraw') {
// Update balance display
const bal = currentUser ? currentUser.balance : 0;
document.getElementById('withdrawBalanceDisplay').textContent = '$' + bal.toFixed(2);
loadWithdrawals();
}
}
// =============================================================================
// Withdrawal
// =============================================================================
function updateWithdrawTotal() {
const amount = parseFloat(document.getElementById('withdrawAmount').value) || 0;
const totalLine = document.getElementById('withdrawTotalLine');
const totalDisplay = document.getElementById('withdrawTotalDisplay');
if (amount >= 2) {
const total = (amount + 1).toFixed(2);
totalDisplay.textContent = '$' + total + ' USDT';
totalLine.style.display = 'block';
} else {
totalLine.style.display = 'none';
}
}
async function submitWithdrawal() {
if (!authToken) return;
const btn = document.getElementById('withdrawSubmitBtn');
const resultEl = document.getElementById('withdrawResult');
const address = document.getElementById('withdrawAddress').value.trim();
const amount = document.getElementById('withdrawAmount').value;
btn.disabled = true;
resultEl.style.display = 'none';
try {
const resp = await fetch(API_BASE + '/api/v1/wallet/withdraw', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + authToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: parseFloat(amount), to_address: address, lang: currentLang })
});
const data = await resp.json();
if (data.success) {
resultEl.className = 'withdraw-result success';
resultEl.textContent = t('withdraw_success')
.replace('${amount}', data.withdrawal.amount.toFixed(2))
.replace('${balance}', data.balance.toFixed(2));
// Update user balance
if (currentUser) {
currentUser.balance = data.balance;
document.getElementById('userBalance').textContent = '$' + data.balance.toFixed(2);
document.getElementById('withdrawBalanceDisplay').textContent = '$' + data.balance.toFixed(2);
}
// Clear form
document.getElementById('withdrawAddress').value = '';
document.getElementById('withdrawAmount').value = '';
// Reload history
loadWithdrawals();
} else {
resultEl.className = 'withdraw-result error';
resultEl.textContent = data.error || 'Error';
}
resultEl.style.display = 'block';
} catch (e) {
resultEl.className = 'withdraw-result error';
resultEl.textContent = 'Connection error';
resultEl.style.display = 'block';
}
btn.disabled = false;
}
async function loadWithdrawals() {
if (!authToken) return;
try {
const resp = await fetch(API_BASE + '/api/v1/wallet/withdrawals', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
const data = await resp.json();
if (data.success && data.withdrawals && data.withdrawals.length > 0) {
renderWithdrawHistory(data.withdrawals);
}
} catch (e) { /* silent */ }
}
function renderWithdrawHistory(withdrawals) {
const container = document.getElementById('withdrawHistory');
let html = '<h4>' + t('withdraw_history') + '</h4>';
withdrawals.forEach(w => {
const date = new Date(w.created_at).toLocaleDateString();
const addrShort = w.to_address ? w.to_address.substring(0, 8) + '...' + w.to_address.substring(30) : '';
const statusKey = 'withdraw_status_' + w.status;
const statusText = t(statusKey);
html += '<div class="withdraw-tx">' +
'<span>' + date + ' ' + addrShort + '</span>' +
'<span class="withdraw-status ' + w.status + '">' + statusText + '</span>' +
'<span class="amount">-' + w.amount.toFixed(2) + ' USDT</span>' +
'</div>';
});
container.innerHTML = html;
container.style.display = 'block';
}
// Close deposit on overlay click
document.getElementById('depositOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) hideDeposit();
});
// =============================================================================
// Sidebar toggle (mobile)
// =============================================================================
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.getElementById('sidebarBackdrop').classList.toggle('open');
}
// Close sidebar when clicking a service item (mobile)
document.querySelectorAll('.service-item[data-quick]').forEach(item => {
item.addEventListener('click', () => {
document.getElementById('sidebar').classList.remove('open');
document.getElementById('sidebarBackdrop').classList.remove('open');
});
});
// =============================================================================
// Language switching
// =============================================================================
function setLang(lang) {
currentLang = lang;
localStorage.setItem('seafare_lang', lang);
// Update active button
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.trim() === lang.toUpperCase());
});
// Update all data-i18n elements
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
el.innerHTML = t(key);
});
// Update placeholder
input.placeholder = t('placeholder');
// Update quick action buttons
renderQuickActions();
// Re-render welcome if user hasn't sent any messages yet
// Skip on initial load — init() will show welcome after checkSession()
if (!userHasSentMessage && sessionChecked) {
messagesDiv.innerHTML = '';
showWelcome();
}
}
function renderQuickActions() {
const qa = document.getElementById('quickActions');
qa.innerHTML = `
<button class="quick-btn" onclick="quickSend('${t('qm_btn_vessels_port')}')">${t('qb_vessels_port')}</button>
<button class="quick-btn" onclick="quickSend('${t('qm_btn_route')}')">${t('qb_route')}</button>
<button class="quick-btn" onclick="quickSend('${t('qm_btn_cargo')}')">${t('qb_cargo')}</button>
<button class="quick-btn" onclick="quickSend('${t('qm_btn_search')}')">${t('qb_search')}</button>
<button class="quick-btn" onclick="quickSend('${t('qm_btn_demurrage')}')">${t('qb_demurrage')}</button>
<button class="quick-btn" onclick="quickSend('${t('qm_btn_contacts')}')">${t('qb_contacts')}</button>
`;
}
// Sidebar service clicks
document.querySelectorAll('.service-item[data-quick]').forEach(item => {
item.addEventListener('click', () => {
const key = 'qm_' + item.getAttribute('data-quick');
quickSend(t(key));
});
});
// =============================================================================
// Chat functions
// =============================================================================
function getTime() {
return new Date().toLocaleTimeString(currentLang === 'ru' ? 'ru-RU' : currentLang === 'es' ? 'es-ES' : 'en-US',
{ hour: '2-digit', minute: '2-digit' });
}
function showWelcome() {
addMessage(t('welcome'), false);
}
function addMessage(text, isUser) {
const div = document.createElement('div');
div.className = `message ${isUser ? 'user' : 'bot'}`;
div.innerHTML = `
<div class="msg-avatar">${isUser ? '&#128100;' : '&#9875;'}</div>
<div>
<div class="msg-content">${text}</div>
<div class="msg-time">${getTime()}</div>
</div>
`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return div;
}
function addTyping() {
const div = document.createElement('div');
div.className = 'message bot';
div.id = 'typing';
div.innerHTML = `
<div class="msg-avatar">&#9875;</div>
<div>
<div class="msg-content">
<div class="typing"><span></span><span></span><span></span></div>
</div>
</div>
`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function removeTyping() {
const el = document.getElementById('typing');
if (el) el.remove();
}
function formatResponse(text) {
// Markdown tables → HTML tables
const lines = text.split('\n');
let result = [];
let inTable = false;
let tableRows = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('|') && line.endsWith('|')) {
// Skip separator rows (|---|---|)
if (/^\|[\s\-:]+\|$/.test(line.replace(/\|/g, '|').replace(/[\s\-:]/g, ''))) {
continue;
}
if (!inTable) { inTable = true; tableRows = []; }
const cells = line.split('|').filter((c, idx, arr) => idx > 0 && idx < arr.length - 1).map(c => c.trim());
tableRows.push(cells);
} else {
if (inTable) {
result.push(buildTable(tableRows));
inTable = false;
tableRows = [];
}
result.push(line);
}
}
if (inTable) result.push(buildTable(tableRows));
text = result.join('\n');
text = text.replace(/\n/g, '<br>');
text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--accent);text-decoration:underline;">$1</a>');
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/```([\s\S]*?)```/g, '<pre>$1</pre>');
text = text.replace(/`(.+?)`/g, '<code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:3px;font-size:12px;">$1</code>');
text = text.replace(/_(.+?)_/g, '<em>$1</em>');
return text;
}
function buildTable(rows) {
if (!rows.length) return '';
let html = '<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:13px;">';
rows.forEach((cells, idx) => {
const tag = idx === 0 ? 'th' : 'td';
const style = idx === 0
? 'padding:6px 10px;border-bottom:1px solid rgba(0,180,216,0.3);color:var(--accent);text-align:left;font-weight:600;'
: 'padding:5px 10px;border-bottom:1px solid rgba(255,255,255,0.05);';
html += '<tr>' + cells.map(c => `<${tag} style="${style}">${c}</${tag}>`).join('') + '</tr>';
});
html += '</table>';
return html;
}
async function sendMessage() {
const text = input.value.trim();
if (!text) return;
input.value = '';
input.style.height = 'auto';
sendBtn.disabled = true;
userHasSentMessage = true;
addMessage(text, true);
addTyping();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
const headers = { 'Content-Type': 'application/json' };
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
const resp = await fetch(`${API_BASE}/api/v1/chat`, {
method: 'POST',
headers,
body: JSON.stringify({ message: text, lang: currentLang }),
signal: controller.signal
});
clearTimeout(timeoutId);
removeTyping();
if (resp.ok) {
const data = await resp.json();
addMessage(formatResponse(data.response), false);
} else if (resp.status === 401) {
doLogout();
} else {
addMessage(t('error_generic'), false);
}
} catch (e) {
removeTyping();
addMessage(t('error_connection'), false);
}
sendBtn.disabled = false;
input.focus();
}
function quickSend(text) {
input.value = text;
sendMessage();
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
async function clearChat() {
if (!confirm(t('confirm_clear'))) return;
messagesDiv.innerHTML = '';
userHasSentMessage = false;
addMessage(t('chat_cleared'), false);
if (authToken) {
try {
await fetch(API_BASE + '/api/v1/chat/history', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + authToken }
});
} catch (e) { /* ignore */ }
}
}
// Auto-resize textarea
input.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// =============================================================================
// Voice Input (Web Speech API)
// =============================================================================
const voiceBtn = document.getElementById('voiceBtn');
let recognition = null;
let isRecording = false;
let voiceFinalTranscript = '';
let voiceStartText = '';
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
voiceBtn.style.display = 'flex';
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onstart = function() {
isRecording = true;
voiceBtn.classList.add('recording');
voiceBtn.title = t('voice_listening');
voiceStartText = input.value;
voiceFinalTranscript = '';
};
recognition.onresult = function(event) {
let interim = '';
voiceFinalTranscript = '';
for (let i = 0; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
voiceFinalTranscript += transcript;
} else {
interim += transcript;
}
}
const prefix = voiceStartText ? voiceStartText.trimEnd() + ' ' : '';
input.value = prefix + voiceFinalTranscript + interim;
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
};
recognition.onend = function() {
isRecording = false;
voiceBtn.classList.remove('recording');
voiceBtn.title = t('voice_tooltip');
if (voiceFinalTranscript) {
const prefix = voiceStartText ? voiceStartText.trimEnd() + ' ' : '';
input.value = fixPunctuation(prefix + voiceFinalTranscript.trim());
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
}
input.focus();
};
recognition.onerror = function(event) {
isRecording = false;
voiceBtn.classList.remove('recording');
voiceBtn.title = t('voice_tooltip');
if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
alert(t('voice_permission'));
}
};
}
function toggleVoice() {
if (!recognition) return;
if (isRecording) {
recognition.stop();
} else {
const langMap = { en: 'en-US', ru: 'ru-RU', es: 'es-ES' };
recognition.lang = langMap[currentLang] || 'en-US';
voiceFinalTranscript = '';
try { recognition.start(); } catch(e) { /* already started */ }
}
}
function fixPunctuation(text) {
if (!text) return text;
text = text.trim();
// Capitalize first letter
text = text.charAt(0).toUpperCase() + text.slice(1);
// Capitalize after sentence-ending punctuation
text = text.replace(/([.!?])\s+([a-zа-яёáéíóúñ])/g, function(m, p, c) {
return p + ' ' + c.toUpperCase();
});
// Add period at end if no punctuation
if (text.length > 2 && !/[.!?…,;:]$/.test(text)) {
text += '.';
}
// Clean up double spaces
text = text.replace(/\s{2,}/g, ' ');
return text;
}
// =============================================================================
// Init
// =============================================================================
(async function init() {
// Load auth config (Google Client ID)
try {
const resp = await fetch(API_BASE + '/api/v1/auth/config');
if (resp.ok) {
const cfg = await resp.json();
googleClientId = cfg.google_client_id || null;
}
} catch (e) { /* offline */ }
setLang(currentLang);
await checkSession();
sessionChecked = true;
// Show welcome only if no chat history was loaded
if (!userHasSentMessage) {
showWelcome();
}
input.focus();
// Load app version
try {
const hResp = await fetch(API_BASE + '/health');
if (hResp.ok) {
const h = await hResp.json();
if (h.version) document.getElementById('appVersion').textContent = 'v' + h.version;
}
} catch (e) { /* offline */ }
})();
</script>
</body>
</html>