dsh-prompt-customizer 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +13 -32
- package/README.i18n.yaml +2 -2
- package/README.md +45 -38
- package/client/client.js +42 -25
- package/client/client.js.map +1 -1
- package/cordis.patch.yml +8 -8
- package/lib/index.js +270 -339
- package/lib/schema.js +51 -69
- package/package.json +61 -61
- package/src/client/PresetsTab.tsx +110 -110
- package/src/client/PreviewTab.tsx +61 -66
- package/src/client/PreviewTools.tsx +31 -31
- package/src/client/SectionsTab.tsx +202 -201
- package/src/client/ToolsTab.tsx +48 -48
- package/src/client/index.ts +116 -115
- package/src/client/locales.ts +101 -97
- package/src/client/presets.ts +268 -268
- package/src/client/styles.ts +41 -41
- package/src/client/types.ts +50 -75
- package/lib/client.js +0 -344
package/lib/schema.js
CHANGED
|
@@ -1,70 +1,52 @@
|
|
|
1
|
-
import Schema from '@deepseek-ai/schemastery'
|
|
2
|
-
|
|
3
|
-
/** Settings namespace for this plugin (lowercase kebab-case). */
|
|
4
|
-
export const NS = 'prompt-customizer'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The plugin's configuration schema, also used as the settings-namespace
|
|
8
|
-
* schema. GUI edits write into this shape under `~/.dsh/settings.yaml`.
|
|
9
|
-
*/
|
|
10
|
-
export const Config = Schema.object({
|
|
11
|
-
// Block these prompt sections by name (filtered out of the assembled system prompt).
|
|
12
|
-
sections: Schema.array(Schema.string()).default([]),
|
|
13
|
-
// Replace a section's text by name (original order kept).
|
|
14
|
-
replace: Schema.dict(Schema.string()).default({}),
|
|
15
|
-
// Inject / overwrite sections (by name + order + text). `custom` marks a
|
|
16
|
-
// section this plugin generated (a hidden marker so it can be identified as
|
|
17
|
-
// deletable, even across presets, without relying on name collisions).
|
|
18
|
-
inject: Schema.array(Schema.object({
|
|
19
|
-
name: Schema.string().required(),
|
|
20
|
-
order: Schema.number().default(120),
|
|
21
|
-
text: Schema.string().required(),
|
|
22
|
-
custom: Schema.boolean().default(false),
|
|
23
|
-
})).default([]),
|
|
24
|
-
// Tool catalog filtering: exclude hides listed tools; include (when non-empty)
|
|
25
|
-
// keeps only listed tools and wins over exclude.
|
|
26
|
-
tools: Schema.object({
|
|
27
|
-
exclude: Schema.array(Schema.string()).default([]),
|
|
28
|
-
include: Schema.array(Schema.string()).default([]),
|
|
29
|
-
}).default({}),
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
data: Schema.object({
|
|
53
|
-
sections: Schema.array(Schema.string()),
|
|
54
|
-
replace: Schema.dict(Schema.string()),
|
|
55
|
-
// Relative order: each entry records the section it should follow.
|
|
56
|
-
order: Schema.array(Schema.object({
|
|
57
|
-
name: Schema.string().required(),
|
|
58
|
-
after: Schema.string(),
|
|
59
|
-
text: Schema.string(),
|
|
60
|
-
custom: Schema.boolean().default(false),
|
|
61
|
-
})),
|
|
62
|
-
tools: Schema.object({
|
|
63
|
-
exclude: Schema.array(Schema.string()),
|
|
64
|
-
include: Schema.array(Schema.string()),
|
|
65
|
-
}),
|
|
66
|
-
}),
|
|
67
|
-
})).default([]),
|
|
68
|
-
// The id of the currently applied preset (only one active at a time).
|
|
69
|
-
activePreset: Schema.string(),
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
/** Settings namespace for this plugin (lowercase kebab-case). */
|
|
4
|
+
export const NS = 'prompt-customizer'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The plugin's configuration schema, also used as the settings-namespace
|
|
8
|
+
* schema. GUI edits write into this shape under `~/.dsh/settings.yaml`.
|
|
9
|
+
*/
|
|
10
|
+
export const Config = Schema.object({
|
|
11
|
+
// Block these prompt sections by name (filtered out of the assembled system prompt).
|
|
12
|
+
sections: Schema.array(Schema.string()).default([]),
|
|
13
|
+
// Replace a section's text by name (original order kept).
|
|
14
|
+
replace: Schema.dict(Schema.string()).default({}),
|
|
15
|
+
// Inject / overwrite sections (by name + order + text). `custom` marks a
|
|
16
|
+
// section this plugin generated (a hidden marker so it can be identified as
|
|
17
|
+
// deletable, even across presets, without relying on name collisions).
|
|
18
|
+
inject: Schema.array(Schema.object({
|
|
19
|
+
name: Schema.string().required(),
|
|
20
|
+
order: Schema.number().default(120),
|
|
21
|
+
text: Schema.string().required(),
|
|
22
|
+
custom: Schema.boolean().default(false),
|
|
23
|
+
})).default([]),
|
|
24
|
+
// Tool catalog filtering: exclude hides listed tools; include (when non-empty)
|
|
25
|
+
// keeps only listed tools and wins over exclude.
|
|
26
|
+
tools: Schema.object({
|
|
27
|
+
exclude: Schema.array(Schema.string()).default([]),
|
|
28
|
+
include: Schema.array(Schema.string()).default([]),
|
|
29
|
+
}).default({}),
|
|
30
|
+
// Presets: complete customization snapshots (full prompt + tool config).
|
|
31
|
+
presets: Schema.array(Schema.object({
|
|
32
|
+
id: Schema.string().required(),
|
|
33
|
+
name: Schema.string().required(),
|
|
34
|
+
data: Schema.object({
|
|
35
|
+
sections: Schema.array(Schema.string()),
|
|
36
|
+
replace: Schema.dict(Schema.string()),
|
|
37
|
+
// Relative order: each entry records the section it should follow.
|
|
38
|
+
order: Schema.array(Schema.object({
|
|
39
|
+
name: Schema.string().required(),
|
|
40
|
+
after: Schema.string(),
|
|
41
|
+
text: Schema.string(),
|
|
42
|
+
custom: Schema.boolean().default(false),
|
|
43
|
+
})),
|
|
44
|
+
tools: Schema.object({
|
|
45
|
+
exclude: Schema.array(Schema.string()),
|
|
46
|
+
include: Schema.array(Schema.string()),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
})).default([]),
|
|
50
|
+
// The id of the currently applied preset (only one active at a time).
|
|
51
|
+
activePreset: Schema.string(),
|
|
70
52
|
})
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-prompt-customizer",
|
|
3
|
-
"description": "System-prompt & tool customizer for DeepSeek Harness with a settings-panel UI: block / replace / inject prompt sections by name, and hide tools from the model catalog by exclude/include. Config lives in the dsh settings document (namespace prompt-customizer).",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"type": "module",
|
|
6
|
-
"private": false,
|
|
7
|
-
"main": "lib/index.js",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"typecheck": "tsc -p tsconfig.client.json --noEmit",
|
|
10
|
-
"build": "tsdown",
|
|
11
|
-
"test": "node --test",
|
|
12
|
-
"check": "npm run typecheck && npm run build && npm run test",
|
|
13
|
-
"prepack": "npm run build"
|
|
14
|
-
},
|
|
15
|
-
"exports": {
|
|
16
|
-
".": "./lib/index.js",
|
|
17
|
-
"./client": "./client/client.js",
|
|
18
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
-
"./package.json": "./package.json"
|
|
20
|
-
},
|
|
21
|
-
"dsh": {
|
|
22
|
-
"bundle": {
|
|
23
|
-
"patch": "./cordis.patch.yml"
|
|
24
|
-
},
|
|
25
|
-
"client": {
|
|
26
|
-
"inject": [
|
|
27
|
-
"@deepseek-ai/dsh-client-connection",
|
|
28
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
29
|
-
"@deepseek-ai/dsh-client-locale",
|
|
30
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
31
|
-
],
|
|
32
|
-
"platform": "web"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
37
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
38
|
-
"react": "^18.2.0",
|
|
39
|
-
"react-dom": "^18.2.0"
|
|
40
|
-
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@deepseek-ai/schemastery": "^3.18.0"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/react": "~18.3.1",
|
|
46
|
-
"@types/react-dom": "~18.3.1",
|
|
47
|
-
"react": "^18.3.1",
|
|
48
|
-
"react-dom": "~18.3.1",
|
|
49
|
-
"tsdown": "^0.22.14",
|
|
50
|
-
"typescript": "^5.6.0"
|
|
51
|
-
},
|
|
52
|
-
"files": [
|
|
53
|
-
"lib",
|
|
54
|
-
"client",
|
|
55
|
-
"src",
|
|
56
|
-
"cordis.patch.yml"
|
|
57
|
-
],
|
|
58
|
-
"publishConfig": {
|
|
59
|
-
"access": "public"
|
|
60
|
-
},
|
|
61
|
-
"license": "MIT"
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-prompt-customizer",
|
|
3
|
+
"description": "System-prompt & tool customizer for DeepSeek Harness with a settings-panel UI: block / replace / inject prompt sections by name, and hide tools from the model catalog by exclude/include. Config lives in the dsh settings document (namespace prompt-customizer).",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"typecheck": "tsc -p tsconfig.client.json --noEmit",
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "node --test",
|
|
12
|
+
"check": "npm run typecheck && npm run build && npm run test",
|
|
13
|
+
"prepack": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/index.js",
|
|
17
|
+
"./client": "./client/client.js",
|
|
18
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"dsh": {
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
},
|
|
25
|
+
"client": {
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-connection",
|
|
28
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
29
|
+
"@deepseek-ai/dsh-client-locale",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
31
|
+
],
|
|
32
|
+
"platform": "web"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
37
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
38
|
+
"react": "^18.2.0",
|
|
39
|
+
"react-dom": "^18.2.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "~18.3.1",
|
|
46
|
+
"@types/react-dom": "~18.3.1",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-dom": "~18.3.1",
|
|
49
|
+
"tsdown": "^0.22.14",
|
|
50
|
+
"typescript": "^5.6.0"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"lib",
|
|
54
|
+
"client",
|
|
55
|
+
"src",
|
|
56
|
+
"cordis.patch.yml"
|
|
57
|
+
],
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"license": "MIT"
|
|
62
62
|
}
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
/** Presets tab: save / apply / export / import complete customization snapshots. */
|
|
2
|
-
import { createElement as h, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
|
|
3
|
-
import type { Config, Inventory, Preset, PresetData, SettingsScope } from './types.ts'
|
|
4
|
-
import type { Translate } from './locales.ts'
|
|
5
|
-
import { addImportedPresets, applyPresetData, buildPresetData, mergeSections, removePreset, serializePreset } from './presets.ts'
|
|
6
|
-
import { s } from './styles.ts'
|
|
7
|
-
|
|
8
|
-
export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
|
|
9
|
-
const presets = cfg.presets ?? []
|
|
10
|
-
const [name, setName] = useState('')
|
|
11
|
-
const fileRef = useRef<HTMLInputElement | null>(null)
|
|
12
|
-
|
|
13
|
-
const blockedNames = new Set(cfg.sections ?? [])
|
|
14
|
-
const merged = mergeSections(inv, cfg, blockedNames)
|
|
15
|
-
const currentNames = new Set(merged.map((sec) => sec.name))
|
|
16
|
-
|
|
17
|
-
// Capture the current customization as a new preset (full snapshot). The
|
|
18
|
-
// absolute order is converted to a relative form (each section records the
|
|
19
|
-
// section it should follow) so the preset stays portable across prompts.
|
|
20
|
-
const saveCurrent = (): void => {
|
|
21
|
-
const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
|
|
22
|
-
const data = buildPresetData(cfg, merged)
|
|
23
|
-
scope.set('presets', [...presets, { id: genId(), name: presetName, data }])
|
|
24
|
-
setName('')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Apply a preset. The patch is computed by the pure helper:
|
|
28
|
-
// - same-name sections are overridden (content + order)
|
|
29
|
-
// - preset sections missing from the current prompt are added
|
|
30
|
-
// - current sections NOT in the preset list are disabled (blocked)
|
|
31
|
-
// - only the preset's ACTIVE sections are unblocked (its own blocked list
|
|
32
|
-
// is preserved)
|
|
33
|
-
const applyPreset = (preset: Preset): void => {
|
|
34
|
-
const patch = applyPresetData(preset.data, cfg, currentNames)
|
|
35
|
-
scope.set('inject', patch.inject)
|
|
36
|
-
scope.set('replace', patch.replace)
|
|
37
|
-
scope.set('sections', patch.sections)
|
|
38
|
-
scope.set('tools', patch.tools)
|
|
39
|
-
scope.set('activePreset', preset.id)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const deletePreset = (id: string): void => {
|
|
43
|
-
const next = removePreset(presets, id, cfg.activePreset)
|
|
44
|
-
scope.set('presets', next.presets)
|
|
45
|
-
if (next.activeId === undefined && cfg.activePreset === id) scope.unset('activePreset')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Serialize a preset to a downloadable JSON file.
|
|
49
|
-
const exportPreset = (preset: Preset): void => {
|
|
50
|
-
const blob = new Blob([JSON.stringify(serializePreset(preset), null, 2)], { type: 'application/json' })
|
|
51
|
-
const url = URL.createObjectURL(blob)
|
|
52
|
-
const a = document.createElement('a')
|
|
53
|
-
a.href = url
|
|
54
|
-
a.download = `${preset.name}.json`
|
|
55
|
-
a.click()
|
|
56
|
-
URL.revokeObjectURL(url)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Import preset(s) from a JSON file. Same-name presets are skipped; only
|
|
60
|
-
// presets with new names are added to the local list.
|
|
61
|
-
const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {
|
|
62
|
-
const file = e.target.files?.[0]
|
|
63
|
-
if (!file) return
|
|
64
|
-
const reader = new FileReader()
|
|
65
|
-
reader.onload = () => {
|
|
66
|
-
try {
|
|
67
|
-
const parsed = JSON.parse(String(reader.result))
|
|
68
|
-
scope.set('presets', addImportedPresets(presets, parsed, genId))
|
|
69
|
-
} catch { /* ignore invalid json */ }
|
|
70
|
-
}
|
|
71
|
-
reader.readAsText(file)
|
|
72
|
-
e.target.value = ''
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return h('div', { style: s.list }, [
|
|
76
|
-
h('div', { style: s.injectBox }, [
|
|
77
|
-
h('div', { style: s.rowTitle }, t('savePreset')),
|
|
78
|
-
h('div', { style: s.injectRow }, [
|
|
79
|
-
h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
|
|
80
|
-
h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
|
|
81
|
-
]),
|
|
82
|
-
]),
|
|
83
|
-
h('div', { style: s.injectBox }, [
|
|
84
|
-
h('div', { style: s.rowTitle }, t('importPreset')),
|
|
85
|
-
h('div', { style: s.injectRow }, [
|
|
86
|
-
h('button', { style: s.mini, onClick: () => fileRef.current?.click() }, t('import')),
|
|
87
|
-
h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
|
|
88
|
-
]),
|
|
89
|
-
]),
|
|
90
|
-
presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
|
|
91
|
-
presets.map((preset) => {
|
|
92
|
-
const active = cfg.activePreset === preset.id
|
|
93
|
-
return h('div', { key: preset.id, style: s.row }, [
|
|
94
|
-
h('div', { style: s.rowBody }, [
|
|
95
|
-
h('div', { style: s.rowTitle }, [
|
|
96
|
-
h('span', { style: s.code }, preset.name),
|
|
97
|
-
active ? h('span', { style: s.badgeOk }, t('active')) : null,
|
|
98
|
-
]),
|
|
99
|
-
]),
|
|
100
|
-
h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
|
|
101
|
-
h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),
|
|
102
|
-
h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
|
|
103
|
-
])
|
|
104
|
-
}),
|
|
105
|
-
])
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function genId(): string {
|
|
109
|
-
return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
|
|
110
|
-
}
|
|
1
|
+
/** Presets tab: save / apply / export / import complete customization snapshots. */
|
|
2
|
+
import { createElement as h, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
|
|
3
|
+
import type { Config, Inventory, Preset, PresetData, SettingsScope } from './types.ts'
|
|
4
|
+
import type { Translate } from './locales.ts'
|
|
5
|
+
import { addImportedPresets, applyPresetData, buildPresetData, mergeSections, removePreset, serializePreset } from './presets.ts'
|
|
6
|
+
import { s } from './styles.ts'
|
|
7
|
+
|
|
8
|
+
export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
|
|
9
|
+
const presets = cfg.presets ?? []
|
|
10
|
+
const [name, setName] = useState('')
|
|
11
|
+
const fileRef = useRef<HTMLInputElement | null>(null)
|
|
12
|
+
|
|
13
|
+
const blockedNames = new Set(cfg.sections ?? [])
|
|
14
|
+
const merged = mergeSections(inv, cfg, blockedNames)
|
|
15
|
+
const currentNames = new Set(merged.map((sec) => sec.name))
|
|
16
|
+
|
|
17
|
+
// Capture the current customization as a new preset (full snapshot). The
|
|
18
|
+
// absolute order is converted to a relative form (each section records the
|
|
19
|
+
// section it should follow) so the preset stays portable across prompts.
|
|
20
|
+
const saveCurrent = (): void => {
|
|
21
|
+
const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
|
|
22
|
+
const data = buildPresetData(cfg, merged)
|
|
23
|
+
scope.set('presets', [...presets, { id: genId(), name: presetName, data }])
|
|
24
|
+
setName('')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Apply a preset. The patch is computed by the pure helper:
|
|
28
|
+
// - same-name sections are overridden (content + order)
|
|
29
|
+
// - preset sections missing from the current prompt are added
|
|
30
|
+
// - current sections NOT in the preset list are disabled (blocked)
|
|
31
|
+
// - only the preset's ACTIVE sections are unblocked (its own blocked list
|
|
32
|
+
// is preserved)
|
|
33
|
+
const applyPreset = (preset: Preset): void => {
|
|
34
|
+
const patch = applyPresetData(preset.data, cfg, currentNames)
|
|
35
|
+
scope.set('inject', patch.inject)
|
|
36
|
+
scope.set('replace', patch.replace)
|
|
37
|
+
scope.set('sections', patch.sections)
|
|
38
|
+
scope.set('tools', patch.tools)
|
|
39
|
+
scope.set('activePreset', preset.id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const deletePreset = (id: string): void => {
|
|
43
|
+
const next = removePreset(presets, id, cfg.activePreset)
|
|
44
|
+
scope.set('presets', next.presets)
|
|
45
|
+
if (next.activeId === undefined && cfg.activePreset === id) scope.unset('activePreset')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Serialize a preset to a downloadable JSON file.
|
|
49
|
+
const exportPreset = (preset: Preset): void => {
|
|
50
|
+
const blob = new Blob([JSON.stringify(serializePreset(preset), null, 2)], { type: 'application/json' })
|
|
51
|
+
const url = URL.createObjectURL(blob)
|
|
52
|
+
const a = document.createElement('a')
|
|
53
|
+
a.href = url
|
|
54
|
+
a.download = `${preset.name}.json`
|
|
55
|
+
a.click()
|
|
56
|
+
URL.revokeObjectURL(url)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Import preset(s) from a JSON file. Same-name presets are skipped; only
|
|
60
|
+
// presets with new names are added to the local list.
|
|
61
|
+
const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {
|
|
62
|
+
const file = e.target.files?.[0]
|
|
63
|
+
if (!file) return
|
|
64
|
+
const reader = new FileReader()
|
|
65
|
+
reader.onload = () => {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(String(reader.result))
|
|
68
|
+
scope.set('presets', addImportedPresets(presets, parsed, genId))
|
|
69
|
+
} catch { /* ignore invalid json */ }
|
|
70
|
+
}
|
|
71
|
+
reader.readAsText(file)
|
|
72
|
+
e.target.value = ''
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return h('div', { style: s.list }, [
|
|
76
|
+
h('div', { style: s.injectBox }, [
|
|
77
|
+
h('div', { style: s.rowTitle }, t('savePreset')),
|
|
78
|
+
h('div', { style: s.injectRow }, [
|
|
79
|
+
h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
|
|
80
|
+
h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
|
|
81
|
+
]),
|
|
82
|
+
]),
|
|
83
|
+
h('div', { style: s.injectBox }, [
|
|
84
|
+
h('div', { style: s.rowTitle }, t('importPreset')),
|
|
85
|
+
h('div', { style: s.injectRow }, [
|
|
86
|
+
h('button', { style: s.mini, onClick: () => fileRef.current?.click() }, t('import')),
|
|
87
|
+
h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
|
|
88
|
+
]),
|
|
89
|
+
]),
|
|
90
|
+
presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
|
|
91
|
+
presets.map((preset) => {
|
|
92
|
+
const active = cfg.activePreset === preset.id
|
|
93
|
+
return h('div', { key: preset.id, style: s.row }, [
|
|
94
|
+
h('div', { style: s.rowBody }, [
|
|
95
|
+
h('div', { style: s.rowTitle }, [
|
|
96
|
+
h('span', { style: s.code }, preset.name),
|
|
97
|
+
active ? h('span', { style: s.badgeOk }, t('active')) : null,
|
|
98
|
+
]),
|
|
99
|
+
]),
|
|
100
|
+
h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
|
|
101
|
+
h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),
|
|
102
|
+
h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
|
|
103
|
+
])
|
|
104
|
+
}),
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function genId(): string {
|
|
109
|
+
return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
|
|
110
|
+
}
|
|
@@ -1,66 +1,61 @@
|
|
|
1
|
-
/** Preview tab: two read-only sub-tabs — the FINAL assembled system prompt
|
|
2
|
-
* after every plugin's filter, and the tools the current preset will inject
|
|
3
|
-
* (whitelist-style, after include/exclude). Neither view edits config. */
|
|
4
|
-
import { createElement as h, useEffect, useRef, useState, type ReactElement } from 'react'
|
|
5
|
-
import type { Preview, SettingsScope } from './types.ts'
|
|
6
|
-
import type { Translate } from './locales.ts'
|
|
7
|
-
import { s } from './styles.ts'
|
|
8
|
-
import { PreviewTools } from './PreviewTools.tsx'
|
|
9
|
-
|
|
10
|
-
const PREVIEW_URL = '/api/prompt-customizer/preview'
|
|
11
|
-
|
|
12
|
-
export function PreviewTab({ scope, t }: { scope: SettingsScope; t: Translate }): ReactElement {
|
|
13
|
-
const [sub, setSub] = useState<'prompt' | 'tools'>('prompt')
|
|
14
|
-
const [data, setData] = useState<Preview | null>(null)
|
|
15
|
-
const [error, setError] = useState<string | null>(null)
|
|
16
|
-
|
|
17
|
-
// Reload whenever the namespace config changes (block/replace/inject), so the
|
|
18
|
-
// section count and final text always reflect the current customization —
|
|
19
|
-
// not just on mount.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
h('div', { style: s.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: data
|
|
63
|
-
? h(PreviewTools, { tools: data.tools, t })
|
|
64
|
-
: null,
|
|
65
|
-
])
|
|
66
|
-
}
|
|
1
|
+
/** Preview tab: two read-only sub-tabs — the FINAL assembled system prompt
|
|
2
|
+
* after every plugin's filter, and the tools the current preset will inject
|
|
3
|
+
* (whitelist-style, after include/exclude). Neither view edits config. */
|
|
4
|
+
import { createElement as h, useEffect, useRef, useState, type ReactElement } from 'react'
|
|
5
|
+
import type { Preview, SettingsScope } from './types.ts'
|
|
6
|
+
import type { Translate } from './locales.ts'
|
|
7
|
+
import { s } from './styles.ts'
|
|
8
|
+
import { PreviewTools } from './PreviewTools.tsx'
|
|
9
|
+
|
|
10
|
+
const PREVIEW_URL = '/api/prompt-customizer/preview'
|
|
11
|
+
|
|
12
|
+
export function PreviewTab({ scope, t, active, refreshId }: { scope: SettingsScope; t: Translate; active: boolean; refreshId: number }): ReactElement {
|
|
13
|
+
const [sub, setSub] = useState<'prompt' | 'tools'>('prompt')
|
|
14
|
+
const [data, setData] = useState<Preview | null>(null)
|
|
15
|
+
const [error, setError] = useState<string | null>(null)
|
|
16
|
+
|
|
17
|
+
// Reload whenever the namespace config changes (block/replace/inject), so the
|
|
18
|
+
// section count and final text always reflect the current customization —
|
|
19
|
+
// not just on mount.
|
|
20
|
+
const inFlight = useRef(false)
|
|
21
|
+
const load = (): void => {
|
|
22
|
+
if (inFlight.current) return
|
|
23
|
+
inFlight.current = true
|
|
24
|
+
setData(null)
|
|
25
|
+
fetch(`${PREVIEW_URL}?t=${Date.now()}`)
|
|
26
|
+
.then((r) => r.json())
|
|
27
|
+
.then((body: Preview) => {
|
|
28
|
+
if (body && body.ok === false) throw new Error((body as { error?: string }).error ?? 'preview failed')
|
|
29
|
+
setData(body)
|
|
30
|
+
setError(null)
|
|
31
|
+
})
|
|
32
|
+
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
33
|
+
.finally(() => { inFlight.current = false })
|
|
34
|
+
}
|
|
35
|
+
useEffect(load, [])
|
|
36
|
+
useEffect(() => scope.subscribe(load), [scope])
|
|
37
|
+
// Refresh when this tab becomes active or when refresh button is clicked
|
|
38
|
+
useEffect(() => { if (active) load() }, [active, refreshId])
|
|
39
|
+
|
|
40
|
+
return h('div', { style: s.list }, [
|
|
41
|
+
h('div', { style: s.bar }, [
|
|
42
|
+
h('button', { style: sub === 'prompt' ? s.tabActive : s.tab, onClick: () => setSub('prompt') }, t('previewPrompt')),
|
|
43
|
+
h('button', { style: sub === 'tools' ? s.tabActive : s.tab, onClick: () => setSub('tools') }, t('previewTools')),
|
|
44
|
+
]),
|
|
45
|
+
error ? h('div', { style: s.error }, String(error)) : null,
|
|
46
|
+
data === null && !error ? h('div', { style: s.muted }, t('loading')) : null,
|
|
47
|
+
sub === 'prompt'
|
|
48
|
+
? data
|
|
49
|
+
? [
|
|
50
|
+
h('div', { style: s.rowTitle }, [
|
|
51
|
+
h('span', { style: s.muted }, t('previewHint')),
|
|
52
|
+
h('span', { style: s.orderTag }, `${data.sections.length} ${t('previewSections')}`),
|
|
53
|
+
]),
|
|
54
|
+
h('pre', { style: s.previewText }, data.text || t('empty')),
|
|
55
|
+
]
|
|
56
|
+
: null
|
|
57
|
+
: data
|
|
58
|
+
? h(PreviewTools, { tools: data.tools, t })
|
|
59
|
+
: null,
|
|
60
|
+
])
|
|
61
|
+
}
|