feat(chat): add prompt template library with / command palette (M9.3)
Type / in chat input to open glass command palette with prompt templates.
Templates support {{variable}} substitution with a mini form. Built-in
templates for Explain, Compare, Summarise, Translate. Templates stored
in localStorage, importable/exportable as JSON. Arrow key navigation
and Enter to select.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
77b985db9c
commit
751edd2790
@@ -59,6 +59,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Prompt palette -->
|
||||
<PromptPalette
|
||||
ref="paletteRef"
|
||||
:query="paletteQuery"
|
||||
:is-open="isPaletteMode"
|
||||
@select="handlePaletteSelect"
|
||||
@close="closePalette"
|
||||
/>
|
||||
|
||||
<!-- Drag overlay -->
|
||||
<div
|
||||
v-if="isDragging"
|
||||
@@ -89,7 +98,7 @@
|
||||
rows="1"
|
||||
:placeholder="placeholder"
|
||||
class="flex-1 resize-none bg-transparent text-sm outline-none min-h-[24px] max-h-[120px] text-white/90 placeholder:text-white/25"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
@keydown="handleKeydown"
|
||||
@input="autoResize"
|
||||
@paste="onPaste"
|
||||
@focus="focused = true"
|
||||
@@ -152,6 +161,7 @@
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useFederatedSearch, type SearchResult } from '@/composables/useFederatedSearch'
|
||||
import SearchResults from '@/components/ui/SearchResults.vue'
|
||||
import PromptPalette from './PromptPalette.vue'
|
||||
import type { ImageAttachment } from '@aiui/core/types/message'
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
@@ -187,6 +197,63 @@ const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const images = ref<ImageAttachment[]>([])
|
||||
|
||||
const paletteRef = ref<InstanceType<typeof PromptPalette> | null>(null)
|
||||
|
||||
// Prompt palette: opens when text starts with "/" (but not "/search " or "/code" etc.)
|
||||
const isPaletteMode = computed(() => {
|
||||
const t = text.value.trimStart()
|
||||
return t === '/' || (t.startsWith('/') && !t.includes(' ') && !t.startsWith('/search') && !t.startsWith('/code') && !t.startsWith('/nostr') && !t.startsWith('/exit'))
|
||||
})
|
||||
|
||||
const paletteQuery = computed(() => {
|
||||
if (!isPaletteMode.value) return ''
|
||||
return text.value.trimStart().slice(1) // strip leading /
|
||||
})
|
||||
|
||||
function handlePaletteSelect(templateText: string) {
|
||||
text.value = templateText
|
||||
nextTick(() => {
|
||||
autoResize()
|
||||
textareaRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function closePalette() {
|
||||
// Add a space to exit palette mode
|
||||
if (text.value.trimStart() === '/') {
|
||||
text.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (isPaletteMode.value && !paletteRef.value?.hasSelectedTemplate) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
paletteRef.value?.navigateUp()
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
paletteRef.value?.navigateDown()
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
paletteRef.value?.selectHighlighted()
|
||||
return
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
closePalette()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
}
|
||||
|
||||
const canSend = computed(() => (text.value.trim().length > 0 || images.value.length > 0) && !props.disabled)
|
||||
|
||||
function send() {
|
||||
|
||||
Reference in New Issue
Block a user