All checks were successful
Tests / unit-tests (push) Successful in 25s
- 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>
873 lines
37 KiB
HTML
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">←</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">☰</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>
|