Vraćen sidebar sa MD fajlovima, dodate teme za terminal
Some checks failed
Tests / unit-tests (push) Failing after 42s

- Sidebar sa listom .md fajlova projekta (toggle dugme, pamti stanje)
- Pregled MD fajlova u overlay-u (Esc za zatvaranje)
- 8 tema: Dark, Dracula, Monokai, Nord, Solarized, Gruvbox, Tokyo Night, Catppuccin
- Izbor teme se čuva u localStorage
- Linkovi: promeni lozinku, svi projekti, odjavi se

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
djuka 2026-02-18 05:59:12 +00:00
parent adea7ca28d
commit 897e62cb17
2 changed files with 355 additions and 70 deletions

View File

@ -120,9 +120,16 @@ func handleChat(w http.ResponseWriter, r *http.Request) {
projectDir := filepath.Join(cfg.ProjectsPath, project)
files, err := ListMarkdownFiles(projectDir)
if err != nil {
log.Printf("ListMarkdownFiles error: %v", err)
files = nil
}
data := map[string]any{
"Project": project,
"ProjectDir": projectDir,
"Files": files,
}
templates.Render(w, "chat.html", data)
}

View File

@ -7,65 +7,333 @@
<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;
}
body {
background: #0d1117;
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;
}
.sidebar-footer a {
color: var(--text-muted);
text-decoration: none;
display: block;
padding: 0.2rem 0;
}
.sidebar-footer a:hover { color: var(--accent); }
/* 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.4rem 0.75rem;
background: #161b22;
border-bottom: 1px solid #2a2a4a;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
padding: 0.35rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
flex-shrink: 0;
}
.terminal-header .title {
color: #e94560;
font-weight: 600;
}
.terminal-header .controls {
display: flex;
gap: 0.75rem;
align-items: center;
}
.terminal-header .status {
font-size: 0.7rem;
color: #6c6c80;
}
.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 a {
color: #a0a0b0;
text-decoration: none;
font-size: 0.75rem;
.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 a:hover { color: #e94560; }
#terminal-container {
flex: 1;
overflow: hidden;
.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 { 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); }
/* 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>
{{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>
{{else}}
<div class="sidebar-files" style="padding: 0.75rem; color: var(--text-muted); font-size: 0.75rem;">
Nema .md fajlova
</div>
{{end}}
<div class="sidebar-footer">
<a href="/change-password">Promeni lozinku</a>
<a href="/projects">&#8592; Svi projekti</a>
<a href="/logout">Odjavi se</a>
</div>
</div>
<!-- Main terminal area -->
<div class="main">
<div class="terminal-header">
<span class="title">claude — {{.Project}}</span>
<div class="controls">
<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>
<a href="/projects">← Projekti</a>
</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>
<a href="/projects" style="color:var(--text-muted);text-decoration:none;font-size:0.7rem;">Projekti</a>
</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>
// ── Themes ──────────────────────────────────────────
const 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'
}
};
// ── Terminal ────────────────────────────────────────
const savedTheme = localStorage.getItem('terminal-theme') || 'dark';
document.getElementById('theme-select').value = savedTheme;
const statusEl = document.getElementById('ws-status');
const container = document.getElementById('terminal-container');
@ -73,29 +341,7 @@
cursorBlink: true,
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
theme: {
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'
},
theme: THEMES[savedTheme] || THEMES.dark,
allowProposedApi: true,
scrollback: 10000,
convertEol: false,
@ -104,22 +350,32 @@
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
term.open(container);
fitAddon.fit();
function applyTheme(name) {
const theme = THEMES[name];
if (!theme) return;
term.options.theme = theme;
document.body.style.background = theme.background;
document.querySelector('.main').style.background = theme.background;
localStorage.setItem('terminal-theme', name);
}
// Apply saved theme to body
applyTheme(savedTheme);
// ── WebSocket ───────────────────────────────────────
let ws;
let reconnectTimer;
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/ws?project={{.Project}}&project_dir={{.ProjectDir}}`);
ws = new WebSocket(proto + '//' + location.host + '/ws?project={{.Project}}&project_dir={{.ProjectDir}}');
ws.binaryType = 'arraybuffer';
ws.onopen = function() {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
// Send initial terminal size
ws.send(JSON.stringify({type: 'resize', cols: term.cols, rows: term.rows}));
term.focus();
};
@ -135,7 +391,6 @@
ws.onclose = function() {
statusEl.textContent = 'Nepovezan';
statusEl.className = 'status';
// Auto-reconnect after 2 seconds
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2000);
};
@ -146,27 +401,50 @@
};
}
// Send keyboard input to server
term.onData(function(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
});
// Send terminal resize to server
term.onResize(function(size) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({type: 'resize', cols: size.cols, rows: size.rows}));
}
});
// Fit terminal on window resize
window.addEventListener('resize', function() {
fitAddon.fit();
});
// Start connection
window.addEventListener('resize', function() { fitAddon.fit(); });
connect();
// ── Sidebar ─────────────────────────────────────────
const sidebar = document.getElementById('sidebar');
const 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={{.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>