253 lines
8.0 KiB
TypeScript
253 lines
8.0 KiB
TypeScript
|
|
import { defineComponent } from 'vue'
|
||
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||
|
|
import { useRoute } from 'vue-router'
|
||
|
|
import { ElMessage } from 'element-plus'
|
||
|
|
import { chunkApi, questionApi, modelApi } from '@/core/api'
|
||
|
|
|
||
|
|
export default defineComponent({
|
||
|
|
name: 'QuestionManage',
|
||
|
|
setup() {
|
||
|
|
const route = useRoute()
|
||
|
|
const projectId = computed(() => route.params.id)
|
||
|
|
|
||
|
|
const loading = ref(false)
|
||
|
|
const isInitialLoad = ref(true)
|
||
|
|
const generating = ref(false)
|
||
|
|
const questions = ref([])
|
||
|
|
const chunks = ref([])
|
||
|
|
const availableModels = ref([])
|
||
|
|
const showGenerateDialog = ref(false)
|
||
|
|
const filterStatus = ref('')
|
||
|
|
const chunkMap = ref({})
|
||
|
|
|
||
|
|
const DEFAULT_GENERATE_PROMPT = '你是一名高质量中文问答数据构建助手。请基于给定 chunk 内容生成准确、自然、可用于训练的数据集问答对。问题必须清晰具体,答案必须直接来自内容或基于内容做合理概括,不要编造原文没有的信息,不要输出与目录、导航、页眉页脚、噪声文字相关的问题。'
|
||
|
|
|
||
|
|
const generateConfig = reactive({
|
||
|
|
model_id: '',
|
||
|
|
chunk_ids: [],
|
||
|
|
count: 3,
|
||
|
|
dirty_data_filter: true,
|
||
|
|
thinking_mode: true,
|
||
|
|
preset_prompt: DEFAULT_GENERATE_PROMPT
|
||
|
|
})
|
||
|
|
|
||
|
|
// Multi-select
|
||
|
|
const selectedQuestions = ref([])
|
||
|
|
|
||
|
|
const filteredQuestions = computed(() => {
|
||
|
|
if (!filterStatus.value) return questions.value
|
||
|
|
return questions.value.filter(q => q.source === filterStatus.value)
|
||
|
|
})
|
||
|
|
|
||
|
|
const generatedCount = computed(() => questions.value.filter(q => q.source === 'generated').length)
|
||
|
|
const manualCount = computed(() => questions.value.filter(q => q.source === 'manual').length)
|
||
|
|
const failedCount = computed(() => questions.value.filter(q => q.status === 'failed').length)
|
||
|
|
const generateModels = computed(() => {
|
||
|
|
return availableModels.value.filter(model => {
|
||
|
|
const type = normalizeModelType(model.model_type, model.model_name)
|
||
|
|
return type === 'chat' || type === 'vlm'
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
const isAllSelected = computed(() => filteredQuestions.value.length > 0 && selectedQuestions.value.length === filteredQuestions.value.length)
|
||
|
|
const selectedCount = computed(() => selectedQuestions.value.length)
|
||
|
|
|
||
|
|
const toggleSelectAll = () => {
|
||
|
|
if (isAllSelected.value) {
|
||
|
|
selectedQuestions.value = []
|
||
|
|
} else {
|
||
|
|
selectedQuestions.value = filteredQuestions.value.map(q => q.id)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleSelect = (id) => {
|
||
|
|
const index = selectedQuestions.value.indexOf(id)
|
||
|
|
if (index === -1) {
|
||
|
|
selectedQuestions.value.push(id)
|
||
|
|
} else {
|
||
|
|
selectedQuestions.value.splice(index, 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const isSelected = (id) => selectedQuestions.value.includes(id)
|
||
|
|
|
||
|
|
const clearSelection = () => {
|
||
|
|
selectedQuestions.value = []
|
||
|
|
}
|
||
|
|
|
||
|
|
const batchDelete = async () => {
|
||
|
|
if (selectedQuestions.value.length === 0) return
|
||
|
|
try {
|
||
|
|
for (const id of selectedQuestions.value) {
|
||
|
|
await questionApi.delete(projectId.value, id)
|
||
|
|
}
|
||
|
|
ElMessage.success(`已删除 ${selectedQuestions.value.length} 个问题`)
|
||
|
|
selectedQuestions.value = []
|
||
|
|
fetchQuestions()
|
||
|
|
} catch (error) {
|
||
|
|
ElMessage.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizeModelType = (modelType, modelName = '') => {
|
||
|
|
if (modelType && modelType !== 'chat') {
|
||
|
|
return modelType
|
||
|
|
}
|
||
|
|
const normalizedName = String(modelName).trim().toLowerCase()
|
||
|
|
if (['rerank', 'bce-reranker', 'gte-rerank'].some(keyword => normalizedName.includes(keyword))) return 'rerank'
|
||
|
|
if (['embedding', 'embed', 'text-embedding', 'bge-', 'gte-', 'm3e', 'e5-', 'jina-embeddings'].some(keyword => normalizedName.includes(keyword))) return 'embedding'
|
||
|
|
if (['vl', 'vision', 'visual', 'multimodal', 'qwen-vl', 'gpt-4o'].some(keyword => normalizedName.includes(keyword))) return 'vlm'
|
||
|
|
return 'chat'
|
||
|
|
}
|
||
|
|
|
||
|
|
const getProviderLabel = (provider) => {
|
||
|
|
const map = {
|
||
|
|
openai: 'OpenAI Compatible',
|
||
|
|
minimax: 'MiniMax',
|
||
|
|
glm: 'GLM',
|
||
|
|
ali: '阿里云百炼'
|
||
|
|
}
|
||
|
|
return map[provider] || provider
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchAvailableModels = async () => {
|
||
|
|
try {
|
||
|
|
const res = await modelApi.list()
|
||
|
|
availableModels.value = Array.isArray(res) ? res : (res?.data || [])
|
||
|
|
if (!generateConfig.model_id && generateModels.value.length) {
|
||
|
|
const defaultModel = generateModels.value.find(model => model.is_default === 'true') || generateModels.value[0]
|
||
|
|
generateConfig.model_id = defaultModel?.id || ''
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
availableModels.value = []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchAllChunks = async () => {
|
||
|
|
const allChunks = []
|
||
|
|
let page = 1
|
||
|
|
let total = 0
|
||
|
|
do {
|
||
|
|
const res = await chunkApi.list(projectId.value, { page, page_size: 100 })
|
||
|
|
const items = res.items || res.data || []
|
||
|
|
total = res.total || res.pagination?.total || items.length
|
||
|
|
allChunks.push(...items)
|
||
|
|
page += 1
|
||
|
|
} while (allChunks.length < total)
|
||
|
|
return allChunks
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchQuestions = async () => {
|
||
|
|
const wasInitial = isInitialLoad.value
|
||
|
|
loading.value = true
|
||
|
|
try {
|
||
|
|
const [chunkList, questionRes] = await Promise.all([
|
||
|
|
fetchAllChunks(),
|
||
|
|
questionApi.list(projectId.value, { page: 1, page_size: 500 })
|
||
|
|
])
|
||
|
|
chunks.value = chunkList
|
||
|
|
chunkMap.value = Object.fromEntries(chunkList.map(chunk => [chunk.id, chunk]))
|
||
|
|
questions.value = questionRes.items || questionRes.data || []
|
||
|
|
} catch (error) {
|
||
|
|
questions.value = []
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
if (wasInitial) {
|
||
|
|
isInitialLoad.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleGenerate = async () => {
|
||
|
|
if (generateConfig.chunk_ids.length === 0) {
|
||
|
|
ElMessage.warning('请选择文本块')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (!generateConfig.model_id) {
|
||
|
|
ElMessage.warning('请选择生成模型')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
generating.value = true
|
||
|
|
try {
|
||
|
|
await questionApi.generate(projectId.value, generateConfig)
|
||
|
|
ElMessage.success('问题生成任务已启动')
|
||
|
|
showGenerateDialog.value = false
|
||
|
|
setTimeout(fetchQuestions, 2000)
|
||
|
|
} catch (error) {
|
||
|
|
ElMessage.error('生成失败')
|
||
|
|
} finally {
|
||
|
|
generating.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = async (question) => {
|
||
|
|
try {
|
||
|
|
await questionApi.delete(projectId.value, question.id)
|
||
|
|
ElMessage.success('删除成功')
|
||
|
|
fetchQuestions()
|
||
|
|
} catch (error) {
|
||
|
|
ElMessage.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const getTypeColor = (type) => {
|
||
|
|
const map = { 'fact': '#22c55e', 'summary': '#818cf8', 'reasoning': '#f59e0b' }
|
||
|
|
return map[type] || '#818cf8'
|
||
|
|
}
|
||
|
|
|
||
|
|
const getTypeName = (type) => {
|
||
|
|
const map = { 'fact': '事实性', 'summary': '总结性', 'reasoning': '推理性' }
|
||
|
|
return map[type] || type
|
||
|
|
}
|
||
|
|
|
||
|
|
const getSourceName = (source) => {
|
||
|
|
const map = { 'generated': 'AI生成', 'manual': '手动', 'failed': '失败' }
|
||
|
|
return map[source] || source
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
fetchAvailableModels()
|
||
|
|
fetchQuestions()
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
route,
|
||
|
|
projectId,
|
||
|
|
loading,
|
||
|
|
isInitialLoad,
|
||
|
|
generating,
|
||
|
|
questions,
|
||
|
|
chunks,
|
||
|
|
availableModels,
|
||
|
|
showGenerateDialog,
|
||
|
|
filterStatus,
|
||
|
|
chunkMap,
|
||
|
|
DEFAULT_GENERATE_PROMPT,
|
||
|
|
generateConfig,
|
||
|
|
selectedQuestions,
|
||
|
|
filteredQuestions,
|
||
|
|
generatedCount,
|
||
|
|
manualCount,
|
||
|
|
failedCount,
|
||
|
|
generateModels,
|
||
|
|
isAllSelected,
|
||
|
|
selectedCount,
|
||
|
|
toggleSelectAll,
|
||
|
|
toggleSelect,
|
||
|
|
isSelected,
|
||
|
|
clearSelection,
|
||
|
|
batchDelete,
|
||
|
|
normalizeModelType,
|
||
|
|
getProviderLabel,
|
||
|
|
fetchAvailableModels,
|
||
|
|
fetchAllChunks,
|
||
|
|
fetchQuestions,
|
||
|
|
handleGenerate,
|
||
|
|
handleDelete,
|
||
|
|
getTypeColor,
|
||
|
|
getTypeName,
|
||
|
|
getSourceName
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|