fix: Enhance Kol Variable Rendering and Management
- Updated the regex for placeholder matching to support more flexible key formats. - Refactored variable storage to allow both ID and key lookups in rendering. - Improved schema validation to check for duplicate keys and enforce non-empty keys. - Added new utility functions for variable value lookup and display name generation. - Enhanced tests to cover new key-based variable scenarios. - Refactored KolPrompt repository to simplify prompt storage and management, including the removal of obsolete revision handling. - Introduced new API endpoints for saving, activating, and archiving prompts. - Added new utility functions for handling Kol placeholders and platform options in the admin web. - Implemented database migrations to simplify prompt storage structure and ensure data integrity.
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { computed, h, 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";
|
||||
|
||||
import {
|
||||
buildKolPlaceholderToken,
|
||||
createKolVariableDefinition,
|
||||
findKolPlaceholderKeyAtPosition,
|
||||
} from "@/lib/kol-placeholders";
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
variables: KolVariableDefinition[];
|
||||
@@ -17,17 +22,46 @@ const emit = defineEmits<{
|
||||
(e: "update:variables", value: KolVariableDefinition[]): void;
|
||||
(e: "ai-generate", description: string): void;
|
||||
(e: "ai-optimize"): void;
|
||||
(e: "focus-variable", key: string): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const aiDescription = ref("");
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const highlightRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const editorPlaceholder = "在这里输入提示词内容,可以使用 {{变量名}} 引用左侧定义的变量";
|
||||
const highlightTokenRE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
||||
|
||||
const highlightedContent = computed(() => {
|
||||
const text = props.content ?? "";
|
||||
if (!text) {
|
||||
return `<span class="editor-placeholder">${escapeHtml(editorPlaceholder)}</span>`;
|
||||
}
|
||||
|
||||
let html = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of text.matchAll(highlightTokenRE)) {
|
||||
const matchIndex = match.index ?? 0;
|
||||
const token = match[0] ?? "";
|
||||
|
||||
html += escapeHtml(text.slice(lastIndex, matchIndex));
|
||||
html += `<span class="token-highlight">${escapeHtml(token)}</span>`;
|
||||
lastIndex = matchIndex + token.length;
|
||||
}
|
||||
|
||||
html += escapeHtml(text.slice(lastIndex));
|
||||
return html;
|
||||
});
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer?.getData("application/kol-variable-type") as KolVariableType;
|
||||
if (!type) return;
|
||||
|
||||
const labelValue = ref("");
|
||||
|
||||
Modal.confirm({
|
||||
title: t("kol.manage.variable.label"),
|
||||
content: () => {
|
||||
@@ -35,34 +69,29 @@ function onDrop(event: DragEvent) {
|
||||
h("input", {
|
||||
class: "ant-input",
|
||||
placeholder: t("common.inputPlease"),
|
||||
value: labelValue.value,
|
||||
onInput: (e: Event) => {
|
||||
(Modal.confirm as any).labelValue = (e.target as HTMLInputElement).value;
|
||||
labelValue.value = (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,
|
||||
};
|
||||
const key = labelValue.value.trim() || t(`kol.manage.variable.${type}`);
|
||||
const existingVariable = props.variables.find((variable) => variable.key === key);
|
||||
|
||||
emit("update:variables", [...props.variables, newVar]);
|
||||
insertToken(`{{${key}}}`);
|
||||
if (!existingVariable) {
|
||||
const newVar: KolVariableDefinition = createKolVariableDefinition(key, {
|
||||
type,
|
||||
});
|
||||
emit("update:variables", [...props.variables, newVar]);
|
||||
}
|
||||
|
||||
insertToken(buildKolPlaceholderToken(key));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to use 'h' since we are in script setup
|
||||
import { h } from "vue";
|
||||
|
||||
function insertToken(token: string) {
|
||||
if (!textareaRef.value) return;
|
||||
|
||||
@@ -78,6 +107,7 @@ function insertToken(token: string) {
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
el.setSelectionRange(start + token.length, start + token.length);
|
||||
syncScroll();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -85,6 +115,45 @@ function handleAiGenerate() {
|
||||
if (!aiDescription.value.trim()) return;
|
||||
emit("ai-generate", aiDescription.value);
|
||||
}
|
||||
|
||||
function handleContentInput(event: Event) {
|
||||
emit("update:content", (event.target as HTMLTextAreaElement).value);
|
||||
syncScroll();
|
||||
}
|
||||
|
||||
function handleCaretInteraction() {
|
||||
if (!textareaRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.value;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = findKolPlaceholderKeyAtPosition(props.content, textarea.selectionStart ?? -1);
|
||||
if (key) {
|
||||
emit("focus-variable", key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncScroll() {
|
||||
if (!textareaRef.value || !highlightRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
highlightRef.value.scrollTop = textareaRef.value.scrollTop;
|
||||
highlightRef.value.scrollLeft = textareaRef.value.scrollLeft;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -126,12 +195,21 @@ function handleAiGenerate() {
|
||||
<span>AI 正在思考中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="highlightRef"
|
||||
class="prompt-highlight"
|
||||
aria-hidden="true"
|
||||
v-html="highlightedContent"
|
||||
></div>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="prompt-textarea"
|
||||
:value="content"
|
||||
@input="e => emit('update:content', (e.target as HTMLTextAreaElement).value)"
|
||||
placeholder="在这里输入提示词内容,可以使用 {{变量名}} 引用左侧定义的变量"
|
||||
:placeholder="editorPlaceholder"
|
||||
@input="handleContentInput"
|
||||
@mouseup="handleCaretInteraction"
|
||||
@keyup="handleCaretInteraction"
|
||||
@scroll="syncScroll"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,21 +244,50 @@ function handleAiGenerate() {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prompt-highlight {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #262626;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prompt-textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
border: none;
|
||||
resize: none;
|
||||
padding: 24px;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
color: #262626;
|
||||
color: transparent;
|
||||
caret-color: #262626;
|
||||
}
|
||||
|
||||
.prompt-textarea::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.prompt-highlight :deep(.token-highlight) {
|
||||
color: #eb2f96;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prompt-highlight :deep(.editor-placeholder) {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user