PromptVault/resources/js/Pages/Prompts/Show.vue

212 lines
7.3 KiB
Vue
Raw Normal View History

2026-10-26 20:12:16 +00:00
<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>