Phase 4: Template system with auto-select and pre-fill

Backend:
- Add templates.json with 5 template definitions (news, policy, business, fiction, social)
- Add template API (/api/template/list, /api/template/auto-select, /api/template/:id/filter-rules)
- Register template blueprint in Flask app

Frontend:
- Add template API client (frontend/src/api/template.js)
- Add template selector UI in Home.vue (chip buttons + auto-select button)
- Add template state management and auto-select logic

Locale:
- Add template keys for th/en/zh

Entity filter rules in templates.json for context-aware filtering in Step 1.
This commit is contained in:
Kunthawat Greethong
2026-06-26 11:44:55 +07:00
parent 3c4c2183c7
commit 166ef73ad2
8 changed files with 434 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
import service from './index'
/**
* Get all available templates
*/
export const getTemplates = () => {
return service.get('/api/template/list')
}
/**
* Auto-select template based on seed data
* @param {Object} data - { text, simulation_requirement? }
*/
export const autoSelectTemplate = (data) => {
return service.post('/api/template/auto-select', data)
}
/**
* Get entity filter rules for a template
* @param {string} templateId
*/
export const getTemplateFilterRules = (templateId) => {
return service.get(`/api/template/${templateId}/filter-rules`)
}

View File

@@ -202,6 +202,43 @@
</div>
</div>
<!-- Template Selector -->
<div class="template-section">
<div class="template-header">
<span class="console-label">{{ $t('templates.title') }}</span>
<button
v-if="files.length > 0 && !templateAutoSelected"
class="auto-select-btn"
@click="autoSelectTemplate"
:disabled="templateLoading"
>
<span v-if="templateLoading"></span>
<span v-else></span>
{{ $t('templates.autoSelect') }}
</button>
</div>
<div class="template-grid">
<button
v-for="tmpl in availableTemplates"
:key="tmpl.id"
class="template-chip"
:class="{ active: selectedTemplate === tmpl.id }"
@click="selectTemplate(tmpl)"
>
<span class="template-chip-icon">{{ tmpl.icon }}</span>
<span class="template-chip-name">{{ tmpl.name }}</span>
</button>
<button
class="template-chip"
:class="{ active: selectedTemplate === 'custom' }"
@click="selectedTemplate = 'custom'"
>
<span class="template-chip-icon"></span>
<span class="template-chip-name">{{ $t('templates.custom') }}</span>
</button>
</div>
</div>
<!-- 分割线 -->
<div class="console-divider">
<span>{{ $t('home.inputParams') }}</span>
@@ -247,10 +284,11 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
import { getTemplates, autoSelectTemplate as autoSelectApi } from '../api/template'
const router = useRouter()
@@ -267,6 +305,52 @@ const loading = ref(false)
const error = ref('')
const isDragOver = ref(false)
// Template state
const availableTemplates = ref([])
const selectedTemplate = ref('custom')
const templateLoading = ref(false)
const templateAutoSelected = ref(false)
// Load templates on mount
onMounted(async () => {
try {
const res = await getTemplates()
if (res.success) {
availableTemplates.value = res.templates
}
} catch (e) {
console.error('Failed to load templates:', e)
}
})
// Select template and fill prompt
const selectTemplate = (tmpl) => {
selectedTemplate.value = tmpl.id
formData.value.simulationRequirement = tmpl.prompt
}
// Auto-select template from uploaded files
const autoSelectTemplate = async () => {
if (files.value.length === 0) return
templateLoading.value = true
try {
// Extract text from first file (simplified — in production read file content)
const text = formData.value.simulationRequirement || files.value[0].name
const res = await autoSelectApi({ text })
if (res.success) {
selectedTemplate.value = res.template_id
formData.value.simulationRequirement = res.prompt
templateAutoSelected.value = true
}
} catch (e) {
console.error('Auto-select failed:', e)
} finally {
templateLoading.value = false
}
}
// 文件输入引用
const fileInput = ref(null)
@@ -770,6 +854,79 @@ const startSimulation = () => {
padding: 8px; /* 内边距形成双重边框感 */
}
/* Template Section */
.template-section {
padding: 16px 20px;
border-bottom: 1px solid #E5E7EB;
}
.template-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.auto-select-btn {
background: linear-gradient(135deg, #FF6B35, #FF8F65);
color: #fff;
border: none;
border-radius: 6px;
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.auto-select-btn:hover {
opacity: 0.9;
}
.auto-select-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.template-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.template-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
background: #F3F4F6;
border: 1px solid #E5E7EB;
border-radius: 20px;
font-size: 0.78rem;
color: #374151;
cursor: pointer;
transition: all 0.15s;
}
.template-chip:hover {
border-color: #FF6B35;
background: #FFF7F3;
}
.template-chip.active {
background: #FF6B35;
color: #fff;
border-color: #FF6B35;
}
.template-chip-icon {
font-size: 1rem;
}
.template-chip-name {
font-weight: 500;
}
.console-section {
padding: 20px;
}