@saltcorn/bootstrap-prompt-theme 0.1.1 → 0.1.2
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/.claude/settings.local.json +5 -1
- package/README.md +27 -38
- package/agent-skill.js +240 -0
- package/index.js +378 -114
- package/package.json +1 -1
- package/generate_theme.js +0 -75
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
"Bash(git -C /home/christian/playground/workspaces/fork/bootstrap-prompt-theme log --oneline -3)",
|
|
5
5
|
"Bash(git -C /home/christian/playground/workspaces/fork/bootstrap-prompt-theme remote -v)",
|
|
6
6
|
"Bash(rm -rf .git)",
|
|
7
|
-
"Bash(git init:*)"
|
|
7
|
+
"Bash(git init:*)",
|
|
8
|
+
"Bash(find /home/christian/playground/workspaces/fork -name form.js -path */models/*)",
|
|
9
|
+
"Bash(find /home/christian/playground/workspaces/saltcorn/node_modules/@dr.pogodin/csurf -name *.js)",
|
|
10
|
+
"Bash(/home/christian/playground/workspaces/fork/bootstrap-prompt-theme/README.md:*)",
|
|
11
|
+
"Bash(grep -r \"isWorkflow\\\\|xhrSubmit\\\\|submitLabel.*Finish\" /home/christian/playground/workspaces/saltcorn/packages --include=*.ts --include=*.js -l)"
|
|
8
12
|
]
|
|
9
13
|
}
|
|
10
14
|
}
|
package/README.md
CHANGED
|
@@ -1,55 +1,48 @@
|
|
|
1
1
|
# bootstrap-prompt-theme
|
|
2
2
|
|
|
3
|
-
A [Saltcorn](https://saltcorn.com/) layout plugin that uses
|
|
3
|
+
A [Saltcorn](https://saltcorn.com/) layout plugin that uses a conversational AI chat to design and refine a CSS overlay on top of standard Bootstrap 5.3.
|
|
4
4
|
|
|
5
5
|
## Relation to any-bootstrap-theme
|
|
6
6
|
|
|
7
7
|
This plugin is derived from [@saltcorn/any-bootstrap-theme](https://github.com/saltcorn/any-bootstrap-theme). The original plugin offers a large set of configuration variables (colors, card styles, link colors, etc.) that are fed into a Sass build to produce a fully compiled Bootstrap CSS file per theme.
|
|
8
8
|
|
|
9
|
-
This variant generates a lightweight CSS overlay
|
|
9
|
+
This variant generates a lightweight CSS overlay by chatting with an LLM. Bootstrap 5.3 exposes almost everything through CSS custom properties (`--bs-*`), so a well-written overlay can restyle the entire UI without recompiling Sass.
|
|
10
10
|
|
|
11
11
|
**Trade-offs:**
|
|
12
12
|
- Much simpler setup — no Sass toolchain, no build step
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- Describe or refine your theme in plain language through a persistent chat
|
|
14
|
+
- The AI applies changes immediately without requiring a page save
|
|
15
15
|
|
|
16
16
|
## Requirements
|
|
17
17
|
|
|
18
18
|
- Saltcorn with the `@saltcorn/large-language-model` plugin installed and configured
|
|
19
|
+
- Saltcorn with the `@saltcorn/agents` plugin installed
|
|
19
20
|
|
|
20
21
|
> **Model recommendation:** Good results have been achieved with `@saltcorn/large-language-model` configured to use **GPT-5.4**.
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
+
## Usage
|
|
23
24
|
|
|
24
25
|
1. Go to **Plugins → bootstrap-prompt-theme → Configure**
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
26
|
+
2. Use the **AI Theme Chat** to describe the visual style you want
|
|
27
|
+
3. The AI generates and applies the CSS overlay immediately — no Save needed
|
|
28
|
+
4. Continue the conversation to refine colors, fonts, spacing, dark mode, or anything else
|
|
29
|
+
5. Use **Clear conversation** to start fresh with a new theme direction
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
The chat history is persisted across page reloads, so you can return to the configuration page and pick up where you left off.
|
|
29
32
|
|
|
30
|
-
###
|
|
33
|
+
### Example conversation
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| Navbar color scheme | Dark, Light, Transparent, etc. |
|
|
36
|
-
| Navbar Fixed Top | Pin the navbar to the top of the viewport |
|
|
37
|
-
| Top padding | 0–5, adjust to match navbar height |
|
|
38
|
-
| Fluid container | Full-width layout |
|
|
39
|
-
| Mode | `light` or `dark` — sets `data-bs-theme` on `<html>` |
|
|
35
|
+
```
|
|
36
|
+
You: A dark cyberpunk theme with neon green accents, deep charcoal
|
|
37
|
+
backgrounds, monospace font, and glowing borders on cards.
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
AI: [applies CSS overlay]
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
white card backgrounds with a subtle box shadow, slate-grey body background (#f4f5f7),
|
|
46
|
-
and teal (#0d9488) as the primary accent color for buttons, links, and active states.
|
|
47
|
-
Use a sans-serif system font stack. In dark mode, use a near-black background (#0f1117)
|
|
48
|
-
with dark navy cards (#1a2133) and keep the teal accent.
|
|
49
|
-
Rounded corners on buttons (border-radius: 6px) and form inputs.
|
|
50
|
-
Table headers should have the navy background with white text.
|
|
51
|
-
```
|
|
41
|
+
You: Make the navbar background slightly lighter and increase the
|
|
42
|
+
card border glow intensity.
|
|
52
43
|
|
|
44
|
+
AI: [applies updated CSS overlay]
|
|
45
|
+
```
|
|
53
46
|
|
|
54
47
|
## How the overlay works
|
|
55
48
|
|
|
@@ -57,24 +50,20 @@ The generated file sets CSS custom properties in `:root` that Bootstrap 5.3 read
|
|
|
57
50
|
|
|
58
51
|
```css
|
|
59
52
|
:root {
|
|
60
|
-
--
|
|
61
|
-
|
|
62
|
-
--bs-
|
|
63
|
-
--bs-
|
|
64
|
-
--bs-primary: var(--my-accent);
|
|
65
|
-
--bs-link-color: var(--my-accent);
|
|
66
|
-
--bs-card-bg: #ffffff;
|
|
53
|
+
--bs-body-bg: #1a1a2e;
|
|
54
|
+
--bs-body-color: #e0e0e0;
|
|
55
|
+
--bs-primary: #00ff88;
|
|
56
|
+
--bs-card-bg: #16213e;
|
|
67
57
|
/* ... */
|
|
68
58
|
}
|
|
69
59
|
|
|
70
60
|
[data-bs-theme="dark"] {
|
|
71
|
-
--bs-body-bg: #
|
|
72
|
-
--bs-card-bg: #1a2133;
|
|
61
|
+
--bs-body-bg: #0d0d1a;
|
|
73
62
|
/* ... */
|
|
74
63
|
}
|
|
75
64
|
|
|
76
|
-
.
|
|
77
|
-
.
|
|
65
|
+
.card { border: 1px solid #00ff88; box-shadow: 0 0 8px #00ff88; }
|
|
66
|
+
.navbar { background-color: #16213e; }
|
|
78
67
|
/* etc. */
|
|
79
68
|
```
|
|
80
69
|
|
package/agent-skill.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
2
|
+
const { pre, code, div } = require("@saltcorn/markup/tags");
|
|
3
|
+
const db = require("@saltcorn/data/db");
|
|
4
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
5
|
+
const { join } = require("path");
|
|
6
|
+
const { writeFile, readdir, unlink } = require("fs").promises;
|
|
7
|
+
|
|
8
|
+
const writeOverlayCSS = async (css, filename = `overlay.${Date.now()}.css`) => {
|
|
9
|
+
const dest = join(__dirname, "public", filename);
|
|
10
|
+
await writeFile(dest, css);
|
|
11
|
+
return filename;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const deleteOldOverlays = async (keepFile) => {
|
|
15
|
+
const publicDir = join(__dirname, "public");
|
|
16
|
+
const files = await readdir(publicDir);
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
if (/^overlay\.\d+\.css$/.test(file) && file !== keepFile)
|
|
19
|
+
await unlink(join(publicDir, file));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SYSTEM_PROMPT = `You are an expert CSS theme designer for Bootstrap 5.3.
|
|
24
|
+
Your task is to generate a complete CSS overlay file that reskins a standard Bootstrap 5.3 app.
|
|
25
|
+
|
|
26
|
+
APPROACH:
|
|
27
|
+
1. Define theme-specific custom properties in :root (colors, fonts, shadows, etc.)
|
|
28
|
+
2. Override Bootstrap 5.3 CSS variables in :root using --bs-* tokens for light mode
|
|
29
|
+
3. Add component-level CSS rules for deeper customisation (navbar, cards, buttons, forms, tables, alerts, badges, modals, pagination, tabs, accordions, etc.)
|
|
30
|
+
4. Add a dark mode section scoped to [data-bs-theme="dark"] that overrides :root custom properties and --bs-* tokens for dark mode. Component-level rules that depend only on CSS variables will automatically adapt — only add extra component rules under [data-bs-theme="dark"] where variables alone are not sufficient.
|
|
31
|
+
|
|
32
|
+
BOOTSTRAP 5.3 CSS VARIABLES to consider overriding in :root and [data-bs-theme="dark"]:
|
|
33
|
+
--bs-body-bg, --bs-body-color, --bs-secondary-bg, --bs-tertiary-bg,
|
|
34
|
+
--bs-border-color, --bs-primary, --bs-link-color, --bs-link-hover-color,
|
|
35
|
+
--bs-card-bg, --bs-card-border-color, --bs-card-cap-bg,
|
|
36
|
+
--bs-dropdown-bg, --bs-dropdown-border-color, --bs-dropdown-link-color,
|
|
37
|
+
--bs-dropdown-link-hover-bg, --bs-dropdown-link-active-bg,
|
|
38
|
+
--bs-table-color, --bs-table-bg, --bs-table-border-color, --bs-table-hover-bg,
|
|
39
|
+
--bs-modal-bg, --bs-modal-border-color,
|
|
40
|
+
--bs-tooltip-bg, --bs-tooltip-color,
|
|
41
|
+
--bs-body-font-family, --bs-body-font-size
|
|
42
|
+
|
|
43
|
+
COMPONENTS to style: body, navbar (.navbar, .navbar-brand, .nav-link), cards (.card, .card-header, .card-footer, .card-title), buttons (.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success), forms (.form-control, .form-select, .form-label, .form-check-input), tables (.table, .table th, .table td), alerts (.alert-*), badges (.badge), modals (.modal-content, .modal-header), pagination (.page-link), tabs (.nav-tabs), links (a), headings (h1-h6), code/pre, hr, scrollbar.
|
|
44
|
+
|
|
45
|
+
DARK MODE STRUCTURE:
|
|
46
|
+
:root { /* light mode theme variables and --bs-* overrides */ }
|
|
47
|
+
/* component rules */
|
|
48
|
+
[data-bs-theme="dark"] { /* dark mode theme variables and --bs-* overrides */ }
|
|
49
|
+
/* any extra component rules needed only in dark mode, prefixed with [data-bs-theme="dark"] */
|
|
50
|
+
|
|
51
|
+
CRITICAL RULES — never break these:
|
|
52
|
+
- Never set overflow:hidden or overflow:clip on ANY element — not on .navbar, .card, .card-body, .card-header, .card-footer, section, #wrapper, #page-inner-content, #content-wrapper, .container, .page-section, or any other element. Bootstrap's Popper.js positions dropdowns with position:absolute outside their parent bounds; overflow:hidden on any ancestor will clip them.
|
|
53
|
+
- Always set .navbar { position: relative; z-index: 1030; } to ensure navbar dropdowns render above all page content.
|
|
54
|
+
- Never apply transform, filter, backdrop-filter, will-change, or perspective to .card, .card-body, .card-header, .card-footer, .container, .page-section, section, #wrapper, or any page-level container. These CSS properties create a new stacking context that can rise above the navbar and obscure its open dropdowns.
|
|
55
|
+
- Never set z-index on .card or page content elements — stacking context on page content is what causes navbar dropdowns to be obscured.
|
|
56
|
+
- Do not override --bs-zindex-dropdown, --bs-zindex-fixed, or .dropdown-menu z-index.
|
|
57
|
+
- Dropdowns must always be fully visible and on top of all other page content, including cards, sections, and containers.
|
|
58
|
+
|
|
59
|
+
IMAGES: The user may attach images to the conversation. Use them as design inspiration — extract colors, typography style, spacing feel, or overall mood and translate that into the CSS overlay. If the user attaches an image without further instruction, derive a theme from it. If they describe what they want alongside the image, use the image to inform the details.
|
|
60
|
+
|
|
61
|
+
LAYOUT CONFIG: Besides CSS, you can control structural layout options via set_layout_config:
|
|
62
|
+
- mode: "light" | "dark" — Bootstrap color mode applied to <html data-bs-theme>
|
|
63
|
+
- menu_style: "Top Navbar" | "Side Navbar" | "No Menu"
|
|
64
|
+
- colorscheme: navbar color class pair, e.g. "navbar-dark bg-dark", "navbar-light bg-light", "navbar-dark bg-primary"
|
|
65
|
+
- fixed_top: true | false — fix navbar to top of viewport
|
|
66
|
+
- fluid: true | false — full-width container vs fixed-width
|
|
67
|
+
- top_pad: 0–5 — Bootstrap spacing scale for top padding on page sections
|
|
68
|
+
- in_card: true | false — wrap page body in a Bootstrap card
|
|
69
|
+
Only call set_layout_config when you want to change structural layout, separate from CSS. Call both tools when a request requires both structural and CSS changes.
|
|
70
|
+
|
|
71
|
+
WORKFLOW:
|
|
72
|
+
1. Call apply_css_overlay with the complete CSS — this is the only way to deliver CSS.
|
|
73
|
+
2. Optionally call set_layout_config for structural layout changes.
|
|
74
|
+
3. After all tool calls, reply with one short sentence confirming what changed. Never include CSS in your text reply.`;
|
|
75
|
+
|
|
76
|
+
const findPlugin = async () => {
|
|
77
|
+
return (
|
|
78
|
+
(await Plugin.findOne({ name: "bootstrap-prompt-theme" })) ||
|
|
79
|
+
(await Plugin.findOne({ name: "@saltcorn/bootstrap-prompt-theme" }))
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const savePluginConfig = async (plugin, patch) => {
|
|
84
|
+
plugin.configuration = { ...(plugin.configuration || {}), ...patch };
|
|
85
|
+
await plugin.upsert();
|
|
86
|
+
getState().processSend({
|
|
87
|
+
refresh_plugin_cfg: plugin.name,
|
|
88
|
+
tenant: db.getTenantSchema(),
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
class GenerateBootstrapThemeSkill {
|
|
93
|
+
static skill_name = "Generate Bootstrap Theme";
|
|
94
|
+
|
|
95
|
+
get skill_label() {
|
|
96
|
+
return "Bootstrap Theme";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
constructor(cfg) {
|
|
100
|
+
Object.assign(this, cfg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static async configFields() {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
systemPrompt() {
|
|
108
|
+
return SYSTEM_PROMPT;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
provideTools() {
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
type: "function",
|
|
115
|
+
renderToolCall({ css }) {
|
|
116
|
+
return pre(
|
|
117
|
+
code(css.slice(0, 300) + (css.length > 300 ? "\n..." : ""))
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
process: async ({ css }) => {
|
|
121
|
+
const filename = await writeOverlayCSS(css);
|
|
122
|
+
const plugin = await findPlugin();
|
|
123
|
+
if (plugin) {
|
|
124
|
+
await savePluginConfig(plugin, {
|
|
125
|
+
overlay_css: css,
|
|
126
|
+
overlay_file: filename,
|
|
127
|
+
});
|
|
128
|
+
await deleteOldOverlays(filename);
|
|
129
|
+
}
|
|
130
|
+
return { filename };
|
|
131
|
+
},
|
|
132
|
+
renderToolResponse: async ({ filename }) => {
|
|
133
|
+
return div(
|
|
134
|
+
{ class: "text-success" },
|
|
135
|
+
`CSS overlay applied: ${filename}`
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
function: {
|
|
139
|
+
name: "apply_css_overlay",
|
|
140
|
+
description:
|
|
141
|
+
"Apply a CSS overlay on top of Bootstrap 5.3 to restyle the Saltcorn UI. Call this only when you have CSS ready to apply — not for questions or clarifications.",
|
|
142
|
+
parameters: {
|
|
143
|
+
type: "object",
|
|
144
|
+
required: ["css"],
|
|
145
|
+
properties: {
|
|
146
|
+
css: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description:
|
|
149
|
+
"Complete valid CSS to write as the overlay. Must start with :root { or a comment. No markdown, no code fences.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "function",
|
|
157
|
+
renderToolCall(params) {
|
|
158
|
+
const entries = Object.entries(params)
|
|
159
|
+
.filter(([, v]) => v !== undefined)
|
|
160
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
161
|
+
.join(", ");
|
|
162
|
+
return pre(code(entries));
|
|
163
|
+
},
|
|
164
|
+
process: async (params) => {
|
|
165
|
+
const plugin = await findPlugin();
|
|
166
|
+
if (plugin) {
|
|
167
|
+
const allowed = [
|
|
168
|
+
"mode",
|
|
169
|
+
"menu_style",
|
|
170
|
+
"colorscheme",
|
|
171
|
+
"fixed_top",
|
|
172
|
+
"fluid",
|
|
173
|
+
"top_pad",
|
|
174
|
+
"in_card",
|
|
175
|
+
];
|
|
176
|
+
const patch = Object.fromEntries(
|
|
177
|
+
Object.entries(params).filter(([k]) => allowed.includes(k))
|
|
178
|
+
);
|
|
179
|
+
await savePluginConfig(plugin, patch);
|
|
180
|
+
}
|
|
181
|
+
return { success: true };
|
|
182
|
+
},
|
|
183
|
+
renderToolResponse: async () =>
|
|
184
|
+
div({ class: "text-success" }, "Layout configuration updated."),
|
|
185
|
+
function: {
|
|
186
|
+
name: "set_layout_config",
|
|
187
|
+
description:
|
|
188
|
+
"Set structural layout configuration for the Saltcorn UI. Only pass the parameters you want to change.",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
mode: {
|
|
193
|
+
type: "string",
|
|
194
|
+
enum: ["light", "dark"],
|
|
195
|
+
description:
|
|
196
|
+
"Bootstrap color mode applied to <html data-bs-theme>",
|
|
197
|
+
},
|
|
198
|
+
menu_style: {
|
|
199
|
+
type: "string",
|
|
200
|
+
enum: ["Top Navbar", "Side Navbar", "No Menu"],
|
|
201
|
+
description: "Navigation menu style",
|
|
202
|
+
},
|
|
203
|
+
colorscheme: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description:
|
|
206
|
+
"Navbar color class pair, e.g. 'navbar-dark bg-dark', 'navbar-light bg-light', 'navbar-dark bg-primary'",
|
|
207
|
+
},
|
|
208
|
+
fixed_top: {
|
|
209
|
+
type: "boolean",
|
|
210
|
+
description: "Fix the navbar to the top of the viewport",
|
|
211
|
+
},
|
|
212
|
+
fluid: {
|
|
213
|
+
type: "boolean",
|
|
214
|
+
description:
|
|
215
|
+
"Use a full-width container instead of fixed-width",
|
|
216
|
+
},
|
|
217
|
+
top_pad: {
|
|
218
|
+
type: "integer",
|
|
219
|
+
minimum: 0,
|
|
220
|
+
maximum: 5,
|
|
221
|
+
description:
|
|
222
|
+
"Top padding for page sections (Bootstrap spacing scale 0–5)",
|
|
223
|
+
},
|
|
224
|
+
in_card: {
|
|
225
|
+
type: "boolean",
|
|
226
|
+
description: "Wrap page body content in a Bootstrap card",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
GenerateBootstrapThemeSkill,
|
|
238
|
+
writeOverlayCSS,
|
|
239
|
+
deleteOldOverlays,
|
|
240
|
+
};
|
package/index.js
CHANGED
|
@@ -28,6 +28,8 @@ const db = require("@saltcorn/data/db");
|
|
|
28
28
|
const Form = require("@saltcorn/data/models/form");
|
|
29
29
|
const Workflow = require("@saltcorn/data/models/workflow");
|
|
30
30
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
31
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
32
|
+
const File = require("@saltcorn/data/models/file");
|
|
31
33
|
const { renderForm, link } = require("@saltcorn/markup");
|
|
32
34
|
const {
|
|
33
35
|
alert,
|
|
@@ -36,10 +38,157 @@ const {
|
|
|
36
38
|
} = require("@saltcorn/markup/layout_utils");
|
|
37
39
|
const { features, getState } = require("@saltcorn/data/db/state");
|
|
38
40
|
const {
|
|
39
|
-
|
|
41
|
+
GenerateBootstrapThemeSkill,
|
|
40
42
|
writeOverlayCSS,
|
|
41
43
|
deleteOldOverlays,
|
|
42
|
-
} = require("./
|
|
44
|
+
} = require("./agent-skill");
|
|
45
|
+
|
|
46
|
+
// Layout defaults — will be replaced by tool-generated config in a future version
|
|
47
|
+
const DEFAULT_IN_CARD = false;
|
|
48
|
+
const DEFAULT_MENU_STYLE = "Top Navbar";
|
|
49
|
+
const DEFAULT_COLORSCHEME = "navbar-light bg-light";
|
|
50
|
+
const DEFAULT_FIXED_TOP = false;
|
|
51
|
+
const DEFAULT_TOP_PAD = 2;
|
|
52
|
+
const DEFAULT_FLUID = false;
|
|
53
|
+
const DEFAULT_MODE = "light";
|
|
54
|
+
|
|
55
|
+
const resetThemeRoute = async (req, res) => {
|
|
56
|
+
if (!req.user) return res.status(403).json({ error: "Not authenticated" });
|
|
57
|
+
try {
|
|
58
|
+
let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
|
|
59
|
+
if (!plugin)
|
|
60
|
+
plugin = await Plugin.findOne({
|
|
61
|
+
name: "@saltcorn/bootstrap-prompt-theme",
|
|
62
|
+
});
|
|
63
|
+
if (plugin) {
|
|
64
|
+
await deleteOldOverlays(null);
|
|
65
|
+
plugin.configuration = {
|
|
66
|
+
...(plugin.configuration || {}),
|
|
67
|
+
overlay_css: null,
|
|
68
|
+
overlay_file: null,
|
|
69
|
+
chat_run_id: null,
|
|
70
|
+
mode: null,
|
|
71
|
+
menu_style: null,
|
|
72
|
+
colorscheme: null,
|
|
73
|
+
fixed_top: null,
|
|
74
|
+
fluid: null,
|
|
75
|
+
top_pad: null,
|
|
76
|
+
in_card: null,
|
|
77
|
+
};
|
|
78
|
+
await plugin.upsert();
|
|
79
|
+
getState().processSend({
|
|
80
|
+
refresh_plugin_cfg: plugin.name,
|
|
81
|
+
tenant: db.getTenantSchema(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
res.json({ success: "ok" });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
getState().log(
|
|
87
|
+
2,
|
|
88
|
+
`bootstrap-prompt-theme reset-theme error: ${err.message}`
|
|
89
|
+
);
|
|
90
|
+
res.json({ error: err.message });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const chatRoute = async (req, res) => {
|
|
95
|
+
if (!req.user) return res.status(403).json({ error: "Not authenticated" });
|
|
96
|
+
try {
|
|
97
|
+
const { userinput, run_id } = req.body;
|
|
98
|
+
if (!userinput?.trim() && !req.files?.file)
|
|
99
|
+
return res.json({
|
|
100
|
+
success: "ok",
|
|
101
|
+
response: "",
|
|
102
|
+
run_id: run_id || undefined,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let agentsLocation =
|
|
106
|
+
getState().plugin_locations?.agents ||
|
|
107
|
+
getState().plugin_locations?.["@saltcorn/agents"];
|
|
108
|
+
if (!agentsLocation)
|
|
109
|
+
return res.json({ error: "Agents plugin not available" });
|
|
110
|
+
const { join } = require("path");
|
|
111
|
+
const {
|
|
112
|
+
process_interaction,
|
|
113
|
+
addToContext,
|
|
114
|
+
wrapSegment,
|
|
115
|
+
saveInteractions,
|
|
116
|
+
} = require(join(agentsLocation, "common.js"));
|
|
117
|
+
|
|
118
|
+
const agentConfig = {
|
|
119
|
+
skills: [{ skill_type: "Generate Bootstrap Theme" }],
|
|
120
|
+
sys_prompt: "",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let run;
|
|
124
|
+
if (!run_id || run_id === "undefined" || run_id === "") {
|
|
125
|
+
run = await WorkflowRun.create({
|
|
126
|
+
status: "Running",
|
|
127
|
+
started_by: req.user?.id,
|
|
128
|
+
context: {
|
|
129
|
+
interactions: [],
|
|
130
|
+
html_interactions: [],
|
|
131
|
+
funcalls: {},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
|
|
135
|
+
if (!plugin)
|
|
136
|
+
plugin = await Plugin.findOne({
|
|
137
|
+
name: "@saltcorn/bootstrap-prompt-theme",
|
|
138
|
+
});
|
|
139
|
+
if (plugin) {
|
|
140
|
+
plugin.configuration = {
|
|
141
|
+
...(plugin.configuration || {}),
|
|
142
|
+
chat_run_id: run.id,
|
|
143
|
+
};
|
|
144
|
+
await plugin.upsert();
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
run = await WorkflowRun.findOne({ id: +run_id });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (req.files?.file) {
|
|
151
|
+
const rawFiles = Array.isArray(req.files.file)
|
|
152
|
+
? req.files.file
|
|
153
|
+
: [req.files.file];
|
|
154
|
+
run.context.interactions = run.context.interactions || [];
|
|
155
|
+
for (const rawFile of rawFiles) {
|
|
156
|
+
const file = await File.from_req_files(rawFile, req.user?.id, 100);
|
|
157
|
+
const b64 = await file.get_contents("base64");
|
|
158
|
+
const imageurl = `data:${file.mimetype};base64,${b64}`;
|
|
159
|
+
await getState().functions.llm_add_message.run("image", imageurl, {
|
|
160
|
+
chat: run.context.interactions,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
await saveInteractions(run);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const userMsg = wrapSegment(
|
|
167
|
+
`<p>${userinput || "(image)"}</p>`,
|
|
168
|
+
"You",
|
|
169
|
+
true
|
|
170
|
+
);
|
|
171
|
+
await addToContext(run, {
|
|
172
|
+
interactions: [
|
|
173
|
+
...(run.context.interactions || []),
|
|
174
|
+
...(userinput?.trim() ? [{ role: "user", content: userinput }] : []),
|
|
175
|
+
],
|
|
176
|
+
html_interactions: [userMsg],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await process_interaction(
|
|
180
|
+
run,
|
|
181
|
+
agentConfig,
|
|
182
|
+
req,
|
|
183
|
+
"Theme Assistant",
|
|
184
|
+
[]
|
|
185
|
+
);
|
|
186
|
+
res.json(result?.json || { success: "ok", response: "", run_id: run.id });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
getState().log(2, `bootstrap-prompt-theme chat error: ${err.message}`);
|
|
189
|
+
res.json({ error: err.message });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
43
192
|
|
|
44
193
|
const isNode = typeof window === "undefined";
|
|
45
194
|
let hasCapacitor = false;
|
|
@@ -90,16 +239,19 @@ const blockDispatch = (config) => ({
|
|
|
90
239
|
}`)
|
|
91
240
|
),
|
|
92
241
|
noBackgroundAtTop: () => true,
|
|
93
|
-
wrapTop: (segment, ix, s) =>
|
|
94
|
-
|
|
242
|
+
wrapTop: (segment, ix, s) => {
|
|
243
|
+
const topPad = config.top_pad ?? DEFAULT_TOP_PAD;
|
|
244
|
+
const fixedTop = config.fixed_top ?? DEFAULT_FIXED_TOP;
|
|
245
|
+
const fluid = config.fluid ?? DEFAULT_FLUID;
|
|
246
|
+
return ["hero", "footer"].includes(segment.type) || segment.noWrapTop
|
|
95
247
|
? s
|
|
96
248
|
: section(
|
|
97
249
|
{
|
|
98
250
|
class: [
|
|
99
251
|
"page-section",
|
|
100
|
-
ix === 0 && `pt-${
|
|
101
|
-
ix === 0 &&
|
|
102
|
-
ix === 0 &&
|
|
252
|
+
ix === 0 && `pt-${topPad}`,
|
|
253
|
+
ix === 0 && fixedTop && isNode && "mt-5",
|
|
254
|
+
ix === 0 && fixedTop && !isNode && "mt-6",
|
|
103
255
|
segment.class,
|
|
104
256
|
segment.invertColor && "bg-primary",
|
|
105
257
|
],
|
|
@@ -110,10 +262,11 @@ const blockDispatch = (config) => ({
|
|
|
110
262
|
}`,
|
|
111
263
|
},
|
|
112
264
|
div(
|
|
113
|
-
{ class: [
|
|
265
|
+
{ class: [fluid ? "container-fluid" : "container"] },
|
|
114
266
|
segment.textStyle && segment.textStyle === "h1" ? h1(s) : s
|
|
115
267
|
)
|
|
116
|
-
)
|
|
268
|
+
);
|
|
269
|
+
},
|
|
117
270
|
});
|
|
118
271
|
|
|
119
272
|
const buildHints = (config = {}) => ({
|
|
@@ -126,7 +279,7 @@ const renderBody = (title, body, alerts, config, role, req) =>
|
|
|
126
279
|
role,
|
|
127
280
|
req,
|
|
128
281
|
layout:
|
|
129
|
-
typeof body === "string" && config.in_card
|
|
282
|
+
typeof body === "string" && (config.in_card ?? DEFAULT_IN_CARD)
|
|
130
283
|
? { type: "card", title, contents: body }
|
|
131
284
|
: body,
|
|
132
285
|
alerts,
|
|
@@ -146,7 +299,7 @@ const base_public_serve = `${linkPrefix()}/public/bootstrap-prompt-theme${
|
|
|
146
299
|
|
|
147
300
|
const wrapIt = (config, bodyAttr, headers, title, body) => {
|
|
148
301
|
return `<!doctype html>
|
|
149
|
-
<html lang="en" data-bs-theme="${config.mode ||
|
|
302
|
+
<html lang="en" data-bs-theme="${config.mode || DEFAULT_MODE}">
|
|
150
303
|
<head>
|
|
151
304
|
<meta charset="utf-8" />
|
|
152
305
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -171,7 +324,11 @@ const wrapIt = (config, bodyAttr, headers, title, body) => {
|
|
|
171
324
|
}/jquery-3.6.0.min.js"></script>
|
|
172
325
|
<script src="${base_public_serve}/bootstrap.bundle.min.js"></script>
|
|
173
326
|
${headersInBody(headers)}
|
|
174
|
-
${
|
|
327
|
+
${
|
|
328
|
+
(config.colorscheme || DEFAULT_COLORSCHEME) === "navbar-light"
|
|
329
|
+
? navbarSolidOnScroll
|
|
330
|
+
: ""
|
|
331
|
+
}
|
|
175
332
|
</body>
|
|
176
333
|
</html>`;
|
|
177
334
|
};
|
|
@@ -375,7 +532,10 @@ const menuWrap = ({
|
|
|
375
532
|
body,
|
|
376
533
|
req,
|
|
377
534
|
}) => {
|
|
378
|
-
const
|
|
535
|
+
const colorscheme = config.colorscheme || DEFAULT_COLORSCHEME;
|
|
536
|
+
const menuStyle = config.menu_style || DEFAULT_MENU_STYLE;
|
|
537
|
+
const fixedTop = config.fixed_top ?? DEFAULT_FIXED_TOP;
|
|
538
|
+
const colschm = colorscheme.split(" ");
|
|
379
539
|
const navbarCol = colschm[0];
|
|
380
540
|
const bg = colschm[1];
|
|
381
541
|
const txt = (colschm[0] || "").includes("dark") ? "text-light" : "";
|
|
@@ -384,11 +544,15 @@ const menuWrap = ({
|
|
|
384
544
|
? mobileBottomNavBar(currentUrl, menu, bg, txt)
|
|
385
545
|
: "";
|
|
386
546
|
const role = !req ? 1 : req.user ? req.user.role_id : 100;
|
|
387
|
-
if ((
|
|
547
|
+
if ((menuStyle === "No Menu" && role > 1) || (!menu && !brand))
|
|
388
548
|
return div({ id: "wrapper" }, div({ id: "page-inner-content" }, body));
|
|
389
|
-
else if (
|
|
549
|
+
else if (menuStyle === "Side Navbar" && isNode) {
|
|
390
550
|
return (
|
|
391
|
-
navbar(brand, menu, currentUrl, {
|
|
551
|
+
navbar(brand, menu, currentUrl, {
|
|
552
|
+
class: "d-md-none",
|
|
553
|
+
colorscheme,
|
|
554
|
+
fixedTop,
|
|
555
|
+
}) +
|
|
392
556
|
div(
|
|
393
557
|
{ id: "wrapper", class: "d-flex with-sidebar" },
|
|
394
558
|
nav(
|
|
@@ -414,7 +578,7 @@ const menuWrap = ({
|
|
|
414
578
|
return (
|
|
415
579
|
div(
|
|
416
580
|
{ id: "wrapper" },
|
|
417
|
-
navbar(brand, menu, currentUrl,
|
|
581
|
+
navbar(brand, menu, currentUrl, { colorscheme, fixedTop }),
|
|
418
582
|
div({ id: "page-inner-content" }, body)
|
|
419
583
|
) + mobileNav
|
|
420
584
|
);
|
|
@@ -453,7 +617,7 @@ const layout = (config) => ({
|
|
|
453
617
|
title,
|
|
454
618
|
body,
|
|
455
619
|
alerts,
|
|
456
|
-
requestFluidLayout ? {
|
|
620
|
+
requestFluidLayout ? { fluid: true } : {},
|
|
457
621
|
role,
|
|
458
622
|
req
|
|
459
623
|
),
|
|
@@ -565,112 +729,199 @@ const configuration_workflow = () =>
|
|
|
565
729
|
},
|
|
566
730
|
};
|
|
567
731
|
},
|
|
568
|
-
onStepSuccess: async (step, ctx) => {
|
|
569
|
-
const prompt = ctx?.prompt;
|
|
570
|
-
if (!prompt) return;
|
|
571
|
-
try {
|
|
572
|
-
let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
|
|
573
|
-
if (!plugin)
|
|
574
|
-
plugin = await Plugin.findOne({
|
|
575
|
-
name: "@saltcorn/bootstrap-prompt-theme",
|
|
576
|
-
});
|
|
577
|
-
if (plugin?.configuration?.last_applied_prompt === prompt) return;
|
|
578
|
-
const css = await generateThemeCSS(prompt);
|
|
579
|
-
if (css) {
|
|
580
|
-
ctx.overlay_css = css;
|
|
581
|
-
ctx.last_applied_prompt = prompt;
|
|
582
|
-
const filename = await writeOverlayCSS(css);
|
|
583
|
-
ctx.overlay_file = filename;
|
|
584
|
-
}
|
|
585
|
-
} catch (error) {
|
|
586
|
-
const msg = error.message || "unknown error";
|
|
587
|
-
getState().log(2, `bootstrap-prompt-theme onLoad failed: ${msg}`);
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
732
|
steps: [
|
|
591
733
|
{
|
|
592
734
|
name: "Theme",
|
|
593
735
|
form: async (ctx) => {
|
|
736
|
+
let chatRunId = "";
|
|
737
|
+
let existingInteractions = "";
|
|
738
|
+
let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
|
|
739
|
+
if (!plugin)
|
|
740
|
+
plugin = await Plugin.findOne({
|
|
741
|
+
name: "@saltcorn/bootstrap-prompt-theme",
|
|
742
|
+
});
|
|
743
|
+
if (plugin?.configuration?.chat_run_id) {
|
|
744
|
+
const run = await WorkflowRun.findOne({
|
|
745
|
+
id: plugin.configuration.chat_run_id,
|
|
746
|
+
});
|
|
747
|
+
if (run?.context?.html_interactions?.length) {
|
|
748
|
+
chatRunId = String(run.id);
|
|
749
|
+
existingInteractions = run.context.html_interactions.join("");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
594
752
|
return new Form({
|
|
595
|
-
|
|
596
|
-
{
|
|
597
|
-
name: "prompt",
|
|
598
|
-
label: "Theme prompt",
|
|
599
|
-
sublabel:
|
|
600
|
-
"Describe the visual style you want (LLM generation coming soon)",
|
|
601
|
-
type: "String",
|
|
602
|
-
fieldview: "textarea",
|
|
603
|
-
},
|
|
753
|
+
additionalHeaders: [
|
|
604
754
|
{
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
755
|
+
headerTag: `<script>
|
|
756
|
+
window._bptDT = new DataTransfer();
|
|
757
|
+
function bptSetFiles(files) {
|
|
758
|
+
for (const f of files) window._bptDT.items.add(f);
|
|
759
|
+
document.getElementById('bpt-file-input').files = window._bptDT.files;
|
|
760
|
+
bptUpdateFileLabel();
|
|
761
|
+
}
|
|
762
|
+
function bptUpdateFileLabel() {
|
|
763
|
+
const n = window._bptDT.files.length;
|
|
764
|
+
const el = document.getElementById('bpt-file-label');
|
|
765
|
+
if (!el) return;
|
|
766
|
+
if (n === 0) { el.innerHTML = ''; }
|
|
767
|
+
else {
|
|
768
|
+
const txt = n === 1 ? window._bptDT.files[0].name : n + ' files';
|
|
769
|
+
el.innerHTML = '<span class="text-muted">' + txt + '</span> <span class="badge text-bg-secondary" style="cursor:pointer;font-size:.65em" onclick="bptClearFiles()" title="Remove">×</span>';
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function bptClearFiles() {
|
|
773
|
+
window._bptDT.items.clear();
|
|
774
|
+
const fi = document.getElementById('bpt-file-input');
|
|
775
|
+
if (fi) fi.value = '';
|
|
776
|
+
bptUpdateFileLabel();
|
|
777
|
+
}
|
|
778
|
+
function bptFileAttach(e) {
|
|
779
|
+
window._bptDT.items.clear();
|
|
780
|
+
bptSetFiles(e.target.files);
|
|
781
|
+
}
|
|
782
|
+
async function bptResetTheme() {
|
|
783
|
+
if (!confirm('Reset theme? This will remove the current CSS overlay and conversation.')) return;
|
|
784
|
+
await fetch('/bootstrap-prompt-theme/reset-theme', {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: { 'Content-Type': 'application/json', 'CSRF-Token': _sc_globalCsrf },
|
|
787
|
+
});
|
|
788
|
+
document.getElementById('bpt-interactions').innerHTML = '';
|
|
789
|
+
document.getElementById('bpt-run-id').value = '';
|
|
790
|
+
}
|
|
791
|
+
async function bptSend() {
|
|
792
|
+
const input = document.getElementById('bpt-input');
|
|
793
|
+
const interactions = document.getElementById('bpt-interactions');
|
|
794
|
+
const runIdInput = document.getElementById('bpt-run-id');
|
|
795
|
+
const sendBtn = document.getElementById('bpt-send-btn');
|
|
796
|
+
const sendIcon = document.getElementById('bpt-send-icon');
|
|
797
|
+
const fileInput = document.getElementById('bpt-file-input');
|
|
798
|
+
const msg = input.value.trim();
|
|
799
|
+
const hasFiles = fileInput?.files?.length > 0;
|
|
800
|
+
if ((!msg && !hasFiles) || sendBtn.disabled) return;
|
|
801
|
+
let fileHtml = '';
|
|
802
|
+
if (hasFiles) {
|
|
803
|
+
fileHtml = Array.from(fileInput.files).map(f => {
|
|
804
|
+
const url = URL.createObjectURL(f);
|
|
805
|
+
return '<img src="' + url + '" style="max-height:60px;max-width:80px;border-radius:4px;margin-top:4px;" alt="' + f.name + '">';
|
|
806
|
+
}).join(' ');
|
|
807
|
+
}
|
|
808
|
+
interactions.innerHTML += '<div class="interaction-segment to-right"><div><div class="badgewrap"><span class="badge bg-secondary">You</span></div>' + (msg ? '<p>' + msg.replace(/</g, '<') + '</p>' : '') + fileHtml + '</div></div>';
|
|
809
|
+
interactions.scrollTop = interactions.scrollHeight;
|
|
810
|
+
input.value = '';
|
|
811
|
+
sendBtn.disabled = true;
|
|
812
|
+
sendIcon.className = 'fas fa-spinner fa-spin';
|
|
813
|
+
const fd = new FormData();
|
|
814
|
+
fd.append('userinput', msg);
|
|
815
|
+
fd.append('run_id', runIdInput.value);
|
|
816
|
+
if (hasFiles) {
|
|
817
|
+
Array.from(fileInput.files).forEach(f => fd.append('file', f));
|
|
818
|
+
bptClearFiles();
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const resp = await fetch('/bootstrap-prompt-theme/chat', {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: { 'CSRF-Token': _sc_globalCsrf },
|
|
824
|
+
body: fd
|
|
825
|
+
});
|
|
826
|
+
const data = await resp.json();
|
|
827
|
+
if (data.run_id) runIdInput.value = data.run_id;
|
|
828
|
+
if (data.response) { interactions.innerHTML += data.response; interactions.scrollTop = interactions.scrollHeight; }
|
|
829
|
+
if (data.error) interactions.innerHTML += '<div class="alert alert-danger mt-2">' + data.error + '</div>';
|
|
830
|
+
} catch (e) {
|
|
831
|
+
interactions.innerHTML += '<div class="alert alert-danger mt-2">Error: ' + e.message + '</div>';
|
|
832
|
+
}
|
|
833
|
+
sendBtn.disabled = false;
|
|
834
|
+
sendIcon.className = 'far fa-paper-plane';
|
|
835
|
+
}
|
|
836
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
837
|
+
const el = document.createElement('div');
|
|
838
|
+
el.innerHTML = [
|
|
839
|
+
'<div class="card mb-4">',
|
|
840
|
+
' <div class="card-header fw-bold"><i class="fas fa-robot me-2"></i>AI Theme Chat</div>',
|
|
841
|
+
' <div class="card-body" id="bpt-card-body">',
|
|
842
|
+
' <p class="text-muted small mb-2">Chat with the AI to build your Bootstrap theme. New themes are visible after page reload or when you click Finish.</p>',
|
|
843
|
+
' <div id="bpt-interactions" style="min-height:80px;max-height:320px;overflow-y:auto;border:1px solid var(--bs-border-color,#dee2e6);padding:10px;margin-bottom:10px;border-radius:4px;background:var(--bs-body-bg,#fff);"></div>',
|
|
844
|
+
' <div class="d-flex gap-2 align-items-end">',
|
|
845
|
+
' <textarea id="bpt-input" class="form-control" rows="2" placeholder="Describe the theme changes you want..."></textarea>',
|
|
846
|
+
' <button type="button" id="bpt-send-btn" class="btn btn-primary" onclick="bptSend()">',
|
|
847
|
+
' <i id="bpt-send-icon" class="far fa-paper-plane"></i>',
|
|
848
|
+
' </button>',
|
|
849
|
+
' </div>',
|
|
850
|
+
' <div class="d-flex align-items-center mt-1 gap-2">',
|
|
851
|
+
' <label class="mb-0 text-muted" style="cursor:pointer" for="bpt-file-input" title="Attach image for design inspiration"><i class="fas fa-paperclip"></i></label>',
|
|
852
|
+
' <input type="file" id="bpt-file-input" class="d-none" accept="image/*" multiple onchange="bptFileAttach(event)">',
|
|
853
|
+
' <span id="bpt-file-label" class="small"></span>',
|
|
854
|
+
' </div>',
|
|
855
|
+
' <input type="hidden" id="bpt-run-id" value="">',
|
|
856
|
+
' </div>',
|
|
857
|
+
'</div>',
|
|
858
|
+
].join('');
|
|
859
|
+
const form = document.querySelector('form.form-namespace');
|
|
860
|
+
if (form) form.parentNode.insertBefore(el, form);
|
|
861
|
+
document.getElementById('bpt-run-id').value = ${JSON.stringify(chatRunId)};
|
|
862
|
+
const interactionsEl = document.getElementById('bpt-interactions');
|
|
863
|
+
interactionsEl.innerHTML = ${JSON.stringify(existingInteractions)};
|
|
864
|
+
interactionsEl.scrollTop = interactionsEl.scrollHeight;
|
|
865
|
+
const inp = document.getElementById('bpt-input');
|
|
866
|
+
inp?.addEventListener('keydown', (e) => {
|
|
867
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); bptSend(); }
|
|
868
|
+
});
|
|
869
|
+
const dropZone = document.getElementById('bpt-card-body');
|
|
870
|
+
let _dragCtr = 0;
|
|
871
|
+
dropZone?.addEventListener('dragover', (e) => e.preventDefault());
|
|
872
|
+
dropZone?.addEventListener('dragenter', (e) => {
|
|
873
|
+
e.preventDefault();
|
|
874
|
+
_dragCtr++;
|
|
875
|
+
dropZone.style.outline = '2px dashed var(--bs-primary, #0d6efd)';
|
|
876
|
+
dropZone.style.outlineOffset = '-4px';
|
|
877
|
+
});
|
|
878
|
+
dropZone?.addEventListener('dragleave', () => {
|
|
879
|
+
_dragCtr--;
|
|
880
|
+
if (_dragCtr === 0) { dropZone.style.outline = ''; dropZone.style.outlineOffset = ''; }
|
|
881
|
+
});
|
|
882
|
+
dropZone?.addEventListener('drop', (e) => {
|
|
883
|
+
e.preventDefault();
|
|
884
|
+
_dragCtr = 0;
|
|
885
|
+
dropZone.style.outline = '';
|
|
886
|
+
dropZone.style.outlineOffset = '';
|
|
887
|
+
const imgs = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
|
|
888
|
+
if (imgs.length) bptSetFiles(imgs);
|
|
889
|
+
});
|
|
890
|
+
inp?.addEventListener('paste', (e) => {
|
|
891
|
+
const pasted = Array.from(e.clipboardData?.items || [])
|
|
892
|
+
.filter(it => it.type.startsWith('image/'))
|
|
893
|
+
.map(it => it.getAsFile())
|
|
894
|
+
.filter(Boolean);
|
|
895
|
+
if (pasted.length) { e.preventDefault(); bptSetFiles(pasted); }
|
|
896
|
+
});
|
|
897
|
+
const submitBtn = form?.querySelector('[type="submit"]');
|
|
898
|
+
const origSubmitHTML = submitBtn ? submitBtn.innerHTML : null;
|
|
899
|
+
if (form) form.addEventListener('submit', (e) => {
|
|
900
|
+
const draft = document.getElementById('bpt-input')?.value?.trim();
|
|
901
|
+
if (draft && !confirm('You have an unsent message. Finish without sending it?')) {
|
|
902
|
+
e.preventDefault();
|
|
903
|
+
if (submitBtn && origSubmitHTML !== null) {
|
|
904
|
+
setTimeout(() => {
|
|
905
|
+
submitBtn.innerHTML = origSubmitHTML;
|
|
906
|
+
submitBtn.disabled = false;
|
|
907
|
+
submitBtn.removeAttribute('disabled');
|
|
908
|
+
}, 100);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
</script>`,
|
|
659
914
|
},
|
|
915
|
+
],
|
|
916
|
+
additionalButtons: [
|
|
660
917
|
{
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
default: "light",
|
|
666
|
-
attributes: {
|
|
667
|
-
options: [
|
|
668
|
-
{ name: "light", label: "Light" },
|
|
669
|
-
{ name: "dark", label: "Dark" },
|
|
670
|
-
],
|
|
671
|
-
},
|
|
918
|
+
label: "Reset theme",
|
|
919
|
+
id: "bpt-reset-btn",
|
|
920
|
+
class: "btn btn-outline-danger",
|
|
921
|
+
onclick: "bptResetTheme()",
|
|
672
922
|
},
|
|
673
923
|
],
|
|
924
|
+
fields: [],
|
|
674
925
|
});
|
|
675
926
|
},
|
|
676
927
|
},
|
|
@@ -680,7 +931,20 @@ const configuration_workflow = () =>
|
|
|
680
931
|
module.exports = {
|
|
681
932
|
sc_plugin_api_version: 1,
|
|
682
933
|
plugin_name: "bootstrap-prompt-theme",
|
|
934
|
+
exchange: () => ({ agent_skills: [GenerateBootstrapThemeSkill] }),
|
|
683
935
|
dependencies: ["@saltcorn/large-language-model"],
|
|
936
|
+
routes: () => [
|
|
937
|
+
{
|
|
938
|
+
url: "/bootstrap-prompt-theme/chat",
|
|
939
|
+
method: "post",
|
|
940
|
+
callback: chatRoute,
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
url: "/bootstrap-prompt-theme/reset-theme",
|
|
944
|
+
method: "post",
|
|
945
|
+
callback: resetThemeRoute,
|
|
946
|
+
},
|
|
947
|
+
],
|
|
684
948
|
layout,
|
|
685
949
|
configuration_workflow,
|
|
686
950
|
onLoad: async (configuration) => {
|
package/package.json
CHANGED
package/generate_theme.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
-
const { join } = require("path");
|
|
3
|
-
const { writeFile, readdir, unlink } = require("fs").promises;
|
|
4
|
-
|
|
5
|
-
const writeOverlayCSS = async (css, filename = `overlay.${Date.now()}.css`) => {
|
|
6
|
-
const dest = join(__dirname, "public", filename);
|
|
7
|
-
await writeFile(dest, css);
|
|
8
|
-
return filename;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const deleteOldOverlays = async (keepFile) => {
|
|
12
|
-
const publicDir = join(__dirname, "public");
|
|
13
|
-
const files = await readdir(publicDir);
|
|
14
|
-
for (const file of files) {
|
|
15
|
-
if (/^overlay\.\d+\.css$/.test(file) && file !== keepFile)
|
|
16
|
-
await unlink(join(publicDir, file));
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const SYSTEM_PROMPT = `You are an expert CSS theme designer for Bootstrap 5.3.
|
|
21
|
-
Your task is to generate a complete CSS overlay file that reskins a standard Bootstrap 5.3 app.
|
|
22
|
-
|
|
23
|
-
APPROACH:
|
|
24
|
-
1. Define theme-specific custom properties in :root (colors, fonts, shadows, etc.)
|
|
25
|
-
2. Override Bootstrap 5.3 CSS variables in :root using --bs-* tokens for light mode
|
|
26
|
-
3. Add component-level CSS rules for deeper customisation (navbar, cards, buttons, forms, tables, alerts, badges, modals, pagination, tabs, accordions, etc.)
|
|
27
|
-
4. Add a dark mode section scoped to [data-bs-theme="dark"] that overrides :root custom properties and --bs-* tokens for dark mode. Component-level rules that depend only on CSS variables will automatically adapt — only add extra component rules under [data-bs-theme="dark"] where variables alone are not sufficient.
|
|
28
|
-
|
|
29
|
-
BOOTSTRAP 5.3 CSS VARIABLES to consider overriding in :root and [data-bs-theme="dark"]:
|
|
30
|
-
--bs-body-bg, --bs-body-color, --bs-secondary-bg, --bs-tertiary-bg,
|
|
31
|
-
--bs-border-color, --bs-primary, --bs-link-color, --bs-link-hover-color,
|
|
32
|
-
--bs-card-bg, --bs-card-border-color, --bs-card-cap-bg,
|
|
33
|
-
--bs-dropdown-bg, --bs-dropdown-border-color, --bs-dropdown-link-color,
|
|
34
|
-
--bs-dropdown-link-hover-bg, --bs-dropdown-link-active-bg,
|
|
35
|
-
--bs-table-color, --bs-table-bg, --bs-table-border-color, --bs-table-hover-bg,
|
|
36
|
-
--bs-modal-bg, --bs-modal-border-color,
|
|
37
|
-
--bs-tooltip-bg, --bs-tooltip-color,
|
|
38
|
-
--bs-body-font-family, --bs-body-font-size
|
|
39
|
-
|
|
40
|
-
COMPONENTS to style: body, navbar (.navbar, .navbar-brand, .nav-link), cards (.card, .card-header, .card-footer, .card-title), buttons (.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success), forms (.form-control, .form-select, .form-label, .form-check-input), tables (.table, .table th, .table td), alerts (.alert-*), badges (.badge), modals (.modal-content, .modal-header), pagination (.page-link), tabs (.nav-tabs), links (a), headings (h1-h6), code/pre, hr, scrollbar.
|
|
41
|
-
|
|
42
|
-
DARK MODE STRUCTURE:
|
|
43
|
-
:root { /* light mode theme variables and --bs-* overrides */ }
|
|
44
|
-
/* component rules */
|
|
45
|
-
[data-bs-theme="dark"] { /* dark mode theme variables and --bs-* overrides */ }
|
|
46
|
-
/* any extra component rules needed only in dark mode, prefixed with [data-bs-theme="dark"] */
|
|
47
|
-
|
|
48
|
-
CRITICAL RULES — never break these:
|
|
49
|
-
- Never set overflow:hidden or overflow:clip on ANY element — not on .navbar, .card, .card-body, .card-header, .card-footer, section, #wrapper, #page-inner-content, #content-wrapper, .container, .page-section, or any other element. Bootstrap's Popper.js positions dropdowns with position:absolute outside their parent bounds; overflow:hidden on any ancestor will clip them.
|
|
50
|
-
- Always set .navbar { position: relative; z-index: 1030; } to ensure navbar dropdowns render above all page content.
|
|
51
|
-
- Never apply transform, filter, backdrop-filter, will-change, or perspective to .card, .card-body, .card-header, .card-footer, .container, .page-section, section, #wrapper, or any page-level container. These CSS properties create a new stacking context that can rise above the navbar and obscure its open dropdowns.
|
|
52
|
-
- Never set z-index on .card or page content elements — stacking context on page content is what causes navbar dropdowns to be obscured.
|
|
53
|
-
- Do not override --bs-zindex-dropdown, --bs-zindex-fixed, or .dropdown-menu z-index.
|
|
54
|
-
- Dropdowns must always be fully visible and on top of all other page content, including cards, sections, and containers.
|
|
55
|
-
|
|
56
|
-
OUTPUT: Only valid CSS. No explanations. No markdown. No code fences. Start directly with /* theme comment */ or :root {`;
|
|
57
|
-
|
|
58
|
-
const generateThemeCSS = async (prompt) => {
|
|
59
|
-
const state = getState();
|
|
60
|
-
if (!state.functions?.llm_generate) {
|
|
61
|
-
state.log(
|
|
62
|
-
5,
|
|
63
|
-
"bootstrap-prompt-theme: llm_generate not available, is large-language-model installed?"
|
|
64
|
-
);
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
console.log("bootstrap-prompt-theme: generating theme from prompt...");
|
|
68
|
-
const result = await state.functions.llm_generate.run(prompt, {
|
|
69
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
70
|
-
});
|
|
71
|
-
state.log(6, `bootstrap-prompt-theme: LLM result: ${result}`);
|
|
72
|
-
return result || null;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
module.exports = { generateThemeCSS, writeOverlayCSS, deleteOldOverlays };
|