slides-grab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/AGENTS.md +80 -0
  2. package/LICENSE +21 -0
  3. package/PROGRESS.md +39 -0
  4. package/README.md +120 -0
  5. package/SETUP.md +51 -0
  6. package/bin/ppt-agent.js +204 -0
  7. package/convert.cjs +184 -0
  8. package/package.json +51 -0
  9. package/prd.json +135 -0
  10. package/prd.md +104 -0
  11. package/scripts/editor-server.js +779 -0
  12. package/scripts/html2pdf.js +217 -0
  13. package/scripts/validate-slides.js +416 -0
  14. package/skills/ppt-design-skill/SKILL.md +38 -0
  15. package/skills/ppt-plan-skill/SKILL.md +37 -0
  16. package/skills/ppt-pptx-skill/SKILL.md +37 -0
  17. package/skills/ppt-presentation-skill/SKILL.md +57 -0
  18. package/src/editor/codex-edit.js +213 -0
  19. package/src/editor/editor.html +1733 -0
  20. package/src/editor/js/editor-bbox.js +332 -0
  21. package/src/editor/js/editor-chat.js +56 -0
  22. package/src/editor/js/editor-direct-edit.js +110 -0
  23. package/src/editor/js/editor-dom.js +55 -0
  24. package/src/editor/js/editor-init.js +284 -0
  25. package/src/editor/js/editor-navigation.js +54 -0
  26. package/src/editor/js/editor-select.js +264 -0
  27. package/src/editor/js/editor-send.js +157 -0
  28. package/src/editor/js/editor-sse.js +163 -0
  29. package/src/editor/js/editor-state.js +32 -0
  30. package/src/editor/js/editor-utils.js +167 -0
  31. package/src/editor/screenshot.js +73 -0
  32. package/src/resolve.js +159 -0
  33. package/templates/chart.html +121 -0
  34. package/templates/closing.html +54 -0
  35. package/templates/content.html +50 -0
  36. package/templates/contents.html +60 -0
  37. package/templates/cover.html +64 -0
  38. package/templates/custom/.gitkeep +0 -0
  39. package/templates/custom/README.md +7 -0
  40. package/templates/diagram.html +98 -0
  41. package/templates/quote.html +31 -0
  42. package/templates/section-divider.html +43 -0
  43. package/templates/split-layout.html +41 -0
  44. package/templates/statistics.html +55 -0
  45. package/templates/team.html +49 -0
  46. package/templates/timeline.html +59 -0
  47. package/themes/corporate.css +8 -0
  48. package/themes/executive.css +10 -0
  49. package/themes/modern-dark.css +9 -0
  50. package/themes/sage.css +9 -0
  51. package/themes/warm.css +8 -0
