Files
geo/apps/admin-web/src/views/KolGenerateView.vue
T
root 162abdc97c
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s
chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:39:09 +08:00

402 lines
9.6 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 KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
import KolDynamicForm from '@/components/kol/KolDynamicForm.vue'
import { kolGenerateApi } from '@/lib/api'
import { formatError } from '@/lib/errors'
import { parseKolCardConfig } from '@/lib/kol-card-config'
import {
getKolVariableInitialValue,
isKolVariableValueEmpty,
normalizeKolVariableDefinition,
} from '@/lib/kol-placeholders'
import { LeftOutlined } from '@ant-design/icons-vue'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const queryClient = useQueryClient()
const { t } = useI18n()
const subscriptionPromptId = computed(() => String(route.params.subscriptionPromptId))
const sourcePage = computed<'workspace' | 'templates' | null>(() => {
if (route.query.source === 'workspace') {
return 'workspace'
}
if (route.query.source === 'templates') {
return 'templates'
}
return null
})
function resolveReturnRoute(): { name: 'workspace' } | { name: 'articles-templates' } {
if (sourcePage.value === 'workspace') {
return { name: 'workspace' }
}
return { name: 'articles-templates' }
}
const schemaQuery = useQuery({
queryKey: ['kol', 'subscription-prompt', 'schema', subscriptionPromptId],
queryFn: () => kolGenerateApi.schema(subscriptionPromptId.value),
enabled: computed(() => !!subscriptionPromptId.value),
})
const values = ref<Record<string, any>>({})
const enableWebSearch = ref(false)
const selectedKnowledgeGroupIds = ref<number[]>([])
const schema = computed(() => schemaQuery.data.value)
const cardConfig = computed(() => parseKolCardConfig(schema.value?.card_config_json))
const allowWebSearch = computed(() => cardConfig.value.allow_web_search)
const allowUserKnowledge = computed(() => cardConfig.value.allow_user_knowledge)
function fieldKey(key?: string | null, id?: string): string {
return key?.trim() || id || ''
}
// Initialize values from schema
watch(
() => schemaQuery.data.value,
(schema) => {
if (schema?.schema_json?.variables) {
const newValues: Record<string, any> = {}
schema.schema_json.variables.forEach((v) => {
newValues[fieldKey(v.key, v.id)] = getKolVariableInitialValue(
normalizeKolVariableDefinition(v),
)
})
values.value = newValues
enableWebSearch.value = false
selectedKnowledgeGroupIds.value = []
}
},
{ immediate: true },
)
const generateMutation = useMutation({
mutationFn: (variables: Record<string, any>) =>
kolGenerateApi.submit(subscriptionPromptId.value, {
variables,
enable_web_search: allowWebSearch.value ? enableWebSearch.value : undefined,
knowledge_group_ids: allowUserKnowledge.value ? selectedKnowledgeGroupIds.value : undefined,
}),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['articles'] }),
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
])
message.info(t('templates.wizard.messages.queued'))
await router.replace(resolveReturnRoute())
},
onError: (error: any) => {
message.error(formatError(error) || t('common.noData'))
},
})
function handleBack() {
void router.push(resolveReturnRoute())
}
function handleSubmit() {
const schemaValue = schemaQuery.data.value
if (!schemaValue?.schema_json?.variables) return
const missingLabels: string[] = []
schemaValue.schema_json.variables.forEach((v) => {
const normalizedVariable = normalizeKolVariableDefinition(v)
if (
normalizedVariable.required &&
isKolVariableValueEmpty(normalizedVariable, values.value[fieldKey(v.key, v.id)])
) {
missingLabels.push(v.label)
}
})
if (missingLabels.length > 0) {
message.warning(`${t('kol.generate.fillVariables')}: ${missingLabels.join(', ')}`)
return
}
generateMutation.mutate(values.value)
}
</script>
<template>
<div class="wizard-page">
<div v-if="schemaQuery.isPending.value" class="loading-state">
<a-spin size="large" />
</div>
<template v-else-if="schema">
<header class="wizard-page__header">
<div class="header-left">
<a-button type="text" shape="circle" @click="handleBack">
<template #icon><LeftOutlined /></template>
</a-button>
<div class="header-title-box">
<h2 class="header-title">
{{ schema.package_name }}
<span class="divider">/</span>
{{ schema.prompt_name }}
<a-tag v-if="schema.platform_hint" color="cyan" class="platform-badge">
{{ schema.platform_hint }}
</a-tag>
</h2>
<p class="header-eyebrow">{{ t('kol.generate.fillVariables') }}</p>
</div>
</div>
</header>
<main class="wizard-page__main">
<section class="wizard-section">
<div class="wizard-card">
<div class="wizard-card__header" style="margin-bottom: 24px">
<div>
<h3>{{ t('kol.generate.fillVariables') }}</h3>
<p>根据设定提供相关信息</p>
</div>
</div>
<div v-if="allowWebSearch || allowUserKnowledge" class="capability-card">
<div v-if="allowWebSearch" class="capability-row">
<div class="capability-copy">
<div class="capability-label">联网搜索</div>
<div class="capability-hint">按需启用生成时可参考实时网页信息</div>
</div>
<a-switch
v-model:checked="enableWebSearch"
:disabled="generateMutation.isPending.value"
/>
</div>
<div v-if="allowUserKnowledge" class="capability-knowledge">
<label class="capability-label">知识库引用</label>
<KnowledgeGroupSelect
v-model="selectedKnowledgeGroupIds"
:disabled="generateMutation.isPending.value"
placeholder="可选,选择你自己的知识库分组辅助生成"
/>
</div>
</div>
<KolDynamicForm
v-model="values"
:variables="schema.schema_json.variables"
class="form-body"
/>
</div>
</section>
</main>
<footer class="wizard-page__footer">
<div></div>
<div class="footer-actions">
<a-button
type="primary"
:loading="generateMutation.isPending.value"
@click="handleSubmit"
>
{{ t('kol.generate.submit') }}
</a-button>
</div>
</footer>
</template>
</div>
</template>
<style scoped>
.wizard-page {
display: flex;
flex-direction: column;
min-height: calc(100vh - 120px);
background: #fff;
}
.loading-state {
display: flex;
justify-content: center;
padding: 100px;
}
.wizard-page__header {
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
border-bottom: 1px solid #edf2f7;
background: #fff;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-title-box {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-title {
margin: 0;
color: #111827;
font-size: 20px;
font-weight: 800;
display: flex;
align-items: center;
}
.divider {
margin: 0 8px;
color: #d9d9d9;
font-weight: 400;
}
.platform-badge {
margin-left: 8px;
}
.header-eyebrow {
margin: 0;
color: #6d7c94;
font-size: 13px;
}
.wizard-page__main {
display: flex;
flex-direction: column;
margin: 0 auto;
width: 100%;
max-width: 860px;
padding: 24px 32px 64px;
}
.wizard-section {
display: flex;
flex-direction: column;
}
.wizard-card {
padding: 32px 0;
}
.wizard-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.wizard-card__header h3 {
margin: 0;
color: #111827;
font-size: 16px;
font-weight: 800;
display: flex;
align-items: center;
}
.wizard-card__header h3::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
border-radius: 2px;
background: #111827;
margin-right: 12px;
}
.wizard-card__header p {
margin: 6px 0 0 16px;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.capability-card {
margin-bottom: 32px;
padding: 20px;
border: 1px solid #e6edf5;
border-radius: 12px;
background: #fafcff;
}
.capability-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.capability-knowledge {
margin-top: 20px;
}
.capability-copy {
min-width: 0;
}
.capability-label {
display: block;
margin-bottom: 8px;
color: #111827;
font-size: 14px;
font-weight: 600;
}
.capability-hint {
color: #667085;
font-size: 13px;
line-height: 1.5;
}
.wizard-page__footer {
position: sticky;
bottom: 0;
z-index: 100;
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid rgba(20, 44, 88, 0.08);
backdrop-filter: blur(14px);
background: rgba(255, 255, 255, 0.88);
}
.footer-actions {
display: flex;
gap: 12px;
}
@media (max-width: 768px) {
.wizard-page__main {
padding: 16px;
}
.wizard-card {
padding: 18px 0;
}
.wizard-page__footer {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.footer-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>