290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Layout } from "@/components/layout/Layout";
|
|
import { FormField, FormSelect, FormTextArea, PrimaryButton, SecondaryButton } from "@/components/shared";
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
import { aiService } from "@/services/ai-service";
|
|
import { showToast } from "@/utils/toast";
|
|
|
|
type PromptVariable = {
|
|
name: string;
|
|
type: "string" | "number" | "boolean" | "array";
|
|
required: boolean;
|
|
default: string;
|
|
};
|
|
|
|
const PromptCreate = (): ReactElement => {
|
|
const navigate = useNavigate();
|
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
const [providers, setProviders] = useState<Array<{ value: string; label: string }>>([]);
|
|
const [models, setModels] = useState<Array<{ value: string; label: string }>>([]);
|
|
|
|
const [form, setForm] = useState({
|
|
name: "",
|
|
description: "",
|
|
use_case: "",
|
|
system_message: "",
|
|
user_template: "",
|
|
provider: "",
|
|
model: "",
|
|
temperature: "0.3",
|
|
max_tokens: "2048",
|
|
tags: "",
|
|
is_default: false,
|
|
});
|
|
const [variables, setVariables] = useState<PromptVariable[]>([
|
|
{ name: "focus_areas", type: "string", required: false, default: "regulatory obligations, deadlines, action items" },
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const loadMeta = async (): Promise<void> => {
|
|
try {
|
|
const [providerData, modelData] = await Promise.all([aiService.getProviders(), aiService.getModels()]);
|
|
setProviders(providerData.map((p) => ({ value: p.name, label: p.displayName || p.name })));
|
|
setModels(modelData.map((m) => ({ value: m.id, label: `${m.id} (${m.provider})` })));
|
|
} catch (err: unknown) {
|
|
showToast.error(
|
|
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error?.message ||
|
|
"Failed to load provider metadata",
|
|
);
|
|
}
|
|
};
|
|
void loadMeta();
|
|
}, []);
|
|
|
|
const parsedTags = useMemo(
|
|
() =>
|
|
form.tags
|
|
.split(",")
|
|
.map((tag) => tag.trim())
|
|
.filter(Boolean),
|
|
[form.tags],
|
|
);
|
|
|
|
const addVariable = (): void => {
|
|
setVariables((prev) => [...prev, { name: "", type: "string", required: false, default: "" }]);
|
|
};
|
|
|
|
const updateVariable = (index: number, patch: Partial<PromptVariable>): void => {
|
|
setVariables((prev) => prev.map((item, idx) => (idx === index ? { ...item, ...patch } : item)));
|
|
};
|
|
|
|
const removeVariable = (index: number): void => {
|
|
setVariables((prev) => prev.filter((_, idx) => idx !== index));
|
|
};
|
|
|
|
const handleSubmit = async (): Promise<void> => {
|
|
if (!form.name.trim() || !form.use_case.trim() || !form.user_template.trim()) {
|
|
showToast.error("Name, use case, and user template are required");
|
|
return;
|
|
}
|
|
|
|
const sanitizedVariables = variables
|
|
.filter((v) => v.name.trim())
|
|
.map((v) => ({
|
|
name: v.name.trim(),
|
|
type: v.type,
|
|
required: v.required,
|
|
...(v.default.trim() ? { default: v.default.trim() } : {}),
|
|
}));
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await aiService.createPrompt({
|
|
name: form.name.trim(),
|
|
description: form.description.trim() || undefined,
|
|
use_case: form.use_case.trim(),
|
|
system_message: form.system_message.trim() || undefined,
|
|
user_template: form.user_template,
|
|
model: form.model || undefined,
|
|
provider: form.provider || undefined,
|
|
temperature: Number(form.temperature),
|
|
max_tokens: Number(form.max_tokens),
|
|
variables: sanitizedVariables,
|
|
tags: parsedTags,
|
|
is_default: form.is_default,
|
|
});
|
|
showToast.success("Prompt created successfully");
|
|
navigate("/tenant/ai/prompts");
|
|
} catch (err: unknown) {
|
|
showToast.error(
|
|
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error?.message ||
|
|
"Failed to create prompt",
|
|
);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Layout
|
|
currentPage="Create Prompt"
|
|
pageHeader={{
|
|
title: "Create Prompt",
|
|
description: "Create a reusable prompt template for AI workflows.",
|
|
}}
|
|
>
|
|
<div className="max-w-5xl mx-auto bg-white border border-[rgba(0,0,0,0.08)] rounded-lg">
|
|
<div className="p-5 md:p-6 space-y-5">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormField
|
|
label="Template Name"
|
|
required
|
|
value={form.name}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
placeholder="e.g., Code Review Assitant"
|
|
helperText="Max 255 characters. Must be unique per tenant and version"
|
|
/>
|
|
<FormField
|
|
label="Use Case"
|
|
required
|
|
value={form.use_case}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, use_case: e.target.value }))}
|
|
placeholder="doc_summary"
|
|
/>
|
|
</div>
|
|
|
|
<FormTextArea
|
|
label="Description"
|
|
value={form.description}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
|
|
placeholder="Describe what this prompt template is used for..."
|
|
helperText="Optional template summary to explain the purpose and expected usage."
|
|
/>
|
|
|
|
<FormTextArea
|
|
label="System Message"
|
|
value={form.system_message}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, system_message: e.target.value }))}
|
|
placeholder="You are a regulatory compliance expert..."
|
|
/>
|
|
|
|
<FormTextArea
|
|
label="User Template"
|
|
required
|
|
value={form.user_template}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, user_template: e.target.value }))}
|
|
placeholder={"Summarize the following document:\n\n{{document_text}}\n\nFocus on: {{focus_areas}}"}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<FormSelect
|
|
label="Provider"
|
|
value={form.provider}
|
|
onValueChange={(value) => setForm((prev) => ({ ...prev, provider: value }))}
|
|
options={providers}
|
|
placeholder="Select provider"
|
|
/>
|
|
<FormSelect
|
|
label="Model"
|
|
value={form.model}
|
|
onValueChange={(value) => setForm((prev) => ({ ...prev, model: value }))}
|
|
options={models}
|
|
placeholder="Select model"
|
|
/>
|
|
<FormField
|
|
label="Temperature"
|
|
type="number"
|
|
value={form.temperature}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, temperature: e.target.value }))}
|
|
/>
|
|
<FormField
|
|
label="Max Tokens"
|
|
type="number"
|
|
value={form.max_tokens}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, max_tokens: e.target.value }))}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
label="Tags (comma separated)"
|
|
value={form.tags}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, tags: e.target.value }))}
|
|
placeholder="compliance, document, summary"
|
|
/>
|
|
|
|
<label className="inline-flex items-center gap-2 text-sm text-[#334155]">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.is_default}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, is_default: e.target.checked }))}
|
|
className="w-4 h-4"
|
|
/>
|
|
Set as default prompt
|
|
</label>
|
|
|
|
<div className="border border-[rgba(0,0,0,0.08)] rounded-md p-4 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-[#0f1724]">Variables</h3>
|
|
<button
|
|
type="button"
|
|
onClick={addVariable}
|
|
className="inline-flex items-center gap-1 text-xs font-medium text-[#112868]"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
Add Variable
|
|
</button>
|
|
</div>
|
|
|
|
{variables.length === 0 && <p className="text-xs text-[#94a3b8]">No variables added.</p>}
|
|
|
|
{variables.map((variable, index) => (
|
|
<div key={index} className="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
|
<FormField
|
|
label="Name"
|
|
value={variable.name}
|
|
onChange={(e) => updateVariable(index, { name: e.target.value })}
|
|
placeholder="focus_areas"
|
|
/>
|
|
<FormSelect
|
|
label="Type"
|
|
value={variable.type}
|
|
onValueChange={(value) => updateVariable(index, { type: value as PromptVariable["type"] })}
|
|
options={[
|
|
{ value: "string", label: "string" },
|
|
{ value: "number", label: "number" },
|
|
{ value: "boolean", label: "boolean" },
|
|
{ value: "array", label: "array" },
|
|
]}
|
|
/>
|
|
<FormField
|
|
label="Default"
|
|
value={variable.default}
|
|
onChange={(e) => updateVariable(index, { default: e.target.value })}
|
|
placeholder="optional default"
|
|
/>
|
|
<label className="inline-flex items-center gap-2 text-sm text-[#334155] h-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={variable.required}
|
|
onChange={(e) => updateVariable(index, { required: e.target.checked })}
|
|
className="w-4 h-4"
|
|
/>
|
|
Required
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeVariable(index)}
|
|
className="h-10 inline-flex items-center justify-center gap-1 text-xs font-medium text-[#ef4444] border border-[rgba(239,68,68,0.2)] rounded-md"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
Remove
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2 border-t border-[rgba(0,0,0,0.06)]">
|
|
<SecondaryButton onClick={() => navigate("/tenant/ai/prompts")}>Cancel</SecondaryButton>
|
|
<PrimaryButton onClick={handleSubmit} disabled={isSubmitting}>
|
|
{isSubmitting ? "Creating..." : "Create Prompt"}
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default PromptCreate;
|