199 lines
6.8 KiB
Vue
199 lines
6.8 KiB
Vue
|
|
<script setup>
|
|||
|
|
import { Link, useForm, usePage } from '@inertiajs/vue3';
|
|||
|
|
import { computed, ref } from 'vue';
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
initial: { type: Object, required: true },
|
|||
|
|
suggestedTags: { type: Array, required: true },
|
|||
|
|
method: { type: String, default: 'post' },
|
|||
|
|
action: { type: String, required: true },
|
|||
|
|
submitLabel: { type: String, default: 'Submit' },
|
|||
|
|
cancelHref: { type: String, default: '/prompts' },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const page = usePage();
|
|||
|
|
|
|||
|
|
const PLACEHOLDER = '{{variable_name}}';
|
|||
|
|
|
|||
|
|
const form = useForm({
|
|||
|
|
name: props.initial.name ?? '',
|
|||
|
|
description: props.initial.description ?? '',
|
|||
|
|
body: props.initial.body ?? '',
|
|||
|
|
tags: (props.initial.tags ?? []).map((tag) => String(tag)),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const tagInput = ref('');
|
|||
|
|
|
|||
|
|
const errors = computed(() => page.props.errors ?? {});
|
|||
|
|
|
|||
|
|
const availableSuggestions = computed(() =>
|
|||
|
|
props.suggestedTags.filter((tag) => !form.tags.includes(tag))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const addTag = (raw) => {
|
|||
|
|
const value = String(raw ?? '').trim().toLowerCase();
|
|||
|
|
|
|||
|
|
if (value === '' || form.tags.includes(value)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
form.tags.push(value);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const removeTag = (index) => {
|
|||
|
|
form.tags.splice(index, 1);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const onTagKeydown = (event) => {
|
|||
|
|
if (event.key === 'Enter' || event.key === ',') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
addTag(tagInput.value);
|
|||
|
|
tagInput.value = '';
|
|||
|
|
} else if (event.key === 'Backspace' && tagInput.value === '' && form.tags.length > 0) {
|
|||
|
|
removeTag(form.tags.length - 1);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const submit = () => {
|
|||
|
|
if (tagInput.value.trim() !== '') {
|
|||
|
|
addTag(tagInput.value);
|
|||
|
|
tagInput.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
form.transform(() => ({
|
|||
|
|
name: form.name,
|
|||
|
|
description: form.description?.trim() === '' ? null : form.description.trim(),
|
|||
|
|
body: form.body,
|
|||
|
|
tags: [...form.tags],
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const submitter = props.method === 'put' ? form.put.bind(form) : form.post.bind(form);
|
|||
|
|
|
|||
|
|
submitter(props.action, {
|
|||
|
|
preserveScroll: true,
|
|||
|
|
onError: () => form.clearErrors(),
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<form
|
|||
|
|
class="space-y-4 rounded-lg border border-slate-200 bg-white p-5 shadow-sm"
|
|||
|
|
@submit.prevent="submit"
|
|||
|
|
>
|
|||
|
|
<div>
|
|||
|
|
<label for="prompt-name" class="mb-1 block text-sm font-medium text-slate-700">
|
|||
|
|
Name
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
id="prompt-name"
|
|||
|
|
v-model="form.name"
|
|||
|
|
type="text"
|
|||
|
|
maxlength="120"
|
|||
|
|
class="w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
|
|||
|
|
/>
|
|||
|
|
<p v-if="errors.name" class="mt-1 text-xs text-red-600">{{ errors.name }}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label
|
|||
|
|
for="prompt-description"
|
|||
|
|
class="mb-1 block text-sm font-medium text-slate-700"
|
|||
|
|
>
|
|||
|
|
Description <span class="font-normal text-slate-400">(optional)</span>
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
id="prompt-description"
|
|||
|
|
v-model="form.description"
|
|||
|
|
rows="2"
|
|||
|
|
maxlength="500"
|
|||
|
|
class="w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
|
|||
|
|
/>
|
|||
|
|
<p v-if="errors.description" class="mt-1 text-xs text-red-600">
|
|||
|
|
{{ errors.description }}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="prompt-body" class="mb-1 block text-sm font-medium text-slate-700">
|
|||
|
|
Prompt body
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
id="prompt-body"
|
|||
|
|
v-model="form.body"
|
|||
|
|
rows="8"
|
|||
|
|
spellcheck="false"
|
|||
|
|
class="w-full rounded-md border border-slate-300 px-3 py-2 font-mono text-sm shadow-sm focus:border-slate-500 focus:outline-none"
|
|||
|
|
/>
|
|||
|
|
<p class="mt-1 text-xs text-slate-500">
|
|||
|
|
Wrap placeholders like <code class="rounded bg-slate-100 px-1 py-0.5">{{ PLACEHOLDER }}</code>
|
|||
|
|
</p>
|
|||
|
|
<p v-if="errors.body" class="mt-1 text-xs text-red-600">{{ errors.body }}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="prompt-tags" class="mb-1 block text-sm font-medium text-slate-700">
|
|||
|
|
Tags
|
|||
|
|
</label>
|
|||
|
|
<div
|
|||
|
|
class="flex flex-wrap items-center gap-1.5 rounded-md border border-slate-300 px-2 py-1.5 shadow-sm focus-within:border-slate-500"
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
v-for="(tag, index) in form.tags"
|
|||
|
|
:key="tag"
|
|||
|
|
class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700"
|
|||
|
|
>
|
|||
|
|
{{ tag }}
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="text-slate-400 hover:text-slate-700"
|
|||
|
|
:aria-label="`Remove tag ${tag}`"
|
|||
|
|
@click="removeTag(index)"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</span>
|
|||
|
|
<input
|
|||
|
|
id="prompt-tags"
|
|||
|
|
v-model="tagInput"
|
|||
|
|
type="text"
|
|||
|
|
placeholder="Type a tag, press Enter…"
|
|||
|
|
class="min-w-28 flex-1 border-0 bg-transparent p-0 text-sm focus:outline-none"
|
|||
|
|
@keydown="onTagKeydown"
|
|||
|
|
@blur="addTag(tagInput)"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<p v-if="errors.tags" class="mt-1 text-xs text-red-600">
|
|||
|
|
{{ Array.isArray(errors.tags) ? errors.tags[0] : errors.tags }}
|
|||
|
|
</p>
|
|||
|
|
<div v-if="availableSuggestions.length > 0" class="mt-2 flex flex-wrap gap-1.5">
|
|||
|
|
<button
|
|||
|
|
v-for="tag in availableSuggestions"
|
|||
|
|
:key="tag"
|
|||
|
|
type="button"
|
|||
|
|
class="rounded-full border border-dashed border-slate-300 px-2 py-0.5 text-xs text-slate-500 hover:border-slate-400 hover:text-slate-700"
|
|||
|
|
@click="addTag(tag)"
|
|||
|
|
>
|
|||
|
|
+ {{ tag }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex items-center justify-between border-t border-slate-100 pt-4">
|
|||
|
|
<Link
|
|||
|
|
:href="cancelHref"
|
|||
|
|
class="text-sm text-slate-500 hover:text-slate-800 hover:underline"
|
|||
|
|
>
|
|||
|
|
Cancel
|
|||
|
|
</Link>
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
:disabled="form.processing"
|
|||
|
|
class="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{{ form.processing ? 'Saving…' : submitLabel }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</template>
|