@sequent-org/moodboard 1.4.26 → 1.4.28

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 (30) hide show
  1. package/package.json +2 -1
  2. package/src/assets/fonts/geist/geist-sans-latin-100-normal.ttf +0 -0
  3. package/src/assets/fonts/geist/geist-sans-latin-200-normal.ttf +0 -0
  4. package/src/assets/fonts/geist/geist-sans-latin-300-normal.ttf +0 -0
  5. package/src/assets/fonts/geist/geist-sans-latin-400-normal.ttf +0 -0
  6. package/src/assets/fonts/geist/geist-sans-latin-500-normal.ttf +0 -0
  7. package/src/assets/fonts/geist/geist-sans-latin-600-normal.ttf +0 -0
  8. package/src/assets/fonts/geist/geist-sans-latin-700-normal.ttf +0 -0
  9. package/src/assets/fonts/geist/geist-sans-latin-800-normal.ttf +0 -0
  10. package/src/assets/fonts/geist/geist-sans-latin-900-normal.ttf +0 -0
  11. package/src/core/SaveManager.js +6 -0
  12. package/src/core/flows/ClipboardFlow.js +16 -6
  13. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +10 -0
  14. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +3 -0
  15. package/src/services/ai/AiClient.js +224 -0
  16. package/src/services/ai/ChatHistoryStore.js +55 -0
  17. package/src/services/ai/ChatPresets.js +40 -0
  18. package/src/services/ai/ChatSessionController.js +220 -0
  19. package/src/ui/chat/ChatComposer.js +198 -0
  20. package/src/ui/chat/ChatExtendedPromptModal.js +131 -0
  21. package/src/ui/chat/ChatMessageList.js +92 -0
  22. package/src/ui/chat/ChatPillMenu.js +141 -0
  23. package/src/ui/chat/ChatSettingsPopup.js +171 -0
  24. package/src/ui/chat/ChatWindow.js +407 -0
  25. package/src/ui/chat/ChatWindowRenderer.js +214 -0
  26. package/src/ui/chat/icons.js +113 -0
  27. package/src/ui/styles/chat.css +920 -0
  28. package/src/ui/styles/index.css +1 -0
  29. package/src/ui/styles/workspace.css +73 -1
  30. package/src/utils/styleLoader.js +2 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Универсальный «пилл с выпадающим меню».
