PromptVault/resources/js/Pages/Prompts/Show.vue
2026-10-26 15:12:16 -05:00

212 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>