212 lines
7.3 KiB
Vue
212 lines
7.3 KiB
Vue
<script setup>
|
||
import { Head, Link, router } from '@inertiajs/vue3';
|
||
import { ref } from 'vue';
|
||
|
||
import ConfirmDialog from '../../Components/ConfirmDialog.vue';
|
||
import RenderPreview from '../../Components/RenderPreview.vue';
|
||
import TagPill from '../../Components/TagPill.vue';
|
||
import { formatDate } from '../../lib/format.js';
|
||
|
||
const props = defineProps({
|
||
prompt: { type: Object, required: true },
|
||
variables: { type: Array, required: true },
|
||
});
|
||
|
||
const rendered = ref(null);
|
||
const useError = ref(null);
|
||
const using = ref(false);
|
||
const showDelete = ref(false);
|
||
const bodyCopied = ref(false);
|
||
|
||
const favorite = () => {
|
||
router.post(`/prompts/${props.prompt.id}/toggle-favorite`, {}, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const duplicate = () => {
|
||
router.post(`/prompts/${props.prompt.id}/duplicate`);
|
||
};
|
||
|
||
const usePrompt = (values) => {
|
||
using.value = true;
|
||
rendered.value = null;
|
||
useError.value = null;
|
||
|
||
fetch(`/prompts/${props.prompt.id}/use`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json',
|
||
},
|
||
body: JSON.stringify({ values }),
|
||
})
|
||
.then(async (response) => {
|
||
const payload = await response.json().catch(() => ({}));
|
||
|
||
if (!response.ok) {
|
||
useError.value = {
|
||
message: payload.message ?? 'Could not render the prompt.',
|
||
missing: payload.missing ?? [],
|
||
};
|
||
|
||
return;
|
||
}
|
||
|
||
rendered.value = payload.rendered;
|
||
router.reload({ preserveScroll: true });
|
||
})
|
||
.catch(() => {
|
||
useError.value = { message: 'Request failed.' };
|
||
})
|
||
.finally(() => {
|
||
using.value = false;
|
||
});
|
||
};
|
||
|
||
const confirmDelete = () => {
|
||
showDelete.value = false;
|
||
router.delete(`/prompts/${props.prompt.id}`);
|
||
};
|
||
|
||
const copyBody = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(props.prompt.body);
|
||
bodyCopied.value = true;
|
||
setTimeout(() => (bodyCopied.value = false), 1500);
|
||
} catch {
|
||
// Clipboard unavailable — ignore.
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<Head :title="prompt.name" />
|
||
<div>
|
||
<Link href="/prompts" class="text-sm text-slate-500 hover:text-slate-800 hover:underline">
|
||
← All prompts
|
||
</Link>
|
||
|
||
<div class="mt-2 flex flex-wrap items-start justify-between gap-3">
|
||
<div class="min-w-0">
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
class="text-xl leading-none"
|
||
:class="prompt.is_favorite ? 'text-amber-500' : 'text-slate-300 hover:text-slate-400'"
|
||
:aria-label="prompt.is_favorite ? 'Unfavorite' : 'Favorite'"
|
||
@click="favorite"
|
||
>
|
||
★
|
||
</button>
|
||
<h1 class="truncate text-2xl font-semibold tracking-tight text-slate-900">
|
||
{{ prompt.name }}
|
||
</h1>
|
||
</div>
|
||
<p v-if="prompt.description" class="mt-1 text-sm text-slate-600">
|
||
{{ prompt.description }}
|
||
</p>
|
||
<div class="mt-2 flex flex-wrap items-center gap-1.5">
|
||
<TagPill
|
||
v-for="tag in prompt.tags"
|
||
:key="tag.id"
|
||
:name="tag.name"
|
||
filterable
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 text-sm">
|
||
<Link
|
||
:href="`/prompts/${prompt.id}/edit`"
|
||
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 shadow-sm hover:bg-slate-50"
|
||
>
|
||
✎ Edit
|
||
</Link>
|
||
<button
|
||
type="button"
|
||
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 shadow-sm hover:bg-slate-50"
|
||
@click="duplicate"
|
||
>
|
||
Duplicate
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="rounded-md border border-red-200 bg-white px-3 py-1.5 text-red-600 shadow-sm hover:bg-red-50"
|
||
@click="showDelete = true"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<dl class="mt-4 flex flex-wrap gap-x-8 gap-y-1 text-xs text-slate-500">
|
||
<div>
|
||
<dt class="inline font-medium">Usage:</dt>
|
||
<dd class="inline">{{ prompt.usage_count }}×</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="inline font-medium">Last used:</dt>
|
||
<dd class="inline">{{ formatDate(prompt.last_used_at) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="inline font-medium">Created:</dt>
|
||
<dd class="inline">{{ formatDate(prompt.created_at) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="inline font-medium">Updated:</dt>
|
||
<dd class="inline">{{ formatDate(prompt.updated_at) }}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<div class="mt-6 grid gap-6 lg:grid-cols-2">
|
||
<div class="space-y-2">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-sm font-semibold text-slate-900">Template</h2>
|
||
<button
|
||
type="button"
|
||
class="text-xs text-slate-500 hover:text-slate-800 hover:underline"
|
||
@click="copyBody"
|
||
>
|
||
{{ bodyCopied ? '✓ Copied' : 'Copy template' }}
|
||
</button>
|
||
</div>
|
||
<pre
|
||
class="max-h-[28rem] overflow-auto whitespace-pre-wrap rounded-lg border border-slate-200 bg-white p-4 font-mono text-xs leading-relaxed text-slate-700 shadow-sm"
|
||
>{{ prompt.body }}</pre>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<RenderPreview
|
||
:body="prompt.body"
|
||
:variables="variables"
|
||
use-label="Use prompt (counts usage)"
|
||
:busy="using"
|
||
@use="usePrompt"
|
||
/>
|
||
<p
|
||
v-if="useError"
|
||
class="rounded-md bg-red-50 px-3 py-2 text-xs text-red-700"
|
||
>
|
||
{{ useError.message }}
|
||
<template v-if="useError.missing?.length">
|
||
({{ useError.missing.join(', ') }})
|
||
</template>
|
||
</p>
|
||
<pre
|
||
v-if="rendered"
|
||
class="max-h-60 overflow-auto whitespace-pre-wrap rounded-md bg-emerald-50 p-3 font-mono text-xs leading-relaxed text-emerald-900"
|
||
>{{ rendered }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ConfirmDialog
|
||
:open="showDelete"
|
||
title="Delete prompt?"
|
||
:message="`“${prompt.name}” will be permanently deleted.`"
|
||
confirm-label="Delete"
|
||
@confirm="confirmDelete"
|
||
@cancel="showDelete = false"
|
||
/>
|
||
</template>
|