@sequent-org/moodboard 1.4.25 → 1.4.27
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 +220 -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/tools/object-tools/selection/TextEditorInteractionController.js +10 -0
- package/src/ui/TextPropertiesPanel.js +7 -1
- 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/ui/text-properties/TextPropertiesPanelState.js +2 -0
- package/src/utils/styleLoader.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/moodboard",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Interactive moodboard",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"axios": "^1.0.0",
|
|
44
|
+
"lucide-static": "^1.16.0",
|
|
44
45
|
"pixi.js": "^7.0.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/core/SaveManager.js
CHANGED
|
@@ -150,6 +150,12 @@ export class SaveManager {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
} catch (error) {
|
|
153
|
+
// Transient data:/blob: URL в объектах — retry бессмысленен, ошибку пользователю не показываем.
|
|
154
|
+
// Такое возникает когда AI-изображение размещено без upload-сервера (dev-режим).
|
|
155
|
+
if (error?.message?.includes('forbidden data/blob src')) {
|
|
156
|
+
console.warn('SaveManager: объект с data:/blob: URL пропущен при сохранении:', error.message);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
153
159
|
console.error('Ошибка автосохранения:', error);
|
|
154
160
|
this.handleSaveError(error, data);
|
|
155
161
|
} finally {
|
|
@@ -19,11 +19,14 @@ export function setupClipboardFlow(core) {
|
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const ensureServerImage = async ({ src, name }) => {
|
|
22
|
+
const ensureServerImage = async ({ src, name, skipUpload }) => {
|
|
23
23
|
const srcValue = typeof src === 'string' ? src.trim() : '';
|
|
24
24
|
if (srcValue && !/^data:image\//i.test(srcValue) && !/^blob:/i.test(srcValue)) {
|
|
25
25
|
return { src: srcValue, name };
|
|
26
26
|
}
|
|
27
|
+
if (skipUpload && srcValue) {
|
|
28
|
+
return { src: srcValue, name };
|
|
29
|
+
}
|
|
27
30
|
if (!core.imageUploadService) {
|
|
28
31
|
alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
|
|
29
32
|
return null;
|
|
@@ -222,9 +225,9 @@ export function setupClipboardFlow(core) {
|
|
|
222
225
|
core._cursor.y = y;
|
|
223
226
|
});
|
|
224
227
|
|
|
225
|
-
core.eventBus.on(Events.UI.PasteImage, async ({ src, name }) => {
|
|
228
|
+
core.eventBus.on(Events.UI.PasteImage, async ({ src, name, skipUpload }) => {
|
|
226
229
|
if (!src) return;
|
|
227
|
-
const uploaded = await ensureServerImage({ src, name });
|
|
230
|
+
const uploaded = await ensureServerImage({ src, name, skipUpload });
|
|
228
231
|
if (!uploaded?.src) return;
|
|
229
232
|
const view = core.pixi.app.view;
|
|
230
233
|
const world = core.pixi.worldLayer || core.pixi.app.stage;
|
|
@@ -283,9 +286,9 @@ export function setupClipboardFlow(core) {
|
|
|
283
286
|
}
|
|
284
287
|
});
|
|
285
288
|
|
|
286
|
-
core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name }) => {
|
|
289
|
+
core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, skipUpload }) => {
|
|
287
290
|
if (!src) return;
|
|
288
|
-
const uploaded = await ensureServerImage({ src, name });
|
|
291
|
+
const uploaded = await ensureServerImage({ src, name, skipUpload });
|
|
289
292
|
if (!uploaded?.src) return;
|
|
290
293
|
const world = core.pixi.worldLayer || core.pixi.app.stage;
|
|
291
294
|
const s = world?.scale?.x || 1;
|
|
@@ -311,11 +314,18 @@ export function setupClipboardFlow(core) {
|
|
|
311
314
|
height: h,
|
|
312
315
|
...revitPayload.properties
|
|
313
316
|
};
|
|
314
|
-
core.createObject(
|
|
317
|
+
const createdData = core.createObject(
|
|
315
318
|
revitPayload.type,
|
|
316
319
|
{ x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
|
|
317
320
|
properties
|
|
318
321
|
);
|
|
322
|
+
// data:-URL изображения (AI-генерация) не проходят через SaveManager,
|
|
323
|
+
// поэтому Events.Save.Success никогда не придёт и объект останется скрытым.
|
|
324
|
+
// Раскрываем сразу, минуя ожидание подтверждения сохранения.
|
|
325
|
+
if (skipUpload && createdData?.id) {
|
|
326
|
+
core._pendingPersistAckVisibilityIds?.delete(createdData.id);
|
|
327
|
+
core._setObjectVisibility?.(createdData.id, true);
|
|
328
|
+
}
|
|
319
329
|
};
|
|
320
330
|
|
|
321
331
|
try {
|
|
@@ -14,6 +14,7 @@ import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
|
|
|
14
14
|
import { FramePropertiesPanel } from '../../ui/FramePropertiesPanel.js';
|
|
15
15
|
import { NotePropertiesPanel } from '../../ui/NotePropertiesPanel.js';
|
|
16
16
|
import { FilePropertiesPanel } from '../../ui/FilePropertiesPanel.js';
|
|
17
|
+
import { ChatWindow } from '../../ui/chat/ChatWindow.js';
|
|
17
18
|
import { bindToolbarEvents, bindTopbarEvents } from '../integration/MoodBoardEventBindings.js';
|
|
18
19
|
|
|
19
20
|
function initToolbar(board) {
|
|
@@ -86,6 +87,14 @@ function initContextMenu(board) {
|
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function initChatWindow(board) {
|
|
91
|
+
if (board?.options?.disableChat === true) return;
|
|
92
|
+
board.chatWindow = new ChatWindow(board.workspaceElement, {
|
|
93
|
+
boardCore: board.coreMoodboard
|
|
94
|
+
});
|
|
95
|
+
board.chatWindow.attach();
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
function initHtmlLayersAndPanels(board) {
|
|
90
99
|
board.htmlTextLayer = new HtmlTextLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
91
100
|
board.htmlTextLayer.attach();
|
|
@@ -127,4 +136,5 @@ export function createMoodBoardUi(board) {
|
|
|
127
136
|
}
|
|
128
137
|
initContextMenu(board);
|
|
129
138
|
initHtmlLayersAndPanels(board);
|
|
139
|
+
initChatWindow(board);
|
|
130
140
|
}
|
|
@@ -73,6 +73,9 @@ export function destroyMoodBoard(board) {
|
|
|
73
73
|
safeDestroy(board.dotGridDebugPanel, 'dotGridDebugPanel');
|
|
74
74
|
board.dotGridDebugPanel = null;
|
|
75
75
|
|
|
76
|
+
safeDestroy(board.chatWindow, 'chatWindow');
|
|
77
|
+
board.chatWindow = null;
|
|
78
|
+
|
|
76
79
|
safeDestroy(board.coreMoodboard, 'coreMoodboard');
|
|
77
80
|
board.coreMoodboard = null;
|
|
78
81
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тонкий HTTP-клиент к /api/ai.
|
|
3
|
+
*
|
|
4
|
+
* Одна ответственность: общение с прокси-сервером (server/).
|
|
5
|
+
* Не знает ни про UI, ни про localStorage. Возвращает обычные данные
|
|
6
|
+
* и async generator для стриминга.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BASE_URL = '/api/ai';
|
|
10
|
+
|
|
11
|
+
export class AiClient {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string} [options.baseUrl='/api/ai']
|
|
15
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this._baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
19
|
+
this._fetch = options.fetchImpl || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
|
|
20
|
+
if (!this._fetch) {
|
|
21
|
+
throw new Error('AiClient: fetch is not available in this environment');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Список доступных провайдеров.
|
|
27
|
+
* @returns {Promise<Array<{id: string, label: string, enabled: boolean}>>}
|
|
28
|
+
*/
|
|
29
|
+
async listProviders() {
|
|
30
|
+
const res = await this._fetch(`${this._baseUrl}/providers`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: { 'Accept': 'application/json' }
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`AiClient.listProviders: ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
const json = await res.json();
|
|
38
|
+
return Array.isArray(json?.providers) ? json.providers : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Не-стриминговый чат.
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {string} args.provider
|
|
45
|
+
* @param {Array<{role: string, content: string}>} args.messages
|
|
46
|
+
* @param {string} [args.system]
|
|
47
|
+
* @param {number} [args.temperature]
|
|
48
|
+
* @param {number} [args.maxTokens]
|
|
49
|
+
* @param {string} [args.model]
|
|
50
|
+
* @param {AbortSignal} [args.signal]
|
|
51
|
+
* @returns {Promise<{text: string}>}
|
|
52
|
+
*/
|
|
53
|
+
async chat({ provider, signal, ...payload }) {
|
|
54
|
+
const res = await this._fetch(`${this._baseUrl}/${provider}/chat`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'Accept': 'application/json'
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ ...payload, stream: false }),
|
|
61
|
+
signal
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const detail = await safeReadError(res);
|
|
65
|
+
throw new Error(`AiClient.chat (${res.status}): ${detail}`);
|
|
66
|
+
}
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Стриминговый чат. Возвращает объект с async iterable для дельт.
|
|
72
|
+
* Отмена — через переданный AbortSignal.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} args
|
|
75
|
+
* @param {string} args.provider
|
|
76
|
+
* @param {Array<{role: string, content: string}>} args.messages
|
|
77
|
+
* @param {string} [args.system]
|
|
78
|
+
* @param {number} [args.temperature]
|
|
79
|
+
* @param {number} [args.maxTokens]
|
|
80
|
+
* @param {string} [args.model]
|
|
81
|
+
* @param {AbortSignal} [args.signal]
|
|
82
|
+
* @returns {Promise<{ deltas: AsyncGenerator<string> }>}
|
|
83
|
+
*/
|
|
84
|
+
async chatStream({ provider, signal, ...payload }) {
|
|
85
|
+
const res = await this._fetch(`${this._baseUrl}/${provider}/chat`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Accept': 'text/event-stream'
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ ...payload, stream: true }),
|
|
92
|
+
signal
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const detail = await safeReadError(res);
|
|
96
|
+
throw new Error(`AiClient.chatStream (${res.status}): ${detail}`);
|
|
97
|
+
}
|
|
98
|
+
if (!res.body) {
|
|
99
|
+
throw new Error('AiClient.chatStream: empty response body');
|
|
100
|
+
}
|
|
101
|
+
return { deltas: parseClientSse(res.body, signal) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Генерация изображения через YandexART.
|
|
106
|
+
* @param {object} args
|
|
107
|
+
* @param {string} args.prompt
|
|
108
|
+
* @param {string} [args.negativePrompt]
|
|
109
|
+
* @param {number} [args.widthRatio]
|
|
110
|
+
* @param {number} [args.heightRatio]
|
|
111
|
+
* @param {number} [args.seed]
|
|
112
|
+
* @param {string} [args.mimeType]
|
|
113
|
+
* @param {string} [args.model]
|
|
114
|
+
* @param {AbortSignal} [args.signal]
|
|
115
|
+
* @returns {Promise<{operationId: string, imageBase64: string, mimeType: string}>}
|
|
116
|
+
*/
|
|
117
|
+
async generateImage({ signal, ...payload }) {
|
|
118
|
+
const res = await this._fetch(`${this._baseUrl}/yandex-art/image`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'Accept': 'application/json'
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(payload),
|
|
125
|
+
signal
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const detail = await safeReadError(res);
|
|
129
|
+
throw new Error(`AiClient.generateImage (${res.status}): ${detail}`);
|
|
130
|
+
}
|
|
131
|
+
return res.json();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Минимальный парсер SSE на клиенте.
|
|
137
|
+
* Контракт сервера (см. server/src/utils/sseWriter.js):
|
|
138
|
+
* data: {"delta":"..."}
|
|
139
|
+
* data: [DONE]
|
|
140
|
+
* event: error
|
|
141
|
+
* data: {"error":"..."}
|
|
142
|
+
*
|
|
143
|
+
* @param {ReadableStream<Uint8Array>} stream
|
|
144
|
+
* @param {AbortSignal} [signal]
|
|
145
|
+
*/
|
|
146
|
+
async function* parseClientSse(stream, signal) {
|
|
147
|
+
const reader = stream.getReader();
|
|
148
|
+
const decoder = new TextDecoder('utf-8');
|
|
149
|
+
let buffer = '';
|
|
150
|
+
|
|
151
|
+
const onAbort = () => {
|
|
152
|
+
try { reader.cancel(); } catch (_) { /* noop */ }
|
|
153
|
+
};
|
|
154
|
+
if (signal) {
|
|
155
|
+
if (signal.aborted) onAbort();
|
|
156
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
while (true) {
|
|
161
|
+
const { value, done } = await reader.read();
|
|
162
|
+
if (done) break;
|
|
163
|
+
buffer += decoder.decode(value, { stream: true });
|
|
164
|
+
|
|
165
|
+
let idx;
|
|
166
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
167
|
+
const rawEvent = buffer.slice(0, idx);
|
|
168
|
+
buffer = buffer.slice(idx + 2);
|
|
169
|
+
|
|
170
|
+
const parsed = parseSseEvent(rawEvent);
|
|
171
|
+
if (!parsed) continue;
|
|
172
|
+
|
|
173
|
+
if (parsed.event === 'error') {
|
|
174
|
+
const err = safeJson(parsed.data);
|
|
175
|
+
throw new Error(err?.error || 'AI stream error');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (parsed.data === '[DONE]') return;
|
|
179
|
+
|
|
180
|
+
const json = safeJson(parsed.data);
|
|
181
|
+
if (json && typeof json.delta === 'string' && json.delta.length > 0) {
|
|
182
|
+
yield json.delta;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
188
|
+
try { reader.releaseLock(); } catch (_) { /* noop */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseSseEvent(raw) {
|
|
193
|
+
const lines = raw.split(/\r?\n/);
|
|
194
|
+
let event = 'message';
|
|
195
|
+
const dataParts = [];
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (!line || line.startsWith(':')) continue;
|
|
198
|
+
if (line.startsWith('event:')) {
|
|
199
|
+
event = line.slice(6).trim();
|
|
200
|
+
} else if (line.startsWith('data:')) {
|
|
201
|
+
dataParts.push(line.slice(5).trimStart());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (dataParts.length === 0) return null;
|
|
205
|
+
return { event, data: dataParts.join('\n') };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function safeJson(text) {
|
|
209
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function safeReadError(res) {
|
|
213
|
+
try {
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
const json = safeJson(text);
|
|
216
|
+
return json?.error ? json.error : (text || res.statusText);
|
|
217
|
+
} catch {
|
|
218
|
+
return res.statusText;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Хранилище истории чата в localStorage.
|
|
3
|
+
*
|
|
4
|
+
* Одна ответственность: CRUD сообщений в localStorage по ключу.
|
|
5
|
+
* Не зависит ни от UI, ни от транспорта — легко тестируется.
|
|
6
|
+
*
|
|
7
|
+
* Формат сообщения:
|
|
8
|
+
* { id, role: 'user'|'assistant'|'system', content, ts, provider? }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_KEY = 'moodboard.ai.chat.history.v1';
|
|
12
|
+
const MAX_MESSAGES = 200;
|
|
13
|
+
|
|
14
|
+
export class ChatHistoryStore {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {Storage} [options.storage] - localStorage по умолчанию
|
|
18
|
+
* @param {string} [options.key]
|
|
19
|
+
* @param {number} [options.maxMessages]
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this._storage = options.storage || (typeof localStorage !== 'undefined' ? localStorage : null);
|
|
23
|
+
this._key = options.key || DEFAULT_KEY;
|
|
24
|
+
this._max = options.maxMessages || MAX_MESSAGES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
load() {
|
|
28
|
+
if (!this._storage) return [];
|
|
29
|
+
try {
|
|
30
|
+
const raw = this._storage.getItem(this._key);
|
|
31
|
+
if (!raw) return [];
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
save(messages) {
|
|
40
|
+
if (!this._storage) return;
|
|
41
|
+
const trimmed = Array.isArray(messages)
|
|
42
|
+
? messages.slice(-this._max)
|
|
43
|
+
: [];
|
|
44
|
+
try {
|
|
45
|
+
this._storage.setItem(this._key, JSON.stringify(trimmed));
|
|
46
|
+
} catch {
|
|
47
|
+
/* квоты/недоступность — игнорируем */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
clear() {
|
|
52
|
+
if (!this._storage) return;
|
|
53
|
+
try { this._storage.removeItem(this._key); } catch { /* noop */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Пресеты системного промпта для чата.
|
|
3
|
+
*
|
|
4
|
+
* Используются кнопками "Manual" / "Style" в композере.
|
|
5
|
+
* Список можно расширять — основное правило: id уникален и стабилен,
|
|
6
|
+
* label короткий (помещается в пилл).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const CHAT_PRESETS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'default',
|
|
12
|
+
label: 'Default',
|
|
13
|
+
kind: 'manual',
|
|
14
|
+
systemPrompt: 'Ты — ассистент внутри инструмента moodboard. Отвечай по делу, кратко и на русском.'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'design-helper',
|
|
18
|
+
label: 'Design helper',
|
|
19
|
+
kind: 'style',
|
|
20
|
+
systemPrompt: 'Ты — ассистент по визуальному дизайну и мудбордам. Помогай с подбором палитр, сетки, типографики, референсов и подачи. Пиши кратко, давай конкретные значения (HEX, размеры, шрифты).'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'copywriter',
|
|
24
|
+
label: 'Copywriter',
|
|
25
|
+
kind: 'style',
|
|
26
|
+
systemPrompt: 'Ты — ассистент-копирайтер. Помогай формулировать заголовки, описания, тексты для слайдов и презентаций. Сохраняй смысл, делай короче и яснее.'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'strict-editor',
|
|
30
|
+
label: 'Strict editor',
|
|
31
|
+
kind: 'manual',
|
|
32
|
+
systemPrompt: 'Ты — строгий редактор. Проверяй текст на ошибки, тавтологии, штампы. Возвращай исправленный вариант и список изменений.'
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_PRESET_ID = 'default';
|
|
37
|
+
|
|
38
|
+
export function getPresetById(id) {
|
|
39
|
+
return CHAT_PRESETS.find((p) => p.id === id) || CHAT_PRESETS[0];
|
|
40
|
+
}
|