feat: template examples + recent templates quick buttons
- Add TemplateExample struct and examples scanning (local dir + git repo) - Exclude examples/ from copy_dir_recursive - Frontend: recent templates (localStorage), template-specific example buttons
This commit is contained in:
@@ -44,7 +44,7 @@ export const api = {
|
||||
}),
|
||||
|
||||
listTemplates: () =>
|
||||
request<{ id: string; name: string; description: string }[]>('/templates'),
|
||||
request<{ id: string; name: string; description: string; examples: { label: string; text: string }[] }[]>('/templates'),
|
||||
|
||||
listSteps: (workflowId: string) =>
|
||||
request<ExecutionLogEntry[]>(`/workflows/${workflowId}/steps`),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { api } from '../api'
|
||||
import examples from '../examples.json'
|
||||
|
||||
@@ -8,13 +8,40 @@ const emit = defineEmits<{
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const RECENT_KEY = 'tori-recent-templates'
|
||||
const MAX_RECENT = 3
|
||||
|
||||
const requirement = ref('')
|
||||
const inputEl = ref<HTMLTextAreaElement>()
|
||||
const templates = ref<{ id: string; name: string; description: string }[]>([])
|
||||
const templates = ref<{ id: string; name: string; description: string; examples: { label: string; text: string }[] }[]>([])
|
||||
const selectedTemplate = ref('')
|
||||
const recentTemplates = ref<{ id: string; name: string }[]>([])
|
||||
|
||||
// Load recent templates from localStorage
|
||||
function loadRecent(): { id: string; name: string }[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveRecent(id: string, name: string) {
|
||||
const list = loadRecent().filter(t => t.id !== id)
|
||||
list.unshift({ id, name })
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, MAX_RECENT)))
|
||||
}
|
||||
|
||||
// Template-specific examples based on current selection
|
||||
const templateExamples = computed(() => {
|
||||
if (!selectedTemplate.value) return []
|
||||
const t = templates.value.find(t => t.id === selectedTemplate.value)
|
||||
return t?.examples ?? []
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
inputEl.value?.focus()
|
||||
recentTemplates.value = loadRecent()
|
||||
try {
|
||||
templates.value = await api.listTemplates()
|
||||
} catch {
|
||||
@@ -22,9 +49,19 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
function selectRecentTemplate(id: string) {
|
||||
selectedTemplate.value = selectedTemplate.value === id ? '' : id
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const text = requirement.value.trim()
|
||||
if (text) emit('submit', text, selectedTemplate.value || undefined)
|
||||
if (!text) return
|
||||
const tplId = selectedTemplate.value || undefined
|
||||
if (tplId) {
|
||||
const t = templates.value.find(t => t.id === tplId)
|
||||
if (t) saveRecent(t.id, t.name)
|
||||
}
|
||||
emit('submit', text, tplId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,6 +76,32 @@ function onSubmit() {
|
||||
@click="requirement = Array.isArray(ex.text) ? ex.text.join('\n') : ex.text"
|
||||
>{{ ex.label }}</span>
|
||||
</div>
|
||||
<div v-if="templateExamples.length" class="create-examples">
|
||||
<span class="example-section-label">模板示例</span>
|
||||
<span
|
||||
v-for="ex in templateExamples"
|
||||
:key="ex.label"
|
||||
class="example-tag template-example"
|
||||
@click="requirement = ex.text"
|
||||
>{{ ex.label }}</span>
|
||||
</div>
|
||||
<div v-if="recentTemplates.length" class="recent-templates">
|
||||
<span class="example-section-label">最近</span>
|
||||
<span
|
||||
v-for="rt in recentTemplates"
|
||||
:key="rt.id"
|
||||
class="example-tag"
|
||||
:class="{ active: selectedTemplate === rt.id }"
|
||||
@click="selectRecentTemplate(rt.id)"
|
||||
>{{ rt.name }}</span>
|
||||
</div>
|
||||
<div v-if="templates.length" class="template-select">
|
||||
<label>模板</label>
|
||||
<select v-model="selectedTemplate">
|
||||
<option value="">自动选择</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="requirement"
|
||||
@@ -48,13 +111,6 @@ function onSubmit() {
|
||||
@keydown.ctrl.enter="onSubmit"
|
||||
@keydown.meta.enter="onSubmit"
|
||||
/>
|
||||
<div v-if="templates.length" class="template-select">
|
||||
<label>模板</label>
|
||||
<select v-model="selectedTemplate">
|
||||
<option value="">自动选择</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="create-hint">Ctrl+Enter 提交</div>
|
||||
<div class="create-actions">
|
||||
<button class="btn-cancel" @click="emit('cancel')">取消</button>
|
||||
@@ -81,6 +137,13 @@ function onSubmit() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-section-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.example-tag {
|
||||
@@ -100,6 +163,23 @@ function onSubmit() {
|
||||
background: rgba(79, 195, 247, 0.08);
|
||||
}
|
||||
|
||||
.example-tag.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
}
|
||||
|
||||
.example-tag.template-example {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.recent-templates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.create-textarea {
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-secondary);
|
||||
|
||||
Reference in New Issue
Block a user