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
- Always use GOOGLE_GEMINI_API_KEY (paid tier) instead of free tier - Default chat model: Gemini 3 Flash (thinking mode, 10K RPD) - Premium option: Gemini 3 Pro (250 RPD, best reasoning) - Removed Flash Lite (2.5) from chat UI — only Gemini 3 generation - Updated fallback chain for paid tier limits - Updated pricing: 3-flash $0.50/$3.00 (was $0.00 on free tier) - Added "Spróbuj Pro" upgrade hint after Flash responses - Updated model info modal with paid tier pricing/limits Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2115 lines
68 KiB
HTML
Executable File
2115 lines
68 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;
|
|
background: #f5f7fa;
|
|
color: #374151;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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: center;
|
|
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-title {
|
|
flex: 1;
|
|
font-size: var(--font-size-sm);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.conversation-delete {
|
|
opacity: 0;
|
|
background: none;
|
|
border: none;
|
|
color: #9ca3af;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: var(--radius-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.conversation-item:hover .conversation-delete {
|
|
opacity: 1;
|
|
}
|
|
|
|
.conversation-delete:hover {
|
|
color: #ef4444;
|
|
background: rgba(239, 68, 68, 0.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 */
|
|
.monthly-cost-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: rgba(255,255,255,0.15);
|
|
padding: 6px 12px;
|
|
border-radius: var(--radius-md);
|
|
font-size: 12px;
|
|
margin-left: var(--spacing-sm);
|
|
}
|
|
|
|
.monthly-cost-badge .cost-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.monthly-cost-badge .cost-label {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.monthly-cost-badge .cost-value {
|
|
font-weight: 600;
|
|
color: #fef08a;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.monthly-cost-badge .cost-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">
|
|
<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>Model AI</strong>
|
|
<p>Wybierz model dopasowany do pytania</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">Domyślny</span>
|
|
</div>
|
|
<p class="thinking-option-desc">Thinking mode — szybki i inteligentny. Dla pytań o firmy, kontakty, strategie.</p>
|
|
<span class="thinking-option-price">~$0.04/pytanie · 10 000 zapytań/dzień</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">Najlepsza analiza i rozumowanie. Dla złożonych raportów i rekomendacji.</p>
|
|
<span class="thinking-option-price">~$0.20/pytanie · limit: $2/dzień</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Monthly cost display -->
|
|
<div class="monthly-cost-badge" title="Twoje koszty AI w tym miesiącu">
|
|
<span class="cost-icon">💰</span>
|
|
<span class="cost-label">Ten miesiąc:</span>
|
|
<span class="cost-value" id="monthlyCostDisplay">$0.00</span>
|
|
</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>
|
|
|
|
<!-- 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 - Informacje techniczne</h2>
|
|
|
|
<div class="model-current">
|
|
<h3>Dostępne modele AI</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;">DOMYŚLNY</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>
|
|
<p class="model-description">
|
|
<strong>Flash</strong> — Gemini 3 Flash z thinking mode, 10 000 zapytań/dzień.
|
|
<strong>Pro</strong> — Gemini 3 Pro, najlepsza analiza dla złożonych pytań.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>📊 Specyfikacja techniczna</h3>
|
|
<table class="specs-table">
|
|
<tr>
|
|
<td>Kontekst:</td>
|
|
<td><strong>1 000 000 tokenów</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Max. odpowiedź:</td>
|
|
<td><strong>65 536 tokenów</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Reasoning:</td>
|
|
<td><strong>7x lepszy</strong> <span class="spec-change">↑ vs 2.5</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Thinking mode:</td>
|
|
<td><strong>Flash: wysoki</strong> / <strong>Pro: zaawansowany</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Kodowanie (SWE-bench):</td>
|
|
<td><strong>78%</strong> <span class="spec-change">↑ najlepszy w klasie</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Nauka (GPQA Diamond):</td>
|
|
<td><strong>90.4%</strong></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="model-specs">
|
|
<h3>💰 Koszty i limity</h3>
|
|
<table class="specs-table">
|
|
<tr>
|
|
<td>⚡ Flash:</td>
|
|
<td><strong>~$0.02-0.05</strong> za odpowiedź (Paid Tier 1)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>🧠 Pro:</td>
|
|
<td><strong>~$0.10-0.30</strong> za odpowiedź (Paid Tier 1)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Limit dzienny Pro:</td>
|
|
<td><strong>$2.00</strong> / dzień</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Limit miesięczny Pro:</td>
|
|
<td><strong>$20.00</strong> / miesiąc</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Limit Flash:</td>
|
|
<td><strong>10 000</strong> zapytań/dzień</td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-size: 0.85em; color: #666; margin-top: 0.5rem;">
|
|
Koszt każdej odpowiedzi widoczny w badge pod wiadomością. Miesięczne zużycie w nagłówku chatu.
|
|
</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;
|
|
const costDisplay = document.getElementById('monthlyCostDisplay');
|
|
if (costDisplay) {
|
|
costDisplay.textContent = '$' + monthlyUsageCost.toFixed(2);
|
|
}
|
|
}
|
|
|
|
// 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 monthly cost 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 (Gemini 3 Flash, thinking mode) - 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 monthly cost
|
|
if (data.monthly_cost !== undefined) {
|
|
monthlyUsageCost = data.monthly_cost;
|
|
document.getElementById('monthlyCostDisplay').textContent = '$' + monthlyUsageCost.toFixed(2);
|
|
}
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
list.innerHTML = conversations.map(conv => `
|
|
<div class="conversation-item ${conv.id === currentConversationId ? 'active' : ''}"
|
|
onclick="loadConversation(${conv.id})"
|
|
data-id="${conv.id}">
|
|
<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>
|
|
<button class="conversation-delete" 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>
|
|
`).join('');
|
|
}
|
|
|
|
// 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);
|
|
// Reload conversations to update list
|
|
loadConversations();
|
|
} 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 mobile sidebar
|
|
function toggleSidebar() {
|
|
document.getElementById('chatSidebar').classList.toggle('mobile-open');
|
|
}
|
|
{% endblock %}
|