@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.
- package/package.json +2 -1
- package/src/assets/fonts/geist/geist-sans-latin-100-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-200-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-300-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-400-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-500-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-600-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-700-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-800-normal.ttf +0 -0
- package/src/assets/fonts/geist/geist-sans-latin-900-normal.ttf +0 -0
- package/src/core/SaveManager.js +6 -0
- package/src/core/flows/ClipboardFlow.js +16 -6
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +10 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +3 -0
- package/src/services/ai/AiClient.js +224 -0
- package/src/services/ai/ChatHistoryStore.js +55 -0
- package/src/services/ai/ChatPresets.js +40 -0
- package/src/services/ai/ChatSessionController.js +220 -0
- package/src/ui/chat/ChatComposer.js +198 -0
- package/src/ui/chat/ChatExtendedPromptModal.js +131 -0
- package/src/ui/chat/ChatMessageList.js +92 -0
- package/src/ui/chat/ChatPillMenu.js +141 -0
- package/src/ui/chat/ChatSettingsPopup.js +171 -0
- package/src/ui/chat/ChatWindow.js +407 -0
- package/src/ui/chat/ChatWindowRenderer.js +214 -0
- package/src/ui/chat/icons.js +113 -0
- package/src/ui/styles/chat.css +920 -0
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/workspace.css +73 -1
- 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
|
+
}
|