@@ -0,0 +1,332 @@
1
+ // editor-bbox.js — BBox drawing, rendering, XPath, target extraction
2
+
3
+ import { state, SLIDE_W, SLIDE_H, TOOL_MODE_DRAW } from './editor-state.js';
4
+ import {
5
+ slideIframe, slideStage, slideWrapper, bboxLayer, drawLayer, drawBox,
6
+ bboxCountEl, contextChipList, sessionFileChip,
7
+ } from './editor-dom.js';
8
+ import {
9
+ currentSlideFile, getSlideState, normalizeBoxStatus, escapeHtml,
10
+ randomId, setStatus, clamp,
11
+ } from './editor-utils.js';
12
+
13
+ // Late-binding callback for updateSendState (avoids circular dependency)
14
+ let _onBboxChange = () => {};
15
+ export function onBboxChange(fn) { _onBboxChange = fn; }
16
+
17
+ export function scaleSlide() {
18
+ const padX = 12;
19
+ const padY = 12;
20
+ const availW = slideStage.clientWidth - padX;
21
+ const availH = slideStage.clientHeight - padY;
22
+ if (availW <= 0 || availH <= 0) return;
23
+
24
+ const scale = Math.min(availW / SLIDE_W, availH / SLIDE_H, 1);
25
+ slideWrapper.style.transform = `scale(${scale})`;
26
+ }
27
+
28
+ export function renderContextChips() {
29
+ if (!contextChipList) return;
30
+ const slide = currentSlideFile();
31
+ if (!slide) {
32
+ contextChipList.innerHTML = '';
33
+ if (sessionFileChip) sessionFileChip.textContent = '--';
34
+ return;
35
+ }
36
+
37
+ if (sessionFileChip) sessionFileChip.textContent = slide;
38
+
39
+ const ss = getSlideState(slide);
40
+ if (!Array.isArray(ss.boxes) || ss.boxes.length === 0) {
41
+ contextChipList.innerHTML = '<span class="context-chip-empty">No bbox selected</span>';
42
+ return;
43
+ }
44
+
45
+ contextChipList.innerHTML = ss.boxes
46
+ .map((box, index) => {
47
+ const statusClass = normalizeBoxStatus(box.status);
48
+ const selectedClass = box.id === ss.selectedBoxId ? 'selected' : '';
49
+ return [
50
+ `<button class="context-chip ${statusClass} ${selectedClass}" data-chip-box-id="${escapeHtml(box.id)}" title="Focus bbox #${index + 1}">`,
51
+ `#${index + 1}`,
52
+ '</button>',
53
+ ].join('');
54
+ })
55
+ .join('');
56
+ }
57
+
58
+ export function renderBboxes() {
59
+ const slide = currentSlideFile();
60
+ if (!slide) {
61
+ bboxLayer.innerHTML = '';
62
+ bboxCountEl.textContent = '0 pending \u00b7 0 review';
63
+ renderContextChips();
64
+ return;
65
+ }
66
+
67
+ const ss = getSlideState(slide);
68
+ bboxLayer.innerHTML = ss.boxes
69
+ .map((box, index) => {
70
+ const statusClass = normalizeBoxStatus(box.status);
71
+ const selectedClass = box.id === ss.selectedBoxId ? 'selected' : '';
72
+ return [
73
+ `<div class="bbox-item ${statusClass} ${selectedClass}" data-box-id="${escapeHtml(box.id)}" style="left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;">`,
74
+ `<div class="bbox-index">${index + 1}</div>`,
75
+ `<div class="bbox-actions">`,
76
+ `<button class="bbox-action-btn bbox-delete" data-box-delete="${escapeHtml(box.id)}" title="Delete bbox">\u00d7</button>`,
77
+ `<button class="bbox-action-btn bbox-check" data-box-check="${escapeHtml(box.id)}" title="Confirm bbox">Check</button>`,
78
+ `<button class="bbox-action-btn bbox-rerun" data-box-rerun="${escapeHtml(box.id)}" title="Mark bbox pending">Rerun</button>`,
79
+ `</div>`,
80
+ `</div>`,
81
+ ].join('');
82
+ })
83
+ .join('');
84
+ bboxLayer.classList.toggle('inert', state.toolMode !== TOOL_MODE_DRAW);
85
+
86
+ const pendingCount = ss.boxes.filter((box) => normalizeBoxStatus(box.status) === 'pending').length;
87
+ const reviewCount = ss.boxes.length - pendingCount;
88
+ bboxCountEl.textContent = `${pendingCount} pending \u00b7 ${reviewCount} review`;
89
+ renderContextChips();
90
+ _onBboxChange();
91
+ }
92
+
93
+ export function clientToSlidePoint(clientX, clientY) {
94
+ const rect = drawLayer.getBoundingClientRect();
95
+ const relX = clamp((clientX - rect.left) / rect.width, 0, 1);
96
+ const relY = clamp((clientY - rect.top) / rect.height, 0, 1);
97
+ return {
98
+ x: Math.round(relX * SLIDE_W),
99
+ y: Math.round(relY * SLIDE_H),
100
+ };
101
+ }
102
+
103
+ export function startDrawing(event) {
104
+ if (state.toolMode !== TOOL_MODE_DRAW) return;
105
+ if (event.button !== 0) return;
106
+ event.preventDefault();
107
+
108
+ state.drawing = true;
109
+ state.drawStart = clientToSlidePoint(event.clientX, event.clientY);
110
+ drawBox.style.display = 'block';
111
+ drawBox.style.left = `${state.drawStart.x}px`;
112
+ drawBox.style.top = `${state.drawStart.y}px`;
113
+ drawBox.style.width = '1px';
114
+ drawBox.style.height = '1px';
115
+ }
116
+
117
+ export function moveDrawing(event) {
118
+ if (state.toolMode !== TOOL_MODE_DRAW) return;
119
+ if (!state.drawing || !state.drawStart) return;
120
+ const current = clientToSlidePoint(event.clientX, event.clientY);
121
+
122
+ const x = Math.min(state.drawStart.x, current.x);
123
+ const y = Math.min(state.drawStart.y, current.y);
124
+ const width = Math.max(1, Math.abs(current.x - state.drawStart.x));
125
+ const height = Math.max(1, Math.abs(current.y - state.drawStart.y));
126
+
127
+ drawBox.style.left = `${x}px`;
128
+ drawBox.style.top = `${y}px`;
129
+ drawBox.style.width = `${width}px`;
130
+ drawBox.style.height = `${height}px`;
131
+ }
132
+
133
+ export function endDrawing(event) {
134
+ if (state.toolMode !== TOOL_MODE_DRAW) return;
135
+ if (!state.drawing || !state.drawStart) return;
136
+
137
+ const slide = currentSlideFile();
138
+ if (!slide) return;
139
+
140
+ const current = clientToSlidePoint(event.clientX, event.clientY);
141
+
142
+ const x = Math.min(state.drawStart.x, current.x);
143
+ const y = Math.min(state.drawStart.y, current.y);
144
+ const width = Math.max(1, Math.abs(current.x - state.drawStart.x));
145
+ const height = Math.max(1, Math.abs(current.y - state.drawStart.y));
146
+
147
+ drawBox.style.display = 'none';
148
+ state.drawing = false;
149
+ state.drawStart = null;
150
+
151
+ if (width < 3 || height < 3) return;
152
+
153
+ const ss = getSlideState(slide);
154
+ const newBox = {
155
+ id: randomId('bbox'),
156
+ x,
157
+ y,
158
+ width,
159
+ height,
160
+ status: 'pending',
161
+ };
162
+
163
+ ss.boxes.push(newBox);
164
+ ss.selectedBoxId = newBox.id;
165
+ renderBboxes();
166
+ setStatus(`Added bbox #${ss.boxes.length} for ${slide}.`);
167
+ }
168
+
169
+ export function clearBboxesForCurrentSlide() {
170
+ const slide = currentSlideFile();
171
+ if (!slide) return;
172
+
173
+ const ss = getSlideState(slide);
174
+ ss.boxes = [];
175
+ ss.selectedBoxId = null;
176
+ renderBboxes();
177
+ setStatus('All bounding boxes cleared for current slide.');
178
+ }
179
+
180
+ // XPath extraction
181
+ export function getXPath(el) {
182
+ if (!el || el.nodeType !== Node.ELEMENT_NODE) return '';
183
+
184
+ const segments = [];
185
+ let node = el;
186
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
187
+ const tag = node.tagName.toLowerCase();
188
+
189
+ let index = 1;
190
+ let sibling = node.previousElementSibling;
191
+ while (sibling) {
192
+ if (sibling.tagName === node.tagName) index += 1;
193
+ sibling = sibling.previousElementSibling;
194
+ }
195
+
196
+ segments.unshift(`${tag}[${index}]`);
197
+ node = node.parentElement;
198
+ }
199
+
200
+ return `/${segments.join('/')}`;
201
+ }
202
+
203
+ export function intersectArea(a, b) {
204
+ const left = Math.max(a.x, b.x);
205
+ const top = Math.max(a.y, b.y);
206
+ const right = Math.min(a.x + a.width, b.x + b.width);
207
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
208
+ if (right <= left || bottom <= top) return 0;
209
+ return (right - left) * (bottom - top);
210
+ }
211
+
212
+ export function extractTargetsForBox(box) {
213
+ const doc = slideIframe.contentDocument;
214
+ if (!doc || !doc.body) return [];
215
+
216
+ const all = Array.from(doc.body.querySelectorAll('*'));
217
+ const candidates = [];
218
+
219
+ for (const el of all) {
220
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
221
+ if (!tag || ['script', 'style', 'link', 'meta', 'noscript'].includes(tag)) continue;
222
+
223
+ const rect = el.getBoundingClientRect();
224
+ if (!rect || rect.width <= 1 || rect.height <= 1) continue;
225
+
226
+ const targetBox = {
227
+ x: rect.left,
228
+ y: rect.top,
229
+ width: rect.width,
230
+ height: rect.height,
231
+ };
232
+
233
+ const area = intersectArea(box, targetBox);
234
+ if (area <= 0) continue;
235
+
236
+ const xpath = getXPath(el);
237
+ if (!xpath) continue;
238
+
239
+ candidates.push({
240
+ xpath,
241
+ tag,
242
+ text: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 140),
243
+ area,
244
+ });
245
+ }
246
+
247
+ candidates.sort((a, b) => b.area - a.area);
248
+
249
+ const dedup = [];
250
+ const seen = new Set();
251
+ for (const item of candidates) {
252
+ if (seen.has(item.xpath)) continue;
253
+ seen.add(item.xpath);
254
+ dedup.push({ xpath: item.xpath, tag: item.tag, text: item.text });
255
+ if (dedup.length >= 12) break;
256
+ }
257
+
258
+ return dedup;
259
+ }
260
+
261
+ // Bbox layer click handler
262
+ export function initBboxLayerEvents() {
263
+ bboxLayer.addEventListener('click', (event) => {
264
+ if (state.toolMode !== TOOL_MODE_DRAW) return;
265
+ const slide = currentSlideFile();
266
+ if (!slide) return;
267
+
268
+ const ss = getSlideState(slide);
269
+ const checkButton = event.target.closest('[data-box-check]');
270
+ if (checkButton) {
271
+ const boxId = checkButton.getAttribute('data-box-check');
272
+ ss.boxes = ss.boxes.filter((box) => box.id !== boxId);
273
+ if (ss.selectedBoxId === boxId) ss.selectedBoxId = null;
274
+ renderBboxes();
275
+ setStatus('BBox confirmed and cleared.');
276
+ event.stopPropagation();
277
+ return;
278
+ }
279
+
280
+ const rerunButton = event.target.closest('[data-box-rerun]');
281
+ if (rerunButton) {
282
+ const boxId = rerunButton.getAttribute('data-box-rerun');
283
+ const box = ss.boxes.find((item) => item.id === boxId);
284
+ if (box) box.status = 'pending';
285
+ ss.selectedBoxId = boxId;
286
+ renderBboxes();
287
+ setStatus('BBox moved back to pending (red).');
288
+ event.stopPropagation();
289
+ return;
290
+ }
291
+
292
+ const deleteButton = event.target.closest('[data-box-delete]');
293
+ if (deleteButton) {
294
+ const boxId = deleteButton.getAttribute('data-box-delete');
295
+ ss.boxes = ss.boxes.filter((box) => box.id !== boxId);
296
+ if (ss.selectedBoxId === boxId) ss.selectedBoxId = null;
297
+ renderBboxes();
298
+ setStatus('Bounding box deleted.');
299
+ event.stopPropagation();
300
+ return;
301
+ }
302
+
303
+ const boxEl = event.target.closest('[data-box-id]');
304
+ if (boxEl) {
305
+ ss.selectedBoxId = boxEl.getAttribute('data-box-id');
306
+ renderBboxes();
307
+ return;
308
+ }
309
+
310
+ ss.selectedBoxId = null;
311
+ renderBboxes();
312
+ });
313
+
314
+ if (contextChipList) {
315
+ contextChipList.addEventListener('click', (event) => {
316
+ const chip = event.target.closest('[data-chip-box-id]');
317
+ if (!chip) return;
318
+
319
+ const slide = currentSlideFile();
320
+ if (!slide) return;
321
+
322
+ const boxId = chip.getAttribute('data-chip-box-id');
323
+ const ss = getSlideState(slide);
324
+ const boxExists = ss.boxes.some((box) => box.id === boxId);
325
+ if (!boxExists) return;
326
+
327
+ ss.selectedBoxId = boxId;
328
+ renderBboxes();
329
+ setStatus(`Selected ${slide} region.`);
330
+ });
331
+ }
332
+ }
@@ -0,0 +1,56 @@
1
+ // editor-chat.js — Chat messages and runs list UI
2
+
3
+ import { chatMessagesEl } from './editor-dom.js';
4
+ import { currentSlideFile, getSlideState, randomId, escapeHtml, formatTime } from './editor-utils.js';
5
+
6
+ export function addChatMessage(kind, text, slide = currentSlideFile()) {
7
+ if (!slide) return;
8
+
9
+ const state = getSlideState(slide);
10
+ state.messages.push({
11
+ id: randomId('msg'),
12
+ kind,
13
+ text,
14
+ at: new Date().toISOString(),
15
+ });
16
+
17
+ while (state.messages.length > 80) {
18
+ state.messages.shift();
19
+ }
20
+
21
+ if (slide === currentSlideFile()) {
22
+ renderChatMessages();
23
+ }
24
+ }
25
+
26
+ export function renderChatMessages() {
27
+ if (!chatMessagesEl) return;
28
+ const slide = currentSlideFile();
29
+ if (!slide) {
30
+ chatMessagesEl.innerHTML = '';
31
+ return;
32
+ }
33
+
34
+ const state = getSlideState(slide);
35
+ const messages = Array.isArray(state.messages) ? state.messages : [];
36
+ if (messages.length === 0) {
37
+ chatMessagesEl.innerHTML = '';
38
+ return;
39
+ }
40
+
41
+ chatMessagesEl.innerHTML = messages
42
+ .map((msg) => {
43
+ const time = formatTime(msg.at);
44
+ return [
45
+ `<div class="message ${msg.kind}">`,
46
+ `${escapeHtml(msg.text)}`,
47
+ `<div class="run-meta" style="margin-top:6px;">${time}</div>`,
48
+ '</div>',
49
+ ].join('');
50
+ })
51
+ .join('');
52
+
53
+ chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
54
+ }
55
+
56
+ export function renderRunsList() {}
@@ -0,0 +1,110 @@
1
+ // editor-direct-edit.js — Style changes, direct save (debounced)
2
+
3
+ import { localFileUpdateBySlide } from './editor-state.js';
4
+ import { slideIframe } from './editor-dom.js';
5
+ import { currentSlideFile, getDirectSaveState, setStatus } from './editor-utils.js';
6
+ import { addChatMessage } from './editor-chat.js';
7
+ import { getSelectedObjectElement, renderObjectSelection, updateObjectEditorControls, readSelectedObjectStyleState } from './editor-select.js';
8
+
9
+ export function serializeSlideDocument(doc) {
10
+ if (!doc?.documentElement) return '';
11
+ const doctype = doc.doctype ? `<!DOCTYPE ${doc.doctype.name}>` : '<!DOCTYPE html>';
12
+ return `${doctype}\n${doc.documentElement.outerHTML}`;
13
+ }
14
+
15
+ async function persistDirectSlideHtml(slide, html, message) {
16
+ if (!slide || !html) return;
17
+
18
+ try {
19
+ const res = await fetch(`/api/slides/${encodeURIComponent(slide)}/save`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ slide, html }),
23
+ });
24
+ const data = await res.json().catch(() => ({}));
25
+
26
+ if (!res.ok) {
27
+ throw new Error(data.error || `Save failed with HTTP ${res.status}`);
28
+ }
29
+
30
+ localFileUpdateBySlide.set(slide, Date.now());
31
+ if (slide === currentSlideFile()) {
32
+ setStatus(message || `${slide} saved.`);
33
+ }
34
+ } catch (error) {
35
+ addChatMessage('error', `[${slide}] Direct edit save failed: ${error.message}`, slide);
36
+ setStatus(`Error: ${error.message}`);
37
+ }
38
+ }
39
+
40
+ function queueDirectSave(slide, html, message) {
41
+ const saveState = getDirectSaveState(slide);
42
+ if (!html) return saveState.chain;
43
+ saveState.chain = saveState.chain
44
+ .catch(() => {})
45
+ .then(() => persistDirectSlideHtml(slide, html, message));
46
+ return saveState.chain;
47
+ }
48
+
49
+ export function scheduleDirectSave(delay = 0, message = 'Object updated and saved.') {
50
+ const slide = currentSlideFile();
51
+ const html = serializeSlideDocument(slideIframe.contentDocument);
52
+ if (!slide || !html) return;
53
+
54
+ const saveState = getDirectSaveState(slide);
55
+ saveState.pendingHtml = html;
56
+ saveState.pendingMessage = message;
57
+ if (saveState.timer) {
58
+ window.clearTimeout(saveState.timer);
59
+ }
60
+ saveState.timer = window.setTimeout(() => {
61
+ saveState.timer = null;
62
+ const nextHtml = saveState.pendingHtml;
63
+ const nextMessage = saveState.pendingMessage;
64
+ saveState.pendingHtml = '';
65
+ queueDirectSave(slide, nextHtml, nextMessage);
66
+ }, Math.max(0, delay));
67
+ }
68
+
69
+ export async function flushDirectSaveForSlide(slide) {
70
+ if (!slide) return;
71
+
72
+ const saveState = getDirectSaveState(slide);
73
+ if (saveState.timer) {
74
+ window.clearTimeout(saveState.timer);
75
+ saveState.timer = null;
76
+ const html = saveState.pendingHtml;
77
+ const message = saveState.pendingMessage;
78
+ saveState.pendingHtml = '';
79
+ await queueDirectSave(slide, html, message);
80
+ return;
81
+ }
82
+
83
+ await saveState.chain.catch(() => {});
84
+ }
85
+
86
+ export function applyTextDecorationToken(el, token, shouldEnable) {
87
+ const frameWindow = slideIframe.contentWindow;
88
+ const styles = frameWindow?.getComputedStyle ? frameWindow.getComputedStyle(el) : null;
89
+ const parts = new Set(
90
+ String(styles?.textDecorationLine || '')
91
+ .split(/\s+/)
92
+ .filter((part) => part === 'underline' || part === 'line-through'),
93
+ );
94
+ if (shouldEnable) {
95
+ parts.add(token);
96
+ } else {
97
+ parts.delete(token);
98
+ }
99
+ el.style.textDecorationLine = parts.size > 0 ? Array.from(parts).join(' ') : 'none';
100
+ }
101
+
102
+ export function mutateSelectedObject(mutator, message, { delay = 0, preserveTextInput = false } = {}) {
103
+ const selected = getSelectedObjectElement();
104
+ if (!selected) return;
105
+ mutator(selected);
106
+ renderObjectSelection();
107
+ updateObjectEditorControls({ preserveTextInput });
108
+ scheduleDirectSave(delay, message);
109
+ setStatus('Saving direct edit...');
110
+ }
@@ -0,0 +1,55 @@
1
+ // editor-dom.js — DOM element references
2
+
3
+ const $ = (sel) => document.querySelector(sel);
4
+
5
+ export const btnPrev = $('#btn-prev');
6
+ export const btnNext = $('#btn-next');
7
+ export const slideCounter = $('#slide-counter');
8
+ export const slideStatusChip = $('#slide-status-chip');
9
+
10
+ export const slideIframe = $('#slide-iframe');
11
+ export const slidePanel = $('#slide-panel');
12
+ export const slideStage = $('#slide-stage');
13
+ export const slideWrapper = $('#slide-wrapper');
14
+ export const bboxLayer = $('#bbox-layer');
15
+ export const objectLayer = $('#object-layer');
16
+ export const objectHoverBox = $('#object-hover-box');
17
+ export const objectSelectedBox = $('#object-selected-box');
18
+ export const drawLayer = $('#draw-layer');
19
+ export const drawBox = $('#draw-box');
20
+ export const toolModeDrawBtn = $('#tool-mode-draw');
21
+ export const toolModeSelectBtn = $('#tool-mode-select');
22
+ export const bboxToolbar = $('#bbox-toolbar');
23
+ export const selectToolbar = $('#select-toolbar');
24
+
25
+ export const toggleBold = $('#toggle-bold');
26
+ export const toggleItalic = $('#toggle-italic');
27
+ export const toggleUnderline = $('#toggle-underline');
28
+ export const toggleStrike = $('#toggle-strike');
29
+ export const alignLeft = $('#align-left');
30
+ export const alignCenter = $('#align-center');
31
+ export const alignRight = $('#align-right');
32
+ export const popoverTextInput = $('#popover-text-input');
33
+ export const popoverApplyText = $('#popover-apply-text');
34
+ export const popoverTextColorInput = $('#popover-text-color-input');
35
+ export const popoverBgColorInput = $('#popover-bg-color-input');
36
+ export const popoverSizeInput = $('#popover-size-input');
37
+ export const popoverApplySize = $('#popover-apply-size');
38
+
39
+ export const chatMessagesEl = $('#chat-messages');
40
+ export const sessionFileChip = $('#session-file-chip');
41
+ export const contextChipList = $('#context-chip-list');
42
+ export const promptInput = $('#prompt-input');
43
+ export const modelSelect = $('#model-select');
44
+ export const btnClearBboxes = $('#btn-clear-bboxes');
45
+ export const bboxCountEl = $('#bbox-count');
46
+ export const btnSend = $('#btn-send');
47
+ export const editorHint = $('#editor-hint');
48
+ export const selectedObjectMini = $('#selected-object-mini');
49
+ export const miniTag = $('#mini-tag');
50
+ export const miniText = $('#mini-text');
51
+ export const selectEmptyHint = $('#select-empty-hint');
52
+
53
+ export const statusDot = $('#status-dot');
54
+ export const statusConn = $('#status-connection');
55
+ export const statusMsg = $('#status-message');