claude-web-chat/templates/chat.html
djuka 3122c5cba9
Some checks failed
Tests / unit-tests (push) Failing after 22s
Pojednostavljen chat na jedan terminal, dodata notifikacija kad Claude završi
Uklonjen multi-tab sistem — sada jedna PTY sesija po stranici.
Dodat idle detection: status "Završeno", flash animacija, browser
notifikacija i treptanje naslova kad je tab u pozadini.
CSS premešten iz inline stilova u style.css.
Dodat /api/projects endpoint i testovi za PTY sesije.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:20:55 +00:00

562 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude — {{.Project}}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--border: #2a2a4a;
--text-primary: #e0e0e0;
--text-secondary: #a0a0b0;
--text-muted: #6c6c80;
--accent: #e94560;
--accent-hover: #ff6b81;
--error: #f44336;
}
body {
background: var(--bg-primary);
overflow: hidden;
height: 100vh;
display: flex;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--text-primary);
}
/* Sidebar */
.sidebar {
width: 240px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar.collapsed { width: 0; overflow: hidden; border: none; }
.sidebar-header {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
}
.sidebar-header h3 { color: var(--text-secondary); font-size: 0.8rem; }
.sidebar-section {
padding: 0.4rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.sidebar-section-title {
font-size: 0.65rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.3rem;
letter-spacing: 0.5px;
}
.sidebar-files {
flex: 1;
overflow-y: auto;
padding: 0.3rem 0;
}
.file-item {
display: block;
padding: 0.3rem 0.75rem;
font-size: 0.75rem;
color: var(--text-secondary);
text-decoration: none;
cursor: pointer;
transition: background 0.1s;
}
.file-item:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); }
.sidebar-footer {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--border);
font-size: 0.7rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.sidebar-footer a {
color: var(--text-secondary);
text-decoration: none;
display: block;
padding: 0.35rem 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.15s ease;
}
.sidebar-footer a:hover {
color: var(--text-primary);
border-color: var(--accent);
background: rgba(233,69,96,0.1);
}
/* Main area */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.35rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
flex-shrink: 0;
}
.terminal-header .left { display: flex; align-items: center; gap: 0.75rem; }
.terminal-header .title { color: var(--accent); font-weight: 600; }
.terminal-header .status { font-size: 0.7rem; color: var(--text-muted); }
.terminal-header .status.connected { color: #4caf50; }
.terminal-header .status.done { color: #ff9800; }
.done-flash { animation: flash-border 0.6s ease-out; }
@keyframes flash-border { 0% { border-color: #ff9800; } 100% { border-color: var(--border); } }
.terminal-header .controls { display: flex; align-items: center; gap: 0.6rem; }
.terminal-header select {
background: var(--bg-primary);
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.15rem 0.3rem;
font-size: 0.7rem;
font-family: inherit;
cursor: pointer;
outline: none;
}
.terminal-header select:hover { border-color: var(--accent); }
.terminal-header button {
background: none;
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 3px;
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
font-family: inherit;
cursor: pointer;
}
.terminal-header button:hover { border-color: var(--accent); color: var(--text-primary); }
/* Terminal container */
#terminal-container { flex: 1; overflow: hidden; }
.xterm { height: 100%; }
/* File viewer overlay */
.file-viewer {
position: fixed;
top: 0; right: 0;
width: 50%; height: 100vh;
background: var(--bg-primary);
border-left: 1px solid var(--border);
z-index: 200;
display: flex;
flex-direction: column;
box-shadow: -4px 0 16px rgba(0,0,0,0.5);
}
.file-viewer.hidden { display: none; }
.file-viewer-header {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
}
.file-viewer-header h3 { color: var(--accent); font-size: 0.85rem; }
.file-viewer-header button {
background: var(--accent);
color: #fff;
border: none;
border-radius: 4px;
padding: 0.3rem 0.7rem;
font-size: 0.75rem;
cursor: pointer;
font-family: inherit;
}
.file-viewer-header button:hover { background: var(--accent-hover); }
.file-viewer-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
font-size: 0.85rem;
line-height: 1.7;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--text-primary);
}
.file-viewer-content h1, .file-viewer-content h2, .file-viewer-content h3 { color: var(--accent); margin: 0.8rem 0 0.4rem; }
.file-viewer-content h1 { font-size: 1.3em; }
.file-viewer-content h2 { font-size: 1.1em; }
.file-viewer-content h3 { font-size: 1em; }
.file-viewer-content p { margin: 0.4rem 0; }
.file-viewer-content pre { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; padding: 0.7rem; overflow-x: auto; margin: 0.5rem 0; }
.file-viewer-content code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.85em; }
.file-viewer-content p code { background: var(--bg-secondary); padding: 0.1rem 0.3rem; border-radius: 3px; }
.file-viewer-content ul, .file-viewer-content ol { padding-left: 1.5rem; margin: 0.3rem 0; }
.file-viewer-content strong { color: #fff; }
.file-viewer-content table { border-collapse: collapse; margin: 0.5rem 0; font-size: 0.8rem; }
.file-viewer-content th, .file-viewer-content td { border: 1px solid var(--border); padding: 0.3rem 0.6rem; text-align: left; }
.file-viewer-content th { background: var(--bg-secondary); color: var(--accent); font-weight: 600; }
.file-viewer-content tr:nth-child(even) { background: rgba(22,27,34,0.5); }
/* Sidebar project list */
.sidebar-projects-list { max-height: 180px; overflow-y: auto; }
.sidebar-projects-list .file-item.active {
color: var(--accent);
background: rgba(233,69,96,0.1);
font-weight: 600;
}
/* Scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h3>{{.Project}}</h3>
<button onclick="toggleSidebar()" title="Sakrij sidebar">&#x2190;</button>
</div>
<!-- Projekti -->
<div class="sidebar-section">
<div class="sidebar-section-title">Projekti</div>
</div>
<div class="sidebar-files sidebar-projects-list">
{{range .Projects}}
<a class="file-item{{if eq .Name $.Project}} active{{end}}" href="/chat/{{.Name}}">{{.Name}}</a>
{{end}}
</div>
{{if .Files}}
<div class="sidebar-section">
<div class="sidebar-section-title">Dokumentacija</div>
</div>
<div class="sidebar-files">
{{range .Files}}
<a class="file-item" href="#" onclick="loadFile('{{.RelPath}}'); return false;">{{.Name}}</a>
{{end}}
</div>
{{end}}
<div class="sidebar-footer">
<a href="/change-password">Promeni lozinku</a>
<a href="/logout">Odjavi se</a>
</div>
</div>
<!-- Main terminal area -->
<div class="main">
<div class="terminal-header">
<div class="left">
<button id="sidebar-toggle" onclick="toggleSidebar()" title="Prikaži/sakrij sidebar">&#9776;</button>
<span class="title">claude</span>
<span id="ws-status" class="status">Povezivanje...</span>
</div>
<div class="controls">
<select id="theme-select" onchange="applyTheme(this.value)" title="Tema">
<option value="dark">Dark</option>
<option value="dracula">Dracula</option>
<option value="monokai">Monokai</option>
<option value="nord">Nord</option>
<option value="solarized">Solarized Dark</option>
<option value="gruvbox">Gruvbox</option>
<option value="tokyo-night">Tokyo Night</option>
<option value="catppuccin">Catppuccin</option>
</select>
</div>
</div>
<div id="terminal-container"></div>
</div>
<!-- File viewer overlay -->
<div id="file-viewer" class="file-viewer hidden">
<div class="file-viewer-header">
<h3 id="file-viewer-title"></h3>
<button onclick="closeFileViewer()">Zatvori (Esc)</button>
</div>
<div class="file-viewer-content" id="file-viewer-content"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
<script>
// ── Config from server ──────────────────────────────
var PAGE_PROJECT = '{{.Project}}';
var PAGE_PROJECT_DIR = '{{.ProjectDir}}';
// ── Themes ──────────────────────────────────────────
var THEMES = {
dark: {
background: '#0d1117', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#0d1117',
selectionBackground: 'rgba(233,69,96,0.3)',
black: '#0d1117', red: '#f44336', green: '#4caf50', yellow: '#ff9800',
blue: '#2196f3', magenta: '#e94560', cyan: '#00bcd4', white: '#e0e0e0',
brightBlack: '#6c6c80', brightRed: '#ff6b81', brightGreen: '#66bb6a', brightYellow: '#ffb74d',
brightBlue: '#64b5f6', brightMagenta: '#ff6b81', brightCyan: '#4dd0e1', brightWhite: '#ffffff'
},
dracula: {
background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2', cursorAccent: '#282a36',
selectionBackground: 'rgba(68,71,90,0.5)',
black: '#21222c', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2',
brightBlack: '#6272a4', brightRed: '#ff6e6e', brightGreen: '#69ff94', brightYellow: '#ffffa5',
brightBlue: '#d6acff', brightMagenta: '#ff92df', brightCyan: '#a4ffff', brightWhite: '#ffffff'
},
monokai: {
background: '#272822', foreground: '#f8f8f2', cursor: '#f8f8f0', cursorAccent: '#272822',
selectionBackground: 'rgba(73,72,62,0.5)',
black: '#272822', red: '#f92672', green: '#a6e22e', yellow: '#f4bf75',
blue: '#66d9ef', magenta: '#ae81ff', cyan: '#a1efe4', white: '#f8f8f2',
brightBlack: '#75715e', brightRed: '#f92672', brightGreen: '#a6e22e', brightYellow: '#f4bf75',
brightBlue: '#66d9ef', brightMagenta: '#ae81ff', brightCyan: '#a1efe4', brightWhite: '#f9f8f5'
},
nord: {
background: '#2e3440', foreground: '#d8dee9', cursor: '#d8dee9', cursorAccent: '#2e3440',
selectionBackground: 'rgba(67,76,94,0.5)',
black: '#3b4252', red: '#bf616a', green: '#a3be8c', yellow: '#ebcb8b',
blue: '#81a1c1', magenta: '#b48ead', cyan: '#88c0d0', white: '#e5e9f0',
brightBlack: '#4c566a', brightRed: '#bf616a', brightGreen: '#a3be8c', brightYellow: '#ebcb8b',
brightBlue: '#81a1c1', brightMagenta: '#b48ead', brightCyan: '#8fbcbb', brightWhite: '#eceff4'
},
solarized: {
background: '#002b36', foreground: '#839496', cursor: '#839496', cursorAccent: '#002b36',
selectionBackground: 'rgba(7,54,66,0.5)',
black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
brightBlack: '#586e75', brightRed: '#cb4b16', brightGreen: '#586e75', brightYellow: '#657b83',
brightBlue: '#839496', brightMagenta: '#6c71c4', brightCyan: '#93a1a1', brightWhite: '#fdf6e3'
},
gruvbox: {
background: '#282828', foreground: '#ebdbb2', cursor: '#ebdbb2', cursorAccent: '#282828',
selectionBackground: 'rgba(60,56,54,0.5)',
black: '#282828', red: '#cc241d', green: '#98971a', yellow: '#d79921',
blue: '#458588', magenta: '#b16286', cyan: '#689d6a', white: '#a89984',
brightBlack: '#928374', brightRed: '#fb4934', brightGreen: '#b8bb26', brightYellow: '#fabd2f',
brightBlue: '#83a598', brightMagenta: '#d3869b', brightCyan: '#8ec07c', brightWhite: '#ebdbb2'
},
'tokyo-night': {
background: '#1a1b26', foreground: '#c0caf5', cursor: '#c0caf5', cursorAccent: '#1a1b26',
selectionBackground: 'rgba(40,52,87,0.5)',
black: '#15161e', red: '#f7768e', green: '#9ece6a', yellow: '#e0af68',
blue: '#7aa2f7', magenta: '#bb9af7', cyan: '#7dcfff', white: '#a9b1d6',
brightBlack: '#414868', brightRed: '#f7768e', brightGreen: '#9ece6a', brightYellow: '#e0af68',
brightBlue: '#7aa2f7', brightMagenta: '#bb9af7', brightCyan: '#7dcfff', brightWhite: '#c0caf5'
},
catppuccin: {
background: '#1e1e2e', foreground: '#cdd6f4', cursor: '#f5e0dc', cursorAccent: '#1e1e2e',
selectionBackground: 'rgba(88,91,112,0.3)',
black: '#45475a', red: '#f38ba8', green: '#a6e3a1', yellow: '#f9e2af',
blue: '#89b4fa', magenta: '#f5c2e7', cyan: '#94e2d5', white: '#bac2de',
brightBlack: '#585b70', brightRed: '#f38ba8', brightGreen: '#a6e3a1', brightYellow: '#f9e2af',
brightBlue: '#89b4fa', brightMagenta: '#f5c2e7', brightCyan: '#94e2d5', brightWhite: '#a6adc8'
}
};
// ── Theme ───────────────────────────────────────────
var savedTheme = localStorage.getItem('terminal-theme') || 'dark';
document.getElementById('theme-select').value = savedTheme;
function applyTheme(name) {
var theme = THEMES[name];
if (!theme) return;
savedTheme = name;
document.body.style.background = theme.background;
document.querySelector('.main').style.background = theme.background;
localStorage.setItem('terminal-theme', name);
term.options.theme = theme;
}
// ── Terminal ─────────────────────────────────────────
var statusEl = document.getElementById('ws-status');
var containerEl = document.getElementById('terminal-container');
var ws = null;
var reconnectTimer = null;
var originalTitle = document.title;
var idleTimer = null;
var isStreaming = false;
var titleBlinkTimer = null;
var theme = THEMES[savedTheme] || THEMES.dark;
var term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
theme: theme,
allowProposedApi: true,
scrollback: 10000,
convertEol: false,
drawBoldTextInBrightColors: true,
});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
term.open(containerEl);
term.onData(function(data) {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
// User typed — reset idle detection and status
isStreaming = false;
clearTimeout(idleTimer);
if (ws && ws.readyState === WebSocket.OPEN) {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
}
});
term.onResize(function(size) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
}
});
containerEl.addEventListener('click', function() { term.focus(); });
function connect() {
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/ws?project=' + encodeURIComponent(PAGE_PROJECT) +
'&project_dir=' + encodeURIComponent(PAGE_PROJECT_DIR);
ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.onopen = function() {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
term.focus();
};
ws.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data));
} else {
term.write(event.data);
}
// Idle detection — reset timer on each output
isStreaming = true;
clearTimeout(idleTimer);
idleTimer = setTimeout(onOutputIdle, 3000);
};
ws.onclose = function() {
statusEl.textContent = 'Nepovezan';
statusEl.className = 'status';
reconnectTimer = setTimeout(connect, 2000);
};
ws.onerror = function() {
statusEl.textContent = 'Greška';
statusEl.className = 'status';
};
}
// ── Idle detection & notifications ───────────────────
function onOutputIdle() {
if (!isStreaming) return;
isStreaming = false;
// Visual indicator — always
statusEl.textContent = 'Završeno';
statusEl.className = 'status done';
var header = document.querySelector('.terminal-header');
header.classList.remove('done-flash');
void header.offsetWidth; // force reflow
header.classList.add('done-flash');
// Background tab — browser notification + title blink
if (document.hidden) {
if (Notification.permission === 'granted') {
var n = new Notification('Claude — ' + PAGE_PROJECT, {
body: 'Odgovor je završen',
tag: 'claude-done'
});
n.onclick = function() { window.focus(); n.close(); };
}
startTitleBlink();
}
}
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Title blink when tab is in background
function startTitleBlink() {
if (titleBlinkTimer) return;
var on = true;
titleBlinkTimer = setInterval(function() {
document.title = on ? '✔ Završeno — ' + PAGE_PROJECT : originalTitle;
on = !on;
}, 1000);
}
function stopTitleBlink() {
if (titleBlinkTimer) {
clearInterval(titleBlinkTimer);
titleBlinkTimer = null;
document.title = originalTitle;
}
}
// Stop blinking when tab gets focus
document.addEventListener('visibilitychange', function() {
if (!document.hidden) stopTitleBlink();
});
window.addEventListener('focus', stopTitleBlink);
// ── Window resize ───────────────────────────────────
window.addEventListener('resize', function() { fitAddon.fit(); });
// ── Apply initial theme and connect ──────────────────
applyTheme(savedTheme);
setTimeout(function() { fitAddon.fit(); }, 50);
connect();
// ── Sidebar ─────────────────────────────────────────
var sidebar = document.getElementById('sidebar');
var sidebarState = localStorage.getItem('sidebar-visible');
if (sidebarState === 'false') sidebar.classList.add('collapsed');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
localStorage.setItem('sidebar-visible', !sidebar.classList.contains('collapsed'));
setTimeout(function() { fitAddon.fit(); }, 200);
}
// ── File viewer ─────────────────────────────────────
function loadFile(relPath) {
fetch('/api/file?project=' + encodeURIComponent(PAGE_PROJECT) + '&path=' + encodeURIComponent(relPath))
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('file-viewer-title').textContent = data.name;
document.getElementById('file-viewer-content').innerHTML = data.html;
document.getElementById('file-viewer').classList.remove('hidden');
})
.catch(function(err) { console.error('Error loading file:', err); });
}
function closeFileViewer() {
document.getElementById('file-viewer').classList.add('hidden');
term.focus();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeFileViewer();
});
</script>
</body>
</html>