feat(kol): three-pane prompt editor + KolManageView UI
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ExperimentOutlined, ThunderboltOutlined, LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import type { KolVariableDefinition, KolVariableType } from "@geo/shared-types";
|
||||
import { newVariableId } from "@/lib/kol-variable-id";
|
||||
import { Modal } from "ant-design-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
variables: KolVariableDefinition[];
|
||||
aiTaskId: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:content", value: string): void;
|
||||
(e: "update:variables", value: KolVariableDefinition[]): void;
|
||||
(e: "ai-generate", description: string): void;
|
||||
(e: "ai-optimize"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const aiDescription = ref("");
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer?.getData("application/kol-variable-type") as KolVariableType;
|
||||
if (!type) return;
|
||||
|
||||
Modal.confirm({
|
||||
title: t("kol.manage.variable.label"),
|
||||
content: () => {
|
||||
return h("div", { style: { marginTop: "16px" } }, [
|
||||
h("input", {
|
||||
class: "ant-input",
|
||||
placeholder: t("common.inputPlease"),
|
||||
onInput: (e: Event) => {
|
||||
(Modal.confirm as any).labelValue = (e.target as HTMLInputElement).value;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
onOk: () => {
|
||||
const label = (Modal.confirm as any).labelValue || t(`kol.manage.variable.${type}`);
|
||||
const id = newVariableId();
|
||||
const key = label;
|
||||
|
||||
const newVar: KolVariableDefinition = {
|
||||
id,
|
||||
key,
|
||||
label,
|
||||
type,
|
||||
required: true,
|
||||
};
|
||||
|
||||
emit("update:variables", [...props.variables, newVar]);
|
||||
insertToken(`{{${key}}}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to use 'h' since we are in script setup
|
||||
import { h } from "vue";
|
||||
|
||||
function insertToken(token: string) {
|
||||
if (!textareaRef.value) return;
|
||||
|
||||
const el = textareaRef.value;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
const text = props.content;
|
||||
|
||||
const newContent = text.substring(0, start) + token + text.substring(end);
|
||||
emit("update:content", newContent);
|
||||
|
||||
// Set cursor position after update
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
el.setSelectionRange(start + token.length, start + token.length);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function handleAiGenerate() {
|
||||
if (!aiDescription.value.trim()) return;
|
||||
emit("ai-generate", aiDescription.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kol-prompt-edit-area" @dragover.prevent @drop="onDrop">
|
||||
<div class="ai-assist-bar">
|
||||
<a-input
|
||||
v-model:value="aiDescription"
|
||||
class="ai-input"
|
||||
:placeholder="t('kol.manage.editor.aiPlaceholder')"
|
||||
:disabled="!!aiTaskId"
|
||||
@press-enter="handleAiGenerate"
|
||||
>
|
||||
<template #prefix>
|
||||
<ExperimentOutlined style="color: #722ed1" />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ai-btn"
|
||||
:loading="!!aiTaskId"
|
||||
@click="handleAiGenerate"
|
||||
>
|
||||
<template #icon><ThunderboltOutlined /></template>
|
||||
{{ t('kol.manage.editor.aiGenerate') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
class="ai-btn"
|
||||
:disabled="!content || !!aiTaskId"
|
||||
@click="emit('ai-optimize')"
|
||||
>
|
||||
{{ t('kol.manage.editor.aiOptimize') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div v-if="aiTaskId" class="ai-loading-overlay">
|
||||
<div class="loading-content">
|
||||
<LoadingOutlined class="loading-icon" />
|
||||
<span>AI 正在思考中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="prompt-textarea"
|
||||
:value="content"
|
||||
@input="e => emit('update:content', (e.target as HTMLTextAreaElement).value)"
|
||||
placeholder="在这里输入提示词内容,可以使用 {{变量名}} 引用左侧定义的变量"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kol-prompt-edit-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-assist-bar {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: #f9f0ff;
|
||||
}
|
||||
|
||||
.ai-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.prompt-textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
resize: none;
|
||||
padding: 24px;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.prompt-textarea::placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.ai-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user