- Added regex to convert [text](url) markdown links to <a> tags - Updated raw URL regex with lookbehind/lookahead to avoid duplicate links - Links now display as clickable text instead of raw markdown syntax Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1275 lines
39 KiB
HTML
Executable File
1275 lines
39 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}NordaGPT - Norda Biznes Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Reset main padding for full-height chat */
|
|
main {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.chat-layout {
|
|
display: flex;
|
|
height: calc(100vh - 73px);
|
|
background: var(--background);
|
|
}
|
|
|
|
/* ============================================
|
|
SIDEBAR - Historia konwersacji
|
|
============================================ */
|
|
.chat-sidebar {
|
|
width: 280px;
|
|
background: #1e1e2e;
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid #2d2d3d;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid #2d2d3d;
|
|
}
|
|
|
|
.new-chat-btn {
|
|
width: 100%;
|
|
padding: var(--spacing-md);
|
|
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 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: #888;
|
|
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: #2d2d3d;
|
|
}
|
|
|
|
.conversation-item.active {
|
|
background: #3d3d4d;
|
|
}
|
|
|
|
.conversation-item svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
opacity: 0.7;
|
|
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: #888;
|
|
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: #666;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.sidebar-loading {
|
|
padding: var(--spacing-lg);
|
|
text-align: center;
|
|
color: #666;
|
|
}
|
|
|
|
/* ============================================
|
|
MAIN CHAT AREA - Styl NordaGPT
|
|
============================================ */
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
|
|
color: white;
|
|
}
|
|
|
|
.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;
|
|
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, #7c3aed 0%, #5b21b6 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;
|
|
}
|
|
|
|
.message-content a {
|
|
color: inherit;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.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: #7c3aed;
|
|
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: #7c3aed;
|
|
color: #7c3aed;
|
|
background: #f5f3ff;
|
|
}
|
|
|
|
/* Input area */
|
|
.chat-input-area {
|
|
padding: var(--spacing-lg);
|
|
background: white;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.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: #7c3aed;
|
|
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, #7c3aed 0%, #5b21b6 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: 73px;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 100;
|
|
width: 280px;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* ============================================
|
|
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, #7c3aed 0%, #5b21b6 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: #7c3aed;
|
|
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, #7c3aed 0%, #5b21b6 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);
|
|
}
|
|
</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>
|
|
<span style="font-size: 1.5rem;">🤖</span>
|
|
<h1>NordaGPT</h1>
|
|
<span class="chat-header-badge">Gemini 2.5</span>
|
|
<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>
|
|
</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>Aktualny model AI</h3>
|
|
<div class="model-badge-large">
|
|
<span class="model-name">Gemini 2.5 Flash-Lite</span>
|
|
<span class="model-provider">Google AI</span>
|
|
</div>
|
|
<p class="model-description">
|
|
Najnowszy model Google zoptymalizowany pod kątem szybkości i przepustowości.
|
|
Oferuje do 65 000 tokenów w odpowiedzi (8x więcej niż poprzedni model).
|
|
</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> <span class="spec-change">↑ było 8 192</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Limit dzienny (RPD):</td>
|
|
<td><strong>1 000 zapytań/dzień</strong> <span class="spec-change">↑ było 250</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Limit minutowy (RPM):</td>
|
|
<td><strong>15 zapytań/minutę</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Tokeny/minutę (TPM):</td>
|
|
<td><strong>250 000 tokenów/min</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Thinking mode:</td>
|
|
<td><strong>Pełny</strong> <span class="spec-change">↑ był eksperymentalny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Koszt:</td>
|
|
<td><strong>Bezpłatny (Free Tier)</strong></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="model-history">
|
|
<h3>📜 Historia rozwoju NordaGPT</h3>
|
|
<div class="timeline">
|
|
<div class="timeline-item current">
|
|
<div class="timeline-date">14.01.2026</div>
|
|
<div class="timeline-content">
|
|
<strong>Gemini 2.5 Flash-Lite</strong>
|
|
<p>Upgrade do najnowszego modelu. 8x dłuższe odpowiedzi, pełny thinking mode, 4x większy limit dzienny.</p>
|
|
<span class="timeline-badge upgrade">Aktualna 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>Dodano 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, limit 8192 tokenów na odpowiedź.</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ą 80 firm.</p>
|
|
<span class="timeline-badge launch">Premiera</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="model-benefits">
|
|
<h3>✨ Co zyskaliśmy przy ostatniej aktualizacji?</h3>
|
|
<ul>
|
|
<li><strong>8x dłuższe odpowiedzi</strong> — z 8 192 → 65 536 tokenów (szczegółowe analizy)</li>
|
|
<li><strong>4x większy limit dzienny</strong> — z 250 → 1 000 zapytań/dzień</li>
|
|
<li><strong>Pełny thinking mode</strong> — zamiast eksperymentalnego (lepsze rozumowanie)</li>
|
|
<li><strong>Szybsza odpowiedź</strong> — Flash-Lite zoptymalizowany pod przepustowość</li>
|
|
<li><strong>Nadal bezpłatny</strong> — pełny Free Tier Google bez ukrytych kosztów</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-messages" id="chatMessages">
|
|
<!-- Empty state - pokazywany gdy brak wiadomości -->
|
|
<div class="empty-state" id="emptyState">
|
|
<div class="empty-state-icon">🤖</div>
|
|
<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('Kto jest prezesem PIXLAB?')">
|
|
Kto jest prezesem PIXLAB?
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Która firma ma najlepsze Google opinie?')">
|
|
Najlepsze Google opinie?
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kiedy następne spotkanie Norda Biznes?')">
|
|
Kiedy następne spotkanie?
|
|
</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kto oferuje usługi IT?')">
|
|
Kto oferuje usługi IT?
|
|
</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
|
|
let currentConversationId = null;
|
|
let conversations = [];
|
|
|
|
// ============================================
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadConversations();
|
|
autoResizeTextarea();
|
|
});
|
|
|
|
// 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">
|
|
<div class="empty-state-icon">🤖</div>
|
|
<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('Kto jest prezesem PIXLAB?')">Kto jest prezesem PIXLAB?</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Która firma ma najlepsze Google opinie?')">Najlepsze Google opinie?</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kiedy następne spotkanie Norda Biznes?')">Kiedy następne spotkanie?</button>
|
|
<button class="suggestion-chip" onclick="sendSuggestion('Kto oferuje usługi IT?')">Kto oferuje usługi IT?</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'
|
|
});
|
|
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' },
|
|
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
|
|
const response = await fetch(`/api/chat/${currentConversationId}/message`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message: message })
|
|
});
|
|
const data = await response.json();
|
|
|
|
// Hide typing indicator
|
|
document.getElementById('typingIndicator').style.display = 'none';
|
|
|
|
if (data.success) {
|
|
addMessage('assistant', data.message);
|
|
// 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) {
|
|
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);
|
|
|
|
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 (must be before raw URL conversion)
|
|
text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
|
|
// Convert raw URLs to links (only those not already in <a> tags)
|
|
text = text.replace(/(?<!href="|">)(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/g, '<a href="$1" target="_blank" rel="noopener">$1</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');
|
|
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 %}
|