Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Replace technical jargon (tokens, thinking mode, benchmarks) with user-friendly language: number of questions per day/week/month, page equivalents (~160 pages per query), how to write questions, costs explanation, and future billing notice. Update dropdown descriptions and limit exceeded messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2690 lines
87 KiB
HTML
Executable File
2690 lines
87 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}NordaGPT - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block container_class %}chat-container-override{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Reset dla pełnoekranowego chatu jak ChatGPT/Claude */
|
|
:root {
|
|
/* Wysokość nagłówka: 73px navbar + 36px admin bar (jeśli admin) */
|
|
--header-height: {% if current_user.is_authenticated and current_user.can_access_admin_panel() %}109px{% else %}73px{% endif %};
|
|
}
|
|
|
|
html, body {
|
|
overflow: hidden !important; /* Blokada scrollowania strony */
|
|
height: 100% !important;
|
|
}
|
|
|
|
/* Ukrycie footera na stronie chatu */
|
|
footer {
|
|
display: none !important;
|
|
}
|
|
|
|
main {
|
|
padding: 0 !important;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: calc(100vh - var(--header-height)) !important; /* Wysokość minus navbar (+ admin bar) */
|
|
max-height: calc(100vh - var(--header-height)) !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
main > .container.chat-container-override {
|
|
max-width: 100% !important;
|
|
width: 100% !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
height: 100%;
|
|
}
|
|
|
|
.chat-layout {
|
|
display: flex;
|
|
flex: 1;
|
|
width: 100%;
|
|
background: var(--background);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ============================================
|
|
SIDEBAR - Historia konwersacji (jasna wersja)
|
|
============================================ */
|
|
.chat-sidebar {
|
|
width: 280px;
|
|
min-width: 200px;
|
|
max-width: 500px;
|
|
background: #f5f7fa;
|
|
color: #374151;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
transition: width 0.2s ease, min-width 0.2s ease;
|
|
}
|
|
|
|
.chat-sidebar.collapsed {
|
|
width: 48px !important;
|
|
min-width: 48px !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chat-sidebar.collapsed .sidebar-header,
|
|
.chat-sidebar.collapsed .sidebar-title,
|
|
.chat-sidebar.collapsed .conversations-list {
|
|
display: none;
|
|
}
|
|
|
|
.chat-sidebar.resizing {
|
|
transition: none;
|
|
user-select: none;
|
|
}
|
|
|
|
/* Sidebar toggle button */
|
|
.sidebar-toggle-btn {
|
|
position: absolute;
|
|
top: 12px;
|
|
right: -14px;
|
|
width: 28px;
|
|
height: 28px;
|
|
background: #f5f7fa;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
color: #6b7280;
|
|
transition: var(--transition);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.sidebar-toggle-btn:hover {
|
|
background: #ffffff;
|
|
color: #374151;
|
|
}
|
|
|
|
.chat-sidebar.collapsed .sidebar-toggle-btn {
|
|
right: -14px;
|
|
}
|
|
|
|
/* Resize handle */
|
|
.sidebar-resize-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
right: -3px;
|
|
width: 6px;
|
|
height: 100%;
|
|
cursor: col-resize;
|
|
z-index: 5;
|
|
}
|
|
|
|
.sidebar-resize-handle:hover,
|
|
.sidebar-resize-handle.active {
|
|
background: linear-gradient(90deg, transparent, rgba(30, 48, 80, 0.15), transparent);
|
|
}
|
|
|
|
.chat-sidebar.collapsed .sidebar-resize-handle {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.new-chat-btn {
|
|
width: 100%;
|
|
padding: var(--spacing-md);
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius-lg);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.new-chat-btn:hover {
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.new-chat-btn svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.sidebar-title {
|
|
padding: var(--spacing-md);
|
|
font-size: var(--font-size-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: #6b7280;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.conversations-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 var(--spacing-sm);
|
|
}
|
|
|
|
.conversation-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
margin-bottom: 2px;
|
|
transition: var(--transition);
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.conversation-item:hover {
|
|
background: #ffffff;
|
|
}
|
|
|
|
.conversation-item.active {
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.conversation-item svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: #9ca3af;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.conversation-item svg.conversation-pin-icon {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.conversation-title {
|
|
flex: 1;
|
|
font-size: var(--font-size-sm);
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
line-height: 1.4;
|
|
min-width: 0;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.conversation-item svg:first-child {
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.conversation-actions {
|
|
display: flex;
|
|
gap: 2px;
|
|
opacity: 0;
|
|
flex-shrink: 0;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.conversation-item:hover .conversation-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.conversation-action-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #9ca3af;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: var(--radius-sm);
|
|
transition: var(--transition);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.conversation-action-btn:hover {
|
|
color: #6b7280;
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.conversation-action-btn.pin-btn:hover {
|
|
color: #dc2626;
|
|
background: rgba(220, 38, 38, 0.1);
|
|
}
|
|
|
|
.conversation-action-btn.delete-btn:hover {
|
|
color: #ef4444;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
.conversation-pin-icon {
|
|
color: #dc2626 !important;
|
|
flex-shrink: 0;
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-right: 2px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.conversations-section-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #9ca3af;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
padding: var(--spacing-sm) var(--spacing-md) 4px;
|
|
}
|
|
|
|
/* Rename inline input */
|
|
.conversation-rename-input {
|
|
flex: 1;
|
|
font-size: var(--font-size-sm);
|
|
border: 1px solid #d1d5db;
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px 6px;
|
|
outline: none;
|
|
background: white;
|
|
min-width: 0;
|
|
}
|
|
|
|
.conversation-rename-input:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
|
|
}
|
|
|
|
/* Keep actions visible for pinned items */
|
|
.conversation-item.pinned .conversation-actions .pin-btn {
|
|
opacity: 1;
|
|
color: #dc2626;
|
|
}
|
|
|
|
/* Unpin button - when hovering pinned item, pin btn shows "unpin" style */
|
|
.conversation-item.pinned:hover .conversation-actions .pin-btn {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.conversation-item.pinned:hover .conversation-actions .pin-btn:hover {
|
|
color: #6b7280;
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.conversation-item.pinned .conversation-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.conversation-item.pinned .conversation-actions .delete-btn,
|
|
.conversation-item.pinned .conversation-actions .rename-btn {
|
|
opacity: 0;
|
|
}
|
|
|
|
.conversation-item.pinned:hover .conversation-actions .delete-btn,
|
|
.conversation-item.pinned:hover .conversation-actions .rename-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.sidebar-empty {
|
|
padding: var(--spacing-lg);
|
|
text-align: center;
|
|
color: #6b7280;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.sidebar-loading {
|
|
padding: var(--spacing-lg);
|
|
text-align: center;
|
|
color: #6b7280;
|
|
}
|
|
|
|
/* ============================================
|
|
MAIN CHAT AREA - Styl NordaGPT
|
|
============================================ */
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
height: 100%; /* Wymuszenie pełnej wysokości */
|
|
max-height: 100%; /* Nie może przekroczyć rodzica */
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
position: relative;
|
|
z-index: 50;
|
|
flex-shrink: 0; /* Header nie może się kurczyć */
|
|
}
|
|
|
|
.chat-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.chat-header h1 {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.chat-header-badge {
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
min-height: 0; /* Kluczowe dla poprawnego działania overflow w flexbox */
|
|
overflow-y: auto;
|
|
padding: var(--spacing-lg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
background: white;
|
|
}
|
|
|
|
.message {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
max-width: 85%;
|
|
animation: messageSlide 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes messageSlide {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.message.user {
|
|
align-self: flex-end;
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.message-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.message.assistant .message-avatar {
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
}
|
|
|
|
.message.user .message-avatar {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.message-content {
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius-lg);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.message.assistant .message-content {
|
|
background: #f3f4f6;
|
|
color: var(--text-primary);
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
|
|
.message.user .message-content {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
/* Thinking info badge - pokazuje tryb i czas odpowiedzi */
|
|
.thinking-info-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
margin-top: var(--spacing-sm);
|
|
padding-top: var(--spacing-sm);
|
|
border-top: 1px solid #e5e7eb;
|
|
font-size: 11px;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.thinking-badge-level {
|
|
color: #2E4872;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.thinking-badge-time {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.thinking-badge-desc {
|
|
color: #9ca3af;
|
|
font-style: italic;
|
|
}
|
|
|
|
.thinking-badge-cost {
|
|
color: #f59e0b;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pro-upgrade-hint {
|
|
color: #9ca3af;
|
|
text-decoration: none;
|
|
font-size: 11px;
|
|
transition: color 0.2s;
|
|
}
|
|
.pro-upgrade-hint:hover {
|
|
color: #2E4872;
|
|
}
|
|
.pro-upgrade-hint strong {
|
|
color: #6366f1;
|
|
}
|
|
|
|
/* Klikalne linki jako kolorowe badge'y */
|
|
.message-content a {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
margin: 1px 2px;
|
|
border-radius: 12px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
font-size: 0.95em;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
/* 🏢 Linki do FIRM (nordabiznes.pl/company/) - pomarańczowy */
|
|
.message-content a.company-link {
|
|
background: #fff7ed;
|
|
color: #c2410c;
|
|
}
|
|
.message-content a.company-link:hover {
|
|
background: #ffedd5;
|
|
color: #9a3412;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(194, 65, 12, 0.2);
|
|
}
|
|
|
|
/* 👤 Linki do OSÓB (nordabiznes.pl/osoba/) - zielony */
|
|
.message-content a.person-link {
|
|
background: #ecfdf5;
|
|
color: #047857;
|
|
}
|
|
.message-content a.person-link:hover {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(4, 120, 87, 0.2);
|
|
}
|
|
|
|
/* 💬 Linki do FORUM - fioletowy */
|
|
.message-content a.forum-link {
|
|
background: #f5f3ff;
|
|
color: #2E4872;
|
|
}
|
|
.message-content a.forum-link:hover {
|
|
background: #ede9fe;
|
|
color: #6d28d9;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* 📰 Linki do AKTUALNOŚCI - zielony */
|
|
.message-content a.news-link {
|
|
background: #ecfdf5;
|
|
color: #059669;
|
|
}
|
|
.message-content a.news-link:hover {
|
|
background: #d1fae5;
|
|
color: #047857;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* 💼 Linki do B2B - żółty */
|
|
.message-content a.b2b-link {
|
|
background: #fefce8;
|
|
color: #ca8a04;
|
|
}
|
|
.message-content a.b2b-link:hover {
|
|
background: #fef9c3;
|
|
color: #a16207;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* 🔗 Linki ZEWNĘTRZNE (www, social media, maps) - niebieski */
|
|
.message-content a.external-link {
|
|
background: #eff6ff;
|
|
color: #1d4ed8;
|
|
}
|
|
.message-content a.external-link:hover {
|
|
background: #dbeafe;
|
|
color: #1e40af;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(29, 78, 216, 0.2);
|
|
}
|
|
|
|
/* Linki w wiadomościach użytkownika - białe tło */
|
|
.message.user .message-content a {
|
|
background: rgba(255,255,255,0.25);
|
|
color: white;
|
|
}
|
|
|
|
.message.user .message-content a:hover {
|
|
background: rgba(255,255,255,0.4);
|
|
}
|
|
|
|
.message-content ul, .message-content ol {
|
|
margin: var(--spacing-sm) 0;
|
|
padding-left: var(--spacing-lg);
|
|
}
|
|
|
|
.message-content li {
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.message-content strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Typing indicator */
|
|
.typing-indicator {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: var(--spacing-md);
|
|
background: #f3f4f6;
|
|
border-radius: var(--radius-lg);
|
|
width: fit-content;
|
|
}
|
|
|
|
.typing-indicator.active {
|
|
display: flex;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #2E4872;
|
|
animation: typingBounce 1.4s infinite;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
|
|
@keyframes typingBounce {
|
|
0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
30% { transform: translateY(-6px); opacity: 1; }
|
|
}
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
padding: var(--spacing-2xl);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: var(--spacing-lg);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.empty-state h2 {
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.suggestions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-sm);
|
|
justify-content: center;
|
|
margin-top: var(--spacing-lg);
|
|
max-width: 600px;
|
|
}
|
|
|
|
.suggestion-chip {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.suggestion-chip:hover {
|
|
border-color: #2E4872;
|
|
color: #2E4872;
|
|
background: #f5f3ff;
|
|
}
|
|
|
|
/* Input area */
|
|
.chat-input-area {
|
|
padding: var(--spacing-lg);
|
|
padding-bottom: calc(var(--spacing-lg) + env(safe-area-inset-bottom, 8px));
|
|
background: white;
|
|
border-top: 1px solid var(--border);
|
|
flex-shrink: 0; /* Input area nie może się kurczyć - zawsze widoczna na dole */
|
|
}
|
|
|
|
.chat-input-wrapper {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.chat-input {
|
|
flex: 1;
|
|
padding: var(--spacing-md);
|
|
border: 2px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
font-size: var(--font-size-base);
|
|
font-family: inherit;
|
|
resize: none;
|
|
min-height: 50px;
|
|
max-height: 150px;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.chat-input:focus {
|
|
outline: none;
|
|
border-color: #2E4872;
|
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
|
}
|
|
|
|
.send-btn {
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.send-btn:hover:not(:disabled) {
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.send-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.send-btn svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
/* Mobile responsive */
|
|
@media (max-width: 768px) {
|
|
.chat-sidebar {
|
|
display: none;
|
|
position: fixed;
|
|
top: var(--header-height);
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 100;
|
|
width: 280px;
|
|
box-shadow: 4px 0 20px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.chat-sidebar.mobile-open {
|
|
display: flex;
|
|
}
|
|
|
|
.mobile-menu-btn {
|
|
display: flex !important;
|
|
}
|
|
|
|
.message {
|
|
max-width: 95%;
|
|
}
|
|
}
|
|
|
|
.mobile-menu-btn {
|
|
display: none;
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: white;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: var(--spacing-sm);
|
|
}
|
|
|
|
/* ============================================
|
|
Thinking Mode Toggle
|
|
============================================ */
|
|
.thinking-toggle {
|
|
position: relative;
|
|
margin-left: var(--spacing-sm);
|
|
}
|
|
|
|
.thinking-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 10px;
|
|
background: rgba(255,255,255,0.2);
|
|
border: 1px solid rgba(255,255,255,0.3);
|
|
border-radius: var(--radius);
|
|
color: white;
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.thinking-btn:hover {
|
|
background: rgba(255,255,255,0.3);
|
|
}
|
|
|
|
.thinking-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.thinking-arrow {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.thinking-toggle.open .thinking-arrow {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.thinking-dropdown {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 8px);
|
|
right: 0;
|
|
width: 280px;
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
z-index: 100;
|
|
overflow: hidden;
|
|
animation: dropdownSlide 0.2s ease;
|
|
}
|
|
|
|
@keyframes dropdownSlide {
|
|
from { opacity: 0; transform: translateY(-8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.thinking-toggle.open .thinking-dropdown {
|
|
display: block;
|
|
}
|
|
|
|
.thinking-dropdown-header {
|
|
padding: var(--spacing-md);
|
|
background: #f5f3ff;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.thinking-dropdown-header strong {
|
|
display: block;
|
|
color: #1e3050;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.thinking-dropdown-header p {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-xs);
|
|
margin: 0;
|
|
}
|
|
|
|
.thinking-option {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.thinking-option:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.thinking-option:hover {
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.thinking-option.active {
|
|
background: #f5f3ff;
|
|
border-left: 3px solid #2E4872;
|
|
}
|
|
|
|
.thinking-option-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.thinking-option-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.thinking-option-name {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.thinking-option-badge {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
background: #2E4872;
|
|
color: white;
|
|
border-radius: var(--radius-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.thinking-option-badge.premium {
|
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
}
|
|
|
|
.thinking-option-badge.free {
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
}
|
|
|
|
.thinking-option-price.free {
|
|
color: #10b981;
|
|
font-weight: 500;
|
|
font-style: normal;
|
|
}
|
|
|
|
.thinking-option-price {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: #6b7280;
|
|
margin-top: 4px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Monthly Cost Badge */
|
|
.usage-bars-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
margin-left: var(--spacing-sm);
|
|
min-width: 120px;
|
|
}
|
|
|
|
.usage-bar-mini {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.usage-bar-label {
|
|
opacity: 0.7;
|
|
min-width: 32px;
|
|
text-align: right;
|
|
color: rgba(255,255,255,0.8);
|
|
}
|
|
|
|
.usage-bar-track {
|
|
flex: 1;
|
|
height: 6px;
|
|
background: rgba(255,255,255,0.15);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
min-width: 50px;
|
|
}
|
|
|
|
.usage-bar-track.global {
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.usage-bar-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
background: #4ade80;
|
|
transition: width 0.5s ease, background 0.3s ease;
|
|
}
|
|
|
|
.usage-bar-fill.warning {
|
|
background: #fbbf24;
|
|
}
|
|
|
|
.usage-bar-fill.danger {
|
|
background: #f87171;
|
|
}
|
|
|
|
.usage-bar-fill.global {
|
|
background: #60a5fa;
|
|
}
|
|
|
|
.usage-bar-fill.global.warning {
|
|
background: #fbbf24;
|
|
}
|
|
|
|
.usage-bar-fill.global.danger {
|
|
background: #f87171;
|
|
}
|
|
|
|
.usage-bar-pct {
|
|
min-width: 24px;
|
|
text-align: right;
|
|
font-weight: 600;
|
|
color: rgba(255,255,255,0.9);
|
|
font-size: 9px;
|
|
}
|
|
|
|
/* Limit exceeded banner */
|
|
.limit-exceeded-banner {
|
|
display: none;
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
color: #92400e;
|
|
padding: 12px 16px;
|
|
border-radius: var(--radius);
|
|
margin: 8px 16px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
text-align: center;
|
|
}
|
|
|
|
.limit-exceeded-banner.active {
|
|
display: block;
|
|
}
|
|
|
|
.limit-exceeded-banner .limit-btn {
|
|
display: inline-block;
|
|
margin-top: 8px;
|
|
padding: 6px 16px;
|
|
background: #92400e;
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.limit-exceeded-banner .limit-btn:hover {
|
|
background: #78350f;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.usage-bars-container {
|
|
min-width: 80px;
|
|
}
|
|
.usage-bar-label {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.thinking-option-desc {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-xs);
|
|
margin: 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.thinking-label {
|
|
display: none;
|
|
}
|
|
|
|
.thinking-dropdown {
|
|
width: 260px;
|
|
right: -60px;
|
|
}
|
|
}
|
|
|
|
/* ============================================
|
|
Model Info Button & Modal
|
|
============================================ */
|
|
.model-info-btn {
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: white;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-left: var(--spacing-xs);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.model-info-btn:hover {
|
|
background: rgba(255,255,255,0.4);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.model-info-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.6);
|
|
z-index: 1000;
|
|
padding: var(--spacing-lg);
|
|
overflow-y: auto;
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
.model-info-modal.active {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.model-info-content {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
max-width: 600px;
|
|
width: 100%;
|
|
padding: var(--spacing-xl);
|
|
margin-top: 60px;
|
|
position: relative;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
|
animation: slideUp 0.3s ease;
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.model-info-close {
|
|
position: absolute;
|
|
top: var(--spacing-md);
|
|
right: var(--spacing-md);
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.model-info-close:hover {
|
|
background: #f3f4f6;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.model-info-content h2 {
|
|
font-size: var(--font-size-xl);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-lg);
|
|
padding-right: var(--spacing-xl);
|
|
}
|
|
|
|
.model-info-content h3 {
|
|
font-size: var(--font-size-md);
|
|
color: var(--text-primary);
|
|
margin: var(--spacing-lg) 0 var(--spacing-md);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.model-current {
|
|
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
|
|
border: 1px solid #c4b5fd;
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.model-current h3 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.model-badge-large {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.model-name {
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-weight: 600;
|
|
font-size: var(--font-size-md);
|
|
}
|
|
|
|
.model-provider {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.model-description {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
}
|
|
|
|
.specs-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.specs-table td {
|
|
padding: var(--spacing-sm) 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.specs-table td:first-child {
|
|
color: var(--text-secondary);
|
|
width: 40%;
|
|
}
|
|
|
|
.specs-table td:last-child {
|
|
text-align: right;
|
|
}
|
|
|
|
.spec-change {
|
|
display: inline-block;
|
|
font-size: var(--font-size-xs);
|
|
color: #16a34a;
|
|
background: #dcfce7;
|
|
padding: 1px 6px;
|
|
border-radius: var(--radius);
|
|
margin-left: var(--spacing-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: var(--spacing-lg);
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 8px;
|
|
bottom: 8px;
|
|
width: 2px;
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
padding-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.timeline-item:last-child {
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: calc(-1 * var(--spacing-lg) + 2px);
|
|
top: 6px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #d1d5db;
|
|
border: 2px solid white;
|
|
}
|
|
|
|
.timeline-item.current::before {
|
|
background: #2E4872;
|
|
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.2);
|
|
}
|
|
|
|
.timeline-date {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-xs);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.timeline-content strong {
|
|
color: var(--text-primary);
|
|
display: block;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.timeline-content p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin: 0 0 var(--spacing-sm);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.timeline-badge {
|
|
display: inline-block;
|
|
font-size: var(--font-size-xs);
|
|
padding: 2px var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
background: #f3f4f6;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.timeline-badge.upgrade {
|
|
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
|
color: white;
|
|
}
|
|
|
|
.timeline-badge.feature {
|
|
background: #dbeafe;
|
|
color: #1d4ed8;
|
|
}
|
|
|
|
.timeline-badge.launch {
|
|
background: #dcfce7;
|
|
color: #16a34a;
|
|
}
|
|
|
|
.model-benefits ul {
|
|
margin: 0;
|
|
padding-left: var(--spacing-lg);
|
|
}
|
|
|
|
.model-benefits li {
|
|
padding: var(--spacing-xs) 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.model-benefits li strong {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ============================================
|
|
Video Help Button & Modal
|
|
============================================ */
|
|
.video-help-btn {
|
|
background: rgba(255,255,255,0.3) !important;
|
|
}
|
|
|
|
.video-help-btn:hover {
|
|
background: rgba(255,255,255,0.5) !important;
|
|
}
|
|
|
|
.video-help-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.8);
|
|
z-index: 1001;
|
|
padding: var(--spacing-lg);
|
|
overflow-y: auto;
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
.video-help-modal.active {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
}
|
|
|
|
.video-help-content {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
max-width: 800px;
|
|
width: 100%;
|
|
padding: var(--spacing-xl);
|
|
margin-top: 40px;
|
|
position: relative;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
animation: slideUp 0.3s ease;
|
|
}
|
|
|
|
.video-help-close {
|
|
position: absolute;
|
|
top: var(--spacing-md);
|
|
right: var(--spacing-md);
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.video-help-close:hover {
|
|
background: #f3f4f6;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.video-help-content h2 {
|
|
font-size: var(--font-size-xl);
|
|
color: var(--text-primary);
|
|
margin: 0 0 var(--spacing-xs);
|
|
padding-right: var(--spacing-xl);
|
|
}
|
|
|
|
.video-help-subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
margin: 0 0 var(--spacing-lg);
|
|
}
|
|
|
|
.video-container {
|
|
position: relative;
|
|
background: #000;
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.video-container video {
|
|
width: 100%;
|
|
display: block;
|
|
max-height: 450px;
|
|
}
|
|
|
|
.video-help-tips {
|
|
background: #f5f3ff;
|
|
border: 1px solid #c4b5fd;
|
|
border-radius: var(--radius);
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.video-help-tips h3 {
|
|
font-size: var(--font-size-md);
|
|
color: var(--text-primary);
|
|
margin: 0 0 var(--spacing-md);
|
|
}
|
|
|
|
.video-help-tips ul {
|
|
margin: 0;
|
|
padding-left: var(--spacing-lg);
|
|
}
|
|
|
|
.video-help-tips li {
|
|
padding: var(--spacing-xs) 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.video-help-tips li strong {
|
|
color: #1e3050;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.video-help-content {
|
|
margin-top: 20px;
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.video-container video {
|
|
max-height: 280px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="chat-layout">
|
|
<!-- Sidebar z historią konwersacji -->
|
|
<aside class="chat-sidebar" id="chatSidebar">
|
|
<button class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Zwiń/rozwiń panel" id="sidebarToggleBtn">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="14" height="14" id="sidebarToggleIcon">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
|
|
|
<div class="sidebar-header">
|
|
<button class="new-chat-btn" onclick="startNewConversation()">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
Nowa rozmowa
|
|
</button>
|
|
</div>
|
|
|
|
<div class="sidebar-title">Historia rozmów</div>
|
|
|
|
<div class="conversations-list" id="conversationsList">
|
|
<div class="sidebar-loading">Ładowanie...</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Główny obszar chatu -->
|
|
<main class="chat-main">
|
|
<header class="chat-header">
|
|
<div class="chat-header-left">
|
|
<button class="mobile-menu-btn" onclick="toggleSidebar()">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="20" height="20">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
</svg>
|
|
</button>
|
|
<img src="{{ url_for('static', filename='img/nordagpt-icon.svg') }}" alt="NordaGPT" style="width: 32px; height: 32px;">
|
|
<h1>NordaGPT</h1>
|
|
<!-- Model Selection Toggle -->
|
|
<div class="thinking-toggle" id="modelToggle">
|
|
<button class="thinking-btn" onclick="toggleModelDropdown()" title="Wybierz model AI">
|
|
<span class="thinking-icon" id="modelIcon">⚡</span>
|
|
<span class="thinking-label" id="modelLabel">Flash</span>
|
|
<svg class="thinking-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24" width="12" height="12">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="thinking-dropdown" id="modelDropdown">
|
|
<div class="thinking-dropdown-header">
|
|
<strong>Tryb pracy AI</strong>
|
|
<p>Flash wystarczy do większości pytań</p>
|
|
</div>
|
|
<div class="thinking-option active" data-model="flash" onclick="setModel('flash')">
|
|
<div class="thinking-option-header">
|
|
<span class="thinking-option-icon">⚡</span>
|
|
<span class="thinking-option-name">Flash</span>
|
|
<span class="thinking-option-badge">Zalecany</span>
|
|
</div>
|
|
<p class="thinking-option-desc">Szybki i inteligentny. Pytania o firmy, kontakty, usługi, branże.</p>
|
|
<span class="thinking-option-price">Wystarczający do większości pytań</span>
|
|
</div>
|
|
<div class="thinking-option" data-model="pro" onclick="setModel('pro')">
|
|
<div class="thinking-option-header">
|
|
<span class="thinking-option-icon">🧠</span>
|
|
<span class="thinking-option-name">Pro</span>
|
|
<span class="thinking-option-badge premium">Premium</span>
|
|
</div>
|
|
<p class="thinking-option-desc">Głębsza analiza, porównania, raporty. Dłuższe odpowiedzi.</p>
|
|
<span class="thinking-option-price">Zużywa limit ~4x szybciej niż Flash</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Usage bars -->
|
|
<div class="usage-bars-container" id="usageBarsContainer" title="Zużycie limitu AI">
|
|
<div class="usage-bar-mini" title="Twój limit dzienny">
|
|
<span class="usage-bar-label">Dziś</span>
|
|
<div class="usage-bar-track">
|
|
<div class="usage-bar-fill" id="dailyUsageBar" style="width: 0%"></div>
|
|
</div>
|
|
<span class="usage-bar-pct" id="dailyUsagePct">0%</span>
|
|
</div>
|
|
<div class="usage-bar-mini" title="Zużycie wszystkich użytkowników portalu w tym miesiącu">
|
|
<span class="usage-bar-label">Portal</span>
|
|
<div class="usage-bar-track global">
|
|
<div class="usage-bar-fill global" id="globalUsageBar" style="width: 0%"></div>
|
|
</div>
|
|
<span class="usage-bar-pct" id="globalUsagePct">0%</span>
|
|
</div>
|
|
</div>
|
|
<button class="model-info-btn" onclick="openModelInfoModal()" title="Informacje o modelu AI">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="model-info-btn video-help-btn" onclick="openVideoHelpModal()" title="Jak korzystać z NordaGPT? (Wideo)">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Limit exceeded banner -->
|
|
<div class="limit-exceeded-banner" id="limitExceededBanner">
|
|
<span id="limitExceededMsg"></span>
|
|
<br>
|
|
<button class="limit-btn" onclick="requestHigherLimits()">Jestem zainteresowany wyższymi limitami</button>
|
|
</div>
|
|
|
|
<!-- Modal z informacjami o modelu AI i historią rozwoju -->
|
|
<div class="model-info-modal" id="modelInfoModal">
|
|
<div class="model-info-content">
|
|
<button class="model-info-close" onclick="closeModelInfoModal()">×</button>
|
|
|
|
<h2>🤖 NordaGPT — Jak to działa</h2>
|
|
|
|
<div class="model-current">
|
|
<h3>Czym jest NordaGPT?</h3>
|
|
<p class="model-description">
|
|
NordaGPT to asystent AI, który zna wszystkie firmy w Izbie Norda Biznes.
|
|
Możesz go zapytać o dowolną firmę, branżę, kontakt, usługę — a on przeszuka
|
|
bazę danych portalu i odpowie po polsku, powołując się na konkretne firmy.
|
|
</p>
|
|
<p class="model-description" style="margin-top: 8px;">
|
|
Każde pytanie, które zadajesz, jest analizowane przez sztuczną inteligencję Google (Gemini).
|
|
Asystent czyta informacje o wszystkich firmach w Izbie (~120 firm, ok. 80 000 słów),
|
|
rozumie Twoje pytanie i przygotowuje odpowiedź dopasowaną do kontekstu.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>⚡ Dwa tryby pracy</h3>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
|
<div class="model-badge-large" style="flex: 1; min-width: 150px; background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); border: 2px solid #3b82f6;">
|
|
<span class="model-name">⚡ Flash</span>
|
|
<span class="model-provider" style="color: #3b82f6;">ZALECANY</span>
|
|
</div>
|
|
<div class="model-badge-large" style="flex: 1; min-width: 150px; background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%); border: 2px solid #a855f7;">
|
|
<span class="model-name">🧠 Pro</span>
|
|
<span class="model-provider" style="color: #a855f7;">PREMIUM</span>
|
|
</div>
|
|
</div>
|
|
<table class="specs-table">
|
|
<tr>
|
|
<td><strong>⚡ Flash</strong></td>
|
|
<td>Szybki i inteligentny. Idealny do pytań o firmy, kontakty, branże, usługi. Odpowiada w kilka sekund.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>🧠 Pro</strong></td>
|
|
<td>Głębsza analiza i dłuższe odpowiedzi. Dla złożonych pytań, porównań, raportów. Zużywa limit ~4x szybciej.</td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-size: 0.85em; color: #666; margin-top: 0.5rem;">
|
|
Zalecamy korzystanie z trybu Flash na co dzień — jest wystarczający do większości pytań.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>📊 Limity użytkowania</h3>
|
|
<p class="model-description">
|
|
Każde pytanie do NordaGPT wiąże się z kosztem — asystent przetwarza dane o wszystkich
|
|
firmach Izby przy każdej rozmowie (odpowiednik przeczytania ~160 stron tekstu).
|
|
Dlatego każdy użytkownik ma indywidualny limit zapytań:
|
|
</p>
|
|
<table class="specs-table" style="margin-top: 8px;">
|
|
<tr>
|
|
<td>Dziennie:</td>
|
|
<td><strong>2-3 pytania</strong> (tryb Flash)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Tygodniowo:</td>
|
|
<td><strong>do 9 pytań</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Miesięcznie:</td>
|
|
<td><strong>do 18 pytań</strong></td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-size: 0.85em; color: #666; margin-top: 0.5rem;">
|
|
Limity tygodniowe i miesięczne nie sumują się wprost — pozwalają na elastyczne korzystanie
|
|
(np. więcej pytań jednego dnia, mniej innego). Tryb Pro zużywa limit ~4x szybciej.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>💡 Jak pisać pytania?</h3>
|
|
<table class="specs-table">
|
|
<tr>
|
|
<td>Twoje pytanie:</td>
|
|
<td>Może mieć <strong>do kilku akapitów</strong> (~2000 słów). W praktyce jedno-dwa zdania w zupełności wystarczą.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Odpowiedź AI:</td>
|
|
<td>Zazwyczaj <strong>2-5 akapitów</strong> z konkretnymi firmami i danymi kontaktowymi.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Historia rozmowy:</td>
|
|
<td>Asystent pamięta wcześniejsze pytania w danej rozmowie — możesz dopytywać.</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>💰 Koszty i przyszłość</h3>
|
|
<p class="model-description">
|
|
Obecnie korzystanie z NordaGPT jest wliczone w członkostwo w Izbie.
|
|
W przyszłości, w miarę rosnącego użycia, może zostać wprowadzone
|
|
indywidualne rozliczanie za wyższe limity. Jeśli potrzebujesz więcej zapytań
|
|
niż pozwala Twój limit — kliknij przycisk „Jestem zainteresowany wyższymi limitami",
|
|
a administrator skontaktuje się z Tobą.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="model-history">
|
|
<h3>📜 Historia rozwoju NordaGPT</h3>
|
|
<div class="timeline">
|
|
<div class="timeline-item current">
|
|
<div class="timeline-date">07.02.2026</div>
|
|
<div class="timeline-content">
|
|
<strong>Paid Tier 1 + Gemini 3 Pro</strong>
|
|
<p>Przejście na płatny tier Google AI. Domyślnie Flash (thinking mode), opcja Pro dla najlepszych odpowiedzi. Hint „Spróbuj Pro" przy każdej odpowiedzi.</p>
|
|
<span class="timeline-badge upgrade">Aktualna wersja</span>
|
|
</div>
|
|
</div>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">28.01.2026</div>
|
|
<div class="timeline-content">
|
|
<strong>Gemini 3 Flash (Preview)</strong>
|
|
<p>Najnowsza generacja AI od Google. 7x lepsze rozumowanie, thinking mode, 78% na benchmarku kodowania.</p>
|
|
<span class="timeline-badge">Upgrade modelu</span>
|
|
</div>
|
|
</div>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">14.01.2026</div>
|
|
<div class="timeline-content">
|
|
<strong>Gemini 2.5 Flash-Lite</strong>
|
|
<p>8x dłuższe odpowiedzi, pełny thinking mode, 4x większy limit dzienny.</p>
|
|
<span class="timeline-badge">Poprzednia wersja</span>
|
|
</div>
|
|
</div>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">13.01.2026</div>
|
|
<div class="timeline-content">
|
|
<strong>Historia konwersacji</strong>
|
|
<p>Sidebar z historią rozmów - możliwość powrotu do wcześniejszych konwersacji.</p>
|
|
<span class="timeline-badge feature">Nowa funkcja</span>
|
|
</div>
|
|
</div>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">Grudzień 2025</div>
|
|
<div class="timeline-content">
|
|
<strong>Gemini 2.0 Flash</strong>
|
|
<p>Pierwszy model AI w NordaGPT. Kontekst 1M tokenów.</p>
|
|
<span class="timeline-badge">Poprzednia wersja</span>
|
|
</div>
|
|
</div>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">Listopad 2025</div>
|
|
<div class="timeline-content">
|
|
<strong>Uruchomienie NordaGPT</strong>
|
|
<p>Premiera asystenta AI dla członków Norda Biznes. Integracja z bazą {{ COMPANY_COUNT }} firm.</p>
|
|
<span class="timeline-badge launch">Premiera</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="model-benefits">
|
|
<h3>✨ Co działa w NordaGPT?</h3>
|
|
<ul>
|
|
<li><strong>Wybór modelu</strong> — Flash (thinking mode, domyślny) lub Pro (premium, najlepsza analiza)</li>
|
|
<li><strong>Baza {{ COMPANY_COUNT }} firm</strong> — pełna wiedza o członkach Izby</li>
|
|
<li><strong>Forum i wydarzenia</strong> — dostęp do dyskusji i kalendarza</li>
|
|
<li><strong>Linki w odpowiedziach</strong> — bezpośrednie odnośniki do profili firm i osób</li>
|
|
<li><strong>Transparentne koszty</strong> — widoczny koszt każdej odpowiedzi i miesięczne zużycie</li>
|
|
<li><strong>Historia rozmów</strong> — pełna historia konwersacji w sidebarze</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal z wideo pomocowym -->
|
|
<div class="video-help-modal" id="videoHelpModal">
|
|
<div class="video-help-content">
|
|
<button class="video-help-close" onclick="closeVideoHelpModal()">×</button>
|
|
<h2>🎬 Jak korzystać z NordaGPT?</h2>
|
|
<p class="video-help-subtitle">Krótki przewodnik (40 sekund)</p>
|
|
|
|
<div class="video-container">
|
|
<video id="helpVideo" controls poster="{{ url_for('static', filename='videos/nordagpt-demo-poster.jpg') }}">
|
|
<source src="{{ url_for('static', filename='videos/nordagpt-demo.mp4') }}" type="video/mp4">
|
|
Twoja przeglądarka nie obsługuje wideo HTML5.
|
|
</video>
|
|
</div>
|
|
|
|
<div class="video-help-tips">
|
|
<h3>💡 Szybkie wskazówki</h3>
|
|
<ul>
|
|
<li><strong>Znajdź firmę:</strong> "Kto oferuje usługi IT?"</li>
|
|
<li><strong>Sprawdź prezesa:</strong> "Kto jest prezesem PIXLAB?"</li>
|
|
<li><strong>Wydarzenia:</strong> "Kiedy następne spotkanie Norda?"</li>
|
|
<li><strong>Rekomendacje:</strong> "Poleć drukarnie z dobrymi opiniami"</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-messages" id="chatMessages">
|
|
<!-- Empty state - pokazywany gdy brak wiadomości -->
|
|
<div class="empty-state" id="emptyState">
|
|
<img src="{{ url_for('static', filename='img/nordagpt-icon.svg') }}" alt="NordaGPT" style="width: 80px; height: 80px; margin-bottom: 1rem;">
|
|
<h2>NordaGPT - Asystent AI Norda Biznes</h2>
|
|
<p>Mogę pomóc Ci znaleźć firmy, usługi, sprawdzić kalendarz wydarzeń, rekomendacje i wiele więcej.</p>
|
|
|
|
<div class="suggestions">
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Szukam partnera do projektu budowlanego')">
|
|
Szukam partnera do projektu
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kto w Izbie zajmuje się marketingiem?')">
|
|
Kto zajmuje się marketingiem?
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Poleć firmę z dobrymi opiniami Google')">
|
|
Poleć firmę z opiniami
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Co nowego na forum?')">
|
|
Co nowego na forum?
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Typing indicator -->
|
|
<div class="message assistant" id="typingIndicator" style="display: none;">
|
|
<div class="message-avatar">AI</div>
|
|
<div class="typing-indicator active">
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-input-area">
|
|
<div class="chat-input-wrapper">
|
|
<textarea
|
|
id="messageInput"
|
|
class="chat-input"
|
|
placeholder="Wpisz swoją wiadomość..."
|
|
rows="1"
|
|
onkeypress="if(event.key==='Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
|
></textarea>
|
|
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
|
|
</svg>
|
|
Wyślij
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// NordaGPT Chat - State
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
let currentConversationId = null;
|
|
let conversations = [];
|
|
let currentModel = 'flash'; // Default model (Gemini 3 Flash - thinking mode, 10K RPD)
|
|
let monthlyUsageCost = 0; // Koszt miesięczny użytkownika
|
|
|
|
// ============================================
|
|
// Model Selection Toggle Functions
|
|
// ============================================
|
|
const MODEL_CONFIG = {
|
|
'flash': { label: 'Flash', icon: '⚡', desc: 'Thinking' },
|
|
'pro': { label: 'Pro', icon: '🧠', desc: 'Analiza' }
|
|
};
|
|
|
|
function toggleModelDropdown() {
|
|
const toggle = document.getElementById('modelToggle');
|
|
toggle.classList.toggle('open');
|
|
}
|
|
|
|
function setModel(model) {
|
|
currentModel = model;
|
|
const config = MODEL_CONFIG[model];
|
|
|
|
// Update UI
|
|
document.getElementById('modelLabel').textContent = config.label;
|
|
document.getElementById('modelIcon').textContent = config.icon;
|
|
|
|
// Update active state
|
|
document.querySelectorAll('.thinking-option').forEach(opt => {
|
|
opt.classList.toggle('active', opt.dataset.model === model);
|
|
});
|
|
|
|
// Close dropdown
|
|
document.getElementById('modelToggle').classList.remove('open');
|
|
|
|
// Save preference to server
|
|
saveModelPreference(model);
|
|
|
|
console.log('Model set to:', model);
|
|
}
|
|
|
|
async function saveModelPreference(model) {
|
|
try {
|
|
await fetch('/api/chat/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
body: JSON.stringify({ model: model })
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to save model preference:', error);
|
|
}
|
|
}
|
|
|
|
function updateMonthlyCost(cost) {
|
|
monthlyUsageCost += cost;
|
|
// Refresh usage bars after each message
|
|
refreshUsageBars();
|
|
}
|
|
|
|
function updateUsageBars(usage) {
|
|
if (!usage) return;
|
|
|
|
// Daily bar (user)
|
|
const dailyBar = document.getElementById('dailyUsageBar');
|
|
const dailyPct = document.getElementById('dailyUsagePct');
|
|
if (dailyBar && dailyPct) {
|
|
const dp = Math.min(usage.daily_percent || 0, 100);
|
|
dailyBar.style.width = dp + '%';
|
|
dailyPct.textContent = Math.round(dp) + '%';
|
|
dailyBar.className = 'usage-bar-fill' + (dp >= 90 ? ' danger' : dp >= 60 ? ' warning' : '');
|
|
}
|
|
|
|
// Global bar (portal)
|
|
const globalBar = document.getElementById('globalUsageBar');
|
|
const globalPct = document.getElementById('globalUsagePct');
|
|
if (globalBar && globalPct) {
|
|
const gp = Math.min(usage.global_monthly_percent || 0, 100);
|
|
globalBar.style.width = gp + '%';
|
|
globalPct.textContent = Math.round(gp) + '%';
|
|
globalBar.className = 'usage-bar-fill global' + (gp >= 90 ? ' danger' : gp >= 60 ? ' warning' : '');
|
|
}
|
|
|
|
// Check if any limit is close or exceeded
|
|
const maxPct = Math.max(usage.daily_percent || 0, usage.weekly_percent || 0, usage.monthly_percent || 0);
|
|
if (maxPct >= 100) {
|
|
showLimitBanner(usage);
|
|
}
|
|
}
|
|
|
|
function showLimitBanner(usage) {
|
|
const banner = document.getElementById('limitExceededBanner');
|
|
const msg = document.getElementById('limitExceededMsg');
|
|
if (!banner || !msg) return;
|
|
|
|
let text = '';
|
|
if ((usage.monthly_percent || 0) >= 100) {
|
|
text = 'Wykorzystano miesięczny limit pytań do NordaGPT. Nowe pytania będą dostępne od 1. dnia kolejnego miesiąca.';
|
|
} else if ((usage.weekly_percent || 0) >= 100) {
|
|
text = 'Wykorzystano tygodniowy limit pytań do NordaGPT. Nowe pytania będą dostępne od poniedziałku.';
|
|
} else if ((usage.daily_percent || 0) >= 100) {
|
|
text = 'Wykorzystano dzisiejszy limit pytań do NordaGPT. Jutro będziesz mógł zadać kolejne pytania.';
|
|
}
|
|
|
|
if (text) {
|
|
msg.textContent = text;
|
|
banner.classList.add('active');
|
|
}
|
|
}
|
|
|
|
async function requestHigherLimits() {
|
|
try {
|
|
const response = await fetch('/api/chat/request-higher-limits', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
const btn = document.querySelector('.limit-exceeded-banner .limit-btn');
|
|
if (btn) {
|
|
btn.textContent = 'Zgłoszenie wysłane';
|
|
btn.disabled = true;
|
|
btn.style.opacity = '0.6';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error requesting higher limits:', error);
|
|
}
|
|
}
|
|
|
|
async function refreshUsageBars() {
|
|
try {
|
|
const response = await fetch('/api/chat/settings');
|
|
const data = await response.json();
|
|
if (data.success && data.usage) {
|
|
updateUsageBars(data.usage);
|
|
}
|
|
} catch (e) {
|
|
console.log('Could not refresh usage bars');
|
|
}
|
|
}
|
|
|
|
// Close model dropdown when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
const toggle = document.getElementById('modelToggle');
|
|
if (toggle && !toggle.contains(e.target)) {
|
|
toggle.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// Model Info Modal Functions
|
|
// ============================================
|
|
function openModelInfoModal() {
|
|
document.getElementById('modelInfoModal').classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeModelInfoModal() {
|
|
document.getElementById('modelInfoModal').classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Close modal on backdrop click
|
|
document.addEventListener('click', function(e) {
|
|
const modal = document.getElementById('modelInfoModal');
|
|
if (e.target === modal) {
|
|
closeModelInfoModal();
|
|
}
|
|
});
|
|
|
|
// Close modal on Escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeModelInfoModal();
|
|
closeVideoHelpModal();
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// Video Help Modal Functions
|
|
// ============================================
|
|
function openVideoHelpModal() {
|
|
document.getElementById('videoHelpModal').classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
// Auto-play video when modal opens
|
|
const video = document.getElementById('helpVideo');
|
|
if (video) {
|
|
video.currentTime = 0;
|
|
// Don't autoplay - let user control
|
|
}
|
|
}
|
|
|
|
function closeVideoHelpModal() {
|
|
document.getElementById('videoHelpModal').classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
// Pause video when modal closes
|
|
const video = document.getElementById('helpVideo');
|
|
if (video) {
|
|
video.pause();
|
|
}
|
|
}
|
|
|
|
// Close video modal on backdrop click
|
|
document.addEventListener('click', function(e) {
|
|
const videoModal = document.getElementById('videoHelpModal');
|
|
if (e.target === videoModal) {
|
|
closeVideoHelpModal();
|
|
}
|
|
});
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadConversations();
|
|
autoResizeTextarea();
|
|
loadModelSettings();
|
|
});
|
|
|
|
// Load model settings and usage from server
|
|
async function loadModelSettings() {
|
|
try {
|
|
const response = await fetch('/api/chat/settings');
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// Always start with Flash - ignore saved preference
|
|
currentModel = 'flash';
|
|
const config = MODEL_CONFIG['flash'];
|
|
document.getElementById('modelLabel').textContent = config.label;
|
|
document.getElementById('modelIcon').textContent = config.icon;
|
|
document.querySelectorAll('.thinking-option').forEach(opt => {
|
|
opt.classList.toggle('active', opt.dataset.model === 'flash');
|
|
});
|
|
// Load usage bars
|
|
if (data.monthly_cost !== undefined) {
|
|
monthlyUsageCost = data.monthly_cost;
|
|
}
|
|
if (data.usage) {
|
|
updateUsageBars(data.usage);
|
|
}
|
|
// Hide usage bars for unlimited users
|
|
if (data.is_unlimited) {
|
|
const container = document.getElementById('usageBarsContainer');
|
|
if (container) container.style.display = 'none';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Using default model:', currentModel);
|
|
}
|
|
}
|
|
|
|
// Load conversations list
|
|
async function loadConversations() {
|
|
try {
|
|
const response = await fetch('/api/chat/conversations');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
conversations = data.conversations;
|
|
renderConversationsList();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading conversations:', error);
|
|
document.getElementById('conversationsList').innerHTML =
|
|
'<div class="sidebar-empty">Nie udało się załadować historii</div>';
|
|
}
|
|
}
|
|
|
|
// Render conversations in sidebar
|
|
function renderConversationsList() {
|
|
const list = document.getElementById('conversationsList');
|
|
|
|
if (conversations.length === 0) {
|
|
list.innerHTML = '<div class="sidebar-empty">Brak poprzednich rozmów.<br>Rozpocznij nową!</div>';
|
|
return;
|
|
}
|
|
|
|
const pinned = conversations.filter(c => c.is_pinned);
|
|
const unpinned = conversations.filter(c => !c.is_pinned);
|
|
|
|
let html = '';
|
|
|
|
if (pinned.length > 0) {
|
|
html += '<div class="conversations-section-title">Przypięte</div>';
|
|
html += pinned.map(conv => renderConversationItem(conv, true)).join('');
|
|
}
|
|
|
|
if (pinned.length > 0 && unpinned.length > 0) {
|
|
html += '<div class="conversations-section-title">Historia</div>';
|
|
}
|
|
|
|
html += unpinned.map(conv => renderConversationItem(conv, false)).join('');
|
|
|
|
list.innerHTML = html;
|
|
}
|
|
|
|
function renderConversationItem(conv, isPinned) {
|
|
return `
|
|
<div class="conversation-item ${conv.id === currentConversationId ? 'active' : ''} ${isPinned ? 'pinned' : ''}"
|
|
onclick="loadConversation(${conv.id})"
|
|
data-id="${conv.id}">
|
|
${isPinned ? '<svg class="conversation-pin-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/></svg>' : '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>'}
|
|
<span class="conversation-title">${escapeHtml(conv.title)}</span>
|
|
<div class="conversation-actions">
|
|
<button class="conversation-action-btn pin-btn" onclick="event.stopPropagation(); togglePin(${conv.id})" title="${isPinned ? 'Odepnij' : 'Przypnij'}">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="${isPinned ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="${isPinned ? '0' : '2'}">
|
|
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="conversation-action-btn rename-btn" onclick="event.stopPropagation(); startRename(${conv.id})" title="Zmień nazwę">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="14" height="14">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="conversation-action-btn delete-btn" onclick="event.stopPropagation(); deleteConversation(${conv.id})" title="Usuń">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="14" height="14">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Toggle pin/unpin conversation
|
|
async function togglePin(conversationId) {
|
|
try {
|
|
const response = await fetch(`/api/chat/${conversationId}/pin`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Update local state
|
|
const conv = conversations.find(c => c.id === conversationId);
|
|
if (conv) conv.is_pinned = data.is_pinned;
|
|
// Re-sort: pinned first, then by updated_at
|
|
conversations.sort((a, b) => {
|
|
if (a.is_pinned !== b.is_pinned) return b.is_pinned ? 1 : -1;
|
|
return new Date(b.updated_at) - new Date(a.updated_at);
|
|
});
|
|
renderConversationsList();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling pin:', error);
|
|
}
|
|
}
|
|
|
|
// Start inline rename
|
|
function startRename(conversationId) {
|
|
const item = document.querySelector(`.conversation-item[data-id="${conversationId}"]`);
|
|
if (!item) return;
|
|
|
|
const titleSpan = item.querySelector('.conversation-title');
|
|
const currentTitle = titleSpan.textContent;
|
|
|
|
// Replace title with input
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.className = 'conversation-rename-input';
|
|
input.value = currentTitle;
|
|
input.maxLength = 255;
|
|
|
|
titleSpan.replaceWith(input);
|
|
input.focus();
|
|
input.select();
|
|
|
|
// Prevent click from loading conversation
|
|
const originalOnclick = item.onclick;
|
|
item.onclick = null;
|
|
|
|
function finishRename(save) {
|
|
const newName = input.value.trim();
|
|
|
|
if (save && newName && newName !== currentTitle) {
|
|
saveRename(conversationId, newName);
|
|
}
|
|
|
|
// Restore title span
|
|
const span = document.createElement('span');
|
|
span.className = 'conversation-title';
|
|
span.textContent = save && newName ? newName : currentTitle;
|
|
input.replaceWith(span);
|
|
item.onclick = originalOnclick;
|
|
}
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') { e.preventDefault(); finishRename(true); }
|
|
if (e.key === 'Escape') { finishRename(false); }
|
|
});
|
|
|
|
input.addEventListener('blur', () => finishRename(true));
|
|
}
|
|
|
|
// Save renamed conversation
|
|
async function saveRename(conversationId, name) {
|
|
try {
|
|
const response = await fetch(`/api/chat/${conversationId}/rename`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
body: JSON.stringify({ name })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Update local state
|
|
const conv = conversations.find(c => c.id === conversationId);
|
|
if (conv) {
|
|
conv.custom_name = data.name;
|
|
conv.title = data.name;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error renaming conversation:', error);
|
|
}
|
|
}
|
|
|
|
// Load a specific conversation
|
|
async function loadConversation(conversationId) {
|
|
currentConversationId = conversationId;
|
|
|
|
// Update sidebar selection
|
|
document.querySelectorAll('.conversation-item').forEach(item => {
|
|
item.classList.toggle('active', parseInt(item.dataset.id) === conversationId);
|
|
});
|
|
|
|
// Hide empty state
|
|
const emptyState = document.getElementById('emptyState');
|
|
if (emptyState) emptyState.style.display = 'none';
|
|
|
|
// Clear messages and show loading
|
|
const messagesDiv = document.getElementById('chatMessages');
|
|
messagesDiv.innerHTML = '<div class="message assistant"><div class="message-avatar">AI</div><div class="message-content">Ładowanie historii...</div></div>';
|
|
|
|
try {
|
|
const response = await fetch(`/api/chat/${conversationId}/history`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
messagesDiv.innerHTML = '';
|
|
data.messages.forEach(msg => {
|
|
addMessage(msg.role, msg.content, false);
|
|
});
|
|
// Re-add typing indicator at the end
|
|
messagesDiv.innerHTML += `
|
|
<div class="message assistant" id="typingIndicator" style="display: none;">
|
|
<div class="message-avatar">AI</div>
|
|
<div class="typing-indicator active">
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
scrollToBottom();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading conversation:', error);
|
|
messagesDiv.innerHTML = '<div class="message assistant"><div class="message-avatar">AI</div><div class="message-content">Nie udało się załadować rozmowy.</div></div>';
|
|
}
|
|
|
|
// Close mobile sidebar
|
|
document.getElementById('chatSidebar').classList.remove('mobile-open');
|
|
}
|
|
|
|
// Start new conversation
|
|
function startNewConversation() {
|
|
currentConversationId = null;
|
|
|
|
// Clear active state in sidebar
|
|
document.querySelectorAll('.conversation-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
|
|
// Show empty state
|
|
const messagesDiv = document.getElementById('chatMessages');
|
|
messagesDiv.innerHTML = `
|
|
<div class="empty-state" id="emptyState">
|
|
<img src="{{ url_for('static', filename='img/nordagpt-icon.svg') }}" alt="NordaGPT" style="width: 80px; height: 80px; margin-bottom: 1rem;">
|
|
<h2>NordaGPT - Asystent AI Norda Biznes</h2>
|
|
<p>Mogę pomóc Ci znaleźć firmy, usługi, sprawdzić kalendarz wydarzeń, rekomendacje i wiele więcej.</p>
|
|
<div class="suggestions">
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Szukam partnera do projektu budowlanego')">Szukam partnera do projektu</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kto w Izbie zajmuje się marketingiem?')">Kto zajmuje się marketingiem?</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Poleć firmę z dobrymi opiniami Google')">Poleć firmę z opiniami</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Co nowego na forum?')">Co nowego na forum?</button>
|
|
</div>
|
|
</div>
|
|
<div class="message assistant" id="typingIndicator" style="display: none;">
|
|
<div class="message-avatar">AI</div>
|
|
<div class="typing-indicator active">
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('messageInput').focus();
|
|
|
|
// Close mobile sidebar
|
|
document.getElementById('chatSidebar').classList.remove('mobile-open');
|
|
}
|
|
|
|
// Delete conversation
|
|
async function deleteConversation(conversationId) {
|
|
if (!confirm('Czy na pewno chcesz usunąć tę rozmowę?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/chat/${conversationId}/delete`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRFToken': csrfToken }
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// If deleted current conversation, start new
|
|
if (currentConversationId === conversationId) {
|
|
startNewConversation();
|
|
}
|
|
// Reload conversations list
|
|
loadConversations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting conversation:', error);
|
|
alert('Nie udało się usunąć rozmowy');
|
|
}
|
|
}
|
|
|
|
// Send message
|
|
async function sendMessage() {
|
|
const input = document.getElementById('messageInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const message = input.value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
// Disable input
|
|
input.value = '';
|
|
input.disabled = true;
|
|
sendBtn.disabled = true;
|
|
|
|
// Hide empty state
|
|
const emptyState = document.getElementById('emptyState');
|
|
if (emptyState) emptyState.style.display = 'none';
|
|
|
|
// Add user message
|
|
addMessage('user', message);
|
|
|
|
// Show typing indicator
|
|
document.getElementById('typingIndicator').style.display = 'flex';
|
|
scrollToBottom();
|
|
|
|
try {
|
|
// Start new conversation if needed
|
|
if (!currentConversationId) {
|
|
const startResponse = await fetch('/api/chat/start', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
body: JSON.stringify({
|
|
title: message.substring(0, 50) + (message.length > 50 ? '...' : '')
|
|
})
|
|
});
|
|
const startData = await startResponse.json();
|
|
if (startData.success) {
|
|
currentConversationId = startData.conversation_id;
|
|
} else {
|
|
throw new Error(startData.error || 'Failed to start conversation');
|
|
}
|
|
}
|
|
|
|
// Send message with model selection
|
|
const response = await fetch(`/api/chat/${currentConversationId}/message`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
body: JSON.stringify({
|
|
message: message,
|
|
model: currentModel
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
|
|
// Hide typing indicator
|
|
document.getElementById('typingIndicator').style.display = 'none';
|
|
|
|
if (data.success) {
|
|
addMessage('assistant', data.message, true, data.tech_info);
|
|
loadConversations();
|
|
// Update cost if available
|
|
if (data.tech_info && data.tech_info.cost_usd) {
|
|
updateMonthlyCost(data.tech_info.cost_usd);
|
|
}
|
|
} else if (data.limit_exceeded) {
|
|
// Show limit banner and usage info
|
|
if (data.usage) updateUsageBars(data.usage);
|
|
showLimitBanner(data.usage || {daily_percent: 100});
|
|
addMessage('assistant', data.error);
|
|
} else {
|
|
addMessage('assistant', 'Przepraszam, wystąpił błąd: ' + (data.error || 'Nieznany błąd'));
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error sending message:', error);
|
|
document.getElementById('typingIndicator').style.display = 'none';
|
|
addMessage('assistant', 'Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.');
|
|
}
|
|
|
|
// Re-enable input
|
|
input.disabled = false;
|
|
sendBtn.disabled = false;
|
|
input.focus();
|
|
}
|
|
|
|
// Send suggestion
|
|
function sendSuggestion(text) {
|
|
document.getElementById('messageInput').value = text;
|
|
sendMessage();
|
|
}
|
|
|
|
// Add message to chat
|
|
function addMessage(role, content, animate = true, techInfo = null) {
|
|
const messagesDiv = document.getElementById('chatMessages');
|
|
const typingIndicator = document.getElementById('typingIndicator');
|
|
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = 'message ' + role;
|
|
if (!animate) messageDiv.style.animation = 'none';
|
|
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'message-avatar';
|
|
avatar.textContent = role === 'assistant' ? 'AI' : '{{ current_user.name[:1].upper() if current_user else "U" }}';
|
|
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.className = 'message-content';
|
|
contentDiv.innerHTML = formatMessage(content);
|
|
|
|
// Add response info badge for AI responses (model, time, cost)
|
|
if (role === 'assistant' && techInfo) {
|
|
const infoBadge = document.createElement('div');
|
|
infoBadge.className = 'thinking-info-badge';
|
|
|
|
const modelName = techInfo.model || 'flash';
|
|
const latencyMs = parseInt(techInfo.latency_ms) || 0;
|
|
const latencySec = (latencyMs / 1000).toFixed(1);
|
|
const costUsd = parseFloat(techInfo.cost_usd) || 0;
|
|
|
|
// Model labels
|
|
const modelLabels = {
|
|
'flash': '⚡ Flash',
|
|
'pro': '🧠 Pro',
|
|
'gemini-3-flash-preview': '⚡ Flash',
|
|
'gemini-2.5-flash-lite': '⚡ Flash (fallback)',
|
|
'gemini-2.5-flash': '⚡ Flash (fallback)',
|
|
'gemini-3-pro-preview': '🧠 Pro'
|
|
};
|
|
const modelLabel = modelLabels[modelName] || modelName;
|
|
|
|
// Format cost
|
|
const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : '$0.00';
|
|
|
|
// Build badge content
|
|
let badgeHTML = `<span class="thinking-badge-level">${modelLabel}</span> · <span class="thinking-badge-time">${latencySec}s</span> · <span class="thinking-badge-cost">${costStr}</span>`;
|
|
|
|
// Add "Try Pro" hint when using Flash model
|
|
if (currentModel === 'flash' && (modelName === 'flash' || modelName === 'gemini-3-flash-preview')) {
|
|
badgeHTML += ` · <a href="#" class="pro-upgrade-hint" onclick="event.preventDefault(); setModel('pro');" title="Przełącz na Gemini 3 Pro dla lepszych odpowiedzi">Lepsze odpowiedzi? <strong>Spróbuj Pro</strong> 🧠</a>`;
|
|
}
|
|
|
|
infoBadge.innerHTML = badgeHTML;
|
|
contentDiv.appendChild(infoBadge);
|
|
|
|
// Update monthly cost if cost provided
|
|
if (costUsd > 0) {
|
|
updateMonthlyCost(costUsd);
|
|
}
|
|
}
|
|
|
|
messageDiv.appendChild(avatar);
|
|
messageDiv.appendChild(contentDiv);
|
|
|
|
// Insert before typing indicator
|
|
if (typingIndicator) {
|
|
messagesDiv.insertBefore(messageDiv, typingIndicator);
|
|
} else {
|
|
messagesDiv.appendChild(messageDiv);
|
|
}
|
|
|
|
scrollToBottom();
|
|
}
|
|
|
|
// Format message content (links, lists, etc.)
|
|
function formatMessage(text) {
|
|
if (!text) return '';
|
|
|
|
// Escape HTML first
|
|
text = escapeHtml(text);
|
|
|
|
// Convert markdown links [text](url) to <a> tags with appropriate class
|
|
// Links: company (orange), person (green), forum (purple), news (green), b2b (yellow), external (blue)
|
|
|
|
// First: Handle internal links starting with /
|
|
text = text.replace(/\[([^\]]+)\]\((\/[^)]+)\)/g, function(match, linkText, url) {
|
|
let linkClass = 'external-link';
|
|
if (url.startsWith('/company/')) {
|
|
linkClass = 'company-link';
|
|
} else if (url.startsWith('/forum/')) {
|
|
linkClass = 'forum-link';
|
|
} else if (url.startsWith('/news/') || url.startsWith('/aktualnosci/')) {
|
|
linkClass = 'news-link';
|
|
} else if (url.startsWith('/b2b/') || url.startsWith('/ogloszenia/')) {
|
|
linkClass = 'b2b-link';
|
|
} else if (url.startsWith('/osoba/')) {
|
|
linkClass = 'person-link';
|
|
}
|
|
return '<a href="' + url + '" class="' + linkClass + '">' + linkText + '</a>';
|
|
});
|
|
|
|
// Then: Handle full URLs (https://)
|
|
text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, function(match, linkText, url) {
|
|
let linkClass = 'external-link';
|
|
if (url.includes('nordabiznes.pl/company/')) {
|
|
linkClass = 'company-link';
|
|
} else if (url.includes('nordabiznes.pl/osoba/')) {
|
|
linkClass = 'person-link';
|
|
} else if (url.includes('nordabiznes.pl/forum/')) {
|
|
linkClass = 'forum-link';
|
|
} else if (url.includes('nordabiznes.pl/news/') || url.includes('nordabiznes.pl/aktualnosci/')) {
|
|
linkClass = 'news-link';
|
|
} else if (url.includes('nordabiznes.pl/b2b/') || url.includes('nordabiznes.pl/ogloszenia/')) {
|
|
linkClass = 'b2b-link';
|
|
}
|
|
return '<a href="' + url + '" target="_blank" rel="noopener" class="' + linkClass + '">' + linkText + '</a>';
|
|
});
|
|
|
|
// Convert raw URLs to links (only those not already in <a> tags)
|
|
text = text.replace(/(?<!href="|">)(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/g, function(match, url) {
|
|
let linkClass = 'external-link';
|
|
if (url.includes('nordabiznes.pl/company/')) {
|
|
linkClass = 'company-link';
|
|
} else if (url.includes('nordabiznes.pl/osoba/')) {
|
|
linkClass = 'person-link';
|
|
}
|
|
return '<a href="' + url + '" target="_blank" rel="noopener" class="' + linkClass + '">' + url + '</a>';
|
|
});
|
|
|
|
// Convert **bold** to <strong>
|
|
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
|
|
// Convert newlines to <br>
|
|
text = text.replace(/\n/g, '<br>');
|
|
|
|
return text;
|
|
}
|
|
|
|
// Escape HTML
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Scroll to bottom
|
|
function scrollToBottom() {
|
|
const messagesDiv = document.getElementById('chatMessages');
|
|
// Prosty i niezawodny scroll do dołu kontenera wiadomości
|
|
requestAnimationFrame(() => {
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
});
|
|
}
|
|
|
|
// Auto-resize textarea
|
|
function autoResizeTextarea() {
|
|
const textarea = document.getElementById('messageInput');
|
|
textarea.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
});
|
|
}
|
|
|
|
// Toggle sidebar (collapse/expand on desktop, show/hide on mobile)
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('chatSidebar');
|
|
const icon = document.getElementById('sidebarToggleIcon');
|
|
|
|
if (window.innerWidth <= 768) {
|
|
sidebar.classList.toggle('mobile-open');
|
|
return;
|
|
}
|
|
|
|
sidebar.classList.toggle('collapsed');
|
|
const isCollapsed = sidebar.classList.contains('collapsed');
|
|
|
|
// Flip arrow direction
|
|
icon.innerHTML = isCollapsed
|
|
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>'
|
|
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"/>';
|
|
|
|
// Remember state
|
|
localStorage.setItem('chatSidebarCollapsed', isCollapsed ? '1' : '0');
|
|
}
|
|
|
|
// Restore sidebar state on load
|
|
(function restoreSidebarState() {
|
|
if (localStorage.getItem('chatSidebarCollapsed') === '1' && window.innerWidth > 768) {
|
|
const sidebar = document.getElementById('chatSidebar');
|
|
const icon = document.getElementById('sidebarToggleIcon');
|
|
sidebar.classList.add('collapsed');
|
|
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>';
|
|
}
|
|
|
|
// Restore saved width
|
|
const savedWidth = localStorage.getItem('chatSidebarWidth');
|
|
if (savedWidth && window.innerWidth > 768) {
|
|
document.getElementById('chatSidebar').style.width = savedWidth + 'px';
|
|
}
|
|
})();
|
|
|
|
// Sidebar resize functionality
|
|
(function initSidebarResize() {
|
|
const handle = document.getElementById('sidebarResizeHandle');
|
|
const sidebar = document.getElementById('chatSidebar');
|
|
let isResizing = false;
|
|
|
|
handle.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
sidebar.classList.add('resizing');
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
const newWidth = Math.max(200, Math.min(500, e.clientX));
|
|
sidebar.style.width = newWidth + 'px';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (!isResizing) return;
|
|
isResizing = false;
|
|
sidebar.classList.remove('resizing');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
localStorage.setItem('chatSidebarWidth', parseInt(sidebar.style.width));
|
|
});
|
|
})();
|
|
{% endblock %}
|