@gongfu/prompt-editor
Version:
A powerful prompt engineering editor SDK for AI applications
1,378 lines (1,367 loc) • 134 kB
JavaScript
"use client"
// src/components/PromptEditor.tsx
import { useCallback, useEffect, useMemo as useMemo2 } from "react";
import MonacoEditor from "@monaco-editor/react";
import { motion as motion3, AnimatePresence as AnimatePresence2 } from "framer-motion";
// src/hooks/usePromptEditorStore.ts
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
var initialTemplate = {
id: "",
name: "Untitled Prompt",
content: "",
variables: [],
tags: []
};
var usePromptEditorStore = create()(
immer((set2, get) => ({
// State
template: initialTemplate,
isDirty: false,
isValid: true,
errors: {},
history: [initialTemplate],
historyIndex: 0,
selectedVariableId: void 0,
previewValues: {},
// Actions
updateTemplate: (updates) => set2((state) => {
Object.assign(state.template, updates);
state.isDirty = true;
addToHistory(state);
}),
updateContent: (content) => set2((state) => {
state.template.content = content;
state.isDirty = true;
addToHistory(state);
}),
addVariable: (variable) => set2((state) => {
if (state.template.variables.some((v) => v.name === variable.name)) {
state.errors[variable.name] = "Variable name already exists";
return;
}
state.template.variables.push(variable);
state.isDirty = true;
state.errors = {};
addToHistory(state);
}),
updateVariable: (name, updates) => set2((state) => {
const index = state.template.variables.findIndex((v) => v.name === name);
if (index !== -1) {
if (updates.name && updates.name !== name) {
const nameExists = state.template.variables.some(
(v, i) => i !== index && v.name === updates.name
);
if (nameExists) {
state.errors[name] = "Variable name already exists";
return;
}
}
Object.assign(state.template.variables[index], updates);
state.isDirty = true;
state.errors = {};
addToHistory(state);
}
}),
removeVariable: (name) => set2((state) => {
state.template.variables = state.template.variables.filter((v) => v.name !== name);
delete state.previewValues[name];
state.isDirty = true;
addToHistory(state);
}),
setPreviewValue: (name, value) => set2((state) => {
state.previewValues[name] = value;
}),
undo: () => set2((state) => {
if (state.historyIndex > 0) {
state.historyIndex--;
state.template = JSON.parse(JSON.stringify(state.history[state.historyIndex]));
state.isDirty = state.historyIndex !== 0;
}
}),
redo: () => set2((state) => {
if (state.historyIndex < state.history.length - 1) {
state.historyIndex++;
state.template = JSON.parse(JSON.stringify(state.history[state.historyIndex]));
state.isDirty = true;
}
}),
reset: () => set2((state) => {
state.template = initialTemplate;
state.isDirty = false;
state.isValid = true;
state.errors = {};
state.history = [initialTemplate];
state.historyIndex = 0;
state.previewValues = {};
}),
loadTemplate: (template) => set2((state) => {
state.template = template;
state.isDirty = false;
state.isValid = true;
state.errors = {};
state.history = [template];
state.historyIndex = 0;
state.previewValues = {};
template.variables.forEach((variable) => {
if (variable.defaultValue !== void 0) {
state.previewValues[variable.name] = variable.defaultValue;
}
});
}),
save: async () => {
const state = get();
if (state.isValid) {
set2((state2) => {
state2.isDirty = false;
state2.template.updatedAt = /* @__PURE__ */ new Date();
});
}
},
validate: () => {
const state = get();
const errors = {};
let isValid = true;
if (!state.template.name || state.template.name.trim() === "") {
errors.name = "Template name is required";
isValid = false;
}
const variableNames = /* @__PURE__ */ new Set();
state.template.variables.forEach((variable, index) => {
if (!variable.name || variable.name.trim() === "") {
errors[`variable_${index}`] = "Variable name is required";
isValid = false;
} else if (variableNames.has(variable.name)) {
errors[`variable_${index}`] = "Duplicate variable name";
isValid = false;
} else {
variableNames.add(variable.name);
}
if (variable.validation?.pattern) {
try {
new RegExp(variable.validation.pattern);
} catch {
errors[`variable_${index}_pattern`] = "Invalid regular expression";
isValid = false;
}
}
});
set2((state2) => {
state2.errors = errors;
state2.isValid = isValid;
});
return isValid;
}
}))
);
function addToHistory(state) {
if (state.historyIndex < state.history.length - 1) {
state.history = state.history.slice(0, state.historyIndex + 1);
}
const newHistory = JSON.parse(JSON.stringify(state.template));
state.history.push(newHistory);
state.historyIndex = state.history.length - 1;
const maxHistory = 50;
if (state.history.length > maxHistory) {
state.history = state.history.slice(-maxHistory);
state.historyIndex = state.history.length - 1;
}
}
// src/components/VariablePanel.tsx
import { useState as useState2 } from "react";
import { motion as motion2 } from "framer-motion";
// src/components/VariableForm.tsx
import { useState } from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
var VariableForm = ({
variable,
onSubmit,
onCancel,
customTypes,
locale = "en"
}) => {
const [formData, setFormData] = useState({
name: variable?.name || "",
type: variable?.type || "text",
description: variable?.description || "",
defaultValue: variable?.defaultValue || "",
required: variable?.required || false,
options: variable?.options || [],
validation: variable?.validation || {}
});
const [errors, setErrors] = useState({});
const labels = {
en: {
name: "Variable Name",
type: "Type",
description: "Description",
defaultValue: "Default Value",
required: "Required",
save: "Save",
cancel: "Cancel",
addOption: "Add Option",
optionLabel: "Label",
optionValue: "Value",
validation: "Validation",
minValue: "Min Value",
maxValue: "Max Value",
pattern: "Pattern (RegExp)",
errorMessage: "Error Message"
},
"zh-CN": {
name: "\u53D8\u91CF\u540D\u79F0",
type: "\u7C7B\u578B",
description: "\u63CF\u8FF0",
defaultValue: "\u9ED8\u8BA4\u503C",
required: "\u5FC5\u586B",
save: "\u4FDD\u5B58",
cancel: "\u53D6\u6D88",
addOption: "\u6DFB\u52A0\u9009\u9879",
optionLabel: "\u6807\u7B7E",
optionValue: "\u503C",
validation: "\u9A8C\u8BC1\u89C4\u5219",
minValue: "\u6700\u5C0F\u503C",
maxValue: "\u6700\u5927\u503C",
pattern: "\u6B63\u5219\u8868\u8FBE\u5F0F",
errorMessage: "\u9519\u8BEF\u63D0\u793A"
}
}[locale];
const typeOptions = [
{ value: "text", label: locale === "zh-CN" ? "\u6587\u672C" : "Text" },
{ value: "number", label: locale === "zh-CN" ? "\u6570\u5B57" : "Number" },
{ value: "boolean", label: locale === "zh-CN" ? "\u5E03\u5C14\u503C" : "Boolean" },
{ value: "select", label: locale === "zh-CN" ? "\u9009\u62E9" : "Select" },
{ value: "multiline", label: locale === "zh-CN" ? "\u591A\u884C\u6587\u672C" : "Multiline" },
...customTypes?.map((t) => ({ value: t.type, label: t.label })) || []
];
const validate = () => {
const newErrors = {};
if (!formData.name?.trim()) {
newErrors.name = locale === "zh-CN" ? "\u53D8\u91CF\u540D\u4E0D\u80FD\u4E3A\u7A7A" : "Variable name is required";
} else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.name)) {
newErrors.name = locale === "zh-CN" ? "\u53D8\u91CF\u540D\u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u4E0B\u5212\u7EBF\uFF0C\u4E14\u4E0D\u80FD\u4EE5\u6570\u5B57\u5F00\u5934" : "Variable name must contain only letters, numbers, and underscores, and cannot start with a number";
}
if (formData.type === "select" && (!formData.options || formData.options.length === 0)) {
newErrors.options = locale === "zh-CN" ? "\u9009\u62E9\u7C7B\u578B\u5FC5\u987B\u81F3\u5C11\u6709\u4E00\u4E2A\u9009\u9879" : "Select type must have at least one option";
}
if (formData.validation?.pattern) {
try {
new RegExp(formData.validation.pattern);
} catch {
newErrors.pattern = locale === "zh-CN" ? "\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F" : "Invalid regular expression";
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onSubmit(formData);
}
};
const handleAddOption = () => {
setFormData({
...formData,
options: [...formData.options || [], { label: "", value: "" }]
});
};
const handleUpdateOption = (index, field, value) => {
const newOptions = [...formData.options || []];
newOptions[index] = { ...newOptions[index], [field]: value };
setFormData({ ...formData, options: newOptions });
};
const handleRemoveOption = (index) => {
const newOptions = [...formData.options || []];
newOptions.splice(index, 1);
setFormData({ ...formData, options: newOptions });
};
return /* @__PURE__ */ jsxs("form", { className: "variable-form", onSubmit: handleSubmit, children: [
/* @__PURE__ */ jsxs("div", { className: "form-group", children: [
/* @__PURE__ */ jsxs("label", { children: [
labels.name,
" *"
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.name,
onChange: (e) => setFormData({ ...formData, name: e.target.value }),
className: errors.name ? "error" : ""
}
),
errors.name && /* @__PURE__ */ jsx("span", { className: "error-message", children: errors.name })
] }),
/* @__PURE__ */ jsxs("div", { className: "form-group", children: [
/* @__PURE__ */ jsxs("label", { children: [
labels.type,
" *"
] }),
/* @__PURE__ */ jsx(
"select",
{
value: formData.type,
onChange: (e) => setFormData({ ...formData, type: e.target.value }),
children: typeOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "form-group", children: [
/* @__PURE__ */ jsx("label", { children: labels.description }),
/* @__PURE__ */ jsx(
"textarea",
{
value: formData.description,
onChange: (e) => setFormData({ ...formData, description: e.target.value }),
rows: 2
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "form-group", children: [
/* @__PURE__ */ jsx("label", { children: labels.defaultValue }),
formData.type === "boolean" ? /* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: formData.defaultValue || false,
onChange: (e) => setFormData({ ...formData, defaultValue: e.target.checked })
}
) : formData.type === "multiline" ? /* @__PURE__ */ jsx(
"textarea",
{
value: formData.defaultValue,
onChange: (e) => setFormData({ ...formData, defaultValue: e.target.value }),
rows: 2
}
) : /* @__PURE__ */ jsx(
"input",
{
type: formData.type === "number" ? "number" : "text",
value: formData.defaultValue,
onChange: (e) => setFormData({ ...formData, defaultValue: e.target.value })
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "form-group", children: /* @__PURE__ */ jsxs("label", { children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: formData.required,
onChange: (e) => setFormData({ ...formData, required: e.target.checked })
}
),
labels.required
] }) }),
formData.type === "select" && /* @__PURE__ */ jsxs("div", { className: "form-group", children: [
/* @__PURE__ */ jsx("label", { children: "Options" }),
formData.options?.map((option, index) => /* @__PURE__ */ jsxs("div", { className: "option-row", children: [
/* @__PURE__ */ jsx(
"input",
{
type: "text",
placeholder: labels.optionLabel,
value: option.label,
onChange: (e) => handleUpdateOption(index, "label", e.target.value)
}
),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
placeholder: labels.optionValue,
value: option.value,
onChange: (e) => handleUpdateOption(index, "value", e.target.value)
}
),
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleRemoveOption(index), children: "\xD7" })
] }, index)),
/* @__PURE__ */ jsxs("button", { type: "button", onClick: handleAddOption, className: "add-option-btn", children: [
"+ ",
labels.addOption
] }),
errors.options && /* @__PURE__ */ jsx("span", { className: "error-message", children: errors.options })
] }),
(formData.type === "text" || formData.type === "number") && /* @__PURE__ */ jsxs("details", { className: "form-group", children: [
/* @__PURE__ */ jsx("summary", { children: labels.validation }),
formData.type === "number" && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { className: "form-row", children: [
/* @__PURE__ */ jsx("label", { children: labels.minValue }),
/* @__PURE__ */ jsx(
"input",
{
type: "number",
value: formData.validation?.min || "",
onChange: (e) => setFormData({
...formData,
validation: { ...formData.validation, min: e.target.value ? Number(e.target.value) : void 0 }
})
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "form-row", children: [
/* @__PURE__ */ jsx("label", { children: labels.maxValue }),
/* @__PURE__ */ jsx(
"input",
{
type: "number",
value: formData.validation?.max || "",
onChange: (e) => setFormData({
...formData,
validation: { ...formData.validation, max: e.target.value ? Number(e.target.value) : void 0 }
})
}
)
] })
] }),
formData.type === "text" && /* @__PURE__ */ jsxs("div", { className: "form-row", children: [
/* @__PURE__ */ jsx("label", { children: labels.pattern }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.validation?.pattern || "",
onChange: (e) => setFormData({
...formData,
validation: { ...formData.validation, pattern: e.target.value }
}),
className: errors.pattern ? "error" : ""
}
),
errors.pattern && /* @__PURE__ */ jsx("span", { className: "error-message", children: errors.pattern })
] }),
/* @__PURE__ */ jsxs("div", { className: "form-row", children: [
/* @__PURE__ */ jsx("label", { children: labels.errorMessage }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.validation?.message || "",
onChange: (e) => setFormData({
...formData,
validation: { ...formData.validation, message: e.target.value }
})
}
)
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "form-actions", children: [
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, className: "btn-secondary", children: labels.cancel }),
/* @__PURE__ */ jsx("button", { type: "submit", className: "btn-primary", children: labels.save })
] })
] });
};
// src/components/VariableList.tsx
import { motion, AnimatePresence } from "framer-motion";
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var VariableList = ({
variables,
editingVariable,
onEdit,
onUpdate,
onRemove,
customTypes,
readOnly = false,
locale = "en"
}) => {
const typeLabels = {
text: locale === "zh-CN" ? "\u6587\u672C" : "Text",
number: locale === "zh-CN" ? "\u6570\u5B57" : "Number",
boolean: locale === "zh-CN" ? "\u5E03\u5C14\u503C" : "Boolean",
select: locale === "zh-CN" ? "\u9009\u62E9" : "Select",
multiline: locale === "zh-CN" ? "\u591A\u884C\u6587\u672C" : "Multiline"
};
return /* @__PURE__ */ jsx2("div", { className: "variable-list", children: /* @__PURE__ */ jsx2(AnimatePresence, { children: variables.map((variable) => /* @__PURE__ */ jsx2(
motion.div,
{
className: "variable-item",
initial: { opacity: 0, y: -10 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 },
layout: true,
children: editingVariable === variable.name ? /* @__PURE__ */ jsx2(
VariableForm,
{
variable,
onSubmit: (updatedVariable) => {
onUpdate(variable.name, updatedVariable);
onEdit(null);
},
onCancel: () => onEdit(null),
customTypes,
locale
}
) : /* @__PURE__ */ jsxs2("div", { className: "variable-item__content", children: [
/* @__PURE__ */ jsxs2("div", { className: "variable-item__header", children: [
/* @__PURE__ */ jsxs2("div", { className: "variable-item__info", children: [
/* @__PURE__ */ jsx2("span", { className: "variable-item__name", children: /* @__PURE__ */ jsx2("code", { children: `{{${variable.name}}}` }) }),
/* @__PURE__ */ jsx2("span", { className: "variable-item__type", children: typeLabels[variable.type] || variable.type }),
variable.required && /* @__PURE__ */ jsx2("span", { className: "variable-item__required", children: "*" })
] }),
!readOnly && /* @__PURE__ */ jsxs2("div", { className: "variable-item__actions", children: [
/* @__PURE__ */ jsx2(
"button",
{
className: "variable-item__action",
onClick: () => onEdit(variable.name),
title: locale === "zh-CN" ? "\u7F16\u8F91" : "Edit",
children: /* @__PURE__ */ jsx2(EditIcon, {})
}
),
/* @__PURE__ */ jsx2(
"button",
{
className: "variable-item__action variable-item__action--danger",
onClick: () => onRemove(variable.name),
title: locale === "zh-CN" ? "\u5220\u9664" : "Delete",
children: /* @__PURE__ */ jsx2(DeleteIcon, {})
}
)
] })
] }),
variable.description && /* @__PURE__ */ jsx2("p", { className: "variable-item__description", children: variable.description }),
variable.defaultValue !== void 0 && /* @__PURE__ */ jsxs2("div", { className: "variable-item__default", children: [
/* @__PURE__ */ jsx2("span", { children: locale === "zh-CN" ? "\u9ED8\u8BA4\u503C\uFF1A" : "Default: " }),
/* @__PURE__ */ jsx2("code", { children: JSON.stringify(variable.defaultValue) })
] }),
variable.type === "select" && variable.options && /* @__PURE__ */ jsxs2("div", { className: "variable-item__options", children: [
/* @__PURE__ */ jsx2("span", { children: locale === "zh-CN" ? "\u9009\u9879\uFF1A" : "Options: " }),
variable.options.map((opt, idx) => /* @__PURE__ */ jsxs2("span", { className: "variable-item__option", children: [
opt.label,
" (",
opt.value,
")"
] }, idx))
] }),
variable.validation && Object.keys(variable.validation).length > 0 && /* @__PURE__ */ jsxs2("div", { className: "variable-item__validation", children: [
/* @__PURE__ */ jsx2("span", { children: locale === "zh-CN" ? "\u9A8C\u8BC1\uFF1A" : "Validation: " }),
variable.validation.min !== void 0 && /* @__PURE__ */ jsxs2("span", { children: [
"Min: ",
variable.validation.min
] }),
variable.validation.max !== void 0 && /* @__PURE__ */ jsxs2("span", { children: [
"Max: ",
variable.validation.max
] }),
variable.validation.pattern && /* @__PURE__ */ jsxs2("span", { children: [
"Pattern: ",
/* @__PURE__ */ jsx2("code", { children: variable.validation.pattern })
] })
] })
] })
},
variable.name
)) }) });
};
var EditIcon = () => /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M12.146.854a.5.5 0 01.708 0l2.292 2.292a.5.5 0 010 .708l-9 9a.5.5 0 01-.168.11l-5 2a.5.5 0 01-.65-.65l2-5a.5.5 0 01.11-.168l9-9zM12.5 2L14 3.5 12.5 5 11 3.5 12.5 2zm-10 10L1 14l2-1.5L12.5 3 11 1.5 1.5 11z" }) });
var DeleteIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: [
/* @__PURE__ */ jsx2("path", { d: "M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z" }),
/* @__PURE__ */ jsx2("path", { d: "M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 01-1-1V2a1 1 0 011-1H6a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1v1zM4 4h8v9a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" })
] });
// src/components/VariablePanel.tsx
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
var VariablePanel = ({
variables,
customTypes,
readOnly = false,
locale = "en"
}) => {
const [isAddingVariable, setIsAddingVariable] = useState2(false);
const [editingVariable, setEditingVariable] = useState2(null);
const { addVariable, updateVariable, removeVariable } = usePromptEditorStore();
const handleAddVariable = (variable) => {
addVariable(variable);
setIsAddingVariable(false);
};
const handleUpdateVariable = (name, updates) => {
updateVariable(name, updates);
setEditingVariable(null);
};
const handleRemoveVariable = (name) => {
if (window.confirm(locale === "zh-CN" ? "\u786E\u5B9A\u5220\u9664\u8BE5\u53D8\u91CF\u5417\uFF1F" : "Are you sure to delete this variable?")) {
removeVariable(name);
}
};
const labels = {
en: {
title: "Variables",
addVariable: "Add Variable",
noVariables: "No variables defined",
description: "Define variables to make your prompt dynamic"
},
"zh-CN": {
title: "\u53D8\u91CF",
addVariable: "\u6DFB\u52A0\u53D8\u91CF",
noVariables: "\u6682\u65E0\u53D8\u91CF",
description: "\u5B9A\u4E49\u53D8\u91CF\u4F7F\u63D0\u793A\u8BCD\u66F4\u52A0\u7075\u6D3B"
}
}[locale];
return /* @__PURE__ */ jsxs3("div", { className: "variable-panel", children: [
/* @__PURE__ */ jsxs3("div", { className: "variable-panel__header", children: [
/* @__PURE__ */ jsx3("h3", { children: labels.title }),
!readOnly && /* @__PURE__ */ jsxs3(
"button",
{
className: "variable-panel__add-btn",
onClick: () => setIsAddingVariable(true),
disabled: isAddingVariable,
children: [
"+ ",
labels.addVariable
]
}
)
] }),
/* @__PURE__ */ jsx3("div", { className: "variable-panel__content", children: variables.length === 0 && !isAddingVariable ? /* @__PURE__ */ jsxs3("div", { className: "variable-panel__empty", children: [
/* @__PURE__ */ jsx3("p", { children: labels.noVariables }),
/* @__PURE__ */ jsx3("small", { children: labels.description })
] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
isAddingVariable && /* @__PURE__ */ jsx3(
motion2.div,
{
initial: { opacity: 0, y: -10 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 },
children: /* @__PURE__ */ jsx3(
VariableForm,
{
onSubmit: handleAddVariable,
onCancel: () => setIsAddingVariable(false),
customTypes,
locale
}
)
}
),
/* @__PURE__ */ jsx3(
VariableList,
{
variables,
editingVariable,
onEdit: setEditingVariable,
onUpdate: handleUpdateVariable,
onRemove: handleRemoveVariable,
customTypes,
readOnly,
locale
}
)
] }) })
] });
};
// src/components/Toolbar.tsx
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
var Toolbar = ({
config,
isDirty,
onSave,
onExport,
onImport,
locale = "en"
}) => {
const { undo, redo, history, historyIndex } = usePromptEditorStore();
const canUndo = historyIndex > 0;
const canRedo = historyIndex < history.length - 1;
const handleImport = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json,.yaml,.yml,.txt";
input.onchange = async (e) => {
const file = e.target.files?.[0];
if (file && onImport) {
await onImport(file);
}
};
input.click();
};
const handleExport = (format) => {
if (onExport) {
onExport(format);
}
};
const labels = {
en: {
save: "Save",
export: "Export",
import: "Import",
undo: "Undo",
redo: "Redo",
format: "Format",
preview: "Preview",
settings: "Settings"
},
"zh-CN": {
save: "\u4FDD\u5B58",
export: "\u5BFC\u51FA",
import: "\u5BFC\u5165",
undo: "\u64A4\u9500",
redo: "\u91CD\u505A",
format: "\u683C\u5F0F\u5316",
preview: "\u9884\u89C8",
settings: "\u8BBE\u7F6E"
}
}[locale];
const defaultConfig = {
showSave: true,
showExport: true,
showImport: true,
showUndo: true,
showRedo: true,
showFormat: false,
showPreview: false,
showSettings: false,
...config
};
return /* @__PURE__ */ jsxs4("div", { className: "prompt-editor__toolbar", children: [
/* @__PURE__ */ jsxs4("div", { className: "toolbar__group", children: [
defaultConfig.showSave && onSave && /* @__PURE__ */ jsxs4(
"button",
{
className: "toolbar__btn toolbar__btn--primary",
onClick: onSave,
disabled: !isDirty,
title: labels.save,
children: [
/* @__PURE__ */ jsx4(SaveIcon, {}),
/* @__PURE__ */ jsx4("span", { children: labels.save })
]
}
),
defaultConfig.showUndo && /* @__PURE__ */ jsx4(
"button",
{
className: "toolbar__btn",
onClick: undo,
disabled: !canUndo,
title: labels.undo,
children: /* @__PURE__ */ jsx4(UndoIcon, {})
}
),
defaultConfig.showRedo && /* @__PURE__ */ jsx4(
"button",
{
className: "toolbar__btn",
onClick: redo,
disabled: !canRedo,
title: labels.redo,
children: /* @__PURE__ */ jsx4(RedoIcon, {})
}
)
] }),
/* @__PURE__ */ jsxs4("div", { className: "toolbar__group", children: [
defaultConfig.showImport && onImport && /* @__PURE__ */ jsxs4(
"button",
{
className: "toolbar__btn",
onClick: handleImport,
title: labels.import,
children: [
/* @__PURE__ */ jsx4(ImportIcon, {}),
/* @__PURE__ */ jsx4("span", { children: labels.import })
]
}
),
defaultConfig.showExport && onExport && /* @__PURE__ */ jsxs4("div", { className: "toolbar__dropdown", children: [
/* @__PURE__ */ jsxs4("button", { className: "toolbar__btn", title: labels.export, children: [
/* @__PURE__ */ jsx4(ExportIcon, {}),
/* @__PURE__ */ jsx4("span", { children: labels.export }),
/* @__PURE__ */ jsx4(ChevronDownIcon, {})
] }),
/* @__PURE__ */ jsxs4("div", { className: "toolbar__dropdown-menu", children: [
/* @__PURE__ */ jsx4("button", { onClick: () => handleExport("json"), children: "JSON" }),
/* @__PURE__ */ jsx4("button", { onClick: () => handleExport("yaml"), children: "YAML" }),
/* @__PURE__ */ jsx4("button", { onClick: () => handleExport("markdown"), children: "Markdown" }),
/* @__PURE__ */ jsx4("button", { onClick: () => handleExport("txt"), children: "Text" })
] })
] })
] }),
defaultConfig.customActions && defaultConfig.customActions.length > 0 && /* @__PURE__ */ jsx4("div", { className: "toolbar__group", children: defaultConfig.customActions.map((action) => /* @__PURE__ */ jsxs4(
"button",
{
className: "toolbar__btn",
onClick: action.onClick,
disabled: action.disabled,
title: action.tooltip,
children: [
action.icon,
/* @__PURE__ */ jsx4("span", { children: action.label })
]
},
action.id
)) })
] });
};
var SaveIcon = () => /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M13.5 2h-11A1.5 1.5 0 001 3.5v9A1.5 1.5 0 002.5 14h11a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0013.5 2zM12 12H4V6h8v6z" }) });
var UndoIcon = () => /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M8 3a5 5 0 00-5 5h2a3 3 0 016 0 3 3 0 01-6 0H3l3-3-3-3v2a5 5 0 105 5V8z" }) });
var RedoIcon = () => /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M8 3a5 5 0 015 5h-2a3 3 0 00-6 0 3 3 0 006 0h2l-3-3 3-3v2a5 5 0 11-5 5V8z" }) });
var ImportIcon = () => /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M8 10l-4-4h3V2h2v4h3l-4 4zm-6 3v1h12v-1H2z" }) });
var ExportIcon = () => /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M8 6l4 4H9v4H7v-4H4l4-4zM2 3v1h12V3H2z" }) });
var ChevronDownIcon = () => /* @__PURE__ */ jsx4("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M2 4l4 4 4-4H2z" }) });
// src/components/PreviewPanel.tsx
import { useMemo } from "react";
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
var PreviewPanel = ({
template,
locale = "en"
}) => {
const { previewValues, setPreviewValue } = usePromptEditorStore();
const previewContent = useMemo(() => {
let content = template.content;
template.variables.forEach((variable) => {
const value = previewValues[variable.name] ?? variable.defaultValue ?? "";
const regex = new RegExp(`{{\\s*${variable.name}\\s*}}`, "g");
content = content.replace(regex, String(value));
});
return content;
}, [template.content, template.variables, previewValues]);
const labels = {
en: {
title: "Preview",
variables: "Variables",
output: "Output",
noContent: "No content to preview"
},
"zh-CN": {
title: "\u9884\u89C8",
variables: "\u53D8\u91CF\u503C",
output: "\u8F93\u51FA\u7ED3\u679C",
noContent: "\u6682\u65E0\u5185\u5BB9\u53EF\u9884\u89C8"
}
}[locale];
return /* @__PURE__ */ jsxs5("div", { className: "preview-panel", children: [
/* @__PURE__ */ jsx5("div", { className: "preview-panel__header", children: /* @__PURE__ */ jsx5("h3", { children: labels.title }) }),
/* @__PURE__ */ jsxs5("div", { className: "preview-panel__content", children: [
template.variables.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "preview-panel__variables", children: [
/* @__PURE__ */ jsx5("h4", { children: labels.variables }),
template.variables.map((variable) => /* @__PURE__ */ jsxs5("div", { className: "preview-panel__variable", children: [
/* @__PURE__ */ jsxs5("label", { children: [
variable.name,
variable.required && /* @__PURE__ */ jsx5("span", { className: "required", children: "*" })
] }),
renderVariableInput(
variable,
previewValues[variable.name],
(value) => setPreviewValue(variable.name, value)
),
variable.description && /* @__PURE__ */ jsx5("small", { children: variable.description })
] }, variable.name))
] }),
/* @__PURE__ */ jsxs5("div", { className: "preview-panel__output", children: [
/* @__PURE__ */ jsx5("h4", { children: labels.output }),
/* @__PURE__ */ jsx5("div", { className: "preview-panel__output-content", children: previewContent ? /* @__PURE__ */ jsx5("pre", { children: previewContent }) : /* @__PURE__ */ jsx5("p", { className: "preview-panel__empty", children: labels.noContent }) })
] })
] })
] });
};
function renderVariableInput(variable, value, onChange) {
switch (variable.type) {
case "text":
return /* @__PURE__ */ jsx5(
"input",
{
type: "text",
value: value || "",
onChange: (e) => onChange(e.target.value),
placeholder: variable.defaultValue
}
);
case "number":
return /* @__PURE__ */ jsx5(
"input",
{
type: "number",
value: value || "",
onChange: (e) => onChange(e.target.value ? Number(e.target.value) : ""),
min: variable.validation?.min,
max: variable.validation?.max,
placeholder: variable.defaultValue
}
);
case "boolean":
return /* @__PURE__ */ jsx5(
"input",
{
type: "checkbox",
checked: value ?? variable.defaultValue ?? false,
onChange: (e) => onChange(e.target.checked)
}
);
case "select":
return /* @__PURE__ */ jsxs5(
"select",
{
value: value || "",
onChange: (e) => onChange(e.target.value),
children: [
/* @__PURE__ */ jsx5("option", { value: "", children: "-- Select --" }),
variable.options?.map((option) => /* @__PURE__ */ jsx5("option", { value: option.value, children: option.label }, option.value))
]
}
);
case "multiline":
return /* @__PURE__ */ jsx5(
"textarea",
{
value: value || "",
onChange: (e) => onChange(e.target.value),
placeholder: variable.defaultValue,
rows: 3
}
);
default:
return /* @__PURE__ */ jsx5(
"input",
{
type: "text",
value: value || "",
onChange: (e) => onChange(e.target.value),
placeholder: variable.defaultValue
}
);
}
}
// src/components/PromptEditor.tsx
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
var PromptEditor = ({
initialTemplate: initialTemplate2,
mode = "light",
readOnly = false,
showLineNumbers = true,
showVariablePanel = true,
showPreview = true,
height = "600px",
width = "100%",
theme,
locale = "en",
onSave,
onExport,
onImport,
customVariableTypes,
toolbar,
className,
style
}) => {
const {
template,
isDirty,
updateContent,
loadTemplate,
save
} = usePromptEditorStore();
useEffect(() => {
if (initialTemplate2) {
loadTemplate(initialTemplate2);
}
}, [initialTemplate2, loadTemplate]);
const handleEditorChange = useCallback((value) => {
if (value !== void 0 && !readOnly) {
updateContent(value);
}
}, [updateContent, readOnly]);
const handleSave = useCallback(async () => {
if (onSave) {
await onSave(template);
await save();
} else {
await save();
}
}, [template, onSave, save]);
const editorOptions = useMemo2(() => ({
readOnly,
lineNumbers: showLineNumbers ? "on" : "off",
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: "on",
fontSize: 14,
fontFamily: 'Consolas, "Courier New", monospace',
padding: { top: 16, bottom: 16 },
automaticLayout: true
}), [readOnly, showLineNumbers]);
const monacoTheme = theme?.monacoTheme || (mode === "dark" ? "vs-dark" : "vs");
const containerStyle = {
height,
width,
...style
};
return /* @__PURE__ */ jsxs6(
"div",
{
className: `prompt-editor prompt-editor--${mode} ${className || ""}`,
style: containerStyle,
"data-theme": mode,
children: [
/* @__PURE__ */ jsx6(
Toolbar,
{
config: toolbar,
isDirty,
onSave: handleSave,
onExport: onExport ? (format) => onExport(template, format) : void 0,
onImport,
locale
}
),
/* @__PURE__ */ jsxs6("div", { className: "prompt-editor__main", children: [
/* @__PURE__ */ jsx6(AnimatePresence2, { children: showVariablePanel && /* @__PURE__ */ jsx6(
motion3.div,
{
className: "prompt-editor__variables",
initial: { width: 0, opacity: 0 },
animate: { width: 300, opacity: 1 },
exit: { width: 0, opacity: 0 },
transition: { duration: 0.2 },
children: /* @__PURE__ */ jsx6(
VariablePanel,
{
variables: template.variables,
customTypes: customVariableTypes,
readOnly,
locale
}
)
}
) }),
/* @__PURE__ */ jsx6("div", { className: "prompt-editor__content", children: /* @__PURE__ */ jsx6(
MonacoEditor,
{
value: template.content,
language: "markdown",
theme: monacoTheme,
options: editorOptions,
onChange: handleEditorChange,
beforeMount: (monaco) => {
registerVariableCompletion(monaco, template.variables);
}
}
) }),
/* @__PURE__ */ jsx6(AnimatePresence2, { children: showPreview && /* @__PURE__ */ jsx6(
motion3.div,
{
className: "prompt-editor__preview",
initial: { width: 0, opacity: 0 },
animate: { width: 400, opacity: 1 },
exit: { width: 0, opacity: 0 },
transition: { duration: 0.2 },
children: /* @__PURE__ */ jsx6(
PreviewPanel,
{
template,
locale
}
)
}
) })
] })
]
}
);
};
function registerVariableCompletion(monaco, variables) {
monaco.languages.registerCompletionItemProvider("markdown", {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
const suggestions = variables.map((variable) => ({
label: `{{${variable.name}}}`,
kind: monaco.languages.CompletionItemKind.Variable,
documentation: variable.description,
insertText: `{{${variable.name}}}`,
range
}));
return { suggestions };
},
triggerCharacters: ["{"]
});
}
// ../../node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/dist/js-yaml.mjs
function isNothing(subject) {
return typeof subject === "undefined" || subject === null;
}
function isObject(subject) {
return typeof subject === "object" && subject !== null;
}
function toArray(sequence) {
if (Array.isArray(sequence))
return sequence;
else if (isNothing(sequence))
return [];
return [sequence];
}
function extend(target, source) {
var index, length, key, sourceKeys;
if (source) {
sourceKeys = Object.keys(source);
for (index = 0, length = sourceKeys.length; index < length; index += 1) {
key = sourceKeys[index];
target[key] = source[key];
}
}
return target;
}
function repeat(string, count) {
var result = "", cycle;
for (cycle = 0; cycle < count; cycle += 1) {
result += string;
}
return result;
}
function isNegativeZero(number) {
return number === 0 && Number.NEGATIVE_INFINITY === 1 / number;
}
var isNothing_1 = isNothing;
var isObject_1 = isObject;
var toArray_1 = toArray;
var repeat_1 = repeat;
var isNegativeZero_1 = isNegativeZero;
var extend_1 = extend;
var common = {
isNothing: isNothing_1,
isObject: isObject_1,
toArray: toArray_1,
repeat: repeat_1,
isNegativeZero: isNegativeZero_1,
extend: extend_1
};
function formatError(exception2, compact) {
var where = "", message = exception2.reason || "(unknown reason)";
if (!exception2.mark)
return message;
if (exception2.mark.name) {
where += 'in "' + exception2.mark.name + '" ';
}
where += "(" + (exception2.mark.line + 1) + ":" + (exception2.mark.column + 1) + ")";
if (!compact && exception2.mark.snippet) {
where += "\n\n" + exception2.mark.snippet;
}
return message + " " + where;
}
function YAMLException$1(reason, mark) {
Error.call(this);
this.name = "YAMLException";
this.reason = reason;
this.mark = mark;
this.message = formatError(this, false);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = new Error().stack || "";
}
}
YAMLException$1.prototype = Object.create(Error.prototype);
YAMLException$1.prototype.constructor = YAMLException$1;
YAMLException$1.prototype.toString = function toString(compact) {
return this.name + ": " + formatError(this, compact);
};
var exception = YAMLException$1;
function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
var head = "";
var tail = "";
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
if (position - lineStart > maxHalfLength) {
head = " ... ";
lineStart = position - maxHalfLength + head.length;
}
if (lineEnd - position > maxHalfLength) {
tail = " ...";
lineEnd = position + maxHalfLength - tail.length;
}
return {
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "\u2192") + tail,
pos: position - lineStart + head.length
// relative position
};
}
function padStart(string, max) {
return common.repeat(" ", max - string.length) + string;
}
function makeSnippet(mark, options) {
options = Object.create(options || null);
if (!mark.buffer)
return null;
if (!options.maxLength)
options.maxLength = 79;
if (typeof options.indent !== "number")
options.indent = 1;
if (typeof options.linesBefore !== "number")
options.linesBefore = 3;
if (typeof options.linesAfter !== "number")
options.linesAfter = 2;
var re = /\r?\n|\r|\0/g;
var lineStarts = [0];
var lineEnds = [];
var match;
var foundLineNo = -1;
while (match = re.exec(mark.buffer)) {
lineEnds.push(match.index);
lineStarts.push(match.index + match[0].length);
if (mark.position <= match.index && foundLineNo < 0) {
foundLineNo = lineStarts.length - 2;
}
}
if (foundLineNo < 0)
foundLineNo = lineStarts.length - 1;
var result = "", i, line;
var lineNoLength = Math.min(mark.line + options.linesAfter, lineEnds.length).toString().length;
var maxLineLength = options.maxLength - (options.indent + lineNoLength + 3);
for (i = 1; i <= options.linesBefore; i++) {
if (foundLineNo - i < 0)
break;
line = getLine(
mark.buffer,
lineStarts[foundLineNo - i],
lineEnds[foundLineNo - i],
mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]),
maxLineLength
);
result = common.repeat(" ", options.indent) + padStart((mark.line - i + 1).toString(), lineNoLength) + " | " + line.str + "\n" + result;
}
line = getLine(mark.buffer, lineStarts[foundLineNo], lineEnds[foundLineNo], mark.position, maxLineLength);
result += common.repeat(" ", options.indent) + padStart((mark.line + 1).toString(), lineNoLength) + " | " + line.str + "\n";
result += common.repeat("-", options.indent + lineNoLength + 3 + line.pos) + "^\n";
for (i = 1; i <= options.linesAfter; i++) {
if (foundLineNo + i >= lineEnds.length)
break;
line = getLine(
mark.buffer,
lineStarts[foundLineNo + i],
lineEnds[foundLineNo + i],
mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]),
maxLineLength
);
result += common.repeat(" ", options.indent) + padStart((mark.line + i + 1).toString(), lineNoLength) + " | " + line.str + "\n";
}
return result.replace(/\n$/, "");
}
var snippet = makeSnippet;
var TYPE_CONSTRUCTOR_OPTIONS = [
"kind",
"multi",
"resolve",
"construct",
"instanceOf",
"predicate",
"represent",
"representName",
"defaultStyle",
"styleAliases"
];
var YAML_NODE_KINDS = [
"scalar",
"sequence",
"mapping"
];
function compileStyleAliases(map2) {
var result = {};
if (map2 !== null) {
Object.keys(map2).forEach(function(style) {
map2[style].forEach(function(alias) {
result[String(alias)] = style;
});
});
}
return result;
}
function Type$1(tag, options) {
options = options || {};
Object.keys(options).forEach(function(name) {
if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) {
throw new exception('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.');
}
});
this.options = options;
this.tag = tag;
this.kind = options["kind"] || null;
this.resolve = options["resolve"] || function() {
return true;
};
this.construct = options["construct"] || function(data) {
return data;
};
this.instanceOf = options["instanceOf"] || null;
this.predicate = options["predicate"] || null;
this.represent = options["represent"] || null;
this.representName = options["representName"] || null;
this.defaultStyle = options["defaultStyle"] || null;
this.multi = options["multi"] || false;
this.styleAliases = compileStyleAliases(options["styleAliases"] || null);
if (YAML_NODE_KINDS.indexOf(this.kind) === -1) {
throw new exception('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.');
}
}
var type = Type$1;
function compileList(schema2, name) {
var result = [];
schema2[name].forEach(function(currentType) {
var newIndex = result.length;
result.forEach(function(previousType, previousIndex) {
if (previousType.tag === currentType.tag && previousType.kind === currentType.kind && previousType.multi === currentType.multi) {
newIndex = previousIndex;
}
});
result[newIndex] = currentType;
});
return result;
}
function compileMap() {
var result = {
scalar: {},
sequence: {},
mapping: {},
fallback: {},
multi: {
scalar: [],
sequence: [],
mapping: [],
fallback: []
}
}, index, length;
function collectType(type2) {
if (type2.multi) {
result.multi[type2.kind].push(type2);
result.multi["fallback"].push(type2);
} else {
result[type2.kind][type2.tag] = result["fallback"][type2.tag] = type2;
}
}
for (index = 0, length = arguments.length; index < length; index += 1) {
arguments[index].forEach(collectType);
}
return result;
}
function Schema$1(definition) {
return this.extend(definition);
}
Schema$1.prototype.extend = function extend2(definition) {
var implicit = [];
var explicit = [];
if (definition instanceof type) {
explicit.push(definition);
} else if (Array.isArray(definition)) {
explicit = explicit.concat(definition);
} else if (definition && (Array.isArray(definition.implicit) || Array.isArray(definition.explicit))) {
if (definition.implicit)
implicit = implicit.concat(definition.implicit);
if (definition.explicit)
explicit = explicit.concat(definition.explicit);
} else {
throw new exception("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");
}
implicit.forEach(function(type$1) {
if (!(type$1 instanceof type)) {
throw new exception("Specified list of YAML types (or a single Type object) contains a non-Type object.");
}
if (type$1.loadKind && type$1.loadKind !== "scalar") {
throw new exception("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");
}
if (type$1.multi) {
throw new exception("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.");
}
});
explicit.forEach(function(type$1) {
if (!(type$1 instanceof type)) {