Files
X-Agents/web/src/views/Agents.vue
DESKTOP-72TV0V4\caoxiaozhu bce8b9240b feat: 优化 Chat 页面和 Agents 页面
- 优化 Chat 页面交互和消息显示
- 增强 Agents 页面功能
- 改进 ChatAgentSelector 组件
- 优化 ChatMessage 和 ChatSidebar 组件
- 更新聊天逻辑 useAgents 和 chat 模块

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:48:42 +08:00

639 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onUnmounted } from 'vue'
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
import { useAgents } from './agents/useAgents'
import './agents/agents.css'
const {
agents,
skillsList,
modelsList,
searchQuery,
filterStatus,
isLoading,
skillsLoading,
modelsLoading,
showCreateModal,
isCreating,
newAgent,
showEditModal,
isEditing,
editingAgent,
showSkillsDropdown,
showSubSkillsDropdown,
skillsSearch,
skillsModeOptions,
avatarOptions,
knowledgeOptions,
skillsOptions,
filteredSkills,
filteredAgents,
stats,
isAllSelected,
isIndeterminate,
isAllSelectedEdit,
isIndeterminateEdit,
fetchAgents,
fetchSkills,
fetchModels,
getSkillLabel,
openCreateModal,
createAgent,
openEdit,
saveEdit,
toggleStatus,
deleteAgent,
toggleSkillsDropdown,
closeSkillsDropdown,
handleSkillsModeClick,
handleSkillsModeClickEdit,
toggleSelectAll,
clearSkills,
toggleSelectAllEdit,
clearSkillsEdit,
toggleSkillsMode,
selectSkillsMode,
toggleSubSkillsDropdown,
closeAllDropdowns,
getSkillsDisplayText,
toggleSkillSelection,
selectAllSkills,
statusClass,
cleanup
} = useAgents()
onUnmounted(() => {
cleanup()
})
</script>
<template>
<!-- 主内容区域 -->
<div class="p-6 min-h-screen">
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<i class="fa-solid fa-robot text-orange-500 text-xl"></i>
<span class="text-xl font-semibold text-white">Agents</span>
</div>
<div class="flex items-center gap-3 text-sm">
<span class="text-gray-400">Total:</span>
<span class="text-white font-medium">{{ stats.total }}</span>
<span class="w-1 h-1 rounded-full bg-gray-500"></span>
<span class="text-gray-400">Active:</span>
<span class="text-green-400 font-medium">{{ stats.active }}</span>
<span class="w-1 h-1 rounded-full bg-gray-500"></span>
<span class="text-gray-400">Inactive:</span>
<span class="text-gray-400 font-medium">{{ stats.inactive }}</span>
</div>
</div>
<button @click="openCreateModal" class="btn-primary">
<i class="fa-solid fa-plus"></i>
New Agent
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search agents by name or skills..."
class="search-input w-full"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
<el-option label="All Status" value="all" />
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
</el-select>
</div>
<!-- Agents 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<!-- 加载状态 -->
<div v-if="isLoading" class="flex items-center justify-center py-20">
<div class="flex flex-col items-center gap-3">
<i class="fa-solid fa-circle-notch fa-spin text-3xl text-orange-500"></i>
<span class="text-gray-400">Loading agents...</span>
</div>
</div>
<table v-else-if="filteredAgents.length > 0" class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-4 py-3 text-sm font-medium text-gray-400">Agent Name</th>
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Skills</th>
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Model</th>
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-center px-4 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
>
{{ agent.avatar }}
</div>
<div>
<div class="font-medium text-white">{{ agent.name }}</div>
<div class="text-xs text-gray-500">{{ agent.description }}</div>
</div>
</div>
</td>
<td class="px-4 py-3 text-center">
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.skills }}</span>
</td>
<td class="px-4 py-3 text-center">
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.model }}</span>
</td>
<td class="px-4 py-3 text-center">
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs transition-all duration-200" :class="agent.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
<span class="w-1.5 h-1.5 rounded-full animate-pulse" :class="agent.status === 'active' ? 'bg-green-500' : 'bg-gray-400'"></span>
<span class="capitalize">{{ agent.status }}</span>
</span>
</td>
<td class="px-4 py-3 text-center text-gray-400 text-sm">{{ agent.createdAt }}</td>
<td class="px-4 py-3">
<div class="flex items-center justify-center gap-2">
<button
@click="toggleStatus(agent)"
class="btn-icon"
:title="agent.status === 'active' ? 'Deactivate' : 'Activate'"
>
<Pause v-if="agent.status === 'active'" class="w-4 h-4 text-gray-400 hover:text-yellow-400 transition-colors" />
<Play v-else class="w-4 h-4 text-gray-400 hover:text-green-400 transition-colors" />
</button>
<button @click="openEdit(agent)" class="btn-icon" title="Edit">
<Edit class="w-4 h-4 text-gray-400 hover:text-white transition-colors" />
</button>
<button
@click.stop="deleteAgent(agent.id)"
class="btn-icon"
title="Delete"
>
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-500 transition-colors" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredAgents.length === 0 && !isLoading" class="empty-box">
<div class="empty-icon">
<i class="fa-solid fa-robot"></i>
</div>
<p class="empty-text">{{ searchQuery || filterStatus !== 'all' ? 'No matching agents found' : 'No agents found' }}</p>
<p class="empty-tip">{{ searchQuery || filterStatus !== 'all' ? 'Try adjusting your search or filter' : 'Click "New Agent" to create one' }}</p>
</div>
</div>
</div>
<!-- 创建智能体弹窗 -->
<Teleport to="body">
<div v-if="showCreateModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="showCreateModal = false">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Create New Agent</h3>
<button @click="showCreateModal = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Agent Name *</label>
<input
v-model="newAgent.name"
type="text"
placeholder="Enter agent name..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="newAgent.description"
rows="3"
placeholder="Describe what this agent does..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Avatar</label>
<div class="flex flex-wrap gap-2">
<button
v-for="avatar in avatarOptions"
:key="avatar"
type="button"
@click="newAgent.avatar = avatar"
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg transition-all"
:class="newAgent.avatar === avatar ? 'bg-primary-orange text-white ring-2 ring-orange-400' : 'bg-dark-600 text-gray-300 hover:bg-dark-500'"
>
{{ avatar }}
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Preferred Model</label>
<el-select
v-model="newAgent.modelId"
placeholder="Select a model..."
class="w-full"
size="large"
clearable
>
<el-option
v-for="model in modelsList"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="flex items-center justify-between">
<span>{{ model.name }}</span>
<span class="text-xs text-gray-500">{{ model.provider }} - {{ model.model }}</span>
</div>
</el-option>
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
<!-- 技能模式选择 - 两级下拉框 -->
<div class="skills-selector">
<!-- 已选 tags 显示区域 -->
<div class="selected-tags" @click="toggleSkillsDropdown">
<div v-if="newAgent.skillsMode === 'all'" class="placeholder">
All Skills
</div>
<div v-else class="tags-container">
<span
v-for="skillId in newAgent.selectedSkills.slice(0, 3)"
:key="skillId"
class="selected-tag"
>
{{ getSkillLabel(skillId) }}
</span>
<span v-if="newAgent.selectedSkills.length > 3" class="more-tag">
+{{ newAgent.selectedSkills.length - 3 }}
</span>
</div>
<svg class="dropdown-icon" :class="{ 'rotate-180': showSkillsDropdown }" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</div>
<!-- 第一级下拉面板模式选择 -->
<Transition name="dropdown">
<div v-if="showSkillsDropdown" class="dropdown-panel">
<div class="skills-mode-options">
<div
v-for="option in skillsModeOptions"
:key="option.value"
class="skills-mode-item"
:class="{ 'active': newAgent.skillsMode === option.value }"
@click="handleSkillsModeClick(option.value)"
>
<div class="mode-radio">
<div v-if="newAgent.skillsMode === option.value" class="radio-dot"></div>
</div>
<div class="mode-content">
<span class="mode-label">{{ option.label }}</span>
<span class="mode-desc">{{ option.desc }}</span>
</div>
<!-- 显示展开图标 -->
<svg v-if="option.value !== 'all'" class="sub-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
</div>
</div>
</div>
</Transition>
<!-- 第二级下拉面板技能列表悬浮窗形式 -->
<Transition name="dropdown">
<div v-if="showSubSkillsDropdown" class="sub-dropdown-panel" @click.stop>
<!-- 标题 -->
<div class="sub-dropdown-header">
<span class="sub-dropdown-title">
{{ newAgent.skillsMode === 'include' ? 'Select skills to include' : 'Select skills to exclude' }}
</span>
<button @click="showSubSkillsDropdown = false" class="sub-dropdown-close">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="search-box">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<input
v-model="skillsSearch"
type="text"
placeholder="Search skills..."
class="search-input"
>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<label class="checkbox-label cursor-pointer">
<input
type="checkbox"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
class="checkbox"
>
<span class="checkbox-text">Select All</span>
</label>
<button
v-if="newAgent.selectedSkills.length > 0"
type="button"
@click="clearSkills"
class="clear-btn"
>
Clear
</button>
</div>
<!-- 技能列表 -->
<div class="options-list">
<template v-if="filteredSkills.length > 0">
<label
v-for="skill in filteredSkills"
:key="skill.value"
class="option-item"
>
<input
type="checkbox"
:value="skill.value"
v-model="newAgent.selectedSkills"
class="checkbox"
>
<div class="option-content">
<span class="option-label">{{ skill.label }}</span>
<span v-if="skill.desc" class="option-desc">{{ skill.desc }}</span>
</div>
</label>
</template>
<div v-else class="no-results">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="no-results-icon">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
</svg>
<span>No skills available</span>
</div>
</div>
</div>
</Transition>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Custom Prompt</label>
<textarea
v-model="newAgent.prompt"
rows="4"
placeholder="Define the agent's behavior and instructions..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="showCreateModal = false"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="createAgent"
:disabled="isCreating || !newAgent.name || (newAgent.skillsMode !== 'all' && newAgent.selectedSkills.length === 0)"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<i v-if="isCreating" class="fa-solid fa-circle-notch fa-spin"></i>
{{ isCreating ? 'Creating...' : 'Create Agent' }}
</button>
</div>
</div>
</div>
</Teleport>
<!-- 编辑智能体弹窗 -->
<Teleport to="body">
<div v-if="showEditModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="showEditModal = false">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Agent</h3>
<button @click="showEditModal = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Agent Name *</label>
<input
v-model="editingAgent.name"
type="text"
placeholder="Enter agent name..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="editingAgent.description"
rows="3"
placeholder="Describe what this agent does..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Avatar</label>
<div class="flex flex-wrap gap-2">
<button
v-for="avatar in avatarOptions"
:key="avatar"
type="button"
@click="editingAgent.avatar = avatar"
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg transition-all"
:class="editingAgent.avatar === avatar ? 'bg-primary-orange text-white ring-2 ring-orange-400' : 'bg-dark-600 text-gray-300 hover:bg-dark-500'"
>
{{ avatar }}
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Preferred Model</label>
<el-select
v-model="editingAgent.modelId"
placeholder="Select a model..."
class="w-full"
size="large"
clearable
>
<el-option
v-for="model in modelsList"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="flex items-center justify-between">
<span>{{ model.name }}</span>
<span class="text-xs text-gray-500">{{ model.provider }} - {{ model.model }}</span>
</div>
</el-option>
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
<!-- 技能模式选择 -->
<div class="skills-selector">
<div class="selected-tags" @click="toggleSkillsDropdown">
<div v-if="editingAgent.skillsMode === 'all'" class="placeholder">
All Skills
</div>
<div v-else class="tags-container">
<span
v-for="skillId in editingAgent.selectedSkills.slice(0, 3)"
:key="skillId"
class="selected-tag"
>
{{ getSkillLabel(skillId) }}
</span>
<span v-if="editingAgent.selectedSkills.length > 3" class="more-tag">
+{{ editingAgent.selectedSkills.length - 3 }}
</span>
</div>
<svg class="dropdown-icon" :class="{ 'rotate-180': showSkillsDropdown }" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</div>
<Transition name="dropdown">
<div v-if="showSkillsDropdown" class="dropdown-panel">
<div class="skills-mode-options">
<div
v-for="option in skillsModeOptions"
:key="option.value"
class="skills-mode-item"
:class="{ 'active': editingAgent.skillsMode === option.value }"
@click="handleSkillsModeClickEdit(option.value)"
>
<div class="mode-radio">
<div v-if="editingAgent.skillsMode === option.value" class="radio-dot"></div>
</div>
<div class="mode-content">
<span class="mode-label">{{ option.label }}</span>
<span class="mode-desc">{{ option.desc }}</span>
</div>
<svg v-if="option.value !== 'all'" class="sub-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
</div>
</div>
</div>
</Transition>
<Transition name="dropdown">
<div v-if="showSubSkillsDropdown" class="sub-dropdown-panel" @click.stop>
<div class="sub-dropdown-header">
<span class="sub-dropdown-title">
{{ editingAgent.skillsMode === 'include' ? 'Select skills to include' : 'Select skills to exclude' }}
</span>
<button @click="showSubSkillsDropdown = false" class="sub-dropdown-close">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
</svg>
</button>
</div>
<div class="search-box">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<input v-model="skillsSearch" type="text" placeholder="Search skills..." class="search-input">
</div>
<div class="action-bar">
<label class="checkbox-label cursor-pointer">
<input type="checkbox" :checked="isAllSelectedEdit" :indeterminate="isIndeterminateEdit" @change="toggleSelectAllEdit" class="checkbox">
<span class="checkbox-text">Select All</span>
</label>
<button v-if="editingAgent.selectedSkills.length > 0" type="button" @click="clearSkillsEdit" class="clear-btn">Clear</button>
</div>
<div class="options-list">
<template v-if="filteredSkills.length > 0">
<label v-for="skill in filteredSkills" :key="skill.value" class="option-item">
<input type="checkbox" :value="skill.value" v-model="editingAgent.selectedSkills" class="checkbox">
<div class="option-content">
<span class="option-label">{{ skill.label }}</span>
<span v-if="skill.desc" class="option-desc">{{ skill.desc }}</span>
</div>
</label>
</template>
<div v-else class="no-results">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="no-results-icon">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
</svg>
<span>No skills available</span>
</div>
</div>
</div>
</Transition>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Prompt</label>
<textarea
v-model="editingAgent.prompt"
rows="4"
placeholder="Define the agent's behavior and instructions..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="showEditModal = false"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="saveEdit"
:disabled="isEditing || !editingAgent.name"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<i v-if="isEditing" class="fa-solid fa-circle-notch fa-spin"></i>
{{ isEditing ? 'Saving...' : 'Save' }}
</button>
</div>
</div>
</div>
</Teleport>
</template>