3
+ *
4
+ * Используется и для переключателя моделей (Yandex/DeepSeek),
5
+ * и для пресетов промпта (Default/Design helper/...).
6
+ *
7
+ * Одна ответственность: отрисовать список опций по triggerEl,
8
+ * показывать/скрывать меню, диспатчить onSelect(id).
9
+ *
10
+ * Не знает, какие именно опции — получает их извне.
11
+ */
12
+
13
+ export class ChatPillMenu {
14
+ /**
15
+ * @param {{ trigger: HTMLElement, menu: HTMLElement, label: HTMLElement, icon?: HTMLElement }} refs
16
+ * @param {{ onSelect: (id: string) => void, getOptions: () => Array<{id: string, label: string, enabled?: boolean, hint?: string, description?: string, icon?: string}>, getActiveId: () => string }} handlers
17
+ */
18
+ constructor(refs, handlers) {
19
+ this._trigger = refs.trigger;
20
+ this._menu = refs.menu;
21
+ this._label = refs.label;
22
+ this._icon = refs.icon || null;
23
+ this._handlers = handlers;
24
+ this._listeners = [];
25
+ this._isOpen = false;
26
+ this._docClickHandler = null;
27
+ }
28
+
29
+ attach() {
30
+ this._on(this._trigger, 'click', (e) => {
31
+ e.stopPropagation();
32
+ this.toggle();
33
+ });
34
+ this._on(this._menu, 'click', (e) => {
35
+ const item = e.target.closest('[data-option-id]');
36
+ if (!item || item.hasAttribute('disabled')) return;
37
+ const id = item.getAttribute('data-option-id');
38
+ this.close();
39
+ this._handlers.onSelect?.(id);
40
+ });
41
+ }
42
+
43
+ refresh() {
44
+ const options = this._handlers.getOptions?.() || [];
45
+ const activeId = this._handlers.getActiveId?.();
46
+
47
+ const fragment = document.createDocumentFragment();
48
+ for (const opt of options) {
49
+ const btn = document.createElement('button');
50
+ btn.type = 'button';
51
+ btn.className = 'moodboard-chat__menu-item';
52
+ if (opt.icon || opt.description) {
53
+ btn.classList.add('moodboard-chat__menu-item--rich');
54
+ }
55
+ btn.setAttribute('data-option-id', opt.id);
56
+ btn.setAttribute('role', 'menuitemradio');
57
+ if (opt.enabled === false) btn.setAttribute('disabled', '');
58
+ if (opt.id === activeId) btn.setAttribute('data-active', 'true');
59
+
60
+ if (opt.icon) {
61
+ const iconSpan = document.createElement('span');
62
+ iconSpan.className = 'moodboard-chat__menu-item-icon';
63
+ iconSpan.innerHTML = opt.icon;
64
+ btn.appendChild(iconSpan);
65
+ }
66
+
67
+ const textWrap = document.createElement('span');
68
+ textWrap.className = 'moodboard-chat__menu-item-text';
69
+
70
+ const labelSpan = document.createElement('span');
71
+ labelSpan.className = 'moodboard-chat__menu-item-label';
72
+ labelSpan.textContent = opt.label;
73
+ textWrap.appendChild(labelSpan);
74
+
75
+ if (opt.description) {
76
+ const descSpan = document.createElement('span');
77
+ descSpan.className = 'moodboard-chat__menu-item-description';
78
+ descSpan.textContent = opt.description;
79
+ textWrap.appendChild(descSpan);
80
+ }
81
+
82
+ btn.appendChild(textWrap);
83
+
84
+ if (opt.hint) {
85
+ const hintSpan = document.createElement('span');
86
+ hintSpan.className = 'moodboard-chat__menu-item-hint';
87
+ hintSpan.textContent = opt.hint;
88
+ btn.appendChild(hintSpan);
89
+ }
90
+
91
+ fragment.appendChild(btn);
92
+ }
93
+ this._menu.replaceChildren(fragment);
94
+
95
+ const active = options.find((o) => o.id === activeId);
96
+ if (active) {
97
+ this._label.textContent = active.label;
98
+ if (this._icon && active.icon) this._icon.innerHTML = active.icon;
99
+ }
100
+ }
101
+
102
+ toggle() {
103
+ if (this._isOpen) this.close();
104
+ else this.open();
105
+ }
106
+
107
+ open() {
108
+ this.refresh();
109
+ this._menu.classList.add('is-open');
110
+ this._trigger.setAttribute('aria-expanded', 'true');
111
+ this._isOpen = true;
112
+ this._docClickHandler = (e) => {
113
+ if (!this._menu.contains(e.target) && !this._trigger.contains(e.target)) {
114
+ this.close();
115
+ }
116
+ };
117
+ document.addEventListener('mousedown', this._docClickHandler);
118
+ }
119
+
120
+ close() {
121
+ if (!this._isOpen) return;
122
+ this._menu.classList.remove('is-open');
123
+ this._trigger.setAttribute('aria-expanded', 'false');
124
+ this._isOpen = false;
125
+ if (this._docClickHandler) {
126
+ document.removeEventListener('mousedown', this._docClickHandler);
127
+ this._docClickHandler = null;
128
+ }
129
+ }
130
+
131
+ destroy() {
132
+ this.close();
133
+ for (const off of this._listeners) off();
134
+ this._listeners = [];
135
+ }
136
+
137
+ _on(el, type, handler) {
138
+ el.addEventListener(type, handler);
139
+ this._listeners.push(() => el.removeEventListener(type, handler));
140
+ }
141
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Попап настроек чата (системный промпт, temperature, maxTokens, очистить историю).
3
+ *
4
+ * Одна ответственность: построить контент попапа, прокинуть значения
5
+ * наружу при изменении, открывать/закрывать по триггеру.
6
+ */
7
+
8
+ export class ChatSettingsPopup {
9
+ /**
10
+ * @param {{ trigger: HTMLElement, popup: HTMLElement }} refs
11
+ * @param {{
12
+ * getSettings: () => { systemPrompt: string, temperature: number, maxTokens: number },
13
+ * onChange: (patch: object) => void,
14
+ * onClearHistory: () => void
15
+ * }} handlers
16
+ */
17
+ constructor(refs, handlers) {
18
+ this._trigger = refs.trigger;
19
+ this._popup = refs.popup;
20
+ this._handlers = handlers;
21
+ this._listeners = [];
22
+ this._docClickHandler = null;
23
+ this._isOpen = false;
24
+ this._fields = null;
25
+ }
26
+
27
+ attach() {
28
+ this._buildContent();
29
+ this._on(this._trigger, 'click', (e) => {
30
+ e.stopPropagation();
31
+ this.toggle();
32
+ });
33
+ }
34
+
35
+ refresh() {
36
+ if (!this._fields) return;
37
+ const s = this._handlers.getSettings?.() || {};
38
+ if (document.activeElement !== this._fields.system) {
39
+ this._fields.system.value = s.systemPrompt || '';
40
+ }
41
+ if (document.activeElement !== this._fields.temperature) {
42
+ this._fields.temperature.value = formatNumber(s.temperature);
43
+ }
44
+ if (document.activeElement !== this._fields.maxTokens) {
45
+ this._fields.maxTokens.value = String(s.maxTokens ?? '');
46
+ }
47
+ }
48
+
49
+ toggle() {
50
+ if (this._isOpen) this.close();
51
+ else this.open();
52
+ }
53
+
54
+ open() {
55
+ this.refresh();
56
+ this._popup.classList.add('is-open');
57
+ this._isOpen = true;
58
+ this._docClickHandler = (e) => {
59
+ if (!this._popup.contains(e.target) && !this._trigger.contains(e.target)) {
60
+ this.close();
61
+ }
62
+ };
63
+ document.addEventListener('mousedown', this._docClickHandler);
64
+ }
65
+
66
+ close() {
67
+ if (!this._isOpen) return;
68
+ this._popup.classList.remove('is-open');
69
+ this._isOpen = false;
70
+ if (this._docClickHandler) {
71
+ document.removeEventListener('mousedown', this._docClickHandler);
72
+ this._docClickHandler = null;
73
+ }
74
+ }
75
+
76
+ destroy() {
77
+ this.close();
78
+ for (const off of this._listeners) off();
79
+ this._listeners = [];
80
+ this._fields = null;
81
+ }
82
+
83
+ _buildContent() {
84
+ const fields = {};
85
+ const fragment = document.createDocumentFragment();
86
+
87
+ fragment.appendChild(this._buildField({
88
+ label: 'Системный промпт',
89
+ input: () => {
90
+ const ta = document.createElement('textarea');
91
+ ta.className = 'moodboard-chat__settings-textarea';
92
+ ta.rows = 3;
93
+ fields.system = ta;
94
+ this._on(ta, 'change', () => this._handlers.onChange?.({ systemPrompt: ta.value }));
95
+ return ta;
96
+ }
97
+ }));
98
+
99
+ fragment.appendChild(this._buildField({
100
+ label: 'Temperature (0–1)',
101
+ input: () => {
102
+ const inp = document.createElement('input');
103
+ inp.type = 'number';
104
+ inp.step = '0.1';
105
+ inp.min = '0';
106
+ inp.max = '2';
107
+ inp.className = 'moodboard-chat__settings-input';
108
+ fields.temperature = inp;
109
+ this._on(inp, 'change', () => {
110
+ const v = Number(inp.value);
111
+ if (!Number.isNaN(v)) this._handlers.onChange?.({ temperature: v });
112
+ });
113
+ return inp;
114
+ }
115
+ }));
116
+
117
+ fragment.appendChild(this._buildField({
118
+ label: 'Max tokens',
119
+ input: () => {
120
+ const inp = document.createElement('input');
121
+ inp.type = 'number';
122
+ inp.step = '100';
123
+ inp.min = '1';
124
+ inp.className = 'moodboard-chat__settings-input';
125
+ fields.maxTokens = inp;
126
+ this._on(inp, 'change', () => {
127
+ const v = Number.parseInt(inp.value, 10);
128
+ if (!Number.isNaN(v) && v > 0) this._handlers.onChange?.({ maxTokens: v });
129
+ });
130
+ return inp;
131
+ }
132
+ }));
133
+
134
+ const row = document.createElement('div');
135
+ row.className = 'moodboard-chat__settings-popup-row';
136
+ const clearBtn = document.createElement('button');
137
+ clearBtn.type = 'button';
138
+ clearBtn.className = 'moodboard-chat__settings-clear';
139
+ clearBtn.textContent = 'Очистить историю';
140
+ this._on(clearBtn, 'click', () => {
141
+ this._handlers.onClearHistory?.();
142
+ this.close();
143
+ });
144
+ row.appendChild(clearBtn);
145
+ fragment.appendChild(row);
146
+
147
+ this._popup.replaceChildren(fragment);
148
+ this._fields = fields;
149
+ }
150
+
151
+ _buildField({ label, input }) {
152
+ const wrap = document.createElement('div');
153
+ wrap.className = 'moodboard-chat__settings-field';
154
+ const labelEl = document.createElement('div');
155
+ labelEl.className = 'moodboard-chat__settings-label';
156
+ labelEl.textContent = label;
157
+ wrap.appendChild(labelEl);
158
+ wrap.appendChild(input());
159
+ return wrap;
160
+ }
161
+
162
+ _on(el, type, handler) {
163
+ el.addEventListener(type, handler);
164
+ this._listeners.push(() => el.removeEventListener(type, handler));
165
+ }
166
+ }
167
+
168
+ function formatNumber(n) {
169
+ if (typeof n !== 'number' || Number.isNaN(n)) return '';
170
+ return String(Math.round(n * 100) / 100);
171
+ }