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

3048 lines
124 KiB
HTML
Raw Permalink Normal View History

<!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>