Settings: prompts como tarjetas + modal editor TipTap (negrita, listas, código, etc.)
- PromptEditorModal.vue: nuevo componente con editor TipTap rich text toolbar: bold, italic, headings, bullet/ordered lists, code, quote, undo/redo - SettingsView: prompts se muestran como tarjetas con preview y botón editar clic abre modal con editor completo + guardar/restaurar - Instalado @tiptap/vue-3 con starter-kit y extensiones (placeholder, code, link, table)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Bold, Italic, List, ListOrdered, Code, Heading, Quote, Undo, Redo } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
title: string
|
||||
content: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
save: [content: string]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.content,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3] },
|
||||
}),
|
||||
Placeholder.configure({ placeholder: 'Editá el prompt...' }),
|
||||
],
|
||||
onUpdate: () => {
|
||||
currentContent.value = editor.value?.getHTML() || ''
|
||||
},
|
||||
})
|
||||
|
||||
const currentContent = ref(props.content)
|
||||
|
||||
watch(() => props.content, (val) => {
|
||||
currentContent.value = val
|
||||
editor.value?.commands.setContent(val)
|
||||
})
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
setTimeout(() => editor.value?.commands.setContent(props.content), 50)
|
||||
}
|
||||
})
|
||||
|
||||
function handleSave() {
|
||||
const html = editor.value?.getHTML() || ''
|
||||
// Convert HTML to plain text for storage (los prompts son texto plano)
|
||||
const text = html
|
||||
.replace(/<p>/g, '')
|
||||
.replace(/<\/p>/g, '\n')
|
||||
.replace(/<br\s*\/?>/g, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
emit('save', text)
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function toggleBold() { editor.value?.chain().focus().toggleBold().run() }
|
||||
function toggleItalic() { editor.value?.chain().focus().toggleItalic().run() }
|
||||
function toggleBulletList() { editor.value?.chain().focus().toggleBulletList().run() }
|
||||
function toggleOrderedList() { editor.value?.chain().focus().toggleOrderedList().run() }
|
||||
function toggleCode() { editor.value?.chain().focus().toggleCodeBlock().run() }
|
||||
function toggleHeading(level: 1 | 2 | 3) { editor.value?.chain().focus().toggleHeading({ level }).run() }
|
||||
function toggleBlockquote() { editor.value?.chain().focus().toggleBlockquote().run() }
|
||||
function undo() { editor.value?.chain().focus().undo().run() }
|
||||
function redo() { editor.value?.chain().focus().redo().run() }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||
<DialogContent class="sm:max-w-[800px] max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="text-sm">{{ title }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-0.5 p-1 rounded-lg bg-muted/50 border mb-2 flex-wrap">
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleBold" title="Negrita"><Bold class="size-3.5" /></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleItalic" title="Cursiva"><Italic class="size-3.5" /></button>
|
||||
<span class="w-px h-5 bg-border mx-0.5" />
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleHeading(1)" title="H1"><Heading class="size-3.5" /><sup class="text-[8px]">1</sup></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleHeading(2)" title="H2"><Heading class="size-3.5" /><sup class="text-[8px]">2</sup></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleHeading(3)" title="H3"><Heading class="size-3.5" /><sup class="text-[8px]">3</sup></button>
|
||||
<span class="w-px h-5 bg-border mx-0.5" />
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleBulletList" title="Lista"><List class="size-3.5" /></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleOrderedList" title="Lista numerada"><ListOrdered class="size-3.5" /></button>
|
||||
<span class="w-px h-5 bg-border mx-0.5" />
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleCode" title="Código"><Code class="size-3.5" /></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="toggleBlockquote" title="Cita"><Quote class="size-3.5" /></button>
|
||||
<span class="w-px h-5 bg-border mx-0.5" />
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="undo" title="Deshacer"><Undo class="size-3.5" /></button>
|
||||
<button class="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors" @click="redo" title="Rehacer"><Redo class="size-3.5" /></button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="flex-1 overflow-y-auto min-h-[300px] rounded-lg border p-4 text-sm">
|
||||
<EditorContent :editor="editor" class="prose prose-sm dark:prose-invert max-w-none focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between pt-3 border-t mt-2">
|
||||
<Button size="sm" variant="ghost" class="text-xs text-muted-foreground" @click="emit('reset')">
|
||||
Restaurar default
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" variant="outline" class="text-xs" @click="emit('update:open', false)">Cancelar</Button>
|
||||
<Button size="sm" class="text-xs" @click="handleSave">Guardar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tiptap {
|
||||
outline: none;
|
||||
min-height: 250px;
|
||||
}
|
||||
.tiptap p.is-editor-empty:first-child::before {
|
||||
color: hsl(var(--muted-foreground));
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.tiptap pre {
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tiptap blockquote {
|
||||
border-left: 3px solid hsl(var(--border));
|
||||
padding-left: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.tiptap ul, .tiptap ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.tiptap h1 { font-size: 1.25rem; font-weight: 700; margin: 0.75rem 0 0.5rem; }
|
||||
.tiptap h2 { font-size: 1.1rem; font-weight: 600; margin: 0.5rem 0 0.25rem; }
|
||||
.tiptap h3 { font-size: 1rem; font-weight: 600; margin: 0.5rem 0 0.25rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user