Files
ObsiGate/frontend/editor-poc.html
T
bruno ac9382d12e
CI / lint (push) Successful in 15s
CI / security (push) Successful in 9s
CI / test (push) Successful in 26s
CI / build (push) Successful in 3s
fix: auto-height on file load so entire content is visible
Added autoHeight() helper called after ta.value is set in both demo mode and API load. Previously only triggered on input event, so loaded files only showed first line (scrollHeight was 0 until user typed).
2026-06-06 00:18:11 -04:00

1148 lines
58 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ObsiGate Editor v1 — POC</title>
<style>
:root {
--bg: #0f1117; --bg2: #161822; --bg3: #1c1f2e; --bg4: #222639;
--bg-hover: #2a2d3e; --text: #e1e4ed; --text2: #9ca3b8; --text3: #6b7280;
--border: #2d3148; --border2: #3a3f58;
--accent: #4a7ab5; --accent-bg: rgba(74,122,181,0.12);
--ai: #8b5cf6; --ai-bg: rgba(139,92,246,0.12); --ai-glow: rgba(139,92,246,0.4);
--ok: #34d399; --warn: #fbbf24; --err: #f87171;
--r: 6px; --R: 8px;
--shadow: 0 8px 32px rgba(0,0,0,0.4);
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--sans); background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
/* Top bar */
.bar { height: 44px; min-height: 44px; display: flex; align-items: center; gap: 8px; padding: 0 12px; background: var(--bg2); border-bottom: 1px solid var(--border); z-index: 100; }
.bar-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.bar-hamburger { width: 28px; height: 28px; border-radius: var(--r); border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
.bar-hamburger:hover { background: var(--bg-hover); color: var(--text); }
.bar-brand { font-weight: 700; font-size: 12px; color: var(--accent); letter-spacing: 0.5px; white-space: nowrap; }
.bar-title { flex: 1; min-width: 80px; background: transparent; border: 1px solid transparent; color: var(--text); font-size: 13px; padding: 3px 6px; border-radius: var(--r); outline: none; font-family: var(--sans); }
.bar-title:hover { border-color: var(--border); }
.bar-title:focus { border-color: var(--accent); background: var(--bg3); }
.bar-right { display: flex; align-items: center; gap: 4px; }
.btn { height: 30px; padding: 0 10px; border-radius: var(--r); border: 1px solid transparent; background: transparent; color: var(--text2); cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 5px; white-space: nowrap; font-family: var(--sans); transition: all 120ms; }
.btn:hover { background: var(--bg-hover); color: var(--text); }
.btn-ai { background: var(--ai-bg); color: var(--ai); border-color: rgba(139,92,246,0.25); font-weight: 600; box-shadow: 0 0 10px var(--ai-glow); }
.btn-ai:hover { background: rgba(139,92,246,0.2); box-shadow: 0 0 18px var(--ai-glow); }
.btn-ai.active { background: var(--ai); color: #fff; }
.btn-ai.off { color: var(--text3); border-color: var(--border); box-shadow: none; background: transparent; }
.save-dot { display: flex; align-items: center; gap: 4px; font-size: 11px; padding: 0 4px; }
.save-dot .dot { width: 6px; height: 6px; border-radius: 50%; }
.save-dot.ok { color: var(--ok); } .save-dot.ok .dot { background: var(--ok); }
.save-dot.dirty { color: var(--warn); } .save-dot.dirty .dot { background: var(--warn); }
.save-dot.saving { color: var(--ai); animation: pulse 0.8s infinite; } .save-dot.saving .dot { background: var(--ai); }
.save-dot.err { color: var(--err); } .save-dot.err .dot { background: var(--err); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
/* Workspace */
.ws { display: flex; flex: 1; overflow: hidden; }
.editor-area { flex: 1; display: flex; overflow: hidden; position: relative; }
.editor-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
.editor-pane.hidden { display: none; }
/* Textarea pane */
.editor-wrap { flex: 1; overflow-y: auto; position: relative; padding: 24px 16px 24px 48px; }
.textarea { width: 100%; height: auto; min-height: 100%; resize: none; overflow: hidden; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--mono); font-size: 13.5px; line-height: 1.75; tab-size: 2; }
.textarea::placeholder { color: var(--text3); }
/* Quick insert + */
.qi-btn { position: absolute; left: 14px; top: 24px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg2); color: var(--text3); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 100ms; }
.qi-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-bg); }
/* Preview pane */
.preview-pane { flex: 0.67; overflow-y: auto; padding: 24px 24px 80px; background: var(--bg2); border-left: 1px solid var(--border); display: none; }
.preview-pane.visible { display: block; }
.preview-content { max-width: 720px; margin: 0 auto; font-family: var(--sans); font-size: 14px; line-height: 1.75; color: var(--text); }
.preview-content h1 { font-size: 26px; margin: 20px 0 10px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.preview-content h2 { font-size: 20px; margin: 18px 0 8px; }
.preview-content h3 { font-size: 16px; margin: 14px 0 6px; }
.preview-content p { margin: 6px 0; }
.preview-content ul, .preview-content ol { margin: 6px 0; padding-left: 24px; }
.preview-content li { margin: 2px 0; }
.preview-content code { background: var(--bg3); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 12px; color: var(--ai); }
.preview-content pre { background: var(--bg3); padding: 12px 16px; border-radius: var(--R); overflow-x: auto; margin: 10px 0; border: 1px solid var(--border); }
.preview-content pre code { background: none; padding: 0; color: var(--text); }
.preview-content blockquote { border-left: 3px solid var(--accent); padding: 4px 14px; margin: 10px 0; color: var(--text2); background: rgba(74,122,181,0.05); border-radius: 0 var(--r) var(--r) 0; }
.preview-content a { color: var(--accent); }
.preview-content table { border-collapse: collapse; margin: 10px 0; width: 100%; }
.preview-content th, .preview-content td { border: 1px solid var(--border); padding: 6px 12px; text-align: left; font-size: 13px; }
.preview-content th { background: var(--bg3); font-weight: 600; }
.preview-content hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
.preview-content img { max-width: 100%; border-radius: var(--r); }
.preview-loading { text-align: center; padding: 40px; color: var(--text3); font-size: 13px; }
/* AI Panel (right) */
.ai-panel { width: 320px; min-width: 320px; background: var(--bg2); border-left: 1px solid var(--border); display: flex; flex-direction: column; transition: transform 200ms, min-width 200ms, width 200ms; overflow: hidden; }
.ai-panel.hidden { transform: translateX(100%); min-width: 0; width: 0; border-left: none; }
.ai-panel-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.ai-panel-title { font-weight: 700; font-size: 13px; color: var(--ai); }
.ai-panel-close { width: 26px; height: 26px; border-radius: var(--r); border: none; background: transparent; color: var(--text3); cursor: pointer; font-size: 15px; }
.ai-panel-close:hover { background: var(--bg-hover); color: var(--text); }
.ai-chat { flex: 1; padding: 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.ai-suggestions { padding: 10px 14px; border-top: 1px solid var(--border); }
.ai-sugg { display: block; width: 100%; text-align: left; padding: 7px 10px; margin-bottom: 3px; border-radius: var(--r); border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: 12px; font-family: var(--sans); }
.ai-sugg:hover { background: var(--bg-hover); color: var(--text); }
.ai-input-row { padding: 8px 14px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
.ai-input { flex: 1; padding: 7px 10px; border-radius: var(--r); border: 1px solid var(--border); background: var(--bg3); color: var(--text); font-size: 12px; outline: none; font-family: var(--sans); }
.ai-input:focus { border-color: var(--ai); }
.ai-send { padding: 7px 12px; border-radius: var(--r); border: none; background: var(--ai); color: #fff; cursor: pointer; font-size: 12px; font-weight: 600; }
.ai-msg { padding: 8px 11px; border-radius: var(--r); font-size: 12px; line-height: 1.5; max-width: 90%; white-space: pre-wrap; }
.ai-msg.user { background: var(--bg3); align-self: flex-end; }
.ai-msg.bot { background: var(--ai-bg); color: var(--ai); align-self: flex-start; }
.ai-msg.loading { color: var(--text3); font-style: italic; align-self: flex-start; }
.ai-chat-placeholder { color: var(--text3); font-size: 12px; padding: 10px; }
/* Slash menu */
.slash { position: absolute; z-index: 200; width: 300px; max-height: 360px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; }
.slash.on { display: block; animation: fadeIn 120ms; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.slash-search { padding: 8px 10px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg4); border-radius: var(--R) var(--R) 0 0; }
.slash-search input { width: 100%; padding: 5px 8px; border-radius: var(--r); border: 1px solid var(--border); background: var(--bg3); color: var(--text); font-size: 12px; outline: none; font-family: var(--sans); }
.slash-section { padding: 6px 12px 3px; font-size: 10px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.4px; }
.slash-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; cursor: pointer; font-size: 12px; color: var(--text2); transition: background 60ms; }
.slash-item:hover, .slash-item.sel { background: var(--bg-hover); color: var(--text); }
.slash-icon { width: 24px; height: 24px; border-radius: var(--r); background: var(--bg3); display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; }
.slash-body { flex: 1; min-width: 0; }
.slash-title { font-weight: 600; font-size: 12px; color: var(--text); }
.slash-desc { font-size: 10px; color: var(--text3); margin-top: 1px; }
.slash-div { height: 1px; margin: 3px 12px; background: var(--border); }
/* Bubble menu */
.bubble { position: fixed; z-index: 150; display: none; align-items: center; gap: 1px; padding: 3px 5px; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); }
.bubble.on { display: flex; animation: fadeIn 100ms; }
.bubble-btn { width: 28px; height: 28px; border-radius: var(--r); border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; transition: all 80ms; }
.bubble-btn:hover { background: var(--bg-hover); color: var(--text); }
.bubble-sep { width: 1px; height: 18px; background: var(--border); margin: 0 3px; }
.bubble-btn.ai { color: var(--ai); }
.bubble-more { position: absolute; top: 100%; right: 0; margin-top: 4px; width: 190px; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; z-index: 160; padding: 3px; }
.bubble-more.on { display: block; }
.bubble-more-item { display: flex; align-items: center; gap: 6px; padding: 6px 9px; font-size: 12px; color: var(--text2); cursor: pointer; border-radius: var(--r); }
.bubble-more-item:hover { background: var(--bg-hover); color: var(--text); }
/* Quick Insert menu */
.qi-menu { position: absolute; z-index: 190; width: 200px; max-height: 380px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.qi-menu.on { display: block; animation: fadeIn 100ms; }
.qi-item { display: flex; align-items: center; gap: 7px; padding: 6px 9px; font-size: 12px; color: var(--text2); cursor: pointer; border-radius: var(--r); }
.qi-item:hover { background: var(--bg-hover); color: var(--text); }
/* Inline form (table dims, code lang) */
.inline-form { position: absolute; z-index: 195; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 8px 10px; min-width: 200px; }
.inline-form.on { display: block; animation: fadeIn 100ms; }
.inline-form-label { font-size: 10px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 6px; }
.inline-form-row { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
.inline-form-row label { font-size: 11px; color: var(--text2); min-width: 55px; }
.inline-form-row input { flex: 1; padding: 4px 7px; border-radius: var(--r); border: 1px solid var(--border); background: var(--bg3); color: var(--text); font-size: 12px; outline: none; font-family: var(--mono); width: 60px; }
.inline-form-row input:focus { border-color: var(--accent); }
.inline-form-actions { display: flex; gap: 4px; justify-content: flex-end; margin-top: 4px; }
.inline-form-actions button { padding: 4px 12px; border-radius: var(--r); border: 1px solid var(--border); background: var(--bg3); color: var(--text); cursor: pointer; font-size: 11px; font-family: var(--sans); }
.inline-form-actions button.ok { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.inline-form-actions button:hover { background: var(--bg-hover); }
.inline-form-actions button.ok:hover { background: #5a8fc5; }
/* Lang picker (in QI menu style) */
.lang-picker { position: absolute; z-index: 195; width: 200px; max-height: 320px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.lang-picker.on { display: block; animation: fadeIn 100ms; }
.lang-item { display: flex; align-items: center; gap: 7px; padding: 6px 9px; font-size: 12px; color: var(--text2); cursor: pointer; border-radius: var(--r); font-family: var(--mono); }
.lang-item:hover, .lang-item.sel { background: var(--bg-hover); color: var(--text); }
/* Drag & Drop */
.drop-overlay { position: fixed; inset: 0; z-index: 500; display: none; align-items: center; justify-content: center; background: rgba(15,17,23,0.85); border: 3px dashed var(--accent); pointer-events: none; }
.drop-overlay.on { display: flex; }
.drop-msg { text-align: center; color: var(--accent); }
.drop-icon { font-size: 44px; margin-bottom: 10px; }
.drop-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.drop-sub { font-size: 13px; color: var(--text2); }
/* Status bar */
.stat { height: 26px; min-height: 26px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; background: var(--bg2); border-top: 1px solid var(--border); font-size: 11px; color: var(--text3); }
.stat-left, .stat-right { display: flex; align-items: center; gap: 14px; }
.stat-ai { color: var(--ai); font-weight: 600; display: none; align-items: center; gap: 4px; }
.stat-ai.on { display: flex; }
.stat-file { color: var(--text2); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Toast */
.toast { position: fixed; bottom: 44px; left: 50%; transform: translateX(-50%); background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); padding: 9px 18px; font-size: 12px; color: var(--text); box-shadow: var(--shadow); z-index: 600; display: none; animation: fadeIn 150ms; }
.toast.on { display: block; }
.toast.ok { border-color: var(--ok); } .toast.err { border-color: var(--err); }
/* Loading spinner */
.spin { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--text3); border-top-color: var(--ai); border-radius: 50%; animation: spin 0.6s linear infinite; margin-right: 6px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Resize handle between panes */
.resize-handle { width: 4px; cursor: col-resize; background: var(--border); flex-shrink: 0; transition: background 150ms; }
.resize-handle:hover { background: var(--accent); }
</style>
</head>
<body>
<!-- Bar -->
<div class="bar">
<div class="bar-left">
<button class="bar-hamburger" title="Menu">&#9776;</button>
<span class="bar-brand">ObsiGate</span>
<input class="bar-title" id="poc-title" placeholder="Titre..." spellcheck="false">
</div>
<div class="bar-right">
<button class="btn" id="btn-preview" title="Toggle preview">&#128065; Preview</button>
<button class="btn btn-ai off" id="btn-ai" title="AI Panel (Ctrl+J)">&#10024; AI</button>
<div class="save-dot ok" id="save-dot"><span class="dot"></span><span id="save-label">Saved</span></div>
</div>
</div>
<!-- Workspace -->
<div class="ws">
<div class="editor-area" id="editor-area">
<!-- Drop overlay -->
<div class="drop-overlay" id="drop-overlay">
<div class="drop-msg"><div class="drop-icon">&#128444;</div><div class="drop-title">Drop files here</div><div class="drop-sub">Images, PDFs, documents</div></div>
</div>
<!-- Editor pane (textarea) -->
<div class="editor-pane" id="editor-pane">
<div class="editor-wrap" id="editor-wrap">
<button class="qi-btn" id="qi-btn" title="Quick insert">&#8853;</button>
<textarea class="textarea" id="ta" placeholder="Type / for commands..." spellcheck="false"></textarea>
<!-- Quick Insert menu -->
<div class="qi-menu" id="qi-menu">
<div class="qi-item" data-qi="image">&#127912; Image</div>
<div class="qi-item" data-qi="code">&#60;/&#62; Code Block</div>
<div class="qi-item" data-qi="table">&#128202; Table</div>
<div class="qi-item" data-qi="divider">&#9472; Divider</div>
<div style="height:1px;margin:3px 6px;background:var(--border)"></div>
<div style="font-size:10px;font-weight:600;color:var(--text3);padding:4px 9px 2px;text-transform:uppercase;letter-spacing:0.4px">&#128208; Diagram</div>
<div class="qi-item" data-qi="mermaid-flowchart">&#128256; Flowchart</div>
<div class="qi-item" data-qi="mermaid-sequence">&#128257; Sequence</div>
<div class="qi-item" data-qi="mermaid-class">&#128258; Class Diagram</div>
<div class="qi-item" data-qi="mermaid-state">&#128259; State Diagram</div>
<div class="qi-item" data-qi="mermaid-er">&#128260; ER Diagram</div>
<div class="qi-item" data-qi="mermaid-gantt">&#128261; Gantt Chart</div>
<div class="qi-item" data-qi="mermaid-pie">&#128262; Pie Chart</div>
</div>
<!-- Inline form (table dims) -->
<div class="inline-form" id="inline-form">
<div class="inline-form-label" id="inline-form-label">Table dimensions</div>
<div class="inline-form-row"><label>Colonnes</label><input type="number" id="if-cols" value="3" min="1" max="12"></div>
<div class="inline-form-row"><label>Lignes</label><input type="number" id="if-rows" value="3" min="1" max="50"></div>
<div class="inline-form-actions">
<button class="ok" id="if-ok">Insérer</button>
<button id="if-cancel">Annuler</button>
</div>
</div>
<!-- Language picker (code block) -->
<div class="lang-picker" id="lang-picker"></div>
<!-- Slash menu -->
<div class="slash" id="slash-menu">
<div class="slash-search"><input type="text" id="slash-search" placeholder="Filter..." autocomplete="off"></div>
<div class="slash-section">Formatting</div>
<div class="slash-item" data-action="h1"><span class="slash-icon">H1</span><div class="slash-body"><div class="slash-title">Heading 1</div><div class="slash-desc">Big section heading</div></div></div>
<div class="slash-item" data-action="h2"><span class="slash-icon">H2</span><div class="slash-body"><div class="slash-title">Heading 2</div><div class="slash-desc">Medium section heading</div></div></div>
<div class="slash-item" data-action="h3"><span class="slash-icon">H3</span><div class="slash-body"><div class="slash-title">Heading 3</div><div class="slash-desc">Small section heading</div></div></div>
<div class="slash-item" data-action="bullet"><span class="slash-icon">&#8226;</span><div class="slash-body"><div class="slash-title">Bullet List</div><div class="slash-desc">Unordered list</div></div></div>
<div class="slash-item" data-action="numbered"><span class="slash-icon">1.</span><div class="slash-body"><div class="slash-title">Numbered List</div><div class="slash-desc">Ordered list</div></div></div>
<div class="slash-item" data-action="checklist"><span class="slash-icon">&#9744;</span><div class="slash-body"><div class="slash-title">Checklist</div><div class="slash-desc">Task list</div></div></div>
<div class="slash-item" data-action="quote"><span class="slash-icon">&#10077;</span><div class="slash-body"><div class="slash-title">Quote</div><div class="slash-desc">Block quote</div></div></div>
<div class="slash-item" data-action="code"><span class="slash-icon">&#60;/&#62;</span><div class="slash-body"><div class="slash-title">Code Block</div><div class="slash-desc">Syntax-highlighted</div></div></div>
<div class="slash-item" data-action="divider"><span class="slash-icon">&#9472;</span><div class="slash-body"><div class="slash-title">Divider</div><div class="slash-desc">Horizontal rule</div></div></div>
<div class="slash-div"></div>
<div class="slash-section">Insert</div>
<div class="slash-item" data-action="image"><span class="slash-icon">&#127912;</span><div class="slash-body"><div class="slash-title">Image</div><div class="slash-desc">![alt](url)</div></div></div>
<div class="slash-item" data-action="link"><span class="slash-icon">&#128279;</span><div class="slash-body"><div class="slash-title">Link</div><div class="slash-desc">[text](url)</div></div></div>
<div class="slash-item" data-action="table"><span class="slash-icon">&#128202;</span><div class="slash-body"><div class="slash-title">Table</div><div class="slash-desc">Data table</div></div></div>
<div class="slash-div"></div>
<div class="slash-section">AI Actions</div>
<div class="slash-item" data-action="ai-improve"><span class="slash-icon" style="color:var(--ai)">&#10024;</span><div class="slash-body"><div class="slash-title">AI: Improve writing</div><div class="slash-desc">Enhance style and clarity</div></div></div>
<div class="slash-item" data-action="ai-fix"><span class="slash-icon" style="color:var(--ai)">&#10024;</span><div class="slash-body"><div class="slash-title">AI: Fix spelling</div><div class="slash-desc">Correct errors</div></div></div>
<div class="slash-item" data-action="ai-continue"><span class="slash-icon" style="color:var(--ai)">&#10024;</span><div class="slash-body"><div class="slash-title">AI: Continue writing</div><div class="slash-desc">Continue from here</div></div></div>
<div class="slash-item" data-action="ai-summarize"><span class="slash-icon" style="color:var(--ai)">&#10024;</span><div class="slash-body"><div class="slash-title">AI: Summarize</div><div class="slash-desc">Condense document</div></div></div>
<div class="slash-item" data-action="ai-translate"><span class="slash-icon" style="color:var(--ai)">&#10024;</span><div class="slash-body"><div class="slash-title">AI: Translate</div><div class="slash-desc">To another language</div></div></div>
</div>
</div>
</div>
<!-- Resize handle -->
<div class="resize-handle" id="resize-handle" title="Drag to resize"></div>
<!-- Preview pane -->
<div class="preview-pane" id="preview-pane">
<div class="preview-content" id="preview-content"><div class="preview-loading">Preview appears here</div></div>
</div>
</div>
<!-- Bubble menu -->
<div class="bubble" id="bubble">
<button class="bubble-btn" data-action="bold" title="Bold (Ctrl+B)"><b>B</b></button>
<button class="bubble-btn" data-action="italic" title="Italic (Ctrl+I)"><i>I</i></button>
<button class="bubble-btn" data-action="strike" title="Strikethrough"><s>S</s></button>
<button class="bubble-btn" data-action="code" title="Inline Code">&#60;/&#62;</button>
<button class="bubble-btn" data-action="link" title="Link">&#128279;</button>
<button class="bubble-btn" data-action="highlight" title="Highlight">&#9728;</button>
<span class="bubble-sep"></span>
<button class="bubble-btn ai" id="bubble-more" title="AI Actions">&#8943;</button>
<div class="bubble-more" id="bubble-more-menu">
<div class="bubble-more-item" data-action="ai-improve">&#10024; Improve writing</div>
<div class="bubble-more-item" data-action="ai-fix">&#9989; Fix spelling</div>
<div class="bubble-more-item" data-action="ai-shorter">&#128260; Make shorter</div>
<div class="bubble-more-item" data-action="ai-longer">&#128261; Make longer</div>
<div class="bubble-more-item" data-action="ai-translate">&#127760; Translate to EN</div>
<div class="bubble-more-item" data-action="ai-continue">&#10133; Continue writing</div>
</div>
</div>
<!-- AI Panel -->
<div class="ai-panel hidden" id="ai-panel">
<div class="ai-panel-head">
<span class="ai-panel-title">&#10024; AI Assistant</span>
<button class="ai-panel-close" id="ai-close">&times;</button>
</div>
<div class="ai-chat" id="ai-chat">
<div class="ai-chat-placeholder">Ask AI about your document. Use slash /ai or select text for AI actions.</div>
</div>
<div class="ai-suggestions">
<button class="ai-sugg">&#10024; Improve overall style</button>
<button class="ai-sugg">&#10133; Add a conclusion</button>
<button class="ai-sugg">&#128221; Summarize document</button>
</div>
<div class="ai-input-row">
<input class="ai-input" id="ai-input" placeholder="Ask AI...">
<button class="ai-send" id="ai-send">Send</button>
</div>
</div>
</div>
<!-- Status bar -->
<div class="stat">
<div class="stat-left">
<span>Type <strong>/</strong> for commands | <strong>Alt+I</strong> Insert</span>
<span>Ctrl+J AI panel</span>
<span>Ctrl+S save</span>
<span class="stat-ai" id="stat-ai">&#10024; AI processing...</span>
</div>
<div class="stat-right">
<span class="stat-file" id="stat-file"></span>
<span id="stat-words">0 words</span>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- marked.js CDN for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
(function() {
'use strict';
// ---- DOM ----
var ta = document.getElementById('ta');
var editorWrap = document.getElementById('editor-wrap');
var editorArea = document.getElementById('editor-area');
var previewPane = document.getElementById('preview-pane');
var previewContent = document.getElementById('preview-content');
var resizeHandle = document.getElementById('resize-handle');
var slashMenu = document.getElementById('slash-menu');
var slashSearch = document.getElementById('slash-search');
var bubble = document.getElementById('bubble');
var bubbleMore = document.getElementById('bubble-more-menu');
var qiMenu = document.getElementById('qi-menu');
var aiPanel = document.getElementById('ai-panel');
var btnAI = document.getElementById('btn-ai');
var btnPreview = document.getElementById('btn-preview');
var saveDot = document.getElementById('save-dot');
var saveLabel = document.getElementById('save-label');
var statFile = document.getElementById('stat-file');
var statWords = document.getElementById('stat-words');
var statAI = document.getElementById('stat-ai');
var toast = document.getElementById('toast');
var dropOverlay = document.getElementById('drop-overlay');
// ---- State ----
var fileVault = null;
var filePath = null;
var originalContent = '';
var isDirty = false;
var previewVisible = false;
var slashItems = [];
var slashVisible = false;
var slashIdx = -1;
var saveTimer = null;
var aiConfigured = null;
// Parse URL params
(function() {
var p = new URLSearchParams(window.location.search);
fileVault = p.get('vault');
filePath = p.get('path');
})();
// ---- Cursor utils ----
function getPos() { return { s: ta.selectionStart, e: ta.selectionEnd }; }
function setPos(p) { ta.setSelectionRange(p, p); ta.focus(); }
function val() { return ta.value; }
function insertAtCursor(text, cursorOffset) {
var p = getPos();
ta.value = val().substring(0, p.s) + text + val().substring(p.e);
var np = cursorOffset != null ? p.s + cursorOffset : p.s + text.length;
setPos(np);
markDirty();
}
function wrapSelection(before, after) {
var p = getPos();
if (p.s === p.e) { insertAtCursor(before + (after || before), before.length); return; }
var sel = val().substring(p.s, p.e);
ta.value = val().substring(0, p.s) + before + sel + (after || before) + val().substring(p.e);
setPos(p.s + before.length + sel.length + (after || before).length);
markDirty();
}
function getLine() {
var p = getPos();
var v = val();
var ls = v.lastIndexOf('\n', p.s - 1) + 1;
var le = v.indexOf('\n', p.s); if (le === -1) le = v.length;
return { text: v.substring(ls, le), start: ls, end: le };
}
function replaceLine(newText) {
var l = getLine();
ta.value = val().substring(0, l.start) + newText + val().substring(l.end);
setPos(l.start + newText.length);
markDirty();
}
function removeTrailingSlash() {
var l = getLine();
replaceLine(l.text.replace(/^\/\s*/, ''));
}
// ---- Dirty / Save ----
function markDirty() {
if (!isDirty) { isDirty = true; updateSaveDot(); }
scheduleAutoSave();
schedulePreview();
}
function updateSaveDot() {
if (isDirty) { saveDot.className = 'save-dot dirty'; saveLabel.textContent = 'Unsaved'; }
else { saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved'; }
}
function scheduleAutoSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(autoSave, 2000);
}
function autoSave() {
if (!isDirty || !fileVault || !filePath) return;
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
var content = val();
fetch('/api/file/' + encodeURIComponent(fileVault) + '/save?path=' + encodeURIComponent(filePath) + '&backup=false', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
}).then(function(r) {
if (!r.ok) throw new Error(r.status);
isDirty = false; originalContent = content;
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
}).catch(function(e) {
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
showToast('Save failed: ' + e.message, 'err');
});
}
function forceSave() {
if (!fileVault || !filePath) { showToast('No file loaded. Use ?vault=X&path=Y', 'err'); return; }
clearTimeout(saveTimer);
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
var content = val();
fetch('/api/file/' + encodeURIComponent(fileVault) + '/save?path=' + encodeURIComponent(filePath), {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
}).then(function(r) {
if (!r.ok) throw new Error(r.status);
isDirty = false; originalContent = content;
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
showToast('Saved', 'ok');
}).catch(function(e) {
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
showToast('Save failed: ' + e.message, 'err');
});
}
// ---- File load ----
function loadFile() {
if (!fileVault || !filePath) {
// Demo mode
ta.value = '# Demo Document\n\nType **/** for commands. Select text for formatting.\n\n## Features\n\n- Slash commands: type / at start of line\n- Bubble menu: select text\n- AI actions: use the sparkle button or slash /ai\n- Preview: click the eye button\n';
autoHeight();
statFile.textContent = 'Demo mode';
document.title = 'ObsiGate Forge — Demo';
originalContent = ta.value;
updateStats();
schedulePreview();
return;
}
statFile.textContent = 'Loading...';
fetch('/api/file/' + encodeURIComponent(fileVault) + '/raw?path=' + encodeURIComponent(filePath))
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
ta.value = data.raw || '';
autoHeight();
originalContent = ta.value;
// Extract title from first # heading or filename
var title = filePath.split('/').pop().replace(/\.md$/, '');
var m = ta.value.match(/^#\s+(.+)/m);
if (m) title = m[1];
document.getElementById('poc-title').value = title;
statFile.textContent = fileVault + '/' + filePath;
document.title = title + ' — ObsiGate Forge';
updateStats();
schedulePreview();
})
.catch(function(e) {
ta.value = '# Error\n\nCould not load: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
statFile.textContent = 'Load error';
originalContent = ta.value;
updateStats();
});
}
// ---- AI calls ----
function aiCall(endpoint, text, extra, callback) {
if (!aiConfigured) {
// Check status first, then retry
fetch('/api/ai/status').then(function(r) { return r.json(); }).then(function(s) {
aiConfigured = s.configured === true;
updateAIButton();
if (aiConfigured) aiCall(endpoint, text, extra, callback);
else { showToast('AI not configured', 'err'); hideAIProcessing(); }
}).catch(function() { showToast('AI status check failed', 'err'); hideAIProcessing(); });
return;
}
showAIProcessing();
var body = { text: text };
if (extra) Object.keys(extra).forEach(function(k) { body[k] = extra[k]; });
fetch('/api/ai/' + endpoint, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(function(r) {
if (!r.ok) throw new Error('AI error: ' + r.status);
return r.json();
}).then(function(data) {
hideAIProcessing();
if (callback) callback(data.result || data.response || '');
else showToast('AI: ' + (data.result || 'done').substring(0, 60) + '...', 'ok');
}).catch(function(e) {
hideAIProcessing();
showToast(e.message, 'err');
if (callback) callback(null);
});
}
function checkAIStatus() {
fetch('/api/ai/status').then(function(r) { return r.json(); }).then(function(s) {
aiConfigured = s.configured === true;
updateAIButton();
}).catch(function() { aiConfigured = false; updateAIButton(); });
}
function updateAIButton() {
if (aiConfigured) { btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)'; }
else { btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured'; }
}
function showAIProcessing() { statAI.classList.add('on'); }
function hideAIProcessing() { statAI.classList.remove('on'); }
// AI actions mapped to endpoints
var AI_MAP = {
'ai-improve': { ep: 'improve', desc: 'improve writing' },
'ai-fix': { ep: 'fix-spelling', desc: 'fix spelling' },
'ai-continue': { ep: 'continue', desc: 'continue writing' },
'ai-summarize': { ep: 'summarize', desc: 'summarize' },
'ai-translate': { ep: 'translate', extra: { language: 'en' }, desc: 'translate' },
'ai-shorter': { ep: 'simplify', desc: 'make shorter' },
'ai-longer': { ep: 'lengthen', desc: 'make longer' },
};
function execAIAction(actionKey) {
var cfg = AI_MAP[actionKey];
if (!cfg) return;
var sel = val().substring(ta.selectionStart, ta.selectionEnd) || val();
aiCall(cfg.ep, sel, cfg.extra || null, function(result) {
if (!result) return;
if (ta.selectionStart !== ta.selectionEnd) {
// Replace selection
var p = getPos();
ta.value = val().substring(0, p.s) + result + val().substring(p.e);
setPos(p.s + result.length);
} else {
// Insert at cursor
insertAtCursor('\n' + result + '\n');
}
markDirty();
});
}
// ---- Markdown Preview ----
var previewTimer = null;
function schedulePreview() {
if (!previewVisible) return;
clearTimeout(previewTimer);
previewTimer = setTimeout(renderPreview, 300);
}
function renderPreview() {
if (!previewVisible) return;
try {
previewContent.innerHTML = marked.parse(val()) || '<div class="preview-loading">Empty document</div>';
} catch(e) {
previewContent.innerHTML = '<div class="preview-loading">Preview error</div>';
}
}
function togglePreview() {
previewVisible = !previewVisible;
previewPane.classList.toggle('visible', previewVisible);
resizeHandle.style.display = previewVisible ? '' : 'none';
btnPreview.style.color = previewVisible ? 'var(--accent)' : '';
btnPreview.style.background = previewVisible ? 'var(--accent-bg)' : '';
if (previewVisible) renderPreview();
}
// ---- Sync scroll between editor and preview ----
var _syncScrolling = false;
editorWrap.addEventListener('scroll', function() {
if (!previewVisible || _syncScrolling) return;
_syncScrolling = true;
var pct = editorWrap.scrollTop / (editorWrap.scrollHeight - editorWrap.clientHeight);
previewPane.scrollTop = pct * (previewPane.scrollHeight - previewPane.clientHeight);
setTimeout(function() { _syncScrolling = false; }, 50);
});
previewPane.addEventListener('scroll', function() {
if (_syncScrolling) return;
_syncScrolling = true;
var pct = previewPane.scrollTop / (previewPane.scrollHeight - previewPane.clientHeight);
editorWrap.scrollTop = pct * (editorWrap.scrollHeight - editorWrap.clientHeight);
setTimeout(function() { _syncScrolling = false; }, 50);
});
// ---- Resize handle ----
(function() {
var dragging = false;
resizeHandle.addEventListener('mousedown', function(e) {
e.preventDefault();
dragging = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function(e) {
if (!dragging) return;
var areaRect = editorArea.getBoundingClientRect();
var ratio = (e.clientX - areaRect.left) / areaRect.width;
ratio = Math.max(0.25, Math.min(0.75, ratio));
var editorPane = document.getElementById('editor-pane');
editorPane.style.flex = ratio;
previewPane.style.flex = (1 - ratio);
});
document.addEventListener('mouseup', function() {
if (!dragging) return;
dragging = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
})();
// ---- Caret position to pixel (for monospace textarea) ----
function caretToPixel(pos) {
if (pos == null) pos = getPos().s;
var v = val();
var before = v.substring(0, pos);
var lineNum = before.split('\n').length - 1;
var colNum = pos - v.lastIndexOf('\n', pos - 1) - 1;
if (colNum < 0) colNum = 0;
var style = getComputedStyle(ta);
var fontSize = parseFloat(style.fontSize);
var lineH = parseFloat(style.lineHeight);
if (isNaN(lineH)) lineH = fontSize * 1.75;
var charW = fontSize * 0.615;
var wrapRect = editorWrap.getBoundingClientRect();
var taRect = ta.getBoundingClientRect();
// Position relative to editorWrap
var x = taRect.left - wrapRect.left + 2 + colNum * charW;
var y = taRect.top - wrapRect.top + (lineNum + 1) * lineH - ta.scrollTop;
// Clamp
if (x < 4) x = 4;
if (x > wrapRect.width - 310) x = wrapRect.width - 310;
if (y > wrapRect.height - 360) y = wrapRect.height - 360;
if (y < 20) y = 20;
return { x: x, y: y };
}
// ---- Slash menu ----
function initSlashItems() { slashItems = Array.from(slashMenu.querySelectorAll('.slash-item')); }
var SLASH_INSERT = {
h1: function() { replaceLine('# '); },
h2: function() { replaceLine('## '); },
h3: function() { replaceLine('### '); },
bullet: function() { replaceLine('- '); },
numbered: function() { replaceLine('1. '); },
checklist: function() { replaceLine('- [ ] '); },
quote: function() { replaceLine('> '); },
divider: function() { insertAtCursor('\n---\n'); },
image: function() { insertAtCursor('![](url)', 2); },
link: function() { insertAtCursor('[text](url)', 1); },
};
function showSlash() {
slashMenu.classList.add('on'); slashVisible = true; slashIdx = -1;
slashSearch.value = ''; slashSearch.focus();
filterSlash('');
// Position below current line at slash character
var pos = getPos().s - 1; // position of the / character
var pt = caretToPixel(pos);
slashMenu.style.left = pt.x + 'px';
slashMenu.style.top = pt.y + 'px';
}
function hideSlash() { slashMenu.classList.remove('on'); slashVisible = false; resetSlash(); }
function filterSlash(q) {
q = q.toLowerCase().trim();
slashItems.forEach(function(item) {
var t = (item.querySelector('.slash-title') || {}).textContent || '';
var d = (item.querySelector('.slash-desc') || {}).textContent || '';
item.style.display = (!q || t.toLowerCase().indexOf(q) >= 0 || d.toLowerCase().indexOf(q) >= 0) ? '' : 'none';
item.classList.remove('sel');
});
updateSlashSections(); slashIdx = -1;
}
function updateSlashSections() {
slashMenu.querySelectorAll('.slash-section').forEach(function(s) {
var n = s.nextElementSibling; var allHidden = true;
while (n && !n.classList.contains('slash-section') && !n.classList.contains('slash-div')) {
if (n.style.display !== 'none') { allHidden = false; break; } n = n.nextElementSibling;
}
s.style.display = allHidden ? 'none' : '';
});
}
function resetSlash() {
slashItems.forEach(function(i) { i.style.display = ''; i.classList.remove('sel'); });
slashMenu.querySelectorAll('.slash-section').forEach(function(s) { s.style.display = ''; });
}
function getVisibleSlash() { return slashItems.filter(function(i) { return i.style.display !== 'none'; }); }
function selectSlashAction(action) {
hideSlash();
// Handle QI-specific actions
if (action && action.indexOf('mermaid-') === 0) {
var type = action.replace('mermaid-', '');
var templates = {
flowchart: 'flowchart TD\n A[Start] --> B[Process] --> C[End]',
sequence: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!',
class: 'classDiagram\n class Animal {\n +name: string\n +eat()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog',
state: 'stateDiagram-v2\n [*] --> Idle\n Idle --> Processing\n Processing --> Done\n Done --> [*]',
er: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE_ITEM : contains',
gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Phase 1\n Task A :a1, 2026-01-01, 30d\n Task B :after a1, 20d',
pie: 'pie title Distribution\n "Category A" : 45\n "Category B" : 30\n "Category C" : 25'
};
var tpl = templates[type] || 'flowchart TD\n A --> B';
insertAtCursor('\n```mermaid\n' + tpl + '\n```\n', 12);
ta.focus(); return;
}
// Table: inline form (not prompt)
if (action === 'table') { showTableForm(); return; }
// Code block: language picker (not bare ```)
if (action === 'code') { showLangPicker(); return; }
if (action && action.indexOf('ai-') === 0) { execAIAction(action); ta.focus(); return; }
var fn = SLASH_INSERT[action];
if (fn) fn(); else if (action) showToast('Action: ' + action);
ta.focus();
}
// ---- Bubble menu ----
function showBubble() {
if (ta.selectionStart === ta.selectionEnd) { hideBubble(); return; }
// Position below selection END (where user finished selecting)
var pt = caretToPixel(ta.selectionEnd);
var wr = editorWrap.getBoundingClientRect();
var top = wr.top + pt.y + 4;
var left = wr.left + pt.x - 10;
// Clamp to viewport (never use textarea fallback)
if (top < 60) top = 60;
if (top + 40 > window.innerHeight - 10) top = window.innerHeight - 50;
if (left < 6) left = 6;
if (left + bubble.offsetWidth > window.innerWidth - 6) left = window.innerWidth - bubble.offsetWidth - 6;
bubble.style.left = left + 'px'; bubble.style.top = top + 'px';
bubble.classList.add('on'); bubbleMore.classList.remove('on');
}
function hideBubble() { bubble.classList.remove('on'); bubbleMore.classList.remove('on'); }
function execBubbleAction(action) {
if (action && action.indexOf('ai-') === 0) {
execAIAction(action);
} else {
switch(action) {
case 'bold': wrapSelection('**'); break;
case 'italic': wrapSelection('*'); break;
case 'strike': wrapSelection('~~'); break;
case 'code': wrapSelection('`'); break;
case 'highlight': wrapSelection('=='); break;
case 'link':
var sel = val().substring(ta.selectionStart, ta.selectionEnd);
var url = prompt('URL:', 'https://');
if (url) wrapSelection('[', '](' + url + ')');
break;
}
}
hideBubble(); ta.focus();
}
// ---- Table form (inline, no prompt) ----
var inlineForm = document.getElementById('inline-form');
var ifCols = document.getElementById('if-cols');
var ifRows = document.getElementById('if-rows');
function showTableForm() {
hideAllMenus();
ifCols.value = '3'; ifRows.value = '3';
var pt = caretToPixel(null);
var wr = editorWrap.getBoundingClientRect();
inlineForm.style.left = (wr.left + pt.x) + 'px';
inlineForm.style.top = (wr.top + pt.y + 8) + 'px';
inlineForm.classList.add('on');
ifCols.focus(); ifCols.select();
}
function insertTableFromForm() {
var cols = parseInt(ifCols.value) || 3;
var rows = parseInt(ifRows.value) || 3;
if (cols < 1) cols = 1; if (rows < 1) rows = 1;
var hdr = '|'; var sep = '|';
for (var c = 1; c <= cols; c++) { hdr += ' Col ' + c + ' |'; sep += '-------|'; }
var body = '';
for (var r = 1; r <= rows; r++) {
body += '\n|';
for (var cc = 1; cc <= cols; cc++) body += ' |';
}
insertAtCursor('\n' + hdr + '\n' + sep + body + '\n', 2);
inlineForm.classList.remove('on');
ta.focus();
}
document.getElementById('if-ok').addEventListener('click', insertTableFromForm);
document.getElementById('if-cancel').addEventListener('click', function() { inlineForm.classList.remove('on'); ta.focus(); });
ifCols.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); insertTableFromForm(); }
if (e.key === 'Escape') { e.preventDefault(); inlineForm.classList.remove('on'); ta.focus(); }
});
ifRows.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); insertTableFromForm(); }
if (e.key === 'Escape') { e.preventDefault(); inlineForm.classList.remove('on'); ta.focus(); }
});
// ---- Language picker (code block) ----
var langPicker = document.getElementById('lang-picker');
var LANGS = ['bash','javascript','typescript','python','java','rust','go','c','cpp','csharp','html','css','scss','json','yaml','toml','xml','sql','dockerfile','makefile','markdown','php','ruby','swift','kotlin','lua','r','perl','scala','elixir','haskell','clojure','erlang','ocaml','zig','nim','groovy','dart','graphql','protobuf','text'];
var langIdx = -1;
function showLangPicker() {
hideAllMenus();
langIdx = -1;
var html = '<div style="font-size:10px;font-weight:600;color:var(--text3);padding:4px 9px 2px;text-transform:uppercase">Langage</div>';
LANGS.forEach(function(l, i) {
html += '<div class="lang-item" data-idx="' + i + '">' + l + '</div>';
});
langPicker.innerHTML = html;
var pt = caretToPixel(null);
var wr = editorWrap.getBoundingClientRect();
langPicker.style.left = (wr.left + pt.x) + 'px';
langPicker.style.top = (wr.top + pt.y + 8) + 'px';
langPicker.classList.add('on');
// Click handler
langPicker.querySelectorAll('.lang-item').forEach(function(item) {
item.addEventListener('click', function() {
var idx = parseInt(this.getAttribute('data-idx'));
insertCodeBlock(LANGS[idx]);
});
});
}
function insertCodeBlock(lang) {
langPicker.classList.remove('on');
insertAtCursor('\n```' + lang + '\n\n```\n', 4 + lang.length);
ta.focus();
}
function hideAllMenus() {
inlineForm.classList.remove('on');
langPicker.classList.remove('on');
qiMenu.classList.remove('on');
qiIdx = -1; qiItems = [];
}
// Lang picker keyboard nav (document-level)
var _origKeydownQI = null; // will store reference — actually we handle in the existing document keydown
// Add lang picker nav to the existing document keydown handler
var _langItems = [];
document.addEventListener('keydown', function(e) {
if (!langPicker.classList.contains('on')) return;
_langItems = Array.from(langPicker.querySelectorAll('.lang-item'));
if (e.key === 'Escape') { e.preventDefault(); langPicker.classList.remove('on'); ta.focus(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); langIdx = (langIdx + 1) % _langItems.length; highlightLang(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); langIdx = (langIdx - 1 + _langItems.length) % _langItems.length; highlightLang(); }
else if (e.key === 'Enter') {
e.preventDefault();
if (langIdx >= 0 && langIdx < _langItems.length) {
var idx = parseInt(_langItems[langIdx].getAttribute('data-idx'));
insertCodeBlock(LANGS[idx]);
}
}
});
function highlightLang() {
_langItems.forEach(function(item, i) { item.classList.toggle('sel', i === langIdx); });
}
// ---- Quick Insert ----
function showQI() {
qiMenu.classList.remove('on');
qiIdx = -1; qiItems = [];
// Position below current line, left-aligned
var pt = caretToPixel(null);
pt.x = Math.max(pt.x - 10, 4);
pt.y += 2;
qiMenu.style.left = pt.x + 'px';
qiMenu.style.top = pt.y + 'px';
qiMenu.classList.add('on');
}
function toggleQI() {
if (qiMenu.classList.contains('on')) { qiMenu.classList.remove('on'); return; }
showQI();
}
// ---- AI Panel ----
function toggleAIPanel() {
aiPanel.classList.toggle('hidden');
btnAI.classList.toggle('active', !aiPanel.classList.contains('hidden'));
}
function sendAIChat() {
var input = document.getElementById('ai-input');
var chat = document.getElementById('ai-chat');
var text = input.value.trim();
if (!text) return;
input.value = '';
var um = document.createElement('div'); um.className = 'ai-msg user'; um.textContent = text;
chat.appendChild(um);
var lm = document.createElement('div'); lm.className = 'ai-msg loading';
lm.innerHTML = '<span class="spin"></span>Thinking...';
chat.appendChild(lm);
chat.scrollTop = chat.scrollHeight;
aiCall('improve', text, null, function(result) {
lm.remove();
var bm = document.createElement('div'); bm.className = 'ai-msg bot';
bm.textContent = result || '(no response)';
chat.appendChild(bm);
chat.scrollTop = chat.scrollHeight;
});
}
// ---- Drag & Drop ----
var dragC = 0;
document.addEventListener('dragenter', function(e) { e.preventDefault(); dragC++; if (dragC === 1) dropOverlay.classList.add('on'); });
document.addEventListener('dragleave', function(e) { e.preventDefault(); dragC--; if (dragC === 0) dropOverlay.classList.remove('on'); });
document.addEventListener('dragover', function(e) { e.preventDefault(); });
document.addEventListener('drop', function(e) {
e.preventDefault(); dragC = 0; dropOverlay.classList.remove('on');
var files = e.dataTransfer.files;
if (files.length > 0) {
var names = Array.from(files).map(function(f) { return f.name; }).join(', ');
showToast('Dropped: ' + names + ' (upload not implemented)', 'ok');
}
});
// ---- Stats ----
function updateStats() {
var v = val(); var words = v.trim() ? v.trim().split(/\s+/).length : 0;
statWords.textContent = words + ' words | ' + v.length + ' chars';
}
// ---- Toast ----
var toastTimer = null;
function showToast(msg, type) {
toast.textContent = msg; toast.className = 'toast on ' + (type || '');
clearTimeout(toastTimer);
toastTimer = setTimeout(function() { toast.classList.remove('on'); }, 2500);
}
// ---- Event handlers ----
ta.addEventListener('keydown', function(e) {
// Slash menu trigger: type / at start of line
if (e.key === '/') {
var l = getLine(); var p = getPos();
var before = l.text.substring(0, p.s - l.start);
if (before === '' || /^\s*$/.test(before)) setTimeout(showSlash, 20);
}
if (e.ctrlKey && e.key === 'j') { e.preventDefault(); toggleAIPanel(); }
if (e.ctrlKey && e.key === 's') { e.preventDefault(); forceSave(); }
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrapSelection('*'); }
// Quick Insert: Alt+I
if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); }
});
// Slash menu keyboard nav (bound to search input which has focus)
slashSearch.addEventListener('keydown', function(e) {
if (!slashVisible) return;
if (e.key === 'Escape') { e.preventDefault(); hideSlash(); ta.focus(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); navSlash(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); navSlash(-1); }
else if (e.key === 'Enter') {
e.preventDefault();
var vis = getVisibleSlash();
if (slashIdx >= 0 && slashIdx < vis.length) selectSlashAction(vis[slashIdx].getAttribute('data-action'));
}
});
// Quick Insert keyboard nav (document-level since no input to focus)
var qiIdx = -1;
var qiItems = [];
document.addEventListener('keydown', function(e) {
if (!qiMenu.classList.contains('on')) return;
qiItems = Array.from(qiMenu.querySelectorAll('.qi-item')); // refresh each time
if (e.key === 'Escape') { e.preventDefault(); qiMenu.classList.remove('on'); qiIdx = -1; ta.focus(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); qiIdx = (qiIdx + 1) % qiItems.length; highlightQI(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); qiIdx = (qiIdx - 1 + qiItems.length) % qiItems.length; highlightQI(); }
else if (e.key === 'Enter') {
e.preventDefault();
if (qiIdx >= 0 && qiIdx < qiItems.length) {
var action = qiItems[qiIdx].getAttribute('data-qi');
qiMenu.classList.remove('on');
selectSlashAction(action);
}
}
});
function highlightQI() {
qiItems.forEach(function(item, i) { item.style.background = i === qiIdx ? 'var(--bg-hover)' : ''; item.style.color = i === qiIdx ? 'var(--text)' : ''; });
}
ta.addEventListener('keyup', function(e) {
if (e.key === '/' && slashVisible) {
var l = getLine(); var p = getPos();
var before = l.text.substring(0, p.s - l.start);
if (!/^\/\S*$/.test(before)) hideSlash();
}
});
ta.addEventListener('input', function() {
updateStats(); markDirty();
autoHeight();
});
// ---- Auto-height textarea ----
function autoHeight() {
ta.style.height = 'auto';
ta.style.height = ta.scrollHeight + 'px';
}
ta.addEventListener('mouseup', function() { setTimeout(showBubble, 50); });
// Bubble follows cursor on any selection change (keyboard too)
document.addEventListener('selectionchange', function() {
if (document.activeElement !== ta) return;
clearTimeout(window._bubbleTimer);
window._bubbleTimer = setTimeout(showBubble, 80);
});
slashSearch.addEventListener('input', function() { filterSlash(slashSearch.value); });
slashMenu.addEventListener('click', function(e) {
var item = e.target.closest('.slash-item');
if (item) selectSlashAction(item.getAttribute('data-action'));
});
function navSlash(dir) {
var vis = getVisibleSlash(); if (vis.length === 0) return;
slashIdx += dir;
if (slashIdx < 0) slashIdx = vis.length - 1;
if (slashIdx >= vis.length) slashIdx = 0;
vis.forEach(function(i, idx) { i.classList.toggle('sel', idx === slashIdx); });
}
bubble.addEventListener('click', function(e) {
var btn = e.target.closest('.bubble-btn'); if (!btn) return;
e.preventDefault(); e.stopPropagation();
if (btn.id === 'bubble-more') { bubbleMore.classList.toggle('on'); return; }
execBubbleAction(btn.getAttribute('data-action'));
});
bubbleMore.addEventListener('click', function(e) {
var item = e.target.closest('.bubble-more-item'); if (!item) return;
e.stopPropagation(); execBubbleAction(item.getAttribute('data-action'));
});
document.getElementById('qi-btn').addEventListener('click', function(e) { e.stopPropagation(); toggleQI(); });
qiMenu.addEventListener('click', function(e) {
var item = e.target.closest('.qi-item'); if (!item) return;
var action = item.getAttribute('data-qi');
qiMenu.classList.remove('on');
selectSlashAction(action);
});
document.addEventListener('click', function(e) {
if (slashVisible && !slashMenu.contains(e.target) && e.target !== ta) hideSlash();
if (bubble.classList.contains('on') && !bubble.contains(e.target)) hideBubble();
if (qiMenu.classList.contains('on') && !qiMenu.contains(e.target) && e.target.id !== 'qi-btn') { qiMenu.classList.remove('on'); qiIdx = -1; qiItems = []; }
if (inlineForm.classList.contains('on') && !inlineForm.contains(e.target)) { inlineForm.classList.remove('on'); ta.focus(); }
if (langPicker.classList.contains('on') && !langPicker.contains(e.target)) { langPicker.classList.remove('on'); ta.focus(); }
});
btnAI.addEventListener('click', toggleAIPanel);
document.getElementById('ai-close').addEventListener('click', toggleAIPanel);
document.getElementById('ai-send').addEventListener('click', sendAIChat);
document.getElementById('ai-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') sendAIChat(); });
document.querySelectorAll('.ai-sugg').forEach(function(b) {
b.addEventListener('click', function() {
document.getElementById('ai-input').value = b.textContent.trim();
sendAIChat();
});
});
btnPreview.addEventListener('click', togglePreview);
document.getElementById('poc-title').addEventListener('input', function() { markDirty(); });
// ---- Init ----
initSlashItems();
loadFile();
checkAIStatus();
updateStats();
console.log('ObsiGate Editor v1 ready');
console.log(' File: ' + (fileVault ? fileVault + '/' + filePath : 'demo mode'));
console.log(' AI: checking...');
console.log(' Type / for commands | Select text for bubble | Ctrl+J AI | Ctrl+S save');
})();
</script>
</body>
</html>