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:
24
frontend/src/api/template.js
Normal file
24
frontend/src/api/template.js
Normal 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`)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user