Qassure-frontend/src/pages/tenant/PromptCreate.tsx

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;