claude-web-chat/templates/chat.html
djuka e60e574287
All checks were successful
Tests / unit-tests (push) Successful in 25s
Uklonjen autofocus sa modala koji prikazuje formu bez poziva
- autofocus na inputu u hidden modalu izazivao prikaz forme
  na nekim browserima pri učitavanju stranice
- Fokus se sada daje programski tek kad se modal otvori
- Isto rešenje primenjeno i na projects.html i chat.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:56:13 +00:00

873 lines
37 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 .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); }
/* Tab bar */
.tab-bar {
display: flex;
align-items: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 0 0.25rem;
flex-shrink: 0;
overflow-x: auto;
gap: 2px;
min-height: 30px;
}
.tab-bar::-webkit-scrollbar { height: 0; }
.tab {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.2rem 0.5rem;
font-size: 0.7rem;
color: var(--text-muted);
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
border-radius: 4px 4px 0 0;
white-space: nowrap;
user-select: none;
transition: color 0.15s, background 0.15s;
}
.tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.03); }
.tab.active { color: var(--text-primary); background: var(--bg-primary); border-color: var(--border); }
.tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 0.65rem;
border-radius: 3px;
color: var(--text-muted);
transition: background 0.1s, color 0.1s;
}
.tab-close:hover { background: rgba(244,67,54,0.3); color: var(--error); }
.tab-add-wrap { position: relative; flex-shrink: 0; }
.tab-add {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 0.85rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 4px;
border: none;
background: none;
font-family: inherit;
transition: color 0.15s, background 0.15s;
}
.tab-add:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); }
.tab-dropdown {
position: absolute;
top: 100%;
left: 0;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
min-width: 180px;
max-height: 300px;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.tab-dropdown.hidden { display: none; }
.tab-dropdown-item {
display: block;
width: 100%;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
font-family: inherit;
color: var(--text-secondary);
background: none;
border: none;
text-align: left;
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.tab-dropdown-item:hover { background: rgba(233,69,96,0.15); color: var(--text-primary); }
/* Terminal containers */
#terminals { flex: 1; overflow: hidden; position: relative; }
.term-pane { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; }
.term-pane.active { display: block; }
.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;
}
.sidebar-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 0.8rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 3px;
transition: color 0.15s, background 0.15s;
line-height: 1;
}
.sidebar-add-btn:hover { color: var(--accent); background: rgba(233,69,96,0.15); }
/* Create project modal */
.create-project-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); display: flex; align-items: center;
justify-content: center; z-index: 300;
}
.create-project-box {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem; width: 260px;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.create-project-box h3 { color: var(--accent); font-size: 0.85rem; margin-bottom: 0.6rem; }
.create-project-box input {
width: 100%; padding: 0.35rem 0.5rem; background: var(--bg-primary);
border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary);
font-size: 0.75rem; font-family: inherit; outline: none;
}
.create-project-box input:focus { border-color: var(--accent); }
/* 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" style="display:flex;justify-content:space-between;align-items:center;">
Projekti
<span class="sidebar-add-btn" onclick="showCreateProjectModal()" title="Novi projekat">+</span>
</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>
<!-- Modal za kreiranje projekta -->
<div id="createProjectModal" class="create-project-modal hidden" onclick="if(event.target===this)this.classList.add('hidden')">
<div class="create-project-box">
<h3>Novi projekat</h3>
<form id="createProjectForm" onsubmit="return createProject(event)">
<input type="text" id="newProjectName" placeholder="ime-projekta" required>
<p style="color:var(--text-muted);font-size:0.65rem;margin:0.3rem 0;">Slova, brojevi, - i _</p>
<div style="display:flex;gap:0.4rem;justify-content:flex-end;">
<button type="button" onclick="document.getElementById('createProjectModal').classList.add('hidden')" style="background:var(--bg-primary);color:var(--text-secondary);border:1px solid var(--border);border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Otkaži</button>
<button type="submit" style="background:var(--accent);color:#fff;border:none;border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Kreiraj</button>
</div>
</form>
<div id="createProjectError" style="color:var(--error);font-size:0.65rem;margin-top:0.3rem;display:none;"></div>
</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 class="tab-bar" id="tab-bar">
<!-- Tabs rendered by JS -->
<div class="tab-add-wrap">
<button class="tab-add" id="tab-add-btn" title="Novi tab">+</button>
<div class="tab-dropdown hidden" id="tab-dropdown"></div>
</div>
</div>
<div id="terminals"></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);
// Apply to all open tabs
tabs.forEach(function(tab) {
tab.term.options.theme = theme;
});
}
// ── Tab system ──────────────────────────────────────
var tabs = [];
var activeTabId = null;
var nextTabId = 1;
var statusEl = document.getElementById('ws-status');
var tabBarEl = document.getElementById('tab-bar');
var terminalsEl = document.getElementById('terminals');
var tabAddBtn = document.getElementById('tab-add-btn');
var tabDropdown = document.getElementById('tab-dropdown');
var projectsCache = null;
function createTerminal() {
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());
return { term: term, fitAddon: fitAddon };
}
function connectTab(tab) {
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/ws?project=' + encodeURIComponent(tab.project) +
'&project_dir=' + encodeURIComponent(tab.projectDir) +
'&tab=' + encodeURIComponent(tab.id);
var ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
tab.ws = ws;
ws.onopen = function() {
if (activeTabId === tab.id) {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
}
ws.send(JSON.stringify({ type: 'resize', cols: tab.term.cols, rows: tab.term.rows }));
if (activeTabId === tab.id) tab.term.focus();
};
ws.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
tab.term.write(new Uint8Array(event.data));
} else {
tab.term.write(event.data);
}
};
ws.onclose = function() {
if (activeTabId === tab.id) {
statusEl.textContent = 'Nepovezan';
statusEl.className = 'status';
}
// Reconnect only if tab still exists
if (tabs.find(function(t) { return t.id === tab.id; })) {
tab.reconnectTimer = setTimeout(function() { connectTab(tab); }, 2000);
}
};
ws.onerror = function() {
if (activeTabId === tab.id) {
statusEl.textContent = 'Greška';
statusEl.className = 'status';
}
};
}
function addTab(project, projectDir) {
var tabId = String(nextTabId++);
var t = createTerminal();
// Create pane
var pane = document.createElement('div');
pane.className = 'term-pane';
pane.id = 'pane-' + tabId;
terminalsEl.appendChild(pane);
t.term.open(pane);
var tab = {
id: tabId,
project: project,
projectDir: projectDir,
term: t.term,
fitAddon: t.fitAddon,
ws: null,
reconnectTimer: null,
container: pane
};
// Wire input
t.term.onData(function(data) {
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) tab.ws.send(data);
});
t.term.onResize(function(size) {
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
tab.ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
}
});
// Click on pane gives focus
pane.addEventListener('click', function() { tab.term.focus(); });
tabs.push(tab);
renderTabs();
switchTab(tabId);
connectTab(tab);
return tab;
}
function switchTab(tabId) {
activeTabId = tabId;
tabs.forEach(function(tab) {
if (tab.id === tabId) {
tab.container.classList.add('active');
} else {
tab.container.classList.remove('active');
}
});
renderTabs();
var active = tabs.find(function(t) { return t.id === tabId; });
if (active) {
setTimeout(function() {
active.fitAddon.fit();
active.term.focus();
}, 50);
// Update status
if (active.ws && active.ws.readyState === WebSocket.OPEN) {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
} else {
statusEl.textContent = 'Nepovezan';
statusEl.className = 'status';
}
}
}
function closeTab(tabId) {
if (tabs.length <= 1) return; // Don't close last tab
var idx = tabs.findIndex(function(t) { return t.id === tabId; });
if (idx === -1) return;
var tab = tabs[idx];
// Clean up
clearTimeout(tab.reconnectTimer);
if (tab.ws) {
tab.ws.onclose = null; // Prevent reconnect
tab.ws.close();
}
tab.term.dispose();
tab.container.remove();
tabs.splice(idx, 1);
// Switch to neighbor if active was closed
if (activeTabId === tabId) {
var newIdx = Math.min(idx, tabs.length - 1);
switchTab(tabs[newIdx].id);
} else {
renderTabs();
}
}
function renderTabs() {
// Remove old tab elements (keep the add button wrapper)
var addWrap = document.querySelector('.tab-add-wrap');
var oldTabs = tabBarEl.querySelectorAll('.tab');
oldTabs.forEach(function(el) { el.remove(); });
tabs.forEach(function(tab) {
var el = document.createElement('div');
el.className = 'tab' + (tab.id === activeTabId ? ' active' : '');
var label = document.createElement('span');
label.textContent = tab.project;
label.addEventListener('click', function(e) {
e.stopPropagation();
switchTab(tab.id);
});
el.appendChild(label);
if (tabs.length > 1) {
var closeBtn = document.createElement('span');
closeBtn.className = 'tab-close';
closeBtn.textContent = '\u00d7';
closeBtn.title = 'Zatvori tab';
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
closeTab(tab.id);
});
el.appendChild(closeBtn);
}
tabBarEl.insertBefore(el, addWrap);
});
}
// ── Add tab dropdown ────────────────────────────────
tabAddBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (!tabDropdown.classList.contains('hidden')) {
tabDropdown.classList.add('hidden');
return;
}
// Fetch projects and show dropdown
if (projectsCache) {
showDropdown(projectsCache);
} else {
fetch('/api/projects')
.then(function(r) { return r.json(); })
.then(function(data) {
projectsCache = data;
showDropdown(data);
})
.catch(function() {
// Fallback: just add tab for current project
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
});
}
});
function showDropdown(projects) {
tabDropdown.innerHTML = '';
projects.forEach(function(p) {
var btn = document.createElement('button');
btn.className = 'tab-dropdown-item';
btn.textContent = p.name;
btn.addEventListener('click', function() {
tabDropdown.classList.add('hidden');
addTab(p.name, p.path);
});
tabDropdown.appendChild(btn);
});
tabDropdown.classList.remove('hidden');
}
// Close dropdown when clicking outside
document.addEventListener('click', function() {
tabDropdown.classList.add('hidden');
});
// ── Window resize ───────────────────────────────────
window.addEventListener('resize', function() {
var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.fitAddon.fit();
});
// ── Apply initial theme ─────────────────────────────
applyTheme(savedTheme);
// ── Create first tab ────────────────────────────────
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
// ── 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() {
var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.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');
var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.term.focus();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeFileViewer();
document.getElementById('createProjectModal').classList.add('hidden');
}
});
// ── Create project ───────────────────────────────────
function showCreateProjectModal() {
document.getElementById('createProjectModal').classList.remove('hidden');
setTimeout(function() {
document.getElementById('newProjectName').focus();
}, 50);
}
function createProject(e) {
e.preventDefault();
var name = document.getElementById('newProjectName').value.trim();
var errEl = document.getElementById('createProjectError');
if (!name) return false;
var form = new FormData();
form.append('name', name);
fetch('/projects/create', {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) {
return r.json().then(function(data) {
if (r.ok) {
projectsCache = null; // Invalidate cache
window.location.href = '/chat/' + encodeURIComponent(name);
} else {
errEl.textContent = data.error || 'Greška pri kreiranju';
errEl.style.display = 'block';
}
});
})
.catch(function() {
errEl.textContent = 'Greška pri kreiranju';
errEl.style.display = 'block';
});
return false;
}
</script>
</body>
</html>