2026-04-17 14:08:34 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-05-09 09:40:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
DownOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
LeftOutlined,
|
|
|
|
|
|
SaveOutlined,
|
|
|
|
|
|
SendOutlined,
|
|
|
|
|
|
} from '@ant-design/icons-vue'
|
2026-05-01 20:39:09 +08:00
|
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
|
|
|
|
|
|
import EditorAiAssistPanel from '@/components/editor-common/EditorAiAssistPanel.vue'
|
|
|
|
|
|
import { kolManageApi } from '@/lib/api'
|
|
|
|
|
|
import { formatError } from '@/lib/errors'
|
|
|
|
|
|
import { mergeKolCardConfig, parseKolCardConfig } from '@/lib/kol-card-config'
|
2026-04-18 15:01:40 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getKolVariableInitialValue,
|
|
|
|
|
|
normalizeKolVariableDefinition,
|
|
|
|
|
|
syncKolVariablesWithContent,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
} from '@/lib/kol-placeholders'
|
|
|
|
|
|
import { buildKolPlatformOptions } from '@/lib/kol-platform-options'
|
|
|
|
|
|
import type {
|
|
|
|
|
|
KolAssistRequest,
|
|
|
|
|
|
KolAssistStreamEvent,
|
|
|
|
|
|
KolCardConfig,
|
|
|
|
|
|
KolVariableDefinition,
|
|
|
|
|
|
} from '@geo/shared-types'
|
|
|
|
|
|
import KolDynamicForm from './KolDynamicForm.vue'
|
|
|
|
|
|
import KolPromptEditArea from './KolPromptEditArea.vue'
|
|
|
|
|
|
import KolVariableConfig from './KolVariableConfig.vue'
|
|
|
|
|
|
import KolVariablePanel from './KolVariablePanel.vue'
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
promptId: number
|
|
|
|
|
|
}>()
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
type AiOptimizeStatus = 'idle' | 'generating' | 'completed' | 'error'
|
|
|
|
|
|
type EditorAiAssistPlacement = 'bottom' | 'top'
|
2026-04-25 22:41:40 +08:00
|
|
|
|
type EditorAiAssistBoundary = {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
left: number
|
|
|
|
|
|
top: number
|
|
|
|
|
|
right: number
|
|
|
|
|
|
bottom: number
|
|
|
|
|
|
}
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
|
|
|
|
|
type PromptSelectionAiSnapshot = {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
start: number
|
|
|
|
|
|
end: number
|
|
|
|
|
|
selectedText: string
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
width: number
|
|
|
|
|
|
placement: EditorAiAssistPlacement
|
|
|
|
|
|
boundary: EditorAiAssistBoundary
|
|
|
|
|
|
}
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
|
|
|
|
|
type PromptSelectionAiPanelState = PromptSelectionAiSnapshot & {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
open: boolean
|
|
|
|
|
|
}
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const emit = defineEmits<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
back: []
|
|
|
|
|
|
}>()
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
const queryClient = useQueryClient()
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-25 22:41:40 +08:00
|
|
|
|
function createDefaultPromptSelectionAiPanelState(): PromptSelectionAiPanelState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
width: 0,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
placement: 'bottom',
|
2026-04-25 22:41:40 +08:00
|
|
|
|
boundary: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
|
|
|
|
start: 0,
|
|
|
|
|
|
end: 0,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
selectedText: '',
|
|
|
|
|
|
}
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const content = ref('')
|
|
|
|
|
|
const variables = ref<KolVariableDefinition[]>([])
|
|
|
|
|
|
const previewOpen = ref(false)
|
|
|
|
|
|
const previewValues = ref<Record<string, any>>({})
|
|
|
|
|
|
const aiBusy = ref(false)
|
|
|
|
|
|
const selectionAiPanel = ref<PromptSelectionAiPanelState>(
|
|
|
|
|
|
createDefaultPromptSelectionAiPanelState(),
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectionAiInstruction = ref('')
|
|
|
|
|
|
const selectionAiPreview = ref('')
|
|
|
|
|
|
const selectionAiError = ref('')
|
|
|
|
|
|
const selectionAiStatus = ref<AiOptimizeStatus>('idle')
|
|
|
|
|
|
const cardConfig = ref<KolCardConfig>({})
|
2026-04-18 13:47:32 +08:00
|
|
|
|
const variableConfigRef = ref<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
focusVariable: (key: string, options?: { focusInput?: boolean }) => void
|
|
|
|
|
|
} | null>(null)
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const metadataForm = reactive({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
name: '',
|
|
|
|
|
|
platform_hint: '通用',
|
2026-04-18 13:47:32 +08:00
|
|
|
|
allow_web_search: false,
|
|
|
|
|
|
allow_user_knowledge: true,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-05-09 09:40:17 +08:00
|
|
|
|
const metaCollapsed = ref(false)
|
|
|
|
|
|
function toggleMetaCollapsed() {
|
|
|
|
|
|
metaCollapsed.value = !metaCollapsed.value
|
|
|
|
|
|
}
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const { data: promptDetail } = useQuery({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
queryKey: computed(() => ['kol', 'prompt', props.promptId]),
|
2026-04-17 14:08:34 +08:00
|
|
|
|
queryFn: () => kolManageApi.getPrompt(props.promptId),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const platformOptions = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const options = buildKolPlatformOptions(t('kol.manage.platformHintDefault'))
|
|
|
|
|
|
const currentValue = metadataForm.platform_hint.trim()
|
2026-04-18 00:47:57 +08:00
|
|
|
|
if (currentValue && !options.some((option) => option.value === currentValue)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return [{ label: currentValue, value: currentValue }, ...options]
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return options
|
|
|
|
|
|
})
|
|
|
|
|
|
const selectionAiStreaming = computed(() => selectionAiStatus.value === 'generating')
|
|
|
|
|
|
const selectionAiHasPreview = computed(() => selectionAiPreview.value.trim().length > 0)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
const selectionAiUiText = computed(() => ({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
title: t('article.editor.aiOptimize.title'),
|
|
|
|
|
|
promptPlaceholder: t('kol.manage.editor.aiOptimizePromptPlaceholder'),
|
|
|
|
|
|
generating: t('article.editor.aiOptimize.generating'),
|
|
|
|
|
|
failed: t('article.editor.aiOptimize.messages.failed'),
|
|
|
|
|
|
disclaimer: t('kol.manage.editor.aiOptimizeDisclaimer'),
|
|
|
|
|
|
regenerate: t('article.editor.aiOptimize.regenerate'),
|
|
|
|
|
|
close: t('article.editor.aiOptimize.close'),
|
|
|
|
|
|
replace: t('kol.manage.editor.aiOptimizeReplace'),
|
|
|
|
|
|
}))
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => promptDetail.value,
|
|
|
|
|
|
(detail) => {
|
|
|
|
|
|
if (detail) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const nextCardConfig = detail.latest_card_config_json ?? {}
|
|
|
|
|
|
const parsedCardConfig = parseKolCardConfig(nextCardConfig)
|
|
|
|
|
|
|
|
|
|
|
|
metadataForm.name = detail.name
|
|
|
|
|
|
metadataForm.platform_hint = detail.platform_hint || '通用'
|
|
|
|
|
|
metadataForm.allow_web_search = parsedCardConfig.allow_web_search
|
|
|
|
|
|
metadataForm.allow_user_knowledge = parsedCardConfig.allow_user_knowledge
|
|
|
|
|
|
content.value = detail.latest_prompt_content ?? ''
|
2026-04-18 15:01:40 +08:00
|
|
|
|
variables.value = (detail.latest_schema_json?.variables ?? []).map((variable) =>
|
|
|
|
|
|
normalizeKolVariableDefinition(variable),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
|
|
|
|
|
cardConfig.value = { ...nextCardConfig }
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
)
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const publishMutation = useMutation({
|
|
|
|
|
|
mutationFn: (payload: ReturnType<typeof buildPromptPayload>) =>
|
|
|
|
|
|
kolManageApi.publishPrompt(props.promptId, payload),
|
2026-04-17 14:08:34 +08:00
|
|
|
|
onSuccess: () => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.success(t('kol.manage.editor.publish') + '成功')
|
|
|
|
|
|
invalidatePromptQueries()
|
2026-04-17 14:08:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.error(formatError(error))
|
2026-04-17 14:08:34 +08:00
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
const saveMutation = useMutation({
|
|
|
|
|
|
mutationFn: (payload: ReturnType<typeof buildPromptPayload>) =>
|
|
|
|
|
|
kolManageApi.savePrompt(props.promptId, payload),
|
2026-04-17 14:08:34 +08:00
|
|
|
|
onSuccess: () => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.success(t('common.save') + '成功')
|
|
|
|
|
|
invalidatePromptQueries()
|
2026-04-17 14:08:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.error(formatError(error))
|
2026-04-17 14:08:34 +08:00
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
let aiAssistAbortController: AbortController | null = null
|
|
|
|
|
|
let selectionAiAbortController: AbortController | null = null
|
2026-04-17 14:08:34 +08:00
|
|
|
|
|
2026-04-18 16:17:11 +08:00
|
|
|
|
function applyAssistResult(event: KolAssistStreamEvent) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const nextContent = (event.content ?? '').trim()
|
2026-04-18 16:17:11 +08:00
|
|
|
|
if (!nextContent) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
content.value = nextContent
|
2026-04-18 16:17:11 +08:00
|
|
|
|
variables.value = syncKolVariablesWithContent(
|
|
|
|
|
|
nextContent,
|
|
|
|
|
|
event.schema?.variables ?? variables.value,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
function handleAssistStreamEvent(event: KolAssistStreamEvent): 'completed' | 'error' | null {
|
|
|
|
|
|
if (event.type === 'start') {
|
|
|
|
|
|
aiBusy.value = true
|
|
|
|
|
|
return null
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'delta') {
|
|
|
|
|
|
aiBusy.value = true
|
|
|
|
|
|
if (typeof event.content === 'string') {
|
|
|
|
|
|
content.value = event.content
|
|
|
|
|
|
} else if (typeof event.delta === 'string' && event.delta) {
|
|
|
|
|
|
content.value += event.delta
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'completed') {
|
|
|
|
|
|
aiBusy.value = false
|
|
|
|
|
|
applyAssistResult(event)
|
|
|
|
|
|
message.success('AI 处理完成')
|
|
|
|
|
|
return 'completed'
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
2026-04-18 16:17:11 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'error') {
|
|
|
|
|
|
aiBusy.value = false
|
|
|
|
|
|
if (typeof event.content === 'string' && event.content.trim()) {
|
|
|
|
|
|
content.value = event.content
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.error(event.error || 'AI 处理失败')
|
|
|
|
|
|
return 'error'
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 16:17:11 +08:00
|
|
|
|
function stopAiAssistStream() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
aiAssistAbortController?.abort()
|
|
|
|
|
|
aiAssistAbortController = null
|
|
|
|
|
|
aiBusy.value = false
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAiAssistStream(payload: KolAssistRequest) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
stopAiAssistStream()
|
2026-04-18 16:17:11 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
aiAssistAbortController = controller
|
|
|
|
|
|
aiBusy.value = true
|
|
|
|
|
|
let terminalState: 'completed' | 'error' | null = null
|
2026-04-18 16:17:11 +08:00
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
try {
|
2026-04-18 16:17:11 +08:00
|
|
|
|
await kolManageApi.streamAssist(
|
|
|
|
|
|
payload,
|
|
|
|
|
|
{
|
|
|
|
|
|
onEvent(event) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
terminalState = handleAssistStreamEvent(event)
|
2026-04-18 16:17:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
onClose() {
|
|
|
|
|
|
if (aiAssistAbortController === controller && terminalState === null) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
aiBusy.value = false
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
controller.signal,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-18 16:17:11 +08:00
|
|
|
|
|
|
|
|
|
|
if (!controller.signal.aborted && terminalState === null) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
aiBusy.value = false
|
|
|
|
|
|
message.error('AI 处理失败')
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
2026-04-17 14:08:34 +08:00
|
|
|
|
} catch (error) {
|
2026-04-18 16:17:11 +08:00
|
|
|
|
if (controller.signal.aborted) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
aiBusy.value = false
|
|
|
|
|
|
message.error(formatError(error))
|
2026-04-18 16:17:11 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
if (aiAssistAbortController === controller) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
aiAssistAbortController = null
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 16:17:11 +08:00
|
|
|
|
async function handleAiGenerate(description: string) {
|
|
|
|
|
|
await runAiAssistStream({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
mode: 'generate',
|
2026-04-18 16:17:11 +08:00
|
|
|
|
description,
|
|
|
|
|
|
prompt_id: props.promptId,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 22:41:40 +08:00
|
|
|
|
function resetSelectionAiOutput() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
selectionAiPreview.value = ''
|
|
|
|
|
|
selectionAiError.value = ''
|
|
|
|
|
|
selectionAiStatus.value = 'idle'
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopSelectionAiStream() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
selectionAiAbortController?.abort()
|
|
|
|
|
|
selectionAiAbortController = null
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (selectionAiStatus.value === 'generating') {
|
|
|
|
|
|
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'idle'
|
|
|
|
|
|
selectionAiError.value = ''
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeSelectionAiPanel() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
stopSelectionAiStream()
|
|
|
|
|
|
selectionAiPanel.value = createDefaultPromptSelectionAiPanelState()
|
|
|
|
|
|
selectionAiInstruction.value = ''
|
|
|
|
|
|
resetSelectionAiOutput()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleOpenSelectionAiPanel(snapshot: PromptSelectionAiSnapshot) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
stopAiAssistStream()
|
|
|
|
|
|
stopSelectionAiStream()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
selectionAiPanel.value = {
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
...snapshot,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
selectionAiInstruction.value = ''
|
|
|
|
|
|
resetSelectionAiOutput()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleSelectionAiStreamEvent(event: KolAssistStreamEvent) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'start') {
|
|
|
|
|
|
selectionAiStatus.value = 'generating'
|
|
|
|
|
|
selectionAiError.value = ''
|
|
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'delta') {
|
|
|
|
|
|
selectionAiStatus.value = 'generating'
|
|
|
|
|
|
selectionAiError.value = ''
|
|
|
|
|
|
if (typeof event.content === 'string') {
|
|
|
|
|
|
selectionAiPreview.value = event.content
|
|
|
|
|
|
} else if (typeof event.delta === 'string') {
|
|
|
|
|
|
selectionAiPreview.value += event.delta
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'completed') {
|
|
|
|
|
|
selectionAiStatus.value = 'completed'
|
|
|
|
|
|
selectionAiError.value = ''
|
|
|
|
|
|
if (typeof event.content === 'string') {
|
|
|
|
|
|
selectionAiPreview.value = event.content
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (event.type === 'error') {
|
|
|
|
|
|
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
|
|
|
|
|
selectionAiError.value = event.error || t('article.editor.aiOptimize.messages.failed')
|
|
|
|
|
|
if (typeof event.content === 'string' && event.content.trim()) {
|
|
|
|
|
|
selectionAiPreview.value = event.content
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!selectionAiHasPreview.value) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.error(selectionAiError.value)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function generateSelectionAiPreview() {
|
|
|
|
|
|
if (!selectionAiPanel.value.open) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const instruction = selectionAiInstruction.value.trim()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (!instruction) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('article.editor.aiOptimize.messages.promptRequired'))
|
|
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
stopSelectionAiStream()
|
|
|
|
|
|
selectionAiPreview.value = ''
|
|
|
|
|
|
selectionAiError.value = ''
|
|
|
|
|
|
selectionAiStatus.value = 'generating'
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const syncedVariables = syncVariablesFromContent()
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
selectionAiAbortController = controller
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await kolManageApi.streamAssist(
|
|
|
|
|
|
{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
mode: 'optimize',
|
2026-04-25 22:41:40 +08:00
|
|
|
|
description: instruction,
|
|
|
|
|
|
current_content: selectionAiPanel.value.selectedText,
|
|
|
|
|
|
schema: { variables: syncedVariables },
|
|
|
|
|
|
prompt_id: props.promptId,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
onEvent: handleSelectionAiStreamEvent,
|
|
|
|
|
|
},
|
|
|
|
|
|
controller.signal,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (!controller.signal.aborted && selectionAiStatus.value === 'generating') {
|
|
|
|
|
|
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (controller.signal.aborted) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
|
|
|
|
|
selectionAiError.value = formatError(error)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (!selectionAiHasPreview.value) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.error(selectionAiError.value)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (selectionAiAbortController === controller) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
selectionAiAbortController = null
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySelectionAiReplacement() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const replacement = selectionAiPreview.value.trim()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (!selectionAiPanel.value.open || !replacement) {
|
|
|
|
|
|
if (!replacement) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('article.editor.aiOptimize.messages.empty'))
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const target = { ...selectionAiPanel.value }
|
|
|
|
|
|
const currentSelectedText = content.value.slice(target.start, target.end)
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (currentSelectedText !== target.selectedText) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('article.editor.aiOptimize.messages.selectionChanged'))
|
|
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextContent =
|
2026-05-01 20:39:09 +08:00
|
|
|
|
content.value.slice(0, target.start) + replacement + content.value.slice(target.end)
|
|
|
|
|
|
content.value = nextContent
|
|
|
|
|
|
variables.value = syncKolVariablesWithContent(nextContent, variables.value)
|
|
|
|
|
|
closeSelectionAiPanel()
|
2026-04-18 16:17:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
function buildPromptPayload(nextVariables: KolVariableDefinition[] = variables.value) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: metadataForm.name.trim(),
|
|
|
|
|
|
platform_hint: metadataForm.platform_hint,
|
|
|
|
|
|
sort_order: promptDetail.value?.sort_order,
|
|
|
|
|
|
content: content.value,
|
|
|
|
|
|
schema: { variables: nextVariables },
|
2026-04-18 13:47:32 +08:00
|
|
|
|
card_config: mergeKolCardConfig(cardConfig.value, {
|
|
|
|
|
|
allow_web_search: metadataForm.allow_web_search,
|
|
|
|
|
|
allow_user_knowledge: metadataForm.allow_user_knowledge,
|
|
|
|
|
|
}),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncVariablesFromContent(): KolVariableDefinition[] {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const syncedVariables = syncKolVariablesWithContent(content.value, variables.value)
|
|
|
|
|
|
variables.value = syncedVariables
|
|
|
|
|
|
return syncedVariables
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPreviewValues(nextVariables: KolVariableDefinition[]): Record<string, any> {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const nextValues: Record<string, any> = {}
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
|
|
|
|
|
nextVariables.forEach((variable) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const fieldKey = variable.key?.trim() || variable.id
|
|
|
|
|
|
nextValues[fieldKey] = getKolVariableInitialValue(variable)
|
|
|
|
|
|
})
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return nextValues
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function invalidatePromptQueries() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
void queryClient.invalidateQueries({ queryKey: ['kol', 'prompt', props.promptId] })
|
|
|
|
|
|
void queryClient.invalidateQueries({ queryKey: ['kol', 'packages'] })
|
2026-04-18 00:47:57 +08:00
|
|
|
|
void queryClient.invalidateQueries({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
queryKey: ['kol', 'packages', promptDetail.value?.package_id, 'prompts'],
|
|
|
|
|
|
})
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateBeforeSubmit(): boolean {
|
|
|
|
|
|
if (!metadataForm.name.trim()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning(t('common.inputPlease') + t('common.title'))
|
|
|
|
|
|
return false
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!content.value.trim()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
message.warning('请输入提示词内容')
|
|
|
|
|
|
return false
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return true
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
function handleSave() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const syncedVariables = syncVariablesFromContent()
|
2026-04-18 00:47:57 +08:00
|
|
|
|
if (!validateBeforeSubmit()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
saveMutation.mutate(buildPromptPayload(syncedVariables))
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 22:41:40 +08:00
|
|
|
|
function handleSaveShortcut(event: KeyboardEvent) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') {
|
|
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
event.stopPropagation()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (event.repeat || saveMutation.isPending.value || publishMutation.isPending.value) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
handleSave()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
function handlePublish() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const syncedVariables = syncVariablesFromContent()
|
2026-04-18 00:47:57 +08:00
|
|
|
|
if (!validateBeforeSubmit()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
publishMutation.mutate(buildPromptPayload(syncedVariables))
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePreview() {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const syncedVariables = syncVariablesFromContent()
|
|
|
|
|
|
previewValues.value = buildPreviewValues(syncedVariables)
|
|
|
|
|
|
previewOpen.value = true
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
|
|
|
|
|
async function handleFocusVariable(key: string) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
syncVariablesFromContent()
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
variableConfigRef.value?.focusVariable(key, { focusInput: false })
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSelectPopupContainer(triggerNode: HTMLElement) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return triggerNode.parentElement ?? triggerNode
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusTagColor(status?: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (status === 'active') return 'green'
|
|
|
|
|
|
if (status === 'archived') return 'orange'
|
|
|
|
|
|
return 'default'
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusLabel(status?: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (status === 'active') return t('kol.manage.status.active')
|
|
|
|
|
|
if (status === 'archived') return t('kol.manage.status.archived')
|
|
|
|
|
|
return t('kol.manage.status.draft')
|
2026-04-18 00:47:57 +08:00
|
|
|
|
}
|
2026-04-18 16:17:11 +08:00
|
|
|
|
|
2026-04-25 22:41:40 +08:00
|
|
|
|
function handleWindowPointerDown(event: PointerEvent) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (!(selectionAiPanel.value.open && selectionAiStatus.value === 'idle')) {
|
|
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const target = event.target
|
2026-04-25 22:41:40 +08:00
|
|
|
|
if (
|
|
|
|
|
|
target instanceof Element &&
|
2026-05-01 20:39:09 +08:00
|
|
|
|
(target.closest('.editor-ai-assist') || target.closest('.prompt-selection-ai-action'))
|
2026-04-25 22:41:40 +08:00
|
|
|
|
) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
closeSelectionAiPanel()
|
2026-04-25 22:41:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
window.addEventListener('keydown', handleSaveShortcut)
|
|
|
|
|
|
window.addEventListener('pointerdown', handleWindowPointerDown)
|
|
|
|
|
|
})
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
2026-04-18 16:17:11 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
window.removeEventListener('keydown', handleSaveShortcut)
|
|
|
|
|
|
window.removeEventListener('pointerdown', handleWindowPointerDown)
|
|
|
|
|
|
stopAiAssistStream()
|
|
|
|
|
|
stopSelectionAiStream()
|
|
|
|
|
|
})
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="kol-prompt-editor">
|
2026-04-18 00:47:57 +08:00
|
|
|
|
<div class="editor-topbar">
|
|
|
|
|
|
<button class="back-btn" type="button" @click="emit('back')">
|
|
|
|
|
|
<LeftOutlined />
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<span>{{ t('common.back') }}</span>
|
2026-04-18 00:47:57 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
<div class="editor-header">
|
|
|
|
|
|
<div class="header-title">
|
2026-04-18 00:47:57 +08:00
|
|
|
|
<div class="title-row">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<span class="prompt-name">
|
|
|
|
|
|
{{ metadataForm.name || promptDetail?.name || t('common.loading') }}
|
|
|
|
|
|
</span>
|
2026-04-18 00:47:57 +08:00
|
|
|
|
<a-tag v-if="promptDetail" :color="statusTagColor(promptDetail.status)">
|
|
|
|
|
|
{{ statusLabel(promptDetail.status) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<span class="prompt-subtitle">{{ t('kol.manage.editor.basicInfo') }}</span>
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
|
<a-button @click="handlePreview">
|
|
|
|
|
|
<template #icon><EyeOutlined /></template>
|
|
|
|
|
|
{{ t('kol.manage.editor.preview') }}
|
|
|
|
|
|
</a-button>
|
2026-04-25 22:41:40 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
:loading="saveMutation.isPending.value"
|
|
|
|
|
|
title="Ctrl/Cmd + S"
|
|
|
|
|
|
aria-keyshortcuts="Control+S Meta+S"
|
|
|
|
|
|
@click="handleSave"
|
|
|
|
|
|
>
|
2026-04-17 14:08:34 +08:00
|
|
|
|
<template #icon><SaveOutlined /></template>
|
2026-04-18 00:47:57 +08:00
|
|
|
|
{{ t('common.save') }}
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="primary" :loading="publishMutation.isPending.value" @click="handlePublish">
|
|
|
|
|
|
<template #icon><SendOutlined /></template>
|
|
|
|
|
|
{{ t('kol.manage.editor.publish') }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-09 09:40:17 +08:00
|
|
|
|
<div class="editor-meta-card" :class="{ 'editor-meta-card--collapsed': metaCollapsed }">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="editor-section-title editor-section-title--toggle"
|
|
|
|
|
|
:aria-expanded="!metaCollapsed"
|
|
|
|
|
|
@click="toggleMetaCollapsed"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DownOutlined
|
|
|
|
|
|
class="editor-section-title__chevron"
|
|
|
|
|
|
:class="{ 'editor-section-title__chevron--collapsed': metaCollapsed }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{{ t('kol.manage.editor.basicInfo') }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<a-form v-show="!metaCollapsed" layout="vertical" class="editor-meta-form">
|
2026-04-18 00:47:57 +08:00
|
|
|
|
<a-form-item :label="t('common.title')" required>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="metadataForm.name"
|
|
|
|
|
|
:placeholder="t('common.inputPlease') + t('common.title')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item :label="t('kol.manage.platformHint')">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="metadataForm.platform_hint"
|
|
|
|
|
|
:options="platformOptions"
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
:get-popup-container="getSelectPopupContainer"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
2026-04-18 13:47:32 +08:00
|
|
|
|
|
|
|
|
|
|
<a-form-item label="生成文章时允许联网搜索">
|
|
|
|
|
|
<div class="editor-switch-field">
|
|
|
|
|
|
<a-switch v-model:checked="metadataForm.allow_web_search" />
|
|
|
|
|
|
<span class="editor-switch-hint">默认关闭,关闭后用户在生成页无法启用联网搜索。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item label="生成文章时允许用户使用自己的知识库">
|
|
|
|
|
|
<div class="editor-switch-field">
|
|
|
|
|
|
<a-switch v-model:checked="metadataForm.allow_user_knowledge" />
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<span class="editor-switch-hint">
|
|
|
|
|
|
默认开启,关闭后用户在生成页无法选择自己的知识库。
|
|
|
|
|
|
</span>
|
2026-04-18 13:47:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
2026-04-18 00:47:57 +08:00
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
<div class="editor-body">
|
|
|
|
|
|
<KolVariablePanel />
|
|
|
|
|
|
<KolPromptEditArea
|
|
|
|
|
|
v-model:content="content"
|
|
|
|
|
|
v-model:variables="variables"
|
2026-04-18 16:17:11 +08:00
|
|
|
|
:ai-busy="aiBusy"
|
2026-04-25 22:41:40 +08:00
|
|
|
|
:selection-ai-open="selectionAiPanel.open"
|
2026-04-17 14:08:34 +08:00
|
|
|
|
@ai-generate="handleAiGenerate"
|
2026-04-25 22:41:40 +08:00
|
|
|
|
@ai-optimize-selection="handleOpenSelectionAiPanel"
|
2026-04-18 00:47:57 +08:00
|
|
|
|
@focus-variable="handleFocusVariable"
|
|
|
|
|
|
/>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<KolVariableConfig ref="variableConfigRef" v-model:variables="variables" />
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="previewOpen"
|
|
|
|
|
|
:title="t('kol.manage.editor.preview')"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
width="600px"
|
|
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<KolDynamicForm v-model:modelValue="previewValues" :variables="variables" />
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</a-modal>
|
2026-04-25 22:41:40 +08:00
|
|
|
|
|
|
|
|
|
|
<EditorAiAssistPanel
|
|
|
|
|
|
v-if="selectionAiPanel.open"
|
|
|
|
|
|
v-model:prompt="selectionAiInstruction"
|
|
|
|
|
|
:x="selectionAiPanel.x"
|
|
|
|
|
|
:y="selectionAiPanel.y"
|
|
|
|
|
|
:width="selectionAiPanel.width"
|
|
|
|
|
|
:placement="selectionAiPanel.placement"
|
|
|
|
|
|
:boundary="selectionAiPanel.boundary"
|
|
|
|
|
|
:status="selectionAiStatus"
|
|
|
|
|
|
:streaming="selectionAiStreaming"
|
|
|
|
|
|
:has-preview="selectionAiHasPreview"
|
|
|
|
|
|
:error="selectionAiError"
|
|
|
|
|
|
:title="selectionAiUiText.title"
|
|
|
|
|
|
:prompt-placeholder="selectionAiUiText.promptPlaceholder"
|
|
|
|
|
|
:continue-placeholder="selectionAiUiText.promptPlaceholder"
|
|
|
|
|
|
:loading-label="selectionAiUiText.generating"
|
|
|
|
|
|
:error-label="selectionAiUiText.failed"
|
|
|
|
|
|
:disclaimer="selectionAiUiText.disclaimer"
|
|
|
|
|
|
:regenerate-label="selectionAiUiText.regenerate"
|
|
|
|
|
|
:discard-label="selectionAiUiText.close"
|
|
|
|
|
|
:apply-label="selectionAiUiText.replace"
|
|
|
|
|
|
@submit="generateSelectionAiPreview"
|
|
|
|
|
|
@close="closeSelectionAiPanel"
|
|
|
|
|
|
@reset="resetSelectionAiOutput"
|
|
|
|
|
|
@apply="applySelectionAiReplacement"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #preview>
|
|
|
|
|
|
<pre class="kol-prompt-editor__ai-preview">{{ selectionAiPreview }}</pre>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</EditorAiAssistPanel>
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.kol-prompt-editor {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
.editor-topbar {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn :deep(.anticon) {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.82);
|
|
|
|
|
|
border: 1px solid rgba(144, 157, 181, 0.24);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
.editor-header {
|
2026-04-18 00:47:57 +08:00
|
|
|
|
margin: 0 16px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 16px;
|
2026-04-17 14:08:34 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2026-04-18 00:47:57 +08:00
|
|
|
|
gap: 16px;
|
2026-04-17 14:08:34 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
.header-title {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 14:08:34 +08:00
|
|
|
|
.prompt-name {
|
2026-04-18 00:47:57 +08:00
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-subtitle {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #667085;
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
2026-04-18 00:47:57 +08:00
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-meta-card {
|
|
|
|
|
|
margin: 12px 16px 0;
|
|
|
|
|
|
padding: 16px 20px 4px;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 09:40:17 +08:00
|
|
|
|
.editor-meta-card.editor-meta-card--collapsed {
|
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
.editor-section-title {
|
|
|
|
|
|
margin: 0 0 16px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 09:40:17 +08:00
|
|
|
|
.editor-section-title--toggle {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-meta-card--collapsed .editor-section-title--toggle {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-section-title--toggle:hover {
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-section-title__chevron {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-section-title__chevron--collapsed {
|
|
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
.editor-meta-form {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(240px, 1.4fr) minmax(200px, 1fr);
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
align-items: start;
|
2026-04-17 14:08:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
overflow: hidden;
|
2026-04-18 00:47:57 +08:00
|
|
|
|
margin: 12px 16px 16px;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 16px;
|
2026-04-17 14:08:34 +08:00
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
}
|
2026-04-18 00:47:57 +08:00
|
|
|
|
|
2026-04-18 13:47:32 +08:00
|
|
|
|
.editor-switch-field {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-switch-hint {
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 22:41:40 +08:00
|
|
|
|
.kol-prompt-editor__ai-preview {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 00:47:57 +08:00
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
|
.editor-meta-form {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.editor-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-meta-form {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-body {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-17 14:08:34 +08:00
|
|
|
|
</style>
|