3048 lines
124 KiB
HTML
3048 lines
124 KiB
HTML
<!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">⚓</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)">× <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>💰 <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>💼 <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()">×</button>
|
||
<h2>📈 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()">×</button>
|
||
<div class="sidebar-header">
|
||
<div class="logo">
|
||
<div class="logo-icon">⚓</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">🚢</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">📍</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">🏢</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">💲</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">🙋</div>
|
||
<div class="service-info">
|
||
<div class="name" data-i18n="svc_contacts">Contact Introduction</div>
|
||
<div class="price" data-i18n="svc_contacts_desc">Shipper ↔ Operator</div>
|
||
</div>
|
||
<span class="price-tag">$10</span>
|
||
</div>
|
||
|
||
<div class="service-item" data-quick="consultation">
|
||
<div class="service-icon">📜</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 • 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">☰</button>
|
||
<h2>⚓ 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;">📈</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">💰</button>
|
||
<button class="header-btn" onclick="showProfile()" data-i18n="profile_btn" title="My Cabinet">💼</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">🎤</button>
|
||
<button class="send-btn" id="sendBtn" onclick="sendMessage()">➤</button>
|
||
</div>
|
||
<div class="input-hint" data-i18n="input_hint">Press Enter to send • 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>" +
|
||
'🚢 <strong>Vessel search</strong> — find any ship by name, IMO, or MMSI<br>' +
|
||
'📍 <strong>Live position</strong> — real-time AIS tracking<br>' +
|
||
'🏢 <strong>Owner & operator</strong> — company information<br>' +
|
||
'💲 <strong>Demurrage</strong> — calculate delay costs<br>' +
|
||
'🙋 <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>' +
|
||
'🚢 <strong>Поиск судна</strong> — найти любое судно по названию, IMO или MMSI<br>' +
|
||
'📍 <strong>Позиция</strong> — отслеживание в реальном времени через AIS<br>' +
|
||
'🏢 <strong>Владелец и оператор</strong> — информация о компаниях<br>' +
|
||
'💲 <strong>Демередж</strong> — расчёт стоимости простоя<br>' +
|
||
'🙋 <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>' +
|
||
'🚢 <strong>Búsqueda de buques</strong> — encontrar cualquier buque por nombre, IMO o MMSI<br>' +
|
||
'📍 <strong>Posición en vivo</strong> — seguimiento AIS en tiempo real<br>' +
|
||
'🏢 <strong>Propietario y operador</strong> — información de empresas<br>' +
|
||
'💲 <strong>Demoras</strong> — calcular costos por demora<br>' +
|
||
'🙋 <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) +
|
||
' | Withdrawn: $' + data.total_withdrawn.toFixed(2) +
|
||
' | 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">✉ <a href="mailto:${c.email}">${c.email}</a></div>` : '';
|
||
const phone = c.phone ? `<div class="pcc-detail">📞 <a href="tel:${c.phone}">${c.phone}</a></div>` : '';
|
||
const addr = c.address ? `<div class="pcc-detail">📍 ${c.address}${c.country ? ', ' + c.country : ''}</div>` : '';
|
||
const website = c.website ? `<div class="pcc-detail">🌐 <a href="${c.website}" target="_blank">${c.website}</a></div>` : '';
|
||
const person = c.contact_person && c.company_name ? `<div class="pcc-detail">👤 ${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()">×</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 ? '👤' : '⚓'}</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">⚓</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>